nautobot-workflow-engine 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot_workflow_engine-1.0.1/PKG-INFO +88 -0
- nautobot_workflow_engine-1.0.1/README.md +67 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/__init__.py +35 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/app-config-schema.json +31 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/datasources.py +257 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/executors.py +1138 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/filters.py +23 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/forms.py +745 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/jobs.py +211 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/migrations/0001_initial.py +152 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/migrations/__init__.py +0 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/models.py +271 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/navigation.py +36 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/scheduling.py +184 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/servicenow.py +348 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/static/nautobot_workflow_engine/js/workflow_list.js +116 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tables.py +22 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tasks.py +135 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/partials/workflow_list_grouped.html +92 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/partials/workflow_tiles.html +129 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_launch.html +2972 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_list.html +144 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_report.html +560 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_run.html +535 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_run_list.html +331 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templatetags/__init__.py +2 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templatetags/workflow_extras.py +38 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/__init__.py +1 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_action_ordering.py +102 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_basic.py +30 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_executors.py +137 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_forms_listdict.py +110 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_repo_filters.py +143 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_servicenow_scheduling.py +291 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/urls.py +27 -0
- nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/views.py +1381 -0
- nautobot_workflow_engine-1.0.1/pyproject.toml +269 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nautobot-workflow-engine
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Workflow Engine
|
|
5
|
+
Keywords: nautobot,nautobot-app,nautobot-plugin
|
|
6
|
+
Author: Eric Fetty
|
|
7
|
+
Requires-Python: >=3.10,<3.14
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Provides-Extra: all
|
|
16
|
+
Requires-Dist: jinja2 (>=3.1)
|
|
17
|
+
Requires-Dist: nautobot (>=3.0.0,<4.0.0)
|
|
18
|
+
Requires-Dist: pyyaml (>=6.0)
|
|
19
|
+
Requires-Dist: requests (>=2.31)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Nautobot Workflow Engine
|
|
23
|
+
|
|
24
|
+
Define automation in YAML, synchronize it through Nautobot's repository integration,
|
|
25
|
+
and launch it from dynamic forms. Execute Python scripts in Nautobot or job templates
|
|
26
|
+
in AWX, with ordered actions, reusable results, scheduling, and run history.
|
|
27
|
+
|
|
28
|
+
## Capabilities
|
|
29
|
+
|
|
30
|
+
- Typed inputs, Nautobot object lookups, dependent choices, conditional visibility,
|
|
31
|
+
validation, and tabular CSV input.
|
|
32
|
+
- Pre-actions, main actions, and post-actions in definition order.
|
|
33
|
+
- Reusable Python helpers and repository-backed Jinja filters.
|
|
34
|
+
- Immediate or scheduled runs, including optional ServiceNow change windows.
|
|
35
|
+
- Execution logs, AWX job monitoring, reporting, and estimated time savings.
|
|
36
|
+
|
|
37
|
+
## Get Started
|
|
38
|
+
|
|
39
|
+
The current package requires **Nautobot 3.x** and **Python 3.10 through 3.13**. The
|
|
40
|
+
selected Nautobot release must also support that Python version. These are package
|
|
41
|
+
requirements, not a claim that every combination has been tested.
|
|
42
|
+
|
|
43
|
+
1. Follow [installation and configuration](docs/admin/install.md) using an approved
|
|
44
|
+
package in your Nautobot deployment.
|
|
45
|
+
2. Configure a trusted workflow source and synchronize its Workflow Engine content.
|
|
46
|
+
3. Run the non-destructive [hostname tutorial](docs/user/app_getting_started.md).
|
|
47
|
+
4. Use the [authoring reference](docs/user/app_use_cases.md) to add inputs and actions.
|
|
48
|
+
|
|
49
|
+
AWX and ServiceNow are optional. Individual workflows may require additional apps,
|
|
50
|
+
Python packages, inventory, or service permissions. Examples are starting points for
|
|
51
|
+
review and adaptation, not production-ready deployment definitions.
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
- [Overview](docs/user/app_overview.md) and [documentation home](docs/index.md).
|
|
56
|
+
- [Workflow authoring](docs/user/app_use_cases.md) and [custom filters](docs/dev/extending.md).
|
|
57
|
+
- [AWX and ServiceNow](docs/user/external_interactions.md).
|
|
58
|
+
- [Compatibility](docs/admin/compatibility_matrix.md), [upgrades](docs/admin/upgrade.md),
|
|
59
|
+
and [release notes](docs/admin/release_notes/index.md).
|
|
60
|
+
- [Security and data handling](docs/admin/security.md).
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
The [custom-filter workflow](examples/workflows/custom-filter-demo.yaml) uses
|
|
65
|
+
[an echo script](examples/scripts/echo_hostname.py) and shared helpers without changing
|
|
66
|
+
inventory. Other examples illustrate inventory operations and optional vendor
|
|
67
|
+
integrations. Review their model, script, and service dependencies before use.
|
|
68
|
+
|
|
69
|
+
## Trust and Execution Logs
|
|
70
|
+
|
|
71
|
+
Workflow scripts and filters are trusted code running inside Nautobot, not sandboxed
|
|
72
|
+
submissions. Limit who can modify their source and who can launch or inspect runs.
|
|
73
|
+
Detailed script execution logging is intentional. Treat inputs, results, output,
|
|
74
|
+
diagnostics, and backups as operational data; do not submit or print credentials.
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
|
|
78
|
+
Read the [contribution guide](docs/dev/contributing.md),
|
|
79
|
+
[development environment guide](docs/dev/dev_environment.md), and
|
|
80
|
+
[architecture overview](docs/dev/arch_decision.md). Keep changes focused, use synthetic
|
|
81
|
+
fixtures, preserve migrations, and include regression coverage for behavioral changes.
|
|
82
|
+
|
|
83
|
+
## Publication Status
|
|
84
|
+
|
|
85
|
+
Open-source preparation is in progress. Apache-2.0 is the intended license, subject
|
|
86
|
+
to copyright-owner approval and a provenance review. No project-wide license grant
|
|
87
|
+
is asserted here until those steps are complete. A private vulnerability-reporting
|
|
88
|
+
contact and final artifact validation are also required before public release.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Nautobot Workflow Engine
|
|
2
|
+
|
|
3
|
+
Define automation in YAML, synchronize it through Nautobot's repository integration,
|
|
4
|
+
and launch it from dynamic forms. Execute Python scripts in Nautobot or job templates
|
|
5
|
+
in AWX, with ordered actions, reusable results, scheduling, and run history.
|
|
6
|
+
|
|
7
|
+
## Capabilities
|
|
8
|
+
|
|
9
|
+
- Typed inputs, Nautobot object lookups, dependent choices, conditional visibility,
|
|
10
|
+
validation, and tabular CSV input.
|
|
11
|
+
- Pre-actions, main actions, and post-actions in definition order.
|
|
12
|
+
- Reusable Python helpers and repository-backed Jinja filters.
|
|
13
|
+
- Immediate or scheduled runs, including optional ServiceNow change windows.
|
|
14
|
+
- Execution logs, AWX job monitoring, reporting, and estimated time savings.
|
|
15
|
+
|
|
16
|
+
## Get Started
|
|
17
|
+
|
|
18
|
+
The current package requires **Nautobot 3.x** and **Python 3.10 through 3.13**. The
|
|
19
|
+
selected Nautobot release must also support that Python version. These are package
|
|
20
|
+
requirements, not a claim that every combination has been tested.
|
|
21
|
+
|
|
22
|
+
1. Follow [installation and configuration](docs/admin/install.md) using an approved
|
|
23
|
+
package in your Nautobot deployment.
|
|
24
|
+
2. Configure a trusted workflow source and synchronize its Workflow Engine content.
|
|
25
|
+
3. Run the non-destructive [hostname tutorial](docs/user/app_getting_started.md).
|
|
26
|
+
4. Use the [authoring reference](docs/user/app_use_cases.md) to add inputs and actions.
|
|
27
|
+
|
|
28
|
+
AWX and ServiceNow are optional. Individual workflows may require additional apps,
|
|
29
|
+
Python packages, inventory, or service permissions. Examples are starting points for
|
|
30
|
+
review and adaptation, not production-ready deployment definitions.
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
- [Overview](docs/user/app_overview.md) and [documentation home](docs/index.md).
|
|
35
|
+
- [Workflow authoring](docs/user/app_use_cases.md) and [custom filters](docs/dev/extending.md).
|
|
36
|
+
- [AWX and ServiceNow](docs/user/external_interactions.md).
|
|
37
|
+
- [Compatibility](docs/admin/compatibility_matrix.md), [upgrades](docs/admin/upgrade.md),
|
|
38
|
+
and [release notes](docs/admin/release_notes/index.md).
|
|
39
|
+
- [Security and data handling](docs/admin/security.md).
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
The [custom-filter workflow](examples/workflows/custom-filter-demo.yaml) uses
|
|
44
|
+
[an echo script](examples/scripts/echo_hostname.py) and shared helpers without changing
|
|
45
|
+
inventory. Other examples illustrate inventory operations and optional vendor
|
|
46
|
+
integrations. Review their model, script, and service dependencies before use.
|
|
47
|
+
|
|
48
|
+
## Trust and Execution Logs
|
|
49
|
+
|
|
50
|
+
Workflow scripts and filters are trusted code running inside Nautobot, not sandboxed
|
|
51
|
+
submissions. Limit who can modify their source and who can launch or inspect runs.
|
|
52
|
+
Detailed script execution logging is intentional. Treat inputs, results, output,
|
|
53
|
+
diagnostics, and backups as operational data; do not submit or print credentials.
|
|
54
|
+
|
|
55
|
+
## Contributing
|
|
56
|
+
|
|
57
|
+
Read the [contribution guide](docs/dev/contributing.md),
|
|
58
|
+
[development environment guide](docs/dev/dev_environment.md), and
|
|
59
|
+
[architecture overview](docs/dev/arch_decision.md). Keep changes focused, use synthetic
|
|
60
|
+
fixtures, preserve migrations, and include regression coverage for behavioral changes.
|
|
61
|
+
|
|
62
|
+
## Publication Status
|
|
63
|
+
|
|
64
|
+
Open-source preparation is in progress. Apache-2.0 is the intended license, subject
|
|
65
|
+
to copyright-owner approval and a provenance review. No project-wide license grant
|
|
66
|
+
is asserted here until those steps are complete. A private vulnerability-reporting
|
|
67
|
+
contact and final artifact validation are also required before public release.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""App declaration for nautobot_workflow_engine."""
|
|
2
|
+
|
|
3
|
+
# Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added
|
|
4
|
+
import os
|
|
5
|
+
from importlib import metadata
|
|
6
|
+
|
|
7
|
+
from nautobot.apps import NautobotAppConfig
|
|
8
|
+
|
|
9
|
+
__version__ = metadata.version(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkflowEngineConfig(NautobotAppConfig):
|
|
13
|
+
"""App configuration for the nautobot_workflow_engine app."""
|
|
14
|
+
|
|
15
|
+
name = "nautobot_workflow_engine"
|
|
16
|
+
verbose_name = "Workflow Engine"
|
|
17
|
+
version = __version__
|
|
18
|
+
author = "Eric Fetty"
|
|
19
|
+
description = "General-purpose workflow engine for Nautobot"
|
|
20
|
+
base_url = "workflow-engine"
|
|
21
|
+
required_settings = []
|
|
22
|
+
default_settings = {
|
|
23
|
+
"awx_external_integration_name": "AWX",
|
|
24
|
+
"workflow_dir": "workflows",
|
|
25
|
+
"filters_dir": "filters",
|
|
26
|
+
"awx_poll_seconds": 5,
|
|
27
|
+
"show_disabled": False,
|
|
28
|
+
"show_add_new_item": os.getenv("SHOW_ADD_NEW_ITEM", "").split(","),
|
|
29
|
+
}
|
|
30
|
+
caching_config = {}
|
|
31
|
+
docs_view_name = "plugins:nautobot_workflow_engine:docs"
|
|
32
|
+
searchable_models = ["workflow"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
config = WorkflowEngineConfig # pylint:disable=invalid-name
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"awx_external_integration_name": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"default": "AWX"
|
|
8
|
+
},
|
|
9
|
+
"workflow_dir": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"default": "workflows"
|
|
12
|
+
},
|
|
13
|
+
"filters_dir": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"default": "filters"
|
|
16
|
+
},
|
|
17
|
+
"awx_poll_seconds": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"default": 5
|
|
20
|
+
},
|
|
21
|
+
"show_disabled": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"default": false
|
|
24
|
+
},
|
|
25
|
+
"show_add_new_item": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"default": []
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Git repository sync functionality for workflows."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.contrib.contenttypes.models import ContentType
|
|
9
|
+
from nautobot.apps.datasources import DatasourceContent
|
|
10
|
+
from nautobot.extras.models import GitRepository
|
|
11
|
+
|
|
12
|
+
from .executors import clear_repo_script_cache, validate_repo_filter_modules
|
|
13
|
+
from .models import Workflow, WorkflowAction, WorkflowFilterModule, WorkflowHelper, WorkflowInput
|
|
14
|
+
|
|
15
|
+
PLUGIN_CFG = settings.PLUGINS_CONFIG.get("nautobot_workflow_engine", {})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _process_includes(job_result, file_handler):
|
|
19
|
+
"""Process any includes in the content."""
|
|
20
|
+
content = file_handler.read_text(encoding="utf-8")
|
|
21
|
+
include_pattern = re.compile(r"""^\s*{%\s*include\s+[\"'](?P<path>[\w\/\.\-]+)[\"']\s*%}""")
|
|
22
|
+
for line in content.splitlines():
|
|
23
|
+
match = include_pattern.search(line)
|
|
24
|
+
if match:
|
|
25
|
+
# Load the template path from the match
|
|
26
|
+
template_path = match.group("path")
|
|
27
|
+
template_file = file_handler.parent / template_path
|
|
28
|
+
if template_file.exists():
|
|
29
|
+
# If the template file exists, read its content and replace the include
|
|
30
|
+
template_content = template_file.read_text(encoding="utf-8")
|
|
31
|
+
content = content.replace(line, template_content)
|
|
32
|
+
else:
|
|
33
|
+
# If the template file doesn't exist, log a warning
|
|
34
|
+
job_result.log(f"Template file not found: {template_file}")
|
|
35
|
+
|
|
36
|
+
return content
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_action_list(data, *keys):
|
|
40
|
+
"""Return first non-empty action list for any provided YAML key alias."""
|
|
41
|
+
for key in keys:
|
|
42
|
+
value = data.get(key)
|
|
43
|
+
if value is None:
|
|
44
|
+
continue
|
|
45
|
+
if isinstance(value, list):
|
|
46
|
+
return value
|
|
47
|
+
# Be forgiving if user accidentally provides an empty scalar/object.
|
|
48
|
+
return []
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_filter_module_path(relative_path):
|
|
53
|
+
"""Return True when the repository-relative path is inside the configured filters directory."""
|
|
54
|
+
filters_dir = Path(PLUGIN_CFG.get("filters_dir", "filters"))
|
|
55
|
+
filters_parts = filters_dir.parts
|
|
56
|
+
rel_parts = relative_path.parts
|
|
57
|
+
return len(rel_parts) >= len(filters_parts) and rel_parts[: len(filters_parts)] == filters_parts
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sync_repo_python_modules(repository_record, repo_path, job_result):
|
|
61
|
+
"""Store repository Python modules in helper or filter-module models."""
|
|
62
|
+
WorkflowHelper.objects.filter(source_repo=repository_record).delete()
|
|
63
|
+
WorkflowFilterModule.objects.filter(source_repo=repository_record).delete()
|
|
64
|
+
|
|
65
|
+
for py_file in repo_path.rglob("*.py"):
|
|
66
|
+
if "__pycache__" in str(py_file) or py_file.name.startswith("."):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
relative_path = py_file.relative_to(repo_path)
|
|
70
|
+
model = WorkflowFilterModule if _is_filter_module_path(relative_path) else WorkflowHelper
|
|
71
|
+
model.objects.create(
|
|
72
|
+
source_repo=repository_record,
|
|
73
|
+
file_path=str(relative_path),
|
|
74
|
+
content=py_file.read_text(encoding="utf-8"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if model is WorkflowFilterModule:
|
|
78
|
+
job_result.log(message=f"Stored filter module: {relative_path}")
|
|
79
|
+
else:
|
|
80
|
+
job_result.log(message=f"Stored Python file: {relative_path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def sync_workflows_from_repo(repository_record: GitRepository, job_result, delete=False):
|
|
84
|
+
"""Sync workflow definitions and helper files from a Git repository."""
|
|
85
|
+
repo_path = Path(repository_record.filesystem_path)
|
|
86
|
+
wf_dir = PLUGIN_CFG["workflow_dir"]
|
|
87
|
+
workflows_path = repo_path / wf_dir
|
|
88
|
+
if not workflows_path.exists():
|
|
89
|
+
clear_repo_script_cache(repository_record)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Track workflows found in this sync
|
|
93
|
+
synced_workflow_keys = set()
|
|
94
|
+
|
|
95
|
+
_sync_repo_python_modules(repository_record, repo_path, job_result)
|
|
96
|
+
validate_repo_filter_modules(repository_record)
|
|
97
|
+
|
|
98
|
+
for yml in sorted(list(workflows_path.glob("*.y*ml"))):
|
|
99
|
+
content = _process_includes(job_result, yml)
|
|
100
|
+
data = yaml.safe_load(content)
|
|
101
|
+
|
|
102
|
+
# Track this workflow key
|
|
103
|
+
workflow_key = data.get("key")
|
|
104
|
+
if workflow_key:
|
|
105
|
+
synced_workflow_keys.add(workflow_key)
|
|
106
|
+
|
|
107
|
+
# Check if workflow is explicitly disabled in YAML
|
|
108
|
+
yaml_enabled = data.get("enabled", True)
|
|
109
|
+
|
|
110
|
+
wf, _ = Workflow.objects.update_or_create(
|
|
111
|
+
key=data["key"],
|
|
112
|
+
defaults={
|
|
113
|
+
"name": data.get("name", data["key"]),
|
|
114
|
+
"description": data.get("description", ""),
|
|
115
|
+
"category": data.get("category", ""),
|
|
116
|
+
"enabled": yaml_enabled, # Use enabled flag from YAML
|
|
117
|
+
"manual_duration_minutes": data.get("manual_duration_minutes"),
|
|
118
|
+
"raw_definition": data,
|
|
119
|
+
"source_repo": repository_record,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
wf.inputs.all().delete()
|
|
123
|
+
for order, ip in enumerate(data.get("inputs", []), start=1):
|
|
124
|
+
ct = None
|
|
125
|
+
if ip.get("type") in ("object", "multiobject") and ip.get("model"):
|
|
126
|
+
app_label, model = ip["model"].split(".")
|
|
127
|
+
try:
|
|
128
|
+
ct = ContentType.objects.get(app_label=app_label, model=model.lower())
|
|
129
|
+
except ContentType.DoesNotExist:
|
|
130
|
+
raise ValueError(f"[Workflow {wf.key}] Invalid model for input {ip['key']}: {ip['model']}")
|
|
131
|
+
|
|
132
|
+
# Normalize validators:
|
|
133
|
+
# - support either a single "regex" (with optional message/flags near root)
|
|
134
|
+
# - or a list under "validators", e.g. [{"regex": "...", "flags": ["IGNORECASE"], "message": "..."}]
|
|
135
|
+
validators = ip.get("validators")
|
|
136
|
+
if not validators:
|
|
137
|
+
if "regex" in ip:
|
|
138
|
+
v = {"regex": ip.get("regex")}
|
|
139
|
+
if "message" in ip:
|
|
140
|
+
v["message"] = ip["message"]
|
|
141
|
+
if "flags" in ip:
|
|
142
|
+
v["flags"] = ip["flags"]
|
|
143
|
+
validators = [v]
|
|
144
|
+
|
|
145
|
+
WorkflowInput.objects.create(
|
|
146
|
+
workflow=wf,
|
|
147
|
+
key=ip["key"],
|
|
148
|
+
label=ip.get("label", ip["key"]),
|
|
149
|
+
input_type=ip["type"],
|
|
150
|
+
content_type=ct,
|
|
151
|
+
required=ip.get("required", False),
|
|
152
|
+
default=ip.get("default"),
|
|
153
|
+
depends_on=ip.get("depends_on", []),
|
|
154
|
+
filter_query=ip.get("filter", {}).get("query"),
|
|
155
|
+
choices=ip.get("choices"),
|
|
156
|
+
columns=ip.get("columns"),
|
|
157
|
+
visible_when=ip.get("show_if"),
|
|
158
|
+
validators=validators,
|
|
159
|
+
order=order,
|
|
160
|
+
)
|
|
161
|
+
# Use bulk deletion to avoid get_absolute_url issues
|
|
162
|
+
WorkflowAction.objects.filter(workflow=wf).delete()
|
|
163
|
+
|
|
164
|
+
phased_actions = [
|
|
165
|
+
("pre", _get_action_list(data, "pre_actions", "pre-actions", "before_actions", "before-actions")),
|
|
166
|
+
# backwards-compatible existing key
|
|
167
|
+
("main", _get_action_list(data, "actions", "main_actions", "main-actions")),
|
|
168
|
+
("post", _get_action_list(data, "post_actions", "post-actions", "after_actions", "after-actions")),
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
for phase, actions in phased_actions:
|
|
172
|
+
for order, act in enumerate(actions, start=1):
|
|
173
|
+
|
|
174
|
+
if act.get("type") == "nautobot":
|
|
175
|
+
script_path = (act.get("script") or "").lstrip("/")
|
|
176
|
+
if not script_path:
|
|
177
|
+
raise ValueError(f"[Workflow {wf.key}] 'script' is required for nautobot actions")
|
|
178
|
+
|
|
179
|
+
# Verify the script exists in WorkflowHelper
|
|
180
|
+
if not WorkflowHelper.objects.filter(source_repo=repository_record, file_path=script_path).exists():
|
|
181
|
+
raise ValueError(f"[Workflow {wf.key}] Script not found in repository: {script_path}")
|
|
182
|
+
|
|
183
|
+
WorkflowAction.objects.create(
|
|
184
|
+
workflow=wf,
|
|
185
|
+
phase=phase,
|
|
186
|
+
order=order,
|
|
187
|
+
action_type="nautobot",
|
|
188
|
+
name=act.get("name", "nautobot"),
|
|
189
|
+
when=act.get("when", ""),
|
|
190
|
+
script_path="/" + script_path,
|
|
191
|
+
with_vars=act.get("with", {}),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
elif act.get("type") == "awx":
|
|
195
|
+
WorkflowAction.objects.create(
|
|
196
|
+
workflow=wf,
|
|
197
|
+
phase=phase,
|
|
198
|
+
order=order,
|
|
199
|
+
action_type="awx",
|
|
200
|
+
name=act.get("name", "awx"),
|
|
201
|
+
when=act.get("when", ""),
|
|
202
|
+
awx_template_id=act.get("template_id"),
|
|
203
|
+
awx_extra_vars=act.get("extra_vars", {}),
|
|
204
|
+
monitor=act.get("monitor", True),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"[Workflow {wf.key}] Unsupported action type in phase '{phase}': {act.get('type')}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Disable workflows that are no longer in the repository (instead of deleting)
|
|
213
|
+
if delete:
|
|
214
|
+
workflows_to_disable = Workflow.objects.filter(
|
|
215
|
+
source_repo=repository_record,
|
|
216
|
+
enabled=True, # Only disable currently enabled workflows
|
|
217
|
+
).exclude(key__in=synced_workflow_keys)
|
|
218
|
+
|
|
219
|
+
disabled_count = 0
|
|
220
|
+
|
|
221
|
+
for workflow in workflows_to_disable:
|
|
222
|
+
# Count related objects for logging
|
|
223
|
+
action_count = workflow.actions.count()
|
|
224
|
+
input_count = workflow.inputs.count()
|
|
225
|
+
run_count = workflow.runs.count()
|
|
226
|
+
|
|
227
|
+
job_result.log(
|
|
228
|
+
message=f"Disabling workflow '{workflow.name}' (key: {workflow.key}) "
|
|
229
|
+
f"with {action_count} actions, {input_count} inputs, {run_count} runs - no longer in repository"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Disable instead of delete to preserve history
|
|
233
|
+
workflow.enabled = False
|
|
234
|
+
workflow.save()
|
|
235
|
+
disabled_count += 1
|
|
236
|
+
|
|
237
|
+
if disabled_count > 0:
|
|
238
|
+
job_result.log(
|
|
239
|
+
message=f"Disabled {disabled_count} workflows no longer present in repository. Run history preserved."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
clear_repo_script_cache(repository_record)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
datasource_contents = []
|
|
246
|
+
|
|
247
|
+
datasource_contents.append(
|
|
248
|
+
(
|
|
249
|
+
"extras.gitrepository",
|
|
250
|
+
DatasourceContent(
|
|
251
|
+
name="workflows",
|
|
252
|
+
content_identifier="nautobot_workflow_engine.workflows",
|
|
253
|
+
icon="mdi-file-document-outline",
|
|
254
|
+
callback=sync_workflows_from_repo,
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
)
|