From 059b911304105dbd8ba20100d686f237a8089453 Mon Sep 17 00:00:00 2001 From: bakebot Date: Fri, 20 Oct 2023 07:43:05 +0000 Subject: [PATCH 1/5] Cookie initialy baked by NetworkToCode Cookie Drift Manager Tool Template: ``` { "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "dir": "nautobot-app", "ref": "develop", "path": null } ``` Cookie: ``` { "remote": "https://github.com/nautobot/nautobot-plugin-ssot.git", "path": "/opt/ntc/drift-manager/outputs/nautobot-plugin-ssot", "repository_path": "/opt/ntc/drift-manager/outputs/nautobot-plugin-ssot", "dir": "", "branch_prefix": "drift-manager", "context": { "codeowner_github_usernames": "@smith-ntc", "full_name": "Network to Code, LLC", "email": "opensource@networktocode.com", "github_org": "nautobot", "plugin_name": "nautobot_ssot", "verbose_name": "Single Source of Truth", "plugin_slug": "nautobot-ssot", "project_slug": "nautobot-plugin-ssot", "repo_url": "https://github.com/nautobot/nautobot-plugin-ssot/", "base_url": "ssot", "min_nautobot_version": "2.0.0", "max_nautobot_version": "2.9999", "camel_name": "NautobotSSOTPlugin", "project_short_description": "Nautobot Single Source of Truth", "model_class_name": "None", "open_source_license": "Apache-2.0", "docs_base_url": "https://docs.nautobot.com", "docs_app_url": "https://docs.nautobot.com/projects/ssot/en/latest", "_template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "_output_dir": "/opt/ntc/drift-manager/outputs", "_repo_dir": "/opt/ntc/drift-manager/outputs/.cookiecutters/cookiecutter-nautobot-app/nautobot-app", "_checkout": "develop" }, "base_branch": "develop", "remote_name": "origin", "pull_request_strategy": "PullRequestStrategy.UPDATE_OR_CREATE", "post_actions": [ "PostAction.BLACK" ], "baked_commit_ref": "", "draft": true } ``` CLI Arguments: ``` { "cookie_dir": "", "input": false, "json_filename": "setup-cookie-ssot.json", "output_dir": "./outputs", "push": true, "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "template_dir": "nautobot-app", "template_ref": "develop", "pull_request": "update-or-create", "post_action": [ "black" ], "disable_post_actions": false, "draft": true } ``` --- .bandit.yml | 2 +- .cookiecutter.json | 58 ++-- .flake8 | 15 +- .github/CODEOWNERS | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .../pull_request_template.md | 39 ++- .github/workflows/ci.yml | 146 ++++++---- .github/workflows/rebake.yml | 118 ++++++++ .github/workflows/upstream_testing.yml | 2 +- .gitignore | 80 +++++- .yamllint.yml | 1 - LICENSE | 2 +- README.md | 130 ++------- development/Dockerfile | 26 +- development/creds.example.env | 21 -- development/development.env | 61 ----- development/docker-compose.base.yml | 17 +- development/docker-compose.dev.yml | 18 +- development/docker-compose.mysql.yml | 8 +- development/docker-compose.postgres.yml | 4 +- development/nautobot_config.py | 204 ++++---------- docs/admin/compatibility_matrix.md | 17 +- docs/admin/install.md | 59 ++-- docs/admin/release_notes/version_1.0.md | 45 ++- docs/admin/uninstall.md | 19 +- docs/admin/upgrade.md | 38 +-- docs/assets/extra.css | 14 +- docs/dev/arch_decision.md | 7 + docs/dev/code_reference/api.md | 5 + docs/dev/code_reference/index.md | 3 + docs/dev/code_reference/package.md | 1 + docs/dev/contributing.md | 38 +-- docs/dev/dev_environment.md | 55 ++-- docs/dev/extending.md | 3 + docs/images/icon-nautobot-ssot.png | Bin 36439 -> 74601 bytes docs/requirements.txt | 9 +- docs/user/app_getting_started.md | 37 +-- docs/user/app_overview.md | 26 +- docs/user/app_use_cases.md | 85 +----- docs/user/external_interactions.md | 58 +--- docs/user/faq.md | 4 - invoke.example.yml | 4 +- invoke.mysql.yml | 4 +- mkdocs.yml | 46 +--- nautobot_ssot/__init__.py | 115 +------- nautobot_ssot/tests/__init__.py | 6 - nautobot_ssot/tests/test_api.py | 8 +- nautobot_ssot/tests/test_basic.py | 17 +- pyproject.toml | 156 ++--------- tasks.py | 258 ++++++++++-------- 51 files changed, 872 insertions(+), 1227 deletions(-) create mode 100644 .github/workflows/rebake.yml create mode 100644 docs/dev/arch_decision.md create mode 100644 docs/dev/code_reference/api.md create mode 100644 docs/dev/code_reference/package.md diff --git a/.bandit.yml b/.bandit.yml index f080f8bfe..56f7a83b1 100644 --- a/.bandit.yml +++ b/.bandit.yml @@ -1,5 +1,5 @@ --- -skips: ["B113"] +skips: [] # No need to check for security issues in the test scripts! exclude_dirs: - "./tests/" diff --git a/.cookiecutter.json b/.cookiecutter.json index f57652825..317323f4b 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -1,25 +1,35 @@ { - "cookiecutter": { - "codeowner_github_usernames": "@glennmatthews @jathanism @lampwins @chadell", - "full_name": "Network to Code, LLC", - "email": "info@networktocode.com", - "github_org": "nautobot", - "plugin_name": "nautobot_ssot", - "verbose_name": "Nautobot Ssot", - "plugin_slug": "nautobot-ssot", - "project_slug": "nautobot-plugin-ssot", - "repo_url": "https://github.com/nautobot/nautobot-plugin-ssot", - "base_url": "ssot", - "min_nautobot_version": "1.4.0", - "max_nautobot_version": "1.9999", - "nautobot_version": "1.5.1", - "camel_name": "NautobotSsot", - "project_short_description": "Nautobot Single Source of Truth", - "version": "1.2.0", - "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/nautobot-ssot/en/latest", - "_template": "../cookiecutter-ntc/nautobot-plugin" - } -} \ No newline at end of file + "cookiecutter": { + "codeowner_github_usernames": "@smith-ntc", + "full_name": "Network to Code, LLC", + "email": "opensource@networktocode.com", + "github_org": "nautobot", + "plugin_name": "nautobot_ssot", + "verbose_name": "Single Source of Truth", + "plugin_slug": "nautobot-ssot", + "project_slug": "nautobot-plugin-ssot", + "repo_url": "https://github.com/nautobot/nautobot-plugin-ssot/", + "base_url": "ssot", + "min_nautobot_version": "2.0.0", + "max_nautobot_version": "2.9999", + "camel_name": "NautobotSSOTPlugin", + "project_short_description": "Nautobot Single Source of Truth", + "model_class_name": "None", + "open_source_license": "Apache-2.0", + "docs_base_url": "https://docs.nautobot.com", + "docs_app_url": "https://docs.nautobot.com/projects/ssot/en/latest", + "_drift_manager": { + "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", + "template_dir": "nautobot-app", + "template_ref": "develop", + "cookie_dir": "", + "branch_prefix": "drift-manager", + "pull_request_strategy": "update-or-create", + "post_actions": [ + "black" + ], + "draft": true, + "baked_commit_ref": "802b921a43ac6c3a5bfe059c4feccc8eef629fed" + } + } +} diff --git a/.flake8 b/.flake8 index 0f6a37709..c9f5e84df 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,10 @@ [flake8] -# E501: Line length is enforced by Black, so flake8 doesn't need to check it -# W503: Black disagrees with this rule, as does PEP 8; Black wins -ignore = E501, W503 -exclude = - .venv - nautobot_ssot/integrations/servicenow/third_party +ignore = + E501, # Line length is enforced by Black, so flake8 doesn't need to check it + W503 # Black disagrees with this rule, as does PEP 8; Black wins +exclude = + migrations, + __pycache__, + manage.py, + settings.py, + .venv diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index af13990a1..283f0a2a4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Default owner(s) of all files in this repository -* @nautobot/plugin-ssot +* @smith-ntc diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index bf98113ba..d8a2dd9bc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,8 +4,8 @@ about: Report a reproducible bug in the current release of nautobot-ssot --- ### Environment -* Python version: -* Nautobot version: +* Python version: +* Nautobot version: * nautobot-ssot version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bd69ddba4..f414bf37c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,7 +5,7 @@ about: Propose a new feature or enhancement --- ### Environment -* Nautobot version: +* Nautobot version: * nautobot-ssot version: +---> ### Use Case diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index 2cf476470..3950227cd 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -1,10 +1,35 @@ -## New Pull Request + -## Change Notes +# Closes: # -## Justification +## What's Changed + + + +## To Do + + +- [ ] Explanation of Change(s) +- [ ] Added change log fragment(s) (for more information see [the documentation](https://docs.nautobot.com/projects/core/en/stable/development/#creating-changelog-fragments)) +- [ ] Attached Screenshots, Payload Example +- [ ] Unit, Integration Tests +- [ ] Documentation Updates (when adding/changing features) +- [ ] Example Plugin Updates (when adding/changing features) +- [ ] Outline Remaining Work, Constraints from Design diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dde2168c5..e7faf21d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,58 +17,69 @@ env: jobs: black: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_SSOT_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: black" run: "poetry run invoke black" bandit: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_SSOT_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: bandit" run: "poetry run invoke bandit" pydocstyle: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_SSOT_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: pydocstyle" run: "poetry run invoke pydocstyle" flake8: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_SSOT_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: flake8" run: "poetry run invoke flake8" + poetry: + runs-on: "ubuntu-22.04" + env: + INVOKE_NAUTOBOT_SSOT_LOCAL: "True" + steps: + - name: "Check out repository code" + uses: "actions/checkout@v4" + - name: "Setup environment" + uses: "networktocode/gh-action-setup-poetry-environment@v4" + - name: "Checking: poetry lock file" + run: "poetry run invoke lock --check" yamllint: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_SSOT_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: yamllint" run: "poetry run invoke yamllint" pylint: @@ -76,27 +87,28 @@ jobs: - "bandit" - "pydocstyle" - "flake8" + - "poetry" - "yamllint" - "black" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" strategy: fail-fast: true matrix: - python-version: ["3.8"] + python-version: ["3.11"] nautobot-version: ["2.0.0"] env: INVOKE_NAUTOBOT_SSOT_PYTHON_VER: "${{ matrix.python-version }}" INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Set up Docker Buildx" id: "buildx" - uses: "docker/setup-buildx-action@v1" + uses: "docker/setup-buildx-action@v3" - name: "Build" - uses: "docker/build-push-action@v2" + uses: "docker/build-push-action@v5" with: builder: "${{ steps.buildx.outputs.name }}" context: "./" @@ -113,42 +125,80 @@ jobs: run: "cp development/creds.example.env development/creds.env" - name: "Linting: pylint" run: "poetry run invoke pylint" + check-migrations: + needs: + - "bandit" + - "pydocstyle" + - "flake8" + - "poetry" + - "yamllint" + - "black" + runs-on: "ubuntu-22.04" + strategy: + fail-fast: true + matrix: + python-version: ["3.11"] + nautobot-version: ["2.0.0"] + env: + INVOKE_NAUTOBOT_SSOT_PYTHON_VER: "${{ matrix.python-version }}" + INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" + steps: + - name: "Check out repository code" + uses: "actions/checkout@v4" + - name: "Setup environment" + uses: "networktocode/gh-action-setup-poetry-environment@v4" + - name: "Set up Docker Buildx" + id: "buildx" + uses: "docker/setup-buildx-action@v3" + - name: "Build" + uses: "docker/build-push-action@v5" + with: + builder: "${{ steps.buildx.outputs.name }}" + context: "./" + push: false + load: true + tags: "${{ env.PLUGIN_NAME }}/nautobot:${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + file: "./development/Dockerfile" + cache-from: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + cache-to: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + build-args: | + NAUTOBOT_VER=${{ matrix.nautobot-version }} + PYTHON_VER=${{ matrix.python-version }} + - name: "Copy credentials" + run: "cp development/creds.example.env development/creds.env" + - name: "Checking: migrations" + run: "poetry run invoke check-migrations" unittest: needs: - "pylint" + - "check-migrations" strategy: fail-fast: true matrix: - python-version: ["3.8"] - db-backend: ["postgresql", "mysql"] - nautobot-version: ["2.0.0"] - # The include is a method to limit the amount of jobs ran. This essentially - # means that in addition to standard postgres and stable, also the lowest - # supported version and with mysql + python-version: ["3.8", "3.11"] + db-backend: ["postgresql"] + nautobot-version: ["stable"] include: - python-version: "3.11" db-backend: "postgresql" nautobot-version: "2.0.0" - - python-version: "3.11" - db-backend: "postgresql" - nautobot-version: "stable" - python-version: "3.11" db-backend: "mysql" - nautobot-version: "2.0.0" - runs-on: "ubuntu-20.04" + nautobot-version: "stable" + runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_SSOT_PYTHON_VER: "${{ matrix.python-version }}" INVOKE_NAUTOBOT_SSOT_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Set up Docker Buildx" id: "buildx" - uses: "docker/setup-buildx-action@v1" + uses: "docker/setup-buildx-action@v3" - name: "Build" - uses: "docker/build-push-action@v2" + uses: "docker/build-push-action@v5" with: builder: "${{ steps.buildx.outputs.name }}" context: "./" @@ -172,15 +222,15 @@ jobs: needs: - "unittest" name: "Publish to GitHub" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" if: "startsWith(github.ref, 'refs/tags/v')" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Set up Python" - uses: "actions/setup-python@v2" + uses: "actions/setup-python@v4" with: - python-version: "3.9" + python-version: "3.11" - name: "Install Python Packages" run: "pip install poetry" - name: "Set env" @@ -192,7 +242,7 @@ jobs: - name: "Upload binaries to release" uses: "svenstaro/upload-release-action@v2" with: - repo_token: "${{ secrets.GH_NAUTOBOT_BOT_TOKEN }}" + repo_token: "${{ secrets.NTC_GITHUB_TOKEN }}" # use GH_NAUTOBOT_BOT_TOKEN for Nautobot Org repos. file: "dist/*" tag: "${{ github.ref }}" overwrite: true @@ -201,15 +251,15 @@ jobs: needs: - "unittest" name: "Push Package to PyPI" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" if: "startsWith(github.ref, 'refs/tags/v')" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Set up Python" - uses: "actions/setup-python@v2" + uses: "actions/setup-python@v4" with: - python-version: "3.9" + python-version: "3.11" - name: "Install Python Packages" run: "pip install poetry" - name: "Set env" @@ -225,9 +275,9 @@ jobs: password: "${{ secrets.PYPI_API_TOKEN }}" slack-notify: needs: - # - "publish_gh" + - "publish_gh" - "publish_pypi" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}" SLACK_MESSAGE: >- @@ -240,7 +290,7 @@ jobs: # ENVs cannot be used directly in job.if. This is a workaround to check # if SLACK_WEBHOOK_URL is present. if: "env.SLACK_WEBHOOK_URL != ''" - uses: "slackapi/slack-github-action@v1.17.0" + uses: "slackapi/slack-github-action@v1" with: payload: | { diff --git a/.github/workflows/rebake.yml b/.github/workflows/rebake.yml new file mode 100644 index 000000000..13d1e3a07 --- /dev/null +++ b/.github/workflows/rebake.yml @@ -0,0 +1,118 @@ +--- +name: "Rebake Cookie" +on: # yamllint disable-line rule:truthy + workflow_call: + inputs: + cookie: + description: "The cookie to rebake" + type: "string" + default: "" + draft: + description: "Whether to create the pull request as a draft" + type: "string" + default: "" + pull-request: + description: "The pull request strategy" + type: "string" + default: "" + template: + description: "The template repository URL" + type: "string" + default: "" + template-dir: + description: "The directory within the template repository to use as the template" + type: "string" + default: "" + template-ref: + description: "The branch or tag to use for the template" + type: "string" + default: "" + drift-manager-tag: + description: "The drift manager Docker image tag to use" + type: "string" + default: "latest" + workflow_dispatch: + inputs: + cookie: + description: "The cookie to rebake" + type: "string" + default: "" + draft: + description: "Whether to create the pull request as a draft" + type: "string" + default: "" + pull-request: + description: "The pull request strategy" + type: "string" + default: "" + template: + description: "The template repository URL" + type: "string" + default: "" + template-dir: + description: "The directory within the template repository to use as the template" + type: "string" + default: "" + template-ref: + description: "The branch or tag to use for the template" + type: "string" + default: "" + drift-manager-tag: + description: "The drift manager Docker image tag to use" + type: "string" + default: "latest" +jobs: + rebake: + runs-on: "ubuntu-22.04" + permissions: + actions: "write" + contents: "write" + packages: "read" + pull-requests: "write" + container: "ghcr.io/nautobot/cookiecutter-nautobot-app-drift-manager/prod:${{ github.event.inputs.drift-manager-tag }}" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + steps: + - name: "Configure Rebake Arguments" + id: "config" + shell: "bash" + run: | + ARGS='--push' + + if [[ '${{ github.event.inputs.draft }}' == 'true' ]]; then + ARGS="$ARGS --draft" + elif [[ '${{ github.event.inputs.draft }}' == 'false' ]]; then + ARGS="$ARGS --no-draft" + elif [[ '${{ github.event.inputs.draft }}' == '' ]]; then + echo "Using repo default value for --draft" + else + echo "ERROR: Invalid value for draft: '${{ github.event.inputs.draft }}'" + exit 1 + fi + + if [[ '${{ github.event.inputs.pull-request }}' != '' ]]; then + ARGS="$ARGS --pull-request='${{ github.event.inputs.pull-request }}'" + fi + + if [[ '${{ github.event.inputs.template }}' != '' ]]; then + ARGS="$ARGS --template='${{ github.event.inputs.template }}'" + fi + + if [[ '${{ github.event.inputs.template-dir }}' != '' ]]; then + ARGS="$ARGS --template-dir='${{ github.event.inputs.template-dir }}'" + fi + + if [[ '${{ github.event.inputs.template-ref }}' != '' ]]; then + ARGS="$ARGS --template-ref='${{ github.event.inputs.template-ref }}'" + fi + + if [[ '${{ github.event.inputs.cookie }}' == '' ]]; then + ARGS="$ARGS '${{ github.repositoryUrl }}'" + else + ARGS="$ARGS '${{ github.event.inputs.cookie }}'" + fi + + echo "args=$ARGS" >> $GITHUB_OUTPUT + - name: "Rebake" + run: | + python -m ntc_cookie_drift_manager rebake ${{ steps.config.outputs.args }} diff --git a/.github/workflows/upstream_testing.yml b/.github/workflows/upstream_testing.yml index febf88faa..b69810f4a 100644 --- a/.github/workflows/upstream_testing.yml +++ b/.github/workflows/upstream_testing.yml @@ -10,4 +10,4 @@ jobs: uses: "nautobot/nautobot/.github/workflows/plugin_upstream_testing_base.yml@develop" with: # Below could potentially be collapsed into a single argument if a concrete relationship between both is enforced invoke_context_name: "NAUTOBOT_SSOT" - plugin_name: "nautobot-ssot" + plugin_name: "nautobot-plugin-ssot" diff --git a/.gitignore b/.gitignore index ad065156a..69ee78fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -189,12 +189,51 @@ $RECYCLE.BIN/ *.lnk ### PyCharm ### -.idea - +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr # CMake cmake-build-*/ +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + # File-based project format *.iws @@ -207,12 +246,21 @@ out/ # JIRA plugin atlassian-ide-plugin.xml +# Cursive Clojure plugin +.idea/replstate.xml + # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + ### PyCharm Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 @@ -221,6 +269,28 @@ fabric.properties # .idea/misc.xml # *.ipr +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + ### vscode ### .vscode/* *.code-workspace @@ -233,11 +303,7 @@ development/*.txt invoke.yml # Docs -docs/README.md -docs/CHANGELOG.md public /compose.yaml /dump.sql - -nautobot_backup.dump -packages/ +/nautobot_ssot/static/nautobot_ssot/docs diff --git a/.yamllint.yml b/.yamllint.yml index 16d28826d..8cc3e9a9f 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -10,5 +10,4 @@ rules: quote-type: "double" ignore: | .venv/ - nautobot_ssot/integrations/aci/diffsync/device-types/ compose.yaml diff --git a/LICENSE b/LICENSE index 087f92f5c..d46cc9753 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache Software License 2.0 -Copyright (c) 2021, Network to Code, LLC +Copyright (c) 2023, Network to Code, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 1bf3ef6e3..a12295790 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ -# Nautobot Single Source of Truth (SSoT) +# Single Source of Truth + +

@@ -11,81 +21,41 @@ An App for Nautobot.

- ## Overview -An app for [Nautobot](https://github.com/nautobot/nautobot). This Nautobot app facilitates integration and data synchronization between various "source of truth" (SoT) systems, with Nautobot acting as a central clearinghouse for data - a Single Source of Truth, if you will. - -The Nautobot SSoT app builds atop the [DiffSync](https://github.com/networktocode/diffsync) Python library and Nautobot's Jobs feature. This enables the rapid development and integration of Jobs that can be run within Nautobot to pull data from other systems ("Data Sources") into Nautobot and/or push data from Nautobot into other systems ("Data Targets") as desired. Key features include the following: - -* A dashboard UI lists all registered Data Sources and Data Targets and provides a summary of the synchronization history. -* The outcome of executing of a data synchronization Job is automatically saved to Nautobot's database for later review. -* Detailed logging output generated by DiffSync is automatically captured and saved to the database as well. - -### Integrations - -This Nautobot application framework includes the following integrations: - -- Cisco ACI -- Arista CloudVision -- Device42 -- Infoblox -- IPFabric -- ServiceNow - -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). +> Developer Note: Add a long (2-3 paragraphs) description of what the App does, what problems it solves, what functionality it adds to Nautobot, what external systems it works with etc. ### Screenshots ---- +> Developer Note: Add any representative screenshots of the App in action. These images should also be added to the `docs/user/app_use_cases.md` section. -The dashboard view of the app. -![Dashboard View](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/dashboard_initial.png) +> Developer Note: Place the files in the `docs/images/` folder and link them using only full URLs from GitHub, for example: `![Overview](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/plugin-overview.png)`. This absolute static linking is required to ensure the README renders properly in GitHub, the docs site, and any other external sites like PyPI. ---- +More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/ssot/en/latest/user/app_use_cases/) page in the documentation. Here's a quick overview of some of the plugin's added functionality: -The detailed view of the example data source that is prepackaged within this app. -![Data Source Detail View](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/data_source_detail.png) - ---- - -The detailed view of an executed sync. -![Sync Detail View](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/sync_detail.png) - ---- - -More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/ssot/en/latest/user/app_use_cases/) page in the documentation. +![](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/placeholder.png) ## Try it out! -This Nautobot app is installed in the Nautobot Community Sandbox found over at [demo.nautobot.com](https://demo.nautobot.com/)! +> Developer Note: Only keep this section if appropriate. Update link to correct sandbox. + +This App is installed in the Nautobot Community Sandbox found over at [demo.nautobot.com](https://demo.nautobot.com/)! > For a full list of all the available always-on sandbox environments, head over to the main page on [networktocode.com](https://www.networktocode.com/nautobot/sandbox-environments/). ## Documentation -Full documentation for this app can be found over on the [Nautobot Docs](https://docs.nautobot.com) website: +Full documentation for this App can be found over on the [Nautobot Docs](https://docs.nautobot.com) website: -* [User Guide](https://docs.nautobot.com/projects/ssot/en/latest/user/app_overview/) - Overview, Using the App, Getting Started, Developing Jobs. -* [Administrator Guide](https://docs.nautobot.com/projects/ssot/en/latest/admin/install/) - How to Install, Configure, Upgrade, or Uninstall the App. -* [Developer Guide](https://docs.nautobot.com/projects/ssot/en/latest/dev/contributing/) - Extending the App, Code Reference, Contribution Guide. -* [Release Notes / Changelog](https://docs.nautobot.com/projects/ssot/en/latest/admin/release_notes/). - -## Note On Integration Compatability - -The SSoT framework includes a number of integrations with external Systems of Record: - -* Cisco ACI -* Arista CloudVision -* Device42 -* Infoblox -* ServiceNow - -> 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. +- [User Guide](https://docs.nautobot.com/projects/ssot/en/latest/user/app_overview/) - Overview, Using the App, Getting Started. +- [Administrator Guide](https://docs.nautobot.com/projects/ssot/en/latest/admin/install/) - How to Install, Configure, Upgrade, or Uninstall the App. +- [Developer Guide](https://docs.nautobot.com/projects/ssot/en/latest/dev/contributing/) - Extending the App, Code Reference, Contribution Guide. +- [Release Notes / Changelog](https://docs.nautobot.com/projects/ssot/en/latest/admin/release_notes/). +- [Frequently Asked Questions](https://docs.nautobot.com/projects/ssot/en/latest/user/faq/). ### Contributing to the Documentation -You can find all the Markdown source for the app documentation under the [`docs`](https://github.com/nautobot/nautobot-plugin-ssot/tree/develop/docs) folder in this repository. For simple edits, a Markdown capable editor is sufficient: clone the repository and edit away. +You can find all the Markdown source for the App documentation under the [`docs`](https://github.com/nautobot/nautobot-plugin-ssot//tree/develop/docs) folder in this repository. For simple edits, a Markdown capable editor is sufficient: clone the repository and edit away. If you need to view the fully-generated documentation site, you can build it with [MkDocs](https://www.mkdocs.org/). A container hosting the documentation can be started using the `invoke` commands (details in the [Development Environment Guide](https://docs.nautobot.com/projects/ssot/en/latest/dev/dev_environment/#docker-development-environment)) on [http://localhost:8001](http://localhost:8001). Using this container, as your changes to the documentation are saved, they will be automatically rebuilt and any pages currently being viewed will be reloaded in your browser. @@ -94,51 +64,3 @@ Any PRs with fixes or improvements are very welcome! ## Questions For any questions or comments, please check the [FAQ](https://docs.nautobot.com/projects/ssot/en/latest/user/faq/) first. Feel free to also swing by the [Network to Code Slack](https://networktocode.slack.com/) (channel `#nautobot`), sign up [here](http://slack.networktocode.com/) if you don't have an account. - -## Acknowledgements - -This project includes code originally written in separate Nautobot apps, which have been merged into this project: - -- [nautobot-plugin-ssot-aci](https://github.com/nautobot/nautobot-plugin-ssot-aci): - Thanks - [@chadell](https://github.com/chadell), - [@dnewood](https://github.com/dnewood), - [@progala](https://github.com/progala), - [@ubajze](https://github.com/ubajze) -- [nautobot-plugin-ssot-arista-cloudvision](https://github.com/nautobot/nautobot-plugin-ssot-arista-cloudvision): - Thanks - [@burnyd](https://github.com/burnyd), - [@chipn](https://github.com/chipn), - [@jdrew82](https://github.com/jdrew82), - [@jvanderaa](https://github.com/jvanderaa), - [@nniehoff](https://github.com/nniehoff), - [@qduk](https://github.com/qduk), - [@ubajze](https://github.com/ubajze) -- [nautobot-plugin-ssot-infoblox](https://github.com/nautobot/nautobot-plugin-ssot-infoblox): - Thanks - [@FragmentedPacket](https://github.com/FragmentedPacket), - [@chadell](https://github.com/chadell), - [@jdrew82](https://github.com/jdrew82), - [@jtdub](https://github.com/jtdub), - [@pke11y](https://github.com/pke11y), - [@smk4664](https://github.com/smk4664), - [@ubajze](https://github.com/ubajze) - [@whitej6](https://github.com/whitej6), -- [nautobot-plugin-ssot-ipfabric](https://github.com/nautobot/nautobot-plugin-ssot-ipfabric): - Thanks - [@FragmentedPacket](https://github.com/FragmentedPacket), - [@armartirosyan](https://github.com/armartirosyan), - [@chadell](https://github.com/chadell), - [@grelleum](https://github.com/grelleum), - [@h4ndzdatm0ld](https://github.com/h4ndzdatm0ld), - [@jdrew82](https://github.com/jdrew82), - [@justinjeffery-ipf](https://github.com/justinjeffery-ipf), - [@pke11y](https://github.com/pke11y), - [@ubajze](https://github.com/ubajze) - [@whitej6](https://github.com/whitej6), -- [nautobot-plugin-ssot-servicenow](https://github.com/nautobot/nautobot-plugin-ssot-servicenow): - Thanks - [@chadell](https://github.com/chadell), - [@glennmatthews](https://github.com/glennmatthews), - [@qduk](https://github.com/qduk), - [@ubajze](https://github.com/ubajze) diff --git a/development/Dockerfile b/development/Dockerfile index b76454434..09a1d2529 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -6,11 +6,11 @@ # ------------------------------------------------------------------------------------- # !!! USE CAUTION WHEN MODIFYING LINES BELOW -# Accepts a desired Nautobot version as build argument, default to 1.4 -ARG NAUTOBOT_VER="1.4" +# Accepts a desired Nautobot version as build argument, default to 2.0.0 +ARG NAUTOBOT_VER="2.0.0" -# Accepts a desired Python version as build argument, default to 3.8 -ARG PYTHON_VER="3.8" +# Accepts a desired Python version as build argument, default to 3.11 +ARG PYTHON_VER="3.11" # Retrieve published development image of Nautobot base which should include most CI dependencies FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} @@ -19,21 +19,21 @@ FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} ARG NAUTOBOT_ROOT=/opt/nautobot ENV prometheus_multiproc_dir=/prom_cache -ENV NAUTOBOT_ROOT ${NAUTOBOT_ROOT} +ENV NAUTOBOT_ROOT=${NAUTOBOT_ROOT} +ENV INVOKE_NAUTOBOT_SSOT_LOCAL=true # Install Poetry manually via its installer script; # We might be using an older version of Nautobot that includes an older version of Poetry # and CI and local development may have a newer version of Poetry # Since this is only used for development and we don't ship this container, pinning Poetry back is not expressly necessary # We also don't need virtual environments in container -ENV POETRY_VERSION=1.5.1 -RUN curl -sSL https://install.python-poetry.org | python3 - && \ +RUN which poetry || curl -sSL https://install.python-poetry.org | python3 - && \ poetry config virtualenvs.create false # !!! USE CAUTION WHEN MODIFYING LINES ABOVE # ------------------------------------------------------------------------------------- # App-specifc system build/test dependencies. -# +# # Example: LDAP requires `libldap2-dev` to be apt-installed before the Python package. # ------------------------------------------------------------------------------------- # --> Start safe to modify section @@ -63,21 +63,19 @@ RUN pip show nautobot | grep "^Version: " | sed -e 's/Version: /nautobot==/' > c # # We can't use the entire freeze as it takes forever to resolve with rigidly fixed non-direct dependencies, # especially those that are only direct to Nautobot but the container included versions slightly mismatch -RUN poetry export -f requirements.txt --without-hashes --extras all --output poetry_freeze_base.txt -RUN poetry export -f requirements.txt --without-hashes --extras all --with dev --output poetry_freeze_all.txt +RUN poetry export -f requirements.txt --without-hashes --output poetry_freeze_base.txt +RUN poetry export -f requirements.txt --with dev --without-hashes --output poetry_freeze_all.txt RUN sort poetry_freeze_base.txt poetry_freeze_all.txt | uniq -u > poetry_freeze_dev.txt # Install all local project as editable, constrained on Nautobot version, to get any additional # direct dependencies of the app -RUN --mount=type=cache,target=/root/.cache/pip \ +RUN --mount=type=cache,target="/root/.cache/pip",sharing=locked \ pip install -c constraints.txt -e .[all] # Install any dev dependencies frozen from Poetry # Can be improved in Poetry 1.2 which allows `poetry install --only dev` -RUN --mount=type=cache,target=/root/.cache/pip \ +RUN --mount=type=cache,target="/root/.cache/pip",sharing=locked \ pip install -c constraints.txt -r poetry_freeze_dev.txt COPY development/nautobot_config.py ${NAUTOBOT_ROOT}/nautobot_config.py # !!! USE CAUTION WHEN MODIFYING LINES ABOVE - -USER root diff --git a/development/creds.example.env b/development/creds.example.env index 780d04b29..26e24fade 100644 --- a/development/creds.example.env +++ b/development/creds.example.env @@ -25,24 +25,3 @@ MYSQL_PASSWORD=${NAUTOBOT_DB_PASSWORD} # NAUTOBOT_DB_HOST=localhost # NAUTOBOT_REDIS_HOST=localhost # NAUTOBOT_CONFIG=development/nautobot_config.py - -NAUTOBOT_ARISTACV_CVP_PASSWORD="changeme" -NAUTOBOT_ARISTACV_CVP_TOKEN="changeme" - -NAUTOBOT_SSOT_INFOBLOX_PASSWORD="changeme" - -# ACI Credentials. Append friendly name to the end to identify each APIC. -NAUTOBOT_APIC_BASE_URI_NTC=https://aci.cloud.networktocode.com -NAUTOBOT_APIC_USERNAME_NTC=admin -NAUTOBOT_APIC_PASSWORD_NTC=super_secret_password -NAUTOBOT_APIC_VERIFY_NTC=False -# NAUTOBOT_APIC_SITE_NTC="NTC ACI" -NAUTOBOT_APIC_BASE_URI_DEVNET=https://sandboxapicdc.cisco.com -NAUTOBOT_APIC_USERNAME_DEVNET=admin -NAUTOBOT_APIC_PASSWORD_DEVNET=super_secret_password -NAUTOBOT_APIC_VERIFY_DEVNET=False -# NAUTOBOT_APIC_SITE_DEVNET="DevNet Sandbox" - -SERVICENOW_PASSWORD="changeme" - -IPFABRIC_API_TOKEN=secrettoken diff --git a/development/development.env b/development/development.env index 4c1459fe6..54f0b8708 100644 --- a/development/development.env +++ b/development/development.env @@ -7,8 +7,6 @@ NAUTOBOT_BANNER_TOP="Local" NAUTOBOT_CHANGELOG_RETENTION=0 NAUTOBOT_DEBUG=True -NAUTOBOT_DJANGO_EXTENSIONS_ENABLED=True -NAUTOBOT_DJANGO_TOOLBAR_ENABLED=True NAUTOBOT_LOG_LEVEL=DEBUG NAUTOBOT_METRICS_ENABLED=True NAUTOBOT_NAPALM_TIMEOUT=5 @@ -38,62 +36,3 @@ POSTGRES_DB=${NAUTOBOT_DB_NAME} MYSQL_USER=${NAUTOBOT_DB_USER} MYSQL_DATABASE=${NAUTOBOT_DB_NAME} MYSQL_ROOT_HOST=% - -NAUTOBOT_HOST="http://nautobot:8080" - -NAUTOBOT_CELERY_TASK_SOFT_TIME_LIMIT=7200 -NAUTOBOT_CELERY_TASK_TIME_LIMIT=7200 - -NAUTOBOT_SSOT_HIDE_EXAMPLE_JOBS="False" -NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS="False" - -NAUTOBOT_SSOT_ENABLE_ACI="True" -NAUTOBOT_SSOT_ACI_TAG="ACI" -NAUTOBOT_SSOT_ACI_TAG_COLOR="0047AB" -NAUTOBOT_SSOT_ACI_TAG_UP="UP" -NAUTOBOT_SSOT_ACI_TAG_UP_COLOR="008000" -NAUTOBOT_SSOT_ACI_TAG_DOWN="DOWN" -NAUTOBOT_SSOT_ACI_TAG_DOWN_COLOR="FF3333" -NAUTOBOT_SSOT_ACI_MANUFACTURER_NAME="Cisco" -NAUTOBOT_SSOT_ACI_IGNORE_TENANTS="[mgmt,infra]" -NAUTOBOT_SSOT_ACI_COMMENTS="Created by ACI SSoT Integration" -NAUTOBOT_SSOT_ACI_SITE="Data Center" - -NAUTOBOT_SSOT_ENABLE_ARISTACV="True" -NAUTOBOT_ARISTACV_CONTROLLER_SITE="" -NAUTOBOT_ARISTACV_CREATE_CONTROLLER="True" -NAUTOBOT_ARISTACV_CVAAS_URL="www.arista.io:443" -NAUTOBOT_ARISTACV_CVP_HOST="" -NAUTOBOT_ARISTACV_CVP_PORT="443" -NAUTOBOT_ARISTACV_CVP_USERNAME="changeme" -NAUTOBOT_ARISTACV_DELETE_ON_SYNC="False" -NAUTOBOT_ARISTACV_IMPORT_ACTIVE="False" -NAUTOBOT_ARISTACV_IMPORT_TAG="False" -NAUTOBOT_ARISTACV_VERIFY=True - -NAUTOBOT_SSOT_ENABLE_DEVICE42="True" -NAUTOBOT_SSOT_DEVICE42_HOST="" -NAUTOBOT_SSOT_DEVICE42_USERNAME="" -NAUTOBOT_SSOT_DEVICE42_PASSWORD="" - -NAUTOBOT_SSOT_ENABLE_INFOBLOX="True" -NAUTOBOT_SSOT_INFOBLOX_DEFAULT_STATUS="Active" -NAUTOBOT_SSOT_INFOBLOX_ENABLE_SYNC_TO_INFOBLOX="True" -NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_IP_ADDRESSES="True" -NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_SUBNETS="True" -NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLANS="True" -NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLAN_VIEWS="True" -NAUTOBOT_SSOT_INFOBLOX_IMPORT_SUBNETS="10.46.128.0/18,192.168.1.0/24" -NAUTOBOT_SSOT_INFOBLOX_URL="https://infoblox.example.com" -NAUTOBOT_SSOT_INFOBLOX_USERNAME="changeme" -NAUTOBOT_SSOT_INFOBLOX_VERIFY_SSL="True" -# NAUTOBOT_SSOT_INFOBLOX_WAPI_VERSION="" - -NAUTOBOT_SSOT_ENABLE_SERVICENOW="True" -SERVICENOW_INSTANCE="" -SERVICENOW_USERNAME="" - -NAUTOBOT_SSOT_ENABLE_IPFABRIC="True" -IPFABRIC_HOST="https://ipfabric.example.com" -IPFABRIC_SSL_VERIFY="True" -IPFABRIC_TIMEOUT=15 diff --git a/development/docker-compose.base.yml b/development/docker-compose.base.yml index 5343ec019..d71659d74 100644 --- a/development/docker-compose.base.yml +++ b/development/docker-compose.base.yml @@ -22,16 +22,15 @@ services: db: condition: "service_healthy" <<: - - *nautobot-build - *nautobot-base + - *nautobot-build worker: entrypoint: - "sh" - "-c" # this is to evaluate the $NAUTOBOT_LOG_LEVEL from the env - - "watchmedo auto-restart --directory './' --pattern '*.py' --recursive -- nautobot-server celery worker -l $$NAUTOBOT_LOG_LEVEL --events" ## $$ because of docker-compose + - "nautobot-server celery worker -l $$NAUTOBOT_LOG_LEVEL --events" ## $$ because of docker-compose depends_on: - nautobot: - condition: "service_healthy" + - "nautobot" healthcheck: interval: "30s" timeout: "10s" @@ -39,3 +38,13 @@ services: retries: 3 test: ["CMD", "bash", "-c", "nautobot-server celery inspect ping --destination celery@$$HOSTNAME"] ## $$ because of docker-compose <<: *nautobot-base + beat: + entrypoint: + - "sh" + - "-c" # this is to evaluate the $NAUTOBOT_LOG_LEVEL from the env + - "nautobot-server celery beat -l $$NAUTOBOT_LOG_LEVEL" ## $$ because of docker-compose + depends_on: + - "nautobot" + healthcheck: + disable: true + <<: *nautobot-base diff --git a/development/docker-compose.dev.yml b/development/docker-compose.dev.yml index 0ddb4d798..eb98fb770 100644 --- a/development/docker-compose.dev.yml +++ b/development/docker-compose.dev.yml @@ -7,21 +7,13 @@ version: "3.8" services: nautobot: command: "nautobot-server runserver 0.0.0.0:8080" - healthcheck: - interval: "5s" - timeout: "5s" - start_period: "10m" # intentionally conservative upper bound for `npm install` time in a fresh setup - retries: 3 - test: - - "CMD" - - "curl" - - "-f" - - "http://localhost:8080/health/" ports: - "8080:8080" volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" + healthcheck: + test: ["CMD", "true"] # Due to layering, disable: true won't work. Instead, change the test docs: entrypoint: "mkdocs serve -v -a 0.0.0.0:8080" ports: @@ -33,9 +25,15 @@ services: disable: true tty: true worker: + entrypoint: + - "sh" + - "-c" # this is to evaluate the $NAUTOBOT_LOG_LEVEL from the env + - "watchmedo auto-restart --directory './' --pattern '*.py' --recursive -- nautobot-server celery worker -l $$NAUTOBOT_LOG_LEVEL --events" ## $$ because of docker-compose volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" + healthcheck: + test: ["CMD", "true"] # Due to layering, disable: true won't work. Instead, change the test # To expose postgres or redis to the host uncomment the following # postgres: # ports: diff --git a/development/docker-compose.mysql.yml b/development/docker-compose.mysql.yml index c7fa6a1fb..062ada948 100644 --- a/development/docker-compose.mysql.yml +++ b/development/docker-compose.mysql.yml @@ -20,6 +20,7 @@ services: image: "mysql:8" command: - "--default-authentication-plugin=mysql_native_password" + - "--max_connections=1000" env_file: - "development.env" - "creds.env" @@ -27,7 +28,12 @@ services: volumes: - "mysql_data:/var/lib/mysql" healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + test: + - "CMD" + - "mysqladmin" + - "ping" + - "-h" + - "localhost" timeout: "20s" retries: 10 volumes: diff --git a/development/docker-compose.postgres.yml b/development/docker-compose.postgres.yml index 55afdb70f..12d1de314 100644 --- a/development/docker-compose.postgres.yml +++ b/development/docker-compose.postgres.yml @@ -7,11 +7,13 @@ services: - "NAUTOBOT_DB_ENGINE=django.db.backends.postgresql" db: image: "postgres:13-alpine" + command: + - "-c" + - "max_connections=200" env_file: - "development.env" - "creds.env" volumes: - # - "./nautobot.sql:/tmp/nautobot.sql" - "postgres_data:/var/lib/postgresql/data" healthcheck: test: "pg_isready --username=$$POSTGRES_USER --dbname=$$POSTGRES_DB" diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 29c3f55b0..686865159 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -1,11 +1,24 @@ """Nautobot development configuration file.""" -# pylint: disable=invalid-envvar-default import os import sys -from nautobot.core.settings import * # noqa: F403 +from nautobot.core.settings import * # noqa: F403 # pylint: disable=wildcard-import,unused-wildcard-import from nautobot.core.settings_funcs import is_truthy, parse_redis_connection +# +# Debug +# + +DEBUG = is_truthy(os.getenv("NAUTOBOT_DEBUG", False)) +_TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" + +if DEBUG and not _TESTING: + DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: True} + + if "debug_toolbar" not in INSTALLED_APPS: # noqa: F405 + INSTALLED_APPS.append("debug_toolbar") # noqa: F405 + if "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE: # noqa: F405 + MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405 # # Misc. settings @@ -14,6 +27,9 @@ ALLOWED_HOSTS = os.getenv("NAUTOBOT_ALLOWED_HOSTS", "").split(" ") SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "") +# +# Database +# nautobot_db_engine = os.getenv("NAUTOBOT_DB_ENGINE", "django.db.backends.postgresql") default_db_settings = { @@ -43,18 +59,28 @@ DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"} # -# Debug +# Redis # -DEBUG = True +# The django-redis cache is used to establish concurrent locks using Redis. +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": parse_redis_connection(redis_database=0), + "TIMEOUT": 300, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, + } +} -# Django Debug Toolbar -DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: DEBUG and not TESTING} +# Redis Cacheops +CACHEOPS_REDIS = parse_redis_connection(redis_database=1) -if DEBUG and "debug_toolbar" not in INSTALLED_APPS: # noqa: F405 - INSTALLED_APPS.append("debug_toolbar") # noqa: F405 -if DEBUG and "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE: # noqa: F405 - MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405 +# +# Celery settings are not defined here because they can be overloaded with +# environment variables. By default they use `CACHES["default"]["LOCATION"]`. +# # # Logging @@ -62,10 +88,8 @@ LOG_LEVEL = "DEBUG" if DEBUG else "INFO" -TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" - # Verbose logging during normal development operation, but quiet logging during unit test execution -if not TESTING: +if not _TESTING: LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -101,147 +125,17 @@ } # -# Redis -# - -# The django-redis cache is used to establish concurrent locks using Redis. The -# django-rq settings will use the same instance/database by default. -# -# This "default" server is now used by RQ_QUEUES. -# >> See: nautobot.core.settings.RQ_QUEUES -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": parse_redis_connection(redis_database=0), - "TIMEOUT": 300, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - }, - } -} - -# RQ_QUEUES is not set here because it just uses the default that gets imported -# up top via `from nautobot.core.settings import *`. - -# Redis Cacheops -CACHEOPS_REDIS = parse_redis_connection(redis_database=1) - -# -# Celery settings are not defined here because they can be overloaded with -# environment variables. By default they use `CACHES["default"]["LOCATION"]`. +# Apps # -# Enable installed plugins. Add the name of each plugin to the list. -PLUGINS = [ - "nautobot_chatops", - "nautobot_device_lifecycle_mgmt", - "nautobot_ssot", -] - -# Plugins configuration settings. These settings are used by various plugins that the user may have installed. -# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. -PLUGINS_CONFIG = { - "nautobot_chatops": { - "enable_slack": True, - "slack_api_token": os.getenv("SLACK_API_TOKEN"), - "slack_signing_secret": os.getenv("SLACK_SIGNING_SECRET"), - "session_cache_timeout": 3600, - "ipfabric_api_token": os.getenv("IPFABRIC_API_TOKEN"), - "ipfabric_host": os.getenv("IPFABRIC_HOST"), - }, - "nautobot_ssot": { - # URL and credentials should be configured as environment variables on the host system - "aci_apics": {x: os.environ[x] for x in os.environ if "APIC" in x}, - # Tag which will be created and applied to all synchronized objects. - "aci_tag": os.getenv("NAUTOBOT_SSOT_ACI_TAG"), - "aci_tag_color": os.getenv("NAUTOBOT_SSOT_ACI_TAG_COLOR"), - # Tags indicating state applied to synchronized interfaces. - "aci_tag_up": os.getenv("NAUTOBOT_SSOT_ACI_TAG_UP"), - "aci_tag_up_color": os.getenv("NAUTOBOT_SSOT_ACI_TAG_UP_COLOR"), - "aci_tag_down": os.getenv("NAUTOBOT_SSOT_ACI_TAG_DOWN"), - "aci_tag_down_color": os.getenv("NAUTOBOT_SSOT_ACI_TAG_DOWN_COLOR"), - # Manufacturer name. Specify existing, or a new one with this name will be created. - "aci_manufacturer_name": os.getenv("NAUTOBOT_SSOT_ACI_MANUFACTURER_NAME"), - # Exclude any tenants you would not like to bring over from ACI. - "aci_ignore_tenants": os.getenv("NAUTOBOT_SSOT_ACI_IGNORE_TENANTS", "").split(","), - # The below value will appear in the Comments field on objects created in Nautobot - "aci_comments": os.getenv("NAUTOBOT_SSOT_ACI_COMMENTS"), - # Site to associate objects. Specify existing, or a new site with this name will be created. - "aci_site": os.getenv("NAUTOBOT_SSOT_ACI_SITE"), - "aristacv_apply_import_tag": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_TAG", False)), - "aristacv_controller_site": os.getenv("NAUTOBOT_ARISTACV_CONTROLLER_SITE", ""), - "aristacv_create_controller": is_truthy(os.getenv("NAUTOBOT_ARISTACV_CREATE_CONTROLLER", False)), - "aristacv_cvaas_url": os.getenv("NAUTOBOT_ARISTACV_CVAAS_URL", "www.arista.io:443"), - "aristacv_cvp_host": os.getenv("NAUTOBOT_ARISTACV_CVP_HOST", ""), - "aristacv_cvp_password": os.getenv("NAUTOBOT_ARISTACV_CVP_PASSWORD", ""), - "aristacv_cvp_port": os.getenv("NAUTOBOT_ARISTACV_CVP_PORT", "443"), - "aristacv_cvp_token": os.getenv("NAUTOBOT_ARISTACV_CVP_TOKEN", ""), - "aristacv_cvp_user": os.getenv("NAUTOBOT_ARISTACV_CVP_USERNAME", ""), - "aristacv_delete_devices_on_sync": is_truthy(os.getenv("NAUTOBOT_ARISTACV_DELETE_ON_SYNC", False)), - "aristacv_from_cloudvision_default_device_role": "network", - "aristacv_from_cloudvision_default_device_role_color": "ff0000", - "aristacv_from_cloudvision_default_site": "cloudvision_imported", - "aristacv_hostname_patterns": [[r"(?P\w{2,3}\d+)-(?P\w+)-\d+"]], - "aristacv_import_active": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_ACTIVE", False)), - "aristacv_role_mappings": { - "bb": "backbone", - "edge": "edge", - "dist": "distribution", - "leaf": "leaf", - "rtr": "router", - "spine": "spine", - }, - "aristacv_site_mappings": { - "ams01": "Amsterdam", - "atl01": "Atlanta", - }, - "aristacv_verify": is_truthy(os.getenv("NAUTOBOT_ARISTACV_VERIFY", True)), - "enable_aci": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ACI")), - "enable_aristacv": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ARISTACV")), - "enable_device42": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_DEVICE42")), - "enable_infoblox": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_INFOBLOX")), - "enable_ipfabric": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_IPFABRIC")), - "enable_servicenow": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_SERVICENOW")), - "hide_example_jobs": is_truthy(os.getenv("NAUTOBOT_SSOT_HIDE_EXAMPLE_JOBS")), - "device42_host": os.getenv("NAUTOBOT_SSOT_DEVICE42_HOST", ""), - "device42_username": os.getenv("NAUTOBOT_SSOT_DEVICE42_USERNAME", ""), - "device42_password": os.getenv("NAUTOBOT_SSOT_DEVICE42_PASSWORD", ""), - "device42_verify_ssl": False, - "device42_defaults": { - "site_status": "Active", - "rack_status": "Active", - "device_role": "Unknown", - }, - "device42_delete_on_sync": False, - "device42_use_dns": True, - "device42_customer_is_facility": True, - "device42_facility_prepend": "", - "device42_role_prepend": "", - "device42_ignore_tag": "", - "device42_hostname_mapping": [], - "infoblox_default_status": os.getenv("NAUTOBOT_SSOT_INFOBLOX_DEFAULT_STATUS", "active"), - "infoblox_enable_sync_to_infoblox": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_ENABLE_SYNC_TO_INFOBLOX")), - "infoblox_import_objects_ip_addresses": is_truthy( - os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_IP_ADDRESSES") - ), - "infoblox_import_objects_subnets": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_SUBNETS")), - "infoblox_import_objects_vlan_views": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLAN_VIEWS")), - "infoblox_import_objects_vlans": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLANS")), - "infoblox_import_subnets": os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_SUBNETS", "").split(","), - "infoblox_password": os.getenv("NAUTOBOT_SSOT_INFOBLOX_PASSWORD"), - "infoblox_url": os.getenv("NAUTOBOT_SSOT_INFOBLOX_URL"), - "infoblox_username": os.getenv("NAUTOBOT_SSOT_INFOBLOX_USERNAME"), - "infoblox_verify_ssl": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_VERIFY_SSL", True)), - "infoblox_wapi_version": os.getenv("NAUTOBOT_SSOT_INFOBLOX_WAPI_VERSION", "v2.12"), - "ipfabric_api_token": os.getenv("IPFABRIC_API_TOKEN"), - "ipfabric_host": os.getenv("IPFABRIC_HOST"), - "ipfabric_ssl_verify": is_truthy(os.getenv("IPFABRIC_VERIFY", "False")), - "ipfabric_timeout": int(os.getenv("IPFABRIC_TIMEOUT", "15")), - "nautobot_host": os.getenv("NAUTOBOT_HOST"), - "servicenow_instance": os.getenv("SERVICENOW_INSTANCE", ""), - "servicenow_password": os.getenv("SERVICENOW_PASSWORD", ""), - "servicenow_username": os.getenv("SERVICENOW_USERNAME", ""), - }, -} - -METRICS_ENABLED = True +# Enable installed Apps. Add the name of each App to the list. +PLUGINS = ["nautobot_ssot"] + +# Apps configuration settings. These settings are used by various Apps that the user may have installed. +# Each key in the dictionary is the name of an installed App and its value is a dictionary of settings. +# PLUGINS_CONFIG = { +# 'nautobot_ssot': { +# 'foo': 'bar', +# 'buzz': 'bazz' +# } +# } diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index 31f7f2e51..1f05e4a03 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -1,17 +1,8 @@ # Compatibility Matrix -While that last supported version will not be strictly enforced--via the max_version setting, any issues with an updated Nautobot supported version in a minor release, will require a bug to be raised and a fix in Nautobot core to address, with no fixes expected in this Nautobot app. This allows the Nautobot Single Source of Truth app the ability to quickly take advantage of the latest features. +!!! warning "Developer Note - Remove Me!" + Explain how the release models of the plugin and of Nautobot work together, how releases are supported, how features and older releases are deprecated etc. | Single Source of Truth Version | Nautobot First Support Version | Nautobot Last Support Version | -| ------------------------------ | ------------------------------ | ----------------------------- | -| 1.0.X | 1.0.3 | 1.99.99 | -| 1.1.X | 1.0.3 | 1.99.99 | -| 1.2.X | 1.0.3 | 1.99.99 | -| 1.3.X | 1.4.0 | 1.99.99 | -| 1.4.X | 1.4.0 | 1.99.99 | -| 1.5.X | 1.4.0 | 1.99.99 | -| 2.0.0-beta.1 | 2.0.0b2 | 2.0.0b2 | -| 2.0.0-beta.2 | 2.0.0b2 | 2.0.0b2 | -| 2.0.0-rc.1 | 2.0.0rc1 | 2.0.0rc1 | -| 2.0.0-rc.2 | 2.0.0rc2 | 2.0.0rc99 | -| 2.0.0 | 2.0.0 | 2.99.09 | +| ------------- | -------------------- | ------------- | +| 1.0.X | 2.0.0 | 1.99.99 | diff --git a/docs/admin/install.md b/docs/admin/install.md index 54efb1791..355b80f5d 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -1,47 +1,41 @@ # Installing the App in Nautobot +Here you will find detailed instructions on how to **install** and **configure** the App within your Nautobot environment. + +!!! warning "Developer Note - Remove Me!" + Detailed instructions on installing the App. You will need to update this section based on any additional dependencies or prerequisites. + ## Prerequisites -- The app is compatible with Nautobot 1.4.0 and higher. +- The plugin is compatible with Nautobot 2.0.0 and higher. - Databases supported: PostgreSQL, MySQL !!! note Please check the [dedicated page](compatibility_matrix.md) for a full compatibility matrix and the deprecation policy. -!!! warning - If upgrading from `1.x` version to `2.x` version of `nautobot-ssot` app, note that it now incorporates features previously provided by individual apps. For details, see the [upgrade guide](../admin/upgrade.md). +### Access Requirements + +!!! warning "Developer Note - Remove Me!" + What external systems (if any) it needs access to in order to work. ## Install Guide !!! note - Nautobot apps can be installed manually or using Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this Nautobot app is [`nautobot-ssot`](https://pypi.org/project/nautobot-ssot/). + Plugins can be installed manually or using Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this plugin is [`nautobot-ssot`](https://pypi.org/project/nautobot-ssot/). -The app is available as a Python package via PyPI and can be installed with `pip`: +The plugin is available as a Python package via PyPI and can be installed with `pip`: ```shell pip install nautobot-ssot ``` -To use specific integrations, add them as extra dependencies: - -```shell -# To install Cisco ACI integration: -pip install nautobot-ssot[aci] - -# To install Arista CloudVision integration: -pip install nautobot-ssot[aristacv] - -# To install all integrations: -pip install nautobot-ssot[all] -``` - -To ensure Single Source of Truth is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-ssot` package and any of the extras: +To ensure Single Source of Truth is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-ssot` package: ```shell echo nautobot-ssot >> local_requirements.txt ``` -Once installed, the Nautobot app needs to be enabled in your Nautobot configuration. The following block of code below shows the additional configuration required to be added to your `nautobot_config.py` file: +Once installed, the plugin needs to be enabled in your Nautobot configuration. The following block of code below shows the additional configuration required to be added to your `nautobot_config.py` file: - Append `"nautobot_ssot"` to the `PLUGINS` list. - Append the `"nautobot_ssot"` dictionary to the `PLUGINS_CONFIG` dictionary and override any defaults. @@ -52,7 +46,7 @@ PLUGINS = ["nautobot_ssot"] # PLUGINS_CONFIG = { # "nautobot_ssot": { -# "hide_example_jobs": True +# ADD YOUR SETTINGS HERE # } # } ``` @@ -75,20 +69,13 @@ sudo systemctl restart nautobot nautobot-worker nautobot-scheduler ## App Configuration -The app behavior can be controlled with the following list of settings: - -| Key | Example | Default | Description | -| ------------------- | ------- | ------- | ---------------------------------------------------------- | -| `hide_example_jobs` | `True` | `False` | A boolean to represent whether or display the example job. | - -## Integrations Configuration - -The `nautobot-ssot` package includes multiple integrations. Each requires extra dependencies defined in `pyproject.toml`. +!!! warning "Developer Note - Remove Me!" + Any configuration required to get the App set up. Edit the table below as per the examples provided. -Set up each integration using the specific guides: +The plugin behavior can be controlled with the following list of settings: -- [Cisco ACI](./integrations/aci_setup.md) -- [Arista CloudVision](./integrations/aristacv_setup.md) -- [Infoblox](./integrations/infoblox_setup.md) -- [IPFabric](./integrations/ipfabric_setup.md) -- [ServiceNow](./integrations/servicenow_setup.md) +| Key | Example | Default | Description | +| ------- | ------ | -------- | ------------------------------------- | +| `enable_backup` | `True` | `True` | A boolean to represent whether or not to run backup configurations within the plugin. | +| `platform_slug_map` | `{"cisco_wlc": "cisco_aireos"}` | `None` | A dictionary in which the key is the platform slug and the value is what netutils uses in any "network_os" parameter. | +| `per_feature_bar_width` | `0.15` | `0.15` | The width of the table bar within the overview report | diff --git a/docs/admin/release_notes/version_1.0.md b/docs/admin/release_notes/version_1.0.md index 8eeaa8803..8cc3e8509 100644 --- a/docs/admin/release_notes/version_1.0.md +++ b/docs/admin/release_notes/version_1.0.md @@ -1,19 +1,48 @@ # v1.0 Release Notes -## v1.0.1 - 2021-10-18 +!!! warning "Developer Note - Remove Me!" + Guiding Principles: -### Changed + - Changelogs are for humans, not machines. + - There should be an entry for every single version. + - The same types of changes should be grouped. + - Versions and sections should be linkable. + - The latest version comes first. + - The release date of each version is displayed. + - Mention whether you follow Semantic Versioning. -- [#8](https://github.com/nautobot/nautobot-plugin-ssot/pull/8) - Switched from Travis CI to GitHub Actions. + Types of changes: -### Fixed + - `Added` for new features. + - `Changed` for changes in existing functionality. + - `Deprecated` for soon-to-be removed features. + - `Removed` for now removed features. + - `Fixed` for any bug fixes. + - `Security` in case of vulnerabilities. + + +This document describes all new features and changes in the release `1.0`. 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 + +- Major features or milestones +- Achieved in this `x.y` release +- Changes to compatibility with Nautobot and/or other plugins, libraries etc. + +## [v1.0.1] - 2021-09-08 -- [#9](https://github.com/nautobot/nautobot-plugin-ssot/pull/9) - Added missing `name` string to `jobs/examples.py`. +### Added -### Removed +### Changed + +### Fixed -- [#7](https://github.com/nautobot/nautobot-plugin-ssot/pull/7) - Removed unnecessary `markdown-include` development/documentation dependency. +- [#123](https://github.com/nautobot/nautobot-plugin-ssot//issues/123) Fixed Tag filtering not working in job launch form ## [v1.0.0] - 2021-08-03 -- Initial Release +### Added + +### Changed + +### Fixed diff --git a/docs/admin/uninstall.md b/docs/admin/uninstall.md index cdc4fef3c..1bbcacfa4 100644 --- a/docs/admin/uninstall.md +++ b/docs/admin/uninstall.md @@ -1,17 +1,18 @@ # Uninstall the App from Nautobot -Here you will find any steps necessary to cleanly remove the app from your Nautobot environment. +Here you will find any steps necessary to cleanly remove the App from your Nautobot environment. -## Uninstall Guide - -Remove the configuration you added in `nautobot_config.py` from `PLUGINS` & `PLUGINS_CONFIG`. +## Database Cleanup -Uninstall the package +Prior to removing the plugin from the `nautobot_config.py`, run the following command to roll back any migration specific to this plugin. -```bash -$ pip3 uninstall nautobot-ssot +```shell +nautobot-server migrate nautobot_plugin_ssot zero ``` -## Database Cleanup +!!! warning "Developer Note - Remove Me!" + Any other cleanup operations to ensure the database is clean after the app is removed. Is there anything else that needs cleaning up, such as CFs, relationships, etc. if they're no longer desired? + +## Remove App configuration -Drop all tables from the app: `nautobot_ssot*`. +Remove the configuration you added in `nautobot_config.py` from `PLUGINS` & `PLUGINS_CONFIG`. diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index ca79d35a7..ed0393589 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -1,38 +1,10 @@ # Upgrading the App -## Upgrade Guide - -When a new release comes out it may be necessary to run a migration of the database to account for any changes in the data models used by this Nautobot app. Execute the command `nautobot-server post-upgrade` within the runtime environment of your Nautobot installation after updating the `nautobot-ssot` package via `pip`. - -### Potential Apps Conflicts - -!!! warning - If upgrading from versions prior to 1.4 of the `nautobot-ssot` app, note that it now incorporates features previously provided by individual apps. - -Conflicting apps list: - -- `nautobot_ssot_aci` -- `nautobot_ssot_arista_cloudvision` -- `nautobot_ssot_infoblox` -- `nautobot_ssot_ipfabric` -- `nautobot_ssot_servicenow` +Here you will find any steps necessary to upgrade the App in your Nautobot environment. -To prevent conflicts during `nautobot-ssot` upgrade: - -- Remove conflicting applications from the `PLUGINS` section in your Nautobot configuration before enabling the latest `nautobot-ssot` version. -- Transfer the configuration for conflicting apps to the `PLUGIN_CONFIG["nautobot_ssot"]` section of your Nautobot configuration. See `development/nautobot_config.py` for an example. Each [integration set up guide](../integrations/) contains a chapter with upgrade instructions. -- Remove conflicting applications from your project's requirements. - -These steps will help prevent issues during `nautobot-ssot` upgrades. Always back up your data and thoroughly test your configuration after these changes. - -!!! note - It's possible to allow conflicting apps to remain in `PLUGINS` during the upgrade process. You can specify the following environment variable to allow conflicting apps (see `development/development.env` for an example): - - ```bash - NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS=True - ``` +## Upgrade Guide - However, this is not recommended. +!!! warning "Developer Note - Remove Me!" + Add more detailed steps on how the app is upgraded in an existing Nautobot setup and any version specifics (such as upgrading between major versions with breaking changes). -!!! warning - If conflicting apps remain in `PLUGINS`, the `nautobot-ssot` app will raise an exception during startup to prevent potential conflicts. +When a new release comes out it may be necessary to run a migration of the database to account for any changes in the data models used by this plugin. Execute the command `nautobot-server post-upgrade` within the runtime environment of your Nautobot installation after updating the `nautobot-ssot` package via `pip`. diff --git a/docs/assets/extra.css b/docs/assets/extra.css index 756f0eb80..dfe2e4b18 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -18,6 +18,15 @@ font-size: 0.7rem; } +/* +* The default max-width is 61rem which does not provide nearly enough space to present code examples or larger tables +*/ +.md-grid { + margin-left: auto; + margin-right: auto; + max-width: 95%; +} + .md-tabs__link { font-size: 0.8rem; } @@ -150,8 +159,3 @@ a.autorefs-external:hover::after { -webkit-mask-image: var(--md-admonition-icon--version-removed); mask-image: var(--md-admonition-icon--version-removed); } - -/* Do not wrap code blocks in markdown tables. */ -div.md-typeset__table>table>tbody>tr>td>code { - white-space: nowrap; -} diff --git a/docs/dev/arch_decision.md b/docs/dev/arch_decision.md new file mode 100644 index 000000000..e7bcbbe40 --- /dev/null +++ b/docs/dev/arch_decision.md @@ -0,0 +1,7 @@ +# Architecture Decision Records + +The intention is to document deviations from a standard Model View Controller (MVC) design. + +!!! warning "Developer Note - Remove Me!" + Optional page, remove if not applicable. + For examples see [Golden Config](https://github.com/nautobot/nautobot-plugin-golden-config/tree/develop/docs/dev/dev_adr.md) and [nautobot-plugin-reservation](https://github.com/networktocode/nautobot-plugin-reservation/blob/develop/docs/dev/dev_adr.md). diff --git a/docs/dev/code_reference/api.md b/docs/dev/code_reference/api.md new file mode 100644 index 000000000..072d8c209 --- /dev/null +++ b/docs/dev/code_reference/api.md @@ -0,0 +1,5 @@ +# Single Source of Truth API Package + +::: nautobot_ssot.api + options: + show_submodules: True diff --git a/docs/dev/code_reference/index.md b/docs/dev/code_reference/index.md index 473f2c40f..ebe9ff7d1 100644 --- a/docs/dev/code_reference/index.md +++ b/docs/dev/code_reference/index.md @@ -1,3 +1,6 @@ # Code Reference Auto-generated code reference documentation from docstrings. + +!!! warning "Developer Note - Remove Me!" + Uses [mkdocstrings](https://mkdocstrings.github.io/) syntax to auto-generate code documentation from docstrings. Two example pages are provided ([api](api.md) and [package](package.md)), add new stubs for each module or package that you think has relevant documentation. diff --git a/docs/dev/code_reference/package.md b/docs/dev/code_reference/package.md new file mode 100644 index 000000000..a7945015a --- /dev/null +++ b/docs/dev/code_reference/package.md @@ -0,0 +1 @@ +::: nautobot_ssot diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index cbed2207b..2337f740c 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -1,46 +1,24 @@ # Contributing to the App +!!! warning "Developer Note - Remove Me!" + Information on how to contribute fixes, functionality, or documentation changes back to the project. + The project is packaged with a light [development environment](dev_environment.md) based on `docker-compose` to help with the local development of the project and to run tests. The project is following Network to Code software development guidelines and is leveraging the following: - Python linting and formatting: `black`, `pylint`, `bandit`, `flake8`, and `pydocstyle`. - YAML linting is done with `yamllint`. -- Django unit test to ensure the Nautobot app is working properly. +- Django unit test to ensure the plugin is working properly. Documentation is built using [mkdocs](https://www.mkdocs.org/). The [Docker based development environment](dev_environment.md#docker-development-environment) automatically starts a container hosting a live version of the documentation website on [http://localhost:8001](http://localhost:8001) that auto-refreshes when you make any changes to your local files. ## Branching Policy -The branching policy includes the following tenets: - -* The `develop` branch is the primary branch to develop off of. -* PRs intended to add new features should be sourced from the `develop` branch. -* PRs intended to address bug fixes and security patches should be sourced from the `develop` branch. -* PRs intended to add new features that break backward compatibility should be discussed before a PR is created. - -Nautobot Single Source of Truth app will observe semantic versioning, as of 1.0. This may result in an quick turn around in minor versions to keep pace with an ever growing feature set. +!!! warning "Developer Note - Remove Me!" + What branching policy is used for this project and where contributions should be made. ## Release Policy -Nautobot Single Source of Truth currently has no intended scheduled release schedule, and will release new features in minor versions. - -When a new release of any kind (e.g. from `develop` to `main`, or a release of a `stable-.`) is created the following should happen. - -- A release PR is created: - - Add and/or update to the changelog in `docs/admin/release_notes/version_..md` file to reflect the changes. - - Update the mkdocs.yml file to include updates when adding a new release_notes version file. - - Change the version from `..-beta` to `..` in pyproject.toml. - - Set the PR to the proper branch, e.g. either `main` or `stable-.`. -- Ensure the tests for the PR pass. -- Merge the PR. -- Create a new tag: - - The tag should be in the form of `v..`. - - The title should be in the form of `v..`. - - The description should be the changes that were added to the `version_..md` document. -- If merged into `main`, then push from `main` to `develop`, in order to retain the merge commit created when the PR was merged. -- If the is a new `.`, create a `stable-.` for the **previous** version, so that security updates to old versions may be applied more easily. -- A post release PR is created: - - Change the version from `..` to `..-beta` in pyproject.toml. - - Set the PR to the proper branch, e.g. either `develop` or `stable-.`. - - Once tests pass, merge. +!!! warning "Developer Note - Remove Me!" + How new versions are released. diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index 17da136dc..f1798807a 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -13,9 +13,9 @@ This is a quick reference guide if you're already familiar with the development The [Invoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to Invoke to override the default configuration: -- `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: latest) -- `project_name`: the default docker compose project name (default: `nautobot_ssot`) -- `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.8) +- `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: 2.0.0) +- `project_name`: the default docker compose project name (default: `nautobot-ssot`) +- `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.11) - `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) - `compose_dir`: the full path to a directory containing the project compose files - `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) @@ -57,8 +57,6 @@ To either stop or destroy the development environment use the following options. --- nautobot_ssot: local: true - compose_files: - - "docker-compose.requirements.yml" ``` Run the following commands: @@ -66,7 +64,7 @@ Run the following commands: ```shell poetry shell poetry install --extras nautobot -export $(cat development/dev.env | xargs) +export $(cat development/development.env | xargs) export $(cat development/creds.env | xargs) invoke start && sleep 5 nautobot-server migrate @@ -101,9 +99,6 @@ The project features a CLI helper based on [Invoke](https://www.pyinvoke.org/) t Each command can be executed with `invoke `. All commands support the arguments `--nautobot-ver` and `--python-ver` if you want to manually define the version of Python and Nautobot to use. Each command also has its own help `invoke --help` -!!! note - To run the mysql (mariadb) development environment, set the environment variable as such `export NAUTOBOT_USE_MYSQL=1`. - #### Local Development Environment ``` @@ -136,7 +131,6 @@ Each command can be executed with `invoke `. All commands support the a unittest Run Django unit tests for the plugin. ``` - ## Project Overview This project provides the ability to develop and manage the Nautobot server locally (with supporting services being *Dockerized*) or by using only Docker containers to manage Nautobot. The main difference between the two environments is the ability to debug and use **pdb** when developing locally. Debugging with **pdb** within the Docker container is more complicated, but can still be accomplished by either entering into the container (via `docker exec`) or attaching your IDE to the container and running the Nautobot service manually within the container. @@ -155,7 +149,7 @@ Poetry is used in lieu of the "virtualenv" commands and is leveraged in both env The `pyproject.toml` file outlines all of the relevant dependencies for the project: - `tool.poetry.dependencies` - the main list of dependencies. -- `tool.poetry.dev-dependencies` - development dependencies, to facilitate linting, testing, and documentation building. +- `tool.poetry.group.dev.dependencies` - development dependencies, to facilitate linting, testing, and documentation building. The `poetry shell` command is used to create and enable a virtual environment managed by Poetry, so all commands ran going forward are executed within the virtual environment. This is similar to running the `source venv/bin/activate` command with virtualenvs. To install project dependencies in the virtual environment, you should run `poetry install` - this will install **both** project and development dependencies. @@ -185,7 +179,7 @@ The first thing you need to do is build the necessary Docker image for Nautobot #14 exporting layers #14 exporting layers 1.2s done #14 writing image sha256:2d524bc1665327faa0d34001b0a9d2ccf450612bf8feeb969312e96a2d3e3503 done -#14 naming to docker.io/nautobot-ssot/nautobot:latest-py3.7 done +#14 naming to docker.io/nautobot-ssot/nautobot:2.0.0-py3.11 done ``` ### Invoke - Starting the Development Environment @@ -216,9 +210,9 @@ This will start all of the Docker containers used for hosting Nautobot. You shou ```bash ➜ docker ps ****CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -ee90fbfabd77 nautobot-ssot/nautobot:latest-py3.7 "nautobot-server rqw…" 16 seconds ago Up 13 seconds nautobot_ssot_worker_1 -b8adb781d013 nautobot-ssot/nautobot:latest-py3.7 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp nautobot_ssot_nautobot_1 -d64ebd60675d nautobot-ssot/nautobot:latest-py3.7 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp nautobot_ssot_docs_1 +ee90fbfabd77 nautobot-ssot/nautobot:2.0.0-py3.11 "nautobot-server rqw…" 16 seconds ago Up 13 seconds nautobot_ssot_worker_1 +b8adb781d013 nautobot-ssot/nautobot:2.0.0-py3.11 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp nautobot_ssot_nautobot_1 +d64ebd60675d nautobot-ssot/nautobot:2.0.0-py3.11 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp nautobot_ssot_docs_1 e72d63129b36 postgres:13-alpine "docker-entrypoint.s…" 25 seconds ago Up 19 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp nautobot_ssot_postgres_1 96c6ff66997c redis:6-alpine "docker-entrypoint.s…" 25 seconds ago Up 21 seconds 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp nautobot_ssot_redis_1 ``` @@ -296,9 +290,9 @@ This will safely shut down all of your running Docker containers for this projec Your environment should now be fully setup, all necessary Docker containers are created and running, and you're logged into Nautobot in your web browser. Now what? -Now you can start developing your Nautobot app in the project folder! +Now you can start developing your plugin in the project folder! -The magic here is the root directory is mounted inside your Docker containers when built and ran, so **any** changes made to the files in here are directly updated to the Nautobot app code running in Docker. This means that as you modify the code in your Nautobot app folder, the changes will be instantly updated in Nautobot. +The magic here is the root directory is mounted inside your Docker containers when built and ran, so **any** changes made to the files in here are directly updated to the Nautobot plugin code running in Docker. This means that as you modify the code in your plugin folder, the changes will be instantly updated in Nautobot. !!! warning There are a few exceptions to this, as outlined in the section [To Rebuild or Not To Rebuild](#to-rebuild-or-not-to-rebuild). @@ -319,7 +313,10 @@ When trying to debug an issue, one helpful thing you can look at are the logs wi !!! note The `-f` tag will keep the logs open, and output them in realtime as they are generated. -So for example, our app is named `nautobot-ssot`, the command would most likely be `docker logs nautobot_ssot_nautobot_1 -f`. You can find the name of all running containers via `docker ps`. +!!! info + Want to limit the log output even further? Use the `--tail <#>` command line argument in conjunction with `-f`. + +So for example, our plugin is named `nautobot-ssot`, the command would most likely be `docker logs nautobot_ssot_nautobot_1 -f`. You can find the name of all running containers via `docker ps`. If you want to view the logs specific to the worker container, simply use the name of that container instead. @@ -345,7 +342,7 @@ Once completed, the new/updated environment variables should now be live. ### Installing Additional Python Packages -If you want your Nautobot app to leverage another available Nautobot app or another Python package, you can easily add them into your Docker environment. +If you want your plugin to leverage another available Nautobot plugin or another Python package, you can easily add them into your Docker environment. ```bash ➜ poetry shell @@ -360,18 +357,18 @@ Once the dependencies are resolved, stop the existing containers, rebuild the Do ➜ invoke start ``` -### Installing Additional Nautobot Apps +### Installing Additional Nautobot Plugins -Let's say for example you want the new Nautobot app you're creating to integrate into Nautobot SSoT. To do this, you will want to integrate into the existing Nautobot SSoT app. +Let's say for example you want the new plugin you're creating to integrate into Slack. To do this, you will want to integrate into the existing Nautobot ChatOps Plugin. ```bash ➜ poetry shell -➜ poetry add nautobot-ssot +➜ poetry add nautobot-chatops ``` -Once you activate the virtual environment via Poetry, you then tell Poetry to install the new Nautobot app. +Once you activate the virtual environment via Poetry, you then tell Poetry to install the new plugin. -Before you continue, you'll need to update the file `development/nautobot_config.py` accordingly with the name of the new Nautobot app under `PLUGINS` and any relevant settings as necessary for the Nautobot app under `PLUGINS_CONFIG`. Since you're modifying the underlying OS (not just Django files), you need to rebuild the image. This is a similar process to updating environment variables, which was explained earlier. +Before you continue, you'll need to update the file `development/nautobot_config.py` accordingly with the name of the new plugin under `PLUGINS` and any relevant settings as necessary for the plugin under `PLUGINS_CONFIG`. Since you're modifying the underlying OS (not just Django files), you need to rebuild the image. This is a similar process to updating environment variables, which was explained earlier. ```bash ➜ invoke stop @@ -379,10 +376,10 @@ Before you continue, you'll need to update the file `development/nautobot_config ➜ invoke start ``` -Once the containers are up and running, you should now see the new Nautobot app installed in your Nautobot instance. +Once the containers are up and running, you should now see the new plugin installed in your Nautobot instance. !!! note - You can even launch an `ngrok` service locally on your laptop, pointing to port 8080 (such as for SSoT development), and it will point traffic directly to your Docker images. + You can even launch an `ngrok` service locally on your laptop, pointing to port 8080 (such as for chatops development), and it will point traffic directly to your Docker images. ### Updating Python Version @@ -394,14 +391,14 @@ namespace.configure( { "nautobot_ssot": { ... - "python_ver": "3.7", + "python_ver": "3.11", ... } } ) ``` -Or set the `INVOKE_NAUTOBOT_GOLDEN_CONFIG_PYTHON_VER` variable. +Or set the `INVOKE_NAUTOBOT_SSOT_PYTHON_VER` variable. ### Updating Nautobot Version @@ -413,7 +410,7 @@ namespace.configure( { "nautobot_ssot": { ... - "nautobot_ver": "1.0.2", + "nautobot_ver": "2.0.0", ... } } diff --git a/docs/dev/extending.md b/docs/dev/extending.md index 035f29235..49b89f464 100644 --- a/docs/dev/extending.md +++ b/docs/dev/extending.md @@ -1,3 +1,6 @@ # Extending the App +!!! warning "Developer Note - Remove Me!" + Information on how to extend the App functionality. + Extending the application is welcome, however it is best to open an issue first, to ensure that a PR would be accepted and makes sense in terms of features and design. diff --git a/docs/images/icon-nautobot-ssot.png b/docs/images/icon-nautobot-ssot.png index a02b02d9aa2c7d04e332d8a6436a207d86764da8..7e00cf6ae0ee76324adab30d68d64206678a85e1 100644 GIT binary patch literal 74601 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
@5hb{hC=~cCz;d zjT^qOLh2&h9Jng5!qAv-4|2oll0yJ;m#VS!TfJ_GQh<;dx2iWrly)> zQY9BxzV4K&+lvMAUeGJU^5aSBCb^g)RA24nW=YN%oCRY5QsI9_!~xGyq#8U+u5g2$ zCPBC`$T4BqgeL0%VUn6LILZCSvPB^&PJvRC6Vbnux_s1|`J%3zp)~au{M}~aHJ)k^ z02E8ZE4lfae_T2Znl!{-+zt*SkG zfNA<2xoICeBPneHG>#0zmsmwC0kVBLUh6O7QiiI8hb1ap);jybhmh0!SPll~51;Tr zMW= zEK`tFqH$wa&aYg?c@ghwN<$BR$|-8K&RUB=fO6N0|A|^@8B9_TRE6Q>d>3uwo&QZ1j`--)a9iEUrhO>EstojOsKV|Od$7^I zV(ZESuj#R}DMH;lnbt*7*7=VvI{Q+U{2Fo4Ya8{nNwcQLiSFI5S7pL7=kN+Dx46y# zBc&1p=nM?DouED`=nb4s1ejKur5D< z`L~&9=eU=0hh(k=DUyNf{IC^QA2wNy_g-BV62fH6?OW+Jn7G0rM;IrrZC@7q`ViLKuzPmG1ECpQ4xfQw$I#eaXxAPJG&WID0CC z-QRWsH*3_McmHsA0JOIh<$rPB5#0|5N_23B0?=*_(793<9d?9UN~HSSs-Q$&dE@oC zJSjR}nR3im7z-JVW!QWEKkTLQB#TI+UuFhBq2L8UiAgD%JTaFnYPK?NS;>@EXoIPxVl!aEd({t zO@(T7Sjp&cR5rnZf;>e1mfl=)_C#XCDaCxX4y8(7v(*qg08}@XN?hDIEO9BDB8vOY z&m&HN>w(gpHXvh;u%X-}0aZb;y2MI_xIR9@9XNEu%#v03wxN(^(i=KT5Cc$U!Z!2h zOHsP%ALjGlhg5>P28`1kfiNAGw8+akWWD+5nJ>S-{94FxXu(IpUHi$6mw27Sf&AQU}uH|dnNXcb1)=_8MaX&<2qKAc3M zJpbZT#Q^Wa=zha{NN1E4GB%S*!DQNbsUb;d5g00AhSK=&96oCWDmo1=bXGoi$_0b{ zs>I|V{7@H}EBfzomY&I(Y7^0U!S3?^X8~Rjp@Tu5FVuxv999-?w9n6&0mbNM=aejd zxD)xS^L1KtSaqu~0}_WMML&vs6sTm2!T9tp!?_^cCmnY|tUd4_rQ=DRoZcUrkCEBh zaZ@ao7tekVt3=d>0jf;xCFYXaNWV< zu_sv`P(d0>>Q+C1#mQ7aFBaDmzH7u|zpcuq;Bh2o zu#p-h4-31%XyOq!l^@hwGAol#)U&OwbK-P1+zVNc!W^baCsNF%KNI_R>15D334+e) z&I3gxnBOQQUVEY(XdNwnvv+ADE5X;DD)NAlzz8DFn6Dv5aMZ}BVr;=`@?1u2J23)Q zqCz%YP4%3ivO{K~IDt``6V2Fs6k74dq&(r{Y|V_T=1e|$$^)1`3`J#ga^P%n4`+Gwx)4{(Xc&c%m5jfl*mEr=nbj3vcXK)&Gw?d z%_8<>yO_T(JQp44^J&8q@zw@YzAOx*#iPrhtt%amzQza+xjYN!ZkT+&f(*hvg5my* zfEoYU1jHE{um+Zu43$Q~9}>mRPg0!xjM2ieg{Um0e6bYqYGg=eXX1kKzt}APhGBgudK}R7n6a4o=Ve(zCrXkWWd}vsY=>^d>!edJcWMDwpR{1Oiw! zy3m?N9@-Ae>~F0xHk`0Rfsr9gyS(|cj~QGq-d|U?1f>?DG|UW%Bst#GL$#weB`)8X z0dG0M%dEM-JTO`yjATi;69KZKUfTmIVeZma!K0~9|J+0_jwk|_aF>0I#UXn=i$RN? z{>a1Er#B7$)}m5ko~enyOGcm6C%!N>L9>`ayA0?4-&Y`q|47)`EP@qPo1Z1A zbotrQ&GsHTL?BY=CxMT=&5B+|Qa4II8c{=9&pj}C3RzrywfC8lK;4)#c&gRB3T%$e zNRirGLyeh(`LIzhTQiI6d288ytoryEV6V9{PpM{Vu||yW=TEF9L|gwMEt81ZW+BaJ z=;uy+nLJ?@XxWi$jJGK)7C?Ri`(m~mcp&-lvGiYkABYuM-I7f^v*8(*D;731?+QPQ zK=vNFK4lRp=mg2@`xpnyhN3*YIIW80_F`Uec*JCL_Sd@N69O-1baGKaC?rkmZOO%0 z&Nz146P{L!!;|&3^LmV-2zL>@3*LexXjPO$zXq)|U5p6^Jn%X=yuSpWeJK~gpj1&> z@`}2l1^GG<$1uEad42~6!V*(wc~gR(Sy5-Sk^_3NiV-F-Q8h>LPI*d}C2MvS(n6|@ zWa;{UK7K516L?9e>i-CZh!7`7yooQT;D|hgNq?e^7VBl!b$U~tHzbbkfqq6IfxQSj z%#36i2gZS}n-0+vKWhQ;b|=#FH*Sy^n}T@8PlZ7L?=-h2oIZSoJbhJ8pJ0AoLX#I_ zDgNU=#TYR+p9fW`3es($p@n98EbvTUZTLBz;{ij!TWRheeh;?{{Jmwb{p#M9jeUn0 zf{%_YMHaIgK?vUjx8AgROn&@BQCtZ{7Nwo5!mtCCKSOQs;?GYIA}jwC4=-iGC8#eT zG?&`GJp3_ge+}>-+a?6^n#!A-NHqoJSra3~1dXy)GxE3P@UNeG87X&Bo)h;>R=HVJ zsLO(+rnCPuGHhz$E>X(Dh#q}_EO%A22)P-*zAB-e8{G6?Eo8GmuDIZyH_i+>TQP1r z;LXc69%wzB4r5av;7;Rlx_Uh^H*2=Zi2i0i9W&!ctwLv1(Q}3yHF5%|?(E~RaXU4j z{@SCTTna$^b1+6|m|EP8wucQUS(~pYUz#>w~gG` z&k`AZj$@0R3+g9+`Bwp!rv2L%&2!t7yne>tIjvx;d}4o612PK{K{&8{D0zWQ9`u_< z^syTsmC-*?vglp9Cmb?rs~OF%Av29N)!x(w)y&YPn1S<)o^6QBfYSGlx9DgOw`9am zs$~cEA;PaRCi;Vq84Wj>5jUj`FEz)~CU1(pA8KH9*hQ~*Vs~k)b<_#=#*n3YZD5C zI}N@jupjocYVP|gSs!Q0s;icWw{AUQZPc4YqPCZo=^z9;q^8B=!6xkZe$QVLOjeB- zH*?C_*@90uCYAiB&#{xLQsbm0Jpq(RUeCQ@bn+=8Vh_KA((^C2<2;Km-@)VZ(QE!# z3px6Cvf1lCO~*w9t{hkoh?n)nXp1p%jl~xkQy6FvwzVxZ+S)1qo;PlP^*lEZe zGa!rCn9+>nzTU{Mqj1*F4Gnz|)Y*KYWz5xza7 zxVRw(P}7tPXWitO2sTU3SDdUK6jR>#LnR=yUEunjIoC8?K|u{-OjHk!EoUc==*48D z$Q|8_rBHd%r#GoUE!qzWEMg`ZeEFHx>aM?I*1Ls-STz1f@0yVwEG}b+i15e9e_@FdhL_ z5{g}LQy%wVS2bSz*`&kj<-$7StN4BxH{E+S;*_o?2Fgx; zbh&I|^9O9CXk*HtVoSWWgfH5N3*8HBkE5f!FIzc1X%|=qAtisv=xSji2Lyf8!VFKNYVZ4-TKa~{0GeT?LEx(cY5eE`d4!>3C z4fT!)B%U#0b9G?oemkq9+I&rTx#41ji(X=1Sqo^wywbocBqTNfmq9HkkAWT6>!|Dik&*4F4SgEMOcm2G;6jIB?ED;S2sStq#k^ z!ycuAt^v$o^gFBe#!3)(7oY(3E_0f*hI)25k?}u<)7O? zSQd913{vT<_il{v*yH6>?bA-Jzfw=Ys=oPJnNSgncN5V#Bq}|zTitlw?o(oBRB_c1@Fi*)Os%^rk ziDOS@ZDtSmw?4`Zfb3kid7XO)+TOi69PpXCM`UH)K_D=2xt7u*GLqw9>C{6qf0sAN zJvP-hWJ3t21Q<(&8M;bz%ZgWkbT9<~N3^L+==}s|@yDgF=-*Qr@D?-^z{_7>0!!W%5ph)dHVXEqQt>wog8JDyT#E`K2=wZDJ|3jc(43`r&f} zcvaX!R?s4+_&~u=vCmLI&S*wF7NGd3xS#QXBt3%Q|E#~FXJjfF3ealG)fqdtgN!{n z=iGx#~NLpG(sL>J4o#Q8yN>@36MXUNtQvAb6PArhE#s zyn9Yic|yIlWElU?>@Rst&aF2ZgLn{=#farOB9X8vH8&qz(#D})X!trityseV;;<2T za3Y>QAY^E&d9v5bX$U_?h!dfgX!gA%bK5#J$FN1^D-wY&DrbGTNnCSYvgiA?=T_K za-9T@e_I|J*X1uzJu7l?3SK9T^3S26y@2M_y+^~ay43xO@h6Pt+WfnTY#^Apg}d5Y ztYYhWS-hQhw6(meNmeAp9OKMS0M2JXg2a?M-{3=W|1sVrK$^I|1WO|9s8&8VF^A+L zcQ}1iczt2)M>mw->^>v|RY99bQ9J>jhWqq^ZC^i-wVM!_e0y8!H4NGhcnWXRPsn8iH8g^KRpY8Z zu_Q(paw!w#ea#G7U){w%4@~LDU+36W&aPM~gZLDjh9JUvw;kU)20ZOh4r+Qw1Ib3d z*zHoPK0ZwDe`9`s@Uw>Asb6`ykZ5aWUwOl=k)OyA- zJu5mTLe4pG??FN2s?N(@8SgU$3WK%F!)xc8<77dz)Ly%_X6-KKK&12Rzs}ej)zyD4 z#?Ji@IO`#v#R^<*^3TjxidNw2-$$|V{)#dPbGbhbdjBQph=($&8$*4K$hADoR^Uef zRw=bT-bkNU+Rzz@(-A8Y|NL!?mr=^wfaR{UX~oDW;&guD(+ODj#sh?tPYBdrH@_`m z1pKql_irA4gnf==h_laE($gl0uqA`Wb6o#pPpsC4CuAT2?XrWV3}=2=T=jvJabK(- ziv*^4fTXC()8nJ?_!^zt>7-M4VGd%3^OjggU4Hk+EC?V**q&4gcVYXc!V8Y$L_+v6 z^i+|sQS0IGRly1sYW4p|f^v2#%TcKyS+C!9-}R-`w|u0+b6E^Sgu zZp9bQyEAh1qawNeMQ+}PoL=N+F2n`9Cvoo!4Not>(TZP(5i-zmG;SE{KJZ+dn#sk~mlF}wub^{hBMV^%}~3mlN{<}w7#NXuzL z;IjVN8Ug;fLB5}Y%WqwVe+@RAt7*+sJw77^II&_l>@Q@Hq!&ghOhLT|CRJF9%x42LfDP zcr*^YTdcozqw4!lw;hCYiY)9ZJ*X3+>0}S#k|A(>&VWn zE~*gO+e`T^N2?;ud;BP#f)N10s+`o=cmOWbBHrBBPtqdeB;2)Rpa?>l&q=*YywVrt zqogelr7}?Dvvgcakw4h}>y`-SOXZbC!E*P4A;%Y4dPQ;azOgS901EH2ta}sWg7iMf z+*(5tVcvwrUzq@8Wo@+*Xq<13WfbafHHyQmXvY@Pg59c7kfp7uyCUe2!Oj~wrLZ}AykM42g zlMg6B7jqrX)P@#U(;|S&Szas&SuYqn*rDKkNZII5vD26SeEfH zcVPKuY}0Y`L%Yx?BkdXYqz=a870+WTwwCU1>I|F0$>D_PUA4;^TbKJ^O*x!(@t9_! zKXolaux&y0-z{TOVqjI!GyRwyw_D-E$J5aCkXAx1f2728Sz;z3?ZE0JnIhq|fQ0G& z!Z!4nnbVR`x&$Fq1THX)bWFD3qIDm@N{LGyhDutnKWB%9rRlST67Aqurqpg^Al=EM za1GYdd-zf1FtAPt>J4;85i}-$rCpVggO&5W>kO+EvUbqWoCevQ#hwHubRM4(z+=>)a!9e;E@U1n7Yo~K-x4X99w zJ>x41X|VY2G*w|9f^B--&TTMY_7ruQPNiyjstVCl16lE$2xTpJZ|)C2|;s3tSoQF=;qo4csPxd-SkR*w43>qFip+ zZ8QS;`{gKJ3k~y%J8!x)&;^Z}Tsp*}2JR^aX>I4jqE)y|2d{!o#S;I>*J^pT%jveyJv^bG8NO6m(etp_e z$!(I=JHdM3?&BtVV2s3HqLAFV#m;h!eXd(8pYPz8!C3m^rlb1ZZ#QoHsd~3hmDOIg zNoW@La1p9=OdWvl$l>H%C&L%$gwWhSZ9dQeycnTul3oOuKLb7hh1=nXYxX>x5LJCNy{zz!>Hl#J_8)peak z!dR@uSb##9vg13&Rse08|`e@P1X;w+aV7eY%O!K>Yrtg@pG^XFTColy^1LA zSC<%6gnp1L^8jZ9#AN1&-UX}uLDbi&QX-sDF}xVsvs{&zZE_Ek0k*S7hP$tE{?e5fygjixGLdyhq1}c-p(hYBm^VHMjqGtG&5sleW0edp5DC#e zwZAoi=iz3OG^d{vfXX#AnM+(jeak-)J+uG%y!;hWm$kR3U@Dp_rPPKx5eRQTW}m~i z9HZe2zx03|BFnXYbmEkw8?Gqa!TdfPA@r7t5=WvsqWsb?#O<*)<*wJr)d!8pg>Blt z{pE#$+M-=Yx4nyjb#RWEk?Q&I9}09_`nu0Dl!F~wT>$Ch^OZ*c8As_b{*5RtZ@f3n zmDZkkz46&C>WB|2(@Su4brr9UTk+Xqwg9h>ySxwlxif*W5-2@!?)gKmGRwt9C5h2Lw$Fg_7``5h`X~v1D0o*X! ztL8tYFP=p_?G9FbBmBM-0`vnhKEBfd1PZB+I-Eu)(c=Vk=18L z_Ad=Lr})Et9jz2aqQ^+y8@TG8tXh^-oBEdPj(&^fCGxWbHXBbBT zp&OHx^Z~ErcrTt7<^|gpXVCr*wvU(TxBj%7bXXHL zrWItom=JZuQ~r*uEp+fLX;L-tPc1on*H0Ww+1R1|-*{J>ST)51-@nQwJUta?L*CP4 z+)D0^SDRVH{^r9$wRi~9U_@xittJSj{s~okZa=cKL+TSt&*}{@giHIuB{ejs@{466 z!XKiay_11&q>D~9=#S0VztLc%1?aBxH{E9U(E%M=D#^;nSQ0l1`?A3{$lXR=m(0YS zmAn+E84nEca|8k3uj!IhjzDc3Us?6>PL$`a4;+w{=LOydc}4C$zpbwuK+_W~#d+LR zCKeK%cutMJZ$*?(kGu9597W@H?dn%!+o!8;&(?CmSJb@8W^hyWOiE~v;c z)v7K{eVw|Pz_t{&B1COR)<@|QO`xIJmr#mU_a2Ul!G8jfKr)YN)=;OxLtFUe*7?a! za7vH0Ov5D-UwL_&!W^gi3Tdh{5FL-g5nD|4jHBSYtF=ik4zmPMP|>Ina+}nw zH6CdaUj~gsk(guT?$Mx5AIi?JFn*Ls`f?3e&I4COz?x%V%v?iANkFI*Ob+Y|=b;6E z^;@z2W28t7%}b74-PQO(vIL#rZzgI0@#_8*kViOMDrhHuIVO)DSvkCP-EQj;sj-KS zbdr5+x68ktNY8TcFv6iknP_|K&$Utel>$KY1^F-;zBIoUj)V>!UyZ3o6}B`D6)XGu z+T;>Gdc^_BkMCA*u@h6cJh8>$09>@)1~68c zu-W$>fQ?}?X{7YmJBv^Nu6I{u#dd@R>S$#~5`NdyWIv(LaM%vk3Su@uR97Oh3t4_b zqyce<%>~oN>90B_B9=RcUBow-P!ihkCE!~{id_XV^mVtSa!q!MwWk@E0QTVDgI&a@J#VZ-#Sh$`pUE}vR%T_! ze2zmk6i-`Hj= z7b^wmAfg+nO3;(n!TC7Ll$_(589mC*WI9k`?{$Y8flma()odYsC0EkWC>E8i%5Ve| zF+)dZ;6;^HA?->&a&aUslN`QBcEx|l4t`HyA@{)(Ak%YHglYMy+Ct)EouEv1%|0{km;1#} zY518b9xyj-IOUIkb>wf*3c&nd&c?E&WGH#8~69?kOrDF6iu?Ib=83 zSO1H`#^C=RUSf&&)BI(2mx0$!vgAT9?>=TYq6Kf36s{-xuMbEz-Vw#(PM1xvjSO)S z$1fpLfLfAd9$`#PFGd9AfX~T6w{vNKUde-`dAtatk7t1j;6a03IUoXEKyh2SsMMBJ z-+|~19Lit+;Lbgm177$hg)<+HXq*p*sS=W%v3PfXIfTzomVBzv88JSiat&bmR$|$H z%J_YA6W2~C0ZRcWs+6~kZf7orMgypfEF5*>DM8W#vUw;!o8?<0?_Q$d4ey`?R9eIv zh$NTosD&w^^ankK?L9q|I?SH|mAg`g*~<5XU?ou?sKXf7BAJ%(-&sFXr0$7rU*g<8 zT#QifKEwDBMkDkHUTs4Sh*sJ?T$JAAC+`*wC-Wrs+%K5_S>?*Fp@ zAK1Pei=CXP7HJPpThg`rf5AG~9q8XO-Fy#meB*(IZ;mqIc^3_Rqav~>^k5mL$y+vw zk0Ha%ioEnY&_#L;Q|}a!2+s;|15jolO0_?`FiKk9YY`Il=<&j$5?)mp=OW{Y6XQv* zEzadwj!R^Y(*(9c%Y7qm)<*Mqc~LRim+L4?xDLrEJT|&7Kp^^UEs&zWrgodDb5f6P zLcW?c%A*ezhE8K-In^1@d(S-N%5ntN`}y6g)tmyUG6VV@@>!c8()RKNxjuq#jEkGk zOP3U>tBUK^z5A6}=uQwz-9`paZuXV1sKs%~y=5=9xu=*tml+*oak z?;Rjbi61QYc1gR0VM974=by%~BA8|K8K*VcSG>;W&Ooa2?81yiJrJ4d&@6^5G_3m6y2a-82|(& zKRPKF5#DVl-j=%sc^U`pr}N+)2B#ikXRonV7Ju=ujz8lcpQ;An;$L|fIn-LUXL|%E z9N^^d-!)NJ6MfUO(@)OllKBl7A<%6=2&Rt^=$`gMAG}{Su!DdJj&#`o0xKgw%9NVO z<{aTTo<@X;NyvmclL3*>3mY3Elb#I!|3n>7mu@+FGoDg;kP!Sxz#P-}{W#gQF)Q6N z#e-+s_YInR{2D3vOq#slT_QeB`bC;+Ya;KH5%F1!IcAi?+C-jXX}0tah_!S-YpwPzC_vf_>Gt@W9 z4qgj~9qxZDhrq@(8t*c8q7R9zE@F+FNSrTrx}p`cR8vQvK%Xry)n`^$h`@%A@R$Ew z`pc&Y&E2b%KfwR9cisVVmF52byl2YpPI^!1E%cHQNI*nERFsQi!(J}-tJiv)UP090 z)oU+Ty|(+aU#|^CMZkiJAPGnp3@u~{B&6-8?CkE8_xb&C&Y77_=DhED-^}c6GW%U; zgFELu=Q-!R<>`+KdZtRLe+i@gZ&l?-ZYZ2K@L1mpc~fgI(lDpSDO%q*LkP@ei*0U) zNrdP+0ifLwfq(x<=;!~Z4-`0VanBy#GZ$9hZ{J~F^OBjo?p?hUZ`bfw%~MXvQRv;% zZ1JM!OozeF1_Mf?udn$ecda5OHL1Z&(+!O-faZ%_Xa$&DI`(PwEte-(0=iWXhMUI@se{dUSM0B7reqt(L_~^o> z#~ipY$3MQOm(lGV8t-wve%p^@_>dArrJ?K2Z}P&S5YfQC)Qe#fV^BClE?1?8Z%{I_ zVBg*FZHVqm?>tbI9=f@(LRyhlsl^sq@A@zUM4JotXzgJkY_LW&=T&OoO~Qb#699G< z_ijv#jA%eHIJD<@)dJ6a=2lROPkw0{3+L_WNWTA((DZ-Q0qGunCV z(f(t+>;=Xs&C8!m~;VnV*-UbjRO`d-gVg%y!@gm zR2)+QGXdQ9aFus`VTj;l#r3yUdG$LsaKhpoT8EMU53H@gJb}mHwJ)F5l+OAM6&Qe% z7q|A&FIzH&p3}Bc91#l5YYepyYK)ZM6j4(jXSy}(LNl5~5&Ld5AW65n;zPpBD^;)fTa2FRbIFY(NzLK zVXESrzbf&~&zG=|w}jp5XwVr21^D(4M`2Tx=`;hF4Qz-1`ua3xPTy0*-!h z!qnEY(brGmU6<~JgFBKOW0tsMv%4F`@Qh@-KK9*^YI6Q?7|$^AY-3R_$b_fa zPP&QcTQ1L~wDz47@x_fF1j=hK>su~=9A2${<9xck4uDV0jkOK{9uiwER>!n9ObYQ< zbd>;5(ZGYii~yDdoUtH4?>0m-3jX}pDm*4IC~zY5W?|$WIOBz(w!b@a8Q8K_vt~mT zb_vhBV0zQ*@7Pg+N5czFn{I#fzx?ek{_eb~sqgRU=S+nyJ31i%#5%LlwC+LG+@IXE zksGNRc%5KzVsZta1YQQ@ffB;C>N4KQUbN4ZD|iX;LF%;y?grkiF6DM_MwhWjp3fJ7 zv!lNZ0$)>?@=5);Tb>HtEv%d)$sBHKlp*rF$a9ieE{dZa@%Y193oE~wYU*b#^ zNvw=HD((&Tb`|M8j4yzgB@y!Ac3 z9JsK#4ZeSU-STkIBJ-o)Ii$Jp>6_WHYYNk*q}$L>Jf;W8Fj5tI$3KY8ZUOK}AyTI7 z0gvFJG`s{8qq@jUS;+&y+%bPu{C(+zyfl~L1a&D7yAqX^d;@sRZht<0;T@G$;}@?P*R>*ST3J%?zdp8t`{ET zngw9DD)@-pQCecsCa)wa`8^3Ey2f5s6`(I*#T`J$|3u_M_&0)7(} zq1uBkaH6c_i>|z;#nk08i_u3encVyvAy8ibCZN~xH1j<6F{`T@W!pSv>;86^7S)G+URz0@sO3AP^xe?arMMKe{@wi zQ9i_3j-*%Wgk@8K&V{n>zd7>yI)SMq;)5dX0^S(vy`j9siP27AGwL z7IcjZpq+T|fnfli@#@~Dg?eI~|I{f|KLu}j?Vj}k8ga?HHbNk9Al!O)$%&Vfj_n}| zk#Rb2uOLWZEA{JMLZngOFq!$>79?$sbQXaiv=Vd6YNv&_SP)G&px!B`tsVg;`SzTGe8VJ5&EUtqSlEjy);Wv7_i_Py^vHgV4Y?9ejnSI%>n2UA2h}VkaW#SzmOGOBLHL+ zl!rBg+ck4%r<-L0tXmrjy!Wgran|Y0AJ_HY3AOYE3(bCg6RH~CyYdM>{()iomPSkF zRyge>H|u7;C&QT+X87}(DtUi)+@L_)Oa1y50!pSEc)0+K3yva61FUp(@d&NMhUe+( z%A&5>nNz3S7fm~_;rH#pLE+{J$}O#!MCfAAh~Y{pXppZy(E(762={ zMgRyDLGL{P0Y2o0!nIC4wIgzkLyDs%Yj)~X~R-nI8zQ~ zq`&oq6Hpum{w93!|4rw0ubfT3MXP6Dui~Qfr=w*$-}=7+{{9WyDK3D*w7s9dGY;jD z?dwts0PzOj+6Wl271ok5|D+{a>)BzN$CCnp1zjTmK-ey~e7JDWPaWEn-=i3g|NAdx zZro7`pPB&o16n974JaZkYEt^wh7Fr4JmnP|c<{a|g_G+OJ|4M?k8%s-1(-2a;cUp= z8-xkD_B23ux}pMu&(UwgvYQ+`od&3)P1C@mS9DWj0Av%0Z&ron-M7mEAXN?=uUA!k zLOABQ3_twIJWe{!eNLug6 zpKqQq0GP&|={_b=Lstm^#T~+%UeLn}FPuu%V+Adhgp1xjfXab9A$;=lQ#fEjmY4ng zcECNpVDq*r2b}Xblr)9;V^1F5C0zWbUZxd%RkCq}M6E*261QI+al;t`qI^ROZQ6Aw z}su8F(3EjnA`PrIiDP8jqn~d{B7ai}QTva|@Z`VeWUup$Fyo z#kUXSweQ%#|NW>$;n2Mh0<=RkOS)7)X8}V0Kh}S=RUHK ze9nJD{oqsZ;^%DTIWN!g?zhZiv<&PDgPVtjHNCy*7Koign)yerhxtNbdJ}#g5Z>^W z^i=>m3Kdo5YO%x}sL0Zn9O>($Ii#}ybk+qRZHNIt#oC)mYjR&Sttp;ui~(pniv7Z? z{=SEAes0mY-a?^Q@$D}h!0_l}T=n}hh1uhBbz7>RPlBw7M#vaNYQHoWmtWBT&v|M~eX5D0}PRsPC4qx~=8ar}AhQqU!{Jy*7`_ZvO%=ds`T8 z|M>cJu?2VEH^PVCJ4Ejx%}0HCK={`W_A+z2dnjQ^Yb8+Dn8?XDM1>i2t4}LI@&~*Ypu6duH)W}AafwB!6;%Q4mQ~lRJQDnqYxNqk! z&FBB4NbjuXyMM6)EKwYPOg;VY%ikJ+HB~PEr(Ilm^$sKbO`9vwUmf3OKxrngM=WFQ zS29VTg~NK$K+#zjKwLk)wdT#b+rXr$yYpI)SJ+Q10L2kF=KLHNKW9d#S*hVg&zZps zU!SMAtBpc{YKZ4ZHr2P>N)%nw6(VApA1z)R_D6JHyd6&z6yN;8b}o3vG%A&JV?!xL zr78>zRzcvp-|d1U6hFG9$_=ZFEIy(K2^6PYl;_r;mALWdU7YunVd65il^y18CA467zsL8 z0u=K`rxc<=b<_orMz;@L87@$Y5n`&a#Nu8#1DIn|c-*-k^N6}RRK;an0C9fTX;($Mq#friV#i)j-tazgS z_Y(s+Cg77_*=`g7?p-qq&7%HeRA9l}aQE3ZSY`3W8{s%bMnNG9#Yd{Fx^;+ipWOT! zuex+695c=WfJ$XPcGVv0#`xs(luulNk=Kb5I%L;iyv(Mie(zqIx{5MD47^Nq7eGme zu``8SsQo|bc{!FZwFls89aivcnRP=Tn@uLS^u#`nJ3q^Pk5(zP$Hk|mo?_Pw?(>Z? zE`X$qdc^=(-Cz@ACQoQ|l>h*MQ*Rwb?~>40U)&+^kiY{Hmh0Rd_k`p7S8K}r{w=#W z=Sj^s=s(|C-o3;77-Xj@zIE*|^JZ@1vp*hTbgQN?wPAJ~toZE>#ijzl^*0W|{Z$G} z$5}s!H);EZIfcU=53z+-UjL%7+8`V_s^YPkvouDp& z;wZ2Io_%4KU)^4T$Awq@Yax|o>03V><~rMp^MOPBfLBW?)L^OJXCMAI6#%K1GYV3|%Dv6z>wg|KnL4v$%ImIp1ao z0>uLljq>74w!sW|?;Z3(Kd37+9!-8btr0-_tNF$sb}IG98Y zT_XTgG*AtA@pEUS_8TubzK4UB?qL0I%KYQ&XLI;L<_N+A7i4+)TYLH57e;vRhd1%r zl?(PbCe5|;0p#Z-9H@Z7vEy0<-v6JQ;1NyX;0~x-ad92fgj2ZJ>dFDisB`cXDQ=b*XqnWMj&lF!WtWK{Jt8nSc&};(Fm(&&=_IFE3(hA>fNwjlwkG z;DEVv(nSg@1FY;M;{(G2XhYrNd>XE0D@@4b-A>NvbX>|gxM4>03UZn z*P&|!00GEBr7B?qpR}kQnrc4SAz3LP zo1vnGT<$wiQet+XU&N*un zuYAz}Bi|KX_wre_?@v88%kOV2fv?~2L3rbTgw05{@6^2Y-=E;S|25jV%^&KK;H@?T zO9SO52tYeiwZs5O{Hv22R*L88q5r4r1>mv3Tm8m<0gG)xu_zpSQigASF5LV9b`_(& zqM|tUaK)6AbMsBFp3N&Rp4pP2q7)Q|h52*L=_g9T;ZM)7_R$JCw@%?{APpsUn|w=v ztG`}FL4I}%k+WMNFbE`4Gv-aggzgdmJS66La=WCPJ@rXb_~ozX@ORJH%IjY-$e&h^ z@E`v^k45vdJoBtxK6UvJB$@aP5P&M2d{Ticuin86UO52Mq_dkpq|*YGyB4Sdv{J^i z+?&@ZL>poNqMioI*|N}L*m^4{4}ZN<=AI48VapRkAKX~j9%KdwVBdHeNF0tWsVxJnaD~#T&L;x!9)=`CpPxCLO_Yji~=Y* zn*V{FBjG5jIGThnc=~j1ST!Gx2>8YiMmgZX_59Cww{h%o1?HcX2}_MSLIL@uikH1@ z6EFC?ZE!FYJOqiBqfuiu{kTf`8f#5_4-0_tr{Q{?LmXLe=F>}CH_`Q^X)ehc)0_Z{ zarHsrb6=arp}WQMG!_SQt}8Ll5a5nSG;OIkcU-y?HtkGDuorrd4eJ0r@OYK;&fenA)B@-`AhKg0rzmT9 zbU@>1h!}%{*IQGF9^K`CRRaPqb`6>YJ!S1L6goA6}EQ3$}g=FTkxP~ zsn?dGx?OnNKld?z?w+c1CZph#EL1eB9}sr#s?sydu3~TBQDt~U60HVWVAk{uy)A6a zRjCS4CD)FYpY}9BP2kZD5bL<|vAG#}Rm!7pK^J1b5&%%Fu^m!4P{D!jqzieV1-|w+ zMDC`B>N%vpErdf;y-_m|`-ax!B=hlFkc{hcBvJ67fmiKr2yZPyBdcvubm!0#TqyNp zTxjkal>i?RUUcDwU4&3FR)kwI;G*a15NB3yn75leS=RrcOs0s36K3s02sPi{l2ht9h2`p0jhUZ zd+63CTk>}7A>&KkL-Tjof!?z zqZI#l)lhoknNVz8$P5)%so%7PbnFK)42frEJQXJfTM`06k5!R$D|7&1@=1upsM*~Mn?Y` z)%q$K`z{pZ4*DOU*e3GUj$bP9i1dv&j-3BNUF6b#V zm*}=_D(}&pt8cpEyI&Y#_3dVPc}kyR?yP{hGn;?r%?ilpTC)zVzP-q2uNbCxb{p3< zo9oHPS8sDW#beHKjM&^x(S1zD09df!2><}aRq7oeLz6-PK!EftemAhAjTf}V8yGqzzKC%MO$DJ;OieR*ON5{!1Xb!Xr7usldxdF6aawXYVOcyOcsJt z&~xywhgYdjcbXHgSJA20*ZE-+YU+l?&0|{8@d}?8-;*%dJa_L7mjr%Qp`P&2h>sxowH(t%Y(h2cCk0DJOpc7q<#RU+L zyGaW(yR?h5uMe!LZ!4{Q6ISI|3i`7}T6Q-{2qLy#bFLam zyo4pt8*{x*K}9{(4L{zfgzd#u z>J^ed@=)@Jd`+a>up+2^^b>KPKj2Q4Kjh-#t+OQzS z{MtU9^&U!~ES3)+7l1OGO{3DO?K1Ip423+Dt}nBFXY-f(nwQMv$fso}TB>9e=7y5l z)1JJUb6@-zH~*zQWi@sU3s>J>0edy#)gFi7zz@@EEX8DgH8av-&NNvb|xO(3rNCe2BZ5Poq`F zpd0v2@;KD3>3NZ9UD4rouwr|jKArpO@1Xg4VI@H4-U?!W51n6(vx=*(*}O=lIWeOy}$~r!aFyrX^{5Q~MOp zIW5P}Zz{1@Z9`oVZ`MiUDh#D^9v>=AH$eAwHxyQrd38?_?m~2(0MLlyD)krOFP-LN zpNB*}zYUXIE00NT(eSlGGOoJ%o<8qAA<)qvz?cgYkM1@5LYVH|$7~uP%Cu>O=xhuC zz;TLKU%rDETsVU%eS0!<%#l6(?3&qJ^sKGuC4q4x4}d}+ury%Jqg9@F!8TwUFMo5M zWvBFT(s6l?I5-DEKu=FV5S8u|5qf(9=FGGkUf=N2sr>vKTU*kBG&Ry_`R*2g_$)I` z_uDk{DtJT|bRW7}0GLFJppd{e$vuqAT0@|cE6D|+l zJ_~U=b8Dyde+pSB{!R0(?{DKDE}7HxnCG59ov(bS${Sxf2qy*2Se0IsWU4xg36Qa$^6p&YKQX2Plq4*BBqzQvH>s#h-6o0A)q7U#O;h8d(Ls1;WjK3NTxRJq8XIm@6YGX&amZtQ_s4^Y6`gWfc;K#uaIa9TxZS5WY_UA`oc{EP1w zNwfsERl#RJ4FpjJ5mn=%sHa(ICc?5|Q}<~uXv0< z+*_frG~lOyDDl&SALBhM`}q5p&El9N$GQd1SXSWPHH$d@yeBBnp?>Z5!0rNY7v{nu zVRjdQBh}jcXzm`2=}y9kNdaIIdk+C+E4}HHS{H8|NsEs1*syGytDscq0_fbP6?$5Y z*Y_y|q*P=DNIt~N1(4DLFmZ%{>5BXQT;aV}Y~s@^7Z{J7F)iTfPo2iopE_-?ud4+< z`{gbC;o35VBUO!;@)G#ySBClMWkonE%U9l5;Lt;Rm^&u}0zsg7(X(>==*BX+-o0jH zZ!sa>qh`{7-3m+!0F!71#A*i?ug%4r1)w35@zNbk+JBS&aN`cCnK;eII6(`NHH#MR z?w;~tQ1;mTcSj*l7;gZKyBsQr)<`Cy7KBnBSpfC8g2eIRQzHavDBIl}zKOD3 zg{~F=MUIE4i>L{Fs1zZOAKD%1W+Qm+@TuGR$Yl z6D~K9dAA)=dOmCW%N%=TYbAiH4(s{9{}bD|`r9QqIiQf4fDfS& z3LON=nzPhhed!Sk+lKKGfOaj~qZJZJ)Z3!z0k~%@bRoLdjUp@gABZ;p+8?1e3tIS! ztmK8xG(W_$s_^qnKz@&5sklI{;5S~pUcnz~n|}kZ3i_&=KgtI<%7<>t(5P7^{P=EI z#%_RiGkH_0*3N~{>1IITZ`+S6R3um=y(3? z_sT)ef7yCYJ7ptRuL^_x+8)8508tLl$UfrS$6Y-KK ze27yknoF`trY<2s@kQWgK>93P#s3D7m_;sdj;!Rl>N2iM&Fe~DBf=>vfo7lx$ntgI zsj2yFHR7N(TQxXGXoUoEo_W&)a4+|%5Ur3*a$uoQ#bw!SJyg`~uhozgbbYq}bPH|E z??=JF-VhyGBYffu)5xaSoHuW)vT;*|;gKjVRY9d94DG5gFi_#=e<<Afaj@jI41eJbXhZB zKEw&?{oI+7>Y*jg5%~<&q=Bb}Afvb#(hC5<%e6qpFo8rk4VVLLb)efi#AbT)gmW&; z5ri97e1$!+HxhmluaYJPzs*pQIo_5!<+d0%7Wzp#&nwLf3$N0qbgX>v{>Mjoz1O+L z5-?A3$;)RMkGt#M5#G6SE7yO2l;%Ep71~h`2MUVbC8{O;09u90%__YB5Np(S7l6~a z6MJx}d!0}fS_cg^CUlY_Hie3Dg0AQcK9vf<$>XZ1I!ULR5l7V4+pAafTnq|v_(Lk(efPR!a^w0)w zUU_d}>1%NfmMdxCubLZgS-|X>$?E@iemuylUNi`cp|EscEGwfKg$Gj~9-Hfv-e~== zP7hq!TnDx)0nK?a3$r>(5}t$+-6;SFI`aP|4!cO}c9L{87&T!DgZJEm09DPRkQlYT z7j0ZygT@k~rejN%KPc=co4V{UA2vI$muVxpou1%Rj^iZM4e+^n4jP%U*Ud9iahOIZS1Bn}wsBw`*7 zQoeQ%Ac}+ak_`ZYWcDT~DTyvX*@No$RA zF6&(1h;1ptuJc#~gA2f?NLUx5I|YFHfHYbF+_tNRO)y!z^wHFfdDg1)MQiFXrTTdI z>uWB6gmZmp5^L7xw4{mfRPF769q&KtozU8Lz@4=KAe#SCQh6)Nu0o0c2P+15Xuki` zuo2Jjh_L3tGQYWLl-sVU!lBT&BtUik{qG&5jA*{%bWXfJnXF^3{`zzg`R4;@REpnY^9K}$F(lXz7PzdM82A7|47J^XyeoP0rW ztpu1xV};Hp$ktQmOny^mty;z7J?TvC^*rtAVQsVk46jzV70!^qFxtP&HeHp1zG?89 zHxI%<^v42Wl+83n-_p(|f3yw?)pO*ZimQ~53m|!ej{{^~R-5`dYTB%wt%C$n4p4GE zk4Xza3UsFcAhqgK$u0nJ-&9q&S*v8a&IKTaaFvl8AxV~`(mUpi#-|sc5yaV)~(o9&Tpm1o^s&~JIrmA^|^J(>s zIMLnc?E)~&Q?Kz6y8x2Lt6z0N7btY`ndoSKH4XuMH={NzSe3I>by*%j&8L8 zq%&K(4Rp}fh1q9;wer%gN^7YRNBXuna$bWu5VKk{NxGn#%TPg6A+(%mgg|G=k7}}` z>KVDM)qL-zYkzSL%jhd)IS81hV7|a?f$0kRq%yb=6=*~hl_w5Fbi!Oxn*0IlKop__ zm7Td!U|1=3C>Ri715iYGRBJYjJVHM`ADcn--(t5?Y?6&_A+eh4rL7AFEzUE_JFr+Q zysT-CD6Ud3E}XWtpX#pE83Fqr6l7*TTwJ9t@g!h&VTPSD-8R+On$=nt0Fy3&)}T8D zfD}Q12A_L#1Y?5yng`MN6ugK_UM3eUQW7?C=^WDOdCu;SK$~3vqR^`9L^H#*Qx@I2 z@th;Jj#OrC8y;Wc(ycH2pyOVGA#EA!g39q}bFqOLyZY!=85BpzyuNvys}xTXT|HZk-hV2=6e#uAP7F8nEJ8Mvs1mqQu~VUgPqV(GCasDA zqK4JLNPZE)N+71*m3a#SssH97~j{3Se z!*t%bEyV-BRgVL63oGPmN{>97{n`?&01f#=uPUxm7v1%jy(Sp1Gf5UqKch3S=IrUU0wz!^a)ozf+Fx-#KdmQ~E!zN;Os>n1V@(5!2=>p|7H|P>2MGB&$Ut z@y1pcaR5}%GgV6c=L6>lu(G^kw8C<^chL-P-&|6&XXn|GQ;ZNU=An2M!>75DG(1w% zQRAF-QCy`iC@h!fBgIcr*>ZTy#{PlmmE{)QkIKEXxJv!1MP!ZdQi#g>x+Bf_aDy!d zz|Y#+b?8n3AcY0M*IzFfOH}$8!Zqgel=w`bR>QI|NH|?xP{~#(0Tf}%O~rpY;ei1; zaqCEMcvYkal|fNuL0x#LLB>8GV;2O&>NfVTD(J~ldi3PJp)DsruNS$mB5vSa5Z0Rh$OL{MIcbh zs3kXLRKjz#G?zw{l2H40qS|68OE3vUs!y7Gj~urWXB7sF=uGd(NPf|#(W#4&L+H(v zhGyhPx6RHD4$kh~;8vAAg^5=>JUWZ3)z5*S1I&7wEGrFhoWRlS8zy1}pcMD_75eYn z^1Gwl)BDoH=mJ2DF*nVjYTW>nl`VJIsHAcMbb3|X2hg1YKnfRtCBUS~2{lu_*#+QX zRlp!Yi;5Q5iI4?gn#3uj{J!O~L@8cc(xWe%TfX~{d$(l>_E>QmEY{*)W419_IN<<~ zx}>c|7^7eoZp7RQCE=W4%)Q4AF^ZL*DaC5My|Fd&@h3p>|&&oeRJ@4KPK$4)^h)ENhOBJF5%PodUqvq83eI z@w+!Up;oyrS*Q>Xw}NG$t2hRr!%M6!kS9pqSLYU1$UjJV>swTL?Lx?eo`PHksnrfj z*a+SCz}ibqj)I#AjzdM*9?c|(xJG@}&0bCE3ZOJqoyo$6r%) zxGHzE`pI^9WOr6=3w)qDE>le+4bo%*xK6p4o~`DJ`?S*{1c^&P+sjYwALvd2fCgXIHG!wxemChxQeT(Dl&>P8l6PkS(u(gXJD}a_|Ay7WC#(tK21ffCW0(K!O@)p zfY_U@h`Xd47Y|Bw0r++U#4JIFds!!kdY!{Wb6l@5%>7K`bh)%oZ~I3|>lO%#Feq!! z(^SKMC2xeScLAvAMULfR{9$xc;47pUEC5EDQa>W->!sBHT;RE!ApCFtzwJDE_78_= z=JH!+XYU{A%jhBM83sY^#L1<2;=^JA5b;usBv95gKlkw=lf3|1f$kIl(pmr<15>nR zP}+iF5*P`iw-U~iS~744kz@hTXhx|#5ga$?=bN7S?C(By;U_7tN2!=ylT|R(a?LJ) z)(Ze}XmLm7DIrxFp!1q3;l6CT_oVs=jYrj>auh4sY1>9;PTM?Em=ANv29?sxTzTu9 z{J`b~J!`iHP|2!V>E%gGWLPW!5!|PS-SX+ItxJGbSkeF|V*sq^P5~fA763TzEmBK& zGG%CaRRT2f8veE8#x6bf|7c2-AK|%+u72W_XRhB_U6u*TsG7E)j@eKbH@pPTd!7iX zes;8;NBiJ&zh)>&*WYPH=ki-dbBnf&78b49c_2MOsaOa$57G1RlYwhNZS!D#661qd zK}i}WKDI`)3p0dIspz;snn{b;7qUD$itZEu(hMG3;L7d@Kq^sr%GOY&)$6J_c(b*sn93)AY6yyy5cHzz2n6u(N0Ls_?4_|_{1>T z&GBBbQC1hgWZa1rT`K_CTma%Km8z7bfITE=rIID+sI+Tm>zxMBt0+@JzPau4i~hE? z{Pd9BMz&l{_3=5T^L%gFMz_-fKvb*1z(?Sk1{eoF8r&v9EO5C5RT8^R48n>;-ZYiT zQ|dnzI2H7J3QHb>{9(7Cf?tT{%Hdno-41u;1zE`{z$GY72Ko_xpf2O;y(F8HmAoA| zk5G=fUR}zUz^PeNGhWX{@fw7~QQQT5Q(elfd&z4u0z@>T5@bu!wKXg~LYsln93OaC z0D=Img&>Jnr6)5_(x5v9fD}Q1xK{$Ko4S(E@~Q+VmLNb!dacLf6tBzw%SnIOkbh}a z=naQeJ(v!_R@rWMgzJV)BaJtn^p8NJteS3T!-u8*+r)&&;%4>ndl-0YG>!UQBeM+x zp~^sc{prBz3iZLhQ~p*`y~q4f!PNr4F0N94gVx0k$V$GXg*OL{HJ}u)kt?`CUCJ|o z5oeko;;fQzJ&IhzpZ~7(K|YcTxKLfjbzZz)!F9khA|3&rQq}yUT*24WrM$t1rv0KA z1C9e9tvW2{ZL|RB*x=wcR zZE+DarVLYF`ai|!!*>^!ZF{V+LVi|QA^$J~j&NdZ z_p5w}OMy45IH-2)=WJQY4?M`u8?NxDTtIGjn%RI{smitTX%2AabtOL$;TgNr%qT>J z*UL&i?7?e_3!8tmO}%Lb4S|Jj5QjNC($(=QolLXy%FzTzHwyr!%|HA&1_2VfW)tZt zPuHz7@fcsP12NWxf>}Pa*ZSBSnvTOchzq&3lGiRoce()5rn~s;S*Aijez~df8f(e| z825I-g)`)Pj6U)*f*ug5ZAwWH<({`5Z*obSlJ;=))wTR2UM&vGaX{?QqqML`hHNqMsZE?W_2w@+lo*iPW?@Q1~M?$k)c8@t}=;X zMGw#ejHaTf$cts86#)VccjPqv-5IcrpgSf^HQ6 z8eITQpdpE!aA9{0|;;vn{11XvTIZKl9Zk0gSTllf2Rkq8b(m*xH1(Y5=7C08`-;V;*jDLRfPN;)940JI(!7eL?{B4cY5rcG`K!#OEj z0FwfM1>GqCG{%c29pY;2sx}1cC7kCK1gI=p2DNU0Z9`#&TurI}*-3T#!^@{Itvm8q zr5{w5gim9|UMR#`SK}1^G=102Xv>oJ6BmogA_6 zGRah=;j3iYUTJ_brf5;KOa-aAcUw$&FvS1>AOJ~3K~#*RUh&j}`c8d(8>Rkdo90ru zo;FXXt9?AKANQ+Irs=q0shCd;>3{WqJ81<-l?Es{7VSw9I_;#bOO%FU=5^d7RQKR= ztl$^Btg2*!lFsKwbYBk{)UN86F9LaE-*P!5CE~q{Dhx*jgI2AxkAtaLeNy&GC;3}oKap-2Xw#jKm7l4G7 z>YV30wspd1BTj$qL;+I*E}x?G$h!!7NV_GZFy5MN}J46ErtL}#UfAux_F?J!jwaiQKpK+>Wfsmo29s4e z5xer@2Z^+Gi@fhdGcE;eUTa;$FT+ZJHnZf!A9fu_cM1S$q-@uS{R(IzZNL;2?2!#; zvK*+m?|N&7dS79MTtn&6r?}LG4YfYpSc||AQCojDW?tL^LdmpQwPvW)QOOOz`?b z9I1QFU1jHW`B9dD{P^RLPX2m1cAT^#?3oGYQ z6spgLe<}P;MgksTTUSZxZNy{304;{vnuDfKZvh_y!1C{E%8yVdF5!x@(ZP^Uq2PEA z)x0+MnpT)cSqfh16{XPc1er{**(At*N@Fmwhyx}A=A{3*XFy>B>_4QaVyg&Y$MJ)` zRxryKVV3ePl~T0(RQjhMsWhL@@U~i$3m=mylV=cA#UhahRGS4j7I7&!;93!vpvU3u)bJciO4E?uVQriZ4+gRJgRK}ue4@~$MMH%%`(NXfYN9qo#>&|uQtuXfjWQI_FhgQfp2F)kHh>NYPz11UYe zHC@GCBf@>RGr^$kX0TK25u<{)$w*8#P*6!o2fZ%vXR9V|*tbpejSvjtrENx;FXs2HFHMn1V+e(c-hNkGCa!;H}I! zb7chTnwI*kV-U!|u^d_e9ZNNGW2dNK?{{nTfUVn{A?r=Xa%XB7X85Lzg1L&Q@XHgD zpb0tR-VS`cUFSz6i%yp2@qSDmr7r)s`IdVnNtG4%32Rl|lx12a`8;8wet^z-8C`+t zHet-z=)E(gu2W^OkStqGR&*J_iz>T@*;0=p{;X}Q27>CjC?h3?$Hw4wseb+jVdA+$ z*HSIszZNM=M1|}LAO41rMzgI0%MBr)`veflbTM;3&dfU={;SQn_^%Kg*i<|#e&iPJnQ_yi*A3Bg;z-i%Gjyr!YtUi{}pk&l;=9G@_flk`w<(IcWxzvPac#)>8 zWg2DXh?yJS<<{;Z*<(GJn?GF5zye5brZlh$G^P=o$u1jmM^H^4#ie5f>Uf+#MsOcq zMu5|v6a^yQ=UgwCmVmnnVurc6^dkvsRMvROuv6oZ{Cs72vBb1|@Mx=Blg=m;RZ`&epO!3pe6 znq1&ywBqQ6D*Z&2`U;F8qGuUcMwK*ZM%!0{9gT0_tLhqlvED>kSx>45$SKK7#yiYa3n3 z{|iNNh^0Ca$~1hk=VZe4&IpD`eB6?9#LJCmh(VCZoN*2_dJkiin5)Z*47$awDI<4z z!FVo_so~>eZsGq(%PkzbnHGS~d$8|5I-L!|8qc@B&zJb>sKQZZ;VkEy95pHPjk=2n zHw8&xGo>S6mJ1{50vSz3pD#n}3iQ{r<9$}k?c|M9(#d+PG^~ot^&n(W0ZGme%hBT$ zjbLYJ^aG3chT`CC=YQPUk-uw&m)%MLKtKn;*&0ODH`%4RUcwslT(R8N2>5_``X`K! zc(dv5!nWF&Z#=EKJJ2^Tyv#S5;V|GGL>_K}>1cvH6zd=tlRN~3Y3r5(ZWLS` z50e3T3WkovffNI;zc3pSU?kDsR?I0PziISEYz}?z`j+)Udg}BcuEkwGc)KORr++=u zeA$#^hO49cwB7ZX3m7snsoA}SrY(&&MABUZ+w@R5Mq-DH0!3736PBn2{I9jGWJ#{4 zU+Qj3h3K3xbJ=va50aegmmp^O){Vstgo=O!8i|UuGz{C7*Iyw>`~0T|R}Qcs|AHiQ z{BkVm#Ojt6@8$j0Ysk4(Kh%#4H90OZVLh|9#k?bssRh9cs;akP{+G~8Lj!3My9JE& zoh?RKk=mNI#i%Wk1xw4KzkT0eSdr6ZDGVu5rxw~{k@Fpm&%7ADsydo18&ZpuQpAUgb!n$2=3tZp zx4f(3`M=5wv#2d?7cv4xmixE35R@$Gz!Xoe3fLz0Xn5p;oOX>=duE9H|!nn9|K-%B1sw z&7yo9V+t%274_*oOd$Z!Zbc;#C}+!+j#G&HGS5_<8>cQZb%&S2;A3%6xpU0wRT5B% zU3I2UI@{1Q?}~3u|Hu85-sn0}9B@(VlR7?QHxOj?L&hFeSYQ)|J-0hcg5^%JEuk<$ z)h9jgU!l&6a+i!je~;2GHk95H{65p&Yiz_Lg_j?cZoqX3-y1tLJkt4^s-MRt4pEw5 z*_BIePE!uR;`1j4fGg}-K9&MqAs_>#Y&mv2#aVB&1p{%Kq(ZTxDaYk8dAb)^7d3i9 zN&59yGalgASEot_M+rT?X1mfqMKh0ct@Z`1Er1_0fZ1jMuaKa!CLr^Ptkj}_69$ie z+Y_P+^JQDQ3l*cK{XQPCd-GR<*__mlk}FAMsjc4k<_(^`wCOfgdn@gmHj(lP3;i6P z7E2~dE3FnTTxgbU1^mKIoJL?KfHM$>CDub7fSnj?fI_wSanlS!E?jBkU!7Nsj0wRe ziX{?DWc7E_C%+>+&40Hs5E34JvQ9CjQ_b?oF%za`Uyze%9OO?wg50a{Oy}oE`;MP8 zD4Z3|ZgaMM((BuojtU-Q22{?5~gi0gh zYW?wa5SL(m+{fK(6V;;TT-Jj|V{_ZLQMUccPOqM+CO4&`8SUK9T70vV-_RlvZ_D?k z)bsv(DNh1qNdv97k1C6=w_9Hi6^mB&d40BY1CTNqO>Si%xH*V$RbL4cTtovdu;6etBKUw8eQIl38X9U`5(AqG9~oS#?VoVWjhZ3q&>9hdp=8(B9cf+YpPRf ztMyKSdu{&8VYH#dz@n*2UuzN|#tv$rc??=qOYc7I!(-6zax(Gg_;UQm65q}OCA!u@ z)JCuYv5JSOcAt6>mtigQQyDxoT2MX$g`0oqD?bZ{h4m%*cR^kvczmP=y?ciVs%sz3wA3l7Hf?oY=TZJr0c&A&?A&Y z{5vPR0M`y^ETX-?Tve)YnVtXkFts{vQSR7q+M8hM!XBsr7uH^>XZ$8BSbQU@+H9WA z0BGoypnX5N^Ccx1)AfdG2x0GLu;AaNe|sCOG>+G_FB$pwBEznZzL9^1C^fBVD!N!| zc_k11u^#r{cUDRSRTC%SLR16tRM@?eN4E$%PB8}JqLAoR>r*v85(1sUSXYJu{XmC) z=mS4f0oA88!|wqV;ij8 z{ue7CY*u2<4ccydDB6|3_a5ETgjFi@rS1Ni98BERI*W&Ku=kl7wW!H$@J(yw4V7E@ z0NB1Mc((q>+t*p{hkn6#0yJMaKgNzmb)W+~*&&Ie=^+bA9=mahKJKT?8&A*3xdbO4 zAV`6xCaU=^Ji_svGR+0tSyViT-6c(;Fb}uc9pw^xAU78r1wc$eClzo`Tj<&?PMtqB zvJJYh?A#+JRq2w(`S*``*_W}@3AG2a#$&T$fXBP%CB)&+sXQd>E(IsXK?rr2EeOrq z1o=}Nu=qa^Z>bA7bK;Vv-P}!oH6I$p*L`9t<-go)N}IyY!l*A7XZ(nVbVm=6T`iuc ze{L4Gsxs96`cnfQaxtii%I)RR!4cYskJe|7rlPj+A{_a{|xIIf~ELE2O!mAwOlYYsxWnmI|j_&Xfg6uD2iqL2Ry^R zl(nFreU-#AjG%p?J3)$Ou_YNc_}ryw+HfQlj6&HI4;$+f3L=hZlZXgZmIG-U!> zSwn-bNw0%+{H=K-!Lq7f$P$1_PF3*u>t5Cc?Z?Z116_dzT$f%+FpGW8b%Pe^CwQar z{t@esRwjX#`NK|9)H@Z(a z2yixwtFoQ5rG+=Ks>CFn)ygUSGAflmE(~~utoqzNC)>Pos31VX*R!GG*C1Smp}K7o zdzPXfCh~CqT!6pS?yZq3MZ;4Ny7yl%7%Jz; z@c<%Xvz>v*kfg_G>}ehAFa!gxR>=iqAToXxtDC+3iU)j5mijv9SmLZ(Rf53?M|T6P z|3)fg^mGwcWdo?h`#YVOo9ml#RBQ~RVJsq3!N2Z11%a>>FxR39o3Xo$`79vr_R%ll z&{H8YA0VqGr-i0&@R|nv%5>&*(1(n(Txd*Ez|m5o?(nxqfXEY>-;{^{t?IhP$(?cW z+4b{(YC^v1C~yHO;u1mlkAhKv)2*0$+U*=L6%*zIVCGUpTyaZlS(=rTsEDPpYUGuP znfAuO2<1e8-HJgrUZnyN86}>=`WuBmkDoSW|I_;P%pv)oWa?XOjgU>Aiuo$>@eSLS zOf`T2jrf3=@)wFcgFGzP_wPaewbw3&K|dmADBp~hpUKPX&1AJRV4hSJd4I@h8t&HX^O{&382WC9yv4RtP6)|yGHp?p8`zygM zdrW;lw#5omkVo?zB1Nz28v3zH$VVhK?Wwz+FGiQX53qRUvt4US>*%chWAxSm zSZ)W*Xye}61KEq3pVHZGWoZO{X-J8kt6azl@M_U!k~h(9S)q3k=oP6c%k!+({6`;! z;{zO`5F*M^A!2C4FjglBI=kDHxFEnFyPY&{LQFEtgFZTduz7SKJ$toK;?tJ4_y@tD zBm*^K2ls9+e+W_H!N6oz&xkMH0zdmm`qNkU z17AFkXRYyz_p#%mpA<@Smsrou>l-!S@iOHu8%c`NrSsV=V7M)Q{l;VrJAY!|YLS4g zEdZnRL!B>&if%v+caG{BV^*~K$lDx7`_}lQy&&bqlp*3~Efkn4LPk zwF|eM4F~^f0rMgLzT;g#?(py_ujpp98YHop3wJCVgcSZ zFPfy*q{{!kSJt}-E0=+nBEbP*O2^K!*$y40Fm+P<$V#?I#(TR{&NXwDiZkY**KW*`+7fKr=erdk}c7{ba%j6 zc&XT!j*tYY55rjh&R5WiE4D@x^<;V!g6jzcGeWp$roC=*an1rkL_n|)r~~i; zIdkBs99}4C>v^xO40g{p0Jxp!t;H~{B zLJfK#J@GmVK!hH!?qd-{c%SXu0h8Lm`t91gR0WK_I5PmEtwrXU>wIoL->M+jD$Nd6 z`-TL%rADZUnM`zHPJ0#YM$b83GPS2<1RKE)U;Q8HW$B?~waZ_04NFqueyhpPJ#3_Z z9EY^Qtk`v3UA(&GycOYBRjhv~l%?-c{!PoXDCdxx#HZ+d0^@btn^lr`M*^MT$KGpT z`P?<3bv;QUg4+z=FmpO-Mz#yEh&(L(JNCb#pqqWYM+VKkoKdF-RHLGB+D$mzxPAqs!9eQVTZ|9moBkvQFo%ynoibNJ;rMR5f!um0oY zaWrL^lpNZStpGgt7lr%EmS%Ix{8J!GQ%Q0}h(sa-DSi$Z@NHC4PM6HT6+q8Q_bAV2 zGEiOiIeGRq3z^Q_&^AM(LmIe(3;j(C^l{Y?Nk$i90X$Bgex_(|WQI>=0KNNpo--!h z>7h;7nTpt&)-bo3&-L45Rubi%U}4fkEwt89|1V)0?yeVx)@BeClmF!$?e2vmir_&a-xyD%)fk18O?Z_+koPzju=5n6QPtK9L9g@pa9~9w5)EkGS zd%wO^;KQ1e;aw5vc&0xt!!=^)ME~1xRN3Bk3nEJs((fuz?p3<CPxD8i!&@1&beY1E=gJhHhDecT}U< z6NaDAFoB+Q*j*LsetG+UN0ed+l$b^(jIkkIi~t5My=inMX*}VU`@}__D0!g;>X;i)}M3b-Y1h&!)ggG&GRDs!4X$v%En+<0$ z9NUYaP=XIABT-If|Ha9Ad#})p1Q&^+iUt7wc6?F1oVi-_l5VY;tQyugcOBmN*4(K9 zwfHiKYEIi$gIVjwyOroa1@7yWDTl*Crhq{Q{W-yHJ+fJl7+raj zAI{%4^=pywWz*u|1>#|=g>4@49-N@-+c{LNYsNVIGI|nk9!-U}vj7r~k^*MvUWpC= zJIbg>2rGnA`M1@g!sOwsb-ZoM!M(}l;XVkQIa-;xc=C$}AFy6P$fl-ad+M^D4Sw)S zbo7E(L6WQXmY?I);j!DZWw`*D$RFF=KMn3uvNiv|7J%tBmI5o--u+@05k*Ng!~Ub7 zpu_jRLt$;n_uvl{M;mL^iWyDEyCBTZI8Hyx|IE_xPV!{xeHlNqT>DkJ`>!%f1e@j|Q$#=|}YX-sN7|Mgw=m|Q98Ubu0! z7mhvpWrvKKT=ng;r0XDbFiMylUWAX%Rl`B4f z_q4~hiR&5`28DuHfbT5 zzQY58d*;!R!X>-OWNb>yxb zN)}>;-l54`P@PDZ+96^y(sM@w@fyO!TQ^Kit`J|7v(E@mJo=3jsWP6bh+d7a=Eny_ zdKULnogTrTXl9d<_7*ffC-#{N+`YS;EA>;H!<}E*>1_mx0q2j8HsCN?WXtJg!t(MJ zWMD@JZhdLiH?kxM878(PLSbM-#?`-f8ttuXMt5iYZ~`8Be+(Qpq;b=_v`wTbr2bTB zXHap$(VIFDzu*HjA-LOSS*Rrcjs;SVOfHq1?@af?2)`WHGd^y+{L%y<7TgCu=z5{L zKF^=T-)VcSXZR@Dl+P3YE&(antQ?n7sW}j)>s~3FEdH2fdBC7Pj+An9^3KHv9NJA_ zxudSf37h8TAM+6MC$EA9bv`YhTH{QgQEq{1UtZ$5Iw^aJH%?6)pUAYkx$6~%s@ zj^JI3`^O-c_`gIVon*1ey65`1ZYRmqb@bX6@^*M*sd!uaO)~k$@#50fskgV7vn9c) z0&8dZ*wM%oh9c}*|CXw)0!}}Le!IcZ1U)c22b0_7hVx}7;-B38aSG!#c*|SdMU5T) z=IDV+$E4ZCG5LGkk+ zhOu1-@%O2NMF1HgdFoMi0ZofrGGSuu4L8>p%iysZuD0Ej_|g6pYY~rC2!K1;ph-DW z02P>~d!=r&m}SmnFdx(*_v zL*hYol-K_8YR#bY)Iz{$9v0q*zTWlr>ZiB<983%qadrs(a6mAYLu~fAiwye!798lG z(hw7{{2Lp$|HR||a_!D{eZxpBv8i_pSfgsQLk=Oc0cVE#(JJ>6IVcREtQ7aXpj=Ui z_$tNAXrG)V{kzpJWQ!N=iEbF)gOW&Ev7$|jm3J%olt48h==Fdhr7{1Nj-nc#mIIt5 z>jKp9o_0@XVywO9d4B-FJS;KK+1LujCRDZ5%cUckpL5+8n9e5@pN zt9u% z7E*a$TCuy6GXj!DxXq@f5s6O?%_jSO&^;ts83?}pX2$*GzSxd0)WIk2DBB}wC>Y2K zUk3sEK?XQtS4J&!Dii=mEE^1E;CC(yOunSEX?=PkV}RgB@q$#P(>Mf!LAprVLwz91 z!|4+~>zku=%JW<|>t$7LBJ4WkPJm*~Lj7v^ndG@XRlRW{Y(rua3c39feI#I0dfce1 z%>E)M*lC~mM6XlbnetLuT*NLTC>$d!yu1DBzI`Yc%}c+v0~SYv4oY?0ts;9(Mx8vw z5Db$T&|1&bZ9@w5I_Q>{T z95uM18DghsU2 z|NLI8t`ERitdw&Y6o7la5SoP++Q-ur^UgI&g#0UgK77{0NYP(P-`WFBb@Lq$G&$2*(*tUjCxZoo}$3gz*AgipVpiDh*q{1Z|4-TuLd6H z3pbX$UtPkZNdLZqy!{b`7q&Bd4k~tVtU`9d5qIAFJ?Q$`BGwVVw>1OEy35%w^DCN5 z7CEvPIaqU=8 z4+ok@6T3od?zUR@c;aSGU%)`cs+E%RGD6}nK1-z0IdR-s{^R$1XhOm<$FRV&OSK73 z3OAo8aonl%C*>kc^gC}eh9M!_%e{|U4>2*aA3rr{^~5j<4#k%>7Za!%s%H8tH0wZ^ z-P8hGenPY1gPi@ceR>+R5IAjUV~3?gU9If{Y3R~B^+n3Mx`ze2-=TNTf>9y`sW>e(>)lOw`}1i=_1gYh_lC4$3df0VkHgp#*Qp%LE+h>YQhua$f(` zt#lGX`^qN!7Z^C~AZu;`r2htO_95gRCtNG*DsXj7bs>=5^U0d|`AUNH-s?K!~T6 zoWP1r_?A)}9k8V_+nxbLc)2|*Ei4AdqC1+`Jm5y^c?h;m+hl`jDG{_$VwX5JzH35u z;9|l)s;WoIBXq|Bu_8bZUasDobE<38JEx?lc|~gmU#AJsApQUj*FHHM2B9M8$By68 z#Assocanb?-1i?ei9EYEw*?9BNF{Kg2OKa*Y#_Y!Q0?e>62(izqO_zSWgJ#L?Kd}V zlSokVo?&wmX3Wa<1Uq_fFxkGceR-Z|wc%JcI6?84DgaK)n5b4^W+lbE@0P~vJ96E= zAnLxzkMiz|tHiCZmp7bcNI_O`a6pP)VHuayAl(n}`%Hkc*ag(B)JQX#Ww0n0yOg|3ec%n1fgfu5?<3 ze^p>Vp}TeuVzu(1G5wsKf6D?G#-j=$BHiwJOB)!o>`T=GS*fDf%X6~}U;~&ARr+u3 zB6Qaf5S1`}Si~KrgU>pNh7${gET{R%>gXkTjqyH0c3~3@@wl!}u6z=K4M_O0A#ixe zTKTo#OUM?mjFx|4UYmt1eXqe}Gyf9$xmP9!*X%cT&LslmgKz9vcxaw8v8gcfvX&ST zKlN{E(u`3j>&*%M8~e3+xZ4LLFxF)r5l8tuDAb7QgBy1;n*d!l>2a>0?r0H-^h4+R zUdSMSpx;<^%aRU-+ozNW{nji_K^!Pr8plIH@v~K5-TCmSeJ`Ux>w&>^^^!ab@|5xv zf(5t#noxHXDS03(Le0AU^IJAp7)os2QlI}s@aVB{kNj<}z)JF4&9D1^Dca3v*KEvI zb%IY|(Ac`r0ep2l_%pgYjkd6}|BUxqS~q+2{?y^aE0ieFHDkJSa0> z_Mb_AT7s1~4MSL0a}yeYe<1mmeJGv#gcer*#j_fp!cLevRCN7JU<%_TkbJ}3G7uvN zYwKpP-y#Rg^eyN;?{Z)Nj(8px=Nu!HOjI=qNKUPR3GT~#8sz?W&DMOh6OTl4w9}g5 z<^Fu)vTC{aYGC_a1e#iWh$?b>_MQ!SZ@8+YK>vKMkyW7@chMsS^jZWefY*8ZSn4H< z2B>mDxy^02(O)+EK$|&6%b;V+i1W8!S)+_;`8{EHUj`H@zo3WWD`29bX;87ncfx#9 z`A|=eXlpA5^S#{s@HpL^uD^2`Nw03d@cF$%9>?n6Pfj^D>B+5<#fPlSy0K! z+@IyoH{n~5_>c-3tGV|*sY^LxvY(y2Ui~+ZO}%Fz6?)gn)(dR$ooS8Qm&-lEXU;M>dC zYNM{j~~j%pB-v(}YIZg5GFFc2Xl=q8^0ef3wwxi{Navuc&avZ*Nf zE1(Dq`qZrG2L%PXRo~=R^SQw;f@ZT?976PcWl1GHl z3pt6A+&TZ$`7VC1s|cT%UvNg~lma3hny4ucIad0AL`&lEMGpU0*tRsyBKhcD(Adxm z^LG~<+?vbU*?osnh4_(k-1O`u^w^_EVF0A?sIR!HL5O`7D<|fwAFaxC@TY}x^e2znDWb)n&OGF*@r!Z5DLlSzQW z5LrryZbv+E+x7Et5x-LJuVKWW=D~kYYMFnE2L$NfI{@$PXX#T(_maOmJKlWWtPx@v z?l3V7f)9>p>BqXUe5O3WM(X3;&RdDWbsyKnPl6`PBq^UW+)ljE56 zG^J6o-T>#jo{4PG(Rn4W|#jChbYML(XPkWYjr;rqz3{KV?d!g?dETce~wk4?|1;E z(Q)&0=W-K^MEU8OThlflm4>+xGdwD%?k*~~Re_+0i3N+4im)yQX@}mKah-=`VBmX7 zBCLUO$Jo#m4^H^NzPZK`I{*}*&m!qpMKdlU{cJ^3ZcEOi zZS2#;;V4ix6`r!F z-Ef=#uC8_4RX_j8L}g5!V3A;Hn7XP3z!T&o^#owyp5d6@Mk31#q!^wS^XF-JCo;CB zgym;s}laq`A?c3nOl21D5a+UszE+XmCbA+8o-bkUo3OPHZ(U8)8)#O(h+xXgUgT8&u zu`JbN1EG%(pdV+;wm*&|B)P{XO4L!^b%knU*2?{&Q9U%m6|>P1m4);~35~KDxpBByW?3 zk6n+@8jdeQ`-r1o73szs7UWm9CB8?df52j`$z($*zU#YX!TDs#823FTEhVRCq#5js zG;)dCZGIZCi>$BV%gu;v?&l()<0g4_QK;@<5k{}ieu_ZXA$ry0HYoXGtG5>TcB_jZ z=k@fd^JX49Ok6?|nQxwS?Ooa5+WngoNNYoJ*%6}pckUPVxe!7_AZ*Ac5{e^&`_TrE z&%+><{41R_gp$?`1hGT(pTp}$VXUM#pz6jN%lWv+@VSY_w-|VAb*CilAakadeFef7!v*}5eubX2-l5;#sRvYv5M~!k66CU%sw9Hm zsYs57!PL#F8|4ZZG@mnz(z5hKOk|+V?dTUf<};We514UfipwMf`6@9Ap}DwbQ8uLF6u6 zNIPKv2x!J{e-I{&1WGVjB;7r>5wRRp8eKL0OsMak`m+*H?77l@?vRD4A+BswfY|oa zSe0x40>k`n(_8;#p9+E}%!x561Zg?%vCQ-tF=1Hdz26ag$@IB$0aC4|T9cc&P6FuN8_7E@Hl25!b(h7lW znmQ$3^A50dli+;I*hmYj)i!ftq^jolP|3PMbT~!qMS|n$`MVnGr;InB3YAMGE#ao3 zz3rJ?XR;E%6!@#gOq#qEtgs(G4&M3HM_}kJi~Y3^&}dNAR&oS3lMk)`cCc-hkV75s zrd4!(bH7?f6M;fV;IJ2IH4`9zdA-RvVy!` znY-P5+m&}7Zq^XrE-x+Hufy?&ZTm(W;<6oxfW}`G214+GM+J%2E$#zZ?zBHX0LV4? zAsewRD(iA~Yf&^~!gjUF$3M_q8Pey>bDz?L(PNOW;+26oc}MdvMNA_nNOcr`L9r%T^1jj2etB47SGg&jT$HhizQqeKrsH%C!(N zf^+JK3RQl2>Q1@QMKXPZZ#PqE0hsCj{*|4S7BXbRZNlxGIY(Us+N>Py@MCRy9J^%WOV?p?L6E@9;Tr0Zw)N zW_LTsi}5R-L-tP*OM*SiXN)h_tw5Pd*)tG8h_HnuCP_-G$~BqV6hQ>?S>uN2`6auo z@q!6E3#FX#M|$&lk035-9nL3PXi2(j@hw)rt zWqJmVlta51ly>Rh4@0Hqd4nw8QJ?7#5?6&xd0$!)v#+&Hr!<3lA)TlIYT2y%<9E+f zzql}?^awHt6TSw4Vl^N_B6c?gpe3yST_Joqz1oq=vpcnk$D{k9)$xCiABG^1cY1;? z5^aEI4Nm6=x?y^1U2ngJ77V>{R8IcoVe6PQ_y+*mWA|cbM*y2%SHR!*!E5ykq+VW; z)RUA@@qvWuBK@*@CFbT?^qKRjMum*V8*%Xm-tVpN#^9L|aL*dj#_bx#ACO|3nmlus}GgaQNNG6844Tms%ApI(8vfw>h9j%%ne`LOm zNFdCqdzTfA(+|qsoVNn$r~C}KidMWJtfl-LLL`(ZEMIG~!JBj*DnfGJdAG`d(7z{_OX zx|HM_ee#HECA-pF(t`Td>Eacqn9%~EuY3IqdH9mCs3qq4MJz=gOi`=S_2j#qiIA3r ztN-*3l}n#5p|LTxt;-jm5!&$4iJr%rva9`b1W)AZi6^oO^|)}%so4GER1p^Zt3Tu> zXCr5u%^5xw#X!~1`uiWfpmLb}bbG~jGd?<94P`Z`N?-bsJ`ETQa@Cxn`1CP+LB44i?=vWTz-{91$N@;Z+HqslgX-DN-N7 zl7tQU^YmWogZEnFnosv5RMNN|R!x7iaCH9ifRpg(u-+@%*O+(yFK7Ftk1dBVW6d2?KB4|Wt;c*UW5Vo4*c4}j60|SS zv~jmrlJD$aX5P?0{-Lgw4-a*(AyI%fZ|-290QU zUiWNe$L*7_QAB^LV84Fc=CwwsJy9>n%Cod5tY)q3v_{>LYIDszO-l_xo=YY+a&2QB zCR9fb^)D($QJAVt;?d-X&HAadXV~!=B%xAMrUfua?|GXndiVJvYcStyCi*jIt;4Y~ z%f=|uJLK1X44GlXj42q()ys{!%d?`0U!^BEs4JkGy@KjvL$2YYtv*f~! z-n19;cc&de4QV6&MJS6xrT&evi%RY)%wsHDbL$XmLT=-fMkoO+;Eh|;W${7(eir~k zd%3H}R<#U#<@7i&WS?CQ1WE1ut=hM1`V9JvQ~;(;FnOaML8R7RDK0!W{L{zPtDoyv zZ>R?`n=AkTjH@$D7Fj4c7#FVR+Yr?Y)`hR=?=^61RF%#e9maflfDLA3i2b*>GGrY} z0VnBJUDYgzRs@q*nIx|JFKt#SUKdQq!~K-*Y+=3_A*}RUqTE|_aMc*WO%nY+tu%}J z7lcW|J}p09#vygC4CD75D^$RAZg^1YC;>F8nqVS#BNozkvl zw&*h;kGi4N>mLtr5E^`o?tYI~K9@jN1U+2~Y`dsr$Kx_e!x5eczeid6Y#wy^lyxos zvtBA-QhN{n$prVlX`ll`W+zfvyF%aKi*RwrqP!t=%MNB$BZSDk*L-RZ?arK`*}|O@ z-+S?5ri&WMTKE&oF!-{?(Ebm{64h@jj~|)7s27KYEM(x8n8!?xiE*%83M#0TG+5vt zaLej`Ws1rV#yA_U>0sOG2tP0kYbmnMy!9s{$0H<2 z=K0)-s|f3NBEU&HHU;7anCIk-lOy%7qI}gRTRC`bOT@J7IW=8(HRn2Sap&5uUA})> z)3}&LXAWfz`>JV4J_oW!9tTTD(bdStB9^sv5!7V|IUu%)OqVR#WCs;hX7ue|Mxp zSM_`CETlNhaN&0EA=4!yott^v=Yy1ZD(vwe!5DdC_B2tH=n_<*$;L4-`Jw2qLWQ>M znN6vrBsJDMr+)`=#=wil#|J;M1c!VRI+>U1k|#Pf4sR`F)=vD=Ud7tA&UCv|C()yU zWB2Y*2mpqsKP}C7i3GkZE>(SO|6F}WmLzCF5o}z1xBi~}$`(u|Nhd|l7IkaEq!0cUU7t#5 z*{BP9a$$%64^7{|SZCKndt%$RjmA#X7>(^ljqRKmjnkNo?KHL;+qP|;-1FXhzhCfR z&+M65vu2IkN+?8~W<(PTzn_+J;2rDMXuro1JKln*?NF@lQw~KNS}5ITc|s`x)d1{RFSgk4|3RpY%E3h|f83#iGQpe4UH zEoOJYlyArvn2?W*{%5KH^ghOeN5a+Ja%2eCBfXX6pH$R{e7#}mK_Qk%@B0CQo=a=A zXIQ9Ge}060cv!c-YUCOuAJ!)Q6_+q26>`4P(Qwz`{W0};MHnTMK&ePl5k&c|C~+w) zMm2JFSIa*aG;F=HdxVi&kaiWCe^u==l&6qPs14w;8|h7Bf#wd>HKxV}mbR*ca>m2d zh<2`OUU}sVD=n)eXH2Yi< zyWjl;=sMjKPo2N$hkSmZJ=_c>dGz%{&egd6fu&z^zsA(IdZ|1h4e%%+Vn&(HwAjN@ z54hseR`y=7H_)KqN($VYLgn&n!x}%WQvW^my}(Brf^wpcMPy!rttE9xD@x!amx$u;wrwd@+ca2r# zJ%_X}oan1GpU$FkLczB6&rJ@-SMG%y&Ixvrg$a{)rgxf*haRF$J}myN`P+g}#*lkxn4E5EfN@r@uJI1vRP^r|vd@xrEwPjmTF~0YEK=xs zo!G}L^~qlM_VmMK(FQW)O%l$aw}SWMFr`)61!+dh`ot$+TPBb7EL#7Nz?q1pQ}rda zjLL^R@I1cif&O#t{AJ@eRtWbAe~n(|4>_PJVU7+U7V;`gfkWc@y+L5~Q+p+S7KO4< z9rCa3NFU;_f&98oRt0i;Xqmr@QV{Iz`^_WOZ^tr6Pj6CXoN2R=dD80E5Mm9O}{9Ggyh()Zn z1Ny*Vv*3!G!Ixq^H9nlS4j0!4-yqfJIso*)26t6l4bV1+z#RA`a6R6=5%IFQxP@yp zwO?wme{EhfreV5lF8ijN-IcwI@*_!UC5O)J>PcRcMOO#1fcRboN($3Pq_ z>-yfie6U*z1>7J!i%(z!zozPcb?=unUs`fVuEuybhv3U-uVJ7boq1*4GRTu)^7l+O za|l#^ls(1}s;f^bKvkZ<5b@Yr8f&#Y2TDq&?F@BE4J)dTb+`qZ!Qzc#R2k|2BY6AL zSiFFrT-3dRQHW@o{+DRbrkmys+8t67slS=o%h>NRbE(y%oS;(A|%;x(^+Io7s zZ-jJ1cim)@Wcds!?MjD4ZLAQYSvH>@P3ND5Td2S^h50T*`FlSD$>JtEqu~tYZ->(T6FS^C8{~2KOvuJ|t z-EJ$pfy&VVg)zABtL%=fRbn|R|o#pXZkbUUW7-Vr9lqRO=jzOHqLiLENCc( z5kb+$^eO+w+~<5)m^Uvm##r)pn=gufMlDem2Q&1~YO37w5NfP!=UOkamx<_@l}5ao zD_rZ1ih_mM3 z;mewF#v%_EY$8HY+&YRlFsLd@hMIS6ZtJLAvfr~ozWX|F=?X>2Xw@)@aPqq zZh7~Q@Zj~r6>>k?$)*0vYBKH8=j@rXC>_jMCYI_~8>A&)=eAEJ|BPd@TE3^-DxvE; zTP*CS8@PaWo8+c4LdD_n3+L?o&94jc)T8yQF_Vz=aEgnxRrRhhjRujyTb6%}yHm&5 z?0?k2IQeE`{>g`rABG8oKnNE75FWH1c3^repEmc0EFgx&$o@R;;dFB5beyxy1s|#U zro6=mL4n#lvXr&>wSL-t!Jp&_U{-bqthJ~g60!WZBnMl0nZrO!7Jpw$xKu;@yrf*^ z4)8fZO>QhU_WEC5tHJfSo16$GlNE{UL7>|hRslc{x_vbgIQuJbaQg?&(*m-ND=<}! z%?R{)B>+IO;Jb~eNyziyl)Li|PE|xdG=h}E4)hRt{$0SAT7?d`7W3bT!2kZ4MSmIw zNQ}XzwKy37R&ZM%!?uAbZ@SDDCYOIALy;*4rqZB^aAvE;)`-)I;=La@d}KNIq4LQX zE0HMn8Nq=>y$!a2hRb4R+XbEVms~Oo=ZycP+D@X)ciz#jL@JUU)qy`S#<%wKOA%@j z6Ssu9dzw5-wjF~*zQc zURVi<7A~f~PuM<7B5VyD$;RA;JzE?%1+BlXFbDX8GxAPP4@rTS1;}Kn9Q`SA2Tg&& z!uNGOBoR**j%o5`-0oEm=HK;Mf9AH>_?@h?xRltLUlz#AhXMFOwnMvg9%!}x zYB{ZQ(4VV+WW6B^@c-^7&cLJ=r9_g=N0nFn_hLvRl^{%0=GD}!b?+KfpHt#d;VF+G z#7=6ZcvoQ!F7ozuD{;hGejfxbWOyXlCVV^MtX&aHeaxKzhx7^R29dWJeNqwU1x@=- z5vgC`?1@rt6E(S7{D9r9O>|- zQ(PWx#=TbS?!ab0Fw05CwG*`lX87xYr#gFsav)@lyyMCmZQZB;O#QXT_W8b>3=&;ct><^` z>(LD6W>9{v40NAKL5o*<&odB!ae}e)LPwn%X=$U_Sq^DD{n?|91Bz`|{UMP&NHcb| zUS-McPje9jL*y=VsKoU=->&dzfc|5rgZ&S`3p|}JAW|b<8}pX`*B7E&+x4u2qEs&5 z+Y*ArxFcH$E*J%zuV5BH9%*gx2jctd(;VNCqTT3Su<3$40jiJ>Q6bBCr*y+Ho8-ct z_;GPnU?0iwb!0rJ4ji7b@KiMquufWtQ!IC2%$0w z4Ud$goqibWlSI7>X`)^UW9DF;(}NCQ$YJesJW>V+?xT6Hn?(F)NM|F}Tb9gm>;W5# z_s!*?g36ChJ z#OhB&vC)Jx_C3&wUxtOf>H#i+v8y`piGG-`(8@x>zY0(*jOe<3F0T&Gc`rkPQN|xR zDBqo!RhqEM4>tVlw!T*h;w{3Rwjmb1!iyLx-&&6lZ4&MLx=ND}G6amV*mP@G>x{iq z*plU#TxY()J>QAWH7eGh2qH$QM8_aR!@z%a1?_<;ocQPZiR9dcC6x*8Qn{Fz^84V? z_?P{>08$r6n<^h9uTF55<_!VQ{4rm(+m}4rmjL(md3CsCmWLmjKU`3dZ-A}AjwOgj zI)j;icAtwZP*Kn2{ijCQa0=baZVeZ8>VaLxo4BcJ?Wx^JOkzP>-}=W#`go=RS#B`R zPP^#p;HOW(RO!Q$Pj8%&u*`u#4clqq@M%vCWQ>lW^lqhq`Ga`$#cWku1+iu?)Y`}Bqg62?*!a=t@nsvzaD)!8>f4&IhM-YlJs#+ z#u)g>cvC!!6toP~7%kJvhsJ#Y<|=BwUY1L+P5-l}HPvGAy@z?eQ?@{lL;5!+>_MC( zNaQen63t%1)n`qowA-L1$7459cbksg{K-1vV6V7F*MyKp13%o^9?d`Kw+Y(e6K(s` zU_ap6Ac1pwedhc%(bNX{pgt4hxDk54oE;`Z1aU&Oy3VEgxw^G_$pIn>?VY|C@C1E zBWWEz(a}~1`{7rSN?+Q+JtnpHohgRP7q1-O-XH?VVkFF&TZ0DN{oKFLk7%pU$8W`` znRvXmD=C(b44EdwR!ou3y+CQaG?*N>_1k6`3AehFxcb1KND*|2#e_ z+-u{%!xpEvRZ+oV$>_lk0;Go?cUjV=M*2wKPQrpy0@nES1v*~;j72h~!iFNgELt)F z9ROp7O~a*%Aq#X}#P*O+Lq`iEQe8X=H8m^=6>##zB-UWPFPSWD==JTW-sDdI(#M!F z)gWs-{m5cL72efSt zq3!F~=Mf1#i+6Z|hVtzy%@4Z7-<(dgwJjSf^{??)`b7e^at119EGm;UL8iWS z9SkMWCjTH&@KBD!@?@&WhVs5+@IP(m=k}fz4n5MnIhE+Ga@Su+K5YP3C?t}>#l5z$ zc0g^F|K<&^Pca0omiB*zqCMzUAs<63KDQ!$YYirTtUtBuh0*Ba9Z*Mw%vqmRwKG_; zHfV4Bz1>16otO@QdX=nN(Qrl9W92Xc(JZIN#e z6+1f>1S$n{fL~X0aeZudJD(3%jkoD>3Ti4<>nB>DD^=(!%DIDYBCW|r8@e+^v)=)~ z{g9$LKMsYYhfpK)lvp&mP^9v{e6bWpOkSwFGI z7=9Vo&DqG5kT`mFPIka27rL^h` zcqBik>kL+mdVVb-AoW;<0gpRGX;LHS7QdunNy^1GRe)m9_&5NngxXP(!&8Uz5Gte) zij~RS`6j`GT#_;j&o`X}s9olGJI4a7&(oUzZ{ehil~ak6+}=|-#BuK~z|cG1x=W2n zeivU>*W{On%W!*SG4LJ;H>*2_1z#R-?(!V|R6HPea1=kS>R(sc{WfpEI500IqV zzCqup^i-D@pwqx;aELU)1m1hZ{0$w&tNC{F{!NUAuYq4yPyIUSYQlVRhvPFOv{o&2 zy?0XE10@}AfG4JUlk5AeTNY2(8C@4<1Zz2eEwx46mUbb z0~u=hT{|JNu20ne6^>}48M_&oiG@ZC#UiW#S_I65n{AMK9L6 z8JV2ZlxTgWSE+WjCs{;u6o(}Vy<5#)^z9N0tQk{hxoXd2?4qcKp7BEQxP7Ne6?QH3 z>njZ%Tg=U5b0Z`_w;-^>41g@oik2GS%+$OQf45Mi`dT|*Y6?v|t$=QPaHr|I!*O!3 z8~M#e4WXhZtQOqVVwdSJpFz@tuNZ%j`H&h@J~giWcd>(T6akUI$F9|*FSv2id!R$w zA&lhOZ$cpwARfJ!l0ob$mi6M2!9?!byAF?Z_>J#kpQl<;_OJa|{d*ZU8i-{u?o$v- z#;Q4&1rwsVvT6qQ>IP4t(A$89yG%g5oRKV`plTuci$D)<{f2O&NA_GFQ7rET7UC(T zp(Hu9Oe%K|`u!tz3ust){ZTAqAU{I+iwszq{(+0|Q;BXH4pP=CHDW@Pwo|`j*~&gl z%sBL-SFkHgjR9qP%!sY$Drmby9K4>wz(&I%z0ONRhv|N_k1$_b!7^b&gOly$w2Glc z0Hz0mF1+P=0zt{!E{F>43kTK&)ptG2AV1uJWlQnsUy~QxhYm71AdigXsp6K`VJO?r zpMk`)n(n$VH_OkFWLCsBYEf;5rS}zh%*UH7t*^m$#LmX+mlY7uds}7W{Ku=c#vftC zOP^M}W%~bEvoBo21l#}_EBTxdqUh*3T>=j+UVPJAR`&YrLu;Vcym@4q?=n(M)5Xi! zgMu_NHW!Cu;+l+x;myQpCdTE!vPbV0o6Ny4OHcj7ZveiVeEig$WaFCzS{F-cD?4jc z4~XRtq!ebkTJ!HgV7_h^$PV)WvB&xfJtwZ<(&R!fyk;J%uem5 z{>?(^`0(nNz#)l&Ls6mYG`_Mx<}`ooJN7^ylsJ1C9liGOTy;9Z7U|UbX8vjZ63r=0 zT=ZDTY)ApSHnf_5H`g0FGFM^268c{5zn4fXou7N1aL40#l2tCUNb_C1kqo4p<0pD2 z4fs=plXt6q;9B+$8$W<>@Pgm+Wq0V%k%P1#i7)wsr?hlRJ2D6ti2h=tjQq}X`4*wv zU6TDc@@IFlJLpL4pb@6Xn?eEz5#VhOB->0jw+RnK-&`QBFrkEZY?Ksp1zmvWP2>s3 zBLwoLD3K}p$}o(J%Zzb6v^T#1Tp{huN3^c6FU#cAxR3z!$vO}DWzm-Su+YY|JwhtW zIAaotpIuxN{0^&weVsgJ#GN@6)oYVRWEUmvC9SR_R?rp`(TET)X(g&UT`yM2$B1hY12MxwFfYKs(W*q8&Vts(n!xj{CjeyL<<+ zR|O>=Q!HgR{Nb^o$bs>G8*!JX2Xr1KX#y*;Nbr~)NT8o$3VAQ&1$W(9`iqV`XzUwr zmXR2ynSt1fEy#;#u{s+VmG@m*AwyoTSNfg@`a-q`qRhUq?&^Lu zn$TRR=VnMD=Hj;>TjtLo6oUA66SiJyHd)6*1_V^m0rS`SAs#kxtmwN~YFt}b$tc|t z^C~%d*m&R+C2r$c&(Xj&01)DqCsOVXRuo<6;-e;hU>f2Iy(rL0lV$)nH+TXJ{XqZ@ z09)1MU8}=Iank}hA&y2?^{bocC~NZSP=G=nDY^{C1xDSAd=fa~#vB$yVmPl&-C!f` zj4(kkAybN)+I~UEk{aVeL1&tVJX7Ra4%(JlTD-~l`icfFYgVC&AyPsbVR%8zBQZv@ zkZUVf_xPVz>-fJ=dHEf?@sk9<8B%CFYiC)=o4L3LrVLxMq?RwDFGg9YOF;jFr2hit zEGlgPDt4E#Pd@ihi3lf6AIzGBBet~s9~ZzhQO(-p%I^CN0aW*^Ut+GW7cZ9}N{FM6 zzqHC*Ayd?q3)qdZ#s0%N%MK#V0!hH%fC+9ufLW{~IdroAQ0}-rErg2x6Dci9HXfsl z83)e2APOAUAA?>K$dcd@O|ZBHAjH$!ASce32Tt(KIIOqG;n($Q5kjn@#r__d!p&uKh?u7XcF?%I8wt?qw8@STKjPnc32aJuoDDaf#`j}T#?hhJVLSy0 z@-HO&<#x$(*!0Z$$LxS%i(3k`d=w(rPq&N$Hx72Q9$$z%m=nnL$8hH64|1dvQUGf! z=OkD}kbmDeX@#r*3)}JC`JFYZKy&!@&O{L)dhu$79-G=+Q&<0PLe}?2ZO*_ViVP3< z#3xfkll$DqWB+1-i2BQP$uz!Nt`l8V5cG$0Z#a1Ya3hOViUSR<^G(6iYqc|3>XN3B z9ntHrFP9Y&VgtA$+3zLVD0>UD2$1|A+qp&GuDENlVq>mz9pYT>6A)0>*@o`Rdc*Bm(E>bc!yA;gLY>mY8wn_} zo`4_1oyoE$SGr&FVj-rRRaRU(y{byw7&QPd(SvLOTs!U?Nx0u$|y-)=O8F z5NJv0kTTf3H?kAl9*~Wy`wIG=GiA6%FSfFaFb_NPvIMRuKdrcl7N$3qq*?EFpEx=M zb#)_%vby8sVe*{dYn$)BOwmLTYVp{RL!U}n zHp1x;IY~nA$H;~r3S-w12>M`(j^T*oc9~vpe3}oM2wjWn_x7K(Dn{_}UJ)!El&uQB z%`@vQ6du5L(fYHS)em_cd>IxjQ5VpUb@1+t{U7jj_hU#fxw1bY>7VCEHPRs^r||wd zYCS{WX<8`+$Z$c9i zS6BKOD)flBlLT9Y34{yDG%#xL$O6g5q~aOp=}_2P<%6uRZ&!8B8?@B@0##Z7`kNEM zarbwxOLpO4$m}?0%4ACM3D_yd3W!{VVTS|@tUHq1=6bp2&Z&+Kf%F3=P9ZbWOJAcGMGpBc8>lmSiC9usx%OwpaTSvbPdK^4-WdUXmHBE*gp^bn1Fwdn@9UHuh zIUGU(^Vx7X8xGAj%UU4&<_u+7u$nbMKeZ*EJPBnWW%Mv+VUoVm>$qXy^yRZH{Yl0G zv#|D8({a}f-DZ)fS0u6uaqd`3VG=4ts< z8%tAqcE7{CR<;93Qdt2|Ba`}qYUtDwP(B?1q~M%$Y*<96Dw3uG?ekHE0^#~h6{*l4 z2!&P9f(;j|?XD2EQwk=!uDsu=I^k+y2=WC5)3cD3nGwuj278oGOmzQFAz@Y?4a4Ng zt6fx=h9dPKJhtD2U+CX#GC?=P{H8*hD3O6Yk$VX;18K)hTf zTHqVOp8W`^34X+K2OT)T4#pe~Ac3byoYQqagqyB;$px-_z{oyZZ~#Fwcr|RIK`T$b zS5iyDAUduW0j*AiDGYZBk&LS5Z*>a+mOscQ3DTzVtY82AlQG8X1pKw6!{YsqH3ca< z85Q4{pJ!5()^i+KuUBL->3@R>6O>+=OhN|vz(U{8ddBqf0X642S60;~USny> z8>|?fk@*83lh6xpd@3SPZiP1Q;W>QhEKO!HY zhj6p%XAS%Mr_a}quzzEE4#LBgK_7!RBKE@@S2@NL$6x2~x8hm4Z2NDXC|;s(h5{+U zA}If^tJ%(hIb~^Te%DOINWkVT4Z-77(+jjIIp7}6Y6(QdWH-Y@+Otf-%5~5RjRR%- zr6fJk7(#psjU>r%d-jeb#I-Kx5bXV0Id>E-cfdq90y0^jyQ~ia#GflURhT{5dwC7C zE=jkx-gtdF9dF+>9}vecg1Im~+*ukVa^~l|t=E_L7~!-kF7l(?d}ccWA$>pw?3-$; zvXnz8g0xr_1x4F-0T?S6{O?dLQhkBvVQLGOxD9xeZyh{M`nIlu@+Np#5y$Tb=%cay zDuD_yS&E&I#c^8JV$JAVs^|Q!Bc;dV+Ld!z`jA z%4uBU(-7YT$W8_zj+OEVp^e4Odtnq=-}hJy77_KH->e%9#lAhZl0UR~j-OSst6U58 zCJI?f9d@z(Hu#8M-C_qn)VRe_AFyc+eC3)^Ca__E!iH3((Xtr;>@)upwsU&mrM*d3 zR$kiv8XDE9B%|_AuKM%Fp46AQe?9$lzTGB1uXLz1wamm)<2TyVo09F24HSeXBEedK ztWB$D$IQB?aae2169SmIrYtN6qayb8sS6wl){7{Kd5nYeGxdcU^o26DnV+b-O^UM3 zzx;t#Fot{p?m0FTHXGv%;gp)adpwivuDg6D&yT!6 z@kbI#tPi6r3GAMp?qduwegI&R|~PqtGC0T5E+u zP@d`ONCduwP9tRvb$Q)?DiFBwtjoyy>yLs|3&j4weP@Qh4@*jJTN7l4^o5Q`;qGnW zmWz5LqVXA>AwRsC4oIxfy#90|`s5Ahu~SdwDPE4J_&#dz z)ON}2c}bukZ$q~kteWvTza`Dhu7Y#E>h24mBAJ*G zyeY3pX7pGL6p1XuOd3ek+{1UlV-Jm>DYHh2#@vkNh#?aYft@k+no_)KUW7Flm#_k2 zvoHjj+;`Wz0bYC5JneFw&)xw6FYTzm@c6hVYTH@vzr%<05(ErQHFakmEhjLa@f7NO zI%j->N{GFL@X5$G^w&q~#!U;kKx^4=QT4n2nc*N4!~(D!WhpnfD~i$MjZFNUPT)1-@>05 z)sx0@W~Qg#pYk?C%Y7`q3*yek+pqsc(n8ktyaJioT$yT4F^VHfh z&*|D)k?*(%YiMy6Aeul^Ex_!U&>)M4%;7lMOyMk7~30_@@mci@~9B^C$&FP zAs%<~s1kv=^&ys9X;EWHP$Y^7C`vyC{;U|de0-SToEQ5OjKY`KH;a~kkbH)oL)g2U zdNYM4eSX{d7oDIYHO&qyjA!5Wm(=-yR_N)bp`1h#!E3PI*u7!moZn=#yF74;gsHYu zc;dBL`sT*ra0FD@Q^TXYli!-ql|2@oYUtkTeIkyJ@&{q}tgP*p0(d!m{?an&jf!(w zy{ufphzyp9dmNWvZ#l<5w(?e6DpxcFiDj*WM*N8a>MVU}2~xXHk*&ENLKve#RL zF6{Az)y|9GG$4n6ZGG9$2>Tv+i6VYw1nRu~)~5Zi=Nh~)rZwpHHudU-b6v*F#7wh`AU?|w zM&)#jUW%O0Ue88tPMw#-lju{X!mv7hLQrZ>geYKoI@;&0;s(xW;}Zv$P)hXNA$X`_XE>!t_f+Loc5qs$>n>E_yQR|01lJePuCrPeX z+}zw{aPR3l6!!CMH?ir#MR*-sv!ld@;oR9#37Syj*Xb?cVHf)T(0{D>#Co>bHClypHCbnJCrFyQ*_?}K=t_YNjE6~CSdht8 zLj%U;gffmE>=XpF#fiRsB=Ko!-~TpKFM!K?+xqj=o$$$^X^94rW(btiG3Euu5v67l z446$RRrrsNUc7g8)7%G@XORp2^k<~#KEBc(HOyB1_b9K!%FDXthV>XqXcHB=@ruCtOii`Tq6} zBvQ)NPKA;3p8m|5*E=l(j{8!ou>DPw+?At155mIqT(P2kM?hWy!(ik|>PXXP zRd@FvF~{+od8U}Tjg}6b7|&5QsNZseLvr#o)DY`DFtj-lC*e1^Aw@r%mH1`Dmd*=Ro!nSw2?@o!61Q+Bd#{M?8~iBkY5y7qH91o314DEJ7{e+xfB# zYNTEUBSFe}>)^5qmp;?M4_OeKG zLh(-u6zcEcyn$U8*WYA6@5Y1_X{IDtre?IFdF=t7fHX94B3c)C_U0yX`6tK zxO2g7h!t-&wH|y~0ldi+rpMXTP-6qG*@OFl$%=xO?h-~(pSit+ub7}~K%RaR&cK6Ub!@xy$cd+zxX(Suy~rZnCJip$J8J6VSZb9xP9O;?Vo0p;LRj>63>){ALX zL0P;&jjx|yo8t1_m@=fMm)Fj1z}qB0fXatl48WvQn^`kXy?+7zxx5cc$IxtU+~Av! z>!ZmG^3TcV{@;<|3R2=hQs7<_UxJanehy3yj0k3obp?F|dP;)-Ti@;we71tBF|r!t zhrd@UktTY`cOJgIkj_*jn9x+-qnB;CILSwR{roE zxll=oGaOWKHuXLuZ8EhfwB}y;GAM7I^ZFvo(*dFOtv^kG!sdy4sSeEz;7g5cSX2Z| z;L=39_f)8i(qcwt!;NTI+u+-w+@2Scp38LnHOWG~#*3(W444)bH?LD*Uh?oeeGMRtHj!em%Bx_?3kkxz-e}oZ zW+P#8QyHAwwc!5?B!M`@%e`zMeZSyUy>7*J{4FI9wO-3Nn!U82_C(5lMW;akp`-SQUxnzyB`VWeF6!R6>eWc zYOB+WxFY1aPy^N+j=6{MqDy>Gxd-FJ==k?0%ersBQz6au|kSsDoBTN^=?`-{G#PNDjkmIB0rr48gAu~2a2E>X(a@6Q#5SyK+8@x4tl z!8O1sK1hUD%b=TO`x!YEDz`KlPN~l>*azc6)!{&MzL~ujnws>!vO0+ z{C=iX%MhW~cV)M00zhyoRBP!!7&BG^E0FkK4wo_h*nP9{4EK$UoZI3%BgNWJjIA0e zl{UD%e;eFpG7r2`X5KmUyj1HIWxShAf!{Tkx*hiYT%-RCOO_&;Tfvj`CrS)IckY@v zQ7-kANz8lfZOI1ac}Am<45JpdA4054^Z z+usgvkv#wK#r?TyHP^Q<^)~(ieG^sjBdkHK#50M<3*8ePf3NYh310qYooM7DQ-^s3 zo|b?eHfb_;I~tE#vQGX+pYTKpH>w2Jp2GQ6=*WA0GFRD$$CVli7g3d2Dw>7;Z`ynz z;wYK0BO+=RbjVWx^LWhC*9ZfgAFnSw;L=BnzqT;es23;otIpj+U1iG!yuMRxnXHtk zVRe0v3?+RI)8qNiS%ejCa414A`*k9pvEkR+Jw#SZ)SVDL=AgmL91xUK>j4z8hdM~h zjHd(A0Vp}_8bj8kbpD()JA9ExiRmp~roR7d(vC^kcdkR+h6dijOtP`^zE3K1`5tw1 z#S18v4(>LmV22|ef+?b!YNbnN8n_ziF|m^wRRJ>{lZv|D>Flz@oIRWuZbf5&@1qX) z>LzZzf236q$PfjR*hF7~I=H~E$G&MTYd?Qp{jFQ?1vBG@hL<%!^WmQ_V(j( z9_hI`mMu$aHELLnyZiU~rI$>e9$lU+JaU0340M)vpx5B%RdI4nmF!&;TM2{zCNt@~ z>hHlSR3z44oHpt}F{5&v+|7$xyio$#p2SbrC^*9TS5LcFT)t?&iszRjMjLgiKm%M#B0 z#7IdJ^=An!h_Sc?nyANNK@&n(1JIpGg`PcFJCdM7)W}Fv5lKAw;>3YvwOv(yCEdb5GtU zEXt;#e!L(;`z|V+RJxy-#xTCr07P^cRgKsm)3SXKySIMZZ-5!xY=%BL@W!FP23q>3 z5MYaoK84po&!A>6YtflnMe$k$T~iYaLbxzrwo?pjGz;pxn*9EC7$Q$xmTK>EVl^N| zL1?@YhP~8r1%U2lXmzhEz*kO>^Dk=W35uftnykPW2MT`}ZScGp{r~zOHT}Y0GQ1FYnH5<%WaB z9Gmyx=JqFRz~_aB_MHqN>z{qho=LJNU)L)w-FhtyA-N^|c#{Pv73RPS_qh_)FU)~! zc>gQ;t7#dwo-4jS7V#){MeGjp)5{i0)imC=8(w~p6}Dtr2t@ag0_Eme7Of3)1}WDI zz|0<;2}SM^pvk<wZaJPhe%f(cl`rhja!A}_4Lglh=H_4tb>u8PwS;M|r` zutoKz3o{)a85$f8(;Ml~c2@8U-&}9TL#tWF(lZ^3EkTfP&|{A*=LWb&n_J=S_(2bk zT{&H7mtl8B0|5xc=}EQ0Tf9z*9&?YPO8!Ja*U7_!3tHd9QDvg(zDlg7k^Rr_T#8(| zuYW8)XbCK}HY8kLs4?FE_+`?YG&>S?V0N>zO>L|kFi8SU<0v0FaL?(+>ww90>XV*p z$s54FLAzBg3j{xPyH^=TXZ=o8b7p+8B0Fu;b+5i)=q3jK1m#xXTHqRh?rQ2SSgXqY zMmq7A7bhG^AeSoaPXlEyr%VSJemOP#l%_4Yp;odxehZwf4xwqch2Vn8%&$z-9 zI*3x~)?0k}(&496d?tw{V#vUGnp~%PafOF5{3Za|41nzezOlQ71H5!!%i53jao=HL%H}%nXsWHBnA#yv6Pm z5qbNSr1ttf1i@~9Wr5`K#c7U%NU+sIf4gwwjm{iKcR*_&HGrlKfO1$G_UX*1 zf>%=boQoL~{t`8DEY3*52E_&yfDrADdphA+iLGc@(&{(YR*n@Ui-=tCPmN)Pgl+Zv zN==Xwa5qHw_#wQpB~_fJE$3Q(@YH3g6@n%YMx))!iIJ5J1&ok=#XF@kcYm>8qF%>W z{ySotr!rV;vwN?hnH%5D+L`@F!u@DNNuj33Upq{hZk@+#UhyAX(%D&wyyv1&6OpJ? z%T38?t1Ajl+76`IzA9O6U{~SjLEKt%_3LdD6zm&15Dlb!BUppG!Hi8{BuJg;vDgMr z%%>Ut?B!%3){))$+Q9J5JX$?_?97|SP|5`mVxI}|@aU4CJjHZe387+xndgbTS!hVk zZhtm_QlPuKv;1G7c7k@i+%GE7CEn1U@v(shbAIx9{&$2-UATo%V2{yR*G**Qq6Uvn z{2oMwX|JDao3zL{Y;j*RqNLyescE(I{3TIT2nM>~JB!)t+~@l|hAVSUtr3NodOjFchxDF5uc>%dlOr;zT^Qgx19mPFnHSLzoGU z+2s(LPxYCae+Ku^8*zOwh*=$$tPgB&R^+iwK7R%0wKzM6(sGumQ5Npad>kK2$!1)y zOa5T9ao=-Rw&_o^21jTaiI`8eogus%fhQ5(ewd+a%IWX?Q>Q#XKbN|N7;CDJ%0jhv zTSzsf>+(i_M>RZaRI5w_>N2|om%-54FGG@l_C8}C;I74dPOB{+4`}*s!@tqxB{n$X zM=A&gVL9eM3?y>_ak4bqO2GkJTMG0~4so~`*C$ZHO~TAdE#y@!s_rp?Q+Yv-(P*ky zKe(pkv_qGh|LQlf${92Cy^;;5Kju#~6a8cAhhD57X2Ll{{WrcFc_$F@j&nvdYx`VK zB+?8@6EM&%pJG*m0TiIrI*}Q|c!SgZY~1@qnue+ZFq} z?BD?bFKhQzmk_99X4rCqeE#0#d}vtxoXS8d0(M6&+~oI6lVeKLss%{ zylPSi7O~Qkg5a*3xRa6*`=^cP3XN`~{sv35qaiXmeMzMX5efSz;UX9-x7m3C4c$J1T)E2d&bpP6@&XJ_vDDLx zz-QO}*Y`(2G(vWvh~uapbWVVXjkKU{jO>~#htGB>Cw!0h z7ghIl2N;wRkQ_ijkPhi?P`Y78Lb^e4q&sv#De3MIX%Pg59>SqZK)MB_k#640^ZntS zKj3`s*=L`<)?RzvJ2J8G)iKor`+u~yNEB}vYsg69uAa-Hj8m-0?Pz0atGZ|X@P9mC zES@^(;9ZKRQEl2lva>-J&96j>%!kWVL`}|zt%2bq5LL#wR8unIz0u~TsE*diMpcc} zY>a|aGI%+c)ov%Ch_f6Oh;F$#n8<}56XRJqux<=@;P|9NXR9lynI)MbG7T3 z@z>k7&*cAFl6aQa7d=)FYtzA^J?E|Ow~S6y2X~DgzVHX86jK^9uhyQGAl~e`C8Dc{ zqcrxEw!h&roED>Kv=zob6r^eort&Gm#!t5i~K zZSAMGyrpcR1oeNTWG(i2Ml)j=@#Zyk2C7@v(Q9WA7NNvwH;U-GEzHNsPPaY$|Ki;m zE7=nWAUFLA5E^k6lLKodi`>r{Ic#b22!-kI0UT4Lb=6TdF_z0&t&S3tcl5oVpY2Wu zq!)`UNGpoQUe+4S`%3{H1AR%apA06ocK(v&DM6*}xK~^xV~chCv$tbs)Io!#l<6%` zTTF$2Gq$#u@&qWyHNtK}*FCw5cNDpE4RMBV<7rab!2AKC^?d#>3;0af`}pycT#Ddt zG7U}4k6DS@lO;Xc$uRJ|4GQrs;zO&xaR>Y9zE1fTMOcKRLa2S{=q5@3GFE!{7fK}ay#yvV9YBLJ$Cu*B6^&A z+)l9OXv@+tL^Y_M)(MSOQ0D+2F#CvEF8y6o(SI^Z=NE*vi1RTlPQv|BD5+IMCw5lB zm-QoOND^t6%UoiV8fPp$KwKbeKMIB8WO(}*?M&LU|3O9m>4;E^4w8f~?*TwiDV?`iBI7mij?pL4?O zy<)XR;aHF&MNFxo^>6k(Qfjr``n9z2$x>MEt*P=BfQDYx5;9Zu3&DE~KC-+Yq;^$8 zzZI=K^C}5jHYxtFIn4u#d0{xD6`%8-^5jCDXL8UP-GQyvaJk=2!L}s-cs#ZI6k6H$6)Kow{?u zXGq>8tUCTW1lIf~bwvo5g@ISGZiU1-Trh}ZS{QlOYumq2G*W*DcinmdNvAi)44mT4+|PX*is< zufn?Sbts~#L=y5g}T~f295x z%g1L%*$K6rB(MCE``boSk$L;gH!bmntxQGV8F9AO&xdEO6jpHi;IxL+N@9yIwx=c! zHb+ggXn}BrVaa{}OqS_DX*b!s@IRG{C}G~?+y1aA(xGLRg>T^FL_NlIjZmX@#?Mu& z^!a~Rj3M^_nr<#cwoiu-e-Wo$Q5+O@B$TYcLvrGE&^@q?A6qsCbCJFxanpQ@Z>+5e zoH5%~-oE^;WTmv!C99;TIxiofBhW;7(FDAQqGh&=UaKs9C_F%LUbH((VPa!=ooDOC z#V+j(b*l61F4qR{Pz^sTC)}_e98H;qh9*#TMg=5&#sfDfn3Jc`_7mggh_N`_gD-F& z?q{JH_X%rT&wpvMKgELAoe}U>oYL`{QiP$+49M6#3@uP2)ppl$eF#zfw5!kLpK?cL ztSlv>os@$MEuAGD&H=Trrm+f8(LBBZQT!8<#4}7zp|QXQ936om6vRJ*=F)ulJc1@g z>p#A3r1N{d-gNN@{xh$pF(O6nqMzO4EX9T z>p*`Ok;E5w;6_^%VW8ImB;tbU!r!t4nOCMsm=iRwgh^9pAGPYgrlY&>MvP7zt7pw* z->h`mQ!vkBoz3t}3!P|1j_>9|hVuF7o*M2H<>2{? z_)<9$-F)1nU12iFK#!ti3&i@Jk}V9<&6E z@46SW`7zUvs_eKuNq@m2&%KAFoWiWgm`W5B%L3j0Bmj})zBz&nRJ@eFviWS-f?I^0^)< zYc>fbrS2SP))EMu6rd&r8pDMa(m_q3PHe@ys;7buHICjd9b203$8Z1xpzo;*np*E%b{54m{rvgt>t_0>7mR} z7xVNmt3cRS6=3qBcL9{Ky7;OT80CF(S>fgef_JlB!Uq`Kl5i*6l+Q@^1`5oK=l zgsVgczeZa?xIvr?WpcwA)qK|8-W?Ahhk^Hillh4eqq|d5KXpZ5q404yz&^v;KbHwiboIx|#!RF4Aq zOL9}+Zq>M@)GgB*^a$J+cg}H1YmT9s&qzxBz$kvZmp*-Y|MCQteNhhxpM^8uqxGM$ zV2I9kO%JGko=jr%0xARf3iefN=y!UP`36;Zj!jM*TCdgXrru_LM$Ed@UDpaig+C_1 zXM!=O;Hg#a^a)>Go|h!k<(&Qu(_iu?e~0exr)yWE6U@OkJ__YkYuiQ>m?SQ@_WaN9 zbZ-7CRzA)EWioGd9aEc=AJFj|^ZcDD1C@%s&NWqK_HBcSd*K;~}Q z2r&qOla_E(TBT(@`j|5WRW@XnypkyO7fy=h#-h=)0p_ggm%H>7W1 zBhMB26F|o(hlx!TH)HvMZd;=sWeD#2D>35hkac+beG8)fE0=97#Fck~O!G_H!X*CXoZ?H^a$z+L_(G3WQB~9n0pN_mty#FWBP?%5`wB z5!@VGqB)f_l)hn=Q;!`U*q!D(=0#ZHV=D?JuJAG%1I98rt5=Oc-@S!idp7Z?|8KMk zsI0DWwS0#n(s-s@%lmfFW3QxnwG)YKPK3f9pR|c22I;Wcb&S=YSdilV0 ze`qTmWy3*wId)T5T8to4wpYbGr+XZ@F3r>QfTP;+ou&AP>vj+ClA~*rXAx5Zbn57} z8Yt1w-y{`~BP+yV^aA))(#f13Q`_CjR^{^E@9kXdm=T{@W@4YYqjOUZhJPHHK=$QV zh?67b5MR<4d_Nj^-lXmG;v7Denn&f_iI81*WPJ>@SEmz2w1NuWN}y$hX>F z>d7hy$Rw)vh&VmY8I?!?5j=Tk>C4(TpU?XC_uOrX$*cv;Z4nct_AIVpxU@vt zp)qs5;}C6+oV}w2#s({!jQi^06L>xzt6~(xD@bj_j@6&(IhES#3r*(Pp zkmHdNtS#oarSUA1Cu5(hDp|Q<*e*f4Zq< z#)Tdik{j3vwJIde8s-n)R{|1tk>rrcql%54{gcVsg|P--(K=h5{HCt#Iw)6N{u|gD zcW8Y%=qjEkoV{e8J9SK6K|z2}XfBx^exyv0 zk!}^PeF)|*JcPOLt>VdOua)S_pT1f~pnLRWJh=p_-Q4*p#fh%~-yC~efC3a}sv&g*uy^ey4&e}POoo_#f0ej1z-(vv9? z^7X~JT(yQN>W$l*aYZ{Bx2o3Er);Pa%g(?Efqk1IhHvqWumBx5fWlpo|38JZJq}j^ zg&`X$YA04;p*GgD`y>!!|1arkbrAwS3Q?n-^F6?4+S=BWTLHakaU{D7DX}3HGq&)Ud>Vx`(Y2h}{*(Y{~ zBxy$+2MP%K2IuP594%XjVv-nNOZvcPv!@vSW~(`&2Rlu3$Z|k~M~m^{qHrwDzIe1h zKT*iaQ|&kmvq@$~^slB)qr&)#fn{O_e(!ruu(H3Dtx@$p zB>^-u$HzJE_JDJLFBW-BBAFZs|C-{`rjM}KIORrRIUN(?e)h}Hl}y&`;Y>xKcpp4s zEeevez1nE%lTBePUVEbJB(LV%vQsYStti8X8;l`~6*Y+dG4Y-}UBdmLucPnaDYxwF z?-`~K=qQ&TXDH7o6pCE9Yy`W~v=z#59iTnC8dDw~ASRvMj~m z^l7w{&q&xVVo>DxPxmZa zFSE^h?n&*jdOh3=^Q2Y&(jF#|;!nm%_v)k#@$Uv(U+Q_tImbtm!1_LW!Q-?Hg72gb z0;2n85=*22_i7;>i#K;K_(DP8CLmA>@J~!f_tU?U5-lhNHj`PSqvj4{=r>{G?A?8B zaP3kXhM(&c8uI?d8A*Ec1Do26SUWzGxUgko6LmYkH0QUU7AyQ~gAsPOasbtModAk1 z<;M*kUF-nlbR|Q)P~+acRFkF|EOsW>_Td*ks-g5A1eB1l(GP&qnmCZ0!)S3WbPEWx z9)27QPC!l731GqCE$s{6bYBt+-tJ8we+Fgc%lj@DnxK7 zeDe{CACJ}LY0$Yp)5t$vj})5H6b&`&)0$Gr=N)b?uj}-e5kR&A9Xn6;F8%45OUTHN z$@v$#FsxA{UH)&%yan$b3>xAnl^2!TK9d* zRlcPRLdEke#^qba(v7wD=6zauR^ellz+icIE6j#KFaZQn=$JZ6%}%Qn39tIqzh}mj zo~@k`fKNt#l}ny}+5FX?hiv^PXydz4S?b!2+i{ZY z14-D|-;pKi_5?`#vgZjqb={FUSoG0tDPae5+w%xXGhmVSb|5xYfJ=$4ZxF}J!B#0! z8MWzK8zBWw|2UFJ3(nm5=iQ~zqlWr^|2V!-$}yQ=1E)M!rmHa??+<=B={g@8ajytG zAObekX~Hy<&-dLx(`AuEaCZDeH|ZmORkdZ#SrLhM*JV<odAJId- zF>NX`Vn5vY^IIYC>h0Z75|;!Ey;S6H)|&XJQp!qcGx)rLY5*C{=Q9D#adf!qW*ArH z&zn{p3U@D0e=E2KFMkBu6v97O7YHc<9swQBu}3uWM^2NnRBm|FD&P6>tMkTh>f|dsQv=zqY;Si z{lf$hJB&V1LuxFWR?qGcUXULuty6p}P_1MJh7FYQ?;2=D4ZjdaGYl|+!bb1Vq7}Zi z2IV8yB{XbL(xDj*SP_h00qq3a8?t67!tfTT5sDrvf<VxxI9SgrMhRs_TDQ(7?^P~wC-@IoYnT0EX7)Bg3X zO+R|*s_gxnv-DaybfN^=OlS?HlA_I4ensQ>do6pHjr>;Du&+6ncMAY2oO;Eg53ET* zoe)`%NRNnhDg^jf!2^nlLMuPEToH>*O&cvs z6Xb2P-C^{A+tL}e&$MdW(|s@u1d8v|3kv%h@+CS1)Y`L-qRn`B853KwDzk+ov)Ra^ zmL|on4Binp05GVBaGA8M{>PXbr(WE*yZY~ya5K;RN=^=ygRmW=p4^{bJLjJtThTaP zIH3|kH31I*hob9A82-3hTu<{x2)*Zy@j+Rj^fa7Rvup@^A+qZI@%naCpL$OJV#U#0 zGVu_p6QFkAp3Zd$-gmALG%?^_rNR^e)TQzDGgTC>rx43Th1Apb@O?*feN1l-NQ#{% z>UW^@a;(TQ^?H$OqxLf4KTXrJh8w+zlM9I~r%s;Yq8gIyBOj6HX5cxu$SKI?eZndv zl8*Ix1TBRS!%#Bg_tx z(CwZ&Gkixc;qjX@5jHwCX8$QXjf;#z_Eo3jXZKHgzzYD=AWFAcZ!%7Tx56o7)sY39 z^|j{%wEpABpGj(x*4`gp8M+`43pcV`AbF>Luo7zO~8yov}wG*eo3IsBNSCmI;|Z?o3O zkJ8->kn=A6XHFW|+aq=u1#Ni1r}$5>P8pe?Vj%Dfhp(cipy038`qQ)TKAFp={@y*< z!d(SZgSzr@as22KUZxhBx=J>5>b-34wG&>?nm@}tX>q&@1mOX%O{fuV_XG5{ppR9E z0Ck_BD^VYg#`pqwI`9T*4cM|J@*2NH+=yPm3S0c!czh*(gYJxlu;RqN9>z2PSMCn7gJVL2?lhUOaAk7=q(C#lx*pMSN+!V}~aL4g-=MF={1!qM({ z@Mu)@)|blUIf;}^!<`3VUb^t#+lW!dl+mACp{#bXW^noo8NDRcW8o( zwJ0)}!W<*CfHGc28n4-W@=#3FO$cSKPSV)z35&)mmuwG90Hbg5rVv$pBP`pR9mzA@ z4v?EpAc_|HoqrlrHD4iRm*w%FgKPn70n;FDCY$BUVRgdX(y#>==ax)`n&SBQ zk;EI4Vj1uczXQ&YdL7{=T!)8*FbKke!0~<$1aS0I)ynH(e|&{g`d-S0=qcVVrpn{bxjV46r&Ysxr$!W4G%NAb gPFveF&wB2NZb_EUm`TrfK)_GsrRIxDdGq)G2h&sN4*&oF literal 36439 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 D@XlCm diff --git a/docs/requirements.txt b/docs/requirements.txt index f30c4b993..d168c88f0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,5 @@ -griffe==0.30.1 -mkdocs==1.4.2 -mkdocs-material==9.0.11 -mkdocstrings==0.22.0 -mkdocstrings-python==1.1.2 +mkdocs==1.5.2 +mkdocs-material==9.1.15 mkdocs-version-annotations==1.0.0 +mkdocstrings-python==1.5.2 +mkdocstrings==0.22.0 diff --git a/docs/user/app_getting_started.md b/docs/user/app_getting_started.md index 6cf1e80c2..24ea00997 100644 --- a/docs/user/app_getting_started.md +++ b/docs/user/app_getting_started.md @@ -1,40 +1,19 @@ # Getting Started with the App +This document provides a step-by-step tutorial on how to get the App going and how to use it. + ## Install the App To install the App, please follow the instructions detailed in the [Installation Guide](../admin/install.md). ## First steps with the App -By default this Nautobot app provides an example Data Source Job and example Data Target Job. You can run this example job to get a feel for the capabilities of the Nautobot app. - ---- -![Example Jobs](../images/example_jobs.png) - - -However, to get the most out of this Nautobot app you will want to find other existing Jobs and/or [create your own Jobs](../dev/jobs.md). Such Jobs can be installed like any other Nautobot Job: - -* by [packaging into a Nautobot Nautobot app](https://nautobot.readthedocs.io/en/stable/plugins/development/#including-jobs) which can then be installed into Nautobot's virtual environment -* by [inclusion in a Git repository](https://nautobot.readthedocs.io/en/stable/models/extras/gitrepository/#jobs) which can be configured in Nautobot and refreshed on demand -* by [manual installation of individual Job source files](https://nautobot.readthedocs.io/en/stable/additional-features/jobs/#writing-jobs) to Nautobot's `JOBS_ROOT` directory - - -Example screenshots of possible Data Sources and Data Targets are shown below. - ---- - -![Example data source - Arista CloudVision](../images/example_cloudvision.png) - ---- +!!! warning "Developer Note - Remove Me!" + What (with screenshots preferably) does it look like to perform the simplest workflow within the App once installed? -![Example data target - ServiceNow](../images/example_servicenow.png) +## What are the next steps? -Once you have other, more useful Jobs installed, these example Jobs can be disabled and removed from the UI by configuring `"hide_example_jobs"` to `True` in your `nautobot_config.py`: +!!! warning "Developer Note - Remove Me!" + After taking the first steps, what else could the users look at doing. -```python -PLUGINS_CONFIG = { - "nautobot_ssot": { - "hide_example_jobs": True, - } -} -``` +You can check out the [Use Cases](app_use_cases.md) section for more examples. diff --git a/docs/user/app_overview.md b/docs/user/app_overview.md index 0be82f47b..06ff5d327 100644 --- a/docs/user/app_overview.md +++ b/docs/user/app_overview.md @@ -1,24 +1,30 @@ # App Overview -An app for Nautobot. This Nautobot app facilitates integration and data synchronization between various "source of truth" (SoT) systems, with Nautobot acting as a central clearinghouse for data - a Single Source of Truth, if you will. +This document provides an overview of the App including critical information and import considerations when applying it to your Nautobot environment. !!! note Throughout this documentation, the terms "app" and "plugin" will be used interchangeably. ## Description -The Nautobot SSoT app builds atop the [DiffSync](https://github.com/networktocode/diffsync) Python library and Nautobot's Jobs feature. This enables the rapid development and integration of Jobs that can be run within Nautobot to pull data from other systems ("Data Sources") into Nautobot and/or push data from Nautobot into other systems ("Data Targets") as desired. Key features include the following: - -* A dashboard UI lists all registered Data Sources and Data Targets and provides a summary of the synchronization history. -* The outcome of executing of a data synchronization Job is automatically saved to Nautobot's database for later review. -* Detailed logging output generated by DiffSync is automatically captured and saved to the database as well. ## Audience (User Personas) - Who should use this App? -* Nautobot app developers looking to sync data from an outside source into Nautobot and/or vice-versa. +!!! warning "Developer Note - Remove Me!" + Who is this meant for/ who is the common user of this app? ## Authors and Maintainers -* Glenn Matthew (@glennmatthews) -* Christian Adell (@chadell) -* Justin Drew (@jdrew82) +!!! warning "Developer Note - Remove Me!" + Add the team and/or the main individuals maintaining this project. Include historical maintainers as well. + +## Nautobot Features Used + +!!! warning "Developer Note - Remove Me!" + What is shown today in the Installed Plugins page in Nautobot. What parts of Nautobot does it interact with, what does it add etc. ? + +### Extras + +!!! warning "Developer Note - Remove Me!" + Custom Fields - things like which CFs are created by this app? + Jobs - are jobs, if so, which ones, installed by this app? diff --git a/docs/user/app_use_cases.md b/docs/user/app_use_cases.md index 7d855bfd1..dc06944fe 100644 --- a/docs/user/app_use_cases.md +++ b/docs/user/app_use_cases.md @@ -1,87 +1,12 @@ # Using the App -## General Usage - -### Dashboard - -The dashboard UI can be accessed from the **Plugins > Single Source of Truth > Dashboard** menu item in Nautobot. - -![Dashboard](../images/dashboard_initial.png) - -The left side of the dashboard lists all discovered Data Sources and Data Targets. In a fresh installation this will include the "Example Data Source" and "Example Data Target"; when you install additional data synchronization Jobs they will be automatically discovered and included in the dashboard as well. - -The right side of the dashboard lists the ten most recent data syncs executed (if any) and summarizes their outcomes. - -### Data Source/Target details - -From the dashboard UI, you can click on the name of any given Data Source or Data Target to access a detailed view of the integration between this system and Nautobot. - -![Data Source detail view](../images/data_source_detail.png) - -This view lists the configuration (if any) of the Data Source or Data Target, provides a table describing the types of data being mapped between Nautobot and the other system, and, at the bottom of the page, lists the history of data synchronization involving this system. - -### Executing a data sync - -To synchronize data between Nautobot and a given Data Source or Data Target, select the **Sync** button for the desired integration from either the Dashboard view or the detailed view. This will bring up a form similar to that of executing any other Nautobot Job. - -![Job submission form](../images/run_job.png) - -Enter any appropriate parameters here, including selecting whether to execute the synchronization as a "dry run" (identifying data to be synchronized, but not actually making any changes to the system) or as an actual database update, and select **Run Job**. - -You will be redirected to a standard Nautobot "Job Result" view, which will update as the Job is enqueued, begins execution, and eventually completes. When execution is complete, an **SSoT Sync Details** button will appear at the top right of the page; you can select this button for a more detailed view of the outcome. - -![Job Result view](../images/job_result.png) - -### Viewing a data sync record - -The detailed view of a single data synchronization attempt between Nautobot and a Data Source/Target can be accessed from the Job Result view as described in the previous section, or by navigating to **Plugins > Single Source of Truth > History** and selecting the desired record from the table presented in that view. - -![Sync detail view](../images/sync_detail.png) +This document describes common use-cases and scenarios for this App. -This view describes in detail everything that occurred during the data synchronization attempt. The primary **Data Sync** tab summarizes the overall outcome of the sync attempt, including a view of the diffs (if any) identified by DiffSync and a summary of the actions taken (create, update, delete) and their outcomes (success, failure, error). - -The **Job Logs** tab shows any general status messages generated by the data synchronization Job as it executed; this is equivalent to the Nautobot "Job Result" view. - -The **Sync Logs** tab shows the logs captured from DiffSync regarding the individual data records being synchronized, details of any contents or changes of these records, and other detailed information. Sync logs can also be accessed directly via the **Plugins > Single Source of Truth > Logs** menu item if desired. +## General Usage -![Sync logs view](../images/sync_logs.png) +## Use-cases and common workflows ## Screenshots -Here is a consolidated view of all the pages within the SSoT Nautobot app. - ---- - -Initial dashboard showing the data targets, data sources and the last 10 syncs. -![Initial Dashboard](../images/dashboard_initial.png) - ---- - -The detail page of the example data source. -![Example Data Source Detail](../images/data_source_detail.png) - ---- - -The detailed page of the ServiceNow Data Target. -![ServiceNow Data Target Detail](../images/example_servicenow.png) - ---- - -The job form page shown prior to running a job. The fields shown here depend on the job developed. -![Job Form](../images/run_job.png) - ---- - -The job result page of running a sync. -![Example Sync Result](../images/job_result.png) - ---- - -The sync detail page for a given sync. -![Sync Detail](../images/sync_detail.png) - ---- - -The sync logs page for a given sync. -![Sync Logs](../images/sync_logs.png) - +!!! warning "Developer Note - Remove Me!" + Ideally captures every view exposed by the App. Should include a relevant dataset. diff --git a/docs/user/external_interactions.md b/docs/user/external_interactions.md index e00fd9d0b..eaba5b561 100644 --- a/docs/user/external_interactions.md +++ b/docs/user/external_interactions.md @@ -1,57 +1,17 @@ # External Interactions -## External System Integrations - -* When using SSoT to build a custom job, be mindful that, depending on how you are retrieving information from a remote data source, you may need to access over specific ports. - -## Prometheus Metrics +This document describes external dependencies and prerequisites for this App to operate, including system requirements, API endpoints, interconnection or integrations to other applications or services, and similar topics. -Nautobot SSoT will add Prometheus metrics for multiple pieces of data that might be of interest in your environment to the `/api/plugins/capacity-metrics/app-metrics` output if the [Nautobot Capacity Metrics](https://github.com/nautobot/nautobot-plugin-capacity-metrics) app is installed and configured. The following metrics are added: +!!! warning "Developer Note - Remove Me!" + Optional page, remove if not applicable. -The Nautobot SSoT app has the Nautobot Capacity Metrics app as a dependency, but it is up to the admin to enable it in the `nautobot_config.py` configuration. - -### Registered Metrics +## External System Integrations -Below are the currently registered metrics for the Nautobot SSoT App: +### From the App to Other Systems -| Metric Name | Type | Labels | Description | -| ------------------------------------------------- | ----- | -------------------------------------------- | ----------------------------------------------- | -| nautobot_ssot_duration_seconds | Gauge | job, phase | Gives a time duration for each phase of a Job | -| nautobot_ssot_sync_total | Gauge | sync_type | Gives a count of SSoT sync totals based on type | -| nautobot_ssot_operation_total | Gauge | job, operation | Total number of objects for each operation in Job | -| nautobot_ssot_sync_memory_usage_bytes | Gauge | job, phase | Memory usage for Job during each phase | +### From Other Systems to the App -### Sample Prometheus Metrics +## Nautobot REST API endpoints -```prometheus -# HELP nautobot_ssot_duration_seconds Nautobot SSoT Job Phase Duration in seconds -# TYPE nautobot_ssot_duration_seconds gauge -nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="source_load_time"} 5314.937 -nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="target_load_time"} 28241.297 -nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="diff_time"} 1405.652 -nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="sync_time"} 21921.814 -nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="sync_duration"} 98351.03 -# HELP nautobot_ssot_sync_total Nautobot SSoT Sync Totals -# TYPE nautobot_ssot_sync_total gauge -nautobot_ssot_sync_total{sync_type="total_syncs"} 4.0 -nautobot_ssot_sync_total{sync_type="pending_syncs"} 0.0 -nautobot_ssot_sync_total{sync_type="running_syncs"} 0.0 -nautobot_ssot_sync_total{sync_type="completed_syncs"} 3.0 -nautobot_ssot_sync_total{sync_type="errored_syncs"} 0.0 -nautobot_ssot_sync_total{sync_type="failed_syncs"} 1.0 -# HELP nautobot_ssot_operation_total Nautobot SSoT operations by Job -# TYPE nautobot_ssot_operation_total gauge -nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="skip"} 0.0 -nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="create"} 2.0 -nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="delete"} 0.0 -nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="update"} 0.0 -nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="no-change"} 1731.0 -# HELP nautobot_ssot_sync_memory_usage_bytes Nautobot SSoT Sync Memory Usage -# TYPE nautobot_ssot_sync_memory_usage_bytes gauge -nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="skip"} 0.0 -nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="create"} 2.0 -nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="delete"} 0.0 -nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="update"} 0.0 -nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="no-change"} 1731.0 -nautobot_ssot_sync_memory_usage_bytes{job="",phase=""} 0.0 -``` +!!! warning "Developer Note - Remove Me!" + API documentation in this doc - including python request examples, curl examples, postman collections referred etc. diff --git a/docs/user/faq.md b/docs/user/faq.md index ec4b0bc4d..318b08dc2 100644 --- a/docs/user/faq.md +++ b/docs/user/faq.md @@ -1,5 +1 @@ # Frequently Asked Questions - -## _Is the application actually a Single Source of Truth?_ - -In reality the application intends to have behaviors as if it was a SSoT. The difference being, the application intends to aggregate data in the real world where it is not feasible to have the System of Record be in a single system. \ No newline at end of file diff --git a/invoke.example.yml b/invoke.example.yml index 6bdf777df..8ca4164a1 100644 --- a/invoke.example.yml +++ b/invoke.example.yml @@ -1,9 +1,9 @@ --- nautobot_ssot: project_name: "nautobot-ssot" - nautobot_ver: "latest" + nautobot_ver: "2.0.0" local: false - python_ver: "3.8" + python_ver: "3.11" compose_dir: "development" compose_files: - "docker-compose.base.yml" diff --git a/invoke.mysql.yml b/invoke.mysql.yml index 78587ea60..4e218bdba 100644 --- a/invoke.mysql.yml +++ b/invoke.mysql.yml @@ -1,9 +1,9 @@ --- nautobot_ssot: project_name: "nautobot-ssot" - nautobot_ver: "latest" + nautobot_ver: "2.0.0" local: false - python_ver: "3.8" + python_ver: "3.11" compose_dir: "development" compose_files: - "docker-compose.base.yml" diff --git a/mkdocs.yml b/mkdocs.yml index 6c3455f2b..4b77edcad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ --- dev_addr: "127.0.0.1:8001" -edit_uri: "blob/develop/docs" +edit_uri: "edit/main/nautobot-plugin-ssot/docs" site_dir: "nautobot_ssot/static/nautobot_ssot/docs" site_name: "Single Source of Truth Documentation" site_url: "https://docs.nautobot.com/projects/ssot/en/latest/" @@ -14,16 +14,17 @@ theme: - "django" - "yaml" features: - - "navigation.tracking" + - "content.action.edit" + - "content.action.view" + - "content.code.copy" + - "navigation.footer" + - "navigation.indexes" - "navigation.tabs" - "navigation.tabs.sticky" - - "navigation.footer" - - "search.suggest" + - "navigation.tracking" - "search.highlight" - "search.share" - - "navigation.indexes" - - "content.action.edit" - - "content.action.view" + - "search.suggest" favicon: "assets/favicon.ico" logo: "assets/nautobot_logo.svg" palette: @@ -101,48 +102,23 @@ nav: - App Overview: "user/app_overview.md" - Getting Started: "user/app_getting_started.md" - Using the App: "user/app_use_cases.md" - - Integrations: - - "user/integrations/index.md" - - Cisco ACI: "user/integrations/aci.md" - - Arista CloudVision: "user/integrations/aristacv.md" - - Device42: "user/integrations/device42.md" - - Infoblox: "user/integrations/infoblox.md" - - IPFabric: "user/integrations/ipfabric.md" - - ServiceNow: "user/integrations/servicenow.md" - - Modeling: "user/modeling.md" - - Performance: "user/performance.md" - Frequently Asked Questions: "user/faq.md" - External Interactions: "user/external_interactions.md" - Administrator Guide: - Install and Configure: "admin/install.md" - - Integrations Installation: - - "admin/integrations/index.md" - - Cisco ACI: "admin/integrations/aci_setup.md" - - Arista CloudVision: "admin/integrations/aristacv_setup.md" - - Device42: "admin/integrations/device42_setup.md" - - Infoblox: "admin/integrations/infoblox_setup.md" - - IPFabric: "admin/integrations/ipfabric_setup.md" - - ServiceNow: "admin/integrations/servicenow_setup.md" - Upgrade: "admin/upgrade.md" - Uninstall: "admin/uninstall.md" - Compatibility Matrix: "admin/compatibility_matrix.md" - Release Notes: - "admin/release_notes/index.md" - - v2.0: "admin/release_notes/version_2.0.md" - - v1.6: "admin/release_notes/version_1.5.md" - - v1.5: "admin/release_notes/version_1.5.md" - - v1.4: "admin/release_notes/version_1.4.md" - - v1.3: "admin/release_notes/version_1.3.md" - - v1.2: "admin/release_notes/version_1.2.md" - - v1.1: "admin/release_notes/version_1.1.md" - v1.0: "admin/release_notes/version_1.0.md" - Developer Guide: - Extending the App: "dev/extending.md" - - Developing Jobs: "dev/jobs.md" - Contributing to the App: "dev/contributing.md" - Development Environment: "dev/dev_environment.md" + - Architecture Decision Records: "dev/arch_decision.md" - Code Reference: - "dev/code_reference/index.md" - - Models: "dev/code_reference/models.md" - - Other classes: "dev/other_classes_reference.md" + - Package: "dev/code_reference/package.md" + - API: "dev/code_reference/api.md" - Nautobot Docs Home ↗︎: "https://docs.nautobot.com" diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index 0aae49685..56c32a5b0 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -1,131 +1,26 @@ """Plugin declaration for nautobot_ssot.""" -import os +# Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added from importlib import metadata -from django.conf import settings -from nautobot.extras.plugins import PluginConfig -from nautobot.core.settings_funcs import is_truthy - -from nautobot_ssot.integrations.utils import each_enabled_integration_module -from nautobot_ssot.utils import logger - __version__ = metadata.version(__name__) - -_CONFLICTING_APP_NAMES = [ - "nautobot_ssot_aci", - "nautobot_ssot_aristacv", - "nautobot_ssot_device42", - "nautobot_ssot_infoblox", - "nautobot_ssot_ipfabric", - "nautobot_ssot_servicenow", -] - - -def _check_for_conflicting_apps(): - intersection = set(_CONFLICTING_APP_NAMES).intersection(set(settings.PLUGINS)) - if intersection: - raise RuntimeError( - f"The following apps are installed and conflict with `nautobot-ssot`: {', '.join(intersection)}." - "See: https://docs.nautobot.com/projects/ssot/en/latest/admin/upgrade/#potential-apps-conflicts" - ) +from nautobot.extras.plugins import NautobotAppConfig -if not is_truthy(os.getenv("NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS", "False")): - _check_for_conflicting_apps() - - -class NautobotSSOTPluginConfig(PluginConfig): +class NautobotSSOTPluginConfig(NautobotAppConfig): """Plugin configuration for the nautobot_ssot plugin.""" name = "nautobot_ssot" verbose_name = "Single Source of Truth" version = __version__ author = "Network to Code, LLC" - description = "Nautobot app that enables Single Source of Truth. Allows users to aggregate distributed data sources and/or distribute Nautobot data to other data sources such as databases and SDN controllers." + description = "Nautobot Single Source of Truth." base_url = "ssot" required_settings = [] min_version = "2.0.0" max_version = "2.9999" - default_settings = { - "aci_apics": [], - "aci_tag": "", - "aci_tag_color": "", - "aci_tag_up": "", - "aci_tag_up_color": "", - "aci_tag_down": "", - "aci_tag_down_color": "", - "aci_manufacturer_name": "", - "aci_ignore_tenants": [], - "aci_comments": "", - "aci_site": "", - "aristacv_apply_import_tag": False, - "aristacv_controller_site": "", - "aristacv_create_controller": False, - "aristacv_cvaas_url": "www.arista.io:443", - "aristacv_cvp_host": "", - "aristacv_cvp_password": "", - "aristacv_cvp_port": "443", - "aristacv_cvp_token": "", - "aristacv_cvp_user": "", - "aristacv_delete_devices_on_sync": False, - "aristacv_from_cloudvision_default_device_role": "", - "aristacv_from_cloudvision_default_device_role_color": "", - "aristacv_from_cloudvision_default_site": "", - "aristacv_hostname_patterns": [], - "aristacv_import_active": False, - "aristacv_role_mappings": {}, - "aristacv_site_mappings": {}, - "aristacv_verify": True, - "device42_host": "", - "device42_username": "", - "device42_password": "", - "device42_defaults": {}, - "device42_delete_on_sync": False, - "device42_use_dns": True, - "device42_customer_is_facility": True, - "device42_facility_prepend": "", - "device42_role_prepend": "", - "device42_ignore_tag": "", - "device42_hostname_mapping": [], - "enable_aci": False, - "enable_aristacv": False, - "enable_device42": False, - "enable_infoblox": False, - "enable_ipfabric": False, - "enable_servicenow": False, - "hide_example_jobs": True, - "infoblox_default_status": "", - "infoblox_enable_rfc1918_network_containers": False, - "infoblox_enable_sync_to_infoblox": False, - "infoblox_import_objects_ip_addresses": False, - "infoblox_import_objects_subnets": False, - "infoblox_import_objects_vlan_views": False, - "infoblox_import_objects_vlans": False, - "infoblox_import_subnets": [], - "infoblox_password": "", - "infoblox_url": "", - "infoblox_username": "", - "infoblox_verify_ssl": True, - "infoblox_wapi_version": "", - "ipfabric_api_token": "", - "ipfabric_host": "", - "ipfabric_ssl_verify": True, - "ipfabric_timeout": 15, - "ipfabric_nautobot_host": "", - "servicenow_instance": "", - "servicenow_password": "", - "servicenow_username": "", - } + default_settings = {} caching_config = {} - def ready(self): - """Trigger callback when database is ready.""" - super().ready() - - for module in each_enabled_integration_module("signals"): - logger.debug("Registering signals for %s", module.__file__) - module.register_signals(self) - config = NautobotSSOTPluginConfig # pylint:disable=invalid-name diff --git a/nautobot_ssot/tests/__init__.py b/nautobot_ssot/tests/__init__.py index 547039b14..37afdda70 100644 --- a/nautobot_ssot/tests/__init__.py +++ b/nautobot_ssot/tests/__init__.py @@ -1,7 +1 @@ """Unit tests for nautobot_ssot plugin.""" - -from django.conf import settings - -if "job_logs" in settings.DATABASES: - settings.DATABASES["job_logs"] = settings.DATABASES["job_logs"].copy() - settings.DATABASES["job_logs"]["TEST"] = {"MIRROR": "default"} diff --git a/nautobot_ssot/tests/test_api.py b/nautobot_ssot/tests/test_api.py index fcf0ed9c1..edf5738ff 100644 --- a/nautobot_ssot/tests/test_api.py +++ b/nautobot_ssot/tests/test_api.py @@ -1,19 +1,19 @@ """Unit tests for nautobot_ssot.""" from django.contrib.auth import get_user_model +from django.test import TestCase from django.urls import reverse -from nautobot.users.models import Token -from nautobot.core.testing import TestCase from rest_framework import status from rest_framework.test import APIClient +from nautobot.users.models import Token User = get_user_model() class PlaceholderAPITest(TestCase): - """Test the nautobot_ssot API.""" + """Test the NautobotSSOTPlugin API.""" - def setUp(self): # pylint: disable=invalid-name + def setUp(self): """Create a superuser and token for API calls.""" self.user = User.objects.create(username="testuser", is_superuser=True) self.token = Token.objects.create(user=self.user) diff --git a/nautobot_ssot/tests/test_basic.py b/nautobot_ssot/tests/test_basic.py index fb1b556d7..126027375 100644 --- a/nautobot_ssot/tests/test_basic.py +++ b/nautobot_ssot/tests/test_basic.py @@ -3,14 +3,27 @@ import os import toml +from nautobot_ssot import __version__ as project_version + + +class TestVersion(unittest.TestCase): + """Test Version is the same.""" + + def test_version(self): + """Verify that pyproject.toml version is same as version specified in the package.""" + parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + poetry_version = toml.load(os.path.join(parent_path, "pyproject.toml"))["tool"]["poetry"]["version"] + self.assertEqual(project_version, poetry_version) + class TestDocsPackaging(unittest.TestCase): """Test Version in doc requirements is the same pyproject.""" def test_version(self): - """Verify that pyproject.toml dev dependecies have the same versions as in the docs requirements.txt.""" + """Verify that pyproject.toml dev dependencies have the same versions as in the docs requirements.txt.""" parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) - poetry_details = toml.load(os.path.join(parent_path, "pyproject.toml"))["tool"]["poetry"]["dev-dependencies"] + poetry_path = os.path.join(parent_path, "pyproject.toml") + poetry_details = toml.load(poetry_path)["tool"]["poetry"]["group"]["dev"]["dependencies"] with open(f"{parent_path}/docs/requirements.txt", "r", encoding="utf-8") as file: requirements = [line for line in file.read().splitlines() if (len(line) > 0 and not line.startswith("#"))] for pkg in requirements: diff --git a/pyproject.toml b/pyproject.toml index 4337687be..5bd452b62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,12 @@ [tool.poetry] name = "nautobot-ssot" -version = "2.0.0" +version = "0.1.0" description = "Nautobot Single Source of Truth" -authors = ["Network to Code, LLC "] +authors = ["Network to Code, LLC "] license = "Apache-2.0" readme = "README.md" -homepage = "https://github.com/nautobot/nautobot-plugin-ssot" -repository = "https://github.com/nautobot/nautobot-plugin-ssot" -documentation = "https://nautobot-plugin-ssot.readthedocs.io" +homepage = "https://github.com/nautobot/nautobot-plugin-ssot/" +repository = "https://github.com/nautobot/nautobot-plugin-ssot/" keywords = ["nautobot", "nautobot-plugin"] classifiers = [ "Intended Audience :: Developers", @@ -27,143 +26,34 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.8,<3.12" +python = ">=3.8,<3.12" +# Used for local development nautobot = "^2.0.0" -diffsync = "^1.6.0" -Jinja2 = { version = ">=2.11.3", optional = true } -Markdown = "!=3.3.5" -PyYAML = { version = ">=6", optional = true } -cloudvision = { version = "^1.9.0", optional = true } -cvprac = { version = "^1.2.2", optional = true } -dnspython = { version = "^2.1.0", optional = true } -nautobot-device-lifecycle-mgmt = { version = "^2.0.0", optional = true } -packaging = ">=21.3, <24" -prometheus-client = "~0.17.1" -ijson = { version = ">=2.5.1", optional = true } -ipfabric = { version = "~6.0.9", optional = true } -ipfabric-diagrams = { version = "~6.0.2", optional = true } -netutils = { version = "^1.0.0", optional = true } -oauthlib = { version = ">=3.1.0", optional = true } -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 } -six = { version = ">=1.13.0", optional = true } -drf-spectacular = "0.26.3" -httpx = { version = ">=0.23.3", optional = true } -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] bandit = "*" black = "*" coverage = "*" django-debug-toolbar = "*" -django-extensions = "*" -# we need to pin flake8 because of package dependencies that cause it to downgrade and -# therefore cause issues with linting since older versions do not take .flake8 as config -flake8 = "^3.9.2" +flake8 = "*" invoke = "*" ipython = "*" -jedi = "^0.17.2" pydocstyle = "*" pylint = "*" pylint-django = "*" pylint-nautobot = "*" -pytest = "*" -python-dotenv = "^0.21.1" yamllint = "*" -markdown-include = "*" toml = "*" +Markdown = "*" # Rendering docs to HTML -mkdocs = "1.4.2" +mkdocs = "1.5.2" # Material for MkDocs theme -mkdocs-material = "9.0.11" +mkdocs-material = "9.1.15" # Render custom markdown for version added/changed/remove notes mkdocs-version-annotations = "1.0.0" # Automatic documentation from sources, for MkDocs -griffe = "0.30.1" mkdocstrings = "0.22.0" -mkdocstrings-python = "1.1.2" -requests-mock = "^1.10.0" -parameterized = "^0.8.1" -myst-parser = "^0.15.2" -nautobot-chatops = { version = "^3.0.0", extras = ["ipfabric"] } -responses = "^0.14.0" - -[tool.poetry.plugins."nautobot_ssot.data_sources"] -"example" = "nautobot_ssot.sync.example:ExampleSyncWorker" - -[tool.poetry.plugins."nautobot_ssot.data_targets"] -"example" = "nautobot_ssot.sync.example:ExampleSyncWorker" - -[tool.poetry.plugins."nautobot.workers"] -"ipfabric" = "nautobot_ssot.integrations.ipfabric.workers:ipfabric" - -[tool.poetry.extras] -aci = [ - "PyYAML", -] -all = [ - "Jinja2", - "PyYAML", - "cloudvision", - "cvprac", - "dnspython", - "ijson", - "ipfabric", - "ipfabric-diagrams", - "nautobot-device-lifecycle-mgmt", - "netutils", - "oauthlib", - "python-magic", - "pytz", - "requests", - "requests-oauthlib", - "six", -] -aristacv = [ - "cloudvision", - "cvprac", -] -device42 = [ - "requests", -] -infoblox = [ - "dnspython", -] -ipfabric = [ - "httpx", - "ipfabric", - "ipfabric-diagrams", - "netutils", -] -# 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 -# For now, we have embedded a copy of PySNow under nautobot_ssot/integrations/servicenow/third_party/pysnow; -# here are its direct packaging dependencies: -pysnow = [ - "requests", - "oauthlib", - "python-magic", - "requests-oauthlib", - "six", - "ijson", - "pytz", -] -servicenow = [ - "Jinja2", - "PyYAML", - "ijson", - "oauthlib", - "python-magic", - "pytz", - "requests", - "requests-oauthlib", - "six", -] -nautobot-device-lifecycle-mgmt = [ - "nautobot-device-lifecycle-mgmt", -] +mkdocstrings-python = "1.5.2" [tool.black] line-length = 120 @@ -182,7 +72,6 @@ exclude = ''' | buck-out | build | dist - | nautobot_ssot/integrations/servicenow/third_party )/ | settings.py # This is where you define files that should not be stylized by black # the root of the project @@ -191,14 +80,9 @@ exclude = ''' [tool.pylint.master] # Include the pylint_django plugin to avoid spurious warnings about Django patterns -load-plugins="pylint_django,pylint_nautobot" +load-plugins="pylint_django, pylint_nautobot" ignore=".venv" -[tool.pylint-nautobot] -supported_nautobot_versions = [ - "2", # Supporting 2.x.y -] - [tool.pylint.basic] # No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes. no-docstring-rgx="^(_|test_|Meta$)" @@ -207,8 +91,7 @@ no-docstring-rgx="^(_|test_|Meta$)" # Line length is enforced by Black, so pylint doesn't need to check it. # Pylint and Black disagree about how to format multi-line arrays; Black wins. disable = """, - line-too-long, - too-few-public-methods, + line-too-long """ [tool.pylint.miscellaneous] @@ -218,6 +101,11 @@ notes = """, XXX, """ +[tool.pylint-nautobot] +supported_nautobot_versions = [ + "2.0.0" +] + [tool.pydocstyle] convention = "google" inherit = false @@ -233,9 +121,3 @@ add_ignore = "D212" [build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" - -[tool.pytest.ini_options] -testpaths = [ - "tests" -] -addopts = "-vv --doctest-modules" diff --git a/tasks.py b/tasks.py index 8680b0a45..93d33a4f4 100644 --- a/tasks.py +++ b/tasks.py @@ -12,16 +12,10 @@ limitations under the License. """ -from distutils.util import strtobool -from invoke import Collection, task as invoke_task import os -from dotenv import load_dotenv - - -def _load_dotenv(): - load_dotenv("./development/development.env") - load_dotenv("./development/creds.env") +from invoke.collection import Collection +from invoke.tasks import task as invoke_task def is_truthy(arg): @@ -36,7 +30,14 @@ def is_truthy(arg): """ if isinstance(arg, bool): return arg - return bool(strtobool(arg)) + + val = str(arg).lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError(f"Invalid truthy value: `{arg}`") # Use pyinvoke configuration for default values, see http://docs.pyinvoke.org/en/stable/concepts/configuration.html @@ -46,8 +47,8 @@ def is_truthy(arg): { "nautobot_ssot": { "nautobot_ver": "2.0.0", - "project_name": "nautobot_ssot", - "python_ver": "3.8", + "project_name": "nautobot-ssot", + "python_ver": "3.11", "local": False, "compose_dir": os.path.join(os.path.dirname(__file__), "development"), "compose_files": [ @@ -62,6 +63,10 @@ def is_truthy(arg): ) +def _is_compose_included(context, name): + return f"docker-compose.{name}.yml" in context.nautobot_ssot.compose_files + + def task(function=None, *args, **kwargs): """Task decorator to override the default Invoke task decorator and add each task to the invoke namespace.""" @@ -167,10 +172,17 @@ def generate_packages(context): run_command(context, command) -@task -def lock(context): +@task( + help={ + "check": ( + "If enabled, check for outdated dependencies in the poetry.lock file, " + "instead of generating a new one. (default: disabled)" + ) + } +) +def lock(context, check=False): """Generate poetry.lock inside the Nautobot container.""" - run_command(context, "poetry lock --no-update") + run_command(context, f"poetry {'check' if check else 'lock --no-update'}") # ------------------------------------------------------------------------------ @@ -358,172 +370,144 @@ def exec(context, service="nautobot", command="bash", file=""): @task( help={ + "db-name": "Database name (default: Nautobot database)", + "input-file": "SQL file to execute and quit (default: empty, start interactive CLI)", + "output-file": "Ouput file, overwrite if exists (default: empty, output to stdout)", "query": "SQL command to execute and quit (default: empty)", - "input": "SQL file to execute and quit (default: empty)", - "output": "Ouput file, overwrite if exists (default: empty)", } ) -def dbshell(context, query="", input="", output=""): +def dbshell(context, db_name="", input_file="", output_file="", query=""): """Start database CLI inside the running `db` container. Doesn't use `nautobot-server dbshell`, using started `db` service container only. """ - if input and query: - raise ValueError("Cannot specify both, `input` and `query` arguments") - if output and not (input or query): - raise ValueError("`output` argument requires `input` or `query` argument") + if input_file and query: + raise ValueError("Cannot specify both, `input_file` and `query` arguments") + if output_file and not (input_file or query): + raise ValueError("`output_file` argument requires `input_file` or `query` argument") - _load_dotenv() + env = {} + if query: + env["_SQL_QUERY"] = query - service = "db" - env_vars = {} - command = ["exec"] + command = [ + "exec", + "--env=_SQL_QUERY" if query else "", + "-- db sh -c '", + ] - if "docker-compose.mysql.yml" in context.nautobot_ssot.compose_files: - env_vars["MYSQL_PWD"] = os.getenv("MYSQL_PASSWORD") + if _is_compose_included(context, "mysql"): command += [ - "--env=MYSQL_PWD", - "--", - service, "mysql", - f"--user='{os.getenv('MYSQL_USER')}'", - f"--database='{os.getenv('MYSQL_DATABASE')}'", + "--user=$MYSQL_USER", + "--password=$MYSQL_PASSWORD", + f"--database={db_name or '$MYSQL_DATABASE'}", ] - if query: - command += [f"--execute='{query}'"] - elif "docker-compose.postgres.yml" in context.nautobot_ssot.compose_files: + elif _is_compose_included(context, "postgres"): command += [ - "--", - service, "psql", - f"--username='{os.getenv('POSTGRES_USER')}'", - f"--dbname='{os.getenv('POSTGRES_DB')}'", + "--username=$POSTGRES_USER", + f"--dbname={db_name or '$POSTGRES_DB'}", ] - if query: - command += [f"--command='{query}'"] else: raise ValueError("Unsupported database backend.") - if input: - command += [f"< '{input}'"] - if output: - command += [f"> '{output}'"] + command += [ + "'", + '<<<"$_SQL_QUERY"' if query else "", + f"< '{input_file}'" if input_file else "", + f"> '{output_file}'" if output_file else "", + ] - docker_compose(context, " ".join(command), env=env_vars, pty=not (input or output or query)) + docker_compose(context, " ".join(command), env=env, pty=not (input_file or output_file or query)) @task( help={ - "input": "SQL dump file to replace the existing database with. This can be generated using `invoke backup-db` (default: `dump.sql`).", + "input-file": "SQL dump file to replace the existing database with. This can be generated using `invoke backup-db` (default: `dump.sql`).", } ) -def import_db(context, input="dump.sql"): +def import_db(context, input_file="dump.sql"): """Stop Nautobot containers and replace the current database with the dump into the running `db` container.""" docker_compose(context, "stop -- nautobot worker") - _load_dotenv() - - service = "db" - env_vars = {} - command = ["exec"] + command = ["exec -- db sh -c '"] - if "docker-compose.mysql.yml" in context.nautobot_ssot.compose_files: - env_vars["MYSQL_PWD"] = os.getenv("MYSQL_PASSWORD") + if _is_compose_included(context, "mysql"): command += [ - "--env=MYSQL_PWD", - "--", - service, "mysql", - f"--user='{os.getenv('MYSQL_USER')}'", - f"--database='{os.getenv('MYSQL_DATABASE')}'", + "--database=$MYSQL_DATABASE", + "--user=$MYSQL_USER", + "--password=$MYSQL_PASSWORD", ] - elif "docker-compose.postgres.yml" in context.nautobot_ssot.compose_files: + elif _is_compose_included(context, "postgres"): command += [ - "--", - service, "psql", - f"--username='{os.getenv('POSTGRES_USER')}'", + "--username=$POSTGRES_USER", "postgres", ] else: raise ValueError("Unsupported database backend.") - command += [f"< '{input}'"] + command += [ + "'", + f"< '{input_file}'", + ] - docker_compose(context, " ".join(command), env=env_vars, pty=False) + docker_compose(context, " ".join(command), pty=False) print("Database import complete, you can start Nautobot now: `invoke start`") @task( help={ - "output": "Ouput file, overwrite if exists (default: `dump.sql`)", + "db-name": "Database name to backup (default: Nautobot database)", + "output-file": "Ouput file, overwrite if exists (default: `dump.sql`)", "readable": "Flag to dump database data in more readable format (default: `True`)", } ) -def backup_db(context, output="dump.sql", readable=True): - """Dump database into `output` file from running `db` container.""" - _load_dotenv() - - service = "db" - env_vars = {} - command = ["exec"] +def backup_db(context, db_name="", output_file="dump.sql", readable=True): + """Dump database into `output_file` file from running `db` container.""" + command = ["exec -- db sh -c '"] - if "docker-compose.mysql.yml" in context.nautobot_ssot.compose_files: - env_vars["MYSQL_PWD"] = os.getenv("MYSQL_ROOT_PASSWORD") + if _is_compose_included(context, "mysql"): command += [ - "--env=MYSQL_PWD", - "--", - service, "mysqldump", "--user=root", + "--password=$MYSQL_ROOT_PASSWORD", "--add-drop-database", "--skip-extended-insert" if readable else "", "--databases", - os.getenv("MYSQL_DATABASE", ""), + db_name if db_name else "$MYSQL_DATABASE", ] - elif "docker-compose.postgres.yml" in context.nautobot_ssot.compose_files: + elif _is_compose_included(context, "postgres"): command += [ - "--", - service, "pg_dump", "--clean", "--create", "--if-exists", - f"--username='{os.getenv('POSTGRES_USER')}'", - f"--dbname='{os.getenv('POSTGRES_DB')}'", + "--username=$POSTGRES_USER", + f"--dbname={db_name or '$POSTGRES_DB'}", + "--inserts" if readable else "", ] - - if readable: - command += ["--inserts"] else: raise ValueError("Unsupported database backend.") - if output: - command += [f"> '{output}'"] + command += [ + "'", + f"> '{output_file}'", + ] - docker_compose(context, " ".join(command), env=env_vars, pty=False) + docker_compose(context, " ".join(command), pty=False) print(50 * "=") - print("The database backup has been successfully completed and saved to the file:") - print(output) - print("If you want to import this database backup, please execute the following command:") - print(f"invoke import-db --input '{output}'") + print("The database backup has been successfully completed and saved to the following file:") + print(output_file) + print("You can import this database backup with the following command:") + print(f"invoke import-db --input-file '{output_file}'") print(50 * "=") -@task(name="help") -def help_task(context): - """Print the help of available tasks.""" - import tasks # pylint: disable=all - - root = Collection.from_module(tasks) - for task_name in sorted(root.task_names): - print(50 * "-") - print(f"invoke {task_name} --help") - context.run(f"invoke {task_name} --help") - - # ------------------------------------------------------------------------------ # DOCS # ------------------------------------------------------------------------------ @@ -533,10 +517,29 @@ def docs(context): command = "mkdocs serve -v" if is_truthy(context.nautobot_ssot.local): - print("Serving Documentation...") + print(">>> Serving Documentation at http://localhost:8001") run_command(context, command) else: - print("Only used when developing locally (i.e. context.nautobot_ssot.local=True)!") + start(context, service="docs") + + +@task +def build_and_check_docs(context): + """Build documentation to be available within Nautobot.""" + command = "mkdocs build --no-directory-urls --strict" + run_command(context, command) + + +@task(name="help") +def help_task(context): + """Print the help of available tasks.""" + import tasks # pylint: disable=all + + root = Collection.from_module(tasks) + for task_name in sorted(root.task_names): + print(50 * "-") + print(f"invoke {task_name} --help") + context.run(f"invoke {task_name} --help") # ------------------------------------------------------------------------------ @@ -597,7 +600,7 @@ def bandit(context): @task def yamllint(context): - """Run yamllint to validate formating adheres to NTC defined YAML standards. + """Run yamllint to validate formatting adheres to NTC defined YAML standards. Args: context (obj): Used to run specific commands @@ -609,7 +612,7 @@ def yamllint(context): @task def check_migrations(context): """Check for missing migrations.""" - command = "nautobot-server --config=nautobot/core/tests/nautobot_config.py makemigrations --dry-run --check" + command = "nautobot-server makemigrations --dry-run --check" run_command(context, command) @@ -621,9 +624,18 @@ def check_migrations(context): "failfast": "fail as soon as a single test fails don't run the entire test suite", "buffer": "Discard output from passing tests", "pattern": "Run specific test methods, classes, or modules instead of all tests", + "verbose": "Enable verbose test output.", } ) -def unittest(context, keepdb=False, label="nautobot_ssot", failfast=False, buffer=True, pattern=""): +def unittest( + context, + keepdb=False, + label="nautobot_ssot", + failfast=False, + buffer=True, + pattern="", + verbose=False, +): """Run Nautobot unit tests.""" command = f"coverage run --module nautobot.core.cli test {label}" @@ -635,6 +647,9 @@ def unittest(context, keepdb=False, label="nautobot_ssot", failfast=False, buffe command += " --buffer" if pattern: command += f" -k='{pattern}'" + if verbose: + command += " --verbosity 2" + run_command(context, command) @@ -648,10 +663,12 @@ def unittest_coverage(context): @task( help={ - "failfast": "fail as soon as a single test fails don't run the entire test suite", + "failfast": "fail as soon as a single test fails don't run the entire test suite. (default: False)", + "keepdb": "Save and re-use test database between test runs for faster re-testing. (default: False)", + "lint-only": "Only run linters; unit tests will be excluded. (default: False)", } ) -def tests(context, failfast=False): +def tests(context, failfast=False, keepdb=False, lint_only=False): """Run all tests for this plugin.""" # If we are not running locally, start the docker containers so we don't have to for each test if not is_truthy(context.nautobot_ssot.local): @@ -668,9 +685,16 @@ def tests(context, failfast=False): pydocstyle(context) print("Running yamllint...") yamllint(context) + print("Running poetry check...") + lock(context, check=True) + print("Running migrations check...") + check_migrations(context) print("Running pylint...") pylint(context) - print("Running unit tests...") - unittest(context, failfast=failfast) + print("Running mkdocs...") + build_and_check_docs(context) + if not lint_only: + print("Running unit tests...") + unittest(context, failfast=failfast, keepdb=keepdb) + unittest_coverage(context) print("All tests have passed!") - unittest_coverage(context) From 8ce6d6f5c8b8a4e8f63379b6cbe1bcb1318bfcb4 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Fri, 20 Oct 2023 08:27:49 +0000 Subject: [PATCH 2/5] chore: Manual fixes --- .bandit.yml | 2 +- .cookiecutter.json | 2 +- .flake8 | 1 + .github/CODEOWNERS | 2 +- .github/workflows/ci.yml | 2 +- .yamllint.yml | 1 + README.md | 130 +++++++++++++++++++----- development/Dockerfile | 4 +- development/creds.example.env | 21 ++++ development/development.env | 59 +++++++++++ development/nautobot_config.py | 117 +++++++++++++++++++-- docs/admin/compatibility_matrix.md | 17 +++- docs/admin/install.md | 57 +++++++---- docs/admin/release_notes/version_1.0.md | 45 ++------ docs/admin/uninstall.md | 9 +- docs/admin/upgrade.md | 36 ++++++- docs/assets/extra.css | 5 + docs/dev/code_reference/index.md | 3 - docs/dev/contributing.md | 38 +++++-- docs/dev/dev_environment.md | 20 ++-- docs/dev/extending.md | 3 - docs/images/icon-nautobot-ssot.png | Bin 74601 -> 36439 bytes docs/user/app_getting_started.md | 37 ++++++- docs/user/app_overview.md | 15 ++- docs/user/app_use_cases.md | 83 ++++++++++++++- docs/user/external_interactions.md | 58 +++++++++-- docs/user/faq.md | 4 + mkdocs.yml | 28 +++++ nautobot_ssot/__init__.py | 113 +++++++++++++++++++- nautobot_ssot/tests/__init__.py | 6 ++ nautobot_ssot/tests/test_api.py | 4 +- poetry.lock | 109 ++++---------------- pyproject.toml | 112 +++++++++++++++++++- 33 files changed, 897 insertions(+), 246 deletions(-) diff --git a/.bandit.yml b/.bandit.yml index 56f7a83b1..f080f8bfe 100644 --- a/.bandit.yml +++ b/.bandit.yml @@ -1,5 +1,5 @@ --- -skips: [] +skips: ["B113"] # No need to check for security issues in the test scripts! exclude_dirs: - "./tests/" diff --git a/.cookiecutter.json b/.cookiecutter.json index 317323f4b..e37d5a731 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -24,7 +24,7 @@ "template_ref": "develop", "cookie_dir": "", "branch_prefix": "drift-manager", - "pull_request_strategy": "update-or-create", + "pull_request_strategy": "create", "post_actions": [ "black" ], diff --git a/.flake8 b/.flake8 index c9f5e84df..18a91265c 100644 --- a/.flake8 +++ b/.flake8 @@ -8,3 +8,4 @@ exclude = manage.py, settings.py, .venv + nautobot_ssot/integrations/servicenow/third_party diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 283f0a2a4..af13990a1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Default owner(s) of all files in this repository -* @smith-ntc +* @nautobot/plugin-ssot diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7faf21d5..12857f050 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -242,7 +242,7 @@ jobs: - name: "Upload binaries to release" uses: "svenstaro/upload-release-action@v2" with: - repo_token: "${{ secrets.NTC_GITHUB_TOKEN }}" # use GH_NAUTOBOT_BOT_TOKEN for Nautobot Org repos. + repo_token: "${{ secrets.GH_NAUTOBOT_BOT_TOKEN }}" file: "dist/*" tag: "${{ github.ref }}" overwrite: true diff --git a/.yamllint.yml b/.yamllint.yml index 8cc3e9a9f..16d28826d 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -10,4 +10,5 @@ rules: quote-type: "double" ignore: | .venv/ + nautobot_ssot/integrations/aci/diffsync/device-types/ compose.yaml diff --git a/README.md b/README.md index a12295790..1bf3ef6e3 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,4 @@ -# Single Source of Truth - - +# Nautobot Single Source of Truth (SSoT) + ## Overview -> Developer Note: Add a long (2-3 paragraphs) description of what the App does, what problems it solves, what functionality it adds to Nautobot, what external systems it works with etc. +An app for [Nautobot](https://github.com/nautobot/nautobot). This Nautobot app facilitates integration and data synchronization between various "source of truth" (SoT) systems, with Nautobot acting as a central clearinghouse for data - a Single Source of Truth, if you will. + +The Nautobot SSoT app builds atop the [DiffSync](https://github.com/networktocode/diffsync) Python library and Nautobot's Jobs feature. This enables the rapid development and integration of Jobs that can be run within Nautobot to pull data from other systems ("Data Sources") into Nautobot and/or push data from Nautobot into other systems ("Data Targets") as desired. Key features include the following: + +* A dashboard UI lists all registered Data Sources and Data Targets and provides a summary of the synchronization history. +* The outcome of executing of a data synchronization Job is automatically saved to Nautobot's database for later review. +* Detailed logging output generated by DiffSync is automatically captured and saved to the database as well. + +### Integrations + +This Nautobot application framework includes the following integrations: + +- Cisco ACI +- Arista CloudVision +- Device42 +- Infoblox +- IPFabric +- ServiceNow + +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). ### Screenshots -> Developer Note: Add any representative screenshots of the App in action. These images should also be added to the `docs/user/app_use_cases.md` section. +--- -> Developer Note: Place the files in the `docs/images/` folder and link them using only full URLs from GitHub, for example: `![Overview](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/plugin-overview.png)`. This absolute static linking is required to ensure the README renders properly in GitHub, the docs site, and any other external sites like PyPI. +The dashboard view of the app. +![Dashboard View](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/dashboard_initial.png) -More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/ssot/en/latest/user/app_use_cases/) page in the documentation. Here's a quick overview of some of the plugin's added functionality: +--- -![](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/placeholder.png) +The detailed view of the example data source that is prepackaged within this app. +![Data Source Detail View](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/data_source_detail.png) -## Try it out! +--- + +The detailed view of an executed sync. +![Sync Detail View](https://raw.githubusercontent.com/nautobot/nautobot-plugin-ssot/develop/docs/images/sync_detail.png) + +--- -> Developer Note: Only keep this section if appropriate. Update link to correct sandbox. +More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/ssot/en/latest/user/app_use_cases/) page in the documentation. + +## Try it out! -This App is installed in the Nautobot Community Sandbox found over at [demo.nautobot.com](https://demo.nautobot.com/)! +This Nautobot app is installed in the Nautobot Community Sandbox found over at [demo.nautobot.com](https://demo.nautobot.com/)! > For a full list of all the available always-on sandbox environments, head over to the main page on [networktocode.com](https://www.networktocode.com/nautobot/sandbox-environments/). ## Documentation -Full documentation for this App can be found over on the [Nautobot Docs](https://docs.nautobot.com) website: +Full documentation for this app can be found over on the [Nautobot Docs](https://docs.nautobot.com) website: -- [User Guide](https://docs.nautobot.com/projects/ssot/en/latest/user/app_overview/) - Overview, Using the App, Getting Started. -- [Administrator Guide](https://docs.nautobot.com/projects/ssot/en/latest/admin/install/) - How to Install, Configure, Upgrade, or Uninstall the App. -- [Developer Guide](https://docs.nautobot.com/projects/ssot/en/latest/dev/contributing/) - Extending the App, Code Reference, Contribution Guide. -- [Release Notes / Changelog](https://docs.nautobot.com/projects/ssot/en/latest/admin/release_notes/). -- [Frequently Asked Questions](https://docs.nautobot.com/projects/ssot/en/latest/user/faq/). +* [User Guide](https://docs.nautobot.com/projects/ssot/en/latest/user/app_overview/) - Overview, Using the App, Getting Started, Developing Jobs. +* [Administrator Guide](https://docs.nautobot.com/projects/ssot/en/latest/admin/install/) - How to Install, Configure, Upgrade, or Uninstall the App. +* [Developer Guide](https://docs.nautobot.com/projects/ssot/en/latest/dev/contributing/) - Extending the App, Code Reference, Contribution Guide. +* [Release Notes / Changelog](https://docs.nautobot.com/projects/ssot/en/latest/admin/release_notes/). + +## Note On Integration Compatability + +The SSoT framework includes a number of integrations with external Systems of Record: + +* Cisco ACI +* Arista CloudVision +* Device42 +* Infoblox +* ServiceNow + +> 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. ### Contributing to the Documentation -You can find all the Markdown source for the App documentation under the [`docs`](https://github.com/nautobot/nautobot-plugin-ssot//tree/develop/docs) folder in this repository. For simple edits, a Markdown capable editor is sufficient: clone the repository and edit away. +You can find all the Markdown source for the app documentation under the [`docs`](https://github.com/nautobot/nautobot-plugin-ssot/tree/develop/docs) folder in this repository. For simple edits, a Markdown capable editor is sufficient: clone the repository and edit away. If you need to view the fully-generated documentation site, you can build it with [MkDocs](https://www.mkdocs.org/). A container hosting the documentation can be started using the `invoke` commands (details in the [Development Environment Guide](https://docs.nautobot.com/projects/ssot/en/latest/dev/dev_environment/#docker-development-environment)) on [http://localhost:8001](http://localhost:8001). Using this container, as your changes to the documentation are saved, they will be automatically rebuilt and any pages currently being viewed will be reloaded in your browser. @@ -64,3 +94,51 @@ Any PRs with fixes or improvements are very welcome! ## Questions For any questions or comments, please check the [FAQ](https://docs.nautobot.com/projects/ssot/en/latest/user/faq/) first. Feel free to also swing by the [Network to Code Slack](https://networktocode.slack.com/) (channel `#nautobot`), sign up [here](http://slack.networktocode.com/) if you don't have an account. + +## Acknowledgements + +This project includes code originally written in separate Nautobot apps, which have been merged into this project: + +- [nautobot-plugin-ssot-aci](https://github.com/nautobot/nautobot-plugin-ssot-aci): + Thanks + [@chadell](https://github.com/chadell), + [@dnewood](https://github.com/dnewood), + [@progala](https://github.com/progala), + [@ubajze](https://github.com/ubajze) +- [nautobot-plugin-ssot-arista-cloudvision](https://github.com/nautobot/nautobot-plugin-ssot-arista-cloudvision): + Thanks + [@burnyd](https://github.com/burnyd), + [@chipn](https://github.com/chipn), + [@jdrew82](https://github.com/jdrew82), + [@jvanderaa](https://github.com/jvanderaa), + [@nniehoff](https://github.com/nniehoff), + [@qduk](https://github.com/qduk), + [@ubajze](https://github.com/ubajze) +- [nautobot-plugin-ssot-infoblox](https://github.com/nautobot/nautobot-plugin-ssot-infoblox): + Thanks + [@FragmentedPacket](https://github.com/FragmentedPacket), + [@chadell](https://github.com/chadell), + [@jdrew82](https://github.com/jdrew82), + [@jtdub](https://github.com/jtdub), + [@pke11y](https://github.com/pke11y), + [@smk4664](https://github.com/smk4664), + [@ubajze](https://github.com/ubajze) + [@whitej6](https://github.com/whitej6), +- [nautobot-plugin-ssot-ipfabric](https://github.com/nautobot/nautobot-plugin-ssot-ipfabric): + Thanks + [@FragmentedPacket](https://github.com/FragmentedPacket), + [@armartirosyan](https://github.com/armartirosyan), + [@chadell](https://github.com/chadell), + [@grelleum](https://github.com/grelleum), + [@h4ndzdatm0ld](https://github.com/h4ndzdatm0ld), + [@jdrew82](https://github.com/jdrew82), + [@justinjeffery-ipf](https://github.com/justinjeffery-ipf), + [@pke11y](https://github.com/pke11y), + [@ubajze](https://github.com/ubajze) + [@whitej6](https://github.com/whitej6), +- [nautobot-plugin-ssot-servicenow](https://github.com/nautobot/nautobot-plugin-ssot-servicenow): + Thanks + [@chadell](https://github.com/chadell), + [@glennmatthews](https://github.com/glennmatthews), + [@qduk](https://github.com/qduk), + [@ubajze](https://github.com/ubajze) diff --git a/development/Dockerfile b/development/Dockerfile index 09a1d2529..c0711e815 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -63,8 +63,8 @@ RUN pip show nautobot | grep "^Version: " | sed -e 's/Version: /nautobot==/' > c # # We can't use the entire freeze as it takes forever to resolve with rigidly fixed non-direct dependencies, # especially those that are only direct to Nautobot but the container included versions slightly mismatch -RUN poetry export -f requirements.txt --without-hashes --output poetry_freeze_base.txt -RUN poetry export -f requirements.txt --with dev --without-hashes --output poetry_freeze_all.txt +RUN poetry export -f requirements.txt --without-hashes --extras all --output poetry_freeze_base.txt +RUN poetry export -f requirements.txt --without-hashes --extras all --with dev --output poetry_freeze_all.txt RUN sort poetry_freeze_base.txt poetry_freeze_all.txt | uniq -u > poetry_freeze_dev.txt # Install all local project as editable, constrained on Nautobot version, to get any additional diff --git a/development/creds.example.env b/development/creds.example.env index 26e24fade..780d04b29 100644 --- a/development/creds.example.env +++ b/development/creds.example.env @@ -25,3 +25,24 @@ MYSQL_PASSWORD=${NAUTOBOT_DB_PASSWORD} # NAUTOBOT_DB_HOST=localhost # NAUTOBOT_REDIS_HOST=localhost # NAUTOBOT_CONFIG=development/nautobot_config.py + +NAUTOBOT_ARISTACV_CVP_PASSWORD="changeme" +NAUTOBOT_ARISTACV_CVP_TOKEN="changeme" + +NAUTOBOT_SSOT_INFOBLOX_PASSWORD="changeme" + +# ACI Credentials. Append friendly name to the end to identify each APIC. +NAUTOBOT_APIC_BASE_URI_NTC=https://aci.cloud.networktocode.com +NAUTOBOT_APIC_USERNAME_NTC=admin +NAUTOBOT_APIC_PASSWORD_NTC=super_secret_password +NAUTOBOT_APIC_VERIFY_NTC=False +# NAUTOBOT_APIC_SITE_NTC="NTC ACI" +NAUTOBOT_APIC_BASE_URI_DEVNET=https://sandboxapicdc.cisco.com +NAUTOBOT_APIC_USERNAME_DEVNET=admin +NAUTOBOT_APIC_PASSWORD_DEVNET=super_secret_password +NAUTOBOT_APIC_VERIFY_DEVNET=False +# NAUTOBOT_APIC_SITE_DEVNET="DevNet Sandbox" + +SERVICENOW_PASSWORD="changeme" + +IPFABRIC_API_TOKEN=secrettoken diff --git a/development/development.env b/development/development.env index 54f0b8708..f6fca705c 100644 --- a/development/development.env +++ b/development/development.env @@ -36,3 +36,62 @@ POSTGRES_DB=${NAUTOBOT_DB_NAME} MYSQL_USER=${NAUTOBOT_DB_USER} MYSQL_DATABASE=${NAUTOBOT_DB_NAME} MYSQL_ROOT_HOST=% + +NAUTOBOT_HOST="http://nautobot:8080" + +NAUTOBOT_CELERY_TASK_SOFT_TIME_LIMIT=7200 +NAUTOBOT_CELERY_TASK_TIME_LIMIT=7200 + +NAUTOBOT_SSOT_HIDE_EXAMPLE_JOBS="False" +NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS="False" + +NAUTOBOT_SSOT_ENABLE_ACI="True" +NAUTOBOT_SSOT_ACI_TAG="ACI" +NAUTOBOT_SSOT_ACI_TAG_COLOR="0047AB" +NAUTOBOT_SSOT_ACI_TAG_UP="UP" +NAUTOBOT_SSOT_ACI_TAG_UP_COLOR="008000" +NAUTOBOT_SSOT_ACI_TAG_DOWN="DOWN" +NAUTOBOT_SSOT_ACI_TAG_DOWN_COLOR="FF3333" +NAUTOBOT_SSOT_ACI_MANUFACTURER_NAME="Cisco" +NAUTOBOT_SSOT_ACI_IGNORE_TENANTS="[mgmt,infra]" +NAUTOBOT_SSOT_ACI_COMMENTS="Created by ACI SSoT Integration" +NAUTOBOT_SSOT_ACI_SITE="Data Center" + +NAUTOBOT_SSOT_ENABLE_ARISTACV="True" +NAUTOBOT_ARISTACV_CONTROLLER_SITE="" +NAUTOBOT_ARISTACV_CREATE_CONTROLLER="True" +NAUTOBOT_ARISTACV_CVAAS_URL="www.arista.io:443" +NAUTOBOT_ARISTACV_CVP_HOST="" +NAUTOBOT_ARISTACV_CVP_PORT="443" +NAUTOBOT_ARISTACV_CVP_USERNAME="changeme" +NAUTOBOT_ARISTACV_DELETE_ON_SYNC="False" +NAUTOBOT_ARISTACV_IMPORT_ACTIVE="False" +NAUTOBOT_ARISTACV_IMPORT_TAG="False" +NAUTOBOT_ARISTACV_VERIFY=True + +NAUTOBOT_SSOT_ENABLE_DEVICE42="True" +NAUTOBOT_SSOT_DEVICE42_HOST="" +NAUTOBOT_SSOT_DEVICE42_USERNAME="" +NAUTOBOT_SSOT_DEVICE42_PASSWORD="" + +NAUTOBOT_SSOT_ENABLE_INFOBLOX="True" +NAUTOBOT_SSOT_INFOBLOX_DEFAULT_STATUS="Active" +NAUTOBOT_SSOT_INFOBLOX_ENABLE_SYNC_TO_INFOBLOX="True" +NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_IP_ADDRESSES="True" +NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_SUBNETS="True" +NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLANS="True" +NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLAN_VIEWS="True" +NAUTOBOT_SSOT_INFOBLOX_IMPORT_SUBNETS="10.46.128.0/18,192.168.1.0/24" +NAUTOBOT_SSOT_INFOBLOX_URL="https://infoblox.example.com" +NAUTOBOT_SSOT_INFOBLOX_USERNAME="changeme" +NAUTOBOT_SSOT_INFOBLOX_VERIFY_SSL="True" +# NAUTOBOT_SSOT_INFOBLOX_WAPI_VERSION="" + +NAUTOBOT_SSOT_ENABLE_SERVICENOW="True" +SERVICENOW_INSTANCE="" +SERVICENOW_USERNAME="" + +NAUTOBOT_SSOT_ENABLE_IPFABRIC="True" +IPFABRIC_HOST="https://ipfabric.example.com" +IPFABRIC_SSL_VERIFY="True" +IPFABRIC_TIMEOUT=15 diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 686865159..a47bbee9b 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -129,13 +129,116 @@ # # Enable installed Apps. Add the name of each App to the list. -PLUGINS = ["nautobot_ssot"] +PLUGINS = [ + "nautobot_chatops", + "nautobot_device_lifecycle_mgmt", + "nautobot_ssot", +] # Apps configuration settings. These settings are used by various Apps that the user may have installed. # Each key in the dictionary is the name of an installed App and its value is a dictionary of settings. -# PLUGINS_CONFIG = { -# 'nautobot_ssot': { -# 'foo': 'bar', -# 'buzz': 'bazz' -# } -# } +PLUGINS_CONFIG = { + "nautobot_chatops": { + "enable_slack": True, + "slack_api_token": os.getenv("SLACK_API_TOKEN"), + "slack_signing_secret": os.getenv("SLACK_SIGNING_SECRET"), + "session_cache_timeout": 3600, + "ipfabric_api_token": os.getenv("IPFABRIC_API_TOKEN"), + "ipfabric_host": os.getenv("IPFABRIC_HOST"), + }, + "nautobot_ssot": { + # URL and credentials should be configured as environment variables on the host system + "aci_apics": {x: os.environ[x] for x in os.environ if "APIC" in x}, + # Tag which will be created and applied to all synchronized objects. + "aci_tag": os.getenv("NAUTOBOT_SSOT_ACI_TAG"), + "aci_tag_color": os.getenv("NAUTOBOT_SSOT_ACI_TAG_COLOR"), + # Tags indicating state applied to synchronized interfaces. + "aci_tag_up": os.getenv("NAUTOBOT_SSOT_ACI_TAG_UP"), + "aci_tag_up_color": os.getenv("NAUTOBOT_SSOT_ACI_TAG_UP_COLOR"), + "aci_tag_down": os.getenv("NAUTOBOT_SSOT_ACI_TAG_DOWN"), + "aci_tag_down_color": os.getenv("NAUTOBOT_SSOT_ACI_TAG_DOWN_COLOR"), + # Manufacturer name. Specify existing, or a new one with this name will be created. + "aci_manufacturer_name": os.getenv("NAUTOBOT_SSOT_ACI_MANUFACTURER_NAME"), + # Exclude any tenants you would not like to bring over from ACI. + "aci_ignore_tenants": os.getenv("NAUTOBOT_SSOT_ACI_IGNORE_TENANTS", "").split(","), + # The below value will appear in the Comments field on objects created in Nautobot + "aci_comments": os.getenv("NAUTOBOT_SSOT_ACI_COMMENTS"), + # Site to associate objects. Specify existing, or a new site with this name will be created. + "aci_site": os.getenv("NAUTOBOT_SSOT_ACI_SITE"), + "aristacv_apply_import_tag": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_TAG", False)), + "aristacv_controller_site": os.getenv("NAUTOBOT_ARISTACV_CONTROLLER_SITE", ""), + "aristacv_create_controller": is_truthy(os.getenv("NAUTOBOT_ARISTACV_CREATE_CONTROLLER", False)), + "aristacv_cvaas_url": os.getenv("NAUTOBOT_ARISTACV_CVAAS_URL", "www.arista.io:443"), + "aristacv_cvp_host": os.getenv("NAUTOBOT_ARISTACV_CVP_HOST", ""), + "aristacv_cvp_password": os.getenv("NAUTOBOT_ARISTACV_CVP_PASSWORD", ""), + "aristacv_cvp_port": os.getenv("NAUTOBOT_ARISTACV_CVP_PORT", "443"), + "aristacv_cvp_token": os.getenv("NAUTOBOT_ARISTACV_CVP_TOKEN", ""), + "aristacv_cvp_user": os.getenv("NAUTOBOT_ARISTACV_CVP_USERNAME", ""), + "aristacv_delete_devices_on_sync": is_truthy(os.getenv("NAUTOBOT_ARISTACV_DELETE_ON_SYNC", False)), + "aristacv_from_cloudvision_default_device_role": "network", + "aristacv_from_cloudvision_default_device_role_color": "ff0000", + "aristacv_from_cloudvision_default_site": "cloudvision_imported", + "aristacv_hostname_patterns": [[r"(?P\w{2,3}\d+)-(?P\w+)-\d+"]], + "aristacv_import_active": is_truthy(os.getenv("NAUTOBOT_ARISTACV_IMPORT_ACTIVE", False)), + "aristacv_role_mappings": { + "bb": "backbone", + "edge": "edge", + "dist": "distribution", + "leaf": "leaf", + "rtr": "router", + "spine": "spine", + }, + "aristacv_site_mappings": { + "ams01": "Amsterdam", + "atl01": "Atlanta", + }, + "aristacv_verify": is_truthy(os.getenv("NAUTOBOT_ARISTACV_VERIFY", True)), + "enable_aci": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ACI")), + "enable_aristacv": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_ARISTACV")), + "enable_device42": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_DEVICE42")), + "enable_infoblox": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_INFOBLOX")), + "enable_ipfabric": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_IPFABRIC")), + "enable_servicenow": is_truthy(os.getenv("NAUTOBOT_SSOT_ENABLE_SERVICENOW")), + "hide_example_jobs": is_truthy(os.getenv("NAUTOBOT_SSOT_HIDE_EXAMPLE_JOBS")), + "device42_host": os.getenv("NAUTOBOT_SSOT_DEVICE42_HOST", ""), + "device42_username": os.getenv("NAUTOBOT_SSOT_DEVICE42_USERNAME", ""), + "device42_password": os.getenv("NAUTOBOT_SSOT_DEVICE42_PASSWORD", ""), + "device42_verify_ssl": False, + "device42_defaults": { + "site_status": "Active", + "rack_status": "Active", + "device_role": "Unknown", + }, + "device42_delete_on_sync": False, + "device42_use_dns": True, + "device42_customer_is_facility": True, + "device42_facility_prepend": "", + "device42_role_prepend": "", + "device42_ignore_tag": "", + "device42_hostname_mapping": [], + "infoblox_default_status": os.getenv("NAUTOBOT_SSOT_INFOBLOX_DEFAULT_STATUS", "active"), + "infoblox_enable_sync_to_infoblox": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_ENABLE_SYNC_TO_INFOBLOX")), + "infoblox_import_objects_ip_addresses": is_truthy( + os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_IP_ADDRESSES") + ), + "infoblox_import_objects_subnets": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_SUBNETS")), + "infoblox_import_objects_vlan_views": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLAN_VIEWS")), + "infoblox_import_objects_vlans": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_OBJECTS_VLANS")), + "infoblox_import_subnets": os.getenv("NAUTOBOT_SSOT_INFOBLOX_IMPORT_SUBNETS", "").split(","), + "infoblox_password": os.getenv("NAUTOBOT_SSOT_INFOBLOX_PASSWORD"), + "infoblox_url": os.getenv("NAUTOBOT_SSOT_INFOBLOX_URL"), + "infoblox_username": os.getenv("NAUTOBOT_SSOT_INFOBLOX_USERNAME"), + "infoblox_verify_ssl": is_truthy(os.getenv("NAUTOBOT_SSOT_INFOBLOX_VERIFY_SSL", True)), + "infoblox_wapi_version": os.getenv("NAUTOBOT_SSOT_INFOBLOX_WAPI_VERSION", "v2.12"), + "ipfabric_api_token": os.getenv("IPFABRIC_API_TOKEN"), + "ipfabric_host": os.getenv("IPFABRIC_HOST"), + "ipfabric_ssl_verify": is_truthy(os.getenv("IPFABRIC_VERIFY", "False")), + "ipfabric_timeout": int(os.getenv("IPFABRIC_TIMEOUT", "15")), + "nautobot_host": os.getenv("NAUTOBOT_HOST"), + "servicenow_instance": os.getenv("SERVICENOW_INSTANCE", ""), + "servicenow_password": os.getenv("SERVICENOW_PASSWORD", ""), + "servicenow_username": os.getenv("SERVICENOW_USERNAME", ""), + }, +} + +METRICS_ENABLED = True diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index 1f05e4a03..31f7f2e51 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -1,8 +1,17 @@ # Compatibility Matrix -!!! warning "Developer Note - Remove Me!" - Explain how the release models of the plugin and of Nautobot work together, how releases are supported, how features and older releases are deprecated etc. +While that last supported version will not be strictly enforced--via the max_version setting, any issues with an updated Nautobot supported version in a minor release, will require a bug to be raised and a fix in Nautobot core to address, with no fixes expected in this Nautobot app. This allows the Nautobot Single Source of Truth app the ability to quickly take advantage of the latest features. | Single Source of Truth Version | Nautobot First Support Version | Nautobot Last Support Version | -| ------------- | -------------------- | ------------- | -| 1.0.X | 2.0.0 | 1.99.99 | +| ------------------------------ | ------------------------------ | ----------------------------- | +| 1.0.X | 1.0.3 | 1.99.99 | +| 1.1.X | 1.0.3 | 1.99.99 | +| 1.2.X | 1.0.3 | 1.99.99 | +| 1.3.X | 1.4.0 | 1.99.99 | +| 1.4.X | 1.4.0 | 1.99.99 | +| 1.5.X | 1.4.0 | 1.99.99 | +| 2.0.0-beta.1 | 2.0.0b2 | 2.0.0b2 | +| 2.0.0-beta.2 | 2.0.0b2 | 2.0.0b2 | +| 2.0.0-rc.1 | 2.0.0rc1 | 2.0.0rc1 | +| 2.0.0-rc.2 | 2.0.0rc2 | 2.0.0rc99 | +| 2.0.0 | 2.0.0 | 2.99.09 | diff --git a/docs/admin/install.md b/docs/admin/install.md index 355b80f5d..517ee86dc 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -2,40 +2,48 @@ Here you will find detailed instructions on how to **install** and **configure** the App within your Nautobot environment. -!!! warning "Developer Note - Remove Me!" - Detailed instructions on installing the App. You will need to update this section based on any additional dependencies or prerequisites. - ## Prerequisites -- The plugin is compatible with Nautobot 2.0.0 and higher. +- The app is compatible with Nautobot 2.0.0 and higher. - Databases supported: PostgreSQL, MySQL !!! note Please check the [dedicated page](compatibility_matrix.md) for a full compatibility matrix and the deprecation policy. -### Access Requirements - -!!! warning "Developer Note - Remove Me!" - What external systems (if any) it needs access to in order to work. +!!! warning + If upgrading from `1.x` version to `2.x` version of `nautobot-ssot` app, note that it now incorporates features previously provided by individual apps. For details, see the [upgrade guide](../admin/upgrade.md). ## Install Guide !!! note - Plugins can be installed manually or using Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this plugin is [`nautobot-ssot`](https://pypi.org/project/nautobot-ssot/). + Nautobot apps can be installed manually or using Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this Nautobot app is [`nautobot-ssot`](https://pypi.org/project/nautobot-ssot/). -The plugin is available as a Python package via PyPI and can be installed with `pip`: +The app is available as a Python package via PyPI and can be installed with `pip`: ```shell pip install nautobot-ssot ``` -To ensure Single Source of Truth is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-ssot` package: +To use specific integrations, add them as extra dependencies: + +```shell +# To install Cisco ACI integration: +pip install nautobot-ssot[aci] + +# To install Arista CloudVision integration: +pip install nautobot-ssot[aristacv] + +# To install all integrations: +pip install nautobot-ssot[all] +``` + +To ensure Single Source of Truth is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-ssot` package and any of the extras: ```shell echo nautobot-ssot >> local_requirements.txt ``` -Once installed, the plugin needs to be enabled in your Nautobot configuration. The following block of code below shows the additional configuration required to be added to your `nautobot_config.py` file: +Once installed, the Nautobot app needs to be enabled in your Nautobot configuration. The following block of code below shows the additional configuration required to be added to your `nautobot_config.py` file: - Append `"nautobot_ssot"` to the `PLUGINS` list. - Append the `"nautobot_ssot"` dictionary to the `PLUGINS_CONFIG` dictionary and override any defaults. @@ -46,7 +54,7 @@ PLUGINS = ["nautobot_ssot"] # PLUGINS_CONFIG = { # "nautobot_ssot": { -# ADD YOUR SETTINGS HERE +# "hide_example_jobs": True # } # } ``` @@ -69,13 +77,20 @@ sudo systemctl restart nautobot nautobot-worker nautobot-scheduler ## App Configuration -!!! warning "Developer Note - Remove Me!" - Any configuration required to get the App set up. Edit the table below as per the examples provided. +The app behavior can be controlled with the following list of settings: + +| Key | Example | Default | Description | +| ------------------- | ------- | ------- | ---------------------------------------------------------- | +| `hide_example_jobs` | `True` | `False` | A boolean to represent whether or display the example job. | + +## Integrations Configuration + +The `nautobot-ssot` package includes multiple integrations. Each requires extra dependencies defined in `pyproject.toml`. -The plugin behavior can be controlled with the following list of settings: +Set up each integration using the specific guides: -| Key | Example | Default | Description | -| ------- | ------ | -------- | ------------------------------------- | -| `enable_backup` | `True` | `True` | A boolean to represent whether or not to run backup configurations within the plugin. | -| `platform_slug_map` | `{"cisco_wlc": "cisco_aireos"}` | `None` | A dictionary in which the key is the platform slug and the value is what netutils uses in any "network_os" parameter. | -| `per_feature_bar_width` | `0.15` | `0.15` | The width of the table bar within the overview report | +- [Cisco ACI](./integrations/aci_setup.md) +- [Arista CloudVision](./integrations/aristacv_setup.md) +- [Infoblox](./integrations/infoblox_setup.md) +- [IPFabric](./integrations/ipfabric_setup.md) +- [ServiceNow](./integrations/servicenow_setup.md) diff --git a/docs/admin/release_notes/version_1.0.md b/docs/admin/release_notes/version_1.0.md index 8cc3e8509..8eeaa8803 100644 --- a/docs/admin/release_notes/version_1.0.md +++ b/docs/admin/release_notes/version_1.0.md @@ -1,48 +1,19 @@ # v1.0 Release Notes -!!! warning "Developer Note - Remove Me!" - Guiding Principles: - - - Changelogs are for humans, not machines. - - There should be an entry for every single version. - - The same types of changes should be grouped. - - Versions and sections should be linkable. - - The latest version comes first. - - The release date of each version is displayed. - - Mention whether you follow Semantic Versioning. - - Types of changes: - - - `Added` for new features. - - `Changed` for changes in existing functionality. - - `Deprecated` for soon-to-be removed features. - - `Removed` for now removed features. - - `Fixed` for any bug fixes. - - `Security` in case of vulnerabilities. - - -This document describes all new features and changes in the release `1.0`. 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 - -- Major features or milestones -- Achieved in this `x.y` release -- Changes to compatibility with Nautobot and/or other plugins, libraries etc. - -## [v1.0.1] - 2021-09-08 - -### Added +## v1.0.1 - 2021-10-18 ### Changed +- [#8](https://github.com/nautobot/nautobot-plugin-ssot/pull/8) - Switched from Travis CI to GitHub Actions. + ### Fixed -- [#123](https://github.com/nautobot/nautobot-plugin-ssot//issues/123) Fixed Tag filtering not working in job launch form +- [#9](https://github.com/nautobot/nautobot-plugin-ssot/pull/9) - Added missing `name` string to `jobs/examples.py`. -## [v1.0.0] - 2021-08-03 +### Removed -### Added +- [#7](https://github.com/nautobot/nautobot-plugin-ssot/pull/7) - Removed unnecessary `markdown-include` development/documentation dependency. -### Changed +## [v1.0.0] - 2021-08-03 -### Fixed +- Initial Release diff --git a/docs/admin/uninstall.md b/docs/admin/uninstall.md index 1bbcacfa4..0314683d3 100644 --- a/docs/admin/uninstall.md +++ b/docs/admin/uninstall.md @@ -2,6 +2,12 @@ Here you will find any steps necessary to cleanly remove the App from your Nautobot environment. +## Uninstall the package + +```bash +$ pip3 uninstall nautobot-ssot +``` + ## Database Cleanup Prior to removing the plugin from the `nautobot_config.py`, run the following command to roll back any migration specific to this plugin. @@ -10,9 +16,6 @@ Prior to removing the plugin from the `nautobot_config.py`, run the following co nautobot-server migrate nautobot_plugin_ssot zero ``` -!!! warning "Developer Note - Remove Me!" - Any other cleanup operations to ensure the database is clean after the app is removed. Is there anything else that needs cleaning up, such as CFs, relationships, etc. if they're no longer desired? - ## Remove App configuration Remove the configuration you added in `nautobot_config.py` from `PLUGINS` & `PLUGINS_CONFIG`. diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index ed0393589..6c60de14a 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -4,7 +4,37 @@ Here you will find any steps necessary to upgrade the App in your Nautobot envir ## Upgrade Guide -!!! warning "Developer Note - Remove Me!" - Add more detailed steps on how the app is upgraded in an existing Nautobot setup and any version specifics (such as upgrading between major versions with breaking changes). +When a new release comes out it may be necessary to run a migration of the database to account for any changes in the data models used by this Nautobot app. Execute the command `nautobot-server post-upgrade` within the runtime environment of your Nautobot installation after updating the `nautobot-ssot` package via `pip`. -When a new release comes out it may be necessary to run a migration of the database to account for any changes in the data models used by this plugin. Execute the command `nautobot-server post-upgrade` within the runtime environment of your Nautobot installation after updating the `nautobot-ssot` package via `pip`. +### Potential Apps Conflicts + +!!! warning + If upgrading from versions prior to 1.4 of the `nautobot-ssot` app, note that it now incorporates features previously provided by individual apps. + +Conflicting apps list: + +- `nautobot_ssot_aci` +- `nautobot_ssot_arista_cloudvision` +- `nautobot_ssot_infoblox` +- `nautobot_ssot_ipfabric` +- `nautobot_ssot_servicenow` + +To prevent conflicts during `nautobot-ssot` upgrade: + +- Remove conflicting applications from the `PLUGINS` section in your Nautobot configuration before enabling the latest `nautobot-ssot` version. +- Transfer the configuration for conflicting apps to the `PLUGIN_CONFIG["nautobot_ssot"]` section of your Nautobot configuration. See `development/nautobot_config.py` for an example. Each [integration set up guide](../integrations/) contains a chapter with upgrade instructions. +- Remove conflicting applications from your project's requirements. + +These steps will help prevent issues during `nautobot-ssot` upgrades. Always back up your data and thoroughly test your configuration after these changes. + +!!! note + It's possible to allow conflicting apps to remain in `PLUGINS` during the upgrade process. You can specify the following environment variable to allow conflicting apps (see `development/development.env` for an example): + + ```bash + NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS=True + ``` + + However, this is not recommended. + +!!! warning + If conflicting apps remain in `PLUGINS`, the `nautobot-ssot` app will raise an exception during startup to prevent potential conflicts. diff --git a/docs/assets/extra.css b/docs/assets/extra.css index dfe2e4b18..1eff1192e 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -159,3 +159,8 @@ a.autorefs-external:hover::after { -webkit-mask-image: var(--md-admonition-icon--version-removed); mask-image: var(--md-admonition-icon--version-removed); } + +/* Do not wrap code blocks in markdown tables. */ +div.md-typeset__table>table>tbody>tr>td>code { + white-space: nowrap; +} diff --git a/docs/dev/code_reference/index.md b/docs/dev/code_reference/index.md index ebe9ff7d1..473f2c40f 100644 --- a/docs/dev/code_reference/index.md +++ b/docs/dev/code_reference/index.md @@ -1,6 +1,3 @@ # Code Reference Auto-generated code reference documentation from docstrings. - -!!! warning "Developer Note - Remove Me!" - Uses [mkdocstrings](https://mkdocstrings.github.io/) syntax to auto-generate code documentation from docstrings. Two example pages are provided ([api](api.md) and [package](package.md)), add new stubs for each module or package that you think has relevant documentation. diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index 2337f740c..cbed2207b 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -1,24 +1,46 @@ # Contributing to the App -!!! warning "Developer Note - Remove Me!" - Information on how to contribute fixes, functionality, or documentation changes back to the project. - The project is packaged with a light [development environment](dev_environment.md) based on `docker-compose` to help with the local development of the project and to run tests. The project is following Network to Code software development guidelines and is leveraging the following: - Python linting and formatting: `black`, `pylint`, `bandit`, `flake8`, and `pydocstyle`. - YAML linting is done with `yamllint`. -- Django unit test to ensure the plugin is working properly. +- Django unit test to ensure the Nautobot app is working properly. Documentation is built using [mkdocs](https://www.mkdocs.org/). The [Docker based development environment](dev_environment.md#docker-development-environment) automatically starts a container hosting a live version of the documentation website on [http://localhost:8001](http://localhost:8001) that auto-refreshes when you make any changes to your local files. ## Branching Policy -!!! warning "Developer Note - Remove Me!" - What branching policy is used for this project and where contributions should be made. +The branching policy includes the following tenets: + +* The `develop` branch is the primary branch to develop off of. +* PRs intended to add new features should be sourced from the `develop` branch. +* PRs intended to address bug fixes and security patches should be sourced from the `develop` branch. +* PRs intended to add new features that break backward compatibility should be discussed before a PR is created. + +Nautobot Single Source of Truth app will observe semantic versioning, as of 1.0. This may result in an quick turn around in minor versions to keep pace with an ever growing feature set. ## Release Policy -!!! warning "Developer Note - Remove Me!" - How new versions are released. +Nautobot Single Source of Truth currently has no intended scheduled release schedule, and will release new features in minor versions. + +When a new release of any kind (e.g. from `develop` to `main`, or a release of a `stable-.`) is created the following should happen. + +- A release PR is created: + - Add and/or update to the changelog in `docs/admin/release_notes/version_..md` file to reflect the changes. + - Update the mkdocs.yml file to include updates when adding a new release_notes version file. + - Change the version from `..-beta` to `..` in pyproject.toml. + - Set the PR to the proper branch, e.g. either `main` or `stable-.`. +- Ensure the tests for the PR pass. +- Merge the PR. +- Create a new tag: + - The tag should be in the form of `v..`. + - The title should be in the form of `v..`. + - The description should be the changes that were added to the `version_..md` document. +- If merged into `main`, then push from `main` to `develop`, in order to retain the merge commit created when the PR was merged. +- If the is a new `.`, create a `stable-.` for the **previous** version, so that security updates to old versions may be applied more easily. +- A post release PR is created: + - Change the version from `..` to `..-beta` in pyproject.toml. + - Set the PR to the proper branch, e.g. either `develop` or `stable-.`. + - Once tests pass, merge. diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index f1798807a..f0a146264 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -290,9 +290,9 @@ This will safely shut down all of your running Docker containers for this projec Your environment should now be fully setup, all necessary Docker containers are created and running, and you're logged into Nautobot in your web browser. Now what? -Now you can start developing your plugin in the project folder! +Now you can start developing your Nautobot app in the project folder! -The magic here is the root directory is mounted inside your Docker containers when built and ran, so **any** changes made to the files in here are directly updated to the Nautobot plugin code running in Docker. This means that as you modify the code in your plugin folder, the changes will be instantly updated in Nautobot. +The magic here is the root directory is mounted inside your Docker containers when built and ran, so **any** changes made to the files in here are directly updated to the Nautobot app code running in Docker. This means that as you modify the code in your Nautobot app folder, the changes will be instantly updated in Nautobot. !!! warning There are a few exceptions to this, as outlined in the section [To Rebuild or Not To Rebuild](#to-rebuild-or-not-to-rebuild). @@ -316,7 +316,7 @@ When trying to debug an issue, one helpful thing you can look at are the logs wi !!! info Want to limit the log output even further? Use the `--tail <#>` command line argument in conjunction with `-f`. -So for example, our plugin is named `nautobot-ssot`, the command would most likely be `docker logs nautobot_ssot_nautobot_1 -f`. You can find the name of all running containers via `docker ps`. +So for example, our app is named `nautobot-ssot`, the command would most likely be `docker logs nautobot_ssot_nautobot_1 -f`. You can find the name of all running containers via `docker ps`. If you want to view the logs specific to the worker container, simply use the name of that container instead. @@ -342,7 +342,7 @@ Once completed, the new/updated environment variables should now be live. ### Installing Additional Python Packages -If you want your plugin to leverage another available Nautobot plugin or another Python package, you can easily add them into your Docker environment. +If you want your Nautobot app to leverage another available Nautobot app or another Python package, you can easily add them into your Docker environment. ```bash ➜ poetry shell @@ -357,18 +357,18 @@ Once the dependencies are resolved, stop the existing containers, rebuild the Do ➜ invoke start ``` -### Installing Additional Nautobot Plugins +### Installing Additional Nautobot Apps -Let's say for example you want the new plugin you're creating to integrate into Slack. To do this, you will want to integrate into the existing Nautobot ChatOps Plugin. +Let's say for example you want the new Nautobot app you're creating to integrate into Nautobot SSoT. To do this, you will want to integrate into the existing Nautobot SSoT app. ```bash ➜ poetry shell -➜ poetry add nautobot-chatops +➜ poetry add nautobot-ssot ``` -Once you activate the virtual environment via Poetry, you then tell Poetry to install the new plugin. +Once you activate the virtual environment via Poetry, you then tell Poetry to install the new Nautobot app. -Before you continue, you'll need to update the file `development/nautobot_config.py` accordingly with the name of the new plugin under `PLUGINS` and any relevant settings as necessary for the plugin under `PLUGINS_CONFIG`. Since you're modifying the underlying OS (not just Django files), you need to rebuild the image. This is a similar process to updating environment variables, which was explained earlier. +Before you continue, you'll need to update the file `development/nautobot_config.py` accordingly with the name of the new Nautobot app under `PLUGINS` and any relevant settings as necessary for the Nautobot app under `PLUGINS_CONFIG`. Since you're modifying the underlying OS (not just Django files), you need to rebuild the image. This is a similar process to updating environment variables, which was explained earlier. ```bash ➜ invoke stop @@ -376,7 +376,7 @@ Before you continue, you'll need to update the file `development/nautobot_config ➜ invoke start ``` -Once the containers are up and running, you should now see the new plugin installed in your Nautobot instance. +Once the containers are up and running, you should now see the new Nautobot app installed in your Nautobot instance. !!! note You can even launch an `ngrok` service locally on your laptop, pointing to port 8080 (such as for chatops development), and it will point traffic directly to your Docker images. diff --git a/docs/dev/extending.md b/docs/dev/extending.md index 49b89f464..035f29235 100644 --- a/docs/dev/extending.md +++ b/docs/dev/extending.md @@ -1,6 +1,3 @@ # Extending the App -!!! warning "Developer Note - Remove Me!" - Information on how to extend the App functionality. - Extending the application is welcome, however it is best to open an issue first, to ensure that a PR would be accepted and makes sense in terms of features and design. diff --git a/docs/images/icon-nautobot-ssot.png b/docs/images/icon-nautobot-ssot.png index 7e00cf6ae0ee76324adab30d68d64206678a85e1..a02b02d9aa2c7d04e332d8a6436a207d86764da8 100644 GIT binary patch literal 36439 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 D@XlCm literal 74601 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
@5hb{hC=~cCz;d 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 diff --git a/docs/user/app_getting_started.md b/docs/user/app_getting_started.md index 24ea00997..741069976 100644 --- a/docs/user/app_getting_started.md +++ b/docs/user/app_getting_started.md @@ -8,12 +8,39 @@ To install the App, please follow the instructions detailed in the [Installation ## First steps with the App -!!! warning "Developer Note - Remove Me!" - What (with screenshots preferably) does it look like to perform the simplest workflow within the App once installed? +By default this Nautobot app provides an example Data Source Job and example Data Target Job. You can run this example job to get a feel for the capabilities of the Nautobot app. -## What are the next steps? +--- +![Example Jobs](../images/example_jobs.png) + + +However, to get the most out of this Nautobot app you will want to find other existing Jobs and/or [create your own Jobs](../dev/jobs.md). Such Jobs can be installed like any other Nautobot Job: + +* by [packaging into a Nautobot Nautobot app](https://nautobot.readthedocs.io/en/stable/plugins/development/#including-jobs) which can then be installed into Nautobot's virtual environment +* by [inclusion in a Git repository](https://nautobot.readthedocs.io/en/stable/models/extras/gitrepository/#jobs) which can be configured in Nautobot and refreshed on demand +* by [manual installation of individual Job source files](https://nautobot.readthedocs.io/en/stable/additional-features/jobs/#writing-jobs) to Nautobot's `JOBS_ROOT` directory + + +Example screenshots of possible Data Sources and Data Targets are shown below. + +--- -!!! warning "Developer Note - Remove Me!" - After taking the first steps, what else could the users look at doing. +![Example data source - Arista CloudVision](../images/example_cloudvision.png) + +--- + +![Example data target - ServiceNow](../images/example_servicenow.png) + +Once you have other, more useful Jobs installed, these example Jobs can be disabled and removed from the UI by configuring `"hide_example_jobs"` to `True` in your `nautobot_config.py`: + +```python +PLUGINS_CONFIG = { + "nautobot_ssot": { + "hide_example_jobs": True, + } +} +``` + +## What are the next steps? You can check out the [Use Cases](app_use_cases.md) section for more examples. diff --git a/docs/user/app_overview.md b/docs/user/app_overview.md index 06ff5d327..260cd86b9 100644 --- a/docs/user/app_overview.md +++ b/docs/user/app_overview.md @@ -2,21 +2,28 @@ This document provides an overview of the App including critical information and import considerations when applying it to your Nautobot environment. +This Nautobot app facilitates integration and data synchronization between various "source of truth" (SoT) systems, with Nautobot acting as a central clearinghouse for data - a Single Source of Truth, if you will. + !!! note Throughout this documentation, the terms "app" and "plugin" will be used interchangeably. ## Description +The Nautobot SSoT app builds atop the [DiffSync](https://github.com/networktocode/diffsync) Python library and Nautobot's Jobs feature. This enables the rapid development and integration of Jobs that can be run within Nautobot to pull data from other systems ("Data Sources") into Nautobot and/or push data from Nautobot into other systems ("Data Targets") as desired. Key features include the following: + +* A dashboard UI lists all registered Data Sources and Data Targets and provides a summary of the synchronization history. +* The outcome of executing of a data synchronization Job is automatically saved to Nautobot's database for later review. +* Detailed logging output generated by DiffSync is automatically captured and saved to the database as well. ## Audience (User Personas) - Who should use this App? -!!! warning "Developer Note - Remove Me!" - Who is this meant for/ who is the common user of this app? +* Nautobot app developers looking to sync data from an outside source into Nautobot and/or vice-versa. ## Authors and Maintainers -!!! warning "Developer Note - Remove Me!" - Add the team and/or the main individuals maintaining this project. Include historical maintainers as well. +* Glenn Matthew (@glennmatthews) +* Christian Adell (@chadell) +* Justin Drew (@jdrew82) ## Nautobot Features Used diff --git a/docs/user/app_use_cases.md b/docs/user/app_use_cases.md index dc06944fe..1b463b045 100644 --- a/docs/user/app_use_cases.md +++ b/docs/user/app_use_cases.md @@ -4,9 +4,86 @@ This document describes common use-cases and scenarios for this App. ## General Usage -## Use-cases and common workflows +### Dashboard + +The dashboard UI can be accessed from the **Plugins > Single Source of Truth > Dashboard** menu item in Nautobot. + +![Dashboard](../images/dashboard_initial.png) + +The left side of the dashboard lists all discovered Data Sources and Data Targets. In a fresh installation this will include the "Example Data Source" and "Example Data Target"; when you install additional data synchronization Jobs they will be automatically discovered and included in the dashboard as well. + +The right side of the dashboard lists the ten most recent data syncs executed (if any) and summarizes their outcomes. + +### Data Source/Target details + +From the dashboard UI, you can click on the name of any given Data Source or Data Target to access a detailed view of the integration between this system and Nautobot. + +![Data Source detail view](../images/data_source_detail.png) + +This view lists the configuration (if any) of the Data Source or Data Target, provides a table describing the types of data being mapped between Nautobot and the other system, and, at the bottom of the page, lists the history of data synchronization involving this system. + +### Executing a data sync + +To synchronize data between Nautobot and a given Data Source or Data Target, select the **Sync** button for the desired integration from either the Dashboard view or the detailed view. This will bring up a form similar to that of executing any other Nautobot Job. + +![Job submission form](../images/run_job.png) + +Enter any appropriate parameters here, including selecting whether to execute the synchronization as a "dry run" (identifying data to be synchronized, but not actually making any changes to the system) or as an actual database update, and select **Run Job**. + +You will be redirected to a standard Nautobot "Job Result" view, which will update as the Job is enqueued, begins execution, and eventually completes. When execution is complete, an **SSoT Sync Details** button will appear at the top right of the page; you can select this button for a more detailed view of the outcome. + +![Job Result view](../images/job_result.png) + +### Viewing a data sync record + +The detailed view of a single data synchronization attempt between Nautobot and a Data Source/Target can be accessed from the Job Result view as described in the previous section, or by navigating to **Plugins > Single Source of Truth > History** and selecting the desired record from the table presented in that view. + +![Sync detail view](../images/sync_detail.png) + +This view describes in detail everything that occurred during the data synchronization attempt. The primary **Data Sync** tab summarizes the overall outcome of the sync attempt, including a view of the diffs (if any) identified by DiffSync and a summary of the actions taken (create, update, delete) and their outcomes (success, failure, error). + +The **Job Logs** tab shows any general status messages generated by the data synchronization Job as it executed; this is equivalent to the Nautobot "Job Result" view. + +The **Sync Logs** tab shows the logs captured from DiffSync regarding the individual data records being synchronized, details of any contents or changes of these records, and other detailed information. Sync logs can also be accessed directly via the **Plugins > Single Source of Truth > Logs** menu item if desired. + +![Sync logs view](../images/sync_logs.png) ## Screenshots -!!! warning "Developer Note - Remove Me!" - Ideally captures every view exposed by the App. Should include a relevant dataset. +Here is a consolidated view of all the pages within the SSoT Nautobot app. + +--- + +Initial dashboard showing the data targets, data sources and the last 10 syncs. +![Initial Dashboard](../images/dashboard_initial.png) + +--- + +The detail page of the example data source. +![Example Data Source Detail](../images/data_source_detail.png) + +--- + +The detailed page of the ServiceNow Data Target. +![ServiceNow Data Target Detail](../images/example_servicenow.png) + +--- + +The job form page shown prior to running a job. The fields shown here depend on the job developed. +![Job Form](../images/run_job.png) + +--- + +The job result page of running a sync. +![Example Sync Result](../images/job_result.png) + +--- + +The sync detail page for a given sync. +![Sync Detail](../images/sync_detail.png) + +--- + +The sync logs page for a given sync. +![Sync Logs](../images/sync_logs.png) + diff --git a/docs/user/external_interactions.md b/docs/user/external_interactions.md index eaba5b561..ff12af5c8 100644 --- a/docs/user/external_interactions.md +++ b/docs/user/external_interactions.md @@ -2,16 +2,58 @@ This document describes external dependencies and prerequisites for this App to operate, including system requirements, API endpoints, interconnection or integrations to other applications or services, and similar topics. -!!! warning "Developer Note - Remove Me!" - Optional page, remove if not applicable. - ## External System Integrations -### From the App to Other Systems +* When using SSoT to build a custom job, be mindful that, depending on how you are retrieving information from a remote data source, you may need to access over specific ports. + +## Prometheus Metrics + +Nautobot SSoT will add Prometheus metrics for multiple pieces of data that might be of interest in your environment to the `/api/plugins/capacity-metrics/app-metrics` output if the [Nautobot Capacity Metrics](https://github.com/nautobot/nautobot-plugin-capacity-metrics) app is installed and configured. The following metrics are added: + +The Nautobot SSoT app has the Nautobot Capacity Metrics app as a dependency, but it is up to the admin to enable it in the `nautobot_config.py` configuration. + +### Registered Metrics + +Below are the currently registered metrics for the Nautobot SSoT App: -### From Other Systems to the App +| Metric Name | Type | Labels | Description | +| ------------------------------------------------- | ----- | -------------------------------------------- | ----------------------------------------------- | +| nautobot_ssot_duration_seconds | Gauge | job, phase | Gives a time duration for each phase of a Job | +| nautobot_ssot_sync_total | Gauge | sync_type | Gives a count of SSoT sync totals based on type | +| nautobot_ssot_operation_total | Gauge | job, operation | Total number of objects for each operation in Job | +| nautobot_ssot_sync_memory_usage_bytes | Gauge | job, phase | Memory usage for Job during each phase | -## Nautobot REST API endpoints +### Sample Prometheus Metrics -!!! warning "Developer Note - Remove Me!" - API documentation in this doc - including python request examples, curl examples, postman collections referred etc. +```prometheus +# HELP nautobot_ssot_duration_seconds Nautobot SSoT Job Phase Duration in seconds +# TYPE nautobot_ssot_duration_seconds gauge +nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="source_load_time"} 5314.937 +nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="target_load_time"} 28241.297 +nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="diff_time"} 1405.652 +nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="sync_time"} 21921.814 +nautobot_ssot_duration_seconds{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="sync_duration"} 98351.03 +# HELP nautobot_ssot_sync_total Nautobot SSoT Sync Totals +# TYPE nautobot_ssot_sync_total gauge +nautobot_ssot_sync_total{sync_type="total_syncs"} 4.0 +nautobot_ssot_sync_total{sync_type="pending_syncs"} 0.0 +nautobot_ssot_sync_total{sync_type="running_syncs"} 0.0 +nautobot_ssot_sync_total{sync_type="completed_syncs"} 3.0 +nautobot_ssot_sync_total{sync_type="errored_syncs"} 0.0 +nautobot_ssot_sync_total{sync_type="failed_syncs"} 1.0 +# HELP nautobot_ssot_operation_total Nautobot SSoT operations by Job +# TYPE nautobot_ssot_operation_total gauge +nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="skip"} 0.0 +nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="create"} 2.0 +nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="delete"} 0.0 +nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="update"} 0.0 +nautobot_ssot_operation_total{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",operation="no-change"} 1731.0 +# HELP nautobot_ssot_sync_memory_usage_bytes Nautobot SSoT Sync Memory Usage +# TYPE nautobot_ssot_sync_memory_usage_bytes gauge +nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="skip"} 0.0 +nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="create"} 2.0 +nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="delete"} 0.0 +nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="update"} 0.0 +nautobot_ssot_sync_memory_usage_bytes{job="plugins-nautobot_ssot-jobs-examples-exampledatasource",phase="no-change"} 1731.0 +nautobot_ssot_sync_memory_usage_bytes{job="",phase=""} 0.0 +``` diff --git a/docs/user/faq.md b/docs/user/faq.md index 318b08dc2..e4955af91 100644 --- a/docs/user/faq.md +++ b/docs/user/faq.md @@ -1 +1,5 @@ # Frequently Asked Questions + +## _Is the application actually a Single Source of Truth?_ + +In reality the application intends to have behaviors as if it was a SSoT. The difference being, the application intends to aggregate data in the real world where it is not feasible to have the System of Record be in a single system. diff --git a/mkdocs.yml b/mkdocs.yml index 4b77edcad..57c64763b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -102,18 +102,44 @@ nav: - App Overview: "user/app_overview.md" - Getting Started: "user/app_getting_started.md" - Using the App: "user/app_use_cases.md" + - Integrations: + - "user/integrations/index.md" + - Cisco ACI: "user/integrations/aci.md" + - Arista CloudVision: "user/integrations/aristacv.md" + - Device42: "user/integrations/device42.md" + - Infoblox: "user/integrations/infoblox.md" + - IPFabric: "user/integrations/ipfabric.md" + - ServiceNow: "user/integrations/servicenow.md" + - Modeling: "user/modeling.md" + - Performance: "user/performance.md" - Frequently Asked Questions: "user/faq.md" - External Interactions: "user/external_interactions.md" - Administrator Guide: - Install and Configure: "admin/install.md" + - Integrations Installation: + - "admin/integrations/index.md" + - Cisco ACI: "admin/integrations/aci_setup.md" + - Arista CloudVision: "admin/integrations/aristacv_setup.md" + - Device42: "admin/integrations/device42_setup.md" + - Infoblox: "admin/integrations/infoblox_setup.md" + - IPFabric: "admin/integrations/ipfabric_setup.md" + - ServiceNow: "admin/integrations/servicenow_setup.md" - Upgrade: "admin/upgrade.md" - Uninstall: "admin/uninstall.md" - Compatibility Matrix: "admin/compatibility_matrix.md" - Release Notes: - "admin/release_notes/index.md" + - v2.0: "admin/release_notes/version_2.0.md" + - v1.6: "admin/release_notes/version_1.5.md" + - v1.5: "admin/release_notes/version_1.5.md" + - v1.4: "admin/release_notes/version_1.4.md" + - v1.3: "admin/release_notes/version_1.3.md" + - v1.2: "admin/release_notes/version_1.2.md" + - v1.1: "admin/release_notes/version_1.1.md" - v1.0: "admin/release_notes/version_1.0.md" - Developer Guide: - Extending the App: "dev/extending.md" + - Developing Jobs: "dev/jobs.md" - Contributing to the App: "dev/contributing.md" - Development Environment: "dev/dev_environment.md" - Architecture Decision Records: "dev/arch_decision.md" @@ -121,4 +147,6 @@ nav: - "dev/code_reference/index.md" - Package: "dev/code_reference/package.md" - API: "dev/code_reference/api.md" + - Models: "dev/code_reference/models.md" + - Other classes: "dev/other_classes_reference.md" - Nautobot Docs Home ↗︎: "https://docs.nautobot.com" diff --git a/nautobot_ssot/__init__.py b/nautobot_ssot/__init__.py index 56c32a5b0..2f16d8a85 100644 --- a/nautobot_ssot/__init__.py +++ b/nautobot_ssot/__init__.py @@ -1,10 +1,38 @@ """Plugin declaration for nautobot_ssot.""" -# Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added +import os from importlib import metadata +from django.conf import settings +from nautobot.extras.plugins import NautobotAppConfig +from nautobot.core.settings_funcs import is_truthy + +from nautobot_ssot.integrations.utils import each_enabled_integration_module +from nautobot_ssot.utils import logger + __version__ = metadata.version(__name__) -from nautobot.extras.plugins import NautobotAppConfig + +_CONFLICTING_APP_NAMES = [ + "nautobot_ssot_aci", + "nautobot_ssot_aristacv", + "nautobot_ssot_device42", + "nautobot_ssot_infoblox", + "nautobot_ssot_ipfabric", + "nautobot_ssot_servicenow", +] + + +def _check_for_conflicting_apps(): + intersection = set(_CONFLICTING_APP_NAMES).intersection(set(settings.PLUGINS)) + if intersection: + raise RuntimeError( + f"The following apps are installed and conflict with `nautobot-ssot`: {', '.join(intersection)}." + "See: https://docs.nautobot.com/projects/ssot/en/latest/admin/upgrade/#potential-apps-conflicts" + ) + + +if not is_truthy(os.getenv("NAUTOBOT_SSOT_ALLOW_CONFLICTING_APPS", "False")): + _check_for_conflicting_apps() class NautobotSSOTPluginConfig(NautobotAppConfig): @@ -14,13 +42,90 @@ class NautobotSSOTPluginConfig(NautobotAppConfig): verbose_name = "Single Source of Truth" version = __version__ author = "Network to Code, LLC" - description = "Nautobot Single Source of Truth." + description = "Nautobot app that enables Single Source of Truth. Allows users to aggregate distributed data sources and/or distribute Nautobot data to other data sources such as databases and SDN controllers." base_url = "ssot" required_settings = [] min_version = "2.0.0" max_version = "2.9999" - default_settings = {} + default_settings = { + "aci_apics": [], + "aci_tag": "", + "aci_tag_color": "", + "aci_tag_up": "", + "aci_tag_up_color": "", + "aci_tag_down": "", + "aci_tag_down_color": "", + "aci_manufacturer_name": "", + "aci_ignore_tenants": [], + "aci_comments": "", + "aci_site": "", + "aristacv_apply_import_tag": False, + "aristacv_controller_site": "", + "aristacv_create_controller": False, + "aristacv_cvaas_url": "www.arista.io:443", + "aristacv_cvp_host": "", + "aristacv_cvp_password": "", + "aristacv_cvp_port": "443", + "aristacv_cvp_token": "", + "aristacv_cvp_user": "", + "aristacv_delete_devices_on_sync": False, + "aristacv_from_cloudvision_default_device_role": "", + "aristacv_from_cloudvision_default_device_role_color": "", + "aristacv_from_cloudvision_default_site": "", + "aristacv_hostname_patterns": [], + "aristacv_import_active": False, + "aristacv_role_mappings": {}, + "aristacv_site_mappings": {}, + "aristacv_verify": True, + "device42_host": "", + "device42_username": "", + "device42_password": "", + "device42_defaults": {}, + "device42_delete_on_sync": False, + "device42_use_dns": True, + "device42_customer_is_facility": True, + "device42_facility_prepend": "", + "device42_role_prepend": "", + "device42_ignore_tag": "", + "device42_hostname_mapping": [], + "enable_aci": False, + "enable_aristacv": False, + "enable_device42": False, + "enable_infoblox": False, + "enable_ipfabric": False, + "enable_servicenow": False, + "hide_example_jobs": True, + "infoblox_default_status": "", + "infoblox_enable_rfc1918_network_containers": False, + "infoblox_enable_sync_to_infoblox": False, + "infoblox_import_objects_ip_addresses": False, + "infoblox_import_objects_subnets": False, + "infoblox_import_objects_vlan_views": False, + "infoblox_import_objects_vlans": False, + "infoblox_import_subnets": [], + "infoblox_password": "", + "infoblox_url": "", + "infoblox_username": "", + "infoblox_verify_ssl": True, + "infoblox_wapi_version": "", + "ipfabric_api_token": "", + "ipfabric_host": "", + "ipfabric_ssl_verify": True, + "ipfabric_timeout": 15, + "ipfabric_nautobot_host": "", + "servicenow_instance": "", + "servicenow_password": "", + "servicenow_username": "", + } caching_config = {} + def ready(self): + """Trigger callback when database is ready.""" + super().ready() + + for module in each_enabled_integration_module("signals"): + logger.debug("Registering signals for %s", module.__file__) + module.register_signals(self) + config = NautobotSSOTPluginConfig # pylint:disable=invalid-name diff --git a/nautobot_ssot/tests/__init__.py b/nautobot_ssot/tests/__init__.py index 37afdda70..547039b14 100644 --- a/nautobot_ssot/tests/__init__.py +++ b/nautobot_ssot/tests/__init__.py @@ -1 +1,7 @@ """Unit tests for nautobot_ssot plugin.""" + +from django.conf import settings + +if "job_logs" in settings.DATABASES: + settings.DATABASES["job_logs"] = settings.DATABASES["job_logs"].copy() + settings.DATABASES["job_logs"]["TEST"] = {"MIRROR": "default"} diff --git a/nautobot_ssot/tests/test_api.py b/nautobot_ssot/tests/test_api.py index edf5738ff..73572c41e 100644 --- a/nautobot_ssot/tests/test_api.py +++ b/nautobot_ssot/tests/test_api.py @@ -1,11 +1,11 @@ """Unit tests for nautobot_ssot.""" from django.contrib.auth import get_user_model -from django.test import TestCase from django.urls import reverse +from nautobot.users.models import Token +from nautobot.core.testing import TestCase from rest_framework import status from rest_framework.test import APIClient -from nautobot.users.models import Token User = get_user_model() diff --git a/poetry.lock b/poetry.lock index b5f7a6407..8379735f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1804,13 +1804,13 @@ six = ">=1.12" [[package]] name = "griffe" -version = "0.30.1" +version = "0.36.7" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"}, - {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"}, + {file = "griffe-0.36.7-py3-none-any.whl", hash = "sha256:7a09f8e9b97c7ebe227f6529a298bf7e0e742a9837ee261cc8260d50b4aa039f"}, + {file = "griffe-0.36.7.tar.gz", hash = "sha256:a6fe60b16720ca0cf63c9e667a4c05eea40dfe4abcf114741885f945b74c7071"}, ] [package.dependencies] @@ -2094,17 +2094,6 @@ files = [ {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, ] -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - [[package]] name = "invoke" version = "2.2.0" @@ -2572,16 +2561,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -2740,13 +2719,13 @@ files = [ [[package]] name = "mkdocs" -version = "1.4.2" +version = "1.5.2" description = "Project documentation with Markdown." optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, - {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, + {file = "mkdocs-1.5.2-py3-none-any.whl", hash = "sha256:60a62538519c2e96fe8426654a67ee177350451616118a41596ae7c876bb7eac"}, + {file = "mkdocs-1.5.2.tar.gz", hash = "sha256:70d0da09c26cff288852471be03c23f0f521fc15cf16ac89c7a3bfb9ae8d24f9"}, ] [package.dependencies] @@ -2755,16 +2734,19 @@ colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" -markdown = ">=3.2.1,<3.4" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -2783,13 +2765,13 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-material" -version = "9.0.11" +version = "9.1.15" description = "Documentation that simply works" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.0.11-py3-none-any.whl", hash = "sha256:90a1e1ed41e90de5d0ab97c874b7bf6af488d0faf4aaea8e5868e01f3f1ed923"}, - {file = "mkdocs_material-9.0.11.tar.gz", hash = "sha256:aff49e4ce622a107ed563b3a6a37dc3660a45a0e4d9e7d4d2c13ce9dc02a7faf"}, + {file = "mkdocs_material-9.1.15-py3-none-any.whl", hash = "sha256:b49e12869ab464558e2dd3c5792da5b748a7e0c48ee83b4d05715f98125a7a39"}, + {file = "mkdocs_material-9.1.15.tar.gz", hash = "sha256:8513ab847c9a541ed3d11a3a7eed556caf72991ee786c31c5aac6691a121088a"}, ] [package.dependencies] @@ -2853,17 +2835,17 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.1.2" +version = "1.5.2" description = "A Python handler for mkdocstrings." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocstrings_python-1.1.2-py3-none-any.whl", hash = "sha256:c2b652a850fec8e85034a9cdb3b45f8ad1a558686edc20ed1f40b4e17e62070f"}, - {file = "mkdocstrings_python-1.1.2.tar.gz", hash = "sha256:f28bdcacb9bcdb44b6942a5642c1ea8b36870614d33e29e3c923e204a8d8ed61"}, + {file = "mkdocstrings_python-1.5.2-py3-none-any.whl", hash = "sha256:ed37ca6d216986e2ac3530c19c3e7be381d1e3d09ea414e4ff467d6fd2cbd9c1"}, + {file = "mkdocstrings_python-1.5.2.tar.gz", hash = "sha256:81eb4a93bc454a253daf247d1a11397c435d641c64fa165324c17c06170b1dfb"}, ] [package.dependencies] -griffe = ">=0.24" +griffe = ">=0.35" mkdocstrings = ">=0.20" [[package]] @@ -3450,21 +3432,6 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] -[[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - [[package]] name = "prometheus-client" version = "0.17.1" @@ -4006,28 +3973,6 @@ files = [ {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, ] -[[package]] -name = "pytest" -version = "7.4.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - [[package]] name = "python-crontab" version = "3.0.0" @@ -4195,7 +4140,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -4203,15 +4147,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -4228,7 +4165,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -4236,7 +4172,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -5337,5 +5272,5 @@ servicenow = ["Jinja2", "PyYAML", "ijson", "oauthlib", "python-magic", "pytz", " [metadata] lock-version = "2.0" -python-versions = "^3.8,<3.12" -content-hash = "0e32a208347fc1924a64aba1f373c26a0ac53bfaadadec4473060e309f37f254" +python-versions = ">=3.8,<3.12" +content-hash = "ffdfa41771733346a0fb1e714ab624f87efbfd821366e94604b4f64079d534d3" diff --git a/pyproject.toml b/pyproject.toml index 5bd452b62..ecd0a4b4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautobot-ssot" -version = "0.1.0" +version = "2.0.0" description = "Nautobot Single Source of Truth" authors = ["Network to Code, LLC "] license = "Apache-2.0" @@ -29,20 +29,45 @@ packages = [ python = ">=3.8,<3.12" # Used for local development nautobot = "^2.0.0" +diffsync = "^1.6.0" +Jinja2 = { version = ">=2.11.3", optional = true } +Markdown = "!=3.3.5" +PyYAML = { version = ">=6", optional = true } +cloudvision = { version = "^1.9.0", optional = true } +cvprac = { version = "^1.2.2", optional = true } +dnspython = { version = "^2.1.0", optional = true } +nautobot-device-lifecycle-mgmt = { version = "^2.0.0", optional = true } +packaging = ">=21.3, <24" +prometheus-client = "~0.17.1" +ijson = { version = ">=2.5.1", optional = true } +ipfabric = { version = "~6.0.9", optional = true } +ipfabric-diagrams = { version = "~6.0.2", optional = true } +netutils = { version = "^1.0.0", optional = true } +oauthlib = { version = ">=3.1.0", optional = true } +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 } +six = { version = ">=1.13.0", optional = true } +drf-spectacular = "0.26.3" +httpx = { version = ">=0.23.3", optional = true } [tool.poetry.group.dev.dependencies] bandit = "*" black = "*" coverage = "*" django-debug-toolbar = "*" +django-extensions = "*" flake8 = "*" invoke = "*" ipython = "*" +jedi = "^0.17.2" pydocstyle = "*" pylint = "*" pylint-django = "*" pylint-nautobot = "*" yamllint = "*" +markdown-include = "*" toml = "*" Markdown = "*" # Rendering docs to HTML @@ -54,6 +79,87 @@ mkdocs-version-annotations = "1.0.0" # Automatic documentation from sources, for MkDocs mkdocstrings = "0.22.0" mkdocstrings-python = "1.5.2" +requests-mock = "^1.10.0" +parameterized = "^0.8.1" +myst-parser = "^0.15.2" +nautobot-chatops = { version = "^3.0.0", extras = ["ipfabric"] } +responses = "^0.14.0" + +[tool.poetry.plugins."nautobot_ssot.data_sources"] +"example" = "nautobot_ssot.sync.example:ExampleSyncWorker" + +[tool.poetry.plugins."nautobot_ssot.data_targets"] +"example" = "nautobot_ssot.sync.example:ExampleSyncWorker" + +[tool.poetry.plugins."nautobot.workers"] +"ipfabric" = "nautobot_ssot.integrations.ipfabric.workers:ipfabric" + +[tool.poetry.extras] +aci = [ + "PyYAML", +] +all = [ + "Jinja2", + "PyYAML", + "cloudvision", + "cvprac", + "dnspython", + "ijson", + "ipfabric", + "ipfabric-diagrams", + "nautobot-device-lifecycle-mgmt", + "netutils", + "oauthlib", + "python-magic", + "pytz", + "requests", + "requests-oauthlib", + "six", +] +aristacv = [ + "cloudvision", + "cvprac", +] +device42 = [ + "requests", +] +infoblox = [ + "dnspython", +] +ipfabric = [ + "httpx", + "ipfabric", + "ipfabric-diagrams", + "netutils", +] +# 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 +# For now, we have embedded a copy of PySNow under nautobot_ssot/integrations/servicenow/third_party/pysnow; +# here are its direct packaging dependencies: +pysnow = [ + "requests", + "oauthlib", + "python-magic", + "requests-oauthlib", + "six", + "ijson", + "pytz", +] +servicenow = [ + "Jinja2", + "PyYAML", + "ijson", + "oauthlib", + "python-magic", + "pytz", + "requests", + "requests-oauthlib", + "six", +] +nautobot-device-lifecycle-mgmt = [ + "nautobot-device-lifecycle-mgmt", +] [tool.black] line-length = 120 @@ -72,6 +178,7 @@ exclude = ''' | buck-out | build | dist + | nautobot_ssot/integrations/servicenow/third_party )/ | settings.py # This is where you define files that should not be stylized by black # the root of the project @@ -91,7 +198,8 @@ no-docstring-rgx="^(_|test_|Meta$)" # Line length is enforced by Black, so pylint doesn't need to check it. # Pylint and Black disagree about how to format multi-line arrays; Black wins. disable = """, - line-too-long + line-too-long, + too-few-public-methods, """ [tool.pylint.miscellaneous] From af6e04e9eafc34683c54a8cc51f4d3bdcfc7aff6 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Fri, 20 Oct 2023 09:05:34 +0000 Subject: [PATCH 3/5] fix: griffe warning --- nautobot_ssot/jobs/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nautobot_ssot/jobs/base.py b/nautobot_ssot/jobs/base.py index 35b6a460a..5c611dcac 100644 --- a/nautobot_ssot/jobs/base.py +++ b/nautobot_ssot/jobs/base.py @@ -3,7 +3,7 @@ from datetime import datetime import traceback import tracemalloc -from typing import Iterable +from typing import Iterable, Optional from django.db.utils import OperationalError from django.templatetags.static import static @@ -19,7 +19,7 @@ from nautobot.extras.jobs import DryRunVar, Job, BooleanVar from nautobot_ssot.choices import SyncLogEntryActionChoices -from nautobot_ssot.models import Sync, SyncLogEntry +from nautobot_ssot.models import BaseModel, Sync, SyncLogEntry DataMapping = namedtuple("DataMapping", ["source_name", "source_url", "target_name", "target_url"]) @@ -172,7 +172,7 @@ def record_memory_trace(step: str): if memory_profiling: record_memory_trace("sync") - def lookup_object(self, model_name, unique_id): # pylint: disable=unused-argument + def lookup_object(self, model_name, unique_id) -> Optional[BaseModel]: # pylint: disable=unused-argument """Look up the Nautobot record, if any, identified by the args. Optional helper method used to build more detailed/accurate SyncLogEntry records from DiffSync logs. @@ -182,7 +182,7 @@ def lookup_object(self, model_name, unique_id): # pylint: disable=unused-argume unique_id (str): DiffSyncModel unique_id or similar unique identifier. Returns: - Union[models.Model, None]: Nautobot model instance, or None + Optional[BaseModel]: Nautobot model instance, or None """ return None From 9bd0d7e798ef13c318dd85908471fe2e8f6fb6c7 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Fri, 20 Oct 2023 09:06:07 +0000 Subject: [PATCH 4/5] chore: Remove pylint --- nautobot_ssot/tests/infoblox/test_client.py | 53 ++++++++++----------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/nautobot_ssot/tests/infoblox/test_client.py b/nautobot_ssot/tests/infoblox/test_client.py index 02a40bc4c..0095e6836 100644 --- a/nautobot_ssot/tests/infoblox/test_client.py +++ b/nautobot_ssot/tests/infoblox/test_client.py @@ -6,7 +6,6 @@ import unittest from unittest.mock import patch -import pytest from requests.models import HTTPError import requests_mock @@ -207,10 +206,10 @@ def test_get_host_record_by_name_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_host_record_by_name(mock_fqdn) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_host_record_by_ip_success(self): """Test get_host_by_ip success.""" @@ -232,10 +231,10 @@ def test_get_host_record_by_ip_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_host_record_by_ip(mock_ip) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_a_record_by_name_success(self): """Test get_a_record_by_name success.""" @@ -257,10 +256,10 @@ def test_get_a_record_by_name_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_a_record_by_name(mock_fqdn) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_a_record_by_ip_success(self): """Test get_a_record_by_ip success.""" @@ -282,10 +281,10 @@ def test_get_a_record_by_ip_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_a_record_by_ip(mock_ip) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_all_dns_views_success(self): """Test get_all_dns_views success.""" @@ -305,10 +304,10 @@ def test_get_all_dns_views_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_all_dns_views() - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_dhcp_lease_from_ipv4_success(self): """Test get_dhcp_lease_from_ipv4 success.""" @@ -330,10 +329,10 @@ def test_get_dhcp_lease_from_ipv4_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_dhcp_lease_from_ipv4(mock_ip) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_dhcp_lease_from_hostname_success(self): """Test get_dhcp_lease_from_hostname success.""" @@ -355,10 +354,10 @@ def test_get_dhcp_lease_from_hostname_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_dhcp_lease_from_hostname(mock_host) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_all_subnets_success(self): """Test get_all_subnets success.""" @@ -400,10 +399,10 @@ def test_get_authoritative_zone_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_authoritative_zone() - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_create_ptr_record_success(self): mock_uri = "record:ptr" @@ -427,10 +426,10 @@ def test_create_ptr_record_failure(self): with requests_mock.Mocker() as req: req.post(f"{LOCALHOST}/{mock_uri}", json=mock_payload, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.create_ptr_record(mock_fqdn, mock_ip_address) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_create_a_record_success(self): mock_uri = "record:a" @@ -453,10 +452,10 @@ def test_create_a_record_failure(self): with requests_mock.Mocker() as req: req.post(f"{LOCALHOST}/{mock_uri}", json="", status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.create_a_record(mock_fqdn, mock_ip_address) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_create_host_record_success(self): mock_uri = "record:host" @@ -574,10 +573,10 @@ def test_find_network_reference_failure(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json="", status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client._find_network_reference(mock_network) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_get_ptr_record_by_name_success(self): """Test get_ptr_record_by_name success.""" @@ -611,10 +610,10 @@ def test_get_ptr_record_by_name_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json="", status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.get_ptr_record_by_name(mock_fqdn) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) def test_search_ipv4_address_success(self): """Test search_ipv4_address success.""" @@ -636,7 +635,7 @@ def test_search_ipv4_address_fail(self): with requests_mock.Mocker() as req: req.get(f"{LOCALHOST}/{mock_uri}", json=mock_response, status_code=404) - with pytest.raises(HTTPError) as err: + with self.assertRaises(HTTPError) as context: self.infoblox_client.search_ipv4_address(mock_ip) - self.assertEqual(err.value.response.status_code, 404) + self.assertEqual(context.exception.response.status_code, 404) From c63e80805d0b7a308b508b0dea9c1da1947b8920 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 24 Oct 2023 08:47:33 +0000 Subject: [PATCH 5/5] chore: Move docs to be filled up --- docs/dev/arch_decision.md | 7 ------- docs/user/app_overview.md | 11 ----------- mkdocs.yml | 18 +++++++----------- 3 files changed, 7 insertions(+), 29 deletions(-) delete mode 100644 docs/dev/arch_decision.md diff --git a/docs/dev/arch_decision.md b/docs/dev/arch_decision.md deleted file mode 100644 index e7bcbbe40..000000000 --- a/docs/dev/arch_decision.md +++ /dev/null @@ -1,7 +0,0 @@ -# Architecture Decision Records - -The intention is to document deviations from a standard Model View Controller (MVC) design. - -!!! warning "Developer Note - Remove Me!" - Optional page, remove if not applicable. - For examples see [Golden Config](https://github.com/nautobot/nautobot-plugin-golden-config/tree/develop/docs/dev/dev_adr.md) and [nautobot-plugin-reservation](https://github.com/networktocode/nautobot-plugin-reservation/blob/develop/docs/dev/dev_adr.md). diff --git a/docs/user/app_overview.md b/docs/user/app_overview.md index 260cd86b9..40334c381 100644 --- a/docs/user/app_overview.md +++ b/docs/user/app_overview.md @@ -24,14 +24,3 @@ The Nautobot SSoT app builds atop the [DiffSync](https://github.com/networktocod * Glenn Matthew (@glennmatthews) * Christian Adell (@chadell) * Justin Drew (@jdrew82) - -## Nautobot Features Used - -!!! warning "Developer Note - Remove Me!" - What is shown today in the Installed Plugins page in Nautobot. What parts of Nautobot does it interact with, what does it add etc. ? - -### Extras - -!!! warning "Developer Note - Remove Me!" - Custom Fields - things like which CFs are created by this app? - Jobs - are jobs, if so, which ones, installed by this app? diff --git a/mkdocs.yml b/mkdocs.yml index 57c64763b..6c3455f2b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ --- dev_addr: "127.0.0.1:8001" -edit_uri: "edit/main/nautobot-plugin-ssot/docs" +edit_uri: "blob/develop/docs" site_dir: "nautobot_ssot/static/nautobot_ssot/docs" site_name: "Single Source of Truth Documentation" site_url: "https://docs.nautobot.com/projects/ssot/en/latest/" @@ -14,17 +14,16 @@ theme: - "django" - "yaml" features: - - "content.action.edit" - - "content.action.view" - - "content.code.copy" - - "navigation.footer" - - "navigation.indexes" + - "navigation.tracking" - "navigation.tabs" - "navigation.tabs.sticky" - - "navigation.tracking" + - "navigation.footer" + - "search.suggest" - "search.highlight" - "search.share" - - "search.suggest" + - "navigation.indexes" + - "content.action.edit" + - "content.action.view" favicon: "assets/favicon.ico" logo: "assets/nautobot_logo.svg" palette: @@ -142,11 +141,8 @@ nav: - Developing Jobs: "dev/jobs.md" - Contributing to the App: "dev/contributing.md" - Development Environment: "dev/dev_environment.md" - - Architecture Decision Records: "dev/arch_decision.md" - Code Reference: - "dev/code_reference/index.md" - - Package: "dev/code_reference/package.md" - - API: "dev/code_reference/api.md" - Models: "dev/code_reference/models.md" - Other classes: "dev/other_classes_reference.md" - Nautobot Docs Home ↗︎: "https://docs.nautobot.com"

@@ -21,41 +11,81 @@ To avoid extra work and temporary links, make sure that publishing docs (or merg An App for Nautobot.