Content-Length: 678328 | pFad | http://github.com/nautobot/nautobot/pull/6752/files/dfedd2722dc7b672ed2760b637eaafbbae85efb0

D9 GraphQL as a Git Data source provider. by djhoward12 · Pull Request #6752 · nautobot/nautobot · GitHub
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL as a Git Data source provider. #6752

Merged
merged 23 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8428c21
initial commit for graphql as a git data source
Jan 9, 2025
c2442f5
Merge branch 'develop' of https://github.com/djhoward12/nautobot into…
Jan 9, 2025
0a26195
Adding conditional for isdir and stripped extension from filename whe…
Jan 9, 2025
89f42b9
Merge branch 'nautobot:develop' into develop
djhoward12 Jan 9, 2025
934cbce
Update nautobot/docs/user-guide/feature-guides/git-data-source.md
djhoward12 Jan 9, 2025
22f63dd
Update nautobot/docs/user-guide/feature-guides/git-data-source.md
djhoward12 Jan 9, 2025
00ca2e2
Update nautobot/docs/user-guide/feature-guides/git-data-source.md
djhoward12 Jan 9, 2025
e24c920
Update nautobot/extras/datasources/git.py
djhoward12 Jan 9, 2025
bd37894
Adjusting tests for missing .gql extension and updates per PR comments.
Jan 10, 2025
317b021
Merge branch 'nautobot:develop' into develop
djhoward12 Jan 10, 2025
dfedd27
Merge branch 'develop' into develop
HanlinMiao Jan 15, 2025
e1574e8
Addressing PR comments, clear error msg, spelling error, add flag, up…
Jan 16, 2025
04c4e70
Merge branch 'develop' into develop
djhoward12 Jan 16, 2025
71f79df
Addressing PR comments. Don't delete query if exists and update has s…
Jan 16, 2025
806dc77
Merge branch 'nautobot:develop' into develop
djhoward12 Jan 16, 2025
290a92c
Update nautobot/extras/datasources/git.py
djhoward12 Jan 16, 2025
0242a2f
moving query=query content inside if. removing extra appends.
Jan 16, 2025
c8f4ae0
Merge branch 'develop' into develop
djhoward12 Jan 17, 2025
a0e241b
Merge branch 'develop' into develop
glennmatthews Jan 21, 2025
b917d0e
updating documentation to be in line with updates in #6801.
Jan 22, 2025
0693ee8
Merge branch 'nautobot:develop' into develop
djhoward12 Jan 22, 2025
30b476b
Merge branch 'develop' into develop
glennmatthews Jan 22, 2025
9c2df03
Update changes/4702.added
glennmatthews Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/4702.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added Graphql as a Git data source provider
glennmatthews marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 14 additions & 0 deletions nautobot/docs/user-guide/feature-guides/git-data-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The feature uses the concept of a `provides` field to map a repository to a use
|[Jobs](../platform-functionality/jobs/index.md)|Jobs are a way for users to execute custom logic on demand from within the Nautobot UI. Jobs can interact directly with Nautobot data to accomplish various data creation, modification, and validation tasks.|
|[Config Contexts](../core-data-model/extras/configcontext.md)|Config contexts can be used to provide additional data that you can't natively store in Nautobot.|
|[Config Context Schemas](../core-data-model/extras/configcontextschema.md)|Schemas enforce data validation on config contexts.|
|[GraphQL queries](../feature-guides/graphql.md)|GraphQL reduces the complexity of performing multiple API calls|

### Examples of Apps Defining Additional Providers

Expand Down Expand Up @@ -275,6 +276,19 @@ config_context_schemas
├── schema_2.json
```

### GraphQL Queries

See also the [GraphQL feature guide](../feature-guides/graphql.md).

GraphQL queries can be used to reduce the complexity of performing multiple API calls while also correlating results by empowering the user to create their own query that provides the user exactly what they want and nothing that they don't, in a single API call.

```no-highlight
▶ tree graphql_queries
graphql_queries
├── query1.gql
├── query2.gql
```

## Additional Git Data Sources

As seen in [Fill out Repository Details](#fill-out-repository-details), the standard installation of Nautobot will come natively with export templates, jobs, and config contexts. Additional data sources can be incorporated using the Nautobot App system. For example, the [nautobot-golden-config](https://github.com/nautobot/nautobot-app-golden-config) App implements four additional data sources.
Expand Down
103 changes: 103 additions & 0 deletions nautobot/extras/datasources/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
DynamicGroup,
ExportTemplate,
GitRepository,
GraphQLQuery,
Job,
JobQueue,
JobResult,
Expand Down Expand Up @@ -935,6 +936,98 @@ def delete_git_export_templates(repository_record, job_result, preserve=None):
job_result.log(msg, level_choice=LogLevelChoices.LOG_WARNING, grouping="export templates")


#
# GraphQL handling
#


def refresh_git_graphql_queries(repository_record, job_result, delete=False):
"""Callback function for GitRepository updates - refresh all GraphQLQuery managed by this repository."""
if "extras.graphqlquery" in repository_record.provided_contents and not delete:
update_git_graphql_queries(repository_record, job_result)
else:
delete_git_graphql_queries(repository_record, job_result)


logger = logging.getLogger(__name__)


def update_git_graphql_queries(repository_record, job_result):
"""Refresh any GraphQL queries provided by this Git repository."""
graphql_query_path = os.path.join(repository_record.filesystem_path, "graphql_queries")
git_repository_content_type = ContentType.objects.get_for_model(GitRepository)
graphql_queries = []

if os.path.isdir(graphql_query_path):
for file in os.listdir(graphql_query_path):
file_path = os.path.join(graphql_query_path, file)
if not os.path.isfile(file_path):
continue

# Remove `.gql` extension from the name if it exists
query_name = file.rsplit(".gql", 1)[0] if file.endswith(".gql") else file

try:
with open(file_path, "r") as fd:
query_content = fd.read().strip()

graphql_query, created = GraphQLQuery.objects.get_or_create(
name=query_name,
owner_content_type=git_repository_content_type,
owner_object_id=repository_record.pk,
defaults={"query": query_content},
)
modified = False

# Check and update the query content
if graphql_query.query != query_content:
graphql_query.query = query_content
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
modified = True

if modified:
graphql_query.save()

graphql_queries.append(query_name)

# Log success
if created:
msg = f"Successfully created GraphQL query: {query_name}"
elif modified:
msg = f"Successfully refreshed GraphQL query: {query_name}"
else:
msg = f"No changes to GraphQL query: {query_name}"
logger.info(msg)
job_result.log(
msg, obj=graphql_query, level_choice=LogLevelChoices.LOG_INFO, grouping="graphql queries"
)

except Exception as exc:
# Log the error but continue processing other files
error_msg = f"Error processing GraphQL query file '{file}': {exc}"
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
logger.error(error_msg)
job_result.log(error_msg, level_choice=LogLevelChoices.LOG_ERROR, grouping="graphql queries")

# Delete any queries not in the preserved list
delete_git_graphql_queries(repository_record, job_result, preserve=graphql_queries)


def delete_git_graphql_queries(repository_record, job_result, preserve=None):
"""Delete GraphQL queries owned by the given Git repository that are not in the preserve list."""
git_repository_content_type = ContentType.objects.get_for_model(GitRepository)
if preserve is None:
preserve = []

for graphql_query in GraphQLQuery.objects.filter(
owner_content_type=git_repository_content_type,
owner_object_id=repository_record.pk,
):
if graphql_query.name not in preserve:
graphql_query.delete()
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
msg = f"Deleted GraphQL query: {graphql_query.name}"
logger.warning(msg)
job_result.log(msg, level_choice=LogLevelChoices.LOG_WARNING, grouping="graphql queries")


# Register built-in callbacks for data types potentially provided by a GitRepository
register_datasource_contents(
[
Expand Down Expand Up @@ -978,5 +1071,15 @@ def delete_git_export_templates(repository_record, job_result, preserve=None):
callback=refresh_git_export_templates,
),
),
(
"extras.gitrepository",
DatasourceContent(
name="graphql queries",
content_identifier="extras.graphqlquery",
icon="mdi-graphql",
weight=400,
callback=refresh_git_graphql_queries,
),
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.2.17 on 2025-01-08 16:59

from django.db import migrations, models
import django.db.models.deletion

import nautobot.extras.utils


class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("extras", "0121_alter_team_contacts"),
]

operations = [
migrations.AddField(
model_name="graphqlquery",
name="owner_content_type",
field=models.ForeignKey(
blank=True,
default=None,
limit_choices_to=nautobot.extras.utils.FeatureQuery("grapql_query_owners"),
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="graphql_queries",
to="contenttypes.contenttype",
),
),
migrations.AddField(
model_name="graphqlquery",
name="owner_object_id",
field=models.UUIDField(blank=True, default=None, null=True),
),
]
15 changes: 15 additions & 0 deletions nautobot/extras/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,21 @@ class GraphQLQuery(
SavedViewMixin,
BaseModel,
):
# A Graphql Query *may* be owned by another model, such as a GitRepository, or it may be un-owned
owner_content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
limit_choices_to=FeatureQuery("grapql_query_owners"),
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
default=None,
null=True,
blank=True,
related_name="graphql_queries",
)
owner_object_id = models.UUIDField(default=None, null=True, blank=True)
owner = GenericForeignKey(
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
ct_field="owner_content_type",
fk_field="owner_object_id",
)
name = models.CharField(max_length=CHARFIELD_MAX_LENGTH, unique=True)
query = models.TextField()
variables = models.JSONField(encoder=DjangoJSONEncoder, default=dict, blank=True)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query {
devices {
name
interfaces {
name
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
device {
name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
device {


}
27 changes: 27 additions & 0 deletions nautobot/extras/tests/test_datasources.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ConfigContextSchema,
ExportTemplate,
GitRepository,
GraphQLQuery,
Job,
JobButton,
JobHook,
Expand Down Expand Up @@ -197,6 +198,15 @@ def assert_export_template_vlan_exists(self, name):
)
self.assertIsNotNone(export_template_vlan)

def assert_graphql_query_exists(self, name="device_names.gql"):
"""Helper function to assert Graphql query exists."""
graphql_query = GraphQLQuery.objects.get(
name=name,
owner_object_id=self.repo.pk,
owner_content_type=ContentType.objects.get_for_model(GitRepository),
)
self.assertIsNotNone(graphql_query)

def assert_job_exists(self, name="MyJob", installed=True):
"""Helper function to assert JobModel and registered Job exist."""
# Is it registered correctly in the database?
Expand Down Expand Up @@ -348,6 +358,10 @@ def test_pull_git_repository_and_refresh_data_with_valid_data(self):
# Case when ContentType.model != ContentType.name, template was added and deleted during sync (#570)
self.assert_export_template_vlan_exists("template.j2")

# Make sure Graphgl queries were loaded
self.assert_graphql_query_exists("device_names")
self.assert_graphql_query_exists("device_interfaces")

# Make sure Jobs were successfully loaded from file and registered as JobModels
self.assert_job_exists(name="MyJob")
self.assert_job_exists(name="MyJobButtonReceiver")
Expand Down Expand Up @@ -439,6 +453,7 @@ def test_pull_git_repository_and_refresh_data_with_bad_data(self):
self.assertFalse(ConfigContextSchema.objects.filter(owner_object_id=self.repo.id).exists())
self.assertFalse(ConfigContext.objects.filter(owner_object_id=self.repo.id).exists())
self.assertFalse(ExportTemplate.objects.filter(owner_object_id=self.repo.id).exists())
self.assertFalse(GraphQLQuery.objects.filter(owner_object_id=self.repo.id).exists())
self.assertFalse(Job.objects.filter(module_name__startswith=f"{self.repo.slug}.").exists())
device = Device.objects.get(name=self.device.name)
self.assertIsNone(device.local_config_context_data)
Expand Down Expand Up @@ -505,6 +520,11 @@ def test_pull_git_repository_and_refresh_data_with_bad_data(self):
grouping="jobs",
message__contains="Error in loading Jobs from Git repository: ",
)
failure_logs.get(
grouping="graphql queries",
message__contains="Error processing GraphQL query file 'bad_device_names.gql': Syntax Error GraphQL (4:5) Expected Name, found }",
)

except (AssertionError, JobLogEntry.DoesNotExist):
for log in log_entries:
print(log.message)
Expand Down Expand Up @@ -630,6 +650,8 @@ def test_git_repository_sync_rollback(self):
self.assert_export_template_device("template.j2")
self.assert_export_template_html_exist("template2.html")
self.assert_export_template_vlan_exists("template.j2")
self.assert_graphql_query_exists(name="device_names")
self.assert_graphql_query_exists(name="device_interfaces")
self.assert_job_exists(name="MyJob")
self.assert_job_exists(name="MyJobButtonReceiver")
self.assert_job_exists(name="MyJobHookReceiver")
Expand Down Expand Up @@ -669,6 +691,8 @@ def test_git_repository_sync_rollback(self):
self.assert_export_template_device("template.j2")
self.assert_export_template_html_exist("template2.html")
self.assert_export_template_vlan_exists("template.j2")
self.assert_graphql_query_exists("device_names")
self.assert_graphql_query_exists("device_interfaces")
self.assert_job_exists(name="MyJob")
self.assert_job_exists(name="MyJobButtonReceiver")
self.assert_job_exists(name="MyJobHookReceiver")
Expand Down Expand Up @@ -711,6 +735,8 @@ def test_git_dry_run(self):
log_entries.get(message__contains="Addition - `export_templates/dcim/device/template.j2`")
log_entries.get(message__contains="Addition - `export_templates/dcim/device/template2.html`")
log_entries.get(message__contains="Addition - `export_templates/ipam/vlan/template.j2`")
log_entries.get(message__contains="Addition - `graphql_queries/device_interfaces.gql`")
log_entries.get(message__contains="Addition - `graphql_queries/device_names.gql`")
log_entries.get(message__contains="Addition - `jobs/__init__.py`")
log_entries.get(message__contains="Addition - `jobs/my_job.py`")
except JobLogEntry.DoesNotExist:
Expand All @@ -721,6 +747,7 @@ def test_git_dry_run(self):
self.assertFalse(ConfigContextSchema.objects.filter(owner_object_id=self.repo.pk).exists())
self.assertFalse(ConfigContext.objects.filter(owner_object_id=self.repo.pk).exists())
self.assertFalse(ExportTemplate.objects.filter(owner_object_id=self.repo.pk).exists())
self.assertFalse(GraphQLQuery.objects.filter(owner_object_id=self.repo.pk).exists())
self.assertFalse(Job.objects.filter(module_name__startswith=self.repo.slug).exists())

# TODO: test dry-run against a branch name
Expand Down
Loading








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/nautobot/nautobot/pull/6752/files/dfedd2722dc7b672ed2760b637eaafbbae85efb0

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy