diff --git a/.cookiecutter.json b/.cookiecutter.json index 020ae61d4..2553384ac 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -14,7 +14,7 @@ "max_nautobot_version": "2.9999", "camel_name": "NautobotSSOTApp", "project_short_description": "Nautobot Single Source of Truth", - "model_class_name": "None", + "model_class_name": "Sync", "open_source_license": "Apache-2.0", "docs_base_url": "https://docs.nautobot.com", "docs_app_url": "https://docs.nautobot.com/projects/ssot/en/latest", diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd890a910..6359b6135 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,7 +3,12 @@ /nautobot_ssot/integrations/aci/ @chadell @nautobot/plugin-ssot /nautobot_ssot/integrations/aristacv/ @qduk @jdrew82 @nautobot/plugin-ssot /nautobot_ssot/integrations/bootstrap/ @bile0026 @nautobot/plugin-ssot +/nautobot_ssot/integrations/citrix_adm/ @jdrew82 @nautobot/plugin-ssot /nautobot_ssot/integrations/device42/ @jdrew82 @nautobot/plugin-ssot +/nautobot_ssot/integrations/dna_center/ @jdrew82 @nautobot/plugin-ssot /nautobot_ssot/integrations/infoblox/ @qduk @jdrew82 @nautobot/plugin-ssot /nautobot_ssot/integrations/ipfabric/ @alhogan @nautobot/plugin-ssot +/nautobot_ssot/integrations/itential/ @jtdub @nautobot/plugin-ssot +/nautobot_ssot/integrations/meraki/ @jdrew82 @nautobot/plugin-ssot /nautobot_ssot/integrations/servicenow/ @glennmatthews @qduk @nautobot/plugin-ssot +/nautobot_ssot/integrations/slurpit/ @lpconsulting321 @pietos @nautobot/plugin-ssot diff --git a/README.md b/README.md index bdca4711d..9dd05c053 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ The Nautobot SSoT app builds atop the [DiffSync](https://github.com/networktocod This Nautobot application framework includes the following integrations: - Cisco ACI +- Bootstrap +- Citrix ADM - Arista CloudVision - Device42 - Cisco DNA Center @@ -35,6 +37,7 @@ This Nautobot application framework includes the following integrations: - Itential - Cisco Meraki - ServiceNow +- Slurpit Read more about integrations [here](https://docs.nautobot.com/projects/ssot/en/latest/user/integrations). To enable and configure integrations follow the instructions from [the install guide](https://docs.nautobot.com/projects/ssot/en/latest/admin/install/#integrations-configuration). @@ -82,6 +85,8 @@ Full documentation for this app can be found over on the [Nautobot Docs](https:/ The SSoT framework includes a number of integrations with external Systems of Record: * Cisco ACI +* Bootstrap +* Citrix ADM * Arista CloudVision * Device42 * Cisco DNA Center @@ -89,6 +94,7 @@ The SSoT framework includes a number of integrations with external Systems of Re * Itential * Cisco Meraki * ServiceNow +* Slurpit > Note that the Arista CloudVision integration is currently incompatible with the [Arista Labs](https://labs.arista.com/) environment due to a TLS issue. It has been confirmed to work in on-prem environments previously. @@ -114,6 +120,12 @@ This project includes code originally written in separate Nautobot apps, which h [@dnewood](https://github.com/dnewood), [@progala](https://github.com/progala), [@ubajze](https://github.com/ubajze) +- [nautobot-plugin-ssot-bootstrap](https://github.com/nautobot/nautobot-plugin-ssot-bootstrap): + Thanks + [@bile0026](https://github.com/bile0026) +- [nautobot-plugin-ssot-citrix-adm](https://github.com/nautobot/nautobot-plugin-ssot-citrix-adm): + Thanks + [@jdrew82](https://github.com/jdrew82) - [nautobot-plugin-ssot-arista-cloudvision](https://github.com/nautobot/nautobot-plugin-ssot-arista-cloudvision): Thanks [@burnyd](https://github.com/burnyd), @@ -151,6 +163,12 @@ This project includes code originally written in separate Nautobot apps, which h [@pke11y](https://github.com/pke11y), [@ubajze](https://github.com/ubajze) [@whitej6](https://github.com/whitej6), +- [nautobot-plugin-ssot-device42](https://github.com/nautobot/nautobot-plugin-ssot-itential): + Thanks + [@jtdub](https://github.com/jtdub) +- [nautobot-plugin-ssot-meraki](https://github.com/nautobot/nautobot-plugin-ssot-meraki): + Thanks + [@jdrew82](https://github.com/jdrew82) - [nautobot-plugin-ssot-servicenow](https://github.com/nautobot/nautobot-plugin-ssot-servicenow): Thanks [@chadell](https://github.com/chadell), diff --git a/development/creds.example.env b/development/creds.example.env index 45757d455..49ec37daa 100644 --- a/development/creds.example.env +++ b/development/creds.example.env @@ -31,6 +31,9 @@ NAUTOBOT_ARISTACV_CVP_TOKEN="changeme" NAUTOBOT_SSOT_DEVICE42_PASSWORD="changeme" +NAUTOBOT_SSOT_CITRIX_ADM_USERNAME="admin" +NAUTOBOT_SSOT_CITRIX_ADM_PASSWORD="changeme" + NAUTOBOT_SSOT_INFOBLOX_PASSWORD="changeme" # ACI Credentials. Append friendly name to the end to identify each APIC. @@ -49,9 +52,5 @@ SERVICENOW_PASSWORD="changeme" IPFABRIC_API_TOKEN=secrettoken -NAUTOBOT_SSOT_ENABLE_BOOTSTRAP="False" -NAUTOBOT_BOOTSTRAP_SSOT_ENVIRONMENT_BRANCH=develop -NAUTOBOT_BOOTSTRAP_SSOT_LOAD_SOURCE=file # or git - MERAKI_ORG_ID='123456' MERAKI_TOKEN='vtx01710aa0fn452740055y1hs60ns8c107ho168' diff --git a/development/development.env b/development/development.env index 04f1bbfb3..5f1b7ee2e 100644 --- a/development/development.env +++ b/development/development.env @@ -69,6 +69,10 @@ NAUTOBOT_ARISTACV_IMPORT_ACTIVE="False" NAUTOBOT_ARISTACV_IMPORT_TAG="False" NAUTOBOT_ARISTACV_VERIFY=True +NAUTOBOT_SSOT_ENABLE_BOOTSTRAP="False" +NAUTOBOT_BOOTSTRAP_SSOT_ENVIRONMENT_BRANCH=develop +NAUTOBOT_BOOTSTRAP_SSOT_LOAD_SOURCE=file # or git + NAUTOBOT_SSOT_ENABLE_DEVICE42="False" NAUTOBOT_SSOT_DEVICE42_HOST="" NAUTOBOT_SSOT_DEVICE42_USERNAME="" @@ -80,6 +84,9 @@ NAUTOBOT_DNAC_SSOT_DNA_CENTER_IMPORT_MERAKIS="False" NAUTOBOT_DNAC_SSOT_DNA_CENTER_UPDATE_LOCATIONS="True" NAUTOBOT_DNAC_SSOT_DNA_CENTER_SHOW_FAILURES="True" +NAUTOBOT_SSOT_ENABLE_CITRIX_ADM="False" +NAUTOBOT_SSOT_CITRIX_ADM_UPDATE_SITES="True" + NAUTOBOT_SSOT_ENABLE_INFOBLOX="False" NAUTOBOT_SSOT_INFOBLOX_DEFAULT_STATUS="Active" NAUTOBOT_SSOT_INFOBLOX_ENABLE_SYNC_TO_INFOBLOX="True" @@ -106,3 +113,6 @@ IPFABRIC_SSL_VERIFY="True" IPFABRIC_TIMEOUT=15 NAUTOBOT_SSOT_ENABLE_ITENTIAL="True" + +NAUTOBOT_SSOT_ENABLE_SLURPIT="False" +SLURPIT_HOST="https://sandbox.slurpit.io" \ No newline at end of file diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 7210ce596..a6d7b7f19 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -219,9 +219,11 @@ "vrf": True, "prefix": True, }, + "citrix_adm_update_sites": is_truthy(os.getenv("NAUTOBOT_SSOT_CITRIX_ADM_UPDATE_SITES", "true")), "enable_aci": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ACI")), "enable_aristacv": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ARISTACV")), "enable_bootstrap": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_BOOTSTRAP", "false")), + "enable_citrix_adm": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_CITRIX_ADM")), "enable_device42": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_DEVICE42")), "enable_dna_center": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_DNA_CENTER")), "enable_infoblox": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_INFOBLOX")), @@ -229,6 +231,7 @@ "enable_itential": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ITENTIAL")), "enable_meraki": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_MERAKI")), "enable_servicenow": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_SERVICENOW")), + "enable_slurpit": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_SLURPIT")), "hide_example_jobs": is_truthy(os.getenv("NAUTOBOT_SSOT_HIDE_EXAMPLE_JOBS")), "device42_defaults": { "site_status": "Active", diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index cce8d22ba..1ab142b33 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -29,3 +29,4 @@ While that last supported version will not be strictly enforced--via the max_ver | 3.0.1 | 2.1.0 | 2.99.99 | | 3.1.0 | 2.1.0 | 2.99.99 | | 3.2.0 | 2.1.0 | 2.99.99 | +| 3.3.0 | 2.1.0 | 2.99.99 | diff --git a/docs/admin/install.md b/docs/admin/install.md index d520a47d0..77646bd7c 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -99,3 +99,4 @@ Set up each integration using the specific guides: - [Itential](./integrations/itential_setup.md) - [Cisco Meraki](./integrations/meraki_setup.md) - [ServiceNow](./integrations/servicenow_setup.md) +- [Slurpit](./integrations/slurpit_setup.md) diff --git a/docs/admin/integrations/bootstrap_setup.md b/docs/admin/integrations/bootstrap_setup.md index eea250ae6..464802896 100644 --- a/docs/admin/integrations/bootstrap_setup.md +++ b/docs/admin/integrations/bootstrap_setup.md @@ -1,6 +1,5 @@ # Bootstrap - ## Description This App will sync data from YAML files into Nautobot to create baseline environments. Most items will receive a custom field associated with them called "System of Record", which will be set to "Bootstrap". These items are then the only ones managed by the Bootstrap SSoT App. Other items within the Nautobot instance will not be affected unless there's items with overlapping names. There is currently two exceptions to this and those are the ComputedField, and GraphQLQuery models since they can't have a custom field associated. If you choose to manage ComputedField or GraphQLQuery objects with the Bootstrap SSoT App, make sure to define them all within the YAML file, since any "locally defined" Computed Fields and GraphQL Queries within Nautobot will end up getting deleted when the job runs. If an item exists in Nautobot by it's identifiers but it does not have the "System of Record" custom field on it, the item will be updated with "Bootstrap" (or `SYSTEM_OF_RECORD` environment variable value) when the App runs. This way no duplicates are created, and the App will not delete any items that are not defined in the Bootstrap data but were manually created in Nautobot. @@ -13,10 +12,9 @@ Before configuring the integration, please ensure, that `nautobot-ssot` app was pip install nautobot-ssot[bootstrap] ``` +## Configuration -### nautobot_config.py - -The settings here are pretty straightforward, `nautobot_environment_branch` will be loaded from the environment variable `NAUTOBOT_BOOTSTRAP_SSOT_ENVIRONMENT_BRANCH`, or default to develop. The rest of the settings define which models/objects you want to have the App sync to Nautobot. There are a couple of caveats to these. For example, for DynamicGroup objects to sync, the filter criteria need to already exist in Nautobot. So, if you are going to have groups that are filtered on platforms/regions/sites/etc make sure not to include DynamicGroup objects in the "models_to_sync" until those items exist. Same for Git Repositories when you want to sync Golden Config-related repositories. The Golden Config App needs to be installed, for the `provided_contents` items to be able to be found. This also goes for the Lifecycle Management app with `Software/ValidatedSoftware` models. +Once the SSoT package has been installed you simply need to enable the integration by setting `enable_bootstrap` to True. There are additional settings that allow you to control which Nautobot objects are defined in your data. The settings are pretty straightforward. Assuming that you're copying the example settings below, the `bootstrap_nautobot_environment_branch` setting will be loaded from the environment variable `NAUTOBOT_BOOTSTRAP_SSOT_ENVIRONMENT_BRANCH`, or default to develop. The `bootstrap_models_to_sync` setting defines which models/objects you want to have the App sync to Nautobot. There are a couple of caveats to this functionality. For example, for DynamicGroup objects to sync, the filter criteria need to already exist in Nautobot. So, if you are going to have groups that are filtered on Platforms, Locations, etc, make sure not to include DynamicGroup objects in your Bootstrap Data until those items exist in Nautobot. If these items are also being synchronized in your Bootstrap Data, they will be created in the correct order. The same goes for Golden Config-related Git Repositories. It should go without saying, but the [Golden Config App](https://github.com/nautobot/nautobot-app-golden-config) must be installed, for the backup, intended, and template `provided_contents` items to be available options in your Bootstrap Data along with support of the `SoftwareLCM, SoftwareImageLCM, and ValidatedSoftware` models from the [Device Lifecycle Management app](https://github.com/nautobot/nautobot-app-device-lifecycle-mgmt). ```python PLUGINS = ["nautobot_ssot"] @@ -24,6 +22,7 @@ PLUGINS = ["nautobot_ssot"] PLUGINS_CONFIG = { "nautobot_ssot": { # Other nautobot_ssot settings ommitted. + "enable_bootstrap": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_BOOTSTRAP", "true")), "bootstrap_nautobot_environment_branch": os.getenv("NAUTOBOT_BOOTSTRAP_SSOT_ENVIRONMENT_BRANCH", "develop"), "bootstrap_models_to_sync": { "secret": True, @@ -57,20 +56,17 @@ PLUGINS_CONFIG = { "vrf": True, "prefix": True, }, - "enable_bootstrap": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_BOOTSTRAP", "false")), } } ``` -## Configuration - -### Bootstrap data +### Bootstrap Data Bootstrap data can be stored in 2 fashions. -1. (Recommended) Bootstrap data can be stored in a Git Repository and referenced in the app as a Git Datasource. A user should create a Git Repository in Nautobot (including any necessary Secrets and SecretsGroups for access) with the word "Bootstrap" in the name, and with a provided content type of `config contexts`. This is how the App will locate the correct repository. The data structure is flat files, and there is a naming scheme to these files. The first one required is `global_settings.yml`. This contains the main data structures of what data can be loaded `Secrets,SecretsGroups,GitRepository,DynamicGroup,Tag,etc`. You can then create additional `.yml` files with naming of your CI environments, i.e. production, development, etc for default values for specific items. This is where the environment variables described below would be matched to pull in additional data from the other YAML files defined in the directory. +1. __Recommended__ Bootstrap data can be stored in a Git Repository and referenced in the app as a Git Datasource. A user should create a Git Repository in Nautobot (including any necessary Secrets and SecretsGroups for access) with the word "Bootstrap" in the name, and with a provided content type of `config contexts`. This is how the App will locate the correct repository. The data structure is flat files, and there is a naming scheme to these files. The first one required is `global_settings.yml`. This contains the main data structures of what data can be loaded. ie `Secrets, SecretsGroups, GitRepository, DynamicGroup, Tag, etc`. You can then create additional `.yml` files with naming of your CI environments, ie `production`, `development`, etc for default values for specific items. This is where the environment variables described below would be matched to pull in additional data from the other YAML files defined in the directory. -2. Bootstrap data can be stored within the `nautobot_ssot/bootstrap/fixtures` directory. Using local files is not recommended as this requires a fork of the plugin and locally editing the YAML data files in the fixtures folder. +2. Bootstrap data can be stored within the `nautobot_ssot/integrations/bootstrap/fixtures` directory. Using local files is not recommended as this requires a fork of the plugin and locally editing the YAML data files in the fixtures folder. A simple structure would look something like this: diff --git a/docs/admin/integrations/citrix_adm_setup.md b/docs/admin/integrations/citrix_adm_setup.md new file mode 100644 index 000000000..9612732cc --- /dev/null +++ b/docs/admin/integrations/citrix_adm_setup.md @@ -0,0 +1,52 @@ +# Citrix ADM Integration Setup + +This guide will walk you through the steps to set up Citrix ADM integration with the `nautobot_ssot` app. + +## Prerequisites + +Before configuring the integration, please ensure, that the `nautobot-ssot` app was [installed with the Citrix ADM integration extra dependencies](../install.md#install-guide). + +```shell +pip install nautobot-ssot[citrix-adm] +``` + +## Configuration + +Access to your Citrix ADM instance is defined using the [ExternalIntegration](https://docs.nautobot.com/projects/core/en/stable/user-guide/platform-functionality/externalintegration/) model which allows you to utilize this integration with multiple instances concurrently. Please bear in mind that it will synchronize all data 1:1 with the specified instance to match exactly, meaning it will delete data missing from an instance. Each ExternalIntegration must specify a SecretsGroup with Secrets that contain the Citrix ADM Username and Password to authenticate with. You can find Secrets and SecretsGroups available under the Secrets menu. + +![Citrix ADM Username](../../images/citrix_adm_username.png) + +![Citrix ADM Password](../../images/citrix_adm_password.png) + +![Citrix ADM SecretsGroup](../../images/citrix_adm_secretsgroup.png) + +The Secrets Group linked to the Citrix ADM ExternalIntegration must contain Secrets defined as per the below: + +| Access Type | Secret Type | +| ----------- | ----------- | +| HTTP(S) | Username | +| HTTP(S) | Password | + +Once the SecretsGroup is created you'll need to create the ExternalIntegration. You'll find this under the Extensibility menu. + +![Citrix ADM ExternalIntegration](../../images/citrix_adm_externalintegration.png) + +> The only required portions are the Name, Remote URL, Verify SSL, and Secrets Group. + +When utilizing multiple SSoT integrations that contain differing Locations you might want to ensure that your existing Locations aren't updated by another integration. You can control whether these updates are made with the `citrix_adm_update_sites` setting in your `nautobot_config.py` file. + +| Configuration Variable | Type | Usage | Default | +| --------------------------------------------------- | ------- | ---------------------------------------------------------- | -------------------- | +| citrix_adm_update_sites | boolean | Whether to update loaded Datacenter Locations. | True | + +Below is an example snippet from `nautobot_config.py` that demonstrates how to enable and configure the Citrix ADM integration: + +```python +PLUGINS_CONFIG = { + "nautobot_ssot": { + "enable_citrix_adm": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_CITRIX_ADM", "true")), + "citrix_adm_update_sites": os.getenv("NAUTOBOT_SSOT_CITRIX_ADM_UPDATE_SITES", "true"), + } +``` + +Once the integration has been enabled you can find instructions on using it in the [Usage instructions](../../user/integrations/citrix_adm.md#usage). diff --git a/docs/admin/integrations/index.md b/docs/admin/integrations/index.md index e8c23e277..23d7582fa 100644 --- a/docs/admin/integrations/index.md +++ b/docs/admin/integrations/index.md @@ -4,6 +4,7 @@ This Nautobot app supports the following integrations: - [Cisco ACI](./aci_setup.md) - [Bootstrap](./bootstrap_setup.md) +- [Citrix ADM](./citrix_adm_setup.md) - [Arista CloudVision](./aristacv_setup.md) - [Device42](./device42_setup.md) - [Cisco DNA Center](./dna_center_setup.md) @@ -12,3 +13,4 @@ This Nautobot app supports the following integrations: - [Itential](./itential_setup.md) - [Cisco Meraki](./meraki_setup.md) - [ServiceNow](./servicenow_setup.md) +- [Slurpit](./slurpit_setup.md) diff --git a/docs/admin/integrations/slurpit_setup.md b/docs/admin/integrations/slurpit_setup.md new file mode 100644 index 000000000..1d7512d27 --- /dev/null +++ b/docs/admin/integrations/slurpit_setup.md @@ -0,0 +1,34 @@ +# Slurpit Integration Setup + +This guide will walk you through steps to set up Slurpit integration with the `nautobot_ssot` app. + +## Prerequisites + +Before configuring the integration, please ensure, that `nautobot-ssot` app was [installed with Slurpit integration extra dependencies](../install.md#install-guide). + +```shell +pip install nautobot-ssot[slurpit] +``` + +## Configuration + +Access to your Slurpit instance is defined using the [ExternalIntegration](https://docs.nautobot.com/projects/core/en/stable/user-guide/platform-functionality/externalintegration/) model which allows you to utilize this integration with multiple instances concurrently. Each ExternalIntegration must specify a SecretsGroup that contains Secrets that contain the Slurpit token to authenticate against that instance. You can find Secrets and SecretsGroups available under the Secrets menu. + +![Slurpit Token](../../images/slurpit_token.png) + +![Slurpit SecretsGroup](../../images/slurpit_secretsgroup.png) + +Please note that is it imperative for the SecretsGroup used for Slurpit uses HTTP(S) Access type and Token. Also note that the name of the Secrets or SecretsGroup are irrelevant but are recommended to be relevant to the Slurpit instance in question. + +Once the SecretsGroup is created you'll need to create the ExternalIntegration. You'll find this under the Extensibility menu. + +![Slurpit ExternalIntegration](../../images/slurpit_externalintegration.png) + +Below is an example snippet from `nautobot_config.py` that demonstrates how to enable and configure the Slurpit integration: + +```python +PLUGINS_CONFIG = { + "nautobot_ssot": { + "enable_slurpit": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_SLURPIT")), + } +``` diff --git a/docs/admin/release_notes/version_3.3.md b/docs/admin/release_notes/version_3.3.md new file mode 100644 index 000000000..ba03c32b0 --- /dev/null +++ b/docs/admin/release_notes/version_3.3.md @@ -0,0 +1,67 @@ +# v3.3 Release Notes + +This document describes all new features and changes in the release. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Release Overview + +There have been two new integrations added to the project! + + 1. Citrix ADM: This integration allows you to pull in the inventory of your Application Delivery Controllers from Citrix ADM into Nautobot. + 2. Slurp`It: This integration enables users to import data from Slurp`It that's a bit more flexible than their custom Nautobot App. + +- Additionally, support for the SoftwareVersion model has been added to the DNA Center and Bootstrap integrations. + +## [v3.3.0 (2024-12-06)](https://github.com/nautobot/nautobot-app-ssot/releases/tag/v3.3.0) + +### Added + +- [#310](https://github.com/nautobot/nautobot-app-ssot/issues/310) - Added common TypedDicts for Contrib SSoT. +- [#449](https://github.com/nautobot/nautobot-app-ssot/issues/449) - Add `delete_records` flag to the ServiceNow DataTarget job +- [#588](https://github.com/nautobot/nautobot-app-ssot/issues/588) - Added support for Software Version object in DNA Center integration. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Added ability to rename Network in Meraki and Datacenter in DNA Center integrations using location_map. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Added support for SoftwareVersion in Bootstrap integration. +- [#599](https://github.com/nautobot/nautobot-app-ssot/issues/599) - Added Citrix ADM integration. +- [#600](https://github.com/nautobot/nautobot-app-ssot/issues/600) - Added integration with Slurpit. + +### Changed + +- [#590](https://github.com/nautobot/nautobot-app-ssot/issues/590) - Improved error message for validated save in contrib model. + +### Removed + +- [#588](https://github.com/nautobot/nautobot-app-ssot/issues/588) - Removed use of OS Version CustomField in DNA Center integration. Now uses Software Version from Nautobot 2.2 and/or Device Lifecycle Management SoftwareLCM object if found. + +### Fixed + +- [#411](https://github.com/nautobot/nautobot-app-ssot/issues/411) - Fixed imports in CustomFields migration that was causing installation issues. +- [#449](https://github.com/nautobot/nautobot-app-ssot/issues/449) - Fix logic used for loading location records to make ServiceNow SSoT Nautobot 2.x compatible +- [#467](https://github.com/nautobot/nautobot-app-ssot/issues/467) - Fix get_tags_by_type() to handle possible RpcError Exception being thrown. +- [#582](https://github.com/nautobot/nautobot-app-ssot/issues/582) - Fixed erroneous print statement in sync logs. +- [#585](https://github.com/nautobot/nautobot-app-ssot/issues/585) - Fixed use of DLM classes with Bootstrap integration. +- [#588](https://github.com/nautobot/nautobot-app-ssot/issues/588) - Fixed hostname mapping functionality in DNA Center integration. It is now available in the Job form. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Fixed Meraki loading of Nautobot Prefixes that have multiple Locations assigned. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Fixed DNA Center loading incorrect location names for Devices. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Fixed KeyError being thrown when port is missing from uplink_settings dict in Meraki integration. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Fixed error in Bootstrap integration in loading ValidatedSoftwareLCM when SoftwareLCM doesn't exist. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Fixed DoesNotExist thrown when attempting to load ContentType that doesn't exist in Bootstrap integration. +- [#599](https://github.com/nautobot/nautobot-app-ssot/issues/599) - Fixed Bootstrap signals that are using create_or_update_custom_field() to pass apps. This was done to correct bug causing Nautobot to crash during startup. +- [#607](https://github.com/nautobot/nautobot-app-ssot/issues/607) - Fix hostname_mapping functionailty in Citrix ADM integration. +- [#610](https://github.com/nautobot/nautobot-app-ssot/issues/610) - Fix delete function for NautobotValidatedSoftware so UUID is used to find object instead of querying for Platform and Software object. +- [#612](https://github.com/nautobot/nautobot-app-ssot/issues/612) - Fixed AttributeError on attempting to load Platforms with no Manufacturer assigned. +- [#614](https://github.com/nautobot/nautobot-app-ssot/issues/614) - Fixed creating platforms with no Manufacturer assigned. +- [#614](https://github.com/nautobot/nautobot-app-ssot/issues/614) - Fixed time_zone attribute normalization on Location objects. +- [#616](https://github.com/nautobot/nautobot-app-ssot/issues/616) - Ensure Devices missing Platform are not loaded from DNA Center. + +### Documentation + +- [#585](https://github.com/nautobot/nautobot-app-ssot/issues/585) - Fix documentation for Bootstrap installation. +- [#605](https://github.com/nautobot/nautobot-app-ssot/issues/605) - Add missing acknowledgements for a few integrations. + +### Housekeeping + +- [#585](https://github.com/nautobot/nautobot-app-ssot/issues/585) - Disabled the BootstrapDataTarget Job as it's not usable at this time. +- [#587](https://github.com/nautobot/nautobot-app-ssot/issues/587) - Changed model_class_name in .cookiecutter.json to a valid model to help with drift management. +- [#593](https://github.com/nautobot/nautobot-app-ssot/issues/593) - Add code owners for DNA Center, Meraki, and Itential integrations. +- [#599](https://github.com/nautobot/nautobot-app-ssot/issues/599) - Consolidated repeat function, parse_hostname_for_role(), from DNA Center and Citrix integrations as SSoT utility function. +- [#605](https://github.com/nautobot/nautobot-app-ssot/issues/605) - Add code owner for Citrix ADM integration. +- [#607](https://github.com/nautobot/nautobot-app-ssot/issues/607) - Remove redundant parse_hostname_for_role() function in Meraki integration that was missed in 599. diff --git a/docs/images/citrix_adm_dashboard.png b/docs/images/citrix_adm_dashboard.png new file mode 100644 index 000000000..bd53c24b4 Binary files /dev/null and b/docs/images/citrix_adm_dashboard.png differ diff --git a/docs/images/citrix_adm_detail-view.png b/docs/images/citrix_adm_detail-view.png new file mode 100644 index 000000000..22b117bb8 Binary files /dev/null and b/docs/images/citrix_adm_detail-view.png differ diff --git a/docs/images/citrix_adm_enabled_job.png b/docs/images/citrix_adm_enabled_job.png new file mode 100644 index 000000000..bb8baf608 Binary files /dev/null and b/docs/images/citrix_adm_enabled_job.png differ diff --git a/docs/images/citrix_adm_externalintegration.png b/docs/images/citrix_adm_externalintegration.png new file mode 100644 index 000000000..a58015b51 Binary files /dev/null and b/docs/images/citrix_adm_externalintegration.png differ diff --git a/docs/images/citrix_adm_job_form.png b/docs/images/citrix_adm_job_form.png new file mode 100644 index 000000000..8ac9fa244 Binary files /dev/null and b/docs/images/citrix_adm_job_form.png differ diff --git a/docs/images/citrix_adm_job_list.png b/docs/images/citrix_adm_job_list.png new file mode 100644 index 000000000..017ee76a7 Binary files /dev/null and b/docs/images/citrix_adm_job_list.png differ diff --git a/docs/images/citrix_adm_job_settings.png b/docs/images/citrix_adm_job_settings.png new file mode 100644 index 000000000..7569a0d0a Binary files /dev/null and b/docs/images/citrix_adm_job_settings.png differ diff --git a/docs/images/citrix_adm_password.png b/docs/images/citrix_adm_password.png new file mode 100644 index 000000000..0dafe76b5 Binary files /dev/null and b/docs/images/citrix_adm_password.png differ diff --git a/docs/images/citrix_adm_secretsgroup.png b/docs/images/citrix_adm_secretsgroup.png new file mode 100644 index 000000000..c0fc6f685 Binary files /dev/null and b/docs/images/citrix_adm_secretsgroup.png differ diff --git a/docs/images/citrix_adm_username.png b/docs/images/citrix_adm_username.png new file mode 100644 index 000000000..516d142cc Binary files /dev/null and b/docs/images/citrix_adm_username.png differ diff --git a/docs/images/dnac_job_form.png b/docs/images/dnac_job_form.png index 33275af38..2de9ee0d4 100644 Binary files a/docs/images/dnac_job_form.png and b/docs/images/dnac_job_form.png differ diff --git a/docs/images/slurpit-detail-view.png b/docs/images/slurpit-detail-view.png new file mode 100644 index 000000000..e4dba77ae Binary files /dev/null and b/docs/images/slurpit-detail-view.png differ diff --git a/docs/images/slurpit-result-view.png b/docs/images/slurpit-result-view.png new file mode 100644 index 000000000..cb437b089 Binary files /dev/null and b/docs/images/slurpit-result-view.png differ diff --git a/docs/images/slurpit_externalintegration.png b/docs/images/slurpit_externalintegration.png new file mode 100644 index 000000000..dd5f2fb69 Binary files /dev/null and b/docs/images/slurpit_externalintegration.png differ diff --git a/docs/images/slurpit_secretsgroup.png b/docs/images/slurpit_secretsgroup.png new file mode 100644 index 000000000..f2ffd9c0c Binary files /dev/null and b/docs/images/slurpit_secretsgroup.png differ diff --git a/docs/images/slurpit_token.png b/docs/images/slurpit_token.png new file mode 100644 index 000000000..2a3bd907c Binary files /dev/null and b/docs/images/slurpit_token.png differ diff --git a/docs/user/integrations/citrix_adm.md b/docs/user/integrations/citrix_adm.md new file mode 100644 index 000000000..dd7077553 --- /dev/null +++ b/docs/user/integrations/citrix_adm.md @@ -0,0 +1,61 @@ +# Citrix ADM SSoT Integration + +The Citrix ADM SSoT integration is built as part of the [Nautobot Single Source of Truth (SSoT)](https://github.com/nautobot/nautobot-app-ssot) app. The SSoT app enables Nautobot to be the aggregation point for data coming from multiple systems of record (SoR). + +From Citrix ADM into Nautobot, it synchronizes the following objects: + +| Citrix ADM | Nautobot | +| ----------------------- | ---------------------------- | +| Datacenter | Location* | +| Devices | Devices | +| Hardwares | DeviceTypes | +| OSVersions | SoftwareVersions | +| Ports | Interfaces | +| Prefixes | Prefixes | +| IP Addresses | IP Addresses | + +## Usage + +Once the app is installed and configured, you will be able to perform an inventory ingestion from an individual or multiple Citrix ADM instances into Nautobot. From the Nautobot SSoT Dashboard view (`/plugins/ssot/`), Citrix ADM will show as a Data Source. + +![Dashboard View](../../images/citrix_adm_dashboard.png) + +From the Dashboard, you can also view more information about the App by clicking on the `Citrix ADM to Nautobot` link and see the Detail view. This view will show the mappings of Citrix ADM objects to Nautobot objects, the sync history, and other configuration details for the App: + +![Detail View](../../images/citrix_adm_detail-view.png) + +In order to utilize this integration you must first enable the Job. You can find the available installed Jobs under Jobs -> Jobs: + +![Job List](../../images/citrix_adm_job_list.png) + +To enable the Job you must click on the orange pencil icon to the right of the `Citrix ADM to Nautobot` Job. You will be presented with the settings for the Job as shown below: + +![Job Settings](../../images/citrix_adm_job_settings.png) + +You'll need to check the `Enabled` checkbox and then the `Update` button at the bottom of the page. You will then see that the play button next to the Job changes to blue and becomes functional, linking to the Job form. + +![Enabled Job](../../images/citrix_adm_enabled_job.png) + +Once the Job is enabled, you'll need to manually create a few objects in Nautobot to use with the Job. First, you'll need to create the Secrets, SecretsGroup, and ExternalIntegration as detailed in the [Citrix ADM Configuration](../../admin/integrations/citrix_adm_setup.md#configuration) instructions. + +> You can utilize multiple Citrix ADM Controllers with this integration as long as you specify a unique Tenant per Controller. The failure to use differing Tenants will have the Devices, Prefixes, and IPAddresses potentially removed if they are non-existent on the additional Controller. Locations should remain unaffected. + +With those configured, you will then need to define a LocationType to use for the imported Networks. With those created, you can run the Job to start the synchronization: + +![Job Form](../../images/citrix_adm_job_form.png) + +If you wish to just test the synchronization but not have any data created in Nautobot you'll want to select the `Dryrun` toggle. Clicking the `Debug` toggle will enable more verbose logging to inform you of what is occuring behind the scenes. After those toggles there are also dropdowns that allow you to specify the Citrix ADM instance(s) to synchronize with and to define the LocationType to use for the imported Datacenters from those instances. In addition, there are also some optional settings on the Job form: + +- Should the LocationType that you specify for the imported Networks require a parent Location to be assigned, you can define this parent one of two ways: + +1. The Parent Location field allows you to define a singular Location that will be assigned as the parent for all imported Datacenter Locations. + +2. The Location Mapping field allows you to define a dictionary of Location mappings. This feature is intended for specifying parent Locations for the Datacenter Locations in Citrix ADM. This is useful if this information is missing from Citrix ADM but required for Nautobot or to allow you to change the information as it's imported to match information from another System of Record. The expected pattern for this field is `{"": {"parent": ""}}`. + +In addition, the ability to assign Roles to your imported Devices as provided with the Hostname Mapping field. This field allows you to specify a list of tuples containing a regular expression pattern to match against Device hostnames and the Role to assign if matched. Ex: [(".*INT-LB.*", "Internal Load-Balancer")] + +- Finally there is an option to specify a Tenant to be assigned to the imported Devices, Prefixes, and IPAddresses. This is handy for cases where you have multiple Citrix ADM instances that are used by differing business units. + +Running this Job will redirect you to a `Nautobot Job Result` view. + +Once the Job has finished you can click on the `SSoT Sync Details` button at the top right of the Job Result page to see detailed information about the data that was synchronized from Citrix ADM and the outcome of the sync Job. diff --git a/docs/user/integrations/index.md b/docs/user/integrations/index.md index 961403d24..1d5f499d2 100644 --- a/docs/user/integrations/index.md +++ b/docs/user/integrations/index.md @@ -3,8 +3,9 @@ This Nautobot app supports the following integrations: - [Cisco ACI](./aci.md) -- [Arista CloudVision](./aristacv.md) - [Bootstrap](./bootstrap.md) +- [Arista CloudVision](./aristacv.md) +- [Citrix ADM](./citrix_adm.md) - [Device42](./device42.md) - [Cisco DNA Center](./dna_center.md) - [Infoblox](./infoblox.md) @@ -12,3 +13,4 @@ This Nautobot app supports the following integrations: - [Itential](./itential.md) - [Cisco Meraki](./meraki.md) - [ServiceNow](./servicenow.md) +- [Slurpit](./slurpit.md) diff --git a/docs/user/integrations/slurpit.md b/docs/user/integrations/slurpit.md new file mode 100644 index 000000000..351f8e60a --- /dev/null +++ b/docs/user/integrations/slurpit.md @@ -0,0 +1,42 @@ +# Slurpit SSoT Integration + +This integration provides a simple way to synchronize data between [Slurpit](https://slurpit.io/) and [Nautobot](https://github.com/nautobot/nautobot). It support multiple data types, including Devices, Interfaces, and IP Addresses. + +It synchronizes the following objects: + +| Slurpit | Nautobot | +| ----------------------- | ---------------------------- | +| Sites | Location | +| Devices | Manufacturer | +| Devices | Platform | +| Devices | Device Type | +| Devices | Device | +| Planning (Hardware Info) | Inventory Item | +| Planning (VLANs) | VLAN | +| Planning (Routing Table) | VRF | +| Planning (Routing Table) | Prefix | +| Planning (Interfaces) | Interface | +| Planning (Interfaces) | IP Address | + +## Usage + +Once the integration is installed and configured, from the Nautobot SSoT Dashboard view (`/plugins/ssot/`), Slurpit will be shown as a Data Source. You can click the **Sync** button to access a form view from which you can run the Slurpit-to-Nautobot synchronization Job. Running the job will redirect you to a Nautobot **Job Result** view, from which you can access the **SSoT Sync Details** view to see detailed information about the outcome of the sync Job. + +There are several options available for the sync Job: + +- **Dryrun**: If enabled, the sync Job will only report the differences between the source and destination without synchronizing any data. +- **Memory Profiling**: If enabled, the sync Job will collect memory profiling data and include it in the Job Result. +- **Slurpit Instance**: The Slurpit instance to sync data from. +- **Site LocationType**: The Nautobot LocationType to use for imported Sites. +- **IPAM Namespace**: The Namespace to use for all imported IPAM data. +- **Ignore Routing Table Prefixes**: If enabled, the sync Job will not import some routing table prefixes such as `0.0.0.0/0`, `::/0`, `224.0.0.0/4` and more. +- **Sync tagged objects only**: If enabled, the sync Job will only import objects that have been tagged with the `SSoT Synced from Slurpit` tag in Nautobot. +- **Task Queue**: The Task Queue to use for the sync Job. + +## Screenshots + +![Detail View](../../images/slurpit-detail-view.png) + +--- + +![Results View](../../images/slurpit-result-view.png) diff --git a/docs/user/modeling.md b/docs/user/modeling.md index 91d7335e3..d106ad312 100644 --- a/docs/user/modeling.md +++ b/docs/user/modeling.md @@ -128,6 +128,16 @@ class IPAddressDict(TypedDict): prefix_length: int ``` +!!! note + Common `TypedDict` objects are included with `nautobot_ssot.contrib.typeddicts` including: + + `ContentTypeDict`, `TagDict`, `LocationDict`, `DeviceDict`, `InterfaceDict`, `PrefixDict`, `VLANDict`, `IPAddressDict`, `VirtualMachineDict` + + ``` + # Example + from nautobot_ssot.contrib.typeddicts import LocationDict + ``` + Having defined this, we can now define our diffsync model: ```python diff --git a/mkdocs.yml b/mkdocs.yml index 2bac83284..9394f2310 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -109,8 +109,9 @@ nav: - Integrations: - "user/integrations/index.md" - Cisco ACI: "user/integrations/aci.md" - - Arista CloudVision: "user/integrations/aristacv.md" - Bootstrap: "user/integrations/bootstrap.md" + - Citrix ADM: "user/integrations/citrix_adm.md" + - Arista CloudVision: "user/integrations/aristacv.md" - Device42: "user/integrations/device42.md" - DNA Center: "user/integrations/dna_center.md" - Infoblox: "user/integrations/infoblox.md" @@ -118,6 +119,7 @@ nav: - Itential: "user/integrations/itential.md" - Cisco Meraki: "user/integrations/meraki.md" - ServiceNow: "user/integrations/servicenow.md" + - Slurpit: "user/integrations/slurpit.md" - Modeling: "user/modeling.md" - Performance: "user/performance.md" - Frequently Asked Questions: "user/faq.md" @@ -127,8 +129,9 @@ nav: - Integrations Installation: - "admin/integrations/index.md" - Cisco ACI: "admin/integrations/aci_setup.md" - - Arista CloudVision: "admin/integrations/aristacv_setup.md" + - Citrix ADM: "admin/integrations/citrix_adm_setup.md" - Bootstrap: "admin/integrations/bootstrap_setup.md" + - Arista CloudVision: "admin/integrations/aristacv_setup.md" - Device42: "admin/integrations/device42_setup.md" - DNA Center: "admin/integrations/dna_center_setup.md" - Infoblox: "admin/integrations/infoblox_setup.md" @@ -136,11 +139,13 @@ nav: - Itential: "admin/integrations/itential_setup.md" - Cisco Meraki: "admin/integrations/meraki_setup.md" - ServiceNow: "admin/integrations/servicenow_setup.md" + - Slurpit: "admin/integrations/slurpit_setup.md" - Upgrade: "admin/upgrade.md" - Uninstall: "admin/uninstall.md" - Compatibility Matrix: "admin/compatibility_matrix.md" - Release Notes: - "admin/release_notes/index.md" + - v3.3: "admin/release_notes/version_3.3.md" - v3.2: "admin/release_notes/version_3.2.md" - v3.1: "admin/release_notes/version_3.1.md" - v3.0: "admin/release_notes/version_3.0.md" diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index b769fbee3..268842ac3 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -18,6 +18,7 @@ "nautobot_ssot_aci", "nautobot_ssot_aristacv", "nautobot_ssot_bootstrap", + "nautobot_ssot_citrix_adm", "nautobot_ssot_device42", "nautobot_ssot_dna_center", "nautobot_ssot_infoblox", @@ -83,6 +84,7 @@ class NautobotSSOTAppConfig(NautobotAppConfig): "aristacv_role_mappings": {}, "aristacv_site_mappings": {}, "aristacv_verify": True, + "citrix_adm_update_sites": True, "device42_host": "", "device42_username": "", "device42_password": "", @@ -102,9 +104,11 @@ class NautobotSSOTAppConfig(NautobotAppConfig): "enable_aristacv": False, "enable_device42": False, "enable_dna_center": False, + "enable_citrix_adm": False, "enable_infoblox": False, "enable_ipfabric": False, "enable_servicenow": False, + "enable_slurpit": False, "enable_itential": False, "hide_example_jobs": True, "ipfabric_api_token": "", diff --git a/nautobot_ssot/contrib/model.py b/nautobot_ssot/contrib/model.py index 12b6fcf60..3889607e5 100644 --- a/nautobot_ssot/contrib/model.py +++ b/nautobot_ssot/contrib/model.py @@ -229,7 +229,9 @@ def _update_obj_with_parameters(cls, obj, parameters, adapter): try: obj.validated_save() except ValidationError as error: - raise ObjectCrudException(f"Validated save failed for Django object. Parameters: {parameters}") from error + raise ObjectCrudException( + f"Validated save failed for Django object:\n{error}\nParameters: {parameters}" + ) from error # Handle relationship association creation. This needs to be after object creation, because relationship # association objects rely on both sides already existing. diff --git a/nautobot_ssot/contrib/typeddicts.py b/nautobot_ssot/contrib/typeddicts.py new file mode 100644 index 000000000..832696788 --- /dev/null +++ b/nautobot_ssot/contrib/typeddicts.py @@ -0,0 +1,74 @@ +"""Common TypedDict definitions used in Many-to-Many relationships.""" + +from typing import TypedDict + + +class ContentTypeDict(TypedDict): + """TypedDict for Django Content Types.""" + + app_label: str + model: str + + +class TagDict(TypedDict): + """TypedDict for Nautobot Tags.""" + + name: str + + +class LocationDict(TypedDict): + """TypedDict for DCIM Locations.""" + + name: str + parent__name: str + + +class DeviceDict(TypedDict): + """TypedDict for DCIM Devices.""" + + name: str + location__name: str + tenant__name: str + rack__name: str + rack__rack_group__name: str + position: int + face: str + virtual_chassis__name: str + vc_position: int + + +class InterfaceDict(TypedDict): + """TypedDict for DCIM INterfaces.""" + + name: str + device__name: str + + +class PrefixDict(TypedDict): + """TypedDict for IPAM Prefixes.""" + + network: str + prefix_length: int + namespace__name: str + + +class VLANDict(TypedDict): + """TypedDict for IPAM VLANs.""" + + vid: int + vlan_group__name: str + + +class IPAddressDict(TypedDict): + """TypedDict for IPAM IP Address.""" + + host: str + prefix_length: int + + +class VirtualMachineDict(TypedDict): + """TypedDict for IPAM .""" + + name: str + cluster__name: str + tenant__name: str diff --git a/nautobot_ssot/integrations/aristacv/diffsync/adapters/cloudvision.py b/nautobot_ssot/integrations/aristacv/diffsync/adapters/cloudvision.py index a9cfb1543..67f55594d 100644 --- a/nautobot_ssot/integrations/aristacv/diffsync/adapters/cloudvision.py +++ b/nautobot_ssot/integrations/aristacv/diffsync/adapters/cloudvision.py @@ -232,7 +232,7 @@ def load_ip_addresses(self, dev: device): def load_device_tags(self, device): """Load device tags from CloudVision.""" system_tags = cloudvision.get_tags_by_type( - client=self.conn.comm_channel, creator_type=TAG.models.CREATOR_TYPE_SYSTEM + client=self.conn.comm_channel, logger=self.job.logger, creator_type=TAG.models.CREATOR_TYPE_SYSTEM ) dev_tags = [ tag diff --git a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py index 49eaebd34..9e06b505a 100644 --- a/nautobot_ssot/integrations/aristacv/utils/cloudvision.py +++ b/nautobot_ssot/integrations/aristacv/utils/cloudvision.py @@ -267,18 +267,21 @@ def get_devices(client, import_active: bool): return devices -def get_tags_by_type(client, creator_type: int = tag_models.CREATOR_TYPE_USER): +def get_tags_by_type(client, logger, creator_type: int = tag_models.CREATOR_TYPE_USER): """Get tags by creator type from CloudVision.""" - tag_stub = tag_services.TagServiceStub(client) - req = tag_services.TagStreamRequest(partial_eq_filter=[tag_models.Tag(creator_type=creator_type)]) - responses = tag_stub.GetAll(req) tags = [] - for resp in responses: - dev_tag = { - "label": resp.value.key.label.value, - "value": resp.value.key.value.value, - } - tags.append(dev_tag) + try: + tag_stub = tag_services.TagServiceStub(client) + req = tag_services.TagStreamRequest(partial_eq_filter=[tag_models.Tag(creator_type=creator_type)]) + responses = tag_stub.GetAll(req) + for resp in responses: + dev_tag = { + "label": resp.value.key.label.value, + "value": resp.value.key.value.value, + } + tags.append(dev_tag) + except grpc.RpcError as err: + logger.error(f"Error when pulling Tags: {err}") return tags diff --git a/nautobot_ssot/integrations/bootstrap/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/bootstrap/diffsync/adapters/nautobot.py index 4a07b4052..5af9cd8d8 100755 --- a/nautobot_ssot/integrations/bootstrap/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/bootstrap/diffsync/adapters/nautobot.py @@ -82,32 +82,40 @@ ) try: - import nautobot_device_lifecycle_mgmt # noqa: F401 + # noqa: F401 + from nautobot_device_lifecycle_mgmt.models import ( + SoftwareLCM as ORMSoftware, + ) + + from nautobot_ssot.integrations.bootstrap.diffsync.models.nautobot import NautobotSoftware - LIFECYCLE_MGMT = True -except ImportError: - LIFECYCLE_MGMT = False + SOFTWARE_LIFECYCLE_MGMT = True +except (ImportError, RuntimeError): + SOFTWARE_LIFECYCLE_MGMT = False -if LIFECYCLE_MGMT: +try: # noqa: F401 from nautobot_device_lifecycle_mgmt.models import ( SoftwareImageLCM as ORMSoftwareImage, ) - from nautobot_device_lifecycle_mgmt.models import ( - SoftwareLCM as ORMSoftware, - ) + from nautobot_ssot.integrations.bootstrap.diffsync.models.nautobot import NautobotSoftwareImage + + SOFTWARE_IMAGE_LIFECYCLE_MGMT = True +except (ImportError, RuntimeError): + SOFTWARE_IMAGE_LIFECYCLE_MGMT = False + +try: # noqa: F401 from nautobot_device_lifecycle_mgmt.models import ( ValidatedSoftwareLCM as ORMValidatedSoftware, ) - # noqa: F401 - from nautobot_ssot.integrations.bootstrap.diffsync.models.nautobot import ( # noqa: F401 - NautobotSoftware, - NautobotSoftwareImage, - NautobotValidatedSoftware, - ) + from nautobot_ssot.integrations.bootstrap.diffsync.models.nautobot import NautobotValidatedSoftware + + VALID_SOFTWARE_LIFECYCLE_MGMT = True +except (ImportError, RuntimeError): + VALID_SOFTWARE_LIFECYCLE_MGMT = False class NautobotAdapter(Adapter): @@ -141,9 +149,11 @@ class NautobotAdapter(Adapter): tag = NautobotTag graph_ql_query = NautobotGraphQLQuery - if LIFECYCLE_MGMT: + if SOFTWARE_LIFECYCLE_MGMT: software = NautobotSoftware + if SOFTWARE_IMAGE_LIFECYCLE_MGMT: software_image = NautobotSoftwareImage + if VALID_SOFTWARE_LIFECYCLE_MGMT: validated_software = NautobotValidatedSoftware top_level = [ @@ -175,9 +185,11 @@ class NautobotAdapter(Adapter): "graph_ql_query", ] - if LIFECYCLE_MGMT: + if SOFTWARE_LIFECYCLE_MGMT: top_level.append("software") + if SOFTWARE_IMAGE_LIFECYCLE_MGMT: top_level.append("software_image") + if VALID_SOFTWARE_LIFECYCLE_MGMT: top_level.append("validated_software") def __init__(self, *args, job=None, sync=None, **kwargs): # noqa: D417 @@ -333,6 +345,11 @@ def load_platform(self): _napalm_args = {} else: _napalm_args = nb_platform.napalm_args + + _manufacturer = "" + if nb_platform.manufacturer is not None: + _manufacturer = nb_platform.manufacturer.name + _sor = "" if "system_of_record" in nb_platform.custom_field_data: _sor = ( @@ -342,7 +359,7 @@ def load_platform(self): ) new_platform = self.platform( name=nb_platform.name, - manufacturer=nb_platform.manufacturer.name, + manufacturer=_manufacturer, network_driver=nb_platform.network_driver, napalm_driver=nb_platform.napalm_driver, napalm_arguments=_napalm_args, @@ -423,7 +440,7 @@ def load_location(self): except AttributeError: _time_zone = nb_location.time_zone else: - _time_zone = None + _time_zone = "" if nb_location.tenant is not None: _tenant = nb_location.tenant.name else: @@ -1240,59 +1257,48 @@ def load_validated_software(self): for nb_validated_software in ORMValidatedSoftware.objects.all(): if self.job.debug: self.job.logger.debug(f"Loading Nautobot ValidatedSoftwareLCM {nb_validated_software}") - try: - _software = ORMSoftware.objects.get( - version=nb_validated_software.software.version, - device_platform=nb_validated_software.software.device_platform.id, - ) - self.get( - self.validated_software, - { - "software": {nb_validated_software.software}, - "valid_since": {nb_validated_software.start}, - "valid_until": {nb_validated_software.end}, - }, - ) - except ObjectNotFound: - _val_software = ORMValidatedSoftware.objects.get( - software=_software, - start=nb_validated_software.start, - end=nb_validated_software.end, - ) - _tags = sorted(list(_val_software.tags.all().values_list("name", flat=True))) - _devices = sorted(list(_val_software.devices.all().values_list("name", flat=True))) - _device_types = sorted(list(_val_software.device_types.all().values_list("model", flat=True))) - _device_roles = sorted(list(_val_software.device_roles.all().values_list("name", flat=True))) - _inventory_items = sorted(list(_val_software.inventory_items.all().values_list("name", flat=True))) - _object_tags = sorted(list(_val_software.object_tags.all().values_list("name", flat=True))) - _sor = "" - if "system_of_record" in nb_validated_software.custom_field_data: - _sor = ( - nb_validated_software.custom_field_data["system_of_record"] - if nb_validated_software.custom_field_data["system_of_record"] is not None - else "" - ) - new_validated_software = self.validated_software( - software=f"{nb_validated_software.software.device_platform} - {nb_validated_software.software.version}", - software_version=nb_validated_software.software.version, - platform=nb_validated_software.software.device_platform.name, - valid_since=nb_validated_software.start, - valid_until=nb_validated_software.end, - preferred_version=nb_validated_software.preferred, - devices=_devices, - device_types=_device_types, - device_roles=_device_roles, - inventory_items=_inventory_items, - object_tags=_object_tags, - tags=_tags, - system_of_record=_sor, - uuid=nb_validated_software.id, - ) - if not check_sor_field(nb_validated_software): - new_validated_software.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST - - self.add(new_validated_software) + _tags = sorted(list(nb_validated_software.tags.all().values_list("name", flat=True))) + _devices = sorted(list(nb_validated_software.devices.all().values_list("name", flat=True))) + _device_types = sorted(list(nb_validated_software.device_types.all().values_list("model", flat=True))) + _device_roles = sorted(list(nb_validated_software.device_roles.all().values_list("name", flat=True))) + _inventory_items = sorted(list(nb_validated_software.inventory_items.all().values_list("name", flat=True))) + _object_tags = sorted(list(nb_validated_software.object_tags.all().values_list("name", flat=True))) + _sor = "" + if "system_of_record" in nb_validated_software.custom_field_data: + _sor = ( + nb_validated_software.custom_field_data["system_of_record"] + if nb_validated_software.custom_field_data["system_of_record"] is not None + else "" + ) + if hasattr(nb_validated_software.software, "device_platform"): + platform = nb_validated_software.software.device_platform.name + else: + platform = nb_validated_software.software.platform.name + new_validated_software, _ = self.get_or_instantiate( + self.validated_software, + ids={ + "software": str(nb_validated_software.software), + "valid_since": nb_validated_software.start, + "valid_until": nb_validated_software.end, + }, + attrs={ + "software_version": nb_validated_software.software.version, + "platform": platform, + "preferred_version": nb_validated_software.preferred, + "devices": _devices, + "device_types": _device_types, + "device_roles": _device_roles, + "inventory_items": _inventory_items, + "object_tags": _object_tags, + "tags": _tags, + "system_of_record": _sor, + "uuid": nb_validated_software.id, + }, + ) + + if not check_sor_field(nb_validated_software): + new_validated_software.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST def load(self): """Load data from Nautobot into DiffSync models.""" @@ -1350,10 +1356,12 @@ def load(self): self.load_tag() if settings.PLUGINS_CONFIG["nautobot_ssot"]["bootstrap_models_to_sync"]["graph_ql_query"]: self.load_graph_ql_query() - if LIFECYCLE_MGMT: + if SOFTWARE_LIFECYCLE_MGMT: if settings.PLUGINS_CONFIG["nautobot_ssot"]["bootstrap_models_to_sync"]["software"]: self.load_software() + if SOFTWARE_IMAGE_LIFECYCLE_MGMT: if settings.PLUGINS_CONFIG["nautobot_ssot"]["bootstrap_models_to_sync"]["software_image"]: self.load_software_image() + if VALID_SOFTWARE_LIFECYCLE_MGMT: if settings.PLUGINS_CONFIG["nautobot_ssot"]["bootstrap_models_to_sync"]["validated_software"]: self.load_validated_software() diff --git a/nautobot_ssot/integrations/bootstrap/diffsync/models/base.py b/nautobot_ssot/integrations/bootstrap/diffsync/models/base.py index f702008c3..fb841e7cc 100755 --- a/nautobot_ssot/integrations/bootstrap/diffsync/models/base.py +++ b/nautobot_ssot/integrations/bootstrap/diffsync/models/base.py @@ -362,7 +362,7 @@ class Platform(DiffSyncModel): _children = {} name: str - manufacturer: str + manufacturer: Optional[str] = None network_driver: Optional[str] = None napalm_driver: Optional[str] = None napalm_arguments: Optional[dict] = None diff --git a/nautobot_ssot/integrations/bootstrap/diffsync/models/nautobot.py b/nautobot_ssot/integrations/bootstrap/diffsync/models/nautobot.py index 474bfef64..76081a68a 100755 --- a/nautobot_ssot/integrations/bootstrap/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/bootstrap/diffsync/models/nautobot.py @@ -78,33 +78,52 @@ ) try: - import nautobot_device_lifecycle_mgmt # noqa: F401 + # noqa: F401 + from nautobot_device_lifecycle_mgmt.models import ( + SoftwareLCM as ORMSoftware, + ) - LIFECYCLE_MGMT = True -except ImportError: - LIFECYCLE_MGMT = False + from nautobot_ssot.integrations.bootstrap.diffsync.models.base import Software -if LIFECYCLE_MGMT: + SOFTWARE_LIFECYCLE_MGMT = True +except (ImportError, RuntimeError): + SOFTWARE_LIFECYCLE_MGMT = False + +try: # noqa: F401 - from nautobot_device_lifecycle_mgmt.models import ( - SoftwareImageLCM as ORMSoftwareImage, + from nautobot.dcim.models import ( + SoftwareVersion as ORMSoftwareVersion, ) + from nautobot_ssot.integrations.bootstrap.diffsync.models.base import Software + + SOFTWARE_VERSION_FOUND = True +except (ImportError, RuntimeError): + SOFTWARE_VERSION_FOUND = False + +try: # noqa: F401 from nautobot_device_lifecycle_mgmt.models import ( - SoftwareLCM as ORMSoftware, + SoftwareImageLCM as ORMSoftwareImage, ) + from nautobot_ssot.integrations.bootstrap.diffsync.models.base import SoftwareImage + + SOFTWARE_IMAGE_LIFECYCLE_MGMT = True +except (ImportError, RuntimeError): + SOFTWARE_IMAGE_LIFECYCLE_MGMT = False + +try: # noqa: F401 from nautobot_device_lifecycle_mgmt.models import ( ValidatedSoftwareLCM as ORMValidatedSoftware, ) - from nautobot_ssot.integrations.bootstrap.diffsync.models.base import ( - Software, - SoftwareImage, - ValidatedSoftware, - ) + from nautobot_ssot.integrations.bootstrap.diffsync.models.base import ValidatedSoftware + + VALID_SOFTWARE_LIFECYCLE_MGMT = True +except (ImportError, RuntimeError): + VALID_SOFTWARE_LIFECYCLE_MGMT = False class NautobotTenantGroup(TenantGroup): @@ -229,7 +248,10 @@ def create(cls, adapter, ids, attrs): _content_types = [] adapter.job.logger.info(f'Creating Nautobot Role: {ids["name"]}') for _model in attrs["content_types"]: - _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + try: + _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + except ContentType.DoesNotExist: + adapter.job.logger.error(f"Unable to find ContentType for {_model}.") _new_role = ORMRole( name=ids["name"], weight=attrs["weight"], @@ -257,7 +279,10 @@ def update(self, attrs): if "content_types" in attrs: for _model in attrs["content_types"]: self.adapter.job.logger.debug(f"Looking up {_model} in content types.") - _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + try: + _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + except ContentType.DoesNotExist: + self.adapter.job.logger.error(f"Unable to find ContentType for {_model}.") _update_role.content_types.set(_content_types) if not check_sor_field(_update_role): _update_role.custom_field_data.update({"system_of_record": os.getenv("SYSTEM_OF_RECORD", "Bootstrap")}) @@ -326,7 +351,9 @@ def create(cls, adapter, ids, attrs): """Create Platform in Nautobot from NautobotPlatform object.""" adapter.job.logger.info(f'Creating Nautobot Platform {ids["name"]}') try: - _manufacturer = ORMManufacturer.objects.get(name=ids["manufacturer"]) + _manufacturer = None + if ids["manufacturer"]: + _manufacturer = ORMManufacturer.objects.get(name=ids["manufacturer"]) _new_platform = ORMPlatform( name=ids["name"], manufacturer=_manufacturer, @@ -387,7 +414,10 @@ def create(cls, adapter, ids, attrs): adapter.job.logger.info(f'Creating Nautobot LocationType: {ids["name"]}') _parent = None for _model in attrs["content_types"]: - _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + try: + _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + except ContentType.DoesNotExist: + adapter.job.logger.error(f"Unable to find ContentType for {_model}.") if "parent" in attrs: try: _parent = ORMLocationType.objects.get(name=attrs["parent"]) @@ -404,7 +434,10 @@ def create(cls, adapter, ids, attrs): _new_location_type.validated_save() for _model in attrs["content_types"]: adapter.job.logger.debug(f"Looking up {_model} in content types.") - _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + try: + _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + except ContentType.DoesNotExist: + adapter.job.logger.error(f"Unable to find ContentType for {_model}.") _new_location_type.content_types.set(_content_types) _new_location_type.custom_field_data.update({"last_synced_from_sor": datetime.today().date().isoformat()}) _new_location_type.custom_field_data.update({"system_of_record": os.getenv("SYSTEM_OF_RECORD", "Bootstrap")}) @@ -431,7 +464,10 @@ def update(self, attrs): if "content_types" in attrs: for _model in attrs["content_types"]: self.adapter.job.logger.debug(f"Looking up {_model} in content types.") - _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + try: + _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + except ContentType.DoesNotExist: + self.adapter.job.logger.error(f"Unable to find ContentType for {_model}.") _update_location_type.content_types.set(_content_types) if not check_sor_field(_update_location_type): _update_location_type.custom_field_data.update( @@ -2099,7 +2135,10 @@ def create(cls, adapter, ids, attrs): adapter.job.logger.info(f'Creating Nautobot Tag: {ids["name"]}') for _model in attrs["content_types"]: adapter.job.logger.debug(f"Looking up {_model} in content types.") - _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + try: + _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + except ContentType.DoesNotExist: + adapter.job.logger.error(f"Unable to find ContentType for {_model}.") _new_tag = ORMTag( name=ids["name"], color=attrs["color"], @@ -2122,7 +2161,10 @@ def update(self, attrs): _content_types = [] for _model in attrs["content_types"]: self.adapter.job.logger.debug(f"Looking up {_model} in content types.") - _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + try: + _content_types.append(lookup_content_type_for_taggable_model_path(_model)) + except ContentType.DoesNotExist: + self.adapter.job.logger.error(f"Unable to find ContentType for {_model}.") _update_tag.content_types.set(_content_types) if attrs.get("description"): _update_tag.description = attrs["description"] @@ -2176,7 +2218,7 @@ def delete(self): self.adapter.job.logger.warning(f"Unable to find GraphQLQuery {self.name} for deletion. {err}") -if LIFECYCLE_MGMT: +if SOFTWARE_LIFECYCLE_MGMT or SOFTWARE_VERSION_FOUND: class NautobotSoftware(Software): """Nautobot implementation of Bootstrap Software model.""" @@ -2184,20 +2226,36 @@ class NautobotSoftware(Software): @classmethod def create(cls, adapter, ids, attrs): """Create Software in Nautobot from NautobotSoftware object.""" - adapter.job.logger.info(f'Creating Nautobot Software object {ids["platform"]} - {ids["version"]}.') _tags = [] for tag in attrs["tags"]: _tags.append(ORMTag.objects.get(name=tag)) _platform = ORMPlatform.objects.get(name=ids["platform"]) - _new_software = ORMSoftware( - version=ids["version"], - alias=attrs["alias"], - device_platform=_platform, - end_of_support=attrs["eos_date"], - long_term_support=attrs["long_term_support"], - pre_release=attrs["pre_release"], - documentation_url=attrs["documentation_url"], - ) + if SOFTWARE_LIFECYCLE_MGMT: + _new_software = ORMSoftware( + version=ids["version"], + alias=attrs["alias"], + device_platform=_platform, + end_of_support=attrs["eos_date"], + long_term_support=attrs["long_term_support"], + pre_release=attrs["pre_release"], + documentation_url=attrs["documentation_url"], + ) + elif SOFTWARE_VERSION_FOUND: + _new_software = ORMSoftwareVersion( + version=ids["version"], + alias=attrs["alias"], + platform=_platform, + end_of_support_date=attrs["eos_date"], + long_term_support=attrs["long_term_support"], + pre_release=attrs["pre_release"], + documentation_url=attrs["documentation_url"], + ) + else: + adapter.job.logger.error( + f"Software model not found so skipping creation of {ids['version']} for {ids['platform']}." + ) + return None + adapter.job.logger.info(f'Creating Nautobot Software object {ids["platform"]} - {ids["version"]}.') if attrs.get("tags"): _new_software.validated_save() _new_software.tags.clear() @@ -2210,10 +2268,15 @@ def create(cls, adapter, ids, attrs): def update(self, attrs): """Update Software in Nautobot from NautobotSoftware object.""" - self.adapter.job.logger.info(f"Updating Software: {self.platform} - {self.version}.") - _tags = [] # noqa: F841 _platform = ORMPlatform.objects.get(name=self.platform) - _update_software = ORMSoftware.objects.get(version=self.version, device_platform=_platform) + if SOFTWARE_IMAGE_LIFECYCLE_MGMT: + _update_software = ORMSoftware.objects.get(version=self.version, device_platform=_platform) + elif SOFTWARE_VERSION_FOUND: + _update_software = ORMSoftwareVersion.objects.get(version=self.version, platform=_platform) + else: + self.adapter.job.logger.error(f"Software model not found so skipping update of {self}.") + return None + self.adapter.job.logger.info(f"Updating Software: {self.platform} - {self.version}.") if "alias" in attrs: _update_software.alias = attrs["alias"] if attrs.get("release_date"): @@ -2254,12 +2317,20 @@ def delete(self): f"Unable to find Software {self.platform} - {self.version} for deletion. {err}" ) + +if SOFTWARE_IMAGE_LIFECYCLE_MGMT: + class NautobotSoftwareImage(SoftwareImage): """Nautobot implementation of Bootstrap SoftwareImage model.""" @classmethod def create(cls, adapter, ids, attrs): """Create SoftwareImage in Nautobot from NautobotSoftwareImage object.""" + if not SOFTWARE_IMAGE_LIFECYCLE_MGMT: + adapter.job.logger.error( + f"SoftwareImageLCM model not found so skipping creation of {attrs['software_version']} for {attrs['platform']}." + ) + return None _tags = [] if attrs["tags"] is not None: for tag in attrs["tags"]: @@ -2286,6 +2357,11 @@ def create(cls, adapter, ids, attrs): def update(self, attrs): """Update SoftwareImage in Nautobot from NautobotSoftwareImage object.""" + if not SOFTWARE_IMAGE_LIFECYCLE_MGMT: + self.adapter.job.logger.error( + f"SoftwareImageLCM model not found so skipping update of {self.software_version} for {self.platform}." + ) + return None self.adapter.job.logger.info(f"Updating Software Image: {self.platform} - {self.software_version}.") _platform = ORMPlatform.objects.get(name=self.platform) _software = ORMSoftware.objects.get(version=self.software_version, device_platform=_platform) @@ -2329,6 +2405,9 @@ def delete(self): except ORMSoftwareImage.DoesNotExist as err: self.adapter.job.logger.warning(f"Unable to find SoftwareImage {self.software} for deletion. {err}") + +if VALID_SOFTWARE_LIFECYCLE_MGMT: + class NautobotValidatedSoftware(ValidatedSoftware): """Nautobot implementation of Bootstrap ValidatedSoftware model.""" @@ -2341,7 +2420,15 @@ def create(cls, adapter, ids, attrs): _inventory_items = [] # noqa: F841 _object_tags = [] # noqa: F841 _platform = ORMPlatform.objects.get(name=attrs["platform"]) - _software = ORMSoftware.objects.get(version=attrs["software_version"], device_platform=_platform) + if SOFTWARE_LIFECYCLE_MGMT: + _software = ORMSoftware.objects.get(version=attrs["software_version"], device_platform=_platform) + elif SOFTWARE_VERSION_FOUND: + _software = ORMSoftwareVersion.objects.get(version=attrs["software_version"], platform=_platform) + else: + adapter.job.logger.error( + f"Model to represent Software version not found so skipping creation of ValidatedSoftware {attrs['software_version']} for {attrs['platform']}." + ) + return None _new_validated_software = ORMValidatedSoftware( software=_software, start=ids["valid_since"] if not None else datetime.today().date(), @@ -2389,7 +2476,6 @@ def create(cls, adapter, ids, attrs): def update(self, attrs): """Update ValidatedSoftware in Nautobot from NautobotValidatedSoftware object.""" - self.adapter.job.logger.info(f"Updating Validated Software - {self} with attrs {attrs}.") _tags = [] # noqa: F841 _devices = [] # noqa: F841 _device_types = [] # noqa: F841 @@ -2397,7 +2483,16 @@ def update(self, attrs): _inventory_items = [] # noqa: F841 _object_tags = [] # noqa: F841 _platform = ORMPlatform.objects.get(name=self.platform) - _software = ORMSoftware.objects.get(version=self.software_version, device_platform=_platform) + if SOFTWARE_LIFECYCLE_MGMT: + _software = ORMSoftware.objects.get(version=self.software_version, device_platform=_platform) + elif SOFTWARE_VERSION_FOUND: + _software = ORMSoftwareVersion.objects.get(version=self.software_version, platform=_platform) + else: + self.adapter.job.logger.error( + f"Model to represent Software version not found so skipping update of ValidatedSoftware for {self.software}." + ) + return None + self.adapter.job.logger.info(f"Updating Validated Software - {self} with attrs {attrs}.") _update_validated_software = ORMValidatedSoftware.objects.get( software=_software, start=self.valid_since, end=self.valid_until ) @@ -2456,11 +2551,7 @@ def update(self, attrs): def delete(self): """Delete ValidatedSoftware in Nautobot from NautobotValidatedSoftware object.""" try: - _platform = ORMPlatform.objects.get(name=self.platform) - _software = ORMSoftware.objects.get(version=self.software_version, device_platform=_platform) - _validated_software = ORMValidatedSoftware.objects.get( - software=_software, start=self.valid_since, end=self.valid_until - ) + _validated_software = ORMValidatedSoftware.objects.get(id=self.uuid) super().delete() _validated_software.delete() return self diff --git a/nautobot_ssot/integrations/bootstrap/fixtures/global_settings.yml b/nautobot_ssot/integrations/bootstrap/fixtures/global_settings.yml index f938033a0..fe0177f59 100755 --- a/nautobot_ssot/integrations/bootstrap/fixtures/global_settings.yml +++ b/nautobot_ssot/integrations/bootstrap/fixtures/global_settings.yml @@ -151,6 +151,12 @@ platform: napalm_driver: "" napalm_arguments: {} description: "Arista Devices" + - name: "linux" + manufacturer: "" + network_driver: "" + napalm_driver: "" + napalm_arguments: {} + description: "Linux Devices" location_type: - name: "Region" parent: "" @@ -248,6 +254,23 @@ location: contact_phone: "" contact_email: "" tags: [] + - name: "Southwest" + location_type: "Region" + parent: "" + status: "Active" + facility: "OR1" + asn: + time_zone: "" + description: "" + tenant: "" + physical_address: "" + shipping_address: "" + latitude: + longitude: + contact_name: "" + contact_phone: "" + contact_email: "" + tags: [] team: - name: "Datacenter" phone: "123-456-7890" diff --git a/nautobot_ssot/integrations/bootstrap/jobs.py b/nautobot_ssot/integrations/bootstrap/jobs.py index 60d13e0f2..5fb8a319b 100644 --- a/nautobot_ssot/integrations/bootstrap/jobs.py +++ b/nautobot_ssot/integrations/bootstrap/jobs.py @@ -157,4 +157,4 @@ def run(self, read_destination, dryrun, memory_profiling, debug, *args, **kwargs super().run(dryrun=self.dryrun, memory_profiling=self.memory_profiling, *args, **kwargs) -jobs = [BootstrapDataSource, BootstrapDataTarget] +jobs = [BootstrapDataSource] diff --git a/nautobot_ssot/integrations/bootstrap/signals.py b/nautobot_ssot/integrations/bootstrap/signals.py index 07c6d144f..9f57f844e 100644 --- a/nautobot_ssot/integrations/bootstrap/signals.py +++ b/nautobot_ssot/integrations/bootstrap/signals.py @@ -16,7 +16,7 @@ def register_signals(sender): nautobot_database_ready.connect(nautobot_database_ready_callback, sender=sender) -def nautobot_database_ready_callback(sender, *, apps, **kwargs): # pylint: disable=unused-argument +def nautobot_database_ready_callback(sender, *, apps, **kwargs): # pylint: disable=unused-argument, too-many-statements """Adds OS Version and Physical Address CustomField to Devices and System of Record and Last Sync'd to Device, and IPAddress. Callback function triggered by the nautobot_database_ready signal when the Nautobot database is fully ready. @@ -49,11 +49,6 @@ def nautobot_database_ready_callback(sender, *, apps, **kwargs): # pylint: disa GitRepository = apps.get_model("extras", "GitRepository") Role = apps.get_model("extras", "Role") - if LIFECYCLE_MGMT: - SoftwareLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareLCM") - SoftwareImageLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareImageLCM") - ValidatedSoftwareLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "ValidatedSoftwareLCM") - signal_to_model_mapping = { "manufacturer": Manufacturer, "platform": Platform, @@ -83,20 +78,30 @@ def nautobot_database_ready_callback(sender, *, apps, **kwargs): # pylint: disa } if LIFECYCLE_MGMT: - signal_to_model_mapping.update( - { - "software": SoftwareLCM, - "software_image": SoftwareImageLCM, - "validated_software": ValidatedSoftwareLCM, - } - ) + try: + SoftwareLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareLCM") + signal_to_model_mapping["software"] = SoftwareLCM + except LookupError as err: + print(f"Unable to find SoftwareLCM model from Device Lifecycle Management App. {err}") + try: + SoftwareImageLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "SoftwareImageLCM") + signal_to_model_mapping["software_image"] = SoftwareImageLCM + except LookupError as err: + print(f"Unable to find SoftwareImageLCM model from Device Lifecycle Management App. {err}") + try: + ValidatedSoftwareLCM = apps.get_model("nautobot_device_lifecycle_mgmt", "ValidatedSoftwareLCM") + signal_to_model_mapping["validated_software"] = ValidatedSoftwareLCM + except LookupError as err: + print(f"Unable to find ValidatedSoftwareLCM model from Device Lifecycle Management App. {err}") sync_custom_field, _ = create_or_update_custom_field( + apps, key="last_synced_from_sor", field_type=CustomFieldTypeChoices.TYPE_DATE, label="Last sync from System of Record", ) sor_custom_field, _ = create_or_update_custom_field( + apps, key="system_of_record", field_type=CustomFieldTypeChoices.TYPE_TEXT, label="System of Record", diff --git a/nautobot_ssot/integrations/citrix_adm/__init__.py b/nautobot_ssot/integrations/citrix_adm/__init__.py new file mode 100644 index 000000000..7f3762e58 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/__init__.py @@ -0,0 +1 @@ +"""Base module for Citrix ADM integration.""" diff --git a/nautobot_ssot/integrations/citrix_adm/constants.py b/nautobot_ssot/integrations/citrix_adm/constants.py new file mode 100644 index 000000000..4e8400a31 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/constants.py @@ -0,0 +1,3 @@ +"""Constants for use within Nautobot SSoT for Citrix ADM.""" + +DEVICETYPE_MAP = {"nsvpx": "NetScaler ADC VPX"} diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/__init__.py b/nautobot_ssot/integrations/citrix_adm/diffsync/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/__init__.py b/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/__init__.py new file mode 100644 index 000000000..2b5b50055 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/__init__.py @@ -0,0 +1 @@ +"""Adapter classes for loading DiffSyncModels with data from Citrix ADM or Nautobot.""" diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/citrix_adm.py b/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/citrix_adm.py new file mode 100644 index 000000000..ff51d96a2 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/citrix_adm.py @@ -0,0 +1,329 @@ +"""Nautobot SSoT Citrix ADM Adapter for Citrix ADM SSoT plugin.""" + +import ipaddress +from decimal import Decimal +from typing import List, Optional + +from diffsync import Adapter +from diffsync.exceptions import ObjectNotFound +from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices +from nautobot.extras.models import ExternalIntegration, Job +from nautobot.tenancy.models import Tenant + +from nautobot_ssot.integrations.citrix_adm.constants import DEVICETYPE_MAP +from nautobot_ssot.integrations.citrix_adm.diffsync.models.citrix_adm import ( + CitrixAdmAddress, + CitrixAdmDatacenter, + CitrixAdmDevice, + CitrixAdmIPAddressOnInterface, + CitrixAdmOSVersion, + CitrixAdmPort, + CitrixAdmSubnet, +) +from nautobot_ssot.integrations.citrix_adm.utils.citrix_adm import ( + CitrixNitroClient, + parse_nsip6s, + parse_nsips, + parse_version, + parse_vlan_bindings, +) +from nautobot_ssot.utils import parse_hostname_for_role + + +class CitrixAdmAdapter(Adapter): # pylint: disable=too-many-instance-attributes + """DiffSync adapter for Citrix ADM.""" + + datacenter = CitrixAdmDatacenter + osversion = CitrixAdmOSVersion + device = CitrixAdmDevice + address = CitrixAdmAddress + prefix = CitrixAdmSubnet + port = CitrixAdmPort + ip_on_intf = CitrixAdmIPAddressOnInterface + + top_level = ["datacenter", "osversion", "device", "prefix", "address", "ip_on_intf"] + + def __init__( + self, + job: Job, + instances: List[ExternalIntegration], + sync=None, + tenant: Optional[Tenant] = None, + ): + """Initialize Citrix ADM. + + Args: + job (Job): Citrix ADM job. + instances (List[ExternalIntegration]): ExternalIntegrations defining Citrix ADM instances. + sync (object, optional): Citrix ADM DiffSync. Defaults to None. + tenant (Tenant, optional): Name of Tenant to associate Devices and IP Addresses with. + """ + super().__init__() + self.job = job + self.sync = sync + self.instances = instances + self.conn = None + self.tenant = tenant + self.adm_site_map = {} + self.adm_device_map = {} + + def create_site_map(self): + """Create mapping of ADM Datacenters to information about the Datacenter.""" + sites = self.conn.get_sites() + for site in sites: + self.adm_site_map[site["id"]] = site + + def load_site(self, site_info: dict): + """Load sites from Citrix ADM into DiffSync models. + + Args: + site_info (dict): Dictionary containing information about Datacenter to be imported. + """ + if not site_info.get("name"): + self.job.logger.error(f"Site is missing name so won't be loaded. {site_info}") + else: + site_name = site_info["name"] + if self.job.location_map and site_name in self.job.location_map: + parent_loc = self.job.location_map[site_name]["parent"] + if "name" in self.job.location_map[site_name]: + site_name = self.job.location_map[site_name]["name"] + elif self.job.parent_location: + parent_loc = self.job.parent_location.name + elif site_info.get("region"): + parent_loc = site_info["region"] + if ( + self.job.location_map + and parent_loc in self.job.location_map + and "name" in self.job.location_map[parent_loc] + ): + parent_loc = self.job.location_map[parent_loc]["name"] + else: + parent_loc = "Global" + _, loaded = self.get_or_instantiate( + self.datacenter, + ids={"name": site_name, "region": parent_loc}, + attrs={ + "latitude": float(round(Decimal(site_info["latitude"] if site_info["latitude"] else 0.0), 6)), + "longitude": float(round(Decimal(site_info["longitude"] if site_info["longitude"] else 0.0), 6)), + "uuid": None, + }, + ) + if loaded and self.job.debug: + self.job.logger.info(f"Loaded Datacenter from Citrix ADM: {site_name}") + + def load_devices(self): + """Load devices from Citrix ADM into DiffSync models.""" + devices = self.conn.get_devices() + for dev in devices: + if not dev.get("hostname"): + self.job.logger.warning(f"Device without hostname will not be loaded. {dev}") + continue + try: + found_dev = self.get(self.device, dev["hostname"]) + if found_dev: + self.job.logger.warning(f"Duplicate Device attempting to be loaded: {dev['hostname']}") + except ObjectNotFound: + site = self.adm_site_map[dev["datacenter_id"]] + self.load_site(site_info=site) + site_name = site["name"] + if ( + self.job.location_map + and site_name in self.job.location_map + and "name" in self.job.location_map[site_name] + ): + site_name = self.job.location_map[site_name]["name"] + role = parse_hostname_for_role( + hostname_map=self.job.hostname_mapping, + device_hostname=dev["hostname"], + default_role="Load-Balancer", + ) + version = parse_version(dev["version"]) + self.get_or_instantiate(self.osversion, ids={"version": version}, attrs={}) + new_dev = self.device( + name=dev["hostname"], + model=DEVICETYPE_MAP[dev["type"]] if dev["type"] in DEVICETYPE_MAP else dev["type"], + role=role, + serial=dev["serialnumber"], + site=site_name, + status="Active" if dev["instance_state"] == "Up" else "Offline", + tenant=self.tenant.name if self.tenant else None, + version=version, + uuid=None, + hanode=dev["ha_ip_address"], + ) + self.add(new_dev) + self.adm_device_map[dev["hostname"]] = dev + + def create_port_map(self): + """Create a port/vlan/ip map for each ADC instance.""" + self.job.logger.info("Retrieving NSIP and port bindings from ADC instances.") + for _, adc in self.adm_device_map.items(): + vlan_bindings = self.conn.get_vlan_bindings(adc) + nsips = self.conn.get_nsip(adc) + nsip6s = self.conn.get_nsip6(adc) + + ports = parse_vlan_bindings(vlan_bindings, adc, self.job) + ports = parse_nsips(nsips, ports, adc) + ports = parse_nsip6s(nsip6s, ports) + + self.adm_device_map[adc["hostname"]]["ports"] = ports + + def load_ports(self): + """Load ports from Citrix ADM into DiffSync models.""" + for _, adc in self.adm_device_map.items(): + for port in adc["ports"]: + try: + self.get(self.port, {"name": port["port"], "device": adc["hostname"]}) + except ObjectNotFound: + dev = self.get(self.device, adc["hostname"]) + new_port = self.add_port( + dev_name=adc["hostname"], + port_name=port["port"], + port_status="ENABLED", + description="", + ) + dev.add_child(new_port) + + def load_addresses(self): + """Load addresses from Citrix ADC instances into Diffsync models.""" + for _, adc in self.adm_device_map.items(): + for port in adc["ports"]: + if port.get("ipaddress"): + addr = f"{port['ipaddress']}/{port['netmask']}" + prefix = ipaddress.ip_interface(addr).network.with_prefixlen + self.load_prefix(prefix=prefix) + _tags = port["tags"] if port.get("tags") else [] + if len(_tags) > 1: + _tags.sort() + _primary = bool("MGMT" in _tags or "MIP" in _tags) + self.load_address( + address=addr, + prefix=prefix, + tags=_tags, + ) + self.load_address_to_interface( + address=addr, + device=adc["hostname"], + port=port["port"], + primary=_primary, + ) + + def add_port( + self, dev_name: str, port_name: str = "Management", port_status: str = "ENABLED", description: str = "" + ): + """Method to add Port DiffSync model. + + Args: + dev_name (str): Name of device port is attached to. + port_name (str, optional): Name of port to create. Defaults to "Management". + port_status (str, optional): Status of port to create. Defaults to "ENABLED". + description (str, optional): Description for port. Defaults to "". + + Returns: + CitrixAdmPort: DiffSync model for Port that was loaded. + """ + new_port = self.port( + name=port_name, + device=dev_name, + status="Active" if port_status == "ENABLED" else "Offline", + description=description, + uuid=None, + ) + self.add(new_port) + return new_port + + def load_prefix(self, prefix: str): + """Load CitrixAdmSubnet DiffSync model with specified data. + + Args: + prefix (str): Prefix to be loaded. + """ + if self.tenant: + namespace = self.tenant.name + else: + namespace = "Global" + try: + self.get(self.prefix, {"prefix": prefix, "namespace": namespace}) + except ObjectNotFound: + new_pf = self.prefix( + prefix=prefix, + namespace=namespace, + tenant=self.tenant.name if self.tenant else None, + uuid=None, + ) + self.add(new_pf) + + def load_address(self, address: str, prefix: str, tags: Optional[list] = None): + """Load CitrixAdmAddress DiffSync model with specified data. + + Args: + address (str): IP Address to be loaded. + prefix (str): Prefix that IP Address resides in. + device (str): Device that IP resides on. + port (str): Interface that IP is configured on. + primary (str): Whether the IP is primary IP for assigned device. Defaults to False. + tags (list): List of tags assigned to IP. Defaults to None. + """ + try: + self.get(self.address, {"address": address, "prefix": prefix}) + except ObjectNotFound: + new_addr = self.address( + address=address, + prefix=prefix, + tenant=self.tenant.name if self.tenant else None, + uuid=None, + tags=tags if tags else [], + ) + self.add(new_addr) + + def load_address_to_interface(self, address: str, device: str, port: str, primary: bool = False): + """Load CitrixAdmIPAddressOnInterface DiffSync model with specified data. + + Args: + address (str): IP Address in mapping. + device (str): Device that IP resides on. + port (str): Interface that IP is configured on. + primary (str): Whether the IP is primary IP for assigned device. Defaults to False. + """ + try: + self.get(self.ip_on_intf, {"address": address, "device": device, "port": port}) + except ObjectNotFound: + new_map = self.ip_on_intf(address=address, device=device, port=port, primary=primary, uuid=None) + self.add(new_map) + + def load(self): + """Load data from Citrix ADM into DiffSync models.""" + for instance in self.instances: + self.job.logger.info(f"Loading data from {instance.name}.") + if instance.secrets_group is not None: + _sg = instance.secrets_group + instance_username = _sg.get_secret_value( + access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP, + secret_type=SecretsGroupSecretTypeChoices.TYPE_USERNAME, + ) + instance_password = _sg.get_secret_value( + access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP, + secret_type=SecretsGroupSecretTypeChoices.TYPE_PASSWORD, + ) + self.conn = CitrixNitroClient( + base_url=instance.remote_url, + user=instance_username, + password=instance_password, + verify=instance.verify_ssl, + job=self.job, + ) + self.conn.login() + self.adm_site_map = {} + self.adm_device_map = {} + + self.create_site_map() + self.load_devices() + self.create_port_map() + self.load_ports() + self.load_addresses() + + self.conn.logout() + else: + self.job.logger.warning( + f"Missing SecretsGroup definition for {instance.name}. This must be defined so we can authenticate instance." + ) diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/nautobot.py new file mode 100644 index 000000000..ea349a69b --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/diffsync/adapters/nautobot.py @@ -0,0 +1,209 @@ +"""Nautobot Adapter for Citrix ADM SSoT plugin.""" + +from collections import defaultdict +from typing import Optional + +from diffsync import Adapter +from diffsync.enum import DiffSyncModelFlags +from diffsync.exceptions import ObjectNotFound +from django.db.models import ProtectedError +from nautobot.dcim.models import Device as OrmDevice +from nautobot.dcim.models import Interface, Location, SoftwareVersion +from nautobot.extras.models import Job +from nautobot.ipam.models import IPAddress, IPAddressToInterface, Prefix +from nautobot.tenancy.models import Tenant + +from nautobot_ssot.integrations.citrix_adm.diffsync.models.nautobot import ( + NautobotAddress, + NautobotDatacenter, + NautobotDevice, + NautobotIPAddressOnInterface, + NautobotOSVersion, + NautobotPort, + NautobotSubnet, +) +from nautobot_ssot.integrations.citrix_adm.utils import nautobot + + +class NautobotAdapter(Adapter): # pylint: disable=too-many-instance-attributes + """DiffSync adapter for Nautobot.""" + + datacenter = NautobotDatacenter + osversion = NautobotOSVersion + device = NautobotDevice + port = NautobotPort + prefix = NautobotSubnet + address = NautobotAddress + ip_on_intf = NautobotIPAddressOnInterface + + top_level = ["datacenter", "osversion", "device", "prefix", "address", "ip_on_intf"] + + def __init__(self, job: Job, sync=None, tenant: Optional[Tenant] = None): + """Initialize Nautobot. + + Args: + job (Job): Nautobot job. + sync (object, optional): Nautobot DiffSync. Defaults to None. + tenant (Tenant, optional): Tenant to associate imported objects with. Used to filter loaded objects. + """ + super().__init__() + self.job = job + self.sync = sync + self.tenant = tenant + self.objects_to_delete = defaultdict(list) + + def load_sites(self): + """Load Sites from Nautobot into DiffSync models.""" + for site in Location.objects.filter(location_type=self.job.dc_loctype): + _, loaded = self.get_or_instantiate( + self.datacenter, + ids={"name": site.name, "region": site.parent.name if site.parent else None}, + attrs={ + "latitude": float(round(site.latitude, 6)) if site.latitude else None, + "longitude": float(round(site.longitude, 6)) if site.longitude else None, + "uuid": site.id, + }, + ) + if loaded and self.job.debug: + self.job.logger.info(f"Loaded {self.job.dc_loctype.name} {site.name} from Nautobot.") + + def load_devices(self): + """Load Devices from Nautobot into DiffSync models.""" + if self.tenant: + devices = OrmDevice.objects.select_related("device_type", "location", "status").filter(tenant=self.tenant) + else: + devices = OrmDevice.objects.select_related("device_type", "location", "status").filter( + _custom_field_data__system_of_record="Citrix ADM" + ) + for dev in devices: + if self.job.debug: + self.job.logger.info(f"Loading Device {dev.name} from Nautobot.") + hanode = dev.custom_field_data.get("ha_node") + new_dev = self.device( + name=dev.name, + model=dev.device_type.model, + role=dev.role.name, + serial=dev.serial, + site=dev.location.name, + status=dev.status.name, + tenant=dev.tenant.name if dev.tenant else "", + version=dev.software_version.version if dev.software_version else None, + uuid=dev.id, + hanode=hanode, + ) + if self.tenant: + new_dev.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST + self.add(new_dev) + + def load_software_versions(self): + """Load Citrix SoftwareVersions from Nautobot into DiffSync models.""" + for osver in SoftwareVersion.objects.filter(platform__manufacturer__name__icontains="Citrix"): + self.get_or_instantiate( + self.osversion, + ids={"version": osver.version}, + attrs={"uuid": osver.id}, + ) + + def load_ports(self): + """Load Interfaces from Nautobot into DiffSync models.""" + if self.tenant: + interfaces = Interface.objects.select_related("device", "status").filter(device__tenant=self.tenant) + else: + interfaces = Interface.objects.select_related("device", "status").filter( + device___custom_field_data__system_of_record="Citrix ADM" + ) + for intf in interfaces: + try: + dev = self.get(self.device, intf.device.name) + new_intf = self.port( + name=intf.name, + device=intf.device.name, + status=intf.status.name, + description=intf.description, + uuid=intf.id, + ) + if self.tenant: + new_intf.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST + self.add(new_intf) + dev.add_child(new_intf) + except ObjectNotFound: + self.job.logger.warning( + f"Unable to find {intf.device.name} loaded so skipping loading port {intf.name}." + ) + + def load_prefixes(self): + """Load Prefixes from Nautobot into DiffSync models.""" + if self.tenant: + prefixes = Prefix.objects.filter(tenant=self.tenant) + else: + prefixes = Prefix.objects.filter(_custom_field_data__system_of_record="Citrix ADM") + for _pf in prefixes: + new_pf = self.prefix( + prefix=str(_pf.prefix), + namespace=_pf.namespace.name, + tenant=_pf.tenant.name if _pf.tenant else None, + uuid=_pf.id, + ) + if self.tenant: + new_pf.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST + self.add(new_pf) + + def load_addresses(self): + """Load IP Addresses from Nautobot into DiffSync models.""" + if self.tenant: + addresses = IPAddress.objects.filter(tenant=self.tenant) + else: + addresses = IPAddress.objects.filter(_custom_field_data__system_of_record="Citrix ADM") + for addr in addresses: + new_ip = self.address( + address=str(addr.address), + prefix=str(addr.parent.prefix), + tenant=addr.tenant.name if addr.tenant else None, + uuid=addr.id, + tags=nautobot.get_tag_strings(addr.tags), + ) + if self.tenant: + new_ip.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST + self.add(new_ip) + for mapping in IPAddressToInterface.objects.filter(ip_address=addr): + new_mapping = self.ip_on_intf( + address=str(addr.address), + device=mapping.interface.device.name, + port=mapping.interface.name, + primary=len(addr.primary_ip4_for.all()) > 0 or len(addr.primary_ip6_for.all()) > 0, + uuid=mapping.id, + ) + if self.tenant: + new_mapping.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST + self.add(new_mapping) + + def sync_complete(self, source: Adapter, *args, **kwargs): + """Label and clean up function for DiffSync sync. + + Once the sync is complete, this function labels all imported objects and then + deletes any objects from Nautobot that need to be deleted in a specific order. + + Args: + source: The DiffSync whose data was used to update this instance. + *args: Positional arguments. + **kwargs: Keyword arguments. + """ + for grouping in ["addresses", "prefixes", "ports", "devices"]: + for nautobot_obj in self.objects_to_delete[grouping]: + try: + if self.job.debug: + self.job.logger.info(f"Deleting {nautobot_obj}.") + nautobot_obj.delete() + except ProtectedError: + self.job.logger.info(f"Deletion failed protected object: {nautobot_obj}") + self.objects_to_delete[grouping] = [] + return super().sync_complete(source, *args, **kwargs) + + def load(self): + """Load data from Nautobot into DiffSync models.""" + self.load_sites() + self.load_devices() + self.load_software_versions() + self.load_ports() + self.load_prefixes() + self.load_addresses() diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/models/__init__.py b/nautobot_ssot/integrations/citrix_adm/diffsync/models/__init__.py new file mode 100644 index 000000000..b2a1f01c7 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/diffsync/models/__init__.py @@ -0,0 +1 @@ +"""DiffSync models and adapters for the Citrix ADM SSoT plugin.""" diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/models/base.py b/nautobot_ssot/integrations/citrix_adm/diffsync/models/base.py new file mode 100644 index 000000000..645f49b85 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/diffsync/models/base.py @@ -0,0 +1,133 @@ +# pylint: disable=duplicate-code +"""DiffSyncModel subclasses for Nautobot-to-Citrix ADM data sync.""" + +from typing import List, Optional +from uuid import UUID + +from diffsync import DiffSyncModel +from diffsync.enum import DiffSyncModelFlags + + +class Datacenter(DiffSyncModel): + """Diffsync model for Citrix ADM datacenters.""" + + model_flags: DiffSyncModelFlags = DiffSyncModelFlags.SKIP_UNMATCHED_DST + + _modelname = "datacenter" + _identifiers = ( + "name", + "region", + ) + _attributes = ("latitude", "longitude") + + name: str + region: Optional[str] = None + latitude: Optional[float] = None + longitude: Optional[float] = None + uuid: Optional[UUID] = None + + +class OSVersion(DiffSyncModel): + """DiffSync model for Citrix ADM device OS versions.""" + + _modelname = "osversion" + _identifiers = ("version",) + _attributes = () + + version: str + + uuid: Optional[UUID] = None + + +class Device(DiffSyncModel): + """DiffSync model for Citrix ADM devices.""" + + _modelname = "device" + _identifiers = ("name",) + _attributes = ( + "model", + "role", + "serial", + "site", + "status", + "tenant", + "version", + "hanode", + ) + _children = {"port": "ports"} + + name: str + model: Optional[str] = None + role: str + serial: Optional[str] = None + site: Optional[str] = None + status: Optional[str] = None + tenant: Optional[str] = None + version: Optional[str] = None + ports: Optional[List["Port"]] = [] + hanode: Optional[str] = None + + uuid: Optional[UUID] = None + + +class Port(DiffSyncModel): + """DiffSync model for Citrix ADM device interfaces.""" + + _modelname = "port" + _identifiers = ("name", "device") + _attributes = ("status", "description") + _children = {} + + name: str + device: str + status: str + description: Optional[str] = None + + uuid: Optional[UUID] = None + + +class Subnet(DiffSyncModel): + """DiffSync model for Citrix ADM management prefixes.""" + + _modelname = "prefix" + _identifiers = ("prefix", "namespace") + _attributes = ("tenant",) + _children = {} + + prefix: str + namespace: str + tenant: Optional[str] = None + + uuid: Optional[UUID] = None + + +class Address(DiffSyncModel): + """DiffSync model for Citrix ADM IP Addresses.""" + + _modelname = "address" + _identifiers = ("address", "prefix") + _attributes = ("tenant", "tags") + _children = {} + + address: str + prefix: str + tenant: Optional[str] = None + tags: Optional[list] = None + + uuid: Optional[UUID] = None + + +class IPAddressOnInterface(DiffSyncModel): + """DiffSync model for Citrix ADM tracking IPAddress on particular Device interfaces.""" + + _modelname = "ip_on_intf" + _identifiers = ("address", "device", "port") + _attributes = ("primary",) + _children = {} + + address: str + device: str + port: str + primary: bool + + uuid: Optional[UUID] = None diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/models/citrix_adm.py b/nautobot_ssot/integrations/citrix_adm/diffsync/models/citrix_adm.py new file mode 100644 index 000000000..4eb581205 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/diffsync/models/citrix_adm.py @@ -0,0 +1,130 @@ +"""Nautobot SSoT Citrix ADM DiffSync models for Nautobot SSoT Citrix ADM SSoT.""" + +from nautobot_ssot.integrations.citrix_adm.diffsync.models.base import ( + Address, + Datacenter, + Device, + IPAddressOnInterface, + OSVersion, + Port, + Subnet, +) + + +class CitrixAdmDatacenter(Datacenter): + """Citrix ADM implementation of Datacenter DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Site in Citrix ADM from Datacenter object.""" + raise NotImplementedError + + def update(self, attrs): + """Update Site in Citrix ADM from Datacenter object.""" + raise NotImplementedError + + def delete(self): + """Delete Site in Citrix ADM from Datacenter object.""" + raise NotImplementedError + + +class CitrixAdmOSVersion(OSVersion): + """Citrix ADM implementation of OSVersion DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create OSVersion in Citrix ADM from OSVersion object.""" + raise NotImplementedError + + def update(self, attrs): + """Update OSVersion in Citrix ADM from OSVersion object.""" + raise NotImplementedError + + def delete(self): + """Delete OSVersion in Citrix ADM from OSVersion object.""" + raise NotImplementedError + + +class CitrixAdmDevice(Device): + """Citrix ADM implementation of Device DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Device in Citrix ADM from Device object.""" + raise NotImplementedError + + def update(self, attrs): + """Update Device in Citrix ADM from Device object.""" + raise NotImplementedError + + def delete(self): + """Delete Device in Citrix ADM from Device object.""" + raise NotImplementedError + + +class CitrixAdmPort(Port): + """Citrix ADM implementation of Port DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Interface in Citrix ADM from Port object.""" + raise NotImplementedError + + def update(self, attrs): + """Update Interface in Citrix ADM from Port object.""" + raise NotImplementedError + + def delete(self): + """Delete Interface in Citrix ADM from Port object.""" + raise NotImplementedError + + +class CitrixAdmSubnet(Subnet): + """Citrix ADM implementation of Subnet DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Prefix in Citrix ADM from Subnet object.""" + raise NotImplementedError + + def update(self, attrs): + """Update Prefix in Citrix ADM from Subnet object.""" + raise NotImplementedError + + def delete(self): + """Delete Prefix in Citrix ADM from Subnet object.""" + raise NotImplementedError + + +class CitrixAdmAddress(Address): + """Citrix ADM implementation of Address DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create IP Address in Citrix ADM from Address object.""" + raise NotImplementedError + + def update(self, attrs): + """Update IP Address in Citrix ADM from Address object.""" + raise NotImplementedError + + def delete(self): + """Delete IP Address in Citrix ADM from Address object.""" + raise NotImplementedError + + +class CitrixAdmIPAddressOnInterface(IPAddressOnInterface): + """Citrix ADM implementation of IPAddressOnInterface DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create IPAddressToInterface in Citrix ADM from CitrixAdmIPAddressOnInterface object.""" + raise NotImplementedError + + def update(self, attrs): + """Update IPAddressToInterface in Citrix ADM from CitrixAdmIPAddressOnInterface object.""" + raise NotImplementedError + + def delete(self): + """Delete IPAddressToInterface in Citrix ADM from CitrixAdmIPAddressOnInterface object.""" + raise NotImplementedError diff --git a/nautobot_ssot/integrations/citrix_adm/diffsync/models/nautobot.py b/nautobot_ssot/integrations/citrix_adm/diffsync/models/nautobot.py new file mode 100644 index 000000000..4adecf80f --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/diffsync/models/nautobot.py @@ -0,0 +1,346 @@ +# pylint: disable=duplicate-code +"""Nautobot DiffSync models for Citrix ADM SSoT.""" + +from datetime import datetime + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from nautobot.dcim.models import Device as NewDevice +from nautobot.dcim.models import DeviceType, Interface, Location, Manufacturer, Platform, SoftwareVersion +from nautobot.extras.models import Role, Status, Tag +from nautobot.ipam.models import IPAddress, IPAddressToInterface, Namespace, Prefix +from nautobot.tenancy.models import Tenant + +from nautobot_ssot.integrations.citrix_adm.diffsync.models.base import ( + Address, + Datacenter, + Device, + IPAddressOnInterface, + OSVersion, + Port, + Subnet, +) + + +class NautobotDatacenter(Datacenter): + """Nautobot implementation of Citrix ADM Datacenter model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Site in Nautobot from NautobotDatacenter object.""" + status_active = Status.objects.get(name="Active") + parent_loc = None + if adapter.job.dc_loctype.parent and ids.get("region"): + parent_loc = Location.objects.get_or_create( + name=ids["region"], location_type=adapter.job.dc_loctype.parent, status=status_active + )[0] + if Location.objects.filter(name=ids["name"]).exists(): + adapter.job.logger.warning(f"Site {ids['name']} already exists so skipping creation.") + return None + new_site = Location( + name=ids["name"], + parent=parent_loc, + status=status_active, + latitude=attrs["latitude"], + longitude=attrs["longitude"], + location_type=adapter.job.dc_loctype, + ) + new_site.validated_save() + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update Site in Nautobot from NautobotDatacenter object.""" + if not settings.PLUGINS_CONFIG.get("nautobot_ssot").get("citrix_adm_update_sites"): + self.adapter.job.logger.warning(f"Update sites setting is disabled so skipping updating {self.name}.") + return None + site = Location.objects.get(id=self.uuid) + if "latitude" in attrs: + site.latitude = attrs["latitude"] + if "longitude" in attrs: + site.longitude = attrs["longitude"] + site.validated_save() + return super().update(attrs) + + +class NautobotOSVersion(OSVersion): + """Nautobot implementation of Citrix ADM Device model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create SoftwareVersion in Nautobot from NautobotOSVersion object.""" + new_ver = SoftwareVersion( + version=ids["version"], + platform=Platform.objects.get(name="citrix.adc"), + status=Status.objects.get(name="Active"), + ) + new_ver.validated_save() + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def delete(self): + """Delete SoftwareVersion in Nautobot from NautobotOSVersion object.""" + ver = SoftwareVersion.objects.get(id=self.uuid) + super().delete() + ver.delete() + return self + + +class NautobotDevice(Device): + """Nautobot implementation of Citrix ADM Device model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Device in Nautobot from NautobotDevice object.""" + lb_role, created = Role.objects.get_or_create(name=attrs["role"]) + if created: + lb_role.content_types.add(ContentType.objects.get_for_model(NewDevice)) + lb_dt, _ = DeviceType.objects.get_or_create( + model=attrs["model"], manufacturer=Manufacturer.objects.get(name="Citrix") + ) + citrix_platform = Platform.objects.get(name="citrix.adc") + new_device = NewDevice( + name=ids["name"], + status=Status.objects.get(name=attrs["status"]), + role=lb_role, + location=Location.objects.get(name=attrs["site"], location_type=adapter.job.dc_loctype), + device_type=lb_dt, + serial=attrs["serial"], + platform=citrix_platform, + ) + if attrs.get("tenant"): + new_device.tenant = Tenant.objects.update_or_create(name=attrs["tenant"])[0] + if attrs.get("version"): + new_device.software_version = SoftwareVersion.objects.get_or_create( + version=attrs["version"], platform=citrix_platform + )[0] + if attrs.get("hanode"): + new_device.custom_field_data["ha_node"] = attrs["hanode"] + new_device.custom_field_data["system_of_record"] = "Citrix ADM" + new_device.custom_field_data["last_synced_from_sor"] = datetime.today().date().isoformat() + new_device.validated_save() + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update Device in Nautobot from NautobotDevice object.""" + device = NewDevice.objects.get(id=self.uuid) + if "model" in attrs: + device.device_type, _ = DeviceType.objects.get_or_create( + model=attrs["model"], manufacturer=Manufacturer.objects.get(name="Citrix") + ) + if "status" in attrs: + device.status = Status.objects.get(name=attrs["status"]) + if "role" in attrs: + device.role = Role.objects.get_or_create(name=attrs["role"])[0] + if "serial" in attrs: + device.serial = attrs["serial"] + if "site" in attrs: + device.location = Location.objects.get(name=attrs["site"]) + if "tenant" in attrs: + if attrs.get("tenant"): + device.tenant = Tenant.objects.update_or_create(name=attrs["tenant"])[0] + else: + device.tenant = None + if "version" in attrs: + if attrs.get("version"): + device.software_version = SoftwareVersion.objects.get_or_create( + version=attrs["version"], platform=Platform.objects.get(name="citrix.adc") + )[0] + else: + device.software_version = None + if "hanode" in attrs: + device.custom_field_data["ha_node"] = attrs["hanode"] + device.custom_field_data["system_of_record"] = "Citrix ADM" + device.custom_field_data["last_synced_from_sor"] = datetime.today().date().isoformat() + device.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Device in Nautobot from NautobotDevice object.""" + dev = NewDevice.objects.get(id=self.uuid) + super().delete() + self.adapter.job.logger.info(f"Deleting Device {dev.name}.") + self.adapter.objects_to_delete["devices"].append(dev) + return self + + +class NautobotPort(Port): + """Nautobot implementation of Citrix ADM Port model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Interface in Nautobot from NautobotPort object.""" + new_port = Interface( + name=ids["name"], + device=NewDevice.objects.get(name=ids["device"]), + status=Status.objects.get(name=attrs["status"]), + description=attrs["description"], + type="virtual", + mgmt_only=bool(ids["name"] == "Management"), + ) + new_port.custom_field_data["system_of_record"] = "Citrix ADM" + new_port.custom_field_data["last_synced_from_sor"] = datetime.today().date().isoformat() + new_port.validated_save() + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update Interface in Nautobot from NautobotPort object.""" + port = Interface.objects.get(self.uuid) + if "status" in attrs: + port.status = Status.objects.get(name=attrs["status"]) + if "description" in attrs: + port.description = attrs["description"] + port.custom_field_data["system_of_record"] = "Citrix ADM" + port.custom_field_data["last_synced_from_sor"] = datetime.today().date().isoformat() + port.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Interface in Nautobot from NautobotPort object.""" + port = Interface.objects.get(id=self.uuid) + super().delete() + self.adapter.job.logger.info(f"Deleting Port {port.name} for {port.device.name}.") + self.adapter.objects_to_delete["ports"].append(port) + return self + + +class NautobotSubnet(Subnet): + """Nautobot implementation of Citrix ADM Subnet model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create Prefix in Nautobot from NautobotSubnet object.""" + namespace = Namespace.objects.get_or_create(name=ids["namespace"])[0] + if adapter.job.debug: + adapter.job.logger.info(f"Creating Prefix {ids['prefix']}.") + _pf = Prefix( + prefix=ids["prefix"], + namespace=namespace, + status=Status.objects.get(name="Active"), + tenant=Tenant.objects.get(name=attrs["tenant"]) if attrs.get("tenant") else None, + ) + _pf.custom_field_data.update({"system_of_record": "Citrix ADM"}) + _pf.custom_field_data.update({"last_synced_from_sor": datetime.today().date().isoformat()}) + _pf.validated_save() + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update IP Address in Nautobot from NautobotAddress object.""" + _pf = Prefix.objects.get(id=self.uuid) + if "tenant" in attrs: + if attrs.get("tenant"): + _pf.tenant = Tenant.objects.get(name=attrs["tenant"]) + else: + _pf.tenant = None + _pf.custom_field_data.update({"system_of_record": "Citrix ADM"}) + _pf.custom_field_data.update({"last_synced_from_sor": datetime.today().date().isoformat()}) + _pf.validated_save() + return super().update(attrs) + + def delete(self): # pylint: disable=inconsistent-return-statements + """Delete Prefix in Nautobot.""" + try: + _pf = Prefix.objects.get(id=self.uuid) + self.adapter.objects_to_delete["prefixes"].append(_pf) + super().delete() + return self + except Prefix.DoesNotExist as err: + if self.adapter.job.debug: + self.adapter.job.logger.warning(f"Unable to find Prefix {self.prefix} {self.uuid} for deletion. {err}") + + +class NautobotAddress(Address): + """Nautobot implementation of Citrix ADM Address model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create IP Address in Nautobot from NautobotAddress object.""" + new_ip = IPAddress( + address=ids["address"], + parent=Prefix.objects.filter(network__net_contains=ids["address"].split("/")[0]).last(), + status=Status.objects.get(name="Active"), + namespace=( + Namespace.objects.get_or_create(name=attrs["tenant"])[0] + if attrs.get("tenant") + else Namespace.objects.get(name="Global") + ), + ) + if attrs.get("tenant"): + new_ip.tenant = Tenant.objects.update_or_create(name=attrs["tenant"])[0] + if attrs.get("tags"): + new_ip.tags.set(attrs["tags"]) + for tag in attrs["tags"]: + new_tag = Tag.objects.get(name=tag) + new_tag.content_types.add(ContentType.objects.get_for_model(NewDevice)) + new_ip.custom_field_data["system_of_record"] = "Citrix ADM" + new_ip.custom_field_data["last_synced_from_sor"] = datetime.today().date().isoformat() + new_ip.validated_save() + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update IP Address in Nautobot from NautobotAddress object.""" + addr = IPAddress.objects.get(id=self.uuid) + if "tenant" in attrs: + if attrs.get("tenant"): + addr.tenant = Tenant.objects.update_or_create(name=attrs["tenant"])[0] + else: + addr.tenant = None + if "tags" in attrs: + addr.tags.set(attrs["tags"]) + for tag in attrs["tags"]: + new_tag = Tag.objects.get(name=tag) + new_tag.content_types.add(ContentType.objects.get_for_model(NewDevice)) + else: + addr.tags.clear() + addr.custom_field_data["system_of_record"] = "Citrix ADM" + addr.custom_field_data["last_synced_from_sor"] = datetime.today().date().isoformat() + addr.validated_save() + return super().update(attrs) + + def delete(self): + """Delete IP Address in Nautobot from NautobotAddress object.""" + addr = IPAddress.objects.get(id=self.uuid) + super().delete() + self.adapter.job.logger.info(f"Deleting IP Address {self}.") + self.adapter.objects_to_delete["addresses"].append(addr) + return self + + +class NautobotIPAddressOnInterface(IPAddressOnInterface): + """Nautobot implementation of Citrix ADM IPAddressOnInterface model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create IPAddressToInterface in Nautobot from IPAddressOnInterface object.""" + new_map = IPAddressToInterface( + ip_address=IPAddress.objects.get(address=ids["address"]), + interface=Interface.objects.get(name=ids["port"], device__name=ids["device"]), + ) + new_map.validated_save() + if attrs.get("primary"): + if new_map.ip_address.ip_version == 4: + new_map.interface.device.primary_ip4 = new_map.ip_address + else: + new_map.interface.device.primary_ip6 = new_map.ip_address + new_map.interface.device.validated_save() + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update IP Address in Nautobot from IPAddressOnInterface object.""" + ip_to_intf = IPAddressToInterface.objects.get(id=self.uuid) + if attrs.get("primary"): + if ip_to_intf.ip_address.ip_version == 4: + ip_to_intf.interface.device.primary_ip4 = ip_to_intf.ip_address + else: + ip_to_intf.interface.device.primary_ip6 = ip_to_intf.ip_address + ip_to_intf.interface.device.validated_save() + ip_to_intf.validated_save() + return super().update(attrs) + + def delete(self): + """Delete IPAddressToInterface in Nautobot from NautobotIPAddressOnInterface object.""" + ip_to_intf = IPAddressToInterface.objects.get(id=self.uuid) + super().delete() + self.adapter.job.logger.info( + f"Deleting IPAddress to Interface mapping between {self.address} and {self.device}'s {self.port} port." + ) + ip_to_intf.delete() + return self diff --git a/nautobot_ssot/integrations/citrix_adm/jobs.py b/nautobot_ssot/integrations/citrix_adm/jobs.py new file mode 100644 index 000000000..cd3001e49 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/jobs.py @@ -0,0 +1,202 @@ +"""Jobs for Citrix ADM SSoT integration.""" + +from ast import literal_eval + +from diffsync.enum import DiffSyncFlags +from django.templatetags.static import static +from django.urls import reverse +from nautobot.core.celery import register_jobs +from nautobot.dcim.models import Location, LocationType +from nautobot.extras.jobs import BooleanVar, Job, JSONVar, MultiObjectVar, ObjectVar, StringVar +from nautobot.extras.models import ExternalIntegration +from nautobot.tenancy.models import Tenant + +from nautobot_ssot.exceptions import JobException +from nautobot_ssot.integrations.citrix_adm.diffsync.adapters import citrix_adm, nautobot +from nautobot_ssot.jobs.base import DataMapping, DataSource, DataTarget + +name = "Citrix ADM SSoT" # pylint: disable=invalid-name + + +class CitrixAdmDataSource(DataSource, Job): # pylint: disable=too-many-instance-attributes + """Citrix ADM SSoT Data Source.""" + + instances = MultiObjectVar( + model=ExternalIntegration, + queryset=ExternalIntegration.objects.all(), + display_field="display", + label="Citrix ADM Instances", + required=True, + ) + dc_loctype = ObjectVar( + model=LocationType, + queryset=LocationType.objects.all(), + query_params={"content_types": "dcim.device"}, + display_field="display", + label="Datacenter LocationType", + description="LocationType to use when importing Datacenters from Citrix ADM. Must have Device ContentType.", + required=True, + ) + parent_location = ObjectVar( + model=Location, + queryset=Location.objects.all(), + query_params={"location_type": "$dc_loctype.parent"}, + display_field="display", + label="Parent Location", + description="Parent Location to assign to imported Datacenters. Required if parent is specified on Datacenter LocationType.", + required=False, + ) + location_map = JSONVar( + label="Location Map", + description="Mapping of Datacenter name to parent and name. Ex: {'US': {'name': 'United States', 'parent': 'North America'}}.", + default={}, + required=False, + ) + hostname_mapping = StringVar( + label="Hostname Mapping", + description="List of tuples containing Device hostname regex patterns to assign to specified Role. ex: [('.*ilb.*', 'Internal Load-Balancer')]", + default=[], + required=False, + ) + tenant = ObjectVar(model=Tenant, queryset=Tenant.objects.all(), display_field="display_name", required=False) + debug = BooleanVar(description="Enable for more verbose debug logging", default=False) + + class Meta: # pylint: disable=too-few-public-methods + """Meta data for Citrix ADM.""" + + name = "Citrix ADM to Nautobot" + data_source = "Citrix ADM" + data_target = "Nautobot" + data_source_icon = static("nautobot_ssot_citrix_adm/citrix_logo.png") + description = "Sync information from Citrix ADM to Nautobot" + field_order = [ + "dryrun", + "debug", + "instances", + "dc_loctype", + "parent_location", + "location_map", + "hostname_mapping", + "tenant", + ] + + def __init__(self): + """Initialize job objects.""" + super().__init__() + self.data = None + self.diffsync_flags = DiffSyncFlags.CONTINUE_ON_FAILURE + + @classmethod + def config_information(cls): + """Dictionary describing the configuration of this DataSource.""" + return {} + + @classmethod + def data_mappings(cls): + """List describing the data mappings involved in this DataSource.""" + return ( + DataMapping("Datacenters", None, "Locations", reverse("dcim:location_list")), + DataMapping("Devices", None, "Devices", reverse("dcim:device_list")), + DataMapping("Ports", None, "Interfaces", reverse("dcim:interface_list")), + DataMapping("Prefixes", None, "Prefixes", reverse("ipam:prefix_list")), + DataMapping("IP Addresses", None, "IP Addresses", reverse("ipam:ipaddress_list")), + ) + + def load_source_adapter(self): + """Load data from Citrix ADM into DiffSync models.""" + self.source_adapter = citrix_adm.CitrixAdmAdapter( + job=self, sync=self.sync, instances=self.instances, tenant=self.tenant + ) + self.source_adapter.load() + + def load_target_adapter(self): + """Load data from Nautobot into DiffSync models.""" + self.target_adapter = nautobot.NautobotAdapter(job=self, sync=self.sync, tenant=self.tenant) + self.target_adapter.load() + + def validate_job_settings(self): + """Validate the settings defined in the Job form are correct.""" + if ( + self.dc_loctype.parent + and not self.parent_location + and (self.location_map and not all(bool("parent" in value) for value in self.location_map.values())) + ): + self.logger.error( + f"{self.dc_loctype.name} requires a parent Location and you've not specified a parent Location. Please review your Job settings." + ) + raise JobException(message=f"Parent Location is required for {self.dc_loctype.name} LocationType.") + + def run( # pylint: disable=arguments-differ, too-many-arguments + self, dryrun, memory_profiling, instances, tenant, debug, *args, **kwargs + ): + """Perform data synchronization.""" + self.instances = instances + self.tenant = tenant + self.debug = debug + self.dryrun = dryrun + self.dc_loctype = kwargs["dc_loctype"] + self.parent_location = kwargs["parent_location"] + self.location_map = kwargs["location_map"] + self.hostname_mapping = literal_eval(kwargs["hostname_mapping"]) + self.validate_job_settings() + self.memory_profiling = memory_profiling + super().run(dryrun=self.dryrun, memory_profiling=self.memory_profiling, *args, **kwargs) + + +class CitrixAdmDataTarget(DataTarget, Job): + """Citrix ADM SSoT Data Target.""" + + instance = ObjectVar( + model=ExternalIntegration, + queryset=ExternalIntegration.objects.all(), + display_field="display", + label="Citrix ADM Instance", + required=True, + ) + tenant = ObjectVar(model=Tenant, queryset=Tenant.objects.all(), display_field="display_name", required=False) + debug = BooleanVar(description="Enable for more verbose debug logging", default=False) + + class Meta: # pylint: disable=too-few-public-methods + """Meta data for Citrix ADM.""" + + name = "Nautobot to Citrix ADM" + data_source = "Nautobot" + data_target = "Citrix ADM" + description = "Sync information from Nautobot to Citrix ADM" + + @classmethod + def config_information(cls): + """Dictionary describing the configuration of this DataTarget.""" + return {} + + @classmethod + def data_mappings(cls): + """List describing the data mappings involved in this DataSource.""" + return () + + def load_source_adapter(self): + """Load data from Nautobot into DiffSync models.""" + self.source_adapter = nautobot.NautobotAdapter(job=self, sync=self.sync, tenant=self.tenant) + self.source_adapter.load() + + def load_target_adapter(self): + """Load data from Citrix ADM into DiffSync models.""" + self.target_adapter = citrix_adm.CitrixAdmAdapter( + job=self, sync=self.sync, instances=self.instance, tenant=self.tenant + ) + self.target_adapter.load() + + def run( # pylint: disable=arguments-differ, too-many-arguments + self, dryrun, memory_profiling, instance, tenant, debug, *args, **kwargs + ): + """Perform data synchronization.""" + self.instance = instance + self.tenant = tenant + self.debug = debug + self.dryrun = dryrun + self.memory_profiling = memory_profiling + super().run(dryrun=self.dryrun, memory_profiling=self.memory_profiling, *args, **kwargs) + + +jobs = [CitrixAdmDataSource] +register_jobs(*jobs) diff --git a/nautobot_ssot/integrations/citrix_adm/signals.py b/nautobot_ssot/integrations/citrix_adm/signals.py new file mode 100644 index 000000000..ab2768658 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/signals.py @@ -0,0 +1,55 @@ +"""Signals triggered when Nautobot starts to perform certain actions.""" + +from nautobot.core.signals import nautobot_database_ready +from nautobot.extras.choices import CustomFieldTypeChoices + +from nautobot_ssot.utils import create_or_update_custom_field + + +def register_signals(sender): + """Register signals for IPFabric integration.""" + nautobot_database_ready.connect(nautobot_database_ready_callback, sender=sender) + + +def nautobot_database_ready_callback(sender, *, apps, **kwargs): # pylint: disable=unused-argument + """Ensure the Citrix Manufacturer is in place for DeviceTypes to use. Adds OS Version CustomField to Devices and System of Record and Last Sync'd to Site, Device, Interface, and IPAddress. + + Callback function triggered by the nautobot_database_ready signal when the Nautobot database is fully ready. + """ + # pylint: disable=invalid-name, too-many-locals + ContentType = apps.get_model("contenttypes", "ContentType") + Manufacturer = apps.get_model("dcim", "Manufacturer") + Device = apps.get_model("dcim", "Device") + Interface = apps.get_model("dcim", "Interface") + Prefix = apps.get_model("ipam", "Prefix") + IPAddress = apps.get_model("ipam", "IPAddress") + Platform = apps.get_model("dcim", "Platform") + + citrix_manu, _ = Manufacturer.objects.update_or_create(name="Citrix") + Platform.objects.update_or_create( + name="citrix.adc", + defaults={ + "name": "citrix.adc", + "napalm_driver": "netscaler", + "manufacturer": citrix_manu, + "network_driver": "citrix_netscaler", + }, + ) + ha_node_field, _ = create_or_update_custom_field( + apps, key="ha_node", field_type=CustomFieldTypeChoices.TYPE_TEXT, label="HA Node" + ) + ha_node_field.content_types.add(ContentType.objects.get_for_model(Device)) + + sor_custom_field = create_or_update_custom_field( + apps, key="system_of_record", field_type=CustomFieldTypeChoices.TYPE_TEXT, label="System of Record" + )[0] + sync_custom_field = create_or_update_custom_field( + apps, + key="last_synced_from_sor", + field_type=CustomFieldTypeChoices.TYPE_DATE, + label="Last sync from System of Record", + )[0] + + for model in [Device, Interface, Prefix, IPAddress]: + sor_custom_field.content_types.add(ContentType.objects.get_for_model(model)) + sync_custom_field.content_types.add(ContentType.objects.get_for_model(model)) diff --git a/nautobot_ssot/integrations/citrix_adm/utils/__init__.py b/nautobot_ssot/integrations/citrix_adm/utils/__init__.py new file mode 100644 index 000000000..05c104799 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/utils/__init__.py @@ -0,0 +1 @@ +"""Utility functions for working with Citrix ADM and Nautobot.""" diff --git a/nautobot_ssot/integrations/citrix_adm/utils/citrix_adm.py b/nautobot_ssot/integrations/citrix_adm/utils/citrix_adm.py new file mode 100644 index 000000000..607de0819 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/utils/citrix_adm.py @@ -0,0 +1,283 @@ +"""Utility functions for working with Citrix ADM.""" + +import re +from typing import List, Optional, Union + +import requests +import urllib3 +from netutils.ip import ipaddress_interface, is_ip_within, netmask_to_cidr + + +# based on client found at https://github.com/slauger/python-nitro +class CitrixNitroClient: + """Client for interacting with Citrix ADM NITRO API.""" + + def __init__( # pylint: disable=too-many-arguments + self, base_url: str, user: str, password: str, job, verify: bool = True + ): + """Initialize NITRO client. + + Args: + base_url (str): Base URL for MAS/ADM API. Must include schema, http(s). + user (str): Username to authenticate with Citrix ADM. + password (str): Password to authenticate with Citrix ADM. + verify (bool, optional): Whether to validate SSL certificate on Citrix ADM or not. Defaults to True. + job (Job): Job logger to notify users of progress. + """ + if base_url.endswith("/"): + base_url = base_url.rstrip("/") + self.url = base_url + self.username = user + self.password = password + self.headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + self.verify = verify + if self.verify is False: + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + self.job = job + + def login(self): + """Login to ADM/MAS and set authorization token to enable further communication.""" + url = "config" + objecttype = "login" + login = {"login": {"username": self.username, "password": self.password}} + payload = f"object={login}" + response = self.request(method="POST", endpoint=url, objecttype=objecttype, data=payload) + if response: + session_id = response["login"][0]["sessionid"] + self.headers["Cookie"] = f"SESSID={session_id}; path=/; SameSite=Lax; secure; HttpOnly" + else: + self.job.logger.error("Error while logging into Citrix ADM. Please validate your configuration is correct.") + raise requests.exceptions.RequestException() + + def logout(self): + """Best practice to logout when session is complete.""" + url = "config" + objecttype = "logout" + logout = {"logout": {"username": self.username, "password": self.password}} + payload = f"object={logout}" + self.headers.pop("_MPS_API_PROXY_MANAGED_INSTANCE_IP", None) + self.headers.pop("_MPS_API_PROXY_MANAGED_INSTANCE_USERNAME", None) + self.headers.pop("_MPS_API_PROXY_MANAGED_INSTANCE_PASSWORD", None) + self.request(method="POST", endpoint=url, objecttype=objecttype, data=payload) + + def request( # pylint: disable=too-many-arguments + self, + method: str, + endpoint: str, + objecttype: str = "", + objectname: str = "", + params: Optional[Union[str, dict]] = None, + data: Optional[str] = None, + ): + """Perform request of specified method to endpoint. + + Args: + method (str): HTTP method to use with request, ie GET, PUT, POST, etc. + endpoint (str): API endpoint to query. + objecttype (str, optional): Specific object type to query the API about. Defaults to "". + objectname (str, optional): Specifc object to query the API about. Defaults to "". + params (Optional[Union[str, dict]], optional): Additional parameters for the request. Defaults to None. + data (Optional[str], optional): Addiontal data payload for the request. Defaults to None. + + Returns: + dict: Dictionary of data about objectname of objecttype with specified parameters if specified. + """ + url = self.url + "/nitro/v1/" + endpoint + "/" + objecttype + + if objectname: + url += "/" + objectname + + if params: + url += "?" + + if isinstance(params, dict): + for key, value in params.items(): + url += key + "=" + value + else: + url += params + + _result = requests.request( + method=method, + url=url, + data=data, + headers=self.headers, + timeout=60, + verify=self.verify, + ) + if _result: + _result.raise_for_status() + _result = _result.json() + if _result.get("errorcode") == 0: + return _result + self.job.logger.warning(f"Failure with request: {_result['message']}") + return {} + + def get_sites(self): + """Gather all sites configured on MAS/ADM instance.""" + if self.job.debug: + self.job.logger.info("Getting sites from Citrix ADM.") + endpoint = "config" + objecttype = "mps_datacenter" + params = {"attrs": "city,zipcode,type,name,region,country,latitude,longitude,id"} + result = self.request("GET", endpoint, objecttype, params=params) + if result: + return result[objecttype] + if self.job.debug: + self.job.logger.error("Error getting sites from Citrix ADM.") + return {} + + def get_devices(self): + """Gather all devices registered to MAS/ADM instance.""" + if self.job.debug: + self.job.logger.info("Getting devices from Citrix ADM.") + endpoint = "config" + objecttype = "managed_device" + params = { + "attrs": "ip_address,hostname,gateway,mgmt_ip_address,description,serialnumber,type,display_name,netmask,datacenter_id,version,instance_state,ha_ip_address" + } + result = self.request("GET", endpoint, objecttype, params=params) + if result: + return result[objecttype] + self.job.logger.error("Error getting devices from Citrix ADM.") + return {} + + def get_nsip(self, adc): + """Gather all nsip addresses from ADC instance using ADM as proxy.""" + endpoint = "config" + objecttype = "nsip" + params = {} + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_USERNAME"] = self.username + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_PASSWORD"] = self.password + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_IP"] = adc["ip_address"] + result = self.request("GET", endpoint, objecttype, params=params) + if result: + return result[objecttype] + if self.job.debug: + self.job.logger.error(f"Error getting nsip from {adc['hostname']}") + return {} + + def get_nsip6(self, adc): + """Gather all nsip6 addresses from ADC instance using ADM as proxy.""" + endpoint = "config" + objecttype = "nsip6" + params = {} + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_USERNAME"] = self.username + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_PASSWORD"] = self.password + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_IP"] = adc["ip_address"] + result = self.request("GET", endpoint, objecttype, params=params) + if result: + return result[objecttype] + if self.job.debug: + self.job.logger.error(f"Error getting nsip6 from {adc['hostname']}") + return {} + + def get_vlan_bindings(self, adc): + """Gather all interface vlan and nsip bindings from ADC instance using ADM as proxy.""" + endpoint = "config" + objecttype = "vlan_binding" + params = {"bulkbindings": "yes"} + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_USERNAME"] = self.username + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_PASSWORD"] = self.password + self.headers["_MPS_API_PROXY_MANAGED_INSTANCE_IP"] = adc["ip_address"] + result = self.request("GET", endpoint, objecttype, params=params) + if result: + return result[objecttype] + if self.job.debug: + self.job.logger.error(f"Error getting vlan bindings from {adc['hostname']}") + return {} + + +def parse_version(version: str): + """Parse Device version from string. + + Args: + version (str): Version string from device API query. + """ + result = "" + match_pattern = r"NetScaler\s(?PNS\d+\.\d+: Build\s\d+\.\d+\.\w+)" + match = re.match(pattern=match_pattern, string=version) + if match: + result = match.group("version") + return result + + +def parse_vlan_bindings(vlan_bindings: List[dict], adc: dict, job) -> List[dict]: + """Parse VLAN Bindings from ADC.""" + ports = [] + for binding in vlan_bindings: + if binding.get("vlan_interface_binding"): + if binding.get("vlan_nsip_binding"): + for nsip in binding["vlan_nsip_binding"]: + vlan = nsip["id"] + ipaddress = nsip["ipaddress"] + netmask = netmask_to_cidr(nsip["netmask"]) + port = binding["vlan_interface_binding"][0]["ifnum"] + record = {"vlan": vlan, "ipaddress": ipaddress, "netmask": netmask, "port": port, "version": 4} + ports.append(record) + if binding.get("vlan_nsip6_binding"): + for nsip6 in binding["vlan_nsip6_binding"]: + vlan = nsip6["id"] + ipaddress, netmask = nsip6["ipaddress"].split("/") + port = binding["vlan_interface_binding"][0]["ifnum"] + record = {"vlan": vlan, "ipaddress": ipaddress, "netmask": netmask, "port": port, "version": 6} + ports.append(record) + else: + if job.debug: + job.logger.warning(f"{adc['hostname']}: VLAN {binding['id']} has no interface binding: {binding}.") + + # Account for NSIP in VLAN 1 which is not returned by get_vlan_bindings() + if vlan_bindings: + ports_dict = {port["ipaddress"]: port for port in ports} + if adc["ip_address"] not in ports_dict: + port = vlan_bindings[0]["vlan_interface_binding"][0]["ifnum"] + netmask = netmask_to_cidr(adc["netmask"]) + ipaddress = adc["ip_address"] + record = {"vlan": "1", "ipaddress": ipaddress, "netmask": netmask, "port": port, "version": 4} + ports.append(record) + + if job.debug: + job.logger.warning(f"{adc['hostname']} is using VLAN 1 for NSIP.") + + return ports + + +def parse_nsips(nsips: List[dict], ports: List[dict], adc: dict) -> List[dict]: + """Parse Netscaler IPv4 Addresses.""" + for nsip in nsips: + for port in ports: + if port["ipaddress"] == nsip["ipaddress"]: + if nsip["type"] == "NSIP": + port["tags"] = ["NSIP"] + break + + if nsip["type"] in ["SNIP", "MIP"] and port["version"] != 6: + network = str(ipaddress_interface(f"{port['ipaddress']}/{port['netmask']}", "network")) + if is_ip_within(nsip["ipaddress"], network): + _tags = ["MGMT"] if nsip["ipaddress"] == adc["mgmt_ip_address"] else [] + _tags = ["MIP"] if nsip["type"] == "MIP" else _tags + record = { + "vlan": port["vlan"], + "ipaddress": nsip["ipaddress"], + "netmask": netmask_to_cidr(nsip["netmask"]), + "port": port["port"], + "version": 4, + "tags": _tags, + } + ports.append(record) + return ports + + +def parse_nsip6s(nsip6s: List[dict], ports: List[dict]) -> List[dict]: + """Parse Netscaler IPv6 Addresses.""" + for nsip6 in nsip6s: + if nsip6["scope"] == "link-local": + vlan = nsip6["vlan"] + ipaddress, netmask = nsip6["ipv6address"].split("/") + port = "L0/1" + record = {"vlan": vlan, "ipaddress": ipaddress, "netmask": netmask, "port": port} + ports.append(record) + + return ports diff --git a/nautobot_ssot/integrations/citrix_adm/utils/nautobot.py b/nautobot_ssot/integrations/citrix_adm/utils/nautobot.py new file mode 100644 index 000000000..a6972f721 --- /dev/null +++ b/nautobot_ssot/integrations/citrix_adm/utils/nautobot.py @@ -0,0 +1,22 @@ +"""Utility functions for working with Nautobot.""" + +from typing import List + +from taggit.managers import TaggableManager + + +def get_tag_strings(list_tags: TaggableManager) -> List[str]: + """Gets string values of all Tags in a list. + + This is the opposite of the `get_tags` function. + + Args: + list_tags (TaggableManager): List of Tag objects to convert to strings. + + Returns: + List[str]: List of string values matching the Tags passed in. + """ + _strings = list(list_tags.names()) + if len(_strings) > 1: + _strings.sort() + return _strings diff --git a/nautobot_ssot/integrations/dna_center/diffsync/adapters/dna_center.py b/nautobot_ssot/integrations/dna_center/diffsync/adapters/dna_center.py index b101af86c..10496916f 100644 --- a/nautobot_ssot/integrations/dna_center/diffsync/adapters/dna_center.py +++ b/nautobot_ssot/integrations/dna_center/diffsync/adapters/dna_center.py @@ -1,7 +1,7 @@ """Nautobot SSoT for Cisco DNA Center Adapter for DNA Center SSoT plugin.""" import json -from typing import List +from typing import List, Optional from diffsync import Adapter from diffsync.exceptions import ObjectNotFound @@ -23,6 +23,7 @@ DnaCenterPrefix, ) from nautobot_ssot.integrations.dna_center.utils.dna_center import DnaCenterClient +from nautobot_ssot.utils import parse_hostname_for_role class DnaCenterAdapter(Adapter): @@ -39,11 +40,11 @@ class DnaCenterAdapter(Adapter): top_level = ["area", "building", "device", "prefix", "ipaddress", "ip_on_intf"] - def __init__(self, *args, job=None, sync=None, client: DnaCenterClient, tenant: Tenant, **kwargs): + def __init__(self, *args, job, sync=None, client: DnaCenterClient, tenant: Tenant, **kwargs): """Initialize DNA Center. Args: - job (object, optional): DNA Center job. Defaults to None. + job (Union[DataSource, DataTarget]): DNA Center job. sync (object, optional): DNA Center DiffSync. Defaults to None. client (DnaCenterClient): DNA Center API client connection object. tenant (Tenant): Tenant to attach to imported objects. Can be set to None for no Tenant to be attached. @@ -63,8 +64,7 @@ def load_locations(self): if locations: # to ensure we process locations in the appropriate order we need to split them into their own list of locations self.dnac_location_map = self.build_dnac_location_map(locations) - areas, buildings, floors = self.parse_and_sort_locations(locations) - self.load_areas(areas) + _, buildings, floors = self.parse_and_sort_locations(locations) self.load_buildings(buildings) self.load_floors(floors) else: @@ -149,34 +149,14 @@ def load_controller_locations(self): attrs={"uuid": None}, ) - def load_areas(self, areas: List[dict]): - """Load areas from DNAC into DiffSync model. + def load_area(self, area: str, area_parent: Optional[str] = None): + """Load area from DNAC into DiffSync model. Args: - areas (List[dict]): List of dictionaries containing location information about a building. + area (str): Name of area to be loaded. + area_parent (Optional[str], optional): Name of area's parent if defined. Defaults to None. """ - for location in areas: - if not settings.PLUGINS_CONFIG["nautobot_ssot"].get("dna_center_import_global"): - if location["name"] == "Global": - continue - parent_name = None - if location.get("parentId") and location["parentId"] in self.dnac_location_map: - parent_name = self.dnac_location_map[location["parentId"]]["name"] - self.dnac_location_map[location["id"]]["parent"] = parent_name - _, loaded = self.get_or_instantiate( - self.area, - ids={"name": location["name"], "parent": parent_name}, - attrs={ - "uuid": None, - }, - ) - if loaded: - if self.job.debug: - self.job.logger.info(f"Loaded {self.job.area_loctype.name} {location['name']}. {location}") - else: - self.job.logger.warning( - f"Duplicate {self.job.area_loctype.name} {location['name']} attempting to be loaded." - ) + self.get_or_instantiate(self.area, ids={"name": area, "parent": area_parent}, attrs={"uuid": None}) def load_buildings(self, buildings: List[dict]): """Load building data from DNAC into DiffSync model. @@ -185,35 +165,46 @@ def load_buildings(self, buildings: List[dict]): buildings (List[dict]): List of dictionaries containing location information about a building. """ for location in buildings: - if location["parentId"] in self.dnac_location_map: - _area = self.dnac_location_map[location["parentId"]] - else: - _area = {"name": "Global", "parent": None} - try: - self.get(self.building, {"name": location["name"], "area": _area["name"]}) - self.job.logger.warning( - f"{self.job.building_loctype.name} {location['name']} already loaded so skipping." - ) - continue - except ObjectNotFound: - if self.job.debug: - self.job.logger.info(f"Loading {self.job.building_loctype.name} {location['name']}. {location}") - address, _ = self.conn.find_address_and_type(info=location["additionalInfo"]) - latitude, longitude = self.conn.find_latitude_and_longitude(info=location["additionalInfo"]) - new_building = self.building( - name=location["name"], - address=address if address else "", - area=_area["name"], - area_parent=_area["parent"], - latitude=latitude[:9].rstrip("0"), - longitude=longitude[:7].rstrip("0"), - tenant=self.tenant.name if self.tenant else None, - uuid=None, - ) - try: - self.add(new_building) - except ValidationError as err: - self.job.logger.warning(f"Unable to load building {location['name']}. {err}") + if self.job.debug: + self.job.logger.info(f"Loading {self.job.building_loctype.name} {location['name']}. {location}") + bldg_name = location["name"] + _area, _area_parent = None, None + if bldg_name in self.job.location_map and "parent" in self.job.location_map[bldg_name]: + _area = self.job.location_map[bldg_name]["parent"] + if "area_parent" in self.job.location_map[bldg_name]: + _area_parent = self.job.location_map[bldg_name]["area_parent"] + if "name" in self.job.location_map[bldg_name]: + bldg_name = self.job.location_map[bldg_name]["name"] + elif location["parentId"] in self.dnac_location_map: + _area = self.dnac_location_map[location["parentId"]]["name"] + _area_parent = self.dnac_location_map[location["parentId"]]["parent"] + if _area in self.job.location_map and ( + "parent" in self.job.location_map[_area] and bldg_name not in self.job.location_map + ): + _area_parent = self.job.location_map[_area]["parent"] + if not settings.PLUGINS_CONFIG["nautobot_ssot"].get("dna_center_import_global"): + if _area == "Global": + _area = None + if _area_parent == "Global": + _area_parent = None + if _area: + self.load_area(area=_area, area_parent=_area_parent) + address, _ = self.conn.find_address_and_type(info=location["additionalInfo"]) + latitude, longitude = self.conn.find_latitude_and_longitude(info=location["additionalInfo"]) + _, loaded = self.get_or_instantiate( + self.building, + ids={"name": bldg_name, "area": _area}, + attrs={ + "address": address if address else "", + "area_parent": _area_parent, + "latitude": latitude[:9].rstrip("0"), + "longitude": longitude[:7].rstrip("0"), + "tenant": self.tenant.name if self.tenant else None, + "uuid": None, + }, + ) + if not loaded: + self.job.logger.warning(f"{self.job.building_loctype.name} {bldg_name} already loaded so skipping.") def load_floors(self, floors: List[dict]): """Load floor data from DNAC into DiffSync model. @@ -224,39 +215,30 @@ def load_floors(self, floors: List[dict]): for location in floors: if self.job.debug: self.job.logger.info(f"Loading floor {location['name']}. {location}") + area_name = None if location["parentId"] in self.dnac_location_map: - _building = self.dnac_location_map[location["parentId"]] + bldg_name = self.dnac_location_map[location["parentId"]]["name"] + area_name = self.dnac_location_map[location["parentId"]]["parent"] else: self.job.logger.warning(f"Parent to {location['name']} can't be found so will be skipped.") continue - floor_name = f"{_building['name']} - {location['name']}" + if bldg_name in self.job.location_map and "name" in self.job.location_map[bldg_name]: + area_name = self.job.location_map[bldg_name]["parent"] + bldg_name = self.job.location_map[bldg_name]["name"] + floor_name = f"{bldg_name} - {location['name']}" try: - self.get( + parent = self.get(self.building, {"name": bldg_name, "area": area_name}) + new_floor, loaded = self.get_or_instantiate( self.floor, - {"name": floor_name, "building": _building["name"]}, + ids={"name": floor_name, "building": bldg_name}, + attrs={"tenant": self.tenant.name if self.tenant else None, "uuid": None}, ) - self.job.logger.warning(f"Duplicate Floor {floor_name} attempting to be loaded.") - except ObjectNotFound: - new_floor = self.floor( - name=floor_name, - building=_building["name"], - tenant=self.tenant.name if self.tenant else None, - uuid=None, + if loaded: + parent.add_child(new_floor) + except ObjectNotFound as err: + self.job.logger.warning( + f"Unable to find {self.job.building_loctype.name} {bldg_name} for {self.job.floor_loctype.name} {floor_name}. {err}" ) - try: - self.add(new_floor) - try: - parent = self.get( - self.building, - {"name": _building["name"], "area": self.dnac_location_map[location["parentId"]]["parent"]}, - ) - parent.add_child(new_floor) - except ObjectNotFound as err: - self.job.logger.warning( - f"Unable to find {self.job.building_loctype.name} {_building['name']} for {self.job.floor_loctype.name} {floor_name}. {err}" - ) - except ValidationError as err: - self.job.logger.warning(f"Unable to load floor {floor_name}. {err}") def parse_and_sort_locations(self, locations: List[dict]): """Separate locations into areas, buildings, and floors for processing. Also sort by siteHierarchy. @@ -311,19 +293,25 @@ def load_devices(self): """Load Device data from DNA Center info DiffSync models.""" devices = self.conn.get_devices() for dev in devices: + if not PLUGIN_CFG.get("dna_center_import_merakis") and ( + (dev.get("family") and "Meraki" in dev["family"]) + or (dev.get("errorDescription") and "Meraki" in dev["errorDescription"]) + ): + continue platform = "unknown" dev_role = "Unknown" vendor = "Cisco" if not dev.get("hostname"): - self.job.logger.warning(f"Device {dev['id']} is missing hostname so will be skipped.") + if self.job.debug: + self.job.logger.warning(f"Device {dev['id']} is missing hostname so will be skipped.") dev["field_validation"] = { "reason": "Failed due to missing hostname.", } self.failed_import_devices.append(dev) continue - if PLUGIN_CFG.get("dna_center_hostname_mapping"): - dev_role = self.conn.parse_hostname_for_role( - hostname_map=PLUGIN_CFG["dna_center_hostname_mapping"], device_hostname=dev["hostname"] + if self.job.hostname_map: + dev_role = parse_hostname_for_role( + hostname_map=self.job.hostname_map, device_hostname=dev["hostname"], default_role="Unknown" ) if dev_role == "Unknown": dev_role = dev["role"] @@ -333,9 +321,14 @@ def load_devices(self): if not dev.get("softwareType") and dev.get("type") and ("3800" in dev["type"] or "9130" in dev["type"]): platform = "cisco_ios" if not dev.get("softwareType") and dev.get("family") and "Meraki" in dev["family"]: - if not PLUGIN_CFG.get("dna_center_import_merakis"): - continue platform = "cisco_meraki" + if platform == "unknown": + self.job.logger.warning(f"Device {dev['hostname']} is missing Platform so will be skipped.") + dev["field_validation"] = { + "reason": "Failed due to missing platform.", + } + self.failed_import_devices.append(dev) + continue if dev.get("type") and "Juniper" in dev["type"]: vendor = "Juniper" dev_details = self.conn.get_device_detail(dev_id=dev["id"]) @@ -349,7 +342,8 @@ def load_devices(self): or loc_data.get("building") == "Unassigned" or not loc_data.get("building") ): - self.job.logger.warning(f"Device {dev['hostname']} is missing building so will not be imported.") + if self.job.debug: + self.job.logger.warning(f"Device {dev['hostname']} is missing building so will not be imported.") dev["field_validation"] = { "reason": "Missing building assignment.", "device_details": dev_details, @@ -364,9 +358,10 @@ def load_devices(self): ) device_found = self.get(self.device, dev["hostname"]) if device_found: - self.job.logger.warning( - f"Duplicate device attempting to be loaded for {dev['hostname']} with ID: {dev['id']} so will not be imported." - ) + if self.job.debug: + self.job.logger.warning( + f"Duplicate device attempting to be loaded for {dev['hostname']} with ID: {dev['id']} so will not be imported." + ) dev["field_validation"] = { "reason": "Failed due to duplicate device found.", "device_details": dev_details, @@ -394,7 +389,8 @@ def load_devices(self): self.add(new_dev) self.load_ports(device_id=dev["id"], dev=new_dev, mgmt_addr=dev["managementIpAddress"]) except ValidationError as err: - self.job.logger.warning(f"Unable to load device {dev['hostname']}. {err}") + if self.job.debug: + self.job.logger.warning(f"Unable to load device {dev['hostname']}. {err}") dev["field_validation"] = { "reason": f"Failed validation. {err}", "device_details": dev_details, @@ -421,7 +417,7 @@ def load_ports(self, device_id: str, dev: DnaCenterDevice, mgmt_addr: str = ""): "mac_addr": port["macAddress"].upper() if port.get("macAddress") else None, }, ) - if found_port: + if found_port and self.job.debug: self.job.logger.warning( f"Duplicate port attempting to be loaded, {port['portName']} for {dev.name}" ) @@ -495,7 +491,7 @@ def load_ip_address(self, host: str, mask_length: int, prefix: str): self.add(new_prefix) try: ip_found = self.get(self.ipaddress, {"host": host, "namespace": namespace}) - if ip_found: + if ip_found and self.job.debug: self.job.logger.warning(f"Duplicate IP Address attempting to be loaded: {host} in {prefix}") except ObjectNotFound: if self.job.debug: diff --git a/nautobot_ssot/integrations/dna_center/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/dna_center/diffsync/adapters/nautobot.py index 542c0d359..d0af61b01 100644 --- a/nautobot_ssot/integrations/dna_center/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/dna_center/diffsync/adapters/nautobot.py @@ -168,23 +168,36 @@ def load_devices(self): devices = OrmDevice.objects.filter(_custom_field_data__system_of_record="DNA Center") for dev in devices: self.device_map[dev.name] = dev.id - version = dev.custom_field_data.get("os_version") + version = None + if getattr(dev, "software_version"): + version = dev.software_version.version if LIFECYCLE_MGMT: + dlm_version = None try: soft_lcm = OrmRelationship.objects.get(label="Software on Device") - version = OrmRelationshipAssociation.objects.get( + dlm_version = OrmRelationshipAssociation.objects.get( relationship=soft_lcm, destination_id=dev.id ).source.version + except OrmRelationship.DoesNotExist: + pass except OrmRelationshipAssociation.DoesNotExist: pass + if dlm_version != version: + version = None + bldg_name, floor_name = None, None + if dev.location.location_type == self.job.floor_loctype: + floor_name = dev.location.name + bldg_name = dev.location.parent.name + if dev.location.location_type == self.job.building_loctype: + bldg_name = dev.location.name new_dev = self.device( name=dev.name, status=dev.status.name, role=dev.role.name, vendor=dev.device_type.manufacturer.name, model=dev.device_type.model, - site=dev.location.parent.name if dev.location.parent else None, - floor=dev.location.name if dev.location else None, + site=bldg_name, + floor=floor_name, serial=dev.serial, version=version, platform=dev.platform.network_driver if dev.platform else "", diff --git a/nautobot_ssot/integrations/dna_center/diffsync/models/base.py b/nautobot_ssot/integrations/dna_center/diffsync/models/base.py index 9da609aa7..58a723134 100644 --- a/nautobot_ssot/integrations/dna_center/diffsync/models/base.py +++ b/nautobot_ssot/integrations/dna_center/diffsync/models/base.py @@ -30,7 +30,7 @@ class Building(DiffSyncModel): name: str address: Optional[str] = None - area: str + area: Optional[str] = None area_parent: Optional[str] = None latitude: Optional[str] = None longitude: Optional[str] = None diff --git a/nautobot_ssot/integrations/dna_center/diffsync/models/nautobot.py b/nautobot_ssot/integrations/dna_center/diffsync/models/nautobot.py index e712b538e..267f1f0ea 100644 --- a/nautobot_ssot/integrations/dna_center/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/dna_center/diffsync/models/nautobot.py @@ -23,12 +23,19 @@ ) try: - import nautobot_device_lifecycle_mgmt # noqa: F401 + from nautobot_device_lifecycle_mgmt import SoftwareLCM # noqa: F401 LIFECYCLE_MGMT = True except ImportError: LIFECYCLE_MGMT = False +try: + from nautobot.dcim.models import SoftwareImageFile, SoftwareVersion # noqa: F401 + + SOFTWARE_VERSION_FOUND_IN_CORE = True +except ImportError: + SOFTWARE_VERSION_FOUND_IN_CORE = False + class NautobotArea(base.Area): """Nautobot implementation of Area DiffSync model.""" @@ -43,15 +50,16 @@ def create(cls, adapter, ids, attrs): location_type=adapter.job.area_loctype, status_id=adapter.status_map["Active"], ) - try: - parents_parent = "Global" - if ids["parent"] == "Global": - parents_parent = None - new_area.parent_id = adapter.region_map[parents_parent][ids["parent"]] - except KeyError: - adapter.job.logger.warning( - f"Unable to find {adapter.job.area_loctype.name} {ids['parent']} for {ids['name']}." - ) + if ids.get("parent"): + try: + parents_parent = "Global" + if ids["parent"] == "Global": + parents_parent = None + new_area.parent_id = adapter.region_map[parents_parent][ids["parent"]] + except KeyError: + adapter.job.logger.warning( + f"Unable to find {adapter.job.area_loctype.name} {ids['parent']} for {ids['name']}." + ) new_area.validated_save() if ids["parent"] not in adapter.region_map: adapter.region_map[ids["parent"]] = {} @@ -214,10 +222,20 @@ def create(cls, adapter, ids, attrs): if attrs.get("tenant"): new_device.tenant_id = adapter.tenant_map[attrs["tenant"]] if attrs.get("version"): - new_device.custom_field_data.update({"os_version": attrs["version"]}) if LIFECYCLE_MGMT: lcm_obj = add_software_lcm(adapter=adapter, platform=platform.network_driver, version=attrs["version"]) assign_version_to_device(adapter=adapter, device=new_device, software_lcm=lcm_obj) + if SOFTWARE_VERSION_FOUND_IN_CORE: + soft_version = SoftwareVersion.objects.get_or_create( + version=attrs["version"], platform=platform, defaults={"status_id": adapter.status_map["Active"]} + )[0] + image, _ = SoftwareImageFile.objects.get_or_create( + image_file_name=f"{platform.name}-{attrs['version']}-dnac-ssot-placeholder", + software_version=soft_version, + status_id=adapter.status_map["Active"], + ) + image.device_types.add(device_type) + new_device.software_version = soft_version new_device.custom_field_data.update({"system_of_record": "DNA Center"}) new_device.custom_field_data.update({"last_synced_from_sor": datetime.today().date().isoformat()}) adapter.objects_to_create["devices"].append(new_device) @@ -260,13 +278,22 @@ def update(self, attrs): if "controller_group" in attrs: device.controller_managed_device_group = self.adapter.job.controller_group if "version" in attrs: - device.custom_field_data.update({"os_version": attrs["version"]}) if LIFECYCLE_MGMT: platform_network_driver = attrs["platform"] if attrs.get("platform") else self.platform lcm_obj = add_software_lcm( adapter=self.adapter, platform=platform_network_driver, version=attrs["version"] ) assign_version_to_device(adapter=self.adapter, device=device, software_lcm=lcm_obj) + if SOFTWARE_VERSION_FOUND_IN_CORE: + if attrs.get("platform"): + platform = attrs["platform"] + else: + platform = self.platform + device.software_version = SoftwareVersion.objects.get_or_create( + version=attrs["version"], + platform__name=platform, + defaults={"status_id": self.adapter.status_map["Active"]}, + )[0] device.custom_field_data.update({"system_of_record": "DNA Center"}) device.custom_field_data.update({"last_synced_from_sor": datetime.today().date().isoformat()}) device.validated_save() @@ -416,6 +443,8 @@ def create(cls, adapter, ids, attrs): def update(self, attrs): """Update IPAddress in Nautobot from IPAddress object.""" ipaddr = IPAddress.objects.get(id=self.uuid) + if "mask_length" in attrs: + ipaddr.mask_length = attrs["mask_length"] if "tenant" in attrs: if attrs.get("tenant"): ipaddr.tenant_id = self.adapter.tenant_map[attrs["tenant"]] diff --git a/nautobot_ssot/integrations/dna_center/jobs.py b/nautobot_ssot/integrations/dna_center/jobs.py index 7767a9ce5..851a74083 100644 --- a/nautobot_ssot/integrations/dna_center/jobs.py +++ b/nautobot_ssot/integrations/dna_center/jobs.py @@ -1,8 +1,11 @@ """Jobs for DNA Center SSoT integration.""" +from ast import literal_eval + +from diffsync.enum import DiffSyncFlags from django.templatetags.static import static from django.urls import reverse -from nautobot.apps.jobs import BooleanVar, JSONVar, ObjectVar +from nautobot.apps.jobs import BooleanVar, JSONVar, ObjectVar, StringVar from nautobot.core.celery import register_jobs from nautobot.dcim.models import Controller, LocationType from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices @@ -57,6 +60,12 @@ class DnaCenterDataSource(DataSource): # pylint: disable=too-many-instance-attr default={}, description="Map of information regarding Locations in DNA Center. Ex: {'': {'parent': ''}}", ) + hostname_map = StringVar( + default=[], + description="List of tuples containing Device hostnames to assign to specified Role. ex: [('core-router.com', 'router')]", + label="Hostname Mapping", + required=False, + ) debug = BooleanVar(description="Enable for more verbose debug logging", default=False) bulk_import = BooleanVar( @@ -83,13 +92,15 @@ class Meta: # pylint: disable=too-few-public-methods "building_loctype", "floor_loctype", "location_map", + "hostname_map", "tenant", ] def __init__(self): """Initiailize Job vars.""" - self.controller_group = None super().__init__() + self.controller_group = None + self.diffsync_flags = DiffSyncFlags.CONTINUE_ON_FAILURE @classmethod def config_information(cls): @@ -111,7 +122,7 @@ def data_mappings(cls): def load_source_adapter(self): """Load data from DNA Center into DiffSync models.""" self.logger.info(f"Loading data from {self.dnac.name}") - verify_controller_managed_device_group(controller=self.dnac) + self.controller_group = verify_controller_managed_device_group(controller=self.dnac) _sg = self.dnac.external_integration.secrets_group username = _sg.get_secret_value( access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP, @@ -175,6 +186,7 @@ def run( building_loctype, floor_loctype, location_map, + hostname_map, bulk_import, tenant, *args, @@ -187,6 +199,7 @@ def run( self.floor_loctype = floor_loctype self.validate_locationtypes() self.location_map = location_map + self.hostname_map = literal_eval(hostname_map) self.tenant = tenant self.debug = debug self.bulk_import = bulk_import diff --git a/nautobot_ssot/integrations/dna_center/signals.py b/nautobot_ssot/integrations/dna_center/signals.py index cf7643f4c..33e50b799 100644 --- a/nautobot_ssot/integrations/dna_center/signals.py +++ b/nautobot_ssot/integrations/dna_center/signals.py @@ -24,13 +24,6 @@ def nautobot_database_ready_callback(sender, *, apps, **kwargs): # pylint: disa IPAddress = apps.get_model("ipam", "IPAddress") Prefix = apps.get_model("ipam", "Prefix") - ver_dict = { - "key": "os_version", - "type": CustomFieldTypeChoices.TYPE_TEXT, - "label": "OS Version", - } - ver_field, _ = CustomField.objects.get_or_create(key=ver_dict["key"], defaults=ver_dict) - ver_field.content_types.add(ContentType.objects.get_for_model(Device)) sor_cf_dict = { "type": CustomFieldTypeChoices.TYPE_TEXT, "key": "system_of_record", diff --git a/nautobot_ssot/integrations/dna_center/utils/dna_center.py b/nautobot_ssot/integrations/dna_center/utils/dna_center.py index c695e299d..00edd69c4 100644 --- a/nautobot_ssot/integrations/dna_center/utils/dna_center.py +++ b/nautobot_ssot/integrations/dna_center/utils/dna_center.py @@ -2,7 +2,7 @@ import logging import re -from typing import List, Tuple +from typing import List from dnacentersdk import api from dnacentersdk.exceptions import dnacentersdkException @@ -198,25 +198,6 @@ def get_port_status(port_info: dict): status = "Planned" return status - @staticmethod - def parse_hostname_for_role(hostname_map: List[Tuple[str, str]], device_hostname: str): - """Parse device hostname from hostname_map to get Device Role. - - Args: - hostname_map (List[Tuple[str, str]]): List of tuples containing regex to compare with hostname and associated DeviceRole name. - device_hostname (str): Hostname of Device to determine role of. - - Returns: - str: Name of DeviceRole. Defaults to Unknown. - """ - device_role = "Unknown" - if hostname_map: - for entry in hostname_map: - match = re.match(pattern=entry[0], string=device_hostname) - if match: - device_role = entry[1] - return device_role - @staticmethod def get_model_name(models: str) -> str: """Obtain DeviceType model from a list of models. diff --git a/nautobot_ssot/integrations/meraki/diffsync/adapters/meraki.py b/nautobot_ssot/integrations/meraki/diffsync/adapters/meraki.py index 5c1205f6e..1c7463cd3 100644 --- a/nautobot_ssot/integrations/meraki/diffsync/adapters/meraki.py +++ b/nautobot_ssot/integrations/meraki/diffsync/adapters/meraki.py @@ -14,8 +14,10 @@ MerakiOSVersion, MerakiPort, MerakiPrefix, + MerakiPrefixLocation, ) -from nautobot_ssot.integrations.meraki.utils.meraki import get_role_from_devicetype, parse_hostname_for_role +from nautobot_ssot.integrations.meraki.utils.meraki import get_role_from_devicetype +from nautobot_ssot.utils import parse_hostname_for_role class MerakiAdapter(Adapter): @@ -27,10 +29,11 @@ class MerakiAdapter(Adapter): device = MerakiDevice port = MerakiPort prefix = MerakiPrefix + prefixlocation = MerakiPrefixLocation ipaddress = MerakiIPAddress ipassignment = MerakiIPAssignment - top_level = ["network", "hardware", "osversion", "device", "prefix", "ipaddress", "ipassignment"] + top_level = ["network", "hardware", "osversion", "device", "prefix", "prefixlocation", "ipaddress", "ipassignment"] def __init__(self, job, sync, client, tenant=None): """Initialize Meraki. @@ -52,12 +55,15 @@ def __init__(self, job, sync, client, tenant=None): def load_networks(self): """Load networks from Meraki dashboard into DiffSync models.""" for net in self.conn.get_org_networks(): + network_name = net["name"] parent_name = None if self.job.network_loctype.parent: if self.job.parent_location: parent_name = self.job.parent_location.name - elif self.job.location_map and net in self.job.location_map: - parent_name = self.job.location_map[net]["parent"] + elif self.job.location_map and network_name in self.job.location_map: + parent_name = self.job.location_map[network_name]["parent"] + if "name" in self.job.location_map[network_name]: + network_name = self.job.location_map[network_name] else: self.job.logger.error( f"Parent Location is required for {self.job.network_loctype.name} but can't determine parent to be assigned to {net}." @@ -65,7 +71,7 @@ def load_networks(self): continue self.get_or_instantiate( self.network, - ids={"name": net["name"], "parent": parent_name}, + ids={"name": network_name, "parent": parent_name}, attrs={ "timezone": net["timeZone"], "notes": net["notes"].rstrip() if net.get("notes") else "", @@ -92,7 +98,9 @@ def load_devices(self): # pylint: disable=too-many-branches if self.job.hostname_mapping and len(self.job.hostname_mapping) > 0: if self.job.debug: self.job.logger.debug(f"Parsing hostname for device {dev['name']} to determine role.") - role = parse_hostname_for_role(dev_hostname=dev["name"], hostname_map=self.job.hostname_mapping) + role = parse_hostname_for_role( + device_hostname=dev["name"], hostname_map=self.job.hostname_mapping, default_role="Unknown" + ) elif self.job.devicetype_mapping and len(self.job.devicetype_mapping) > 0: if self.job.debug: self.job.logger.debug(f"Parsing device model for device {dev['name']} to determine role.") @@ -157,41 +165,43 @@ def load_firewall_ports(self, device: DiffSyncModel, serial: str, network_id: st for link in uplinks: if link["interface"] == port and link["status"] == "active": uplink_status = "Active" - port_uplink_settings = uplink_settings[port] - new_port, loaded = self.get_or_instantiate( - self.port, - ids={"name": port, "device": device.name}, - attrs={ - "management": True, - "enabled": port_uplink_settings["enabled"], - "port_type": "1000base-t", - "port_status": uplink_status, - "tagging": port_uplink_settings["vlanTagging"]["enabled"], - "uuid": None, - }, - ) - if loaded: - self.add(new_port) - device.add_child(new_port) - if port_uplink_settings["svis"]["ipv4"]["assignmentMode"] == "static": - port_svis = port_uplink_settings["svis"]["ipv4"] - prefix = ipaddress_interface(ip=port_svis["address"], attr="network.with_prefixlen") - self.load_prefix( - location=self.conn.network_map[network_id]["name"], - prefix=prefix, - ) - self.load_ipaddress( - address=port_svis["address"], - prefix=prefix, - ) - self.load_ipassignment( - address=port_svis["address"], - dev_name=device.name, - port=port, - primary=bool(uplink_status == "Active" and not primary_found), - ) - if uplink_status == "Active": - primary_found = True + if uplink_settings.get(port): + port_uplink_settings = uplink_settings[port] + new_port, loaded = self.get_or_instantiate( + self.port, + ids={"name": port, "device": device.name}, + attrs={ + "management": True, + "enabled": port_uplink_settings["enabled"], + "port_type": "1000base-t", + "port_status": uplink_status, + "tagging": port_uplink_settings["vlanTagging"]["enabled"], + "uuid": None, + }, + ) + if loaded: + self.add(new_port) + device.add_child(new_port) + if port_uplink_settings["svis"]["ipv4"]["assignmentMode"] == "static": + port_svis = port_uplink_settings["svis"]["ipv4"] + prefix = ipaddress_interface(ip=port_svis["address"], attr="network.with_prefixlen") + self.load_prefix(prefix=prefix) + self.load_prefix_location( + prefix=prefix, + location=self.conn.network_map[network_id]["name"], + ) + self.load_ipaddress( + address=port_svis["address"], + prefix=prefix, + ) + self.load_ipassignment( + address=port_svis["address"], + dev_name=device.name, + port=port, + primary=bool(uplink_status == "Active" and not primary_found), + ) + if uplink_status == "Active": + primary_found = True if lan_ports: self.process_lan_ports(device, lan_ports) @@ -245,9 +255,10 @@ def load_switch_ports(self, device: DiffSyncModel, serial: str): ip=f"{mgmt_ports[port]['staticIp']}/{netmask_to_cidr(netmask=mgmt_ports[port]['staticSubnetMask'])}", attr="network.with_prefixlen", ) - self.load_prefix( - location=self.conn.network_map[self.device_map[device.name]["networkId"]]["name"], + self.load_prefix(prefix=prefix) + self.load_prefix_location( prefix=prefix, + location=self.conn.network_map[self.device_map[device.name]["networkId"]]["name"], ) self.load_ipaddress( address=f"{mgmt_ports[port]['staticIp']}/{netmask_to_cidr(mgmt_ports[port]['staticSubnetMask'])}", @@ -299,9 +310,10 @@ def load_ap_ports(self, device: DiffSyncModel, serial: str): ip=f"{mgmt_ports[port]['staticIp']}/{netmask_to_cidr(netmask=mgmt_ports[port]['staticSubnetMask'])}", attr="network.with_prefixlen", ) - self.load_prefix( - location=self.conn.network_map[self.device_map[device.name]["networkId"]]["name"], + self.load_prefix(prefix=prefix) + self.load_prefix_location( prefix=prefix, + location=self.conn.network_map[self.device_map[device.name]["networkId"]]["name"], ) self.load_ipaddress( address=f"{mgmt_ports[port]['staticIp']}/{netmask_to_cidr(mgmt_ports[port]['staticSubnetMask'])}", @@ -314,23 +326,25 @@ def load_ap_ports(self, device: DiffSyncModel, serial: str): primary=True, ) - def load_prefix(self, location: str, prefix: str): + def load_prefix(self, prefix: str): """Load Prefixes of devices into DiffSync models.""" if self.tenant: namespace = self.tenant.name else: namespace = "Global" - try: - self.get(self.prefix, {"prefix": prefix, "namespace": namespace}) - except ObjectNotFound: - new_pf = self.prefix( - prefix=prefix, - location=location, - namespace=namespace, - tenant=self.tenant.name if self.tenant else None, - uuid=None, - ) - self.add(new_pf) + self.get_or_instantiate( + self.prefix, + ids={"prefix": prefix, "namespace": namespace}, + attrs={"tenant": self.tenant.name if self.tenant else None, "uuid": None}, + ) + + def load_prefix_location(self, prefix: str, location: str): + """Load Prefix Locations of devices into DiffSync models.""" + self.get_or_instantiate( + self.prefixlocation, + ids={"prefix": prefix, "location": location}, + attrs={"uuid": None}, + ) def load_ipaddress(self, address: str, prefix: str): """Load IPAddresses of devices into DiffSync models.""" diff --git a/nautobot_ssot/integrations/meraki/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/meraki/diffsync/adapters/nautobot.py index 3af64c44a..28a2c92fd 100644 --- a/nautobot_ssot/integrations/meraki/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/meraki/diffsync/adapters/nautobot.py @@ -19,7 +19,7 @@ SoftwareVersion, ) from nautobot.extras.models import Note, Role, Status -from nautobot.ipam.models import IPAddress, IPAddressToInterface, Namespace, Prefix +from nautobot.ipam.models import IPAddress, IPAddressToInterface, Namespace, Prefix, PrefixLocationAssignment from nautobot.tenancy.models import Tenant from nautobot_ssot.integrations.meraki.diffsync.models.nautobot import ( @@ -31,6 +31,7 @@ NautobotOSVersion, NautobotPort, NautobotPrefix, + NautobotPrefixLocation, ) from nautobot_ssot.integrations.meraki.utils.nautobot import get_tag_strings @@ -44,10 +45,11 @@ class NautobotAdapter(Adapter): # pylint: disable=too-many-instance-attributes device = NautobotDevice port = NautobotPort prefix = NautobotPrefix + prefixlocation = NautobotPrefixLocation ipaddress = NautobotIPAddress ipassignment = NautobotIPAssignment - top_level = ["network", "hardware", "osversion", "device", "prefix", "ipaddress", "ipassignment"] + top_level = ["network", "hardware", "osversion", "device", "prefix", "prefixlocation", "ipaddress", "ipassignment"] status_map = {} tenant_map = {} @@ -188,11 +190,27 @@ def load_prefixes(self): for prefix in prefixes: new_pf = self.prefix( prefix=str(prefix.prefix), - location=prefix.location.name if prefix.location else "", namespace=prefix.namespace.name, tenant=prefix.tenant.name if prefix.tenant else None, uuid=prefix.id, ) + if getattr(prefix, "locations"): + for location in prefix.locations.all(): + pf_loc, loaded = self.get_or_instantiate( + self.prefixlocation, + ids={"prefix": str(prefix.prefix), "location": location.name}, + attrs={"uuid": location.id}, + ) + if loaded and self.tenant: + pf_loc.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST + elif getattr(prefix, "location"): + pf_loc, loaded = self.get_or_instantiate( + self.prefixlocation, + ids={"prefix": str(prefix.prefix), "location": prefix.location.name}, + attrs={"uuid": prefix.location.id}, + ) + if loaded and self.tenant: + pf_loc.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST if self.tenant: new_pf.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(new_pf) @@ -285,10 +303,8 @@ def process_objects_to_create(self): # pylint: disable=too-many-branches self.job.logger.info("Performing bulk create of Prefixes in Nautobot") Prefix.objects.bulk_create(self.objects_to_create["prefixes"], batch_size=250) if len(self.objects_to_create["prefix_locs"]) > 0: - self.job.logger.info("Performing assignment of Locations to Prefixes in Nautobot") - for pair in self.objects_to_create["prefix_locs"]: - update_pf = Prefix.objects.get(id=pair[0]) - update_pf.locations.add(pair[1]) + self.job.logger.info("Performing bulk create of PrefixLocationAssignments in Nautobot") + PrefixLocationAssignment.objects.bulk_create(self.objects_to_create["prefix_locs"], batch_size=250) if len(self.objects_to_create["ipaddrs"]) > 0: self.job.logger.info("Performing bulk create of IP Addresses in Nautobot") IPAddress.objects.bulk_create(self.objects_to_create["ipaddrs"], batch_size=250) diff --git a/nautobot_ssot/integrations/meraki/diffsync/models/base.py b/nautobot_ssot/integrations/meraki/diffsync/models/base.py index a8aeedc64..d980ce57b 100644 --- a/nautobot_ssot/integrations/meraki/diffsync/models/base.py +++ b/nautobot_ssot/integrations/meraki/diffsync/models/base.py @@ -103,17 +103,30 @@ class Prefix(DiffSyncModel): _modelname = "prefix" _identifiers = ("prefix", "namespace") - _attributes = ("location", "tenant") + _attributes = ("tenant",) _children = {} prefix: str namespace: str - location: str tenant: Optional[str] = None uuid: Optional[UUID] = None +class PrefixLocation(DiffSyncModel): + """DiffSync model for tracking Locations assigned to Prefixes in Meraki.""" + + _modelname = "prefixlocation" + _identifiers = ("prefix", "location") + _attributes = () + _children = {} + + prefix: str + location: str + + uuid: Optional[UUID] = None + + class IPAddress(DiffSyncModel): """DiffSync model for Meraki IP Addresses.""" diff --git a/nautobot_ssot/integrations/meraki/diffsync/models/meraki.py b/nautobot_ssot/integrations/meraki/diffsync/models/meraki.py index f9d788a2a..b3d16c6c0 100644 --- a/nautobot_ssot/integrations/meraki/diffsync/models/meraki.py +++ b/nautobot_ssot/integrations/meraki/diffsync/models/meraki.py @@ -10,6 +10,7 @@ OSVersion, Port, Prefix, + PrefixLocation, ) @@ -115,6 +116,23 @@ def delete(self): return self +class MerakiPrefixLocation(PrefixLocation): + """Meraki implementation of PrefixLocation DiffSync model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create PrefixLocation in Meraki from MerakiPrefixLocation object.""" + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update PrefixLocation in Meraki from MerakiPrefixLocation object.""" + return super().update(attrs) + + def delete(self): + """Delete PrefixLocation in Meraki from MerakiPrefixLocation object.""" + return self + + class MerakiIPAddress(IPAddress): """Meraki implementation of IPAddress DiffSync model.""" diff --git a/nautobot_ssot/integrations/meraki/diffsync/models/nautobot.py b/nautobot_ssot/integrations/meraki/diffsync/models/nautobot.py index 5fc6c9769..5d9e1665f 100644 --- a/nautobot_ssot/integrations/meraki/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/meraki/diffsync/models/nautobot.py @@ -6,7 +6,7 @@ from nautobot.dcim.models import DeviceType, Interface, Location, SoftwareVersion from nautobot.extras.models import Note, Role from nautobot.ipam.models import IPAddress as OrmIPAddress -from nautobot.ipam.models import IPAddressToInterface +from nautobot.ipam.models import IPAddressToInterface, PrefixLocationAssignment from nautobot.ipam.models import Prefix as OrmPrefix from nautobot_ssot.integrations.meraki.diffsync.models.base import ( @@ -18,6 +18,7 @@ OSVersion, Port, Prefix, + PrefixLocation, ) @@ -261,7 +262,7 @@ def delete(self): class NautobotPrefix(Prefix): - """Nautobot implementation of Meraki Port model.""" + """Nautobot implementation of Meraki Prefix model.""" @classmethod def create(cls, adapter, ids, attrs): @@ -272,8 +273,6 @@ def create(cls, adapter, ids, attrs): status_id=adapter.status_map["Active"], tenant_id=adapter.tenant_map[attrs["tenant"]] if attrs.get("tenant") else None, ) - if attrs.get("location"): - adapter.objects_to_create["prefix_locs"].append((new_pf.id, adapter.site_map[attrs["location"]])) new_pf.custom_field_data["system_of_record"] = "Meraki SSoT" new_pf.custom_field_data["last_synced_from_sor"] = datetime.today().date().isoformat() adapter.objects_to_create["prefixes"].append(new_pf) @@ -283,11 +282,6 @@ def create(cls, adapter, ids, attrs): def update(self, attrs): """Update Prefix in Nautobot from NautobotPrefix object.""" prefix = OrmPrefix.objects.get(id=self.uuid) - if "location" in attrs: - if attrs.get("location"): - prefix.locations.add(self.adapter.site_map[attrs["location"]]) - else: - prefix.locations.remove(self.adapter.site_map[self.location]) if "tenant" in attrs: if attrs.get("tenant"): prefix.tenant_id = self.adapter.tenant_map[attrs["tenant"]] @@ -306,6 +300,26 @@ def delete(self): return self +class NautobotPrefixLocation(PrefixLocation): + """Nautobot implementation of Meraki PrefixLocation model.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create PrefixLocationAssignment in Nautobot from NautobotPrefixLocation object.""" + new_assignment = PrefixLocationAssignment( + prefix_id=adapter.prefix_map[ids["prefix"]], location=adapter.site_map[ids["location"]] + ) + adapter.objects_to_create["prefix_locs"].append(new_assignment) + return super().create(adapter=adapter, ids=ids, attrs=attrs) + + def delete(self): + """Delete Prefix in Nautobot from NautobotPrefix object.""" + del_pf = PrefixLocationAssignment.objects.get(id=self.uuid) + super().delete() + del_pf.delete() + return self + + class NautobotIPAddress(IPAddress): """Nautobot implementation of Meraki Port model.""" diff --git a/nautobot_ssot/integrations/meraki/utils/meraki.py b/nautobot_ssot/integrations/meraki/utils/meraki.py index cfd58ede1..158d5b490 100644 --- a/nautobot_ssot/integrations/meraki/utils/meraki.py +++ b/nautobot_ssot/integrations/meraki/utils/meraki.py @@ -1,7 +1,5 @@ """Utility functions for working with Meraki.""" -import re - import meraki @@ -205,24 +203,6 @@ def get_appliance_switchports(self, network_id: str) -> list: return ports -def parse_hostname_for_role(dev_hostname: str, hostname_map: dict) -> str: - """Parse device hostname to get Device Role. - - Args: - dev_hostname (str): Hostname of Device to determine role of. - hostname_map (dict): Dictionary of hostname's mapped to their Role. - - Returns: - str: Name of DeviceRole. Defaults to Unknown. - """ - dev_role = "Unknown" - for entry in hostname_map: - match = re.match(pattern=entry[0], string=dev_hostname) - if match: - dev_role = entry[1] - return dev_role - - def get_role_from_devicetype(dev_model: str, devicetype_map: dict) -> str: """Get Device Role using DeviceType from devicetype_mapping Setting. diff --git a/nautobot_ssot/integrations/servicenow/diffsync/adapter_nautobot.py b/nautobot_ssot/integrations/servicenow/diffsync/adapter_nautobot.py index a31469f41..c2bc4680f 100644 --- a/nautobot_ssot/integrations/servicenow/diffsync/adapter_nautobot.py +++ b/nautobot_ssot/integrations/servicenow/diffsync/adapter_nautobot.py @@ -63,6 +63,7 @@ def load_locations(self): ancestor = self.site_filter.parent while ancestor is not None: locations.insert(0, ancestor) + ancestor = ancestor.parent else: locations = Location.objects.all() diff --git a/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py b/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py index 3ba5f94c2..591864e3e 100644 --- a/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py +++ b/nautobot_ssot/integrations/servicenow/diffsync/adapter_servicenow.py @@ -81,7 +81,7 @@ def load(self): # Load all Nautobot ancestor records as well # This is so in case the Nautobot ancestors exist in ServiceNow but aren't linked to the record, # we link them together instead of creating new, redundant ancestor records in ServiceNow. - ancestor = self.site_filter.region + ancestor = self.site_filter.parent while ancestor is not None: try: self.get(self.location, ancestor.name) diff --git a/nautobot_ssot/integrations/servicenow/jobs.py b/nautobot_ssot/integrations/servicenow/jobs.py index f8bcdbf5f..edad66371 100644 --- a/nautobot_ssot/integrations/servicenow/jobs.py +++ b/nautobot_ssot/integrations/servicenow/jobs.py @@ -1,5 +1,6 @@ """ServiceNow Data Target Job.""" +from diffsync.enum import DiffSyncFlags from django.core.exceptions import ObjectDoesNotExist from django.templatetags.static import static from django.urls import reverse @@ -16,7 +17,7 @@ name = "SSoT - ServiceNow" # pylint: disable=invalid-name -class ServiceNowDataTarget(DataTarget, Job): # pylint: disable=abstract-method +class ServiceNowDataTarget(DataTarget, Job): # pylint: disable=abstract-method, too-many-instance-attributes """Job syncing data from Nautobot to ServiceNow.""" debug = BooleanVar(description="Enable for more verbose logging.") @@ -79,11 +80,14 @@ def load_target_adapter(self): self.target_adapter = ServiceNowDiffSync(client=snc, job=self, sync=self.sync, site_filter=self.site_filter) self.target_adapter.load() - def run(self, dryrun, memory_profiling, site_filter, *args, **kwargs): # pylint:disable=arguments-differ + def run(self, dryrun, memory_profiling, delete_records, site_filter, *args, **kwargs): # pylint:disable=arguments-differ """Run sync.""" self.dryrun = dryrun self.memory_profiling = memory_profiling self.site_filter = site_filter + self.delete_records = delete_records + if not self.delete_records: + self.diffsync_flags |= DiffSyncFlags.SKIP_UNMATCHED_DST super().run(dryrun, memory_profiling, *args, **kwargs) def lookup_object(self, model_name, unique_id): diff --git a/nautobot_ssot/integrations/slurpit/__init__.py b/nautobot_ssot/integrations/slurpit/__init__.py new file mode 100644 index 000000000..209637e8d --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/__init__.py @@ -0,0 +1 @@ +"""Slurpit SSoT.""" diff --git a/nautobot_ssot/integrations/slurpit/constants.py b/nautobot_ssot/integrations/slurpit/constants.py new file mode 100644 index 000000000..15a839fe7 --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/constants.py @@ -0,0 +1,11 @@ +"""Constants used by Slurpit Integration.""" + +from django.conf import settings + +CONFIG = settings.PLUGINS_CONFIG.get("nautobot_ssot", {}) + +# Required Settings +DEFAULT_DEVICE_ROLE = CONFIG.get("slurpit_default_device_role", "Network Device") +DEFAULT_DEVICE_ROLE_COLOR = CONFIG.get("slurpit_default_device_role_color", "ff0000") +DEFAULT_DEVICE_STATUS = CONFIG.get("slurpit_default_device_status", "Active") +DEFAULT_DEVICE_STATUS_COLOR = CONFIG.get("slurpit_default_device_status_color", "ff0000") diff --git a/nautobot_ssot/integrations/slurpit/diffsync/__init__.py b/nautobot_ssot/integrations/slurpit/diffsync/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/integrations/slurpit/diffsync/adapters/__init__.py b/nautobot_ssot/integrations/slurpit/diffsync/adapters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/integrations/slurpit/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/slurpit/diffsync/adapters/nautobot.py new file mode 100644 index 000000000..ab1a9f810 --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/diffsync/adapters/nautobot.py @@ -0,0 +1,57 @@ +# pylint: disable=R0801 +"""DiffSync adapter for Nautobot.""" + +from nautobot_ssot.contrib import NautobotAdapter +from nautobot_ssot.integrations.slurpit.diffsync.models import ( + DeviceModel, + DeviceTypeModel, + InterfaceModel, + InventoryItemModel, + IPAddressModel, + LocationModel, + ManufacturerModel, + NautobotIPAddressToInterfaceModel, + PlatformModel, + PrefixModel, + RoleModel, + VLANModel, + VRFModel, +) + + +class NautobotDiffSyncAdapter(NautobotAdapter): + """DiffSync adapter for Nautobot.""" + + def _load_objects(self, diffsync_model): + """Given a diffsync model class, load a list of models from the database and return them. Passing in job kwargs for model filtering.""" + parameter_names = self._get_parameter_names(diffsync_model) + for database_object in diffsync_model._get_queryset(data=self.job.kwargs): # pylint: disable=W0212 + self._load_single_object(database_object, diffsync_model, parameter_names) + + location = LocationModel + manufacturer = ManufacturerModel + device_type = DeviceTypeModel + platform = PlatformModel + role = RoleModel + device = DeviceModel + interface = InterfaceModel + inventory_item = InventoryItemModel + vlan = VLANModel + vrf = VRFModel + prefix = PrefixModel + ipaddress = IPAddressModel + ipassignment = NautobotIPAddressToInterfaceModel + top_level = ( + "location", + "manufacturer", + "device_type", + "platform", + "role", + "device", + "vlan", + "vrf", + "prefix", + "ipaddress", + "interface", + "ipassignment", + ) diff --git a/nautobot_ssot/integrations/slurpit/diffsync/adapters/slurpit.py b/nautobot_ssot/integrations/slurpit/diffsync/adapters/slurpit.py new file mode 100644 index 000000000..a624900cf --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/diffsync/adapters/slurpit.py @@ -0,0 +1,514 @@ +# pylint: disable=R0801 +"""DiffSync adapter for Slurpit.""" + +import asyncio +import ipaddress +from datetime import datetime +from decimal import Decimal, InvalidOperation + +from diffsync import Adapter +from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound +from nautobot.dcim.models import LocationType +from nautobot.extras.models import Status +from netutils.mac import mac_to_format + +from nautobot_ssot.integrations.slurpit import constants +from nautobot_ssot.integrations.slurpit.diffsync.models import ( + DeviceModel, + DeviceTypeModel, + InterfaceModel, + InventoryItemModel, + IPAddressModel, + IPAddressToInterfaceModel, + LocationModel, + ManufacturerModel, + PlatformModel, + PrefixModel, + RoleModel, + VLANModel, + VRFModel, +) + + +# Helper function for latitude formatting +def format_latitude(latitude_str): + """Format latitude string to Decimal with 6 decimal places.""" + try: + latitude_decimal = Decimal(latitude_str).quantize(Decimal("0.000001")) + while len(latitude_decimal.as_tuple().digits) > 8: + latitude_decimal = latitude_decimal.quantize(Decimal("0.00001")) + return latitude_decimal + except (InvalidOperation, ValueError): + return None + + +class SlurpitAdapter(Adapter): + """DiffSync adapter for Slurpit.""" + + # Model mappings + location = LocationModel + manufacturer = ManufacturerModel + device_type = DeviceTypeModel + platform = PlatformModel + role = RoleModel + device = DeviceModel + interface = InterfaceModel + inventory_item = InventoryItemModel + vlan = VLANModel + vrf = VRFModel + prefix = PrefixModel + ipaddress = IPAddressModel + ipassignment = IPAddressToInterfaceModel + top_level = ( + "location", + "manufacturer", + "device_type", + "platform", + "role", + "device", + "vlan", + "vrf", + "prefix", + "ipaddress", + "interface", + "ipassignment", + ) + + def __init__(self, *args, api_client, job=None, **kwargs): + """Initialize the Slurpit adapter.""" + super().__init__(*args, **kwargs) + self.client = api_client + self.job = job + self.filtered_networks = [] + self.ipaddress_by_device = {} + self.hostname_to_primary_ip = {} + + # Utility for running async coroutines synchronously + def run_async(self, coroutine): + """Run an async coroutine synchronously.""" + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop.run_until_complete(coroutine) + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop.run_until_complete(coroutine) + + def unique_vendors(self): + """Get unique vendors from the devices.""" + devices = self.run_async(self.client.device.get_devices()) + vendors = {device.brand for device in devices} + return [{"brand": item} for item in vendors] + + def unique_device_type(self): + """Get unique device types from the devices.""" + devices = self.run_async(self.client.device.get_devices()) + device_types = {(device.brand, device.device_type, device.device_os) for device in devices} + return [{"brand": item[0], "device_type": item[1], "device_os": item[2]} for item in device_types] + + def unique_platforms(self): + """Get unique platforms from the devices.""" + devices = self.run_async(self.client.device.get_devices()) + return {device.device_os: device.brand for device in devices} + + def filter_networks(self): + """Filter out networks based on ignore prefixes and normalize network/mask fields.""" + if self.job.ignore_prefixes: + ignore_prefixes = [ + "0.0.0.0/0", + "0.0.0.0/32", + "::/0", + "224.0.0.0/4", + "255.255.255.255", + "ff00::/8", + "169.254.0.0/16", + "fe80::/10", + "127.0.0.0/8", + "::1/128", + ] + else: + ignore_prefixes = [] + + def normalize_network(entry): + network = entry.get("Network", "") + mask = entry.get("Mask", "") + if "/" in network: + entry["normalized_prefix"] = network + elif mask: + entry["normalized_prefix"] = f"{network}/{mask}" + else: + entry["normalized_prefix"] = network + return entry + + def should_ignore(network): + try: + net = ipaddress.ip_network(network, strict=False) + # if net.prefixlen in {32, 128}: + # return True + return any(net == ipaddress.ip_network(ignore, strict=False) for ignore in ignore_prefixes) + except ValueError: + return False + + network_list = self.planning_results("routing-table") + self.filtered_networks = [ + normalize_network(entry) + for entry in network_list + if not should_ignore(normalize_network(entry)["normalized_prefix"]) + ] + return self.filtered_networks + + async def filter_interfaces(self, interfaces): + """Filter interfaces based on the filtered networks.""" + precomputed_filtered_networks = [ + {"network": ipaddress.ip_network(prefix["normalized_prefix"], strict=False), "Vrf": prefix.get("Vrf", None)} + for prefix in self.filtered_networks + ] + + async def normalize_and_find_prefix(entry): + address = entry.get("IP", "") + if address: + if isinstance(address, list): + address = address[0] + if "/" not in address: + address = f"{address}/32" + else: + return None + + try: + network = ipaddress.ip_network(address, strict=False) + entry["normalized_address"] = address + except ValueError: + return None + + for prefix in precomputed_filtered_networks: + if network.subnet_of(prefix["network"]): + entry["prefix"] = str(prefix["network"]) + entry["vrf"] = prefix["Vrf"] + break + + return entry + + # Concurrent execution of tasks + tasks = [normalize_and_find_prefix(entry) for entry in interfaces if entry.get("IP")] + + # Run tasks concurrently + filtered_interfaces = await asyncio.gather(*tasks) + + results = [entry for entry in filtered_interfaces if entry] + + # Filter out None values and return results + return results + + def planning_results(self, planning_name): + """Get planning results for a specific planning name.""" + plannings = self.run_async(self.client.planning.get_plannings()) + planning = next((plan.to_dict() for plan in plannings if plan.slug == planning_name), None) + if not planning: + raise IndexError(f"No planning found for name: {planning_name}") + + search_data = {"planning_id": planning["id"], "unique_results": True, "latest": True} + results = self.run_async(self.client.planning.search_plannings(search_data, limit=30000)) + return results if results else [] + + # Data loading functions + def load_locations(self): + """Load locations from Slurpit.""" + _loc_type = LocationType.objects.get(name="Site") + _status = Status.objects.get(name="Active") + for site in self.run_async(self.client.site.get_sites()): + address = [site.number, site.street, site.city, site.state, site.country, site.county, site.zipcode] + data = { + "name": site.sitename, + "description": site.description, + "latitude": format_latitude(site.latitude), + "longitude": format_latitude(site.longitude), + "contact_phone": site.phonenumber, + "physical_address": "\n".join(address), + "location_type__name": self.job.site_loctype.name, + "status__name": "Active", + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + location = self.location(**data) + self.add(location) + + def load_vendors(self): + """Load manufacturers from Slurpit.""" + vendors = self.unique_vendors() + for vendor in vendors: + manufacturer = self.manufacturer( + name=vendor["brand"], + system_of_record="Slurpit", + last_synced_from_sor=datetime.today().date().isoformat(), + ) + self.add(manufacturer) + + def load_device_types(self): + """Load device types from Slurpit.""" + device_types = self.unique_device_type() + for device_type in device_types: + data = { + "model": device_type["device_type"], + "manufacturer__name": device_type["brand"], + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + model = self.device_type(**data) + self.add(model) + + def load_platforms(self): + """Load platforms from Slurpit.""" + platforms = self.unique_platforms() + for platform in platforms: + platform_data = { + "name": platform, + "manufacturer__name": platforms[platform], + "network_driver": platform, + "napalm_driver": platform, + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + model = self.platform(**platform_data) + self.add(model) + + def load_roles(self): + """Load device roles.""" + role = self.role( + name=constants.DEFAULT_DEVICE_ROLE, + color=constants.DEFAULT_DEVICE_ROLE_COLOR, + content_types=[{"app_label": "dcim", "model": "device"}], + system_of_record="Slurpit", + last_synced_from_sor=datetime.today().date().isoformat(), + ) + self.add(role) + + def load_devices(self): + """Load devices from Slurpit.""" + devices = self.run_async(self.client.device.get_devices()) + for device in devices: + data = { + "name": device.hostname, + "location__name": device.site, + "device_type__manufacturer__name": device.brand, + "device_type__model": device.device_type, + "platform__name": device.device_os, + "role__name": constants.DEFAULT_DEVICE_ROLE, + "status__name": "Active", + "location__location_type__name": self.job.site_loctype.name, + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + if device.ipv4: + self.hostname_to_primary_ip[device.hostname] = device.ipv4 + self.add(self.device(**data)) + + def load_interfaces(self): + """Load interfaces from Slurpit.""" + interfaces = self.planning_results("interfaces") + for interface in interfaces: # pylint: disable=too-many-nested-blocks + if interface.get("Interface", ""): + try: + description = interface.get("Description", "") + mac = "" if isinstance(interface.get("MAC", ""), list) else interface.get("MAC", "") + enabled = "up" in interface.get("Line", "").lower() + data = { + "name": interface["Interface"], + "device__name": interface["hostname"], + "description": description, + "enabled": enabled, + "type": "1000base-t", + "status__name": "Active", + "mtu": 1500, + "mgmt_only": False, + "mac_address": mac_to_format(mac, "MAC_COLON_TWO").upper() if mac else "00:00:00:00:00:01", + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + + ipaddress_info = self.ipaddress_by_device.get( + f"{interface.get('hostname')}__{interface.get('Interface')}" + ) + if ipaddress_info: + for ip_address in ipaddress_info: + interface_match_data = { + "interface__name": data["name"], + "interface__device__name": interface.get("hostname"), + "ip_address__host": ip_address.get("host"), + } + if self.hostname_to_primary_ip.get(interface.get("hostname")) == ip_address.get("host"): + interface_match_data["interface__device__primary_ip4__host"] = ip_address.get("host") + + self.add(self.ipassignment(**interface_match_data)) + + new_interface = self.interface(**data) + self.add(new_interface) + # dev.add_child(new_interface) + except ObjectNotFound: + self.job.logger.warning(f"Device {interface['hostname']} not found") + except ObjectAlreadyExists as err: + self.job.logger.warning(f"Duplicate interface {new_interface.name}. {err}") + + def load_inventory_items(self): + """Load inventory items from Slurpit.""" + inventory_items = self.planning_results("hardware-info") + for item in inventory_items: + if item.get("Name", "") or item.get("Product", ""): + try: + name = item.get("Name") if item.get("Name") else item.get("Product") + dev = self.get(self.device, {"name": item["hostname"]}) + new_item = self.inventory_item( + name=name, + part_id=item.get("Product", ""), + serial=item.get("Serial", ""), + description=item.get("Descr", ""), + device__name=item.get("hostname"), + tags=[{"name": "SSoT Synced from Slurpit"}], + system_of_record="Slurpit", + last_synced_from_sor=datetime.today().date().isoformat(), + ) + self.add(new_item) + dev.add_child(new_item) + except ObjectNotFound: + self.job.logger.warning(f"Device {item['hostname']} not found") + except ObjectAlreadyExists as err: + self.job.logger.warning(f"Unable to load {new_item.name} as it appears to be a duplicate. {err}") + + def load_vlans(self): + """Load VLANs from Slurpit.""" + vlans = self.planning_results("vlans") + for vlan in vlans: + try: + data = { + "vid": vlan.get("Vlan", ""), + "name": vlan.get("Name", ""), + "status__name": "Active", + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + vlan = self.vlan(**data) + self.add(vlan) + except ObjectAlreadyExists as err: + self.job.logger.warning(f"Duplicate VLAN {vlan.name}. {err}") + + def load_vrfs(self): + """Load VRFs from Slurpit.""" + vrfs = {vrf["Vrf"] for vrf in self.planning_results("routing-table") if vrf.get("Vrf", "")} + for vrf in vrfs: + try: + data = { + "name": vrf, + "namespace__name": self.job.namespace.name, + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + new_vrf = self.vrf(**data) + self.add(new_vrf) + except ObjectAlreadyExists as err: + self.job.logger.warning(f"Duplicate VRF {new_vrf.name}. {err}") + + def load_prefixes(self): + """Load prefixes from Slurpit.""" + routes = self.filter_networks() + for route in routes: + try: + data = { + "network": route.get("normalized_prefix", "").split("/")[0], + "prefix_length": route.get("normalized_prefix", "").split("/")[1], + "status__name": "Active", + "namespace__name": self.job.namespace.name, + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + if vrf_name := route.get("Vrf"): + data["vrfs"] = [{"name": vrf_name}] + prefix = self.prefix(**data) + self.add(prefix) + except ObjectAlreadyExists as err: + self.job.logger.warning(f"Duplicate prefix {prefix.network}. {err}") + + def load_ip_addresses(self): + """Load IP addresses from Slurpit.""" + interfaces = self.planning_results("interfaces") + ip_addresses = self.run_async(self.filter_interfaces(interfaces)) + + self.ipaddress_by_device = {} + + for ip_address in ip_addresses: + try: + mask_length = int(ip_address.get("prefix", "").split("/")[1]) if ip_address.get("prefix") else 32 + data = { + "host": ip_address.get("normalized_address", "").split("/")[0], + "mask_length": mask_length, + "status__name": "Active", + "assigned_object__app_label": "interface", + "assigned_object__device__name": ip_address.get("hostname", ""), + "assigned_object__name": ip_address.get("Interface", ""), + "assigned_object__model": "dcim", + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + + try: + self.ipaddress_by_device[f"{ip_address.get('hostname', '')}__{ip_address.get('Interface')}"].append( + data + ) + except KeyError: + self.ipaddress_by_device[f"{ip_address.get('hostname', '')}__{ip_address.get('Interface')}"] = [ + data + ] + if prefix := ip_address.get("prefix"): + network_data = prefix.split("/")[0] + mask_length_data = prefix.split("/")[1] + else: + network_data = ip_address.get("normalized_address").split("/")[0] + mask_length_data = ip_address.get("normalized_address").split("/")[1] + + try: + cached_prefix = self.get(self.prefix, {"network": network_data, "prefix_length": mask_length_data}) + except ObjectNotFound: + cached_prefix = None + + if not cached_prefix: + prefix_data = { + "network": network_data, + "prefix_length": mask_length_data, + "namespace__name": self.job.namespace.name, + "status__name": "Active", + "tags": [{"name": "SSoT Synced from Slurpit"}], + "system_of_record": "Slurpit", + "last_synced_from_sor": datetime.today().date().isoformat(), + } + self.add(self.prefix(**prefix_data)) + new_ip = self.ipaddress(**data) + self.add(new_ip) + except ObjectNotFound: + self.job.logger.warning(f"Interface {ip_address.get('Interface')} not found") + except ObjectAlreadyExists as err: + self.job.logger.warning(f"Duplicate IP address {new_ip.host}. {err}") + + # Unified load function + def load(self): + """Load all data models.""" + self.load_locations() + self.load_vendors() + self.load_device_types() + self.load_platforms() + self.load_roles() + self.load_devices() + self.load_inventory_items() + self.load_vlans() + self.load_vrfs() + self.load_prefixes() + self.load_ip_addresses() + self.load_interfaces() diff --git a/nautobot_ssot/integrations/slurpit/diffsync/models/__init__.py b/nautobot_ssot/integrations/slurpit/diffsync/models/__init__.py new file mode 100644 index 000000000..86314be40 --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/diffsync/models/__init__.py @@ -0,0 +1,35 @@ +"""Slurpit Diffsync Models.""" + +from .models import ( + DeviceModel, + DeviceTypeModel, + InterfaceModel, + InventoryItemModel, + IPAddressModel, + IPAddressToInterfaceModel, + LocationModel, + ManufacturerModel, + NautobotIPAddressToInterfaceModel, + PlatformModel, + PrefixModel, + RoleModel, + VLANModel, + VRFModel, +) + +__all__ = ( + "LocationModel", + "ManufacturerModel", + "DeviceTypeModel", + "PlatformModel", + "RoleModel", + "DeviceModel", + "InterfaceModel", + "InventoryItemModel", + "VLANModel", + "VRFModel", + "PrefixModel", + "IPAddressModel", + "IPAddressToInterfaceModel", + "NautobotIPAddressToInterfaceModel", +) diff --git a/nautobot_ssot/integrations/slurpit/diffsync/models/models.py b/nautobot_ssot/integrations/slurpit/diffsync/models/models.py new file mode 100644 index 000000000..eb88d91a6 --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/diffsync/models/models.py @@ -0,0 +1,410 @@ +# pylint: disable=R0801 +"""Data models for the DiffSync integration.""" + +from typing import List, Optional, Union + +try: + from typing import Annotated # Python>=3.9 +except ImportError: + from typing_extensions import Annotated + + +from nautobot.dcim.models import Device, DeviceType, Interface, InventoryItem, Location, Manufacturer, Platform +from nautobot.extras.models import Role +from nautobot.ipam.models import VLAN, VRF, IPAddress, IPAddressToInterface, Prefix +from netaddr import EUI +from pydantic import field_serializer +from typing_extensions import TypedDict # pylint: disable=C0412 + +from nautobot_ssot.contrib import CustomFieldAnnotation, NautobotModel +from nautobot_ssot.integrations.slurpit import constants +from nautobot_ssot.tests.contrib_base_classes import ContentTypeDict, TagDict + + +class ModelQuerySetMixin: + """Mixin only getting objects that are tagged.""" + + @classmethod + def get_queryset(cls, data): + """Get the queryset for the model.""" + tagged = data.get("sync_slurpit_tagged_only") + if tagged and hasattr(cls._model, "_custom_field_data"): + if hasattr(cls._model, "tags"): + return cls._model.objects.filter(tags__name="SSoT Synced from Slurpit") + return cls._model.objects.filter(_custom_field_data__system_of_record="Slurpit") + return cls._model.objects.all() + + @classmethod + def _get_queryset(cls, data): + """Get the queryset used to load the models data from Nautobot.""" + available_fields = {field.name for field in cls._model._meta.get_fields()} + parameter_names = [ + parameter for parameter in list(cls._identifiers) + list(cls._attributes) if parameter in available_fields + ] + # Here we identify any foreign keys (i.e. fields with '__' in them) so that we can load them directly in the + # first query if this function hasn't been overridden. + prefetch_related_parameters = [parameter.split("__")[0] for parameter in parameter_names if "__" in parameter] + qs = cls.get_queryset(data=data) + return qs.prefetch_related(*prefetch_related_parameters) + + +class LocationModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a Location.""" + + _model = Location + _modelname = "location" + _identifiers = ("name",) + _attributes = ( + "location_type__name", + "description", + "status__name", + "contact_phone", + "physical_address", + "latitude", + "longitude", + "tags", + "system_of_record", + "last_synced_from_sor", + ) + + name: str + description: Optional[str] + location_type__name: str + status__name: str + contact_phone: Optional[str] + physical_address: Optional[str] + latitude: Optional[float] + longitude: Optional[float] + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class ManufacturerModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a Manufacturer.""" + + _model = Manufacturer + _modelname = "manufacturer" + _identifiers = ("name",) + _attributes = ("system_of_record", "last_synced_from_sor") + + name: str + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class DeviceTypeModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a DeviceType.""" + + _model = DeviceType + _modelname = "device_type" + _identifiers = ("model", "manufacturer__name") + _attributes = ("tags", "system_of_record", "last_synced_from_sor") + + model: str + manufacturer__name: str + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class PlatformModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a Platform.""" + + _model = Platform + _modelname = "platform" + _identifiers = ("name", "manufacturer__name") + _attributes = ("network_driver", "napalm_driver", "system_of_record", "last_synced_from_sor") + + name: str + manufacturer__name: str + network_driver: str + napalm_driver: str + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class RoleModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a Role.""" + + _model = Role + _modelname = "role" + _identifiers = ("name",) + _attributes = ( + "content_types", + "color", + "system_of_record", + "last_synced_from_sor", + ) + + name: str + color: Optional[str] + content_types: List[ContentTypeDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + @classmethod + def get_queryset(cls, data=None): + """Get the queryset for the Role model.""" + return cls._model.objects.filter(name=constants.DEFAULT_DEVICE_ROLE) + + +class DeviceModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a Device.""" + + _model = Device + _modelname = "device" + _identifiers = ("name",) + _attributes = ( + "location__name", + "location__parent__name", + "location__location_type__name", + "location__parent__location_type__name", + "device_type__manufacturer__name", + "device_type__model", + "platform__name", + "role__name", + "serial", + "status__name", + "tags", + "system_of_record", + "last_synced_from_sor", + ) + _children = {"inventory_item": "inventory_items"} + + name: str + location__name: Optional[str] = None + location__location_type__name: Optional[str] = None + location__parent__name: Optional[str] = None + location__parent__location_type__name: Optional[str] = None + device_type__manufacturer__name: str + device_type__model: str + platform__name: Optional[str] = None + role__name: str + serial: Optional[str] = "" + status__name: str + inventory_items: List["InventoryItemModel"] = [] + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class InventoryItemModel(ModelQuerySetMixin, NautobotModel): + """Data model representing an InventoryItem.""" + + _model = InventoryItem + _modelname = "inventory_item" + _identifiers = ("name", "device__name") + _attributes = ( + "description", + "part_id", + "serial", + "tags", + "system_of_record", + "last_synced_from_sor", + ) + + name: str + part_id: Optional[str] + serial: Optional[str] + description: Optional[str] + device__name: str + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class VLANModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a VLAN.""" + + _model = VLAN + _modelname = "vlan" + _identifiers = ("vid", "name") + _attributes = ("status__name", "tags", "system_of_record", "last_synced_from_sor") + + vid: int + name: str + status__name: str + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class VRFModel(ModelQuerySetMixin, NautobotModel): + """data model representing a VRF.""" + + _model = VRF + _modelname = "vrf" + _identifiers = ("name",) + _attributes = ("namespace__name", "tags", "system_of_record", "last_synced_from_sor") + + name: str + namespace__name: str + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class VRFDict(TypedDict): + """TypedDict for VRF data.""" + + name: str + + +class PrefixModel(ModelQuerySetMixin, NautobotModel): + """Data model representing a Prefix.""" + + _model = Prefix + _modelname = "prefix" + _identifiers = ("network",) + _attributes = ( + "prefix_length", + "status__name", + "namespace__name", + "vrfs", + "tags", + "system_of_record", + "last_synced_from_sor", + ) + + network: str + prefix_length: int + status__name: str + namespace__name: str + vrfs: List[VRFDict] = [] + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class IPAddressModel(ModelQuerySetMixin, NautobotModel): + """Data model representing an IPAddress.""" + + _model = IPAddress + _modelname = "ipaddress" + _identifiers = ("host", "mask_length") + _attributes = ("status__name", "tags", "system_of_record", "last_synced_from_sor") + + host: str + mask_length: int + status__name: str + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + +class InterfaceModel(ModelQuerySetMixin, NautobotModel): + """Data model representing an Interface.""" + + _model = Interface + _modelname = "interface" + _identifiers = ("name", "device__name") + _attributes = ( + "description", + "enabled", + "mac_address", + "mgmt_only", + "mtu", + "type", + "status__name", + "tags", + "system_of_record", + "last_synced_from_sor", + ) + + device__name: str + description: Optional[str] = "" + enabled: bool + mac_address: Optional[Union[str, EUI]] = "" + mgmt_only: bool + mtu: Optional[int] + name: str + type: str + status__name: str + tags: List[TagDict] = [] + system_of_record: Annotated[str, CustomFieldAnnotation(name="system_of_record", key="system_of_record")] + last_synced_from_sor: Annotated[str, CustomFieldAnnotation(name="last_synced_from_sor", key="last_synced_from_sor")] + + @field_serializer("mac_address") + def serialize_mac_address(self, value): + """Serialize a MAC address to a string.""" + return str(value) + + +class IPAddressToInterfaceModel(ModelQuerySetMixin, NautobotModel): + """Shared data model representing an IPAddressToInterface.""" + + _model = IPAddressToInterface + _modelname = "ipassignment" + _identifiers = ("interface__device__name", "interface__name", "ip_address__host") + _attributes = ( + "interface__device__primary_ip4__host", + "interface__device__primary_ip6__host", + ) + _children = {} + + interface__device__name: str + interface__name: str + ip_address__host: str + interface__device__primary_ip4__host: Optional[str] = None + interface__device__primary_ip6__host: Optional[str] = None + + +class NautobotIPAddressToInterfaceModel(IPAddressToInterfaceModel): + """IPAddressToInterface model for Nautobot.""" + + @classmethod + def create(cls, adapter, ids, attrs): + """Create IPAddressToInterface in Nautobot.""" + if adapter.job.logger.debug: + adapter.job.logger.debug(f"Creating IPAddressToInterface {ids} {attrs}") + intf = Interface.objects.get(name=ids["interface__name"], device__name=ids["interface__device__name"]) + obj = IPAddressToInterface( + ip_address=IPAddress.objects.get(host=ids["ip_address__host"], tenant=intf.device.tenant), + interface=intf, + ) + obj.validated_save() + if ( + attrs.get("interface__device__primary_ip4__host") + and ids["ip_address__host"] == attrs["interface__device__primary_ip4__host"] + ): + obj.interface.device.primary_ip4 = IPAddress.objects.get( + host=attrs["interface__device__primary_ip4__host"], + tenant=obj.interface.device.tenant, + ) + obj.interface.device.validated_save() + if ( + attrs.get("interface__device__primary_ip6__host") + and ids["ip_address__host"] == attrs["interface__device__primary_ip6__host"] + ): + obj.interface.device.primary_ip6 = IPAddress.objects.get( + host=attrs["interface__device__primary_ip6__host"], + tenant=obj.interface.device.tenant, + ) + obj.interface.device.validated_save() + return super().create_base(adapter=adapter, ids=ids, attrs=attrs) + + def update(self, attrs): + """Update IPAddressToInterface in Nautobot.""" + obj = self.get_from_db() + if ( + attrs.get("interface__device__primary_ip4__host") + and self.ip_address__host == attrs["interface__device__primary_ip4__host"] + ): + obj.interface.device.primary_ip4 = IPAddress.objects.get( + host=attrs["interface__device__primary_ip4__host"], tenant=obj.interface.device.tenant + ) + obj.interface.device.validated_save() + if ( + attrs.get("interface__device__primary_ip6__host") + and self.ip_address__host == attrs["interface__device__primary_ip6__host"] + ): + obj.interface.device.primary_ip6 = IPAddress.objects.get( + host=attrs["interface__device__primary_ip6__host"], tenant=obj.interface.device.tenant + ) + obj.interface.device.validated_save() + return super().update_base(attrs) + + def delete(self): + """Delete IPAddressToInterface in Nautobot.""" + return super().delete_base() diff --git a/nautobot_ssot/integrations/slurpit/jobs.py b/nautobot_ssot/integrations/slurpit/jobs.py new file mode 100644 index 000000000..06590a906 --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/jobs.py @@ -0,0 +1,145 @@ +# pylint: disable=R0801 +"""Slurpit DataSource job class.""" + +import slurpit +from django.contrib.contenttypes.models import ContentType +from django.templatetags.static import static +from django.urls import reverse +from nautobot.dcim.models import Device, LocationType +from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices +from nautobot.extras.jobs import BooleanVar, Job, ObjectVar +from nautobot.extras.models import ExternalIntegration +from nautobot.ipam.models import Namespace + +from nautobot_ssot.integrations.slurpit import constants +from nautobot_ssot.integrations.slurpit.diffsync.adapters.nautobot import NautobotDiffSyncAdapter +from nautobot_ssot.integrations.slurpit.diffsync.adapters.slurpit import SlurpitAdapter +from nautobot_ssot.jobs.base import DataMapping, DataSource + + +class SlurpitDataSource(DataSource, Job): # pylint: disable=too-many-instance-attributes + """SSoT Job class.""" + + credentials = ObjectVar( + model=ExternalIntegration, + queryset=ExternalIntegration.objects.all(), + display_field="name", + required=True, + label="Slurpit Instance", + ) + + site_loctype = ObjectVar( + model=LocationType, + queryset=LocationType.objects.all(), + display_field="name", + required=False, + label="Site LocationType", + description="LocationType to use for imported Sites from Slurpit. If unspecified, will revert to Site LocationType.", + ) + + namespace = ObjectVar( + model=Namespace, + queryset=Namespace.objects.all(), + display_field="name", + required=False, + label="IPAM Namespace", + description="Namespace to use for imported IPAM objects from Slurpit. If unspecified, will revert to Global Namespace.", + ) + + ignore_prefixes = BooleanVar( + default=True, + label="Ignore Routing Table Prefixes", + description="Ignore some prefixes that are used for routing tables and not IPAM such as 0.0.0.0/0.", + ) + + sync_slurpit_tagged_only = BooleanVar( + default=True, + label="Sync tagged objects only", + description="Only sync objects that have the 'SSoT Synced from Slurpit' Tag.", + ) + + kwargs = {} + + class Meta: + """Metadata for the SlurpitDataSource job.""" + + name = "Slurpit Data Source" + description = "Sync information from Slurpit to Nautobot." + data_source = "Slurpit" + data_source_icon = static("nautobot_ssot_slurpit/slurpit.png") + + @classmethod + def data_mappings(cls): + """List describing the data mappings involved in this DataSource.""" + return ( + DataMapping("Site", None, "Location", reverse("dcim:location_list")), + DataMapping("Manufacturer", None, "Manufacturer", reverse("dcim:manufacturer_list")), + DataMapping("Device Type", None, "Device Type", reverse("dcim:devicetype_list")), + DataMapping("Platform", None, "Platform", reverse("dcim:platform_list")), + DataMapping("Role", None, "Role", reverse("extras:role_list")), + DataMapping("Device", None, "Device", reverse("dcim:device_list")), + DataMapping("Interface", None, "Interface", reverse("dcim:interface_list")), + DataMapping("IP Address", None, "IP Address", reverse("ipam:ipaddress_list")), + DataMapping("Prefix", None, "Prefix", reverse("ipam:prefix_list")), + DataMapping("VLAN", None, "VLAN", reverse("ipam:vlan_list")), + DataMapping("VRF", None, "VRF", reverse("ipam:vrf_list")), + ) + + @classmethod + def config_information(cls): + """Dictionary describing the configuration options for this DataSource.""" + return { + "Default Device Role": constants.DEFAULT_DEVICE_ROLE, + "Default Device Role Color": constants.DEFAULT_DEVICE_ROLE_COLOR, + "Default Device Status": constants.DEFAULT_DEVICE_STATUS, + "Default Device Status Color": constants.DEFAULT_DEVICE_STATUS_COLOR, + } + + def load_source_adapter(self): + """Load the source adapter.""" + self.logger.info("Loading source adapter: Slurpit") + token = self.credentials.secrets_group.get_secret_value( + access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP, secret_type=SecretsGroupSecretTypeChoices.TYPE_TOKEN + ) + client = slurpit.api(url=self.credentials.remote_url, api_key=token, verify=self.credentials.verify_ssl) + self.source_adapter = SlurpitAdapter(api_client=client, job=self) + self.source_adapter.load() + + def load_target_adapter(self): + """Load the target adapter.""" + self.logger.info("Loading target adapter: Nautobot") + self.target_adapter = NautobotDiffSyncAdapter(job=self) + self.target_adapter.load() + + # pylint: disable-next=too-many-arguments, arguments-differ + def run( + self, + dryrun, + memory_profiling, + credentials, + site_loctype, + namespace, + ignore_prefixes, + sync_slurpit_tagged_only, + *args, + **kwargs, + ): + """Run the Slurpit DataSource job.""" + self.logger.info("Running Slurpit DataSource job") + self.credentials = credentials + self.site_loctype = site_loctype + if not self.site_loctype: + self.site_loctype = LocationType.objects.get_or_create(name="Site")[0] + self.site_loctype.content_types.add(ContentType.objects.get_for_model(Device)) + self.namespace = namespace + if not self.namespace: + self.namespace = Namespace.objects.get(name="Global") + self.ignore_prefixes = ignore_prefixes + + self.kwargs = { + "sync_slurpit_tagged_only": sync_slurpit_tagged_only, + } + super().run(dryrun=dryrun, memory_profiling=memory_profiling, *args, **kwargs) + + +jobs = [SlurpitDataSource] diff --git a/nautobot_ssot/integrations/slurpit/signals.py b/nautobot_ssot/integrations/slurpit/signals.py new file mode 100644 index 000000000..c8309b327 --- /dev/null +++ b/nautobot_ssot/integrations/slurpit/signals.py @@ -0,0 +1,102 @@ +# pylint: disable=duplicate-code +"""Signal handlers for Slurpit integration.""" + +from typing import List, Optional + +from nautobot.core.choices import ColorChoices +from nautobot.core.signals import nautobot_database_ready +from nautobot.extras.choices import CustomFieldTypeChoices + + +def register_signals(sender): + """Register signals for Slurpit integration.""" + nautobot_database_ready.connect(nautobot_database_ready_callback, sender=sender) + + +def create_custom_field(key: str, label: str, models: List, apps, cf_type: Optional[str] = "type_date"): + """Create custom field on a given model instance type. + + Args: + key (str): Natural key + label (str): Label description + models (List): List of Django Models + apps: Django Apps + cf_type: (str, optional): Type of Field. Supports 'type_text' or 'type_date'. Defaults to 'type_date'. + """ + ContentType = apps.get_model("contenttypes", "ContentType") # pylint:disable=invalid-name + CustomField = apps.get_model("extras", "CustomField") # pylint:disable=invalid-name + if cf_type == "type_date": + custom_field, _ = CustomField.objects.get_or_create( + key=key, + type=CustomFieldTypeChoices.TYPE_DATE, + label=label, + ) + else: + custom_field, _ = CustomField.objects.get_or_create( + key=key, + type=CustomFieldTypeChoices.TYPE_TEXT, + label=label, + ) + for model in models: + custom_field.content_types.add(ContentType.objects.get_for_model(model)) + custom_field.save() + + +def nautobot_database_ready_callback(sender, *, apps, **kwargs): # pylint: disable=unused-argument + """Callback function triggered by the nautobot_database_ready signal when the Nautobot database is fully ready.""" + # pylint: disable=invalid-name, too-many-locals + Device = apps.get_model("dcim", "Device") + DeviceType = apps.get_model("dcim", "DeviceType") + Manufacturer = apps.get_model("dcim", "Manufacturer") + Platform = apps.get_model("dcim", "Platform") + Role = apps.get_model("extras", "Role") + InventoryItem = apps.get_model("dcim", "InventoryItem") + Interface = apps.get_model("dcim", "Interface") + IPAddress = apps.get_model("ipam", "IPAddress") + Location = apps.get_model("dcim", "Location") + VLAN = apps.get_model("ipam", "VLAN") + VRF = apps.get_model("ipam", "VRF") + Prefix = apps.get_model("ipam", "Prefix") + ContentType = apps.get_model("contenttypes", "ContentType") + Tag = apps.get_model("extras", "Tag") + + synced_tag, _ = Tag.objects.get_or_create( + name="SSoT Synced from Slurpit", + defaults={ + "description": "Object synced at some point from Slurpit to Nautobot", + "color": ColorChoices.COLOR_LIGHT_GREEN, + }, + ) + synced_tag.content_types.add(ContentType.objects.get_for_model(Device)) + synced_tag.content_types.add(ContentType.objects.get_for_model(DeviceType)) + synced_tag.content_types.add(ContentType.objects.get_for_model(InventoryItem)) + synced_tag.content_types.add(ContentType.objects.get_for_model(Interface)) + synced_tag.content_types.add(ContentType.objects.get_for_model(IPAddress)) + synced_tag.content_types.add(ContentType.objects.get_for_model(Location)) + synced_tag.content_types.add(ContentType.objects.get_for_model(VLAN)) + synced_tag.content_types.add(ContentType.objects.get_for_model(VRF)) + synced_tag.content_types.add(ContentType.objects.get_for_model(Prefix)) + + Tag.objects.get_or_create( + name="SSoT Safe Delete", + defaults={ + "description": "Safe Delete Mode tag to flag an object, but not delete from Nautobot.", + "color": ColorChoices.COLOR_RED, + }, + ) + synced_from_models = [ + Device, + DeviceType, + InventoryItem, + Interface, + IPAddress, + Location, + VLAN, + VRF, + Prefix, + Manufacturer, + Platform, + Role, + ] + create_custom_field("system_of_record", "System of Record", synced_from_models, apps=apps, cf_type="type_text") + create_custom_field("last_synced_from_sor", "Last sync from System of Record", synced_from_models, apps=apps) diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index 972f8952d..a2d775cf4 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -193,7 +193,6 @@ def record_memory_trace(step: str): self.logger.info("As `dryrun` is set, skipping the actual data sync.") else: self.logger.info("Syncing from %s to %s...", self.source_adapter, self.target_adapter) - print("I'm executing the sync now") self.execute_sync() execute_sync_time = datetime.now() self.sync.sync_time = execute_sync_time - calculate_diff_time diff --git a/nautobot_ssot/migrations/0007_replace_dashed_custom_fields.py b/nautobot_ssot/migrations/0007_replace_dashed_custom_fields.py index 9bcd73088..5875846b9 100644 --- a/nautobot_ssot/migrations/0007_replace_dashed_custom_fields.py +++ b/nautobot_ssot/migrations/0007_replace_dashed_custom_fields.py @@ -1,7 +1,4 @@ from django.db import migrations -from nautobot.dcim.models import Device, DeviceType, Interface, Location, Manufacturer -from nautobot.extras.models import Role -from nautobot.ipam.models import VLAN, IPAddress CF_KEY_CHANGE_MAP = { "ssot_synced_to_servicenow": "ssot-synced-to-servicenow", @@ -12,6 +9,7 @@ def replace_dashed_custom_fields(apps, schema_editor): + """Replace dashes in CustomField keys with underscore.""" CustomField = apps.get_model("extras", "customfield") for new_key, old_key in CF_KEY_CHANGE_MAP.items(): @@ -23,7 +21,17 @@ def replace_dashed_custom_fields(apps, schema_editor): custom_field.key = new_key custom_field.save() - for model in [Device, DeviceType, Interface, Manufacturer, Location, VLAN, Role, IPAddress]: + for app, model in [ + ("dcim", "Device"), + ("dcim", "DeviceType"), + ("dcim", "Interface"), + ("dcim", "Manufacturer"), + ("dcim", "Location"), + ("ipam", "VLAN"), + ("extras", "Role"), + ("ipam", "IPAddress"), + ]: + model = apps.get_model(app, model) cf_list = [] for instance in model.objects.all(): for new_cf, old_cf in CF_KEY_CHANGE_MAP.items(): diff --git a/nautobot_ssot/static/nautobot_ssot_citrix_adm/citrix_logo.png b/nautobot_ssot/static/nautobot_ssot_citrix_adm/citrix_logo.png new file mode 100644 index 000000000..1c357afd1 Binary files /dev/null and b/nautobot_ssot/static/nautobot_ssot_citrix_adm/citrix_logo.png differ diff --git a/nautobot_ssot/static/nautobot_ssot_slurpit/slurpit.png b/nautobot_ssot/static/nautobot_ssot_slurpit/slurpit.png new file mode 100644 index 000000000..7329baee8 Binary files /dev/null and b/nautobot_ssot/static/nautobot_ssot_slurpit/slurpit.png differ diff --git a/nautobot_ssot/static/nautobot_ssot_slurpit/slurpit_logo.png b/nautobot_ssot/static/nautobot_ssot_slurpit/slurpit_logo.png new file mode 100644 index 000000000..a6b8bc3c3 Binary files /dev/null and b/nautobot_ssot/static/nautobot_ssot_slurpit/slurpit_logo.png differ diff --git a/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py b/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py index dabef2a20..07fe49abc 100644 --- a/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py +++ b/nautobot_ssot/tests/aristacv/test_utils_cloudvision.py @@ -129,7 +129,7 @@ def test_get_tags_by_type(self): device_tag_stub.TagServiceStub.return_value.GetAll.return_value = [mock_tag] with patch("nautobot_ssot.integrations.aristacv.utils.cloudvision.tag_services", device_tag_stub): - results = cloudvision.get_tags_by_type(client=self.client) + results = cloudvision.get_tags_by_type(client=self.client, logger=MagicMock()) expected = [{"label": "test", "value": "test"}] self.assertEqual(results, expected) diff --git a/nautobot_ssot/tests/bootstrap/fixtures/global_settings.json b/nautobot_ssot/tests/bootstrap/fixtures/global_settings.json index 68eb82261..af409885c 100644 --- a/nautobot_ssot/tests/bootstrap/fixtures/global_settings.json +++ b/nautobot_ssot/tests/bootstrap/fixtures/global_settings.json @@ -211,6 +211,14 @@ "napalm_driver": "", "napalm_arguments": {}, "description": "Arista Devices" + }, + { + "name": "linux", + "manufacturer": "", + "network_driver": "", + "napalm_driver": "", + "napalm_arguments": {}, + "description": "Linux Devices" } ], "location_type": [ @@ -281,6 +289,20 @@ "contact_phone": "", "contact_email": "", "tags": [] + }, + { + "name": "Southwest", + "location_type": "Region", + "status": "Active", + "facility": "OR1", + "time_zone": "", + "description": "", + "physical_address": "", + "shipping_address": "", + "contact_name": "", + "contact_phone": "", + "contact_email": "", + "tags": [] } ], "team": [ diff --git a/nautobot_ssot/tests/bootstrap/test_setup.py b/nautobot_ssot/tests/bootstrap/test_setup.py index 8a56c11e7..118030959 100644 --- a/nautobot_ssot/tests/bootstrap/test_setup.py +++ b/nautobot_ssot/tests/bootstrap/test_setup.py @@ -695,7 +695,9 @@ def _setup_manufacturers(self): def _setup_platforms(self): for _platform in GLOBAL_YAML_SETTINGS["platform"]: - _manufac = Manufacturer.objects.get(name=_platform["manufacturer"]) + _manufac = None + if _platform["manufacturer"]: + _manufac = Manufacturer.objects.get(name=_platform["manufacturer"]) _platf = Platform.objects.create( name=_platform["name"], manufacturer=_manufac, diff --git a/nautobot_ssot/tests/citrix_adm/__init__.py b/nautobot_ssot/tests/citrix_adm/__init__.py new file mode 100644 index 000000000..1545f7692 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/__init__.py @@ -0,0 +1 @@ +"""Unit tests for Citrix ADM integration.""" diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/__init__.py b/nautobot_ssot/tests/citrix_adm/fixtures/__init__.py new file mode 100644 index 000000000..fda7f67d4 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/__init__.py @@ -0,0 +1,22 @@ +"""Fixtures for tests.""" + +import json + + +def load_json(path): + """Load a json file.""" + with open(path, encoding="utf-8") as file: + return json.loads(file.read()) + + +SITE_FIXTURE_SENT = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_sites_sent.json") +SITE_FIXTURE_RECV = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_sites_recv.json") +DEVICE_FIXTURE_SENT = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_devices_sent.json") +DEVICE_FIXTURE_RECV = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_devices_recv.json") +VLAN_FIXTURE_SENT = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_sent.json") +VLAN_FIXTURE_RECV = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_recv.json") +NSIP6_FIXTURE_SENT = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_sent.json") +NSIP6_FIXTURE_RECV = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_recv.json") +ADM_DEVICE_MAP_FIXTURE = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/adm_device_map.json") +NSIP_FIXTURE_SENT = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_sent.json") +NSIP_FIXTURE_RECV = load_json("./nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_recv.json") diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/adm_device_map.json b/nautobot_ssot/tests/citrix_adm/fixtures/adm_device_map.json new file mode 100644 index 000000000..51b8895c5 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/adm_device_map.json @@ -0,0 +1,30 @@ +{ + "TEST": { + "hostname": "TEST", + "ip_address": "192.168.1.3", + "mgmt_ip_address": "192.168.1.5", + "netmask": "255.255.255.0", + "ports": [ + { + "ipaddress": "192.168.1.3", + "netmask": 24, + "port": "10/1", + "version": 4, + "vlan": "5", + "tags": [ + "NSIP" + ] + }, + { + "ipaddress": "192.168.1.5", + "netmask": 24, + "port": "0/1", + "version": 4, + "vlan": "5", + "tags": [ + "MGMT" + ] + } + ] + } +} \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_devices_recv.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_devices_recv.json new file mode 100644 index 000000000..3cb684399 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_devices_recv.json @@ -0,0 +1,62 @@ +[ + { + "gateway": "56.881.284.240", + "mgmt_ip_address": "128.08.54.48", + "description": "", + "serialnumber": "78NKST0SJ2", + "display_name": "172.18.77.78-172.18.77.79", + "type": "nsvpx", + "netmask": "255.255.255.192", + "ha_ip_address": "172.18.77.78", + "datacenter_id": "28aa2970-0160-4860-aca8-a85f89268803", + "hostname": "UYLLBFRCXM55-EA", + "ip_address": "172.18.77.79", + "version": "NetScaler NS12.1: Build 63.22.nc, Date: Oct 13 2021, 01:18:50 (64-bit)", + "instance_state": "Up" + }, + { + "gateway": "48.14.5.5", + "mgmt_ip_address": "85.52.0.128", + "description": "", + "serialnumber": "44WDBA1XS3", + "display_name": "10.62.7.168-10.62.7.169", + "type": "nsvpx", + "netmask": "255.255.255.0", + "ha_ip_address": "10.62.7.168", + "datacenter_id": "28aa2970-0160-4860-aca8-a85f89268803", + "hostname": "OLQE-WHOO-KAL-WKH-SndJhcc3-X", + "ip_address": "10.62.7.169", + "version": "NetScaler NS13.1: Build 37.38.nc, Date: Nov 23 2022, 04:42:36 (64-bit)", + "instance_state": "Up" + }, + { + "gateway": "64.131.96.4", + "mgmt_ip_address": "13.250.66.32", + "description": "", + "serialnumber": "AA7D14BP22", + "display_name": "10.220.11.138-10.220.11.139", + "type": "nsvpx", + "netmask": "255.255.255.192", + "ha_ip_address": "10.220.11.138", + "datacenter_id": "4f91c698-4a2f-4e08-bc57-827d6531a444", + "hostname": "AGSPAMKMRN53", + "ip_address": "10.220.11.139", + "version": "NetScaler NS12.1: Build 57.18.nc, Date: Jun 9 2020, 11:42:01 (64-bit)", + "instance_state": "Up" + }, + { + "gateway": "1.81.7.1", + "mgmt_ip_address": "65.61.6.121", + "description": "", + "serialnumber": "98ATECSRNJ", + "display_name": "10.62.7.111-10.62.7.112", + "type": "nsvpx", + "netmask": "255.255.255.0", + "ha_ip_address": "10.62.7.110", + "datacenter_id": "28aa2970-0160-4860-aca8-a85f89268803", + "hostname": "OGI-MSCI-IMS-Mctdgj-Pqsf-M", + "ip_address": "10.62.7.111", + "version": "NetScaler NS12.1: Build 63.22.nc, Date: Oct 13 2021, 01:18:50 (64-bit)", + "instance_state": "Up" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_devices_sent.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_devices_sent.json new file mode 100644 index 000000000..dcf968806 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_devices_sent.json @@ -0,0 +1,73 @@ +{ + "errorcode": 0, + "message": "Done", + "operation": "get", + "resourceType": "managed_device", + "username": "a1pnautobot", + "tenant_name": "Owner", + "tenant_id": "ed2aa84e-b157-4a2b-9a3e-489149fb7842", + "resrc_total_count": 0, + "resourceName": "", + "managed_device": [ + { + "gateway": "56.881.284.240", + "mgmt_ip_address": "128.08.54.48", + "description": "", + "serialnumber": "78NKST0SJ2", + "display_name": "172.18.77.78-172.18.77.79", + "type": "nsvpx", + "netmask": "255.255.255.192", + "ha_ip_address": "172.18.77.78", + "datacenter_id": "28aa2970-0160-4860-aca8-a85f89268803", + "hostname": "UYLLBFRCXM55-EA", + "ip_address": "172.18.77.79", + "version": "NetScaler NS12.1: Build 63.22.nc, Date: Oct 13 2021, 01:18:50 (64-bit)", + "instance_state": "Up" + }, + { + "gateway": "48.14.5.5", + "mgmt_ip_address": "85.52.0.128", + "description": "", + "serialnumber": "44WDBA1XS3", + "display_name": "10.62.7.168-10.62.7.169", + "type": "nsvpx", + "netmask": "255.255.255.0", + "ha_ip_address": "10.62.7.168", + "datacenter_id": "28aa2970-0160-4860-aca8-a85f89268803", + "hostname": "OLQE-WHOO-KAL-WKH-SndJhcc3-X", + "ip_address": "10.62.7.169", + "version": "NetScaler NS13.1: Build 37.38.nc, Date: Nov 23 2022, 04:42:36 (64-bit)", + "instance_state": "Up" + }, + { + "gateway": "64.131.96.4", + "mgmt_ip_address": "13.250.66.32", + "description": "", + "serialnumber": "AA7D14BP22", + "display_name": "10.220.11.138-10.220.11.139", + "type": "nsvpx", + "netmask": "255.255.255.192", + "ha_ip_address": "10.220.11.138", + "datacenter_id": "4f91c698-4a2f-4e08-bc57-827d6531a444", + "hostname": "AGSPAMKMRN53", + "ip_address": "10.220.11.139", + "version": "NetScaler NS12.1: Build 57.18.nc, Date: Jun 9 2020, 11:42:01 (64-bit)", + "instance_state": "Up" + }, + { + "gateway": "1.81.7.1", + "mgmt_ip_address": "65.61.6.121", + "description": "", + "serialnumber": "98ATECSRNJ", + "display_name": "10.62.7.111-10.62.7.112", + "type": "nsvpx", + "netmask": "255.255.255.0", + "ha_ip_address": "10.62.7.110", + "datacenter_id": "28aa2970-0160-4860-aca8-a85f89268803", + "hostname": "OGI-MSCI-IMS-Mctdgj-Pqsf-M", + "ip_address": "10.62.7.111", + "version": "NetScaler NS12.1: Build 63.22.nc, Date: Oct 13 2021, 01:18:50 (64-bit)", + "instance_state": "Up" + } + ] +} \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_recv.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_recv.json new file mode 100644 index 000000000..34f7fc162 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_recv.json @@ -0,0 +1,171 @@ +[ + [ + { + "ipv6address": "fe80::1234:5678:9abc:dev1/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA" + } + ], + [ + { + "ipv6address": "fe80::1234:5678:9abc:dev2/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA", + "ndowner": "255", + "operationalndowner": "0", + "mptcpadvertise": "NO" + } + ], + [ + { + "ipv6address": "fe80::1234:5678:9abc:dev3/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA", + "ndowner": "255", + "operationalndowner": "0", + "mptcpadvertise": "NO" + } + ], + [ + { + "ipv6address": "fe80::1234:5678:9abc:dev4/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA", + "ndowner": "255", + "operationalndowner": "0", + "mptcpadvertise": "NO" + } + ] +] \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_sent.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_sent.json new file mode 100644 index 000000000..a93462870 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip6_sent.json @@ -0,0 +1,191 @@ +[ + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "nsip6": [ + { + "ipv6address": "fe80::1234:5678:9abc:dev1/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA" + } + ] + }, + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "nsip6": [ + { + "ipv6address": "fe80::1234:5678:9abc:dev2/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA", + "ndowner": "255", + "operationalndowner": "0", + "mptcpadvertise": "NO" + } + ] + }, + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "nsip6": [ + { + "ipv6address": "fe80::1234:5678:9abc:dev3/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA", + "ndowner": "255", + "operationalndowner": "0", + "mptcpadvertise": "NO" + } + ] + }, + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "nsip6": [ + { + "ipv6address": "fe80::1234:5678:9abc:dev4/64", + "td": "0", + "scope": "link-local", + "iptype": [ + "NSIP" + ], + "vlan": "1", + "nd": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "state": "ENABLED", + "curstate": "ACTIVE", + "map": "0.0.0.0", + "decrementhoplimit": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "ip6hostrtgw": "::", + "metric": 0, + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "ospf6lsatype": "EXTERNAL", + "ownernode": "255", + "ownerdownresponse": "YES", + "systemtype": "HA", + "ndowner": "255", + "operationalndowner": "0", + "mptcpadvertise": "NO" + } + ] + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_recv.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_recv.json new file mode 100644 index 000000000..be9df5b8c --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_recv.json @@ -0,0 +1,87 @@ +[ + { + "ipaddress": "192.168.0.1", + "td": "0", + "type": "NSIP", + "netmask": "255.255.255.0", + "flags": "40", + "arp": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "SECUREONLY", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "decrementttl": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "hostrtgwact": "0.0.0.0", + "metric": 0, + "ospfareaval": "0", + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "vipvsrvrrhiactivecount": "0", + "vipvsrvrrhiactiveupcount": "0", + "ospflsatype": "TYPE5", + "state": "ENABLED", + "freeports": "1032094", + "iptype": [ + "NSIP" + ], + "icmpresponse": "NONE", + "ownernode": "255", + "arpresponse": "NONE", + "ownerdownresponse": "YES" + }, + { + "ipaddress": "192.168.0.2", + "td": "0", + "type": "SNIP", + "netmask": "255.255.255.0", + "flags": "4", + "arp": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "decrementttl": "DISABLED", + "dynamicrouting": "DISABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "hostrtgwact": "0.0.0.0", + "metric": 0, + "ospfareaval": "0", + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": true, + "vipvsercount": "0", + "vipvserdowncount": "0", + "vipvsrvrrhiactivecount": "0", + "vipvsrvrrhiactiveupcount": "0", + "ospflsatype": "TYPE5", + "state": "ENABLED", + "freeports": "1032080", + "iptype": [ + "SNIP", + "GSLBsiteIP" + ], + "icmpresponse": "NONE", + "ownernode": "255", + "arpresponse": "NONE", + "ownerdownresponse": "YES" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_sent.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_sent.json new file mode 100644 index 000000000..3cb665ece --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_nsip_sent.json @@ -0,0 +1,92 @@ +{ + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "nsip": [ + { + "ipaddress": "192.168.0.1", + "td": "0", + "type": "NSIP", + "netmask": "255.255.255.0", + "flags": "40", + "arp": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "SECUREONLY", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "decrementttl": "DISABLED", + "dynamicrouting": "ENABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "hostrtgwact": "0.0.0.0", + "metric": 0, + "ospfareaval": "0", + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": false, + "vipvsercount": "0", + "vipvserdowncount": "0", + "vipvsrvrrhiactivecount": "0", + "vipvsrvrrhiactiveupcount": "0", + "ospflsatype": "TYPE5", + "state": "ENABLED", + "freeports": "1032094", + "iptype": [ + "NSIP" + ], + "icmpresponse": "NONE", + "ownernode": "255", + "arpresponse": "NONE", + "ownerdownresponse": "YES" + }, + { + "ipaddress": "192.168.0.2", + "td": "0", + "type": "SNIP", + "netmask": "255.255.255.0", + "flags": "4", + "arp": "ENABLED", + "icmp": "ENABLED", + "vserver": "DISABLED", + "telnet": "ENABLED", + "ssh": "ENABLED", + "gui": "ENABLED", + "snmp": "ENABLED", + "ftp": "ENABLED", + "mgmtaccess": "ENABLED", + "restrictaccess": "DISABLED", + "decrementttl": "DISABLED", + "dynamicrouting": "DISABLED", + "hostroute": "DISABLED", + "advertiseondefaultpartition": "DISABLED", + "networkroute": "DISABLED", + "tag": "0", + "hostrtgwact": "0.0.0.0", + "metric": 0, + "ospfareaval": "0", + "vserverrhilevel": "ONE_VSERVER", + "viprtadv2bsd": true, + "vipvsercount": "0", + "vipvserdowncount": "0", + "vipvsrvrrhiactivecount": "0", + "vipvsrvrrhiactiveupcount": "0", + "ospflsatype": "TYPE5", + "state": "ENABLED", + "freeports": "1032080", + "iptype": [ + "SNIP", + "GSLBsiteIP" + ], + "icmpresponse": "NONE", + "ownernode": "255", + "arpresponse": "NONE", + "ownerdownresponse": "YES" + } + ] +} \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_sites_recv.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_sites_recv.json new file mode 100644 index 000000000..b7c246b8c --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_sites_recv.json @@ -0,0 +1,57 @@ +[ + { + "city": "", + "zipcode": "", + "type": "1", + "name": "Default", + "region": "", + "country": "", + "longitude": "-1", + "id": "0c686cbd-dd4e-4402-afb3-0294f5b4cb93", + "latitude": "-1" + }, + { + "city": "Atlanta", + "zipcode": "30009", + "type": "1", + "name": "Delta HQ", + "region": "East", + "country": "USA", + "longitude": "-84.320000", + "id": "28aa2970-0160-4860-aca8-a85f89268803", + "latitude": "34.030000" + }, + { + "city": "Las Vegas", + "zipcode": "89044", + "type": "1", + "name": "ARIA", + "region": "West", + "country": "USA", + "longitude": "-115.1768183", + "id": "4f91c698-4a2f-4e08-bc57-827d6531a444", + "latitude": "36.1071859" + }, + { + "city": "Austin", + "zipcode": "78732", + "type": "1", + "name": "Apple Inc.", + "region": "South", + "country": "USA", + "longitude": "-97.734766", + "id": "6fa14721-299c-4d5a-b584-75a8a3dddadc", + "latitude": "30.432546" + }, + { + "city": "New York City", + "zipcode": "10018", + "type": "1", + "name": "NTC Corporate HQ", + "region": "North", + "country": "USA", + "longitude": "-73.989429", + "id": "7d29e100-ae0c-4580-ba86-b72df0b6cfd8", + "latitude": "40.753146" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_sites_sent.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_sites_sent.json new file mode 100644 index 000000000..dfbcdd286 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_sites_sent.json @@ -0,0 +1,68 @@ +{ + "errorcode": 0, + "message": "Done", + "operation": "get", + "resourceType": "mps_datacenter", + "username": "r1ohvxgmgbt", + "tenant_name": "Owner", + "tenant_id": "ce7dl08m-m175-0e3v-1o1u-181043bp8045", + "resrc_total_count": 0, + "resourceName": "", + "mps_datacenter": [ + { + "city": "", + "zipcode": "", + "type": "1", + "name": "Default", + "region": "", + "country": "", + "longitude": "-1", + "id": "0c686cbd-dd4e-4402-afb3-0294f5b4cb93", + "latitude": "-1" + }, + { + "city": "Atlanta", + "zipcode": "30009", + "type": "1", + "name": "Delta HQ", + "region": "East", + "country": "USA", + "longitude": "-84.320000", + "id": "28aa2970-0160-4860-aca8-a85f89268803", + "latitude": "34.030000" + }, + { + "city": "Las Vegas", + "zipcode": "89044", + "type": "1", + "name": "ARIA", + "region": "West", + "country": "USA", + "longitude": "-115.1768183", + "id": "4f91c698-4a2f-4e08-bc57-827d6531a444", + "latitude": "36.1071859" + }, + { + "city": "Austin", + "zipcode": "78732", + "type": "1", + "name": "Apple Inc.", + "region": "South", + "country": "USA", + "longitude": "-97.734766", + "id": "6fa14721-299c-4d5a-b584-75a8a3dddadc", + "latitude": "30.432546" + }, + { + "city": "New York City", + "zipcode": "10018", + "type": "1", + "name": "NTC Corporate HQ", + "region": "North", + "country": "USA", + "longitude": "-73.989429", + "id": "7d29e100-ae0c-4580-ba86-b72df0b6cfd8", + "latitude": "40.753146" + } + ] +} \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_recv.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_recv.json new file mode 100644 index 000000000..b5c1a6877 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_recv.json @@ -0,0 +1,146 @@ +[ + [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "80", + "vlan_interface_binding": [ + { + "id": "80", + "ifnum": "10/1", + "tagged": true, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "80", + "ipaddress": "192.168.0.1", + "netmask": "255.255.255.0", + "td": "0", + "stateflag": "1", + "ownergroup": "" + } + ] + } + ], + [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "10/1", + "tagged": false, + "stateflag": "4" + }, + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "363", + "vlan_interface_binding": [ + { + "id": "363", + "ifnum": "10/1", + "tagged": true, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "363", + "ipaddress": "192.168.1.2", + "netmask": "255.255.255.0", + "stateflag": "1" + } + ] + } + ], + [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "5", + "vlan_interface_binding": [ + { + "id": "5", + "ifnum": "1/1", + "tagged": false, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "5", + "ipaddress": "192.168.1.3", + "netmask": "255.255.255.224", + "stateflag": "1" + } + ] + } + ], + [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "10/1", + "tagged": false, + "stateflag": "4" + }, + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "250", + "vlan_interface_binding": [ + { + "id": "250", + "ifnum": "10/1", + "tagged": true, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "250", + "ipaddress": "192.168.1.4", + "netmask": "255.255.255.0", + "td": "0", + "stateflag": "1", + "ownergroup": "" + } + ] + } + ] +] \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_sent.json b/nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_sent.json new file mode 100644 index 000000000..88f57f129 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/fixtures/get_vlan_bindings_sent.json @@ -0,0 +1,166 @@ +[ + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "vlan_binding": [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "80", + "vlan_interface_binding": [ + { + "id": "80", + "ifnum": "10/1", + "tagged": true, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "80", + "ipaddress": "192.168.0.1", + "netmask": "255.255.255.0", + "td": "0", + "stateflag": "1", + "ownergroup": "" + } + ] + } + ] + }, + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "vlan_binding": [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "10/1", + "tagged": false, + "stateflag": "4" + }, + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "363", + "vlan_interface_binding": [ + { + "id": "363", + "ifnum": "10/1", + "tagged": true, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "363", + "ipaddress": "192.168.1.2", + "netmask": "255.255.255.0", + "stateflag": "1" + } + ] + } + ] + }, + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "vlan_binding": [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "5", + "vlan_interface_binding": [ + { + "id": "5", + "ifnum": "1/1", + "tagged": false, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "5", + "ipaddress": "192.168.1.3", + "netmask": "255.255.255.224", + "stateflag": "1" + } + ] + } + ] + }, + { + "errorcode": 0, + "message": "Done", + "severity": "NONE", + "vlan_binding": [ + { + "id": "1", + "vlan_interface_binding": [ + { + "id": "1", + "ifnum": "10/1", + "tagged": false, + "stateflag": "4" + }, + { + "id": "1", + "ifnum": "LO/1", + "tagged": false, + "stateflag": "4" + } + ] + }, + { + "id": "250", + "vlan_interface_binding": [ + { + "id": "250", + "ifnum": "10/1", + "tagged": true, + "stateflag": "4" + } + ], + "vlan_nsip_binding": [ + { + "id": "250", + "ipaddress": "192.168.1.4", + "netmask": "255.255.255.0", + "td": "0", + "stateflag": "1", + "ownergroup": "" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/citrix_adm/test_adapters_citrix_adm.py b/nautobot_ssot/tests/citrix_adm/test_adapters_citrix_adm.py new file mode 100644 index 000000000..28f4ce3d1 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/test_adapters_citrix_adm.py @@ -0,0 +1,158 @@ +"""Test Citrix ADM adapter.""" + +from unittest.mock import MagicMock + +from diffsync.exceptions import ObjectNotFound +from nautobot.core.testing import TransactionTestCase +from nautobot.extras.models import JobResult + +from nautobot_ssot.integrations.citrix_adm.diffsync.adapters.citrix_adm import CitrixAdmAdapter +from nautobot_ssot.integrations.citrix_adm.jobs import CitrixAdmDataSource +from nautobot_ssot.tests.citrix_adm.fixtures import ( + ADM_DEVICE_MAP_FIXTURE, + DEVICE_FIXTURE_RECV, + NSIP6_FIXTURE_RECV, + SITE_FIXTURE_RECV, + VLAN_FIXTURE_RECV, +) + + +class TestCitrixAdmAdapterTestCase(TransactionTestCase): # pylint: disable=too-many-instance-attributes + """Test NautobotSsotCitrixAdmAdapter class.""" + + databases = ("default", "job_logs") + + def __init__(self, *args, **kwargs): + """Initialize test case.""" + self.sor_cf = None + self.status_active = None + self.hq_site = None + self.test_dev = None + self.intf = None + self.addr = None + super().__init__(*args, **kwargs) + + def setUp(self): + """Configure shared objects for test cases.""" + super().setUp() + self.instance = MagicMock() + self.instance.name = "Test" + self.instance.remote_url = "https://test.example.com" + self.instance.verify_ssl = True + + self.citrix_adm_client = MagicMock() + self.citrix_adm_client.get_sites.return_value = SITE_FIXTURE_RECV + self.citrix_adm_client.get_devices.return_value = DEVICE_FIXTURE_RECV + self.citrix_adm_client.get_vlan_bindings.side_effect = VLAN_FIXTURE_RECV + self.citrix_adm_client.get_nsip6.side_effect = NSIP6_FIXTURE_RECV + self.job = CitrixAdmDataSource() + self.job.debug = True + self.job.location_map = {} + self.job.parent_location = None + self.job.hostname_mapping = {} + self.job.logger.warning = MagicMock() + self.job.logger.info = MagicMock() + self.job.job_result = JobResult.objects.create( + name=self.job.class_path, task_name="fake task", worker="default" + ) + self.citrix_adm = CitrixAdmAdapter(job=self.job, sync=None, instances=[self.instance]) + self.citrix_adm.conn = self.citrix_adm_client + + def test_load_site(self): + """Test Nautobot SSoT Citrix ADM load_site() function.""" + self.citrix_adm.load_site(site_info=SITE_FIXTURE_RECV[2]) + self.assertEqual( + {"ARIA__West"}, + {site.get_unique_id() for site in self.citrix_adm.get_all("datacenter")}, + ) + self.job.logger.info.assert_called_with("Loaded Datacenter from Citrix ADM: ARIA") + + def test_load_site_w_location_map(self): + """Test Nautobot SSoT Citrix ADM load_site() function with location_map from Job form.""" + site_info = SITE_FIXTURE_RECV[3] + self.job.debug = True + self.job.location_map = {"Apple Inc.": {"name": "Apple", "parent": "Cupertino"}} + self.citrix_adm.load_site(site_info=site_info) + self.assertEqual( + {"Apple__Cupertino"}, + {site.get_unique_id() for site in self.citrix_adm.get_all("datacenter")}, + ) + self.job.logger.info.assert_called_with("Loaded Datacenter from Citrix ADM: Apple") + + def test_load_devices(self): + """Test the Nautobot SSoT Citrix ADM load_devices() function.""" + self.citrix_adm.adm_site_map[DEVICE_FIXTURE_RECV[0]["datacenter_id"]] = SITE_FIXTURE_RECV[1] + self.citrix_adm_client.get_devices.return_value = [DEVICE_FIXTURE_RECV[0]] + self.citrix_adm.load_devices() + self.assertEqual( + {"UYLLBFRCXM55-EA"}, + {dev.get_unique_id() for dev in self.citrix_adm.get_all("device")}, + ) + + def test_load_devices_duplicate(self): + """Test the Nautobot SSoT Citrix ADM load_devices() function with duplicate devices.""" + self.citrix_adm.adm_site_map[DEVICE_FIXTURE_RECV[3]["datacenter_id"]] = SITE_FIXTURE_RECV[2] + self.citrix_adm_client.get_devices.return_value = [DEVICE_FIXTURE_RECV[3]] + self.citrix_adm.load_devices() + self.citrix_adm.load_devices() + self.job.logger.warning.assert_called_with( + "Duplicate Device attempting to be loaded: OGI-MSCI-IMS-Mctdgj-Pqsf-M" + ) + + def test_load_devices_without_hostname(self): + """Test the Nautobot SSoT Citrix ADM load_devices() function with a device missing hostname.""" + self.citrix_adm_client.get_devices.return_value = [{"hostname": ""}] + self.citrix_adm.load_devices() + self.job.logger.warning.assert_called_with("Device without hostname will not be loaded. {'hostname': ''}") + + def test_load_ports(self): + """Test the Nautobot SSoT Citrix ADM load_ports() function.""" + self.citrix_adm.adm_device_map = ADM_DEVICE_MAP_FIXTURE + self.citrix_adm.get = MagicMock() + self.citrix_adm.get.side_effect = [ObjectNotFound, MagicMock(), ObjectNotFound, MagicMock()] + self.citrix_adm.load_ports() + expected_ports = { + f"{port['port']}__{adc['hostname']}" + for _, adc in self.citrix_adm.adm_device_map.items() + for port in adc["ports"] + } + expected_ports = list(expected_ports) + actual_ports = [port.get_unique_id() for port in self.citrix_adm.get_all("port")] + self.assertEqual(sorted(expected_ports), sorted(actual_ports)) + + def test_load_addresses(self): + """Test the Nautobot SSoT Citrix ADM load_addresses() function.""" + self.citrix_adm.adm_device_map = ADM_DEVICE_MAP_FIXTURE + self.citrix_adm.load_prefix = MagicMock() + self.citrix_adm.load_address = MagicMock() + self.citrix_adm.load_address_to_interface = MagicMock() + self.citrix_adm.load_addresses() + self.citrix_adm.load_prefix.assert_called_with(prefix="192.168.1.0/24") + self.citrix_adm.load_address.assert_called_with( + address="192.168.1.5/24", + prefix="192.168.1.0/24", + tags=["MGMT"], + ) + self.citrix_adm.load_address_to_interface.assert_called_with( + address="192.168.1.5/24", device="TEST", port="0/1", primary=True + ) + + def test_load_prefix(self): + """Test the Nautobot SSoT Citrix ADM load_prefix() function.""" + self.citrix_adm.load_prefix(prefix="10.0.0.0/16") + self.assertEqual({"10.0.0.0/16__Global"}, {pf.get_unique_id() for pf in self.citrix_adm.get_all("prefix")}) + + def test_load_address(self): + """Test the Nautobot SSoT Citrix ADM load_address() function.""" + self.citrix_adm.load_address(address="10.0.0.1/24", prefix="10.0.0.0/24", tags=["TEST"]) + self.assertEqual( + {"10.0.0.1/24__10.0.0.0/24"}, + {addr.get_unique_id() for addr in self.citrix_adm.get_all("address")}, + ) + + def test_load_address_to_interface(self): + """Test the Nautobot SSoT Citrix ADM load_address_to_interface() function.""" + self.citrix_adm.load_address_to_interface(address="10.0.0.1/24", device="TEST", port="mgmt", primary=True) + self.assertEqual( + {"10.0.0.1/24__TEST__mgmt"}, {map.get_unique_id() for map in self.citrix_adm.get_all("ip_on_intf")} + ) diff --git a/nautobot_ssot/tests/citrix_adm/test_adapters_nautobot.py b/nautobot_ssot/tests/citrix_adm/test_adapters_nautobot.py new file mode 100644 index 000000000..84d4cb621 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/test_adapters_nautobot.py @@ -0,0 +1,239 @@ +"""Test Nautobot adapter.""" + +from unittest.mock import MagicMock + +from diffsync.exceptions import ObjectNotFound +from django.contrib.contenttypes.models import ContentType +from django.db.models import ProtectedError +from nautobot.core.testing import TransactionTestCase +from nautobot.dcim.models import ( + Device, + DeviceType, + Interface, + Location, + LocationType, + Manufacturer, +) +from nautobot.extras.models import JobResult, Role, Status +from nautobot.ipam.models import IPAddress, IPAddressToInterface, Namespace, Prefix +from nautobot.tenancy.models import Tenant + +from nautobot_ssot.integrations.citrix_adm.diffsync.adapters.nautobot import NautobotAdapter +from nautobot_ssot.integrations.citrix_adm.jobs import CitrixAdmDataSource + + +class NautobotDiffSyncTestCase(TransactionTestCase): + """Test the NautobotAdapter class.""" + + databases = ("default", "job_logs") + + def __init__(self, *args, **kwargs): + """Initialize shared variables.""" + super().__init__(*args, **kwargs) + self.hq_site = None + self.ny_region = None + + def setUp(self): # pylint: disable=too-many-locals + """Per-test-case data setup.""" + super().setUp() + self.status_active = Status.objects.get(name="Active") + + self.job = CitrixAdmDataSource() + self.job.job_result = JobResult.objects.create( + name=self.job.class_path, task_name="fake task", worker="default" + ) + self.nb_adapter = NautobotAdapter(job=self.job, sync=None) + self.job.logger.info = MagicMock() + self.job.logger.warning = MagicMock() + self.build_nautobot_objects() + + def build_nautobot_objects(self): + """Build out Nautobot objects to test loading.""" + test_tenant = Tenant.objects.get_or_create(name="Test")[0] + region_type = LocationType.objects.get_or_create(name="Region", nestable=True)[0] + self.ny_region = Location.objects.create(name="NY", location_type=region_type, status=self.status_active) + self.ny_region.validated_save() + + site_type = LocationType.objects.get_or_create(name="Site", parent=region_type)[0] + site_type.content_types.add(ContentType.objects.get_for_model(Device)) + self.job.dc_loctype = site_type + self.job.parent_location = self.ny_region + self.hq_site = Location.objects.create( + parent=self.ny_region, name="HQ", location_type=site_type, status=self.status_active + ) + self.hq_site.validated_save() + + citrix_manu, _ = Manufacturer.objects.get_or_create(name="Citrix") + srx_devicetype, _ = DeviceType.objects.get_or_create(model="SDX", manufacturer=citrix_manu) + core_role, _ = Role.objects.get_or_create(name="CORE") + core_role.content_types.add(ContentType.objects.get_for_model(Device)) + + core_router = Device.objects.create( + name="edge-fw.test.com", + device_type=srx_devicetype, + role=core_role, + serial="FQ123456", + location=self.hq_site, + status=self.status_active, + tenant=test_tenant, + ) + core_router._custom_field_data["system_of_record"] = "Citrix ADM" # pylint: disable=protected-access + core_router.validated_save() + mgmt_intf = Interface.objects.create( + name="Management", + type="virtual", + device=core_router, + status=self.status_active, + ) + mgmt_intf.validated_save() + + global_ns = Namespace.objects.get_or_create(name="Global")[0] + mgmt4_pf = Prefix.objects.create( + prefix="10.1.1.0/24", namespace=global_ns, status=self.status_active, tenant=test_tenant + ) + mgmt6_pf = Prefix.objects.create( + prefix="2001:db8:3333:4444:5555:6666:7777:8888/128", + namespace=global_ns, + status=self.status_active, + tenant=test_tenant, + ) + mgmt4_pf._custom_field_data["system_of_record"] = "Citrix ADM" # pylint: disable=protected-access + mgmt4_pf.validated_save() + mgmt6_pf._custom_field_data["system_of_record"] = "Citrix ADM" # pylint: disable=protected-access + mgmt6_pf.validated_save() + + mgmt_addr = IPAddress.objects.create( + address="10.1.1.1/24", + namespace=global_ns, + parent=mgmt4_pf, + status=self.status_active, + tenant=test_tenant, + ) + mgmt_addr._custom_field_data["system_of_record"] = "Citrix ADM" # pylint: disable=protected-access + mgmt_addr.validated_save() + mgmt_addr6 = IPAddress.objects.create( + address="2001:db8:3333:4444:5555:6666:7777:8888/128", + parent=mgmt6_pf, + status=self.status_active, + tenant=test_tenant, + ) + mgmt_addr6._custom_field_data["system_of_record"] = "Citrix ADM" # pylint: disable=protected-access + mgmt_addr6.validated_save() + + IPAddressToInterface.objects.create(ip_address=mgmt_addr, interface=mgmt_intf) + IPAddressToInterface.objects.create(ip_address=mgmt_addr6, interface=mgmt_intf) + core_router.primary_ip4 = mgmt_addr + core_router.primary_ip6 = mgmt_addr6 + core_router.validated_save() + + def test_load_sites(self): + """Test the load_sites() function.""" + self.nb_adapter.load_sites() + self.assertEqual( + { + "HQ__NY", + }, + {site.get_unique_id() for site in self.nb_adapter.get_all("datacenter")}, + ) + self.job.logger.info.assert_called_once_with("Loaded Site HQ from Nautobot.") + + def test_load_devices(self): + """Test the load_devices() function.""" + self.nb_adapter.load_devices() + self.assertEqual( + {"edge-fw.test.com"}, + {dev.get_unique_id() for dev in self.nb_adapter.get_all("device")}, + ) + self.job.logger.info.assert_any_call("Loading Device edge-fw.test.com from Nautobot.") + + def test_load_ports_success(self): + """Test the load_ports() function success.""" + self.nb_adapter.load_devices() + self.nb_adapter.load_ports() + self.assertEqual( + {"Management__edge-fw.test.com"}, + {port.get_unique_id() for port in self.nb_adapter.get_all("port")}, + ) + + def test_load_ports_missing_device(self): + """Test the load_ports() function with missing device.""" + self.nb_adapter.get = MagicMock() + self.nb_adapter.get.side_effect = ObjectNotFound + self.nb_adapter.load_ports() + self.job.logger.warning.assert_called_once_with( + "Unable to find edge-fw.test.com loaded so skipping loading port Management." + ) + + def test_load_addresses(self): + """Test the load_addresses() function.""" + self.nb_adapter.load_addresses() + self.assertEqual( + { + "10.1.1.1/24__10.1.1.0/24", + "2001:db8:3333:4444:5555:6666:7777:8888/128__2001:db8:3333:4444:5555:6666:7777:8888/128", + }, + {addr.get_unique_id() for addr in self.nb_adapter.get_all("address")}, + ) + + def test_load_prefixes(self): + """Test the load_prefix() function.""" + self.nb_adapter.load_prefixes() + self.assertEqual( + {"10.1.1.0/24__Global", "2001:db8:3333:4444:5555:6666:7777:8888/128__Global"}, + {pf.get_unique_id() for pf in self.nb_adapter.get_all("prefix")}, + ) + + def test_sync_complete(self): + """Test the sync_complete() method in the NautobotAdapter.""" + self.nb_adapter.objects_to_delete = { + "devices": [MagicMock()], + "ports": [MagicMock()], + "prefixes": [MagicMock()], + "addresses": [MagicMock()], + } + self.nb_adapter.job = MagicMock() + self.nb_adapter.job.logger.info = MagicMock() + + deleted_objs = [] + for group in ["addresses", "ports", "devices"]: + deleted_objs.extend(self.nb_adapter.objects_to_delete[group]) + + self.nb_adapter.sync_complete(diff=MagicMock(), source=MagicMock()) + + for obj in deleted_objs: + self.assertTrue(obj.delete.called) + self.assertEqual(len(self.nb_adapter.objects_to_delete["addresses"]), 0) + self.assertEqual(len(self.nb_adapter.objects_to_delete["prefixes"]), 0) + self.assertEqual(len(self.nb_adapter.objects_to_delete["ports"]), 0) + self.assertEqual(len(self.nb_adapter.objects_to_delete["devices"]), 0) + self.assertTrue(self.nb_adapter.job.logger.info.called) + self.assertTrue(self.nb_adapter.job.logger.info.call_count, 4) + self.assertTrue(self.nb_adapter.job.logger.info.call_args_list[0].startswith("Deleting")) + self.assertTrue(self.nb_adapter.job.logger.info.call_args_list[1].startswith("Deleting")) + self.assertTrue(self.nb_adapter.job.logger.info.call_args_list[2].startswith("Deleting")) + self.assertTrue(self.nb_adapter.job.logger.info.call_args_list[3].startswith("Deleting")) + + def test_sync_complete_protected_error(self): + """ + Tests that ProtectedError exception is handled when deleting objects from Nautobot. + """ + mock_dev = MagicMock() + mock_dev.delete.side_effect = ProtectedError(msg="Cannot delete protected object.", protected_objects=mock_dev) + self.nb_adapter.objects_to_delete["devices"].append(mock_dev) + self.nb_adapter.sync_complete(source=self.nb_adapter, diff=MagicMock()) + self.job.logger.info.assert_called() + self.job.logger.info.calls[1].starts_with("Deletion failed protected object") + + def test_load(self): + """Test the load() function.""" + self.nb_adapter.load_sites = MagicMock() + self.nb_adapter.load_devices = MagicMock() + self.nb_adapter.load_ports = MagicMock() + self.nb_adapter.load_prefixes = MagicMock() + self.nb_adapter.load_addresses = MagicMock() + self.nb_adapter.load() + self.nb_adapter.load_sites.assert_called_once() + self.nb_adapter.load_devices.assert_called_once() + self.nb_adapter.load_ports.assert_called_once() + self.nb_adapter.load_prefixes.assert_called_once() + self.nb_adapter.load_addresses.assert_called_once() diff --git a/nautobot_ssot/tests/citrix_adm/test_models_nautobot.py b/nautobot_ssot/tests/citrix_adm/test_models_nautobot.py new file mode 100644 index 000000000..8a114f0f3 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/test_models_nautobot.py @@ -0,0 +1,83 @@ +"""Test the Nautobot CRUD functions for all DiffSync models.""" + +from unittest.mock import MagicMock + +from diffsync import Adapter +from django.contrib.contenttypes.models import ContentType +from django.test import override_settings +from nautobot.core.testing import TransactionTestCase +from nautobot.dcim.models import Device, Location, LocationType +from nautobot.extras.models import Status + +from nautobot_ssot.integrations.citrix_adm.diffsync.models.nautobot import NautobotDatacenter + + +@override_settings(PLUGINS_CONFIG={"nautobot_ssot": {"enable_citrix_adm": True}}) +class TestNautobotDatacenter(TransactionTestCase): + """Test the NautobotDatacenter class.""" + + def setUp(self): + """Configure shared objects.""" + super().setUp() + self.adapter = Adapter() + self.adapter.job = MagicMock() + self.adapter.job.logger.warning = MagicMock() + self.status_active = Status.objects.get(name="Active") + self.test_dc = NautobotDatacenter(name="Test", region="", latitude=None, longitude=None, uuid=None) + region_lt = LocationType.objects.get_or_create(name="Region")[0] + self.global_region = Location.objects.create(name="Global", location_type=region_lt, status=self.status_active) + site_lt = LocationType.objects.get_or_create(name="Site", parent=region_lt)[0] + site_lt.content_types.add(ContentType.objects.get_for_model(Device)) + self.site_obj = Location.objects.create( + name="HQ", + location_type=site_lt, + parent=self.global_region, + status=self.status_active, + ) + self.adapter.job.dc_loctype = site_lt + self.adapter.job.parent_loc = None + + def test_create(self): + """Validate the NautobotDatacenter create() method creates a Site.""" + self.site_obj.delete() + ids = {"name": "HQ", "region": "Global"} + attrs = {"latitude": 12.345, "longitude": -67.89} + result = NautobotDatacenter.create(self.adapter, ids, attrs) + self.assertIsInstance(result, NautobotDatacenter) + + site_obj = Location.objects.get(name="HQ") + self.assertEqual(site_obj.parent, self.global_region) + self.assertEqual(float(site_obj.latitude), attrs["latitude"]) + self.assertEqual(float(site_obj.longitude), attrs["longitude"]) + + def test_create_with_duplicate_site(self): + """Validate the NautobotDatacenter create() method handling of duplicate Site.""" + ids = {"name": "HQ", "region": ""} + attrs = {} + NautobotDatacenter.create(self.adapter, ids, attrs) + self.adapter.job.logger.warning.assert_called_with("Site HQ already exists so skipping creation.") + + @override_settings(PLUGINS_CONFIG={"nautobot_ssot": {"citrix_adm_update_sites": True}}) + def test_update(self): + """Validate the NautobotDatacenter update() method updates a Site.""" + self.test_dc.uuid = self.site_obj.id + update_attrs = { + "latitude": 12.345, + "longitude": -67.89, + } + actual = NautobotDatacenter.update(self=self.test_dc, attrs=update_attrs) + self.site_obj.refresh_from_db() + self.assertEqual(float(self.site_obj.latitude), update_attrs["latitude"]) + self.assertEqual(float(self.site_obj.longitude), update_attrs["longitude"]) + self.assertEqual(actual, self.test_dc) + + @override_settings(PLUGINS_CONFIG={"nautobot_ssot": {"citrix_adm_update_sites": False}}) + def test_update_setting_disabled(self): + """Validate the NautobotDatacenter update() method doesn't update a Site if setting is False.""" + self.test_dc.adapter = MagicMock() + self.test_dc.adapter.job = MagicMock() + self.test_dc.adapter.job.logger.warning = MagicMock() + NautobotDatacenter.update(self=self.test_dc, attrs={}) + self.test_dc.adapter.job.logger.warning.assert_called_once_with( + "Update sites setting is disabled so skipping updating Test." + ) diff --git a/nautobot_ssot/tests/citrix_adm/test_utils_citrix_adm.py b/nautobot_ssot/tests/citrix_adm/test_utils_citrix_adm.py new file mode 100644 index 000000000..4c43ae464 --- /dev/null +++ b/nautobot_ssot/tests/citrix_adm/test_utils_citrix_adm.py @@ -0,0 +1,258 @@ +"""Utility functions for working with Citrix ADM.""" + +import logging +from unittest.mock import MagicMock, patch + +import requests +from nautobot.core.testing import TestCase +from requests.exceptions import HTTPError + +from nautobot_ssot.integrations.citrix_adm.utils.citrix_adm import ( + CitrixNitroClient, + parse_nsip6s, + parse_nsips, + parse_version, + parse_vlan_bindings, +) +from nautobot_ssot.tests.citrix_adm.fixtures import ( + DEVICE_FIXTURE_RECV, + DEVICE_FIXTURE_SENT, + NSIP6_FIXTURE_RECV, + NSIP6_FIXTURE_SENT, + NSIP_FIXTURE_RECV, + NSIP_FIXTURE_SENT, + SITE_FIXTURE_RECV, + SITE_FIXTURE_SENT, + VLAN_FIXTURE_RECV, + VLAN_FIXTURE_SENT, +) + +LOGGER = logging.getLogger(__name__) +# pylint: disable=too-many-public-methods + + +class TestCitrixAdmClient(TestCase): + """Test the Citrix ADM client and calls.""" + + databases = ("default", "job_logs") + + def setUp(self): + """Configure common variables for tests.""" + self.base_url = "https://example.com" + self.user = "user" + self.password = "password" # nosec: B105 + self.verify = True + self.job = MagicMock() + self.job.debug = True + self.job.logger.info = MagicMock() + self.job.logger.warning = MagicMock() + self.client = CitrixNitroClient(self.base_url, self.user, self.password, self.job, self.verify) + + def test_init(self): + """Validate the class initializer works as expected.""" + self.assertEqual(self.client.url, self.base_url) + self.assertEqual(self.client.username, self.user) + self.assertEqual(self.client.password, self.password) + self.assertEqual(self.client.verify, self.verify) + + def test_url_updated(self): + """Validate the URL is updated if a trailing slash is included in URL.""" + self.base_url = "https://example.com/" + self.client = CitrixNitroClient(self.base_url, self.user, self.password, self.job, self.verify) + self.assertEqual(self.client.url, self.base_url.rstrip("/")) + + @patch.object(CitrixNitroClient, "request") + def test_login(self, mock_request): + """Validate functionality of the login() method success.""" + mock_response = MagicMock() + mock_response = {"login": [{"sessionid": "1234"}]} + mock_request.return_value = mock_response + self.client.login() + self.assertEqual(self.client.headers["Cookie"], "SESSID=1234; path=/; SameSite=Lax; secure; HttpOnly") + + @patch.object(CitrixNitroClient, "request") + def test_login_failure(self, mock_request): + """Validate functionality of the login() method failure.""" + mock_response = MagicMock() + mock_response = {} + mock_request.return_value = mock_response + with self.assertRaises(requests.exceptions.RequestException): + self.client.login() + self.job.logger.error.assert_called_once_with( + "Error while logging into Citrix ADM. Please validate your configuration is correct." + ) + + @patch.object(CitrixNitroClient, "request") + def test_logout(self, mock_request): + """Validate functionality of the logout() method success.""" + self.client.logout() + mock_request.assert_called_with( + method="POST", + endpoint="config", + objecttype="logout", + data="object={'logout': {'username': 'user', 'password': 'password'}}", + ) + + @patch("nautobot_ssot.integrations.citrix_adm.utils.citrix_adm.requests.request") + def test_request(self, mock_request): + """Validate functionality of the request() method success.""" + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.return_value = {"errorcode": 0} + mock_request.return_value = mock_response + + endpoint = "example" + objecttype = "sample" + objectname = "test" + params = {"param1": "value1", "param2": "value2"} + data = '{"key": "value"}' + + response = self.client.request("POST", endpoint, objecttype, objectname, params, data) + + mock_request.assert_called_with( + method="POST", + url="https://example.com/nitro/v1/example/sample/test?param1=value1param2=value2", + data='{"key": "value"}', + headers={"Accept": "application/json", "Content-Type": "application/json"}, + timeout=60, + verify=True, + ) + mock_response.raise_for_status.assert_called_once() + self.assertEqual(response, {"errorcode": 0}) + + @patch("nautobot_ssot.integrations.citrix_adm.utils.citrix_adm.requests.request") + def test_request_failure(self, mock_request): + """Validate functionality of the request() method failure.""" + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.raise_for_status.side_effect = HTTPError + mock_request.return_value = mock_response + + endpoint = "example" + objecttype = "sample" + objectname = "test" + params = "test" + data = '{"key": "value"}' + + with self.assertRaises(requests.exceptions.HTTPError): + result = self.client.request("POST", endpoint, objecttype, objectname, params, data) + self.assertEqual(result, {}) + mock_response.raise_for_status.assert_called_once() + + @patch.object(CitrixNitroClient, "request") + def test_get_sites_success(self, mock_request): + """Validate functionality of the get_sites() method success.""" + mock_request.return_value = SITE_FIXTURE_SENT + expected = self.client.get_sites() + self.assertEqual(SITE_FIXTURE_RECV, expected) + + @patch.object(CitrixNitroClient, "request") + def test_get_sites_failure(self, mock_request): + """Validate functionality of the get_sites() method failure.""" + mock_request.return_value = {} + expected = self.client.get_sites() + self.job.logger.error.assert_called_once_with("Error getting sites from Citrix ADM.") + self.assertEqual(expected, {}) + + @patch.object(CitrixNitroClient, "request") + def test_get_devices_success(self, mock_request): + """Validate functionality of the get_devices() method success.""" + mock_request.return_value = DEVICE_FIXTURE_SENT + expected = self.client.get_devices() + self.assertEqual(DEVICE_FIXTURE_RECV, expected) + + @patch.object(CitrixNitroClient, "request") + def test_get_devices_failure(self, mock_request): + """Validate functionality of the get_devices() method failure.""" + mock_request.return_value = {} + expected = self.client.get_devices() + self.job.logger.error.assert_called_once_with("Error getting devices from Citrix ADM.") + self.assertEqual(expected, {}) + + @patch.object(CitrixNitroClient, "request") + def test_get_nsip_success(self, mock_request): + """Validate functionality of the get_nsip6() method success.""" + adc = {"hostname": "test", "ip_address": ""} + mock_request.return_value = NSIP_FIXTURE_SENT + expected = self.client.get_nsip(adc) + self.assertEqual(NSIP_FIXTURE_RECV, expected) + + @patch.object(CitrixNitroClient, "request") + def test_get_nsip_failure(self, mock_request): + """Validate functionality of the get_nsip() method failure.""" + adc = {"hostname": "test", "ip_address": ""} + mock_request.return_value = {} + actual = self.client.get_nsip(adc) + self.job.logger.error.assert_called_once_with("Error getting nsip from test") + self.assertEqual(actual, {}) + + @patch.object(CitrixNitroClient, "request") + def test_get_nsip6_success(self, mock_request): + """Validate functionality of the get_nsip() method success.""" + adc = {"hostname": "test", "ip_address": ""} + mock_request.side_effect = NSIP6_FIXTURE_SENT + for expected in NSIP6_FIXTURE_RECV: + actual = self.client.get_nsip6(adc) + self.assertEqual(actual, expected) + + @patch.object(CitrixNitroClient, "request") + def test_get_nsip6_failure(self, mock_request): + """Validate functionality of the get_nsip6() method failure.""" + adc = {"hostname": "test", "ip_address": ""} + mock_request.return_value = {} + actual = self.client.get_nsip6(adc) + self.job.logger.error.assert_called_once_with("Error getting nsip6 from test") + self.assertEqual(actual, {}) + + @patch.object(CitrixNitroClient, "request") + def test_get_vlan_bindings_success(self, mock_request): + """Validate functionality of the get_vlan_bindings() method success.""" + adc = {"hostname": "test", "ip_address": ""} + mock_request.side_effect = VLAN_FIXTURE_SENT + for expected in VLAN_FIXTURE_RECV: + actual = self.client.get_vlan_bindings(adc) + self.assertEqual(actual, expected) + + @patch.object(CitrixNitroClient, "request") + def test_get_vlan_bindings_failure(self, mock_request): + """Validate functionality of the get_vlan_bindings() method failure.""" + adc = {"hostname": "test", "ip_address": ""} + mock_request.return_value = {} + actual = self.client.get_vlan_bindings(adc) + self.job.logger.error.assert_called_once_with("Error getting vlan bindings from test") + self.assertEqual(actual, {}) + + def test_parse_version(self): + """Validate functionality of the parse_version function.""" + version = "NetScaler NS13.1: Build 37.38.nc, Date: Nov 23 2022, 04:42:36 (64-bit)" + expected = "NS13.1: Build 37.38.nc" + actual = parse_version(version=version) + self.assertEqual(actual, expected) + + def test_parse_vlan_bindings(self): + """Validate functionality of the parse_vlan_bindings function.""" + vlan_bindings = VLAN_FIXTURE_RECV[0] + adc = {"hostname": "test", "ip_address": "192.168.0.1", "netmask": "255.255.255.0"} + actual = parse_vlan_bindings(vlan_bindings=vlan_bindings, adc=adc, job=self) + expected = [{"ipaddress": "192.168.0.1", "netmask": 24, "port": "10/1", "version": 4, "vlan": "80"}] + self.assertEqual(actual, expected) + + def test_parse_nsips(self): + """Validate functionality of the parse_nsips function.""" + nsips = NSIP_FIXTURE_RECV + adc = {"hostname": "test", "mgmt_ip_address": "192.168.0.2"} + ports = [{"ipaddress": "192.168.0.1", "netmask": 24, "port": "10/1", "version": 4, "vlan": "80"}] + expected = [ + {"ipaddress": "192.168.0.1", "netmask": 24, "tags": ["NSIP"], "port": "10/1", "version": 4, "vlan": "80"}, + {"ipaddress": "192.168.0.2", "netmask": 24, "tags": ["MGMT"], "port": "10/1", "version": 4, "vlan": "80"}, + ] + actual = parse_nsips(nsips=nsips, adc=adc, ports=ports) + self.assertEqual(actual, expected) + + def test_parse_nsip6s(self): + """Validate functionality of the parse_nsip6s function.""" + nsip6s = NSIP6_FIXTURE_RECV[0] + ports = [] + expected = [{"ipaddress": "fe80::1234:5678:9abc:dev1", "netmask": "64", "port": "L0/1", "vlan": "1"}] + actual = parse_nsip6s(nsip6s=nsip6s, ports=ports) + self.assertEqual(actual, expected) diff --git a/nautobot_ssot/tests/dna_center/fixtures/expected_dnac_location_map.json b/nautobot_ssot/tests/dna_center/fixtures/expected_dnac_location_map.json index 343c8a023..cf93cadd9 100644 --- a/nautobot_ssot/tests/dna_center/fixtures/expected_dnac_location_map.json +++ b/nautobot_ssot/tests/dna_center/fixtures/expected_dnac_location_map.json @@ -107,7 +107,7 @@ "5c882916-4f65-45b5-a5cc-ca5fa927cba6": { "name": "Forschungszentrum", "loc_type": "area", - "parent": "building" + "parent": "Global" }, "1c3bb089-2a74-4fdf-96e3-d1815ac67e38": { "name": "Building 1", diff --git a/nautobot_ssot/tests/dna_center/test_adapters_dna_center.py b/nautobot_ssot/tests/dna_center/test_adapters_dna_center.py index 36442cfa8..22e859ca3 100644 --- a/nautobot_ssot/tests/dna_center/test_adapters_dna_center.py +++ b/nautobot_ssot/tests/dna_center/test_adapters_dna_center.py @@ -43,7 +43,7 @@ class TestDnaCenterAdapterTestCase(TransactionTestCase): # pylint: disable=too- databases = ("default", "job_logs") - def setUp(self): + def setUp(self): # pylint: disable=too-many-statements """Initialize test case.""" super().setUp() self.dna_center_client = MagicMock() @@ -127,6 +127,11 @@ def setUp(self): self.job.building_loctype = self.site_loc_type self.job.floor_loctype = self.floor_loc_type self.job.dnac = dnac + self.job.location_map = {} + self.job.hostname_map = {} + self.job.logger.warning = MagicMock() + self.job.logger.error = MagicMock() + self.job.logger.info = MagicMock() self.job.controller_group = ControllerManagedDeviceGroup.objects.get_or_create( name="DNA Center Managed Devices", controller=dnac )[0] @@ -135,9 +140,6 @@ def setUp(self): name=self.job.class_path, task_name="fake task", user=None, id=uuid.uuid4() ) self.dna_center = DnaCenterAdapter(job=self.job, sync=None, client=self.dna_center_client, tenant=None) - self.dna_center.job.logger.warning = MagicMock() - self.dna_center.job.logger.error = MagicMock() - self.dna_center.job.logger.info = MagicMock() self.dna_center.dnac_location_map = EXPECTED_DNAC_LOCATION_MAP def test_build_dnac_location_map(self): @@ -166,13 +168,11 @@ def test_parse_and_sort_locations(self): def test_load_locations_success(self): """Test Nautobot SSoT for Cisco DNA Center load_locations() function successfully.""" - self.dna_center.load_areas = MagicMock() self.dna_center.load_buildings = MagicMock() self.dna_center.load_floors = MagicMock() self.dna_center_client.get_location.return_value = [{"name": "NY"}] self.dna_center.load_locations() self.dna_center_client.get_locations.assert_called() - self.dna_center.load_areas.assert_called_once() self.dna_center.load_buildings.assert_called_once() self.dna_center.load_floors.assert_called_once() @@ -184,23 +184,31 @@ def test_load_locations_failure(self): "No location data was returned from DNAC. Unable to proceed." ) - def test_load_areas_w_global(self): - """Test Nautobot SSoT for Cisco DNA Center load_areas() function with Global area.""" - self.dna_center.load_areas(areas=EXPECTED_AREAS) + def test_load_area_w_global(self): + """Test Nautobot SSoT for Cisco DNA Center load_area() function with Global area.""" + for area in EXPECTED_AREAS: + hierarchy = area["siteNameHierarchy"].split("/") + if isinstance(hierarchy, list) and len(hierarchy) > 1: + self.dna_center.load_area(area=hierarchy[-1], area_parent=hierarchy[-2]) + else: + self.dna_center.load_area(area=hierarchy[0]) area_expected = sorted( [f"{x['name']}__{x['parent']}" for x in EXPECTED_DNAC_LOCATION_MAP.values() if x["loc_type"] == "area"] ) area_actual = sorted([area.get_unique_id() for area in self.dna_center.get_all("area")]) self.assertEqual(area_actual, area_expected) - self.dna_center.job.logger.info.assert_called_with( - "Loaded Region Sydney. {'parentId': '262696b1-aa87-432b-8a21-db9a77c51f23', 'additionalInfo': [{'nameSpace': 'Location', 'attributes': {'addressInheritedFrom': '262696b1-aa87-432b-8a21-db9a77c51f23', 'type': 'area'}}], 'name': 'Sydney', 'instanceTenantId': '623f029857259506a56ad9bd', 'id': '6e404051-4c06-4dab-adaa-72c5eeac577b', 'siteHierarchy': '9e5f9fc2-032e-45e8-994c-4a00629648e8/262696b1-aa87-432b-8a21-db9a77c51f23/6e404051-4c06-4dab-adaa-72c5eeac577b', 'siteNameHierarchy': 'Global/Australia/Sydney'}" - ) @override_settings(PLUGINS_CONFIG={"nautobot_ssot": {"dna_center_import_global": False}}) - def test_load_areas_wo_global(self): - """Test Nautobot SSoT for Cisco DNA Center load_areas() function without Global area.""" + def test_load_area_wo_global(self): + """Test Nautobot SSoT for Cisco DNA Center load_area() function without Global area.""" self.dna_center.dnac_location_map = EXPECTED_DNAC_LOCATION_MAP_WO_GLOBAL - self.dna_center.load_areas(areas=EXPECTED_AREAS_WO_GLOBAL) + for area in EXPECTED_AREAS_WO_GLOBAL: + hierarchy = area["siteNameHierarchy"].split("/") + if hierarchy[-2] == "Global": + area_parent = None + else: + area_parent = hierarchy[-2] + self.dna_center.load_area(area=hierarchy[-1], area_parent=area_parent) area_expected = [ f"{x['name']}__{x['parent']}" for x in EXPECTED_DNAC_LOCATION_MAP_WO_GLOBAL.values() @@ -252,6 +260,7 @@ def test_load_buildings_wo_global(self): def test_load_buildings_duplicate(self): """Test Nautobot SSoT for Cisco DNA Center load_buildings() function with duplicate building.""" + self.dna_center.load_area = MagicMock() self.dna_center_client.find_address_and_type.side_effect = [ ("", "building"), ("", "building"), @@ -261,22 +270,74 @@ def test_load_buildings_duplicate(self): ("", "building"), ("", "building"), ("", "building"), + ("", "building"), ] self.dna_center.load_buildings(buildings=EXPECTED_BUILDINGS) self.dna_center.load_buildings(buildings=[EXPECTED_BUILDINGS[0]]) self.dna_center.job.logger.warning.assert_called_with("Site Building1 already loaded so skipping.") - def test_load_buildings_with_validation_error(self): - """Test Nautobot SSoT for Cisco DNA Center load_buildings() function with a ValidationError.""" - self.dna_center.add = MagicMock() - self.dna_center.add.side_effect = ValidationError(message="Building load failed!") - self.dna_center.load_buildings(buildings=[EXPECTED_BUILDINGS[0]]) - self.dna_center.job.logger.warning.assert_called_with( - "Unable to load building Building1. ['Building load failed!']" - ) + def test_load_buildings_w_location_map_building_change(self): + """Test Nautobot SSoT for Cisco DNA Center load_buildings() function with Job location_map and building data.""" + self.job.location_map = {"Rome": {"parent": "Italy", "area_parent": "Europe"}} + self.dna_center_client.find_address_and_type.side_effect = [("", "building")] + + self.dna_center.load_area = MagicMock() + self.dna_center.load_buildings(buildings=[EXPECTED_BUILDINGS[7]]) + self.dna_center.load_area.assert_called_with(area="Italy", area_parent="Europe") + loaded_bldg = self.dna_center.get("building", {"name": "Rome", "area": "Italy"}) + self.assertEqual(loaded_bldg.name, "Rome") + self.assertEqual(loaded_bldg.area, "Italy") + self.assertEqual(loaded_bldg.area_parent, "Europe") + + def test_load_buildings_w_location_map_area_change(self): + """Test Nautobot SSoT for Cisco DNA Center load_buildings() function with Job location_map and area data.""" + self.job.location_map = {"SanDiego": {"parent": "California"}} + self.dna_center_client.find_address_and_type.side_effect = [("", "building")] + + self.dna_center.load_area = MagicMock() + self.dna_center.load_buildings(buildings=[EXPECTED_BUILDINGS[5]]) + self.dna_center.load_area.assert_called_with(area="SanDiego", area_parent="California") + loaded_bldg = self.dna_center.get("building", {"name": "1", "area": "SanDiego"}) + self.assertEqual(loaded_bldg.name, "1") + self.assertEqual(loaded_bldg.area, "SanDiego") + self.assertEqual(loaded_bldg.area_parent, "California") + + def test_load_buildings_w_location_map_area_and_bldg_change(self): + """Test Nautobot SSoT for Cisco DNA Center load_buildings() function with Job location_map and area and building data.""" + self.job.location_map = {"HQ": {"parent": "New York", "area_parent": "USA"}, "New York": {"parent": "New York"}} + self.dna_center_client.find_address_and_type.side_effect = [("", "building")] + + self.dna_center.load_area = MagicMock() + test_bldg = [ + { + "parentId": "04509ce4-6c88-40a3-b444-9e00f2cd97f2", + "additionalInfo": [ + { + "nameSpace": "Location", + "attributes": { + "latitude": "39.156233", + "type": "building", + "longitude": "-74.690192", + }, + } + ], + "name": "HQ", + "id": "2d0d8545-b6de-4dda-a0f7-93bcd7d0e738", + "siteHierarchy": "9e5f9fc2-032e-45e8-994c-4a00629648e8/04509ce4-6c88-40a3-b444-9e00f2cd97f2/2d0d8545-b6de-4dda-a0f7-93bcd7d0e738", + "siteNameHierarchy": "Global/SanDiego/HQ", + } + ] + self.dna_center.load_buildings(buildings=test_bldg) + self.dna_center.load_area.assert_called_with(area="New York", area_parent="USA") + loaded_bldg = self.dna_center.get("building", {"name": "HQ", "area": "New York"}) + self.assertEqual(loaded_bldg.name, "HQ") + self.assertEqual(loaded_bldg.area, "New York") + self.assertEqual(loaded_bldg.area_parent, "USA") def test_load_floors(self): """Test Nautobot SSoT for Cisco DNA Center load_floors() function.""" + self.job.location_map = {} + self.dna_center.get = MagicMock() self.dna_center.load_floors(floors=EXPECTED_FLOORS) floor_expected = [ "Building1 - Floor1__Building1", diff --git a/nautobot_ssot/tests/dna_center/test_models_nautobot.py b/nautobot_ssot/tests/dna_center/test_models_nautobot.py index 39f7c30a8..3cd39b65c 100644 --- a/nautobot_ssot/tests/dna_center/test_models_nautobot.py +++ b/nautobot_ssot/tests/dna_center/test_models_nautobot.py @@ -346,4 +346,4 @@ def test_create(self): self.assertTrue(new_dev.location_id) self.assertEqual(new_dev.location_id, hq_floor.id) self.assertEqual(new_dev.tenant_id, self.ga_tenant.id) - self.assertTrue(new_dev.custom_field_data["os_version"], self.attrs["version"]) + self.assertTrue(new_dev.software_version.version, self.attrs["version"]) diff --git a/nautobot_ssot/tests/dna_center/test_utils_dna_center.py b/nautobot_ssot/tests/dna_center/test_utils_dna_center.py index 9fe2a8e2e..fd06b12ee 100644 --- a/nautobot_ssot/tests/dna_center/test_utils_dna_center.py +++ b/nautobot_ssot/tests/dna_center/test_utils_dna_center.py @@ -201,20 +201,6 @@ def test_get_port_status(self, name, sent, received): # pylint: disable=unused- actual = self.dnac.get_port_status(port_info=sent) self.assertEqual(actual, received) - def test_parse_hostname_for_role_success(self): - """Validate the functionality of the parse_hostname_for_role method success.""" - hostname_mapping = [(".*EDGE.*", "Edge"), (".*DMZ.*", "DMZ")] - hostname = "DMZ-switch.example.com" - result = self.dnac.parse_hostname_for_role(hostname_map=hostname_mapping, device_hostname=hostname) - self.assertEqual(result, "DMZ") - - def test_parse_hostname_for_role_failure(self): - """Validate the functionality of the parse_hostname_for_role method failure.""" - hostname_mapping = [] - hostname = "core-router.example.com" - result = self.dnac.parse_hostname_for_role(hostname_map=hostname_mapping, device_hostname=hostname) - self.assertEqual(result, "Unknown") - def test_get_model_name_single_model(self): """Validate the functionality of get_model_name method with single model in string.""" test_model = "CSR1000v" diff --git a/nautobot_ssot/tests/meraki/test_models_nautobot.py b/nautobot_ssot/tests/meraki/test_models_nautobot.py index 998e5601e..cf97ff479 100644 --- a/nautobot_ssot/tests/meraki/test_models_nautobot.py +++ b/nautobot_ssot/tests/meraki/test_models_nautobot.py @@ -44,18 +44,17 @@ def setUp(self): self.adapter.tenant_map = {"Test": self.test_tenant.id, "Update": self.update_tenant.id} self.adapter.status_map = {"Active": self.status_active.id} self.adapter.prefix_map = {} - self.adapter.objects_to_create = {"prefixes": [], "prefix_locs": []} + self.adapter.objects_to_create = {"prefixes": []} self.adapter.objects_to_delete = {"prefixes": []} def test_create(self): """Validate the NautobotPrefix create() method creates a Prefix.""" self.prefix.delete() ids = {"prefix": "10.0.0.0/24", "namespace": "Test"} - attrs = {"location": "Test", "tenant": "Test"} + attrs = {"tenant": "Test"} result = NautobotPrefix.create(self.adapter, ids, attrs) self.assertIsInstance(result, NautobotPrefix) self.assertEqual(len(self.adapter.objects_to_create["prefixes"]), 1) - self.assertEqual(len(self.adapter.objects_to_create["prefix_locs"]), 1) subnet = self.adapter.objects_to_create["prefixes"][0] self.assertEqual(str(subnet.prefix), ids["prefix"]) self.assertEqual(self.adapter.prefix_map[ids["prefix"]], subnet.id) @@ -66,15 +65,13 @@ def test_update(self): test_pf = NautobotPrefix( prefix="10.0.0.0/24", namespace="Test", - location="Test", tenant="Test", uuid=self.prefix.id, ) test_pf.adapter = self.adapter - update_attrs = {"location": "Update", "tenant": "Update"} + update_attrs = {"tenant": "Update"} actual = NautobotPrefix.update(self=test_pf, attrs=update_attrs) self.prefix.refresh_from_db() - self.assertEqual(self.prefix.location, self.update_site) self.assertEqual(self.prefix.tenant, self.update_tenant) self.assertEqual(actual, test_pf) @@ -84,7 +81,6 @@ def test_delete(self, mock_prefix): test_pf = NautobotPrefix( prefix="10.0.0.0/24", namespace="Test", - location="Test", tenant="Test", uuid=self.prefix.id, ) diff --git a/nautobot_ssot/tests/slurpit/__init__.py b/nautobot_ssot/tests/slurpit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nautobot_ssot/tests/slurpit/fixtures/devices.json b/nautobot_ssot/tests/slurpit/fixtures/devices.json new file mode 100644 index 000000000..11e2557c6 --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/devices.json @@ -0,0 +1,146 @@ +[ + { + "id": "16", + "hostname": "sandbox-xr", + "fqdn": "sandbox-iosxr-1.cisco.com", + "ipv4": "", + "port": "22", + "device_os": "cisco_xr", + "device_type": "IOS XRv 9000", + "brand": "Cisco", + "disabled": "0", + "vault_id": "2", + "vault": "admin", + "site": "Cisco HQ", + "added": "manual", + "last_seen": null, + "createddate": "2024-10-15 09:33:02", + "changeddate": "2024-10-15 09:33:02" + }, + { + "id": "34", + "hostname": "mikrotik_routeros_v6", + "fqdn": "mikrotik_routeros_v6.netbox.slurpit.io", + "ipv4": "172.16.238.41", + "port": "22", + "device_os": "mikrotik_routeros", + "device_type": "RB1200", + "brand": "MikroTik", + "disabled": "0", + "vault_id": null, + "vault": null, + "site": null, + "added": "finder", + "last_seen": "2024-11-13 20:00:11", + "createddate": "2024-11-13 19:00:15", + "changeddate": "2024-11-13 19:00:15" + }, + { + "id": "35", + "hostname": "cisco_ios", + "fqdn": "cisco_ios.netbox.slurpit.io", + "ipv4": "172.16.238.40", + "port": "22", + "device_os": "cisco_ios", + "device_type": "3945", + "brand": "Cisco", + "disabled": "0", + "vault_id": null, + "vault": null, + "site": null, + "added": "finder", + "last_seen": "2024-11-13 20:00:11", + "createddate": "2024-11-13 19:00:15", + "changeddate": "2024-11-13 19:00:15" + }, + { + "id": "36", + "hostname": "mikrotik_routeros_v7", + "fqdn": "mikrotik_routeros_v7.netbox.slurpit.io", + "ipv4": "172.16.238.42", + "port": "22", + "device_os": "mikrotik_routeros", + "device_type": "RB1200", + "brand": "MikroTik", + "disabled": "0", + "vault_id": null, + "vault": null, + "site": null, + "added": "finder", + "last_seen": "2024-11-13 20:00:11", + "createddate": "2024-11-13 19:00:15", + "changeddate": "2024-11-13 19:00:15" + }, + { + "id": "37", + "hostname": "hp_comware", + "fqdn": "hp_comware.netbox.slurpit.io", + "ipv4": "172.16.238.45", + "port": "22", + "device_os": "hp_comware", + "device_type": "VSR1000", + "brand": "Hewlett Packard Enterprise", + "disabled": "0", + "vault_id": null, + "vault": null, + "site": null, + "added": "finder", + "last_seen": "2024-11-13 20:00:11", + "createddate": "2024-11-13 19:00:15", + "changeddate": "2024-11-13 19:00:15" + }, + { + "id": "38", + "hostname": "juniper_junos", + "fqdn": "juniper_junos.netbox.slurpit.io", + "ipv4": "172.16.238.46", + "port": "22", + "device_os": "juniper_junos", + "device_type": "VMX", + "brand": "Juniper Networks", + "disabled": "0", + "vault_id": null, + "vault": null, + "site": null, + "added": "finder", + "last_seen": "2024-11-13 20:00:11", + "createddate": "2024-11-13 19:00:15", + "changeddate": "2024-11-13 19:00:15" + }, + { + "id": "39", + "hostname": "arista_eos", + "fqdn": "arista_eos.netbox.slurpit.io", + "ipv4": "172.16.238.43", + "port": "22", + "device_os": "arista_eos", + "device_type": "VEOS", + "brand": "Arista Networks", + "disabled": "0", + "vault_id": null, + "vault": null, + "site": null, + "added": "finder", + "last_seen": "2024-11-13 20:00:11", + "createddate": "2024-11-13 19:00:15", + "changeddate": "2024-11-13 19:00:15" + }, + { + "id": "40", + "hostname": "cisco_nxos", + "fqdn": "cisco_nxos.netbox.slurpit.io", + "ipv4": "172.16.238.44", + "port": "22", + "device_os": "cisco_nxos", + "device_type": "NEXUS 7000", + "brand": "Cisco", + "disabled": "0", + "vault_id": null, + "vault": null, + "site": null, + "added": "finder", + "last_seen": "2024-11-13 20:00:11", + "createddate": "2024-11-13 19:00:15", + "changeddate": "2024-11-13 19:00:15" + } + ] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/fixtures/interfaces.json b/nautobot_ssot/tests/slurpit/fixtures/interfaces.json new file mode 100644 index 000000000..6947e2456 --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/interfaces.json @@ -0,0 +1,614 @@ +[ + { + "Interface": "tunnel-ip3", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "MgmtEth0/RP0/CPU0/0", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "005056bfd307", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.d307", + "Description": "foo", + "IP_fqdn": "", + "IP": "10.10.20.175", + "Duplex": "Full", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "GigabitEthernet0/0/0/0", + "Line": "administratively down", + "Protocol": "administratively down", + "MAC_normalized": "005056bfbb17", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.bb17", + "Description": "", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "unknown", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "GigabitEthernet0/0/0/1", + "Line": "administratively down", + "Protocol": "administratively down", + "MAC_normalized": "005056bf1337", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.1337", + "Description": "test", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "unknown", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "GigabitEthernet0/0/0/2", + "Line": "down", + "Protocol": "down", + "MAC_normalized": "005056bf8181", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.8181", + "Description": "#test#", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "unknown", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "GigabitEthernet0/0/0/3", + "Line": "administratively down", + "Protocol": "administratively down", + "MAC_normalized": "005056bfafc9", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.afc9", + "Description": "test", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "unknown", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "GigabitEthernet0/0/0/4", + "Line": "administratively down", + "Protocol": "administratively down", + "MAC_normalized": "005056bfbedb", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.bedb", + "Description": "", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "unknown", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "GigabitEthernet0/0/0/5", + "Line": "administratively down", + "Protocol": "administratively down", + "MAC_normalized": "005056bf3ee8", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.3ee8", + "Description": "port-test-eem", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "unknown", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "GigabitEthernet0/0/0/6", + "Line": "down", + "Protocol": "down", + "MAC_normalized": "005056bfee29", + "MAC_vendor": "VMware, Inc.", + "MAC": "0050.56bf.ee29", + "Description": "test06", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "unknown", + "Speed": "1000Mb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback82", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "88.88.88.88", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback99", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "88.88.88.88", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback100", + "Line": "administratively down", + "Protocol": "administratively down", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "testing-eem", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback102", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "192.168.102.2", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback144", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "88.88.88.88", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback152", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "10.0.152.1", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback153", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "10.0.152.2", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback9999", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "2.2.2.2/32", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback2147483647", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "10.0.0.23", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Null0", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback1", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "Ruddy NETCONF loopback", + "IP_fqdn": "", + "IP": "10.1.1.1", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback2", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "10.10.35.50", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback3", + "Line": "administratively down", + "Protocol": "administratively down", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "TEST-TRIGGER-EEM", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback4", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "Miguel Brito NETCONF loopback", + "IP_fqdn": "", + "IP": "10.1.1.20", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback5", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "192.168.5.1", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback6", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "192.168.1.4/32", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback10", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "192.168.10.1/32", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback30", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "192.168.8.2", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback35", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "192.168.35.2", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback81", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "10.10.10.10", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Bundle-Ether6.6477", + "Line": "down", + "Protocol": "down", + "MAC_normalized": "001795008f37", + "MAC_vendor": "Cisco Systems, Inc", + "MAC": "0017.9500.8f37", + "Description": "SBI-ML3-3-184671-AKC-4096k-STATE-BANK-OF-INDIA-3-4651111-30102023", + "IP_fqdn": "", + "IP": "10.171.221.134", + "Duplex": "", + "Speed": "", + "Vlan": "2800", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Loopback0", + "Line": "up", + "Protocol": "up", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "Loopback Interface", + "IP_fqdn": "", + "IP": "192.168.9.2", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Bundle-Ether1", + "Line": "down", + "Protocol": "down", + "MAC_normalized": "001795008f38", + "MAC_vendor": "Cisco Systems, Inc", + "MAC": "0017.9500.8f38", + "Description": "", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "Full", + "Speed": "0Kb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "Bundle-Ether6", + "Line": "down", + "Protocol": "down", + "MAC_normalized": "001795008f37", + "MAC_vendor": "Cisco Systems, Inc", + "MAC": "0017.9500.8f37", + "Description": "#test#", + "IP_fqdn": "", + "IP": "Unknown", + "Duplex": "Full", + "Speed": "0Kb/s", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + }, + { + "Interface": "BVI100", + "Line": "down", + "Protocol": "down", + "MAC_normalized": "", + "MAC_vendor": "", + "MAC": "", + "Description": "", + "IP_fqdn": "", + "IP": "192.168.10.1", + "Duplex": "", + "Speed": "", + "Vlan": "", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-14 0:12:00" + ] + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/fixtures/inventory_items.json b/nautobot_ssot/tests/slurpit/fixtures/inventory_items.json new file mode 100644 index 000000000..4226d6608 --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/inventory_items.json @@ -0,0 +1,35 @@ +[ + { + "Name": "0/RP0", + "Descr": "Cisco IOS-XRv 9000 Centralized Route Processor", + "Product": "R-IOSXRV9000-RP-C", + "Version": "V01", + "Serial": "F2E02123A63", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 8:08:00" + ] + }, + { + "Name": "Rack 0", + "Descr": "Cisco IOS-XRv 9000 Centralized Virtual Router", + "Product": "R-IOSXRV9000-CC", + "Version": "V01", + "Serial": "B550ED1D0D9", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 8:08:00" + ] + }, + { + "Name": "0/0", + "Descr": "Cisco IOS-XRv 9000 Centralized Line Card", + "Product": "R-IOSXRV9000-LC-C", + "Version": "V01", + "Serial": "86E4A261046", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 8:08:00" + ] + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/fixtures/plannings.json b/nautobot_ssot/tests/slurpit/fixtures/plannings.json new file mode 100644 index 000000000..bf1c2f98a --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/plannings.json @@ -0,0 +1,519 @@ +[ + { + "id": "3", + "name": "Interfaces", + "slug": "interfaces", + "comment": "Discover all interfaces", + "disabled": "0", + "createddate": "2023-04-03 12:41:24", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Interface", + "type": "text", + "key": true + }, + { + "column": "Line", + "type": "text", + "key": true + }, + { + "column": "Protocol", + "type": "text", + "key": true + }, + { + "column": "IP", + "type": "ip", + "key": true + }, + { + "column": "MAC", + "type": "mac", + "key": true + }, + { + "column": "Description", + "type": "text", + "key": true + }, + { + "column": "Duplex", + "type": "text", + "key": true + }, + { + "column": "Speed", + "type": "text", + "key": true + }, + { + "column": "Vlan", + "type": "text", + "key": true + } + ] + }, + { + "id": "4", + "name": "Hardware info", + "slug": "hardware-info", + "comment": "Discover all hardware information like serial numbers", + "disabled": "0", + "createddate": "2023-04-03 13:14:42", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Name", + "type": "text", + "key": true + }, + { + "column": "Descr", + "type": "text", + "key": false + }, + { + "column": "Product", + "type": "text", + "key": false + }, + { + "column": "Version", + "type": "text", + "key": true + }, + { + "column": "Serial", + "type": "text", + "key": true + } + ] + }, + { + "id": "5", + "name": "Vlans", + "slug": "vlans", + "comment": "Discover all vlans", + "disabled": "0", + "createddate": "2023-04-03 13:14:53", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Vlan", + "type": "text", + "key": true + }, + { + "column": "Name", + "type": "text", + "key": true + }, + { + "column": "Status", + "type": "text", + "key": true + }, + { + "column": "Interfaces", + "type": "text", + "key": true + } + ] + }, + { + "id": "6", + "name": "Mac addresses", + "slug": "mac-addresses", + "comment": "Discover all Mac addresses", + "disabled": "0", + "createddate": "2023-04-03 13:15:22", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Interface", + "type": "text", + "key": true + }, + { + "column": "MAC", + "type": "mac", + "key": true + }, + { + "column": "Type", + "type": "text", + "key": true + }, + { + "column": "Vlan", + "type": "text", + "key": true + } + ] + }, + { + "id": "7", + "name": "Software versions", + "slug": "software-versions", + "comment": "Discover all software versions", + "disabled": "0", + "createddate": "2023-04-03 13:15:36", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Version", + "type": "text", + "key": true + }, + { + "column": "File", + "type": "text", + "key": true + }, + { + "column": "Uptime", + "type": "text", + "key": false + }, + { + "column": "Type", + "type": "text", + "key": true + }, + { + "column": "Serial", + "type": "text", + "key": true + }, + { + "column": "Boot", + "type": "text", + "key": false + }, + { + "column": "Reboot Reason", + "type": "text", + "key": false + } + ] + }, + { + "id": "8", + "name": "ARP", + "slug": "arp", + "comment": "Discover all ARP entries", + "disabled": "0", + "createddate": "2023-04-03 13:17:02", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "IP Address", + "type": "ip", + "key": true + }, + { + "column": "Age", + "type": "text", + "key": false + }, + { + "column": "Mac", + "type": "mac", + "key": true + }, + { + "column": "Type", + "type": "text", + "key": false + }, + { + "column": "Interface", + "type": "text", + "key": true + }, + { + "column": "VRF", + "type": "text", + "key": true + } + ] + }, + { + "id": "14", + "name": "LLDP", + "slug": "lldp", + "comment": "Discover neighbor information with LLDP", + "disabled": "0", + "createddate": "2023-07-03 18:11:08", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Interface", + "type": "text", + "key": true + }, + { + "column": "Neighbor_interface", + "type": "text", + "key": true + }, + { + "column": "Neighbord_interface_desc", + "type": "text", + "key": true + }, + { + "column": "Neighbor", + "type": "text", + "key": true + }, + { + "column": "System_description", + "type": "text", + "key": true + }, + { + "column": "Management_ip", + "type": "ip", + "key": true + } + ] + }, + { + "id": "15", + "name": "CDP", + "slug": "cdp", + "comment": "Discover Cisco neighbor information with CDP", + "disabled": "0", + "createddate": "2023-07-03 18:32:00", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Destination_host", + "type": "text", + "key": true + }, + { + "column": "Management_ip", + "type": "ip", + "key": true + }, + { + "column": "Platform", + "type": "text", + "key": false + }, + { + "column": "Remote_port", + "type": "text", + "key": true + }, + { + "column": "Local_port", + "type": "text", + "key": true + }, + { + "column": "Software_version", + "type": "text", + "key": false + }, + { + "column": "Capabilities", + "type": "text", + "key": false + } + ] + }, + { + "id": "16", + "name": "Clock", + "slug": "clock", + "comment": "Discover Clock information", + "disabled": "0", + "createddate": "2023-07-03 18:35:29", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Time", + "type": "text", + "key": false + }, + { + "column": "Timezone", + "type": "text", + "key": false + }, + { + "column": "Dayweek", + "type": "text", + "key": false + }, + { + "column": "Month", + "type": "text", + "key": false + }, + { + "column": "Day", + "type": "text", + "key": false + }, + { + "column": "Year", + "type": "text", + "key": false + } + ] + }, + { + "id": "17", + "name": "Routing Table", + "slug": "routing-table", + "comment": "Discover the routing table", + "disabled": "0", + "createddate": "2023-07-03 18:38:49", + "changeddate": "2024-10-15 09:00:07", + "columns": [ + { + "column": "Network", + "type": "text", + "key": true + }, + { + "column": "Mask", + "type": "text", + "key": true + }, + { + "column": "Vrf", + "type": "text", + "key": true + }, + { + "column": "Nexthop_ip", + "type": "text", + "key": true + }, + { + "column": "Nexthop_if", + "type": "text", + "key": true + }, + { + "column": "Nexthop_vrf", + "type": "text", + "key": true + }, + { + "column": "Protocol", + "type": "text", + "key": true + }, + { + "column": "Type", + "type": "text", + "key": true + }, + { + "column": "Distance", + "type": "text", + "key": true + }, + { + "column": "Metric", + "type": "text", + "key": true + }, + { + "column": "Uptime", + "type": "text", + "key": false + } + ] + }, + { + "id": "18", + "name": "VRFs", + "slug": null, + "comment": "Discover all VRFs", + "disabled": "0", + "createddate": "2024-10-16 11:18:17", + "changeddate": "2024-10-16 11:18:17", + "columns": [ + { + "column": "VRF", + "type": "text", + "key": true + }, + { + "column": "VRF_ID", + "type": "text", + "key": true + }, + { + "column": "Interfaces", + "type": "text", + "key": true + } + ] + }, + { + "id": "19", + "name": "License", + "slug": null, + "comment": "Discover the License status of your devices", + "disabled": "0", + "createddate": "2024-10-16 11:18:17", + "changeddate": "2024-10-16 11:18:17", + "columns": [ + { + "column": "Feature", + "type": "text", + "key": true + }, + { + "column": "Issued", + "type": "text", + "key": true + }, + { + "column": "Description", + "type": "text", + "key": false + }, + { + "column": "Expires", + "type": "text", + "key": false + }, + { + "column": "Period_used", + "type": "text", + "key": false + }, + { + "column": "License_type", + "type": "text", + "key": true + }, + { + "column": "License_state", + "type": "text", + "key": false + }, + { + "column": "License_count", + "type": "text", + "key": true + }, + { + "column": "License_priority", + "type": "text", + "key": false + } + ] + } + ] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/fixtures/prefixes.json b/nautobot_ssot/tests/slurpit/fixtures/prefixes.json new file mode 100644 index 000000000..4d53e0713 --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/prefixes.json @@ -0,0 +1,495 @@ +[ + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.10.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback10", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.10.1/32" + }, + { + "Vrf": "", + "Protocol": "S", + "Network": "222.228.1.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Null0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "222.228.1.1/32" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.9.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.9.2/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.102.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback102", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.102.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.102.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback102", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.102.2/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.35.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback35", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.35.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.35.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback35", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.35.2/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.5.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback5", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.5.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.5.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback5", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.5.1/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.9.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.9.0/24" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.8.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback30", + "Uptime": "1d17h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.8.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.8.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback30", + "Uptime": "1d17h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.8.2/32" + }, + { + "Vrf": "", + "Protocol": "S", + "Network": "111.118.1.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Null0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "111.118.1.1/32" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.1.4", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback6", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "192.168.1.4/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.10.20.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "MgmtEth0/RP0/CPU0/0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.10.20.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.10.20.175", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "MgmtEth0/RP0/CPU0/0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.10.20.175/32" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.1.1.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback1", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.1.1.1/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.10.35.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback2", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.10.35.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.10.35.50", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback2", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.10.35.50/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.10.10.0", + "Mask": "28", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback81", + "Uptime": "3d11h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.10.10.0/28" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.10.10.10", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback81", + "Uptime": "3d11h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.10.10.10/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "88.88.88.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback82", + "Uptime": "3d08h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "88.88.88.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "88.88.88.88", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback82", + "Uptime": "3d08h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "88.88.88.88/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.1.1.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback1", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.1.1.0/24" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.0.152.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback152", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.0.152.0/24" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.0.152.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback152", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.0.152.1/32" + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "2.2.2.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback9999", + "Uptime": "17:37:12", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "2.2.2.2/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.171.221.134", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "", + "Uptime": "17:37:12", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.171.221.134/32" + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.0.0.23", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "", + "Uptime": "17:37:12", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ], + "normalized_prefix": "10.0.0.23/32" + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/fixtures/routing_table.json b/nautobot_ssot/tests/slurpit/fixtures/routing_table.json new file mode 100644 index 000000000..ac32217ac --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/routing_table.json @@ -0,0 +1,466 @@ +[ + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.10.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback10", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "S", + "Network": "222.228.1.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Null0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.9.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.102.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback102", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.102.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback102", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.35.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback35", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.35.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback35", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.5.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback5", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.5.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback5", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.9.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "192.168.8.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback30", + "Uptime": "1d17h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.8.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback30", + "Uptime": "1d17h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "S", + "Network": "111.118.1.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Null0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "192.168.1.4", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback6", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.10.20.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "MgmtEth0/RP0/CPU0/0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.10.20.175", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "MgmtEth0/RP0/CPU0/0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.1.1.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback1", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.10.35.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback2", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.10.35.50", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback2", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.10.10.0", + "Mask": "28", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback81", + "Uptime": "3d11h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.10.10.10", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback81", + "Uptime": "3d11h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "88.88.88.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback82", + "Uptime": "3d08h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "88.88.88.88", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback82", + "Uptime": "3d08h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.1.1.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback1", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "C", + "Network": "10.0.152.0", + "Mask": "24", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback152", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "10.0.152.1", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback152", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "S*", + "Network": "0.0.0.0", + "Mask": "0", + "Distance": "1", + "Metric": "0", + "Type": "via", + "Nexthop_ip": "10.10.20.254", + "Nexthop_if": "MgmtEth0/RP0/CPU0/0", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "L", + "Network": "2.2.2.2", + "Mask": "32", + "Distance": "", + "Metric": "", + "Type": "directly", + "Nexthop_ip": "connected", + "Nexthop_if": "Loopback9999", + "Uptime": "17:37:12", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + }, + { + "Vrf": "", + "Protocol": "S*", + "Network": "0.0.0.0", + "Mask": "0", + "Distance": "1", + "Metric": "0", + "Type": "via", + "Nexthop_ip": "10.10.20.254", + "Nexthop_if": "", + "Uptime": "4d04h", + "hostname": "sandbox-xr", + "timestamp": [ + "2024-11-13 21:09:01" + ] + } +] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/fixtures/sites.json b/nautobot_ssot/tests/slurpit/fixtures/sites.json new file mode 100644 index 000000000..750d33f79 --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/sites.json @@ -0,0 +1,38 @@ +[ + { + "id": "1", + "sitename": "Slurpit HQ", + "street": "Domplein", + "number": "9", + "city": "Utrecht", + "county": "", + "state": "", + "zipcode": "3512JC", + "country": "Utrecht", + "status": "1", + "description": "", + "phonenumber": "", + "longitude": "47.301.056", + "latitude": "52.099.108", + "createddate": "2024-10-15 09:11:16", + "changeddate": "2024-10-15 09:11:16" + }, + { + "id": "2", + "sitename": "Cisco HQ", + "street": "Zanker Road", + "number": "3850", + "city": "San Jose", + "county": "Santa Clara County", + "state": "California", + "zipcode": "95134", + "country": "United States", + "status": "1", + "description": "Building 1", + "phonenumber": "", + "longitude": "-12.193.695.709.822.100", + "latitude": "3.741.466", + "createddate": "2024-10-15 09:11:16", + "changeddate": "2024-10-15 09:11:16" + } + ] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/fixtures/vlans.json b/nautobot_ssot/tests/slurpit/fixtures/vlans.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/nautobot_ssot/tests/slurpit/fixtures/vlans.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/nautobot_ssot/tests/slurpit/test_jobs.py b/nautobot_ssot/tests/slurpit/test_jobs.py new file mode 100644 index 000000000..09b157cd9 --- /dev/null +++ b/nautobot_ssot/tests/slurpit/test_jobs.py @@ -0,0 +1,40 @@ +"""Test Slurpit Jobs.""" + +from copy import deepcopy + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse + +from nautobot_ssot.integrations.slurpit import jobs + +CONFIG = settings.PLUGINS_CONFIG.get("nautobot_ssot", {}) +BACKUP_CONFIG = deepcopy(CONFIG) + + +class SlurpitJobTest(TestCase): + """Test the Slurpit job.""" + + def test_metadata(self): + """Verify correctness of the Job Meta attributes.""" + meta = jobs.SlurpitDataSource.Meta + self.assertEqual("Slurpit Data Source", jobs.SlurpitDataSource.name) + self.assertEqual("Slurpit Data Source", meta.name) + self.assertEqual("Slurpit", meta.data_source) + self.assertEqual("Sync information from Slurpit to Nautobot.", meta.description) + + def test_data_mapping(self): + """Verify correctness of the data_mappings() API.""" + mappings = jobs.SlurpitDataSource.data_mappings() + expected_mappings = [ + ("Site", None, "Location", reverse("dcim:location_list")), + ("Manufacturer", None, "Manufacturer", reverse("dcim:manufacturer_list")), + ("Device Type", None, "Device Type", reverse("dcim:devicetype_list")), + ("Platform", None, "Platform", reverse("dcim:platform_list")), + ] + + for i, (source_name, source_url, target_name, target_url) in enumerate(expected_mappings): + self.assertEqual(source_name, mappings[i].source_name) + self.assertIsNone(source_url) + self.assertEqual(target_name, mappings[i].target_name) + self.assertEqual(target_url, mappings[i].target_url) diff --git a/nautobot_ssot/tests/slurpit/test_slurpit_adapter.py b/nautobot_ssot/tests/slurpit/test_slurpit_adapter.py new file mode 100644 index 000000000..7cc7ed5f2 --- /dev/null +++ b/nautobot_ssot/tests/slurpit/test_slurpit_adapter.py @@ -0,0 +1,138 @@ +"""Unit tests for the Slurpit DiffSync adapter class.""" + +import json +from unittest import TestCase +from unittest.mock import AsyncMock, MagicMock + +from nautobot.dcim.models import LocationType +from nautobot.extras.models import JobResult +from nautobot.ipam.models import Namespace +from slurpit.models.device import Device +from slurpit.models.planning import Planning +from slurpit.models.site import Site +from slurpit.utils.utils import handle_response_data + +from nautobot_ssot.integrations.slurpit import constants +from nautobot_ssot.integrations.slurpit.diffsync.adapters.slurpit import SlurpitAdapter +from nautobot_ssot.integrations.slurpit.jobs import SlurpitDataSource + + +def load_json(path): + """Load a json file.""" + with open(path, encoding="utf-8") as file: + return json.loads(file.read()) + + +SITE_FIXTURE = handle_response_data(load_json("./nautobot_ssot/tests/slurpit/fixtures/sites.json"), object_class=Site) +DEVICE_FIXTURE = handle_response_data( + load_json("./nautobot_ssot/tests/slurpit/fixtures/devices.json"), object_class=Device +) +PLANNING_FIXTURE = handle_response_data( + load_json("./nautobot_ssot/tests/slurpit/fixtures/plannings.json"), object_class=Planning +) +INVENTORY_ITEM_FIXTURE = load_json("./nautobot_ssot/tests/slurpit/fixtures/inventory_items.json") +VLANS_FIXTURE = load_json("./nautobot_ssot/tests/slurpit/fixtures/vlans.json") +ROUTING_TABLE_FIXTURE = load_json("./nautobot_ssot/tests/slurpit/fixtures/routing_table.json") +INTERFACES_FIXTURE = load_json("./nautobot_ssot/tests/slurpit/fixtures/interfaces.json") + + +class SlurpitDiffSyncTestCase(TestCase): + """Test the SlurpitDiffSync adapter class.""" + + def setUp(self): + slurpit_client = AsyncMock() + slurpit_client.site.get_sites = AsyncMock(return_value=SITE_FIXTURE) + slurpit_client.device.get_devices = AsyncMock(return_value=DEVICE_FIXTURE) + slurpit_client.planning.get_plannings = AsyncMock(return_value=PLANNING_FIXTURE) + + job = SlurpitDataSource() + job.job_result = JobResult.objects.create(name=job.class_path, task_name="fake task", worker="default") + job.site_loctype = LocationType.objects.get_or_create(name="Site")[0] + job.namespace = Namespace.objects.get_or_create(name="Global")[0] + self.slurpit = SlurpitAdapter(job=job, api_client=slurpit_client) + + def site_effect(value): + return { + "hardware-info": INVENTORY_ITEM_FIXTURE, + "vlans": VLANS_FIXTURE, + "routing-table": ROUTING_TABLE_FIXTURE, + "interfaces": INTERFACES_FIXTURE, + }.get(value, []) + + self.slurpit.planning_results = MagicMock(return_value=PLANNING_FIXTURE, side_effect=site_effect) + self.slurpit.load() + + def test_loading_data(self): + """Test the load() function.""" + print(self.slurpit.get_all("location")) + self.assertEqual( + {site.sitename for site in SITE_FIXTURE}, + {site.name for site in self.slurpit.get_all("location")}, + ) + + self.assertEqual( + {brand["brand"] for brand in self.slurpit.unique_vendors()}, + {vendor.name for vendor in self.slurpit.get_all("manufacturer")}, + ) + + self.assertEqual( + {device_type["device_type"] for device_type in self.slurpit.unique_device_type()}, + {device_type.model for device_type in self.slurpit.get_all("device_type")}, + ) + + self.assertEqual( + set(self.slurpit.unique_platforms()), + {platform.name for platform in self.slurpit.get_all("platform")}, + ) + + self.assertEqual(constants.DEFAULT_DEVICE_ROLE, self.slurpit.get_all("role")[0].name) + + self.assertEqual( + {device.hostname for device in DEVICE_FIXTURE}, + {device.name for device in self.slurpit.get_all("device")}, + ) + + inventory_item_names = [ + inventory_item.get("Name") or inventory_item.get("Product") + for inventory_item in self.slurpit.planning_results("hardware-info") + ] + + self.assertEqual( + set(inventory_item_names), + {inventory_item.name for inventory_item in self.slurpit.get_all("inventory_item")}, + ) + + self.assertEqual( + {vlan["Name"] for vlan in self.slurpit.planning_results("vlans")}, + {vlan.name for vlan in self.slurpit.get_all("vlan")}, + ) + + vrfs = {vrf["Vrf"] for vrf in self.slurpit.planning_results("routing-table") if vrf.get("Vrf", "")} + + self.assertEqual( + vrfs, + {vrf.name for vrf in self.slurpit.get_all("vrf")}, + ) + + self.assertEqual( + { + prefix["normalized_prefix"].split("/")[0] + for prefix in load_json("./nautobot_ssot/tests/slurpit/fixtures/prefixes.json") + }, + {prefix.network for prefix in self.slurpit.get_all("prefix")}, + ) + + interfaces = self.slurpit.planning_results("interfaces") + + self.assertEqual( + { + ip["normalized_address"].split("/")[0] + for ip in self.slurpit.run_async(self.slurpit.filter_interfaces(interfaces)) + }, + {ip.host for ip in self.slurpit.get_all("ipaddress")}, + ) + + self.assertEqual( + {interface["Interface"] for interface in self.slurpit.planning_results("interfaces")}, + {interface.name for interface in self.slurpit.get_all("interface")}, + ) diff --git a/nautobot_ssot/tests/test_utils.py b/nautobot_ssot/tests/test_utils.py new file mode 100644 index 000000000..886eff5e4 --- /dev/null +++ b/nautobot_ssot/tests/test_utils.py @@ -0,0 +1,27 @@ +"""Tests for utility functions.""" + +import unittest + +from nautobot_ssot.utils import parse_hostname_for_role + + +class TestSSoTUtils(unittest.TestCase): + """Test SSoT utility functions.""" + + def test_parse_hostname_for_role_success(self): + """Validate the functionality of the parse_hostname_for_role method success.""" + hostname_mapping = [(".*EDGE.*", "Edge"), (".*DMZ.*", "DMZ")] + hostname = "DMZ-switch.example.com" + result = parse_hostname_for_role( + hostname_map=hostname_mapping, device_hostname=hostname, default_role="Unknown" + ) + self.assertEqual(result, "DMZ") + + def test_parse_hostname_for_role_failure(self): + """Validate the functionality of the parse_hostname_for_role method failure.""" + hostname_mapping = [] + hostname = "core-router.example.com" + result = parse_hostname_for_role( + hostname_map=hostname_mapping, device_hostname=hostname, default_role="Unknown" + ) + self.assertEqual(result, "Unknown") diff --git a/nautobot_ssot/utils.py b/nautobot_ssot/utils.py index b0333b455..7ca3a77f4 100644 --- a/nautobot_ssot/utils.py +++ b/nautobot_ssot/utils.py @@ -1,10 +1,19 @@ """Utility functions for Nautobot SSoT App.""" import logging +import re +from typing import List, Tuple -from nautobot.dcim.models import Controller, ControllerManagedDeviceGroup from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices -from nautobot.extras.models import CustomField, SecretsGroup +from nautobot.extras.models import SecretsGroup + +try: + from nautobot.dcim.models import Controller, ControllerManagedDeviceGroup + + CONTROLLER_FOUND = True +except (ImportError, RuntimeError): + CONTROLLER_FOUND = False + logger = logging.getLogger("nautobot.ssot") @@ -26,25 +35,48 @@ def get_username_password_https_from_secretsgroup(group: SecretsGroup): return username, password -def verify_controller_managed_device_group(controller: Controller) -> ControllerManagedDeviceGroup: - """Validate that Controller Managed Device Group exists or create it. +if CONTROLLER_FOUND: - Args: - controller (Controller): Controller for associated ManagedDeviceGroup. + def verify_controller_managed_device_group(controller: Controller) -> ControllerManagedDeviceGroup: + """Validate that Controller Managed Device Group exists or create it. - Returns: - ControllerManagedDeviceGroup: The ControllerManagedDeviceGroup that was either found or created for the Controller. - """ - return ControllerManagedDeviceGroup.objects.get_or_create( - controller=controller, defaults={"name": f"{controller.name} Managed Devices"} - )[0] + Args: + controller (Controller): Controller for associated ManagedDeviceGroup. + + Returns: + ControllerManagedDeviceGroup: The ControllerManagedDeviceGroup that was either found or created for the Controller. + """ + return ControllerManagedDeviceGroup.objects.get_or_create( + controller=controller, defaults={"name": f"{controller.name} Managed Devices"} + )[0] -def create_or_update_custom_field(key, field_type, label): +def create_or_update_custom_field(apps, key, field_type, label): """Create or update a custom field object.""" + CustomField = apps.get_model("extras", "CustomField") # pylint: disable=invalid-name cf_dict = { "type": field_type, "key": key, "label": label, } return CustomField.objects.update_or_create(key=cf_dict["key"], defaults=cf_dict) + + +def parse_hostname_for_role(hostname_map: List[Tuple[str, str]], device_hostname: str, default_role: str): + """Parse device hostname from hostname_map to get Device Role. + + Args: + hostname_map (List[Tuple[str, str]]): List of tuples containing regex to compare with hostname and associated DeviceRole name. + device_hostname (str): Hostname of Device to determine role of. + default_role (str): String representing default Role to return if no match found. + + Returns: + str: Name of DeviceRole. Defaults to default_role. + """ + device_role = default_role + if hostname_map: # pylint: disable=duplicate-code + for entry in hostname_map: + match = re.match(pattern=entry[0], string=device_hostname) + if match: + device_role = entry[1] + return device_role diff --git a/poetry.lock b/poetry.lock index 57f5a3020..c7cbc1cd0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -13,108 +13,108 @@ files = [ [[package]] name = "aiohttp" -version = "3.10.10" +version = "3.10.11" description = "Async http client/server framework (asyncio)" optional = true python-versions = ">=3.8" files = [ - {file = "aiohttp-3.10.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f"}, - {file = "aiohttp-3.10.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9"}, - {file = "aiohttp-3.10.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8"}, - {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1"}, - {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a"}, - {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd"}, - {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026"}, - {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b"}, - {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d"}, - {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7"}, - {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a"}, - {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc"}, - {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68"}, - {file = "aiohttp-3.10.10-cp310-cp310-win32.whl", hash = "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257"}, - {file = "aiohttp-3.10.10-cp310-cp310-win_amd64.whl", hash = "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6"}, - {file = "aiohttp-3.10.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f"}, - {file = "aiohttp-3.10.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb"}, - {file = "aiohttp-3.10.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871"}, - {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c"}, - {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38"}, - {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb"}, - {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7"}, - {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911"}, - {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092"}, - {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142"}, - {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9"}, - {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1"}, - {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a"}, - {file = "aiohttp-3.10.10-cp311-cp311-win32.whl", hash = "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94"}, - {file = "aiohttp-3.10.10-cp311-cp311-win_amd64.whl", hash = "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959"}, - {file = "aiohttp-3.10.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c"}, - {file = "aiohttp-3.10.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28"}, - {file = "aiohttp-3.10.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f"}, - {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138"}, - {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742"}, - {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7"}, - {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16"}, - {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8"}, - {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6"}, - {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a"}, - {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9"}, - {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a"}, - {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205"}, - {file = "aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628"}, - {file = "aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf"}, - {file = "aiohttp-3.10.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28"}, - {file = "aiohttp-3.10.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d"}, - {file = "aiohttp-3.10.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79"}, - {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e"}, - {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6"}, - {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42"}, - {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e"}, - {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc"}, - {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a"}, - {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414"}, - {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3"}, - {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67"}, - {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b"}, - {file = "aiohttp-3.10.10-cp313-cp313-win32.whl", hash = "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8"}, - {file = "aiohttp-3.10.10-cp313-cp313-win_amd64.whl", hash = "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151"}, - {file = "aiohttp-3.10.10-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486"}, - {file = "aiohttp-3.10.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb"}, - {file = "aiohttp-3.10.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7"}, - {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8"}, - {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8"}, - {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa"}, - {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d"}, - {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f"}, - {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2"}, - {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572"}, - {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480"}, - {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a"}, - {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b"}, - {file = "aiohttp-3.10.10-cp38-cp38-win32.whl", hash = "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c"}, - {file = "aiohttp-3.10.10-cp38-cp38-win_amd64.whl", hash = "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce"}, - {file = "aiohttp-3.10.10-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24"}, - {file = "aiohttp-3.10.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc"}, - {file = "aiohttp-3.10.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7"}, - {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c"}, - {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5"}, - {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090"}, - {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762"}, - {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554"}, - {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527"}, - {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2"}, - {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8"}, - {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab"}, - {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91"}, - {file = "aiohttp-3.10.10-cp39-cp39-win32.whl", hash = "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983"}, - {file = "aiohttp-3.10.10-cp39-cp39-win_amd64.whl", hash = "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23"}, - {file = "aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a"}, + {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"}, + {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"}, + {file = "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115"}, + {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc"}, + {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d"}, + {file = "aiohttp-3.10.11-cp310-cp310-win32.whl", hash = "sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120"}, + {file = "aiohttp-3.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695"}, + {file = "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9"}, + {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730"}, + {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8"}, + {file = "aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9"}, + {file = "aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d"}, + {file = "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087"}, + {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e"}, + {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4"}, + {file = "aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb"}, + {file = "aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413"}, + {file = "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1"}, + {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d"}, + {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00"}, + {file = "aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71"}, + {file = "aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339"}, + {file = "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca"}, + {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9"}, + {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7"}, + {file = "aiohttp-3.10.11-cp38-cp38-win32.whl", hash = "sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4"}, + {file = "aiohttp-3.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6"}, + {file = "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc"}, + {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261"}, + {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f"}, + {file = "aiohttp-3.10.11-cp39-cp39-win32.whl", hash = "sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9"}, + {file = "aiohttp-3.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb"}, + {file = "aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7"}, ] [package.dependencies] aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" @@ -150,13 +150,13 @@ files = [ [[package]] name = "amqp" -version = "5.2.0" +version = "5.3.1" description = "Low-level AMQP client for Python (fork of amqplib)." optional = false python-versions = ">=3.6" files = [ - {file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"}, - {file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"}, + {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, + {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, ] [package.dependencies] @@ -291,13 +291,13 @@ wheel = ">=0.23.0,<1.0" [[package]] name = "async-timeout" -version = "4.0.3" +version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] @@ -1088,20 +1088,20 @@ files = [ [[package]] name = "diffsync" -version = "2.0.0" +version = "2.0.1" description = "Library to easily sync/diff/update 2 different data sources" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "diffsync-2.0.0-py3-none-any.whl", hash = "sha256:59f864a115abc5b0aa3b9db0d44deff59c81cd5469e5894326c27e29511e3aab"}, - {file = "diffsync-2.0.0.tar.gz", hash = "sha256:712bc85a24f49ef6075344dc3a16c85e27b1416154c46fd5de7acf72e8321a9b"}, + {file = "diffsync-2.0.1-py3-none-any.whl", hash = "sha256:c375139d2d0c060106ef4f724044bd70ec03296f90255cd41831946e0544a585"}, + {file = "diffsync-2.0.1.tar.gz", hash = "sha256:7f6fe7705b0669f0249b18c97231b74bdfa2530117ad98f3b79a563c07ff7728"}, ] [package.dependencies] colorama = ">=0.4.3,<0.5.0" packaging = ">=21.3,<24.0" pydantic = ">=2.0.0,<3.0.0" -structlog = ">=20.1.0,<23.0.0" +structlog = ">=20.1.0" typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] @@ -1510,13 +1510,13 @@ django = ">=4.2" [[package]] name = "dnacentersdk" -version = "2.7.4" +version = "2.7.7" description = "Cisco DNA Center Platform SDK" optional = true python-versions = "<4.0,>=3.8" files = [ - {file = "dnacentersdk-2.7.4-py3-none-any.whl", hash = "sha256:befab14a8a7a2dc3e6f4a51e66ec3bf3937f97dbad091098670d1a0620eb87a3"}, - {file = "dnacentersdk-2.7.4.tar.gz", hash = "sha256:91cbe7c7664afd59afca9150819cf45c5b6d0705e218dadf21fee1882a2f0802"}, + {file = "dnacentersdk-2.7.7-py3-none-any.whl", hash = "sha256:815bb07cf5a9c498b4e6ae971a986b2a3de7285c0989ea7854aebd32492a918b"}, + {file = "dnacentersdk-2.7.7.tar.gz", hash = "sha256:95bfc91e10fbb91866a816a29cd25cde1986cb924157edbc2c8786a48fa4405d"}, ] [package.dependencies] @@ -1596,13 +1596,13 @@ sidecar = ["drf-spectacular-sidecar"] [[package]] name = "drf-spectacular-sidecar" -version = "2024.7.1" +version = "2024.11.1" description = "Serve self-contained distribution builds of Swagger UI and Redoc with Django" optional = false python-versions = ">=3.6" files = [ - {file = "drf_spectacular_sidecar-2024.7.1-py3-none-any.whl", hash = "sha256:5dc8b38ad153e90b328152674c7959bf114bf86360a617a5a4516e135cb832bc"}, - {file = "drf_spectacular_sidecar-2024.7.1.tar.gz", hash = "sha256:beb992d6ece806a2d422ad626983e2472c0a5550de9647a7ed6764716a5abdfe"}, + {file = "drf_spectacular_sidecar-2024.11.1-py3-none-any.whl", hash = "sha256:e2efd49c5bd1a607fd5d120d9da58d78e587852db8220b8880282a849296ff83"}, + {file = "drf_spectacular_sidecar-2024.11.1.tar.gz", hash = "sha256:fcfccc72cbdbe41e93f8416fa0c712d14126b8d1629e65c09c07c8edea24aad0"}, ] [package.dependencies] @@ -1669,59 +1669,61 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "fonttools" -version = "4.54.1" +version = "4.55.0" description = "Tools to manipulate font files" optional = true python-versions = ">=3.8" files = [ - {file = "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2"}, - {file = "fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882"}, - {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10"}, - {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e"}, - {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e"}, - {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44"}, - {file = "fonttools-4.54.1-cp310-cp310-win32.whl", hash = "sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02"}, - {file = "fonttools-4.54.1-cp310-cp310-win_amd64.whl", hash = "sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d"}, - {file = "fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20"}, - {file = "fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2"}, - {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7"}, - {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, - {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8"}, - {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a"}, - {file = "fonttools-4.54.1-cp311-cp311-win32.whl", hash = "sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc"}, - {file = "fonttools-4.54.1-cp311-cp311-win_amd64.whl", hash = "sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6"}, - {file = "fonttools-4.54.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d"}, - {file = "fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08"}, - {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263"}, - {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab"}, - {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d"}, - {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714"}, - {file = "fonttools-4.54.1-cp312-cp312-win32.whl", hash = "sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac"}, - {file = "fonttools-4.54.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e"}, - {file = "fonttools-4.54.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff"}, - {file = "fonttools-4.54.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb"}, - {file = "fonttools-4.54.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a"}, - {file = "fonttools-4.54.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c"}, - {file = "fonttools-4.54.1-cp313-cp313-win32.whl", hash = "sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58"}, - {file = "fonttools-4.54.1-cp313-cp313-win_amd64.whl", hash = "sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d"}, - {file = "fonttools-4.54.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ed2f80ca07025551636c555dec2b755dd005e2ea8fbeb99fc5cdff319b70b23b"}, - {file = "fonttools-4.54.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dc080e5a1c3b2656caff2ac2633d009b3a9ff7b5e93d0452f40cd76d3da3b3c"}, - {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d152d1be65652fc65e695e5619e0aa0982295a95a9b29b52b85775243c06556"}, - {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8583e563df41fdecef31b793b4dd3af8a9caa03397be648945ad32717a92885b"}, - {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d1d353ef198c422515a3e974a1e8d5b304cd54a4c2eebcae708e37cd9eeffb1"}, - {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fda582236fee135d4daeca056c8c88ec5f6f6d88a004a79b84a02547c8f57386"}, - {file = "fonttools-4.54.1-cp38-cp38-win32.whl", hash = "sha256:e7d82b9e56716ed32574ee106cabca80992e6bbdcf25a88d97d21f73a0aae664"}, - {file = "fonttools-4.54.1-cp38-cp38-win_amd64.whl", hash = "sha256:ada215fd079e23e060157aab12eba0d66704316547f334eee9ff26f8c0d7b8ab"}, - {file = "fonttools-4.54.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13"}, - {file = "fonttools-4.54.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac"}, - {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986"}, - {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55"}, - {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9"}, - {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33"}, - {file = "fonttools-4.54.1-cp39-cp39-win32.whl", hash = "sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a"}, - {file = "fonttools-4.54.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7"}, - {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, - {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, + {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61"}, + {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69"}, + {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f"}, + {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60"}, + {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6"}, + {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81"}, + {file = "fonttools-4.55.0-cp310-cp310-win32.whl", hash = "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880"}, + {file = "fonttools-4.55.0-cp310-cp310-win_amd64.whl", hash = "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b"}, + {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51"}, + {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189"}, + {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967"}, + {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6"}, + {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3"}, + {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c"}, + {file = "fonttools-4.55.0-cp311-cp311-win32.whl", hash = "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05"}, + {file = "fonttools-4.55.0-cp311-cp311-win_amd64.whl", hash = "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6"}, + {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7"}, + {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246"}, + {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a"}, + {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40"}, + {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d"}, + {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c"}, + {file = "fonttools-4.55.0-cp312-cp312-win32.whl", hash = "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6"}, + {file = "fonttools-4.55.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c"}, + {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9"}, + {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c"}, + {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c"}, + {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd"}, + {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4"}, + {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18"}, + {file = "fonttools-4.55.0-cp313-cp313-win32.whl", hash = "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b"}, + {file = "fonttools-4.55.0-cp313-cp313-win_amd64.whl", hash = "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998"}, + {file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:00f7cf55ad58a57ba421b6a40945b85ac7cc73094fb4949c41171d3619a3a47e"}, + {file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f27526042efd6f67bfb0cc2f1610fa20364396f8b1fc5edb9f45bb815fb090b2"}, + {file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e67974326af6a8879dc2a4ec63ab2910a1c1a9680ccd63e4a690950fceddbe"}, + {file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61dc0a13451143c5e987dec5254d9d428f3c2789a549a7cf4f815b63b310c1cc"}, + {file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e526b325a903868c62155a6a7e24df53f6ce4c5c3160214d8fe1be2c41b478"}, + {file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7ef9068a1297714e6fefe5932c33b058aa1d45a2b8be32a4c6dee602ae22b5c"}, + {file = "fonttools-4.55.0-cp38-cp38-win32.whl", hash = "sha256:55718e8071be35dff098976bc249fc243b58efa263768c611be17fe55975d40a"}, + {file = "fonttools-4.55.0-cp38-cp38-win_amd64.whl", hash = "sha256:553bd4f8cc327f310c20158e345e8174c8eed49937fb047a8bda51daf2c353c8"}, + {file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f901cef813f7c318b77d1c5c14cf7403bae5cb977cede023e22ba4316f0a8f6"}, + {file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c9679fc0dd7e8a5351d321d8d29a498255e69387590a86b596a45659a39eb0d"}, + {file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2820a8b632f3307ebb0bf57948511c2208e34a4939cf978333bc0a3f11f838"}, + {file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23bbbb49bec613a32ed1b43df0f2b172313cee690c2509f1af8fdedcf0a17438"}, + {file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a656652e1f5d55b9728937a7e7d509b73d23109cddd4e89ee4f49bde03b736c6"}, + {file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f50a1f455902208486fbca47ce33054208a4e437b38da49d6721ce2fef732fcf"}, + {file = "fonttools-4.55.0-cp39-cp39-win32.whl", hash = "sha256:161d1ac54c73d82a3cded44202d0218ab007fde8cf194a23d3dd83f7177a2f03"}, + {file = "fonttools-4.55.0-cp39-cp39-win_amd64.whl", hash = "sha256:ca7fd6987c68414fece41c96836e945e1f320cda56fc96ffdc16e54a44ec57a2"}, + {file = "fonttools-4.55.0-py3-none-any.whl", hash = "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f"}, + {file = "fonttools-4.55.0.tar.gz", hash = "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71"}, ] [package.extras] @@ -1740,88 +1742,103 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "frozenlist" -version = "1.4.1" +version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = true python-versions = ">=3.8" files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] [[package]] @@ -1993,70 +2010,70 @@ colorama = ">=0.4" [[package]] name = "grpcio" -version = "1.67.0" +version = "1.68.0" description = "HTTP/2-based RPC framework" optional = true python-versions = ">=3.8" files = [ - {file = "grpcio-1.67.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:bd79929b3bb96b54df1296cd3bf4d2b770bd1df6c2bdf549b49bab286b925cdc"}, - {file = "grpcio-1.67.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:16724ffc956ea42967f5758c2f043faef43cb7e48a51948ab593570570d1e68b"}, - {file = "grpcio-1.67.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:2b7183c80b602b0ad816315d66f2fb7887614ead950416d60913a9a71c12560d"}, - {file = "grpcio-1.67.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efe32b45dd6d118f5ea2e5deaed417d8a14976325c93812dd831908522b402c9"}, - {file = "grpcio-1.67.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe89295219b9c9e47780a0f1c75ca44211e706d1c598242249fe717af3385ec8"}, - {file = "grpcio-1.67.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa8d025fae1595a207b4e47c2e087cb88d47008494db258ac561c00877d4c8f8"}, - {file = "grpcio-1.67.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f95e15db43e75a534420e04822df91f645664bf4ad21dfaad7d51773c80e6bb4"}, - {file = "grpcio-1.67.0-cp310-cp310-win32.whl", hash = "sha256:a6b9a5c18863fd4b6624a42e2712103fb0f57799a3b29651c0e5b8119a519d65"}, - {file = "grpcio-1.67.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6eb68493a05d38b426604e1dc93bfc0137c4157f7ab4fac5771fd9a104bbaa6"}, - {file = "grpcio-1.67.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:e91d154689639932305b6ea6f45c6e46bb51ecc8ea77c10ef25aa77f75443ad4"}, - {file = "grpcio-1.67.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cb204a742997277da678611a809a8409657b1398aaeebf73b3d9563b7d154c13"}, - {file = "grpcio-1.67.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:ae6de510f670137e755eb2a74b04d1041e7210af2444103c8c95f193340d17ee"}, - {file = "grpcio-1.67.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74b900566bdf68241118f2918d312d3bf554b2ce0b12b90178091ea7d0a17b3d"}, - {file = "grpcio-1.67.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4e95e43447a02aa603abcc6b5e727d093d161a869c83b073f50b9390ecf0fa8"}, - {file = "grpcio-1.67.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bb94e66cd8f0baf29bd3184b6aa09aeb1a660f9ec3d85da615c5003154bc2bf"}, - {file = "grpcio-1.67.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:82e5bd4b67b17c8c597273663794a6a46a45e44165b960517fe6d8a2f7f16d23"}, - {file = "grpcio-1.67.0-cp311-cp311-win32.whl", hash = "sha256:7fc1d2b9fd549264ae585026b266ac2db53735510a207381be509c315b4af4e8"}, - {file = "grpcio-1.67.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac11ecb34a86b831239cc38245403a8de25037b448464f95c3315819e7519772"}, - {file = "grpcio-1.67.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:227316b5631260e0bef8a3ce04fa7db4cc81756fea1258b007950b6efc90c05d"}, - {file = "grpcio-1.67.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d90cfdafcf4b45a7a076e3e2a58e7bc3d59c698c4f6470b0bb13a4d869cf2273"}, - {file = "grpcio-1.67.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:77196216d5dd6f99af1c51e235af2dd339159f657280e65ce7e12c1a8feffd1d"}, - {file = "grpcio-1.67.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15c05a26a0f7047f720da41dc49406b395c1470eef44ff7e2c506a47ac2c0591"}, - {file = "grpcio-1.67.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3840994689cc8cbb73d60485c594424ad8adb56c71a30d8948d6453083624b52"}, - {file = "grpcio-1.67.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5a1e03c3102b6451028d5dc9f8591131d6ab3c8a0e023d94c28cb930ed4b5f81"}, - {file = "grpcio-1.67.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:682968427a63d898759474e3b3178d42546e878fdce034fd7474ef75143b64e3"}, - {file = "grpcio-1.67.0-cp312-cp312-win32.whl", hash = "sha256:d01793653248f49cf47e5695e0a79805b1d9d4eacef85b310118ba1dfcd1b955"}, - {file = "grpcio-1.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:985b2686f786f3e20326c4367eebdaed3e7aa65848260ff0c6644f817042cb15"}, - {file = "grpcio-1.67.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:8c9a35b8bc50db35ab8e3e02a4f2a35cfba46c8705c3911c34ce343bd777813a"}, - {file = "grpcio-1.67.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:42199e704095b62688998c2d84c89e59a26a7d5d32eed86d43dc90e7a3bd04aa"}, - {file = "grpcio-1.67.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:c4c425f440fb81f8d0237c07b9322fc0fb6ee2b29fbef5f62a322ff8fcce240d"}, - {file = "grpcio-1.67.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:323741b6699cd2b04a71cb38f502db98f90532e8a40cb675393d248126a268af"}, - {file = "grpcio-1.67.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:662c8e105c5e5cee0317d500eb186ed7a93229586e431c1bf0c9236c2407352c"}, - {file = "grpcio-1.67.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f6bd2ab135c64a4d1e9e44679a616c9bc944547357c830fafea5c3caa3de5153"}, - {file = "grpcio-1.67.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:2f55c1e0e2ae9bdd23b3c63459ee4c06d223b68aeb1961d83c48fb63dc29bc03"}, - {file = "grpcio-1.67.0-cp313-cp313-win32.whl", hash = "sha256:fd6bc27861e460fe28e94226e3673d46e294ca4673d46b224428d197c5935e69"}, - {file = "grpcio-1.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:cf51d28063338608cd8d3cd64677e922134837902b70ce00dad7f116e3998210"}, - {file = "grpcio-1.67.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:7f200aca719c1c5dc72ab68be3479b9dafccdf03df530d137632c534bb6f1ee3"}, - {file = "grpcio-1.67.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0892dd200ece4822d72dd0952f7112c542a487fc48fe77568deaaa399c1e717d"}, - {file = "grpcio-1.67.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:f4d613fbf868b2e2444f490d18af472ccb47660ea3df52f068c9c8801e1f3e85"}, - {file = "grpcio-1.67.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c69bf11894cad9da00047f46584d5758d6ebc9b5950c0dc96fec7e0bce5cde9"}, - {file = "grpcio-1.67.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9bca3ca0c5e74dea44bf57d27e15a3a3996ce7e5780d61b7c72386356d231db"}, - {file = "grpcio-1.67.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:014dfc020e28a0d9be7e93a91f85ff9f4a87158b7df9952fe23cc42d29d31e1e"}, - {file = "grpcio-1.67.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d4ea4509d42c6797539e9ec7496c15473177ce9abc89bc5c71e7abe50fc25737"}, - {file = "grpcio-1.67.0-cp38-cp38-win32.whl", hash = "sha256:9d75641a2fca9ae1ae86454fd25d4c298ea8cc195dbc962852234d54a07060ad"}, - {file = "grpcio-1.67.0-cp38-cp38-win_amd64.whl", hash = "sha256:cff8e54d6a463883cda2fab94d2062aad2f5edd7f06ae3ed030f2a74756db365"}, - {file = "grpcio-1.67.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:62492bd534979e6d7127b8a6b29093161a742dee3875873e01964049d5250a74"}, - {file = "grpcio-1.67.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eef1dce9d1a46119fd09f9a992cf6ab9d9178b696382439446ca5f399d7b96fe"}, - {file = "grpcio-1.67.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:f623c57a5321461c84498a99dddf9d13dac0e40ee056d884d6ec4ebcab647a78"}, - {file = "grpcio-1.67.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54d16383044e681f8beb50f905249e4e7261dd169d4aaf6e52eab67b01cbbbe2"}, - {file = "grpcio-1.67.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2a44e572fb762c668e4812156b81835f7aba8a721b027e2d4bb29fb50ff4d33"}, - {file = "grpcio-1.67.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:391df8b0faac84d42f5b8dfc65f5152c48ed914e13c522fd05f2aca211f8bfad"}, - {file = "grpcio-1.67.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfd9306511fdfc623a1ba1dc3bc07fbd24e6cfbe3c28b4d1e05177baa2f99617"}, - {file = "grpcio-1.67.0-cp39-cp39-win32.whl", hash = "sha256:30d47dbacfd20cbd0c8be9bfa52fdb833b395d4ec32fe5cff7220afc05d08571"}, - {file = "grpcio-1.67.0-cp39-cp39-win_amd64.whl", hash = "sha256:f55f077685f61f0fbd06ea355142b71e47e4a26d2d678b3ba27248abfe67163a"}, - {file = "grpcio-1.67.0.tar.gz", hash = "sha256:e090b2553e0da1c875449c8e75073dd4415dd71c9bde6a406240fdf4c0ee467c"}, + {file = "grpcio-1.68.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:619b5d0f29f4f5351440e9343224c3e19912c21aeda44e0c49d0d147a8d01544"}, + {file = "grpcio-1.68.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:a59f5822f9459bed098ffbceb2713abbf7c6fd13f2b9243461da5c338d0cd6c3"}, + {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:c03d89df516128febc5a7e760d675b478ba25802447624edf7aa13b1e7b11e2a"}, + {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44bcbebb24363d587472089b89e2ea0ab2e2b4df0e4856ba4c0b087c82412121"}, + {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79f81b7fbfb136247b70465bd836fa1733043fdee539cd6031cb499e9608a110"}, + {file = "grpcio-1.68.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:88fb2925789cfe6daa20900260ef0a1d0a61283dfb2d2fffe6194396a354c618"}, + {file = "grpcio-1.68.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:99f06232b5c9138593ae6f2e355054318717d32a9c09cdc5a2885540835067a1"}, + {file = "grpcio-1.68.0-cp310-cp310-win32.whl", hash = "sha256:a6213d2f7a22c3c30a479fb5e249b6b7e648e17f364598ff64d08a5136fe488b"}, + {file = "grpcio-1.68.0-cp310-cp310-win_amd64.whl", hash = "sha256:15327ab81131ef9b94cb9f45b5bd98803a179c7c61205c8c0ac9aff9d6c4e82a"}, + {file = "grpcio-1.68.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:3b2b559beb2d433129441783e5f42e3be40a9e1a89ec906efabf26591c5cd415"}, + {file = "grpcio-1.68.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e46541de8425a4d6829ac6c5d9b16c03c292105fe9ebf78cb1c31e8d242f9155"}, + {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c1245651f3c9ea92a2db4f95d37b7597db6b246d5892bca6ee8c0e90d76fb73c"}, + {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1931c7aa85be0fa6cea6af388e576f3bf6baee9e5d481c586980c774debcb4"}, + {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ff09c81e3aded7a183bc6473639b46b6caa9c1901d6f5e2cba24b95e59e30"}, + {file = "grpcio-1.68.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8c73f9fbbaee1a132487e31585aa83987ddf626426d703ebcb9a528cf231c9b1"}, + {file = "grpcio-1.68.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b2f98165ea2790ea159393a2246b56f580d24d7da0d0342c18a085299c40a75"}, + {file = "grpcio-1.68.0-cp311-cp311-win32.whl", hash = "sha256:e1e7ed311afb351ff0d0e583a66fcb39675be112d61e7cfd6c8269884a98afbc"}, + {file = "grpcio-1.68.0-cp311-cp311-win_amd64.whl", hash = "sha256:e0d2f68eaa0a755edd9a47d40e50dba6df2bceda66960dee1218da81a2834d27"}, + {file = "grpcio-1.68.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8af6137cc4ae8e421690d276e7627cfc726d4293f6607acf9ea7260bd8fc3d7d"}, + {file = "grpcio-1.68.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4028b8e9a3bff6f377698587d642e24bd221810c06579a18420a17688e421af7"}, + {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:f60fa2adf281fd73ae3a50677572521edca34ba373a45b457b5ebe87c2d01e1d"}, + {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e18589e747c1e70b60fab6767ff99b2d0c359ea1db8a2cb524477f93cdbedf5b"}, + {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d30f3fee9372796f54d3100b31ee70972eaadcc87314be369360248a3dcffe"}, + {file = "grpcio-1.68.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7e0a3e72c0e9a1acab77bef14a73a416630b7fd2cbd893c0a873edc47c42c8cd"}, + {file = "grpcio-1.68.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a831dcc343440969aaa812004685ed322cdb526cd197112d0db303b0da1e8659"}, + {file = "grpcio-1.68.0-cp312-cp312-win32.whl", hash = "sha256:5a180328e92b9a0050958ced34dddcb86fec5a8b332f5a229e353dafc16cd332"}, + {file = "grpcio-1.68.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bddd04a790b69f7a7385f6a112f46ea0b34c4746f361ebafe9ca0be567c78e9"}, + {file = "grpcio-1.68.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:fc05759ffbd7875e0ff2bd877be1438dfe97c9312bbc558c8284a9afa1d0f40e"}, + {file = "grpcio-1.68.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:15fa1fe25d365a13bc6d52fcac0e3ee1f9baebdde2c9b3b2425f8a4979fccea1"}, + {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:32a9cb4686eb2e89d97022ecb9e1606d132f85c444354c17a7dbde4a455e4a3b"}, + {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dba037ff8d284c8e7ea9a510c8ae0f5b016004f13c3648f72411c464b67ff2fb"}, + {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0efbbd849867e0e569af09e165363ade75cf84f5229b2698d53cf22c7a4f9e21"}, + {file = "grpcio-1.68.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:4e300e6978df0b65cc2d100c54e097c10dfc7018b9bd890bbbf08022d47f766d"}, + {file = "grpcio-1.68.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:6f9c7ad1a23e1047f827385f4713b5b8c6c7d325705be1dd3e31fb00dcb2f665"}, + {file = "grpcio-1.68.0-cp313-cp313-win32.whl", hash = "sha256:3ac7f10850fd0487fcce169c3c55509101c3bde2a3b454869639df2176b60a03"}, + {file = "grpcio-1.68.0-cp313-cp313-win_amd64.whl", hash = "sha256:afbf45a62ba85a720491bfe9b2642f8761ff348006f5ef67e4622621f116b04a"}, + {file = "grpcio-1.68.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:f8f695d9576ce836eab27ba7401c60acaf9ef6cf2f70dfe5462055ba3df02cc3"}, + {file = "grpcio-1.68.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9fe1b141cda52f2ca73e17d2d3c6a9f3f3a0c255c216b50ce616e9dca7e3441d"}, + {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:4df81d78fd1646bf94ced4fb4cd0a7fe2e91608089c522ef17bc7db26e64effd"}, + {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46a2d74d4dd8993151c6cd585594c082abe74112c8e4175ddda4106f2ceb022f"}, + {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17278d977746472698460c63abf333e1d806bd41f2224f90dbe9460101c9796"}, + {file = "grpcio-1.68.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:15377bce516b1c861c35e18eaa1c280692bf563264836cece693c0f169b48829"}, + {file = "grpcio-1.68.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc5f0a4f5904b8c25729a0498886b797feb817d1fd3812554ffa39551112c161"}, + {file = "grpcio-1.68.0-cp38-cp38-win32.whl", hash = "sha256:def1a60a111d24376e4b753db39705adbe9483ef4ca4761f825639d884d5da78"}, + {file = "grpcio-1.68.0-cp38-cp38-win_amd64.whl", hash = "sha256:55d3b52fd41ec5772a953612db4e70ae741a6d6ed640c4c89a64f017a1ac02b5"}, + {file = "grpcio-1.68.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:0d230852ba97654453d290e98d6aa61cb48fa5fafb474fb4c4298d8721809354"}, + {file = "grpcio-1.68.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:50992f214264e207e07222703c17d9cfdcc2c46ed5a1ea86843d440148ebbe10"}, + {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:14331e5c27ed3545360464a139ed279aa09db088f6e9502e95ad4bfa852bb116"}, + {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f84890b205692ea813653ece4ac9afa2139eae136e419231b0eec7c39fdbe4c2"}, + {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cf343c6f4f6aa44863e13ec9ddfe299e0be68f87d68e777328bff785897b05"}, + {file = "grpcio-1.68.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fd2c2d47969daa0e27eadaf15c13b5e92605c5e5953d23c06d0b5239a2f176d3"}, + {file = "grpcio-1.68.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:18668e36e7f4045820f069997834e94e8275910b1f03e078a6020bd464cb2363"}, + {file = "grpcio-1.68.0-cp39-cp39-win32.whl", hash = "sha256:2af76ab7c427aaa26aa9187c3e3c42f38d3771f91a20f99657d992afada2294a"}, + {file = "grpcio-1.68.0-cp39-cp39-win_amd64.whl", hash = "sha256:e694b5928b7b33ca2d3b4d5f9bf8b5888906f181daff6b406f4938f3a997a490"}, + {file = "grpcio-1.68.0.tar.gz", hash = "sha256:7e7483d39b4a4fddb9906671e9ea21aaad4f031cdfc349fec76bdfa1e404543a"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.67.0)"] +protobuf = ["grpcio-tools (>=1.68.0)"] [[package]] name = "h11" @@ -2097,13 +2114,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.6" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = true python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, - {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] @@ -2363,13 +2380,13 @@ files = [ [[package]] name = "ipfabric" -version = "6.10.0" +version = "6.10.4" description = "Python package for interacting with IP Fabric" optional = true python-versions = "<4.0,>=3.8" files = [ - {file = "ipfabric-6.10.0-py3-none-any.whl", hash = "sha256:5f419f1fbe1c9fa939c15969ae8a8e541d3c4e18f2588bc81ee3ba028af21c7a"}, - {file = "ipfabric-6.10.0.tar.gz", hash = "sha256:02b3b47fd10aada88891d7fe56b56782508778daecad34d37f75e3828e3faa07"}, + {file = "ipfabric-6.10.4-py3-none-any.whl", hash = "sha256:c948cd8c5554c6524afa6bb20a320759a14437169c5f736951d790e033f6175b"}, + {file = "ipfabric-6.10.4.tar.gz", hash = "sha256:b19cfa7bc327e005ec8cd8100dca1b0c0971681433103f70c5d5def50816d16b"}, ] [package.dependencies] @@ -3379,13 +3396,13 @@ testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4, [[package]] name = "nautobot" -version = "2.3.8" +version = "2.3.11" description = "Source of truth and network automation platform." optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "nautobot-2.3.8-py3-none-any.whl", hash = "sha256:1624480705158ba29fb3c9e0dc3d80e4809115b9a8584a90a787f3e7b37b484c"}, - {file = "nautobot-2.3.8.tar.gz", hash = "sha256:25319ccec7f99478b506d899159e02f02c8dcf61cd14c68401787a6e1e301dd1"}, + {file = "nautobot-2.3.11-py3-none-any.whl", hash = "sha256:f6ab9599a927904f5e956b1dc80c80dc53bdd44222c383bc073dd81f0b3bf727"}, + {file = "nautobot-2.3.11.tar.gz", hash = "sha256:d65e5b10612bfd7092469ddc76a19e5fe9f1eb37157ae09bba71b132bbc240e1"}, ] [package.dependencies] @@ -3428,7 +3445,7 @@ nh3 = ">=0.2.15,<0.3.0" packaging = ">=23.1" Pillow = ">=10.3.0,<10.4.0" prometheus-client = ">=0.20.0,<0.21.0" -psycopg2-binary = ">=2.9.9,<2.10.0" +psycopg2-binary = ">=2.9.10,<2.10.0" python-slugify = ">=8.0.3,<8.1.0" pyuwsgi = ">=2.0.26,<2.1.0" PyYAML = ">=6.0.2,<6.1.0" @@ -3436,9 +3453,9 @@ social-auth-app-django = ">=5.4.2,<5.5.0" svgwrite = ">=1.4.2,<1.5.0" [package.extras] -all = ["django-auth-ldap (>=4.8.0,<4.9.0)", "django-storages (==1.14.3)", "mysqlclient (>=2.2.3,<2.3.0)", "napalm (>=4.1.0,<6.0.0)", "social-auth-core[saml] (>=4.5.3,<4.6.0)"] +all = ["django-auth-ldap (>=4.8.0,<4.9.0)", "django-storages (==1.14.3)", "mysqlclient (>=2.2.5,<2.3.0)", "napalm (>=4.1.0,<6.0.0)", "social-auth-core[saml] (>=4.5.3,<4.6.0)"] ldap = ["django-auth-ldap (>=4.8.0,<4.9.0)"] -mysql = ["mysqlclient (>=2.2.3,<2.3.0)"] +mysql = ["mysqlclient (>=2.2.5,<2.3.0)"] napalm = ["napalm (>=4.1.0,<6.0.0)"] remote-storage = ["django-storages (==1.14.3)"] sso = ["social-auth-core[saml] (>=4.5.3,<4.6.0)"] @@ -4272,20 +4289,21 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-extra-types" -version = "2.9.0" +version = "2.10.0" description = "Extra Pydantic types." optional = true python-versions = ">=3.8" files = [ - {file = "pydantic_extra_types-2.9.0-py3-none-any.whl", hash = "sha256:f0bb975508572ba7bf3390b7337807588463b7248587e69f43b1ad7c797530d0"}, - {file = "pydantic_extra_types-2.9.0.tar.gz", hash = "sha256:e061c01636188743bb69f368dcd391f327b8cfbfede2fe1cbb1211b06601ba3b"}, + {file = "pydantic_extra_types-2.10.0-py3-none-any.whl", hash = "sha256:b19943914e6286548254f5079d1da094e9c0583ee91a8e611e9df24bfd07dbcd"}, + {file = "pydantic_extra_types-2.10.0.tar.gz", hash = "sha256:552c47dd18fe1d00cfed75d9981162a2f3203cf7e77e55a3d3e70936f59587b9"}, ] [package.dependencies] pydantic = ">=2.5.2" +typing-extensions = "*" [package.extras] -all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<3)", "pytz (>=2024.1)", "semver (>=3.0.2)", "tzdata (>=2024.1)"] +all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<4)", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)"] pendulum = ["pendulum (>=3.0.0,<4.0.0)"] phonenumbers = ["phonenumbers (>=8,<9)"] pycountry = ["pycountry (>=23)"] @@ -4294,13 +4312,13 @@ semver = ["semver (>=3.0.2)"] [[package]] name = "pydantic-settings" -version = "2.6.0" +version = "2.6.1" description = "Settings management using Pydantic" optional = true python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"}, - {file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"}, + {file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"}, + {file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"}, ] [package.dependencies] @@ -4423,13 +4441,13 @@ pylint = ">=1.7" [[package]] name = "pymdown-extensions" -version = "10.11.2" +version = "10.12" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.11.2-py3-none-any.whl", hash = "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf"}, - {file = "pymdown_extensions-10.11.2.tar.gz", hash = "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049"}, + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, ] [package.dependencies] @@ -4584,58 +4602,66 @@ files = [ [[package]] name = "pyuwsgi" -version = "2.0.26" +version = "2.0.28.post1" description = "The uWSGI server" optional = false python-versions = "*" files = [ - {file = "pyuwsgi-2.0.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe6149521f6545548452ae66d670be7ece962045952d07af7fdd156409771d4"}, - {file = "pyuwsgi-2.0.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ce17f4a114e0ca53686748e7a4556e62c7a0edc8a6033e076eb3bc4db5489f"}, - {file = "pyuwsgi-2.0.26-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:244c81e050e240ebc8a1c455db8aefc6b5c9f4582551b34905092da0e03415a3"}, - {file = "pyuwsgi-2.0.26-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596c55b36b72fbb04b6e024578e8f9867185b6b07f50fcead75e71b6534154e7"}, - {file = "pyuwsgi-2.0.26-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a9e82434e640eb5c7ce0845c3cfd6711088cff3b0265d98e6a34216972eec07"}, - {file = "pyuwsgi-2.0.26-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df91af8b7dfd573683ccecfa646d447e4a23be7dc84329c0633e70faa9431ba8"}, - {file = "pyuwsgi-2.0.26-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ffa63939802f29873a8b92e437fd9e17fccde0a80004260a2abe8ad71b42bd1d"}, - {file = "pyuwsgi-2.0.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:910642f91877e8dac6d0286c79688543d0d573e43e78d030d998faee8ca49bdb"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b20664df152f225d56f6ba810347d47efa126d9ce751e6e5fbad07a12d7cdae2"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:35aeca4b60e883796f0c2fa4b78fa34e5f31947b317b7d63526aa68a31036467"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f95c39b8713121536e5f0c4292eab30adcb8dabd088641fd34b5641d4fd81d"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85f8a3b09b41177496e5ed84aa9e0e3f815ac53422d37f72eec7933609f742e9"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9b9bed61cf18002ed7e9cef81c42e6b825c01b7c8d983f2cff223f905987e64"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0bc7cb23a35b7da5ead97b2e9ea2894a74c96d9864782789526e66fdfcf5f91"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb69885e3f02a7538137197a8618766cda7e29bd7da005fce699e6385215a786"}, - {file = "pyuwsgi-2.0.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c7bb17f907f7e165d45b676939c9641dbf4d7c8c532caaa704006c6be69c3b60"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:01aac38118cd35adb141a7ddbd721845c3b895a18d6cfddca3a237e0da5e5fc7"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ead0863e90397be562ca9816b2e0704f8e59e734ab158eb467db333814af704e"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf4e2828ca8c213f91673ff960a29ebbc037d743e57b2758ea5a76dd13c0b01c"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d48accc82c82d637be27ebb153c17a2773758df582602d7f8c1702e9dcf8ea3a"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c87bd20906bceebf0bfb3fc4f25b12ca3943aafeaf0dc289df9144d4ee41f9c5"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5f14b676f8aacd79a2106695657c212b4469cd98f1624dc0473e1e0e695bcc9"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70aec45ba640742df86e0f1a0baa01964b162d11f24ee70d56146bd6331462f9"}, - {file = "pyuwsgi-2.0.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c8ded279e1c4da7273f25d658979071520ae97712650f70ea50715618cb51910"}, - {file = "pyuwsgi-2.0.26-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:288f34589e7d76e70f4ebaccfeb34b76cefa661f41302b38722f305a22310e1f"}, - {file = "pyuwsgi-2.0.26-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:524c5620aff07c72f9ef6526712f92a06ff9741c0c3ea7b46284045de1b8db40"}, - {file = "pyuwsgi-2.0.26-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9f5f0c377d9efc04d12937e79567628c4c1f72c0991f45c6dbb76541ca1b683"}, - {file = "pyuwsgi-2.0.26-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db26c9ca688fb19e1716c7ab8e09811a9bd431576432ca626d89e9ebbef96fc1"}, - {file = "pyuwsgi-2.0.26-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:489ff6e77fcbe14ca7a853916388ddb4bd4e087dd243abef0b7a4732563401ba"}, - {file = "pyuwsgi-2.0.26-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3d07829faf43b5981487130a929eb8521aefdf39dd723c50e2e168362dafafdc"}, - {file = "pyuwsgi-2.0.26-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d9e21e147f851a77ab893a27466a166a52450558c88ce885974c2e63f6e3c298"}, - {file = "pyuwsgi-2.0.26-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7cfa8b9524bccca9052c6fd2682ec2fc744a9397eaf5febde8e60334fac4313a"}, - {file = "pyuwsgi-2.0.26-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a13acc603cca50510dd2b0772f398bad1bcfaa5c418069becba016edac22ac4"}, - {file = "pyuwsgi-2.0.26-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:544e2f791912dc5f38cb1159eae8ba037cdd472b40e2fcfc0ea7fb973aaebaaa"}, - {file = "pyuwsgi-2.0.26-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c807ae36058dd7ae2653451081536c10886c74fc06aa1bf7a28cc2f0c815307"}, - {file = "pyuwsgi-2.0.26-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:af04d0ff93a89c65369c8b0b10d07394046e7d78694fad998b316aa7f8ca3e1d"}, - {file = "pyuwsgi-2.0.26-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:fb1ba584cd07339f2adad5eb33ab51854330748426a63fbd58cb39031ffb7498"}, - {file = "pyuwsgi-2.0.26-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:709d3704dcb9e85576a31a1c04782fe17df6a577a6eaf07dd2b7c34982ca905f"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a81548e59852276af18d4404512cfcbaf20c40f38f345bb80b1a87f1dc7285c"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bb840399ff19b3e2308e33b2cd37143bc55b1666e99b38370660b95127b081aa"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115cf76fe6358a7863e4f28d9dfd2fa51f1f198a7259ee5fec2e7cb52d0060bf"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fd517e4336ebae0493063a031da0e6a53ff436dc5cd551b57f387d38459085c"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14769dd0c41bc531dba7fc8fe202a11d984d93f251896ffcc74daa0476eda966"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:312575bcc9513dfd7d76d993b7753d18b9a36e54cffca6b72ad2e1397636c7fd"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3d839d584276784f660168b89209c28ce9a1cb14f8911322f21d919c2566e49d"}, - {file = "pyuwsgi-2.0.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ea8e3e197f54902b34c5648880bc3c111ad4f677a7350203d1052606cc04119b"}, - {file = "pyuwsgi-2.0.26.tar.gz", hash = "sha256:c7f167545939764a1c6fcd0f861023f641ca09f9806f1f4b7e48b9ea2682db8e"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:611e6585a51b3a1f9619e1069dcdc1b8bf37ad7aa16b271fce2ca3e1440fc548"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c2caedcc6fd0cd217b65ab863a51e18032b3ce81316d0a079652ed43ed8ba68"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9382286b9cf94b5d826388cda7097235b0f1348c7549c8b71100ecfc8d74c58"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30798db4c0c9b36a12fd831cfe621c69569e226d177b3c28c6a191e2a819604f"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a4fbc27ccbe24cffb65ce89fbb7cf2e8a0af625b7706179786a810688cefd7f"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1289847ba14ae2ba4f918c57e9d257ecd82a43f6c7a026e155577596c6304f1"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:91bdc9fcd1e13088ef57f7e58e427c9539e9d2c6d75157f55b5d17ef599e61ee"}, + {file = "pyuwsgi-2.0.28.post1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1b0a27f1b9c63a1ac9788a068e5905b8ebedb1b460b9256ac85d1318fd6a9a6"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36d9c628067fc1c58534b800aacbb7499813b214cabe9128fb5ba79ad32ff9b9"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:219032fe8bb8306cb05e7bc509ff134c853cfcbd7d809a867d5b8ecd589bef5d"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1573ac212201ccbfea00b93d88fde89205d3c2f0a4d0c973058aa0b8745d4a2d"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6105bd14fa2e61505dc116574ba79f3f6e0f425f206d3bec2337463457167aba"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44bfebc678b2dde10382197b9199ce546b699a672e05e139a3827efb121e704f"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e4e4775246f2ab079ea4bcf69d70441ffe81eba82a88eb4da6ae9debf334511f"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e227836984735fdfa26d3be4927a6ae060ad8a0d28ef4a6adcd4f47fbfd7e876"}, + {file = "pyuwsgi-2.0.28.post1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf4f03d8b74d9754efd8cc6b0566258ccc1123a8c2fe49a11835242a6fb27efc"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9390e8a2297186ede814d5863a06fa98b91295c813fdd3d08fe1357793476486"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06a0294910de374ff43032b41333f7f9b62d59308b7f3eea29bc64d78fb93613"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64b40503a87621e79efe4306b77595a0cbcb69afa4f3428a85e4e8ac46068d88"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1157f49bfb360c1883feec82553adbed1e4a447e5ced66f36525a92f0e46397e"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941d44a67760dc173c0f8318f8b7bd1ef7927533d6efb4641b2ea9e934f09981"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a90b72a25ba1a34299f4958237a12f18154b373a9a0a93a6267af5e8798c1ee"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c29fdf5baaff9d717aedb63e7b78b90bec561afc099f952db44699adcb0c575"}, + {file = "pyuwsgi-2.0.28.post1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e8fd6f2116d2afce52965c940a96a939e9d9c7409f3e19ba445a25e33779f08"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd67086df87921bc8837ef018cd7cf02834136f4735811375c1e17b776b40"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f27ee3445eab37fabbe3afc6af68220c8f0bd28c5228d6a2ec7886d080614ad4"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdd7ec17f795049ef346dc10efbfce6c1a9500f3f39262c86107c70b9a83cb9"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4563fa76d64a2c309eb902511a1fcdce66865a03785c61494db5c53136ffa7a"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32838668eab84ac6847876d9da72768552fca556e09a1fe3a63facb976bd12ad"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9feafdfa73a632767a0cb1bafd285e36fba925b9c9ddd2b3311f2963c917c9f"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:20d23ddbf28a831543d586c9e5727c9c28d7be7bd7cb853c7db2e0c529f605bf"}, + {file = "pyuwsgi-2.0.28.post1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c0b1b09bdf836a6cacf35e2bb06f6ad4b7dff6c6ad48895e1b1c0b0d19bbbfcc"}, + {file = "pyuwsgi-2.0.28.post1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:735861b77e607e133f9905a5e0ba6505ef6788df1cfdb6af06e6dfef07dfa9af"}, + {file = "pyuwsgi-2.0.28.post1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dbe7d0bb8184ef6c9bec1a3593a02bd27de0348807c97533069a7bca2603d19"}, + {file = "pyuwsgi-2.0.28.post1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4fcf8a76ad40af8c3c1a1e98f11be03e999e4957f48da6a180f5b8fc40b963c4"}, + {file = "pyuwsgi-2.0.28.post1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c969668db4599b6a5c927ad4bc1d698ea8c57fb10a943b51402fe80a97cdfc9"}, + {file = "pyuwsgi-2.0.28.post1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:5b6bec34ca5b200dfc8b10c74b0a9e3da051747f5faa300f681bbb46cf573fd0"}, + {file = "pyuwsgi-2.0.28.post1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:8dc521dd980ee431d4e6fc9a018ad1df4840c4551f281a598878083f3d155243"}, + {file = "pyuwsgi-2.0.28.post1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:1e6afc8916098ca35119dc0c619f29ef572f72c6f4425f29604c17397ae313b2"}, + {file = "pyuwsgi-2.0.28.post1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2f118d7211e4afb7e8248cd380c1eac3e87604c1143bbccac1679c3a3642e22"}, + {file = "pyuwsgi-2.0.28.post1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:327800fa58bfba5d6f2bd19331e812e0e1250aaa681874f5b92998f68e6bab75"}, + {file = "pyuwsgi-2.0.28.post1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b351e76c9a18dc3e8a635994a98571b9ca7f10e371a0085ae3c8cd95f3db66cd"}, + {file = "pyuwsgi-2.0.28.post1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c6f658625f2470d905276987aec898a690eee33f49c9c3961ca9d912abd046"}, + {file = "pyuwsgi-2.0.28.post1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9436ac4450730e1bcca4591ebb781eabcfcf93462114e199b951118032144323"}, + {file = "pyuwsgi-2.0.28.post1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:0b1c131288b6b473e39c5b02e63c34b64422665749dcadee41ef98d64b5db5d0"}, + {file = "pyuwsgi-2.0.28.post1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a8a932c9d34b2f0007eabf04728ffae7be8bfb87a044daee50a82b6796ad5b34"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f55071012ce5d728ac428be78c3710e986acc02dcfc1d72f3a76541b85e25cfb"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fa60c4fb1e483c079d1f3767863bc41884f6322f0744ac5d6398b0e505e9d21e"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad20990917364f8f9b3fc62424edc6d9a992146bc5e15d37b1bbc9c3934ddf9b"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:167d05f635c46287ee8709b09f422fbc16310acad3c9bd100a267679c061fe62"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2300dba7037089a23b0eb4d2b037391be4d89b73e597671d0f23903a529cd552"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f61adc1ab15da41675af36256feccd0cafb797c703499930fd5b3b381b0b6273"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dc85ba45af7a7b89e3a722cf9cbba724fabb2c3b5caf3c25d67286ab329df97c"}, + {file = "pyuwsgi-2.0.28.post1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:81e59789af3b568ed1c4484b4bac9e4b79810f4774d8d3ab9056eb3f500e1e94"}, + {file = "pyuwsgi-2.0.28.post1.tar.gz", hash = "sha256:3b85217fd489d623512066ffed0cfc4c95bd3321655e9c6ea13cf6c7f064c9b0"}, ] [[package]] @@ -4716,13 +4742,13 @@ pyyaml = "*" [[package]] name = "redis" -version = "5.1.1" +version = "5.2.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" files = [ - {file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"}, - {file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"}, + {file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"}, + {file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"}, ] [package.dependencies] @@ -4749,105 +4775,105 @@ rpds-py = ">=0.7.0" [[package]] name = "regex" -version = "2024.9.11" +version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, - {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, - {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, - {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, - {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, - {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, - {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, - {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, - {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, - {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, - {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, - {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, - {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, - {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, - {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, - {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] [[package]] @@ -4957,114 +4983,114 @@ py = ">=1.4.26,<2.0.0" [[package]] name = "rpds-py" -version = "0.20.0" +version = "0.20.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, - {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, - {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, - {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, - {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, - {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, - {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, - {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, - {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, - {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, - {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, - {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, - {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, - {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, - {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, + {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"}, + {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899"}, + {file = "rpds_py-0.20.1-cp310-none-win32.whl", hash = "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff"}, + {file = "rpds_py-0.20.1-cp310-none-win_amd64.whl", hash = "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711"}, + {file = "rpds_py-0.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75"}, + {file = "rpds_py-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3"}, + {file = "rpds_py-0.20.1-cp311-none-win32.whl", hash = "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732"}, + {file = "rpds_py-0.20.1-cp311-none-win_amd64.whl", hash = "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84"}, + {file = "rpds_py-0.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17"}, + {file = "rpds_py-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5"}, + {file = "rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c"}, + {file = "rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb"}, + {file = "rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e"}, + {file = "rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb"}, + {file = "rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782"}, + {file = "rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e"}, + {file = "rpds_py-0.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191"}, + {file = "rpds_py-0.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc"}, + {file = "rpds_py-0.20.1-cp38-none-win32.whl", hash = "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1"}, + {file = "rpds_py-0.20.1-cp38-none-win_amd64.whl", hash = "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425"}, + {file = "rpds_py-0.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad"}, + {file = "rpds_py-0.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf"}, + {file = "rpds_py-0.20.1-cp39-none-win32.whl", hash = "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca"}, + {file = "rpds_py-0.20.1-cp39-none-win_amd64.whl", hash = "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a"}, + {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, ] [[package]] @@ -5106,23 +5132,23 @@ files = [ [[package]] name = "setuptools" -version = "75.2.0" +version = "75.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"}, - {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"}, + {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, + {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] [[package]] name = "singledispatch" @@ -5150,6 +5176,23 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "slurpit-sdk" +version = "0.9.58" +description = "A robust Python SDK for slurpit" +optional = true +python-versions = "<3.13,>=3.8" +files = [ + {file = "slurpit_sdk-0.9.58-py3-none-any.whl", hash = "sha256:391f2aaaee5cf65d83ea44db49a3f5b404cb01773d5a62364c72f0658944ecf3"}, + {file = "slurpit_sdk-0.9.58.tar.gz", hash = "sha256:1e5c367c44c198c4fb9ee4e53fce5b3252686ae7e924a1b19105d4e7af48fc50"}, +] + +[package.dependencies] +httpx = ">=0.27.0,<0.28.0" + +[package.extras] +pandas = ["pandas (>=2.2.2,<3.0.0)"] + [[package]] name = "smmap" version = "5.0.1" @@ -5350,13 +5393,13 @@ test = ["pytest"] [[package]] name = "sqlparse" -version = "0.5.1" +version = "0.5.2" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, - {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, + {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, + {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, ] [package.extras] @@ -5384,20 +5427,20 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "structlog" -version = "22.3.0" +version = "24.4.0" description = "Structured Logging for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "structlog-22.3.0-py3-none-any.whl", hash = "sha256:b403f344f902b220648fa9f286a23c0cc5439a5844d271fec40562dbadbc70ad"}, - {file = "structlog-22.3.0.tar.gz", hash = "sha256:e7509391f215e4afb88b1b80fa3ea074be57a5a17d794bd436a5c949da023333"}, + {file = "structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610"}, + {file = "structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4"}, ] [package.extras] -dev = ["structlog[docs,tests,typing]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] -tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] -typing = ["mypy", "rich", "twisted"] +dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] [[package]] name = "svgwrite" @@ -5448,13 +5491,13 @@ files = [ [[package]] name = "tomli" -version = "2.0.2" +version = "2.1.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, ] [[package]] @@ -5626,13 +5669,13 @@ files = [ [[package]] name = "wheel" -version = "0.44.0" +version = "0.45.0" description = "A built-package format for Python" optional = false python-versions = ">=3.8" files = [ - {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"}, - {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"}, + {file = "wheel-0.45.0-py3-none-any.whl", hash = "sha256:52f0baa5e6522155090a09c6bd95718cc46956d1b51d537ea5454249edb671c7"}, + {file = "wheel-0.45.0.tar.gz", hash = "sha256:a57353941a3183b3d5365346b567a260a0602a0f8a635926a7dede41b94c674a"}, ] [package.extras] @@ -5868,8 +5911,10 @@ type = ["pytest-mypy"] [extras] aci = ["PyYAML"] -all = ["Jinja2", "PyYAML", "cloudvision", "cvprac", "dnacentersdk", "dnspython", "ijson", "ipfabric", "meraki", "nautobot-device-lifecycle-mgmt", "netutils", "oauthlib", "python-magic", "pytz", "requests", "requests-oauthlib", "six"] +all = ["Jinja2", "PyYAML", "cloudvision", "cvprac", "dnacentersdk", "dnspython", "ijson", "ipfabric", "meraki", "nautobot-device-lifecycle-mgmt", "netutils", "oauthlib", "python-magic", "pytz", "requests", "requests-oauthlib", "six", "slurpit-sdk", "urllib3"] aristacv = ["cloudvision", "cvprac"] +bootstrap = ["pytz"] +citrix-adm = ["netutils", "requests", "urllib3"] device42 = ["requests"] dna-center = ["dnacentersdk", "netutils"] infoblox = ["dnspython"] @@ -5878,8 +5923,9 @@ meraki = ["meraki"] nautobot-device-lifecycle-mgmt = ["nautobot-device-lifecycle-mgmt"] pysnow = ["ijson", "oauthlib", "python-magic", "pytz", "requests", "requests-oauthlib", "six"] servicenow = ["Jinja2", "PyYAML", "ijson", "oauthlib", "python-magic", "pytz", "requests", "requests-oauthlib", "six"] +slurpit = ["slurpit-sdk"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "f2319b7b5319197b7b329fa8325e1fdda18528d29a346e625b307fef97622706" +content-hash = "686a1e20a00ecbef1ac2e16c1ef9e295a3cc164ef0eabb5ee1507ad01c10b27c" diff --git a/pyproject.toml b/pyproject.toml index 36e087a96..63ff2d397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautobot-ssot" -version = "3.2.0" +version = "3.3.0" description = "Nautobot Single Source of Truth" authors = ["Network to Code, LLC "] license = "Apache-2.0" @@ -49,6 +49,7 @@ python-magic = { version = ">=0.4.15", optional = true } pytz = { version = ">=2019.3", optional = true } requests = { version = ">=2.21.0", optional = true } requests-oauthlib = { version = ">=1.3.0", optional = true } +urllib3 = { version = ">=2.2.3", optional = true } six = { version = ">=1.13.0", optional = true } httpx = { version = ">=0.23.3", optional = true } # Used by the Itential SSoT as a retry mechanism for HTTP failures in the AutomationGatewayClient. @@ -56,6 +57,7 @@ retry = "^0.9.2" # used for DNA Center integration dnacentersdk = { version = "^2.5.6", optional = true } meraki = { version = "^1.37.2,<1.46.0", optional = true } +slurpit-sdk = { version = "^0.9.58", optional = true } [tool.poetry.group.dev.dependencies] coverage = "*" @@ -118,13 +120,23 @@ all = [ "requests", "requests-oauthlib", "six", + "urllib3", "dnacentersdk", - "meraki" + "meraki", + "slurpit_sdk", ] aristacv = [ "cloudvision", "cvprac", ] +bootstrap = [ + "pytz" +] +citrix-adm = [ + "requests", + "urllib3", + "netutils", +] device42 = [ "requests", ] @@ -143,6 +155,9 @@ ipfabric = [ meraki = [ "meraki", ] +slurpit = [ + "slurpit_sdk", +] # pysnow = "^0.7.17" # PySNow is currently pinned to an older version of pytz as a dependency, which blocks compatibility with newer # versions of Nautobot. See https://github.com/rbw/pysnow/pull/186