infrahub-server 1.3.8__py3-none-any.whl → 1.4.0__py3-none-any.whl

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.
Files changed (172) hide show
  1. infrahub/api/internal.py +5 -0
  2. infrahub/artifacts/tasks.py +17 -22
  3. infrahub/branch/merge_mutation_checker.py +38 -0
  4. infrahub/cli/__init__.py +2 -2
  5. infrahub/cli/context.py +7 -3
  6. infrahub/cli/db.py +5 -16
  7. infrahub/cli/upgrade.py +10 -29
  8. infrahub/computed_attribute/tasks.py +36 -46
  9. infrahub/config.py +57 -6
  10. infrahub/constants/environment.py +1 -0
  11. infrahub/core/attribute.py +15 -7
  12. infrahub/core/branch/tasks.py +43 -41
  13. infrahub/core/constants/__init__.py +21 -6
  14. infrahub/core/constants/infrahubkind.py +2 -0
  15. infrahub/core/diff/coordinator.py +3 -1
  16. infrahub/core/diff/repository/repository.py +0 -8
  17. infrahub/core/diff/tasks.py +11 -8
  18. infrahub/core/graph/__init__.py +1 -1
  19. infrahub/core/graph/index.py +1 -2
  20. infrahub/core/graph/schema.py +50 -29
  21. infrahub/core/initialization.py +81 -47
  22. infrahub/core/ipam/tasks.py +4 -3
  23. infrahub/core/merge.py +8 -10
  24. infrahub/core/migrations/__init__.py +2 -0
  25. infrahub/core/migrations/graph/__init__.py +4 -0
  26. infrahub/core/migrations/graph/m036_drop_attr_value_index.py +45 -0
  27. infrahub/core/migrations/graph/m037_index_attr_vals.py +577 -0
  28. infrahub/core/migrations/query/attribute_add.py +27 -2
  29. infrahub/core/migrations/schema/attribute_kind_update.py +156 -0
  30. infrahub/core/migrations/schema/tasks.py +6 -5
  31. infrahub/core/models.py +5 -1
  32. infrahub/core/node/proposed_change.py +43 -0
  33. infrahub/core/protocols.py +12 -0
  34. infrahub/core/query/attribute.py +32 -14
  35. infrahub/core/query/diff.py +11 -0
  36. infrahub/core/query/ipam.py +13 -7
  37. infrahub/core/query/node.py +51 -10
  38. infrahub/core/query/resource_manager.py +3 -3
  39. infrahub/core/schema/basenode_schema.py +8 -0
  40. infrahub/core/schema/definitions/core/__init__.py +10 -1
  41. infrahub/core/schema/definitions/core/ipam.py +28 -2
  42. infrahub/core/schema/definitions/core/propose_change.py +15 -0
  43. infrahub/core/schema/definitions/core/webhook.py +3 -0
  44. infrahub/core/schema/definitions/internal.py +1 -1
  45. infrahub/core/schema/generated/attribute_schema.py +1 -1
  46. infrahub/core/schema/generic_schema.py +10 -0
  47. infrahub/core/schema/manager.py +10 -1
  48. infrahub/core/schema/node_schema.py +22 -17
  49. infrahub/core/schema/profile_schema.py +8 -0
  50. infrahub/core/schema/schema_branch.py +9 -5
  51. infrahub/core/schema/template_schema.py +8 -0
  52. infrahub/core/validators/attribute/kind.py +5 -1
  53. infrahub/core/validators/checks_runner.py +5 -5
  54. infrahub/core/validators/determiner.py +22 -2
  55. infrahub/core/validators/tasks.py +6 -7
  56. infrahub/core/validators/uniqueness/checker.py +4 -2
  57. infrahub/core/validators/uniqueness/model.py +1 -0
  58. infrahub/core/validators/uniqueness/query.py +57 -7
  59. infrahub/database/__init__.py +2 -1
  60. infrahub/events/__init__.py +20 -0
  61. infrahub/events/constants.py +7 -0
  62. infrahub/events/generator.py +29 -2
  63. infrahub/events/proposed_change_action.py +203 -0
  64. infrahub/generators/tasks.py +24 -20
  65. infrahub/git/base.py +4 -7
  66. infrahub/git/integrator.py +21 -12
  67. infrahub/git/repository.py +15 -30
  68. infrahub/git/tasks.py +121 -106
  69. infrahub/graphql/field_extractor.py +69 -0
  70. infrahub/graphql/manager.py +15 -11
  71. infrahub/graphql/mutations/account.py +2 -2
  72. infrahub/graphql/mutations/action.py +8 -2
  73. infrahub/graphql/mutations/artifact_definition.py +4 -1
  74. infrahub/graphql/mutations/branch.py +10 -5
  75. infrahub/graphql/mutations/graphql_query.py +2 -1
  76. infrahub/graphql/mutations/main.py +14 -8
  77. infrahub/graphql/mutations/menu.py +2 -1
  78. infrahub/graphql/mutations/proposed_change.py +230 -8
  79. infrahub/graphql/mutations/relationship.py +5 -0
  80. infrahub/graphql/mutations/repository.py +2 -1
  81. infrahub/graphql/mutations/tasks.py +7 -9
  82. infrahub/graphql/mutations/webhook.py +4 -1
  83. infrahub/graphql/parser.py +15 -6
  84. infrahub/graphql/queries/__init__.py +10 -1
  85. infrahub/graphql/queries/account.py +3 -3
  86. infrahub/graphql/queries/branch.py +2 -2
  87. infrahub/graphql/queries/diff/tree.py +3 -3
  88. infrahub/graphql/queries/event.py +13 -3
  89. infrahub/graphql/queries/ipam.py +23 -1
  90. infrahub/graphql/queries/proposed_change.py +84 -0
  91. infrahub/graphql/queries/relationship.py +2 -2
  92. infrahub/graphql/queries/resource_manager.py +3 -3
  93. infrahub/graphql/queries/search.py +3 -2
  94. infrahub/graphql/queries/status.py +3 -2
  95. infrahub/graphql/queries/task.py +2 -2
  96. infrahub/graphql/resolvers/ipam.py +440 -0
  97. infrahub/graphql/resolvers/many_relationship.py +4 -3
  98. infrahub/graphql/resolvers/resolver.py +5 -5
  99. infrahub/graphql/resolvers/single_relationship.py +3 -2
  100. infrahub/graphql/schema.py +25 -5
  101. infrahub/graphql/types/__init__.py +2 -2
  102. infrahub/graphql/types/attribute.py +3 -3
  103. infrahub/graphql/types/event.py +68 -0
  104. infrahub/groups/tasks.py +6 -6
  105. infrahub/lock.py +3 -2
  106. infrahub/menu/generator.py +8 -0
  107. infrahub/message_bus/operations/__init__.py +9 -12
  108. infrahub/message_bus/operations/git/file.py +6 -5
  109. infrahub/message_bus/operations/git/repository.py +12 -20
  110. infrahub/message_bus/operations/refresh/registry.py +15 -9
  111. infrahub/message_bus/operations/send/echo.py +7 -4
  112. infrahub/message_bus/types.py +1 -0
  113. infrahub/permissions/__init__.py +2 -1
  114. infrahub/permissions/constants.py +13 -0
  115. infrahub/permissions/globals.py +31 -2
  116. infrahub/permissions/manager.py +8 -5
  117. infrahub/pools/prefix.py +7 -5
  118. infrahub/prefect_server/app.py +31 -0
  119. infrahub/prefect_server/bootstrap.py +18 -0
  120. infrahub/proposed_change/action_checker.py +206 -0
  121. infrahub/proposed_change/approval_revoker.py +40 -0
  122. infrahub/proposed_change/branch_diff.py +3 -1
  123. infrahub/proposed_change/checker.py +45 -0
  124. infrahub/proposed_change/constants.py +32 -2
  125. infrahub/proposed_change/tasks.py +182 -150
  126. infrahub/py.typed +0 -0
  127. infrahub/server.py +29 -17
  128. infrahub/services/__init__.py +13 -28
  129. infrahub/services/adapters/cache/__init__.py +4 -0
  130. infrahub/services/adapters/cache/nats.py +2 -0
  131. infrahub/services/adapters/cache/redis.py +3 -0
  132. infrahub/services/adapters/message_bus/__init__.py +0 -2
  133. infrahub/services/adapters/message_bus/local.py +1 -2
  134. infrahub/services/adapters/message_bus/nats.py +6 -8
  135. infrahub/services/adapters/message_bus/rabbitmq.py +7 -9
  136. infrahub/services/adapters/workflow/__init__.py +1 -0
  137. infrahub/services/adapters/workflow/local.py +1 -8
  138. infrahub/services/component.py +2 -1
  139. infrahub/task_manager/event.py +56 -0
  140. infrahub/task_manager/models.py +9 -0
  141. infrahub/tasks/artifact.py +6 -7
  142. infrahub/tasks/check.py +4 -7
  143. infrahub/telemetry/tasks.py +15 -18
  144. infrahub/transformations/tasks.py +10 -6
  145. infrahub/trigger/tasks.py +4 -3
  146. infrahub/types.py +4 -0
  147. infrahub/validators/events.py +7 -7
  148. infrahub/validators/tasks.py +6 -7
  149. infrahub/webhook/models.py +45 -45
  150. infrahub/webhook/tasks.py +25 -24
  151. infrahub/workers/dependencies.py +143 -0
  152. infrahub/workers/infrahub_async.py +19 -43
  153. infrahub/workflows/catalogue.py +16 -2
  154. infrahub/workflows/initialization.py +5 -4
  155. infrahub/workflows/models.py +2 -0
  156. infrahub_sdk/client.py +2 -2
  157. infrahub_sdk/ctl/repository.py +51 -0
  158. infrahub_sdk/ctl/schema.py +9 -9
  159. infrahub_sdk/node/node.py +2 -2
  160. infrahub_sdk/pytest_plugin/items/graphql_query.py +1 -1
  161. infrahub_sdk/schema/repository.py +1 -1
  162. infrahub_sdk/testing/docker.py +1 -1
  163. infrahub_sdk/utils.py +2 -2
  164. {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/METADATA +7 -5
  165. {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/RECORD +172 -156
  166. infrahub_testcontainers/container.py +17 -0
  167. infrahub_testcontainers/docker-compose-cluster.test.yml +56 -1
  168. infrahub_testcontainers/docker-compose.test.yml +56 -1
  169. infrahub_testcontainers/helpers.py +4 -1
  170. {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/LICENSE.txt +0 -0
  171. {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/WHEEL +0 -0
  172. {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
+
5
+ from infrahub.types import is_large_attribute_type
6
+
7
+ from ..query import AttributeMigrationQuery
8
+ from ..shared import AttributeSchemaMigration, MigrationResult
9
+
10
+ if TYPE_CHECKING:
11
+ from infrahub.core.branch.models import Branch
12
+ from infrahub.core.timestamp import Timestamp
13
+ from infrahub.database import InfrahubDatabase
14
+
15
+
16
+ class AttributeKindUpdateMigrationQuery(AttributeMigrationQuery):
17
+ name = "migration_attribute_kind"
18
+ insert_return = False
19
+
20
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
21
+ branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
22
+ self.params.update(branch_params)
23
+ needs_index = not is_large_attribute_type(self.migration.new_attribute_schema.kind)
24
+ self.params["needs_index"] = needs_index
25
+ self.params["branch"] = self.branch.name
26
+ self.params["branch_level"] = self.branch.hierarchy_level
27
+ self.params["at"] = self.at.to_string()
28
+ self.params["attr_name"] = self.migration.previous_attribute_schema.name
29
+ new_attr_value_labels = "AttributeValue"
30
+ if needs_index:
31
+ new_attr_value_labels += ":AttributeValueIndexed"
32
+ # ruff: noqa: S608
33
+ query = """
34
+ // ------------
35
+ // start with all the Attribute vertices we might care about
36
+ // ------------
37
+ MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
38
+ WHERE attr.name = $attr_name
39
+ WITH DISTINCT n, attr
40
+
41
+ // ------------
42
+ // for each Attribute, find the most recent active edge and AttributeValue vertex that needs to be [un]indexed
43
+ // ------------
44
+ CALL (n, attr) {
45
+ MATCH (n)-[r1:HAS_ATTRIBUTE]->(attr:Attribute)-[r2:HAS_VALUE]->(av)
46
+ WHERE all(r IN [r1, r2] WHERE %(branch_filter)s)
47
+ WITH r2, av, r1.status = "active" AND r2.status = "active" AS is_active
48
+ ORDER BY r2.branch_level DESC, r2.from DESC, r2.status = "active" DESC, r1.branch_level DESC, r1.from DESC, r1.status = "active" DESC
49
+ LIMIT 1
50
+ WITH r2 AS has_value_e, av, "AttributeValueIndexed" IN labels(av) AS is_indexed
51
+ WHERE is_active AND is_indexed <> $needs_index
52
+ RETURN has_value_e, av
53
+ }
54
+
55
+ // ------------
56
+ // check if the correct AttributeValue vertex to use exists
57
+ // create it if not
58
+ // ------------
59
+ WITH DISTINCT av.is_default AS av_is_default, av.value AS av_value
60
+ CALL (av_is_default, av_value) {
61
+ OPTIONAL MATCH (existing_av:AttributeValue {is_default: av_is_default, value: av_value})
62
+ WHERE "AttributeValueIndexed" IN labels(existing_av) = $needs_index
63
+ WITH existing_av WHERE existing_av IS NULL
64
+ LIMIT 1
65
+ CREATE (:%(new_attr_value_labels)s {is_default: av_is_default, value: av_value})
66
+ }
67
+
68
+ // ------------
69
+ // get all the AttributeValue vertices that need to be updated again and run the updates
70
+ // ------------
71
+ WITH 1 AS one
72
+ LIMIT 1
73
+ MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
74
+ WHERE attr.name = $attr_name
75
+ WITH DISTINCT n, attr
76
+
77
+ // ------------
78
+ // for each Attribute, find the most recent active edge and AttributeValue vertex that needs to be [un]indexed
79
+ // ------------
80
+ CALL (n, attr) {
81
+ MATCH (n)-[r1:HAS_ATTRIBUTE]->(attr:Attribute)-[r2:HAS_VALUE]->(av)
82
+ WHERE all(r IN [r1, r2] WHERE %(branch_filter)s)
83
+ WITH r2, av, r1.status = "active" AND r2.status = "active" AS is_active
84
+ ORDER BY r2.branch_level DESC, r2.from DESC, r2.status = "active" DESC, r1.branch_level DESC, r1.from DESC, r1.status = "active" DESC
85
+ LIMIT 1
86
+ WITH r2 AS has_value_e, av, "AttributeValueIndexed" IN labels(av) AS is_indexed
87
+ WHERE is_active AND is_indexed <> $needs_index
88
+ RETURN has_value_e, av
89
+ }
90
+
91
+
92
+ // ------------
93
+ // create and update the HAS_VALUE edges
94
+ // ------------
95
+ CALL (attr, has_value_e, av) {
96
+ // ------------
97
+ // get the correct AttributeValue vertex b/c it definitely exists now
98
+ // ------------
99
+ MATCH (new_av:%(new_attr_value_labels)s {is_default: av.is_default, value: av.value})
100
+ WHERE "AttributeValueIndexed" IN labels(new_av) = $needs_index
101
+ LIMIT 1
102
+
103
+ // ------------
104
+ // create the new HAS_VALUE edge
105
+ // ------------
106
+ CREATE (attr)-[new_has_value_e:HAS_VALUE]->(new_av)
107
+ SET new_has_value_e = properties(has_value_e)
108
+ SET new_has_value_e.status = "active"
109
+ SET new_has_value_e.branch = $branch
110
+ SET new_has_value_e.branch_level = $branch_level
111
+ SET new_has_value_e.from = $at
112
+ SET new_has_value_e.to = NULL
113
+
114
+ // ------------
115
+ // if we are updating on a branch and the existing edge is on the default branch,
116
+ // then create a new deleted edge on this branch
117
+ // ------------
118
+ WITH attr, has_value_e, av
119
+ WHERE has_value_e.branch <> $branch
120
+ CREATE (attr)-[deleted_has_value_e:HAS_VALUE]->(av)
121
+ SET deleted_has_value_e = properties(has_value_e)
122
+ SET deleted_has_value_e.status = "deleted"
123
+ SET deleted_has_value_e.branch = $branch
124
+ SET deleted_has_value_e.branch_level = $branch_level
125
+ SET deleted_has_value_e.from = $at
126
+ SET deleted_has_value_e.to = NULL
127
+ }
128
+
129
+ // ------------
130
+ // if the existing edge is on the same branch as the update,
131
+ // then set its "to" time
132
+ // ------------
133
+ CALL (has_value_e) {
134
+ WITH has_value_e
135
+ WHERE has_value_e.branch = $branch
136
+ SET has_value_e.to = $at
137
+ }
138
+ """ % {
139
+ "schema_kind": self.migration.previous_schema.kind,
140
+ "branch_filter": branch_filter,
141
+ "new_attr_value_labels": new_attr_value_labels,
142
+ }
143
+ self.add_to_query(query)
144
+
145
+
146
+ class AttributeKindUpdateMigration(AttributeSchemaMigration):
147
+ name: str = "attribute.kind.update"
148
+ queries: Sequence[type[AttributeMigrationQuery]] = [AttributeKindUpdateMigrationQuery] # type: ignore[assignment]
149
+
150
+ async def execute(self, db: InfrahubDatabase, branch: Branch, at: Timestamp | str | None = None) -> MigrationResult:
151
+ is_indexed_previous = is_large_attribute_type(self.previous_attribute_schema.kind)
152
+ is_indexed_new = is_large_attribute_type(self.new_attribute_schema.kind)
153
+ if is_indexed_previous is is_indexed_new:
154
+ return MigrationResult()
155
+
156
+ return await super().execute(db=db, branch=branch, at=at)
@@ -10,17 +10,18 @@ from prefect.logging import get_run_logger
10
10
  from infrahub.core.branch import Branch # noqa: TC001
11
11
  from infrahub.core.migrations import MIGRATION_MAP
12
12
  from infrahub.core.path import SchemaPath # noqa: TC001
13
- from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
13
+ from infrahub.workers.dependencies import get_database
14
14
  from infrahub.workflows.utils import add_branch_tag
15
15
 
16
16
  from .models import SchemaApplyMigrationData, SchemaMigrationPathResponseData
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from infrahub.core.schema import MainSchemaTypes
20
+ from infrahub.database import InfrahubDatabase
20
21
 
21
22
 
22
23
  @flow(name="schema_apply_migrations", flow_run_name="Apply schema migrations", persist_result=True)
23
- async def schema_apply_migrations(message: SchemaApplyMigrationData, service: InfrahubServices) -> list[str]:
24
+ async def schema_apply_migrations(message: SchemaApplyMigrationData) -> list[str]:
24
25
  await add_branch_tag(branch_name=message.branch.name)
25
26
  log = get_run_logger()
26
27
 
@@ -55,7 +56,7 @@ async def schema_apply_migrations(message: SchemaApplyMigrationData, service: In
55
56
  new_node_schema=new_node_schema,
56
57
  previous_node_schema=previous_node_schema,
57
58
  schema_path=migration.path,
58
- service=service,
59
+ database=await get_database(),
59
60
  )
60
61
 
61
62
  async for _, result in batch.execute():
@@ -75,13 +76,13 @@ async def schema_path_migrate(
75
76
  branch: Branch,
76
77
  migration_name: str,
77
78
  schema_path: SchemaPath,
78
- service: InfrahubServices,
79
+ database: InfrahubDatabase,
79
80
  new_node_schema: MainSchemaTypes | None = None,
80
81
  previous_node_schema: MainSchemaTypes | None = None,
81
82
  ) -> SchemaMigrationPathResponseData:
82
83
  log = get_run_logger()
83
84
 
84
- async with service.database.start_session() as db:
85
+ async with database.start_session() as db:
85
86
  node_kind = None
86
87
  if new_node_schema:
87
88
  node_kind = new_node_schema.kind
infrahub/core/models.py CHANGED
@@ -569,7 +569,11 @@ class HashableModel(BaseModel):
569
569
 
570
570
  for field_name in other.model_fields.keys():
571
571
  if not hasattr(self, field_name):
572
- setattr(self, field_name, getattr(other, field_name))
572
+ try:
573
+ setattr(self, field_name, getattr(other, field_name))
574
+ except ValueError:
575
+ # handles the case where self and other are different types and other has fields that self does not
576
+ pass
573
577
  continue
574
578
 
575
579
  attr_other = getattr(other, field_name)
@@ -0,0 +1,43 @@
1
+ from typing import cast
2
+
3
+ from infrahub.core.constants.infrahubkind import THREADCOMMENT
4
+ from infrahub.core.manager import NodeManager
5
+ from infrahub.core.node import Node
6
+ from infrahub.core.protocols import CoreProposedChange as CoreProposedChangeProtocol
7
+ from infrahub.database import InfrahubDatabase
8
+
9
+
10
+ class CoreProposedChange(Node):
11
+ async def to_graphql(
12
+ self,
13
+ db: InfrahubDatabase,
14
+ fields: dict | None = None,
15
+ related_node_ids: set | None = None,
16
+ filter_sensitive: bool = False,
17
+ permissions: dict | None = None,
18
+ include_properties: bool = True,
19
+ ) -> dict:
20
+ response = await super().to_graphql(
21
+ db,
22
+ fields=fields,
23
+ related_node_ids=related_node_ids,
24
+ filter_sensitive=filter_sensitive,
25
+ permissions=permissions,
26
+ include_properties=include_properties,
27
+ )
28
+
29
+ if fields:
30
+ if "total_comments" in fields:
31
+ total_comments = 0
32
+ proposed_change = cast(CoreProposedChangeProtocol, self)
33
+ change_comments = await proposed_change.comments.get_relationships(db=db)
34
+ total_comments += len(change_comments)
35
+
36
+ threads = await proposed_change.threads.get_peers(db=db)
37
+ thread_comments = await NodeManager.query(
38
+ db=db, schema=THREADCOMMENT, filters={"thread__ids": list(threads.keys())}
39
+ )
40
+ total_comments += len(thread_comments)
41
+ response["total_comments"] = {"value": total_comments}
42
+
43
+ return response
@@ -319,6 +319,7 @@ class CoreCheckDefinition(CoreTaskTarget):
319
319
 
320
320
 
321
321
  class CoreCustomWebhook(CoreWebhook, CoreTaskTarget):
322
+ shared_key: StringOptional
322
323
  transformation: RelationshipManager
323
324
 
324
325
 
@@ -480,7 +481,10 @@ class CoreProposedChange(CoreTaskTarget):
480
481
  source_branch: String
481
482
  destination_branch: String
482
483
  state: Enum
484
+ is_draft: Boolean
485
+ total_comments: IntegerOptional
483
486
  approved_by: RelationshipManager
487
+ rejected_by: RelationshipManager
484
488
  reviewers: RelationshipManager
485
489
  created_by: RelationshipManager
486
490
  comments: RelationshipManager
@@ -554,6 +558,14 @@ class InternalAccountToken(CoreNode):
554
558
  account: RelationshipManager
555
559
 
556
560
 
561
+ class InternalIPPrefixAvailable(BuiltinIPPrefix):
562
+ pass
563
+
564
+
565
+ class InternalIPRangeAvailable(BuiltinIPAddress):
566
+ last_address: IPHost
567
+
568
+
557
569
  class InternalRefreshToken(CoreNode):
558
570
  expiration: DateTime
559
571
  account: RelationshipManager
@@ -5,9 +5,16 @@ from typing import TYPE_CHECKING, Any
5
5
  from infrahub.core.constants import AttributeDBNodeType
6
6
  from infrahub.core.constants.relationship_label import RELATIONSHIP_TO_NODE_LABEL, RELATIONSHIP_TO_VALUE_LABEL
7
7
  from infrahub.core.constants.schema import FlagProperty, NodeProperty
8
+ from infrahub.core.graph.schema import (
9
+ GraphAttributeIPHostNode,
10
+ GraphAttributeIPNetworkNode,
11
+ GraphAttributeValueIndexedNode,
12
+ GraphAttributeValueNode,
13
+ )
8
14
  from infrahub.core.query import Query, QueryNode, QueryRel, QueryType
9
15
  from infrahub.core.timestamp import Timestamp
10
16
  from infrahub.core.utils import build_regex_attrs
17
+ from infrahub.types import is_large_attribute_type
11
18
 
12
19
  if TYPE_CHECKING:
13
20
  from infrahub.core.attribute import BaseAttribute
@@ -56,12 +63,14 @@ class AttributeUpdateValueQuery(AttributeQuery):
56
63
 
57
64
  prop_list = [f"{key}: ${key}" for key in content.keys()]
58
65
 
59
- labels = ["AttributeValue"]
66
+ labels = [GraphAttributeValueNode.get_default_label()]
60
67
  node_type = self.attr.get_db_node_type()
61
- if node_type == AttributeDBNodeType.IPHOST:
62
- labels.append("AttributeIPHost")
63
- elif node_type == AttributeDBNodeType.IPNETWORK:
64
- labels.append("AttributeIPNetwork")
68
+ if AttributeDBNodeType.INDEXED in node_type:
69
+ labels.append(GraphAttributeValueIndexedNode.get_default_label())
70
+ if AttributeDBNodeType.IPHOST in node_type:
71
+ labels.append(GraphAttributeIPHostNode.get_default_label())
72
+ if AttributeDBNodeType.IPNETWORK in node_type:
73
+ labels.append(GraphAttributeIPNetworkNode.get_default_label())
65
74
 
66
75
  query = """
67
76
  MATCH (a:Attribute { uuid: $attr_uuid })
@@ -198,6 +207,9 @@ async def default_attribute_query_filter(
198
207
  support_profiles: bool = False,
199
208
  ) -> tuple[list[QueryElement], dict[str, Any], list[str]]:
200
209
  """Generate Query String Snippet to filter the right node."""
210
+ attribute_value_label = GraphAttributeValueNode.get_default_label()
211
+ if attribute_kind and not is_large_attribute_type(attribute_kind):
212
+ attribute_value_label = GraphAttributeValueIndexedNode.get_default_label()
201
213
 
202
214
  query_filter: list[QueryElement] = []
203
215
  query_params: dict[str, Any] = {}
@@ -226,33 +238,35 @@ async def default_attribute_query_filter(
226
238
  query_filter.append(QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]))
227
239
 
228
240
  if filter_value is None:
229
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
241
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
230
242
  else:
231
243
  if partial_match:
232
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
244
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
233
245
  query_where.append(
234
246
  f"toLower(toString(av.{filter_name})) CONTAINS toLower(toString(${param_prefix}_{filter_name}))"
235
247
  )
236
248
  elif attribute_kind and attribute_kind == "List" and not isinstance(filter_value, list):
237
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
249
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
238
250
  filter_value = build_regex_attrs(values=[filter_value])
239
251
  query_where.append(f"toString(av.{filter_name}) =~ ${param_prefix}_{filter_name}")
240
252
  elif filter_name == "isnull":
241
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
253
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
242
254
  elif support_profiles:
243
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
255
+ query_filter.append(QueryNode(name="av", labels=[attribute_value_label]))
244
256
  query_where.append(f"(av.{filter_name} = ${param_prefix}_{filter_name} OR av.is_default)")
245
257
  else:
246
258
  query_filter.append(
247
259
  QueryNode(
248
- name="av", labels=["AttributeValue"], params={filter_name: f"${param_prefix}_{filter_name}"}
260
+ name="av",
261
+ labels=[attribute_value_label],
262
+ params={filter_name: f"${param_prefix}_{filter_name}"},
249
263
  )
250
264
  )
251
265
  query_params[f"{param_prefix}_{filter_name}"] = filter_value
252
266
 
253
267
  elif filter_name == "values" and isinstance(filter_value, list):
254
268
  query_filter.extend(
255
- (QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]), QueryNode(name="av", labels=["AttributeValue"]))
269
+ (QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]), QueryNode(name="av", labels=[attribute_value_label]))
256
270
  )
257
271
  if attribute_kind and attribute_kind == "List":
258
272
  query_params[f"{param_prefix}_{filter_name}"] = build_regex_attrs(values=filter_value)
@@ -267,10 +281,14 @@ async def default_attribute_query_filter(
267
281
  query_filter.append(QueryRel(labels=[RELATIONSHIP_TO_VALUE_LABEL]))
268
282
 
269
283
  if filter_value is None:
270
- query_filter.append(QueryNode(name="av", labels=["AttributeValue"]))
284
+ query_filter.append(QueryNode(name="av", labels=[GraphAttributeValueNode.get_default_label()]))
271
285
  else:
272
286
  query_filter.append(
273
- QueryNode(name="av", labels=["AttributeValue"], params={filter_name: f"${param_prefix}_{filter_name}"})
287
+ QueryNode(
288
+ name="av",
289
+ labels=[GraphAttributeValueNode.get_default_label()],
290
+ params={filter_name: f"${param_prefix}_{filter_name}"},
291
+ )
274
292
  )
275
293
  query_params[f"{param_prefix}_{filter_name}"] = filter_value
276
294
 
@@ -100,6 +100,17 @@ class DiffCountChanges(Query):
100
100
  return branch_count_map
101
101
 
102
102
 
103
+ async def get_num_changes_in_time_range_by_branch(
104
+ branch_names: list[str],
105
+ from_time: Timestamp,
106
+ to_time: Timestamp,
107
+ db: InfrahubDatabase,
108
+ ) -> dict[str, int]:
109
+ query = await DiffCountChanges.init(db=db, branch_names=branch_names, diff_from=from_time, diff_to=to_time)
110
+ await query.execute(db=db)
111
+ return query.get_num_changes_by_branch()
112
+
113
+
103
114
  class DiffCalculationQuery(DiffQuery):
104
115
  type = QueryType.READ
105
116
  insert_limit = False
@@ -5,6 +5,7 @@ from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, Iterable
6
6
 
7
7
  from infrahub.core.constants import InfrahubKind
8
+ from infrahub.core.graph.schema import GraphAttributeIPHostNode, GraphAttributeIPNetworkNode
8
9
  from infrahub.core.ipam.constants import AllIPTypes, IPAddressType, IPNetworkType
9
10
  from infrahub.core.query import QueryType
10
11
  from infrahub.core.registry import registry
@@ -21,8 +22,8 @@ if TYPE_CHECKING:
21
22
  from infrahub.database import InfrahubDatabase
22
23
 
23
24
 
24
- PREFIX_ATTRIBUTE_LABEL = "AttributeIPNetwork"
25
- ADDRESS_ATTRIBUTE_LABEL = "AttributeIPHost"
25
+ PREFIX_ATTRIBUTE_LABEL = GraphAttributeIPNetworkNode.get_default_label()
26
+ ADDRESS_ATTRIBUTE_LABEL = GraphAttributeIPHostNode.get_default_label()
26
27
 
27
28
 
28
29
  @dataclass
@@ -280,14 +281,13 @@ class IPPrefixUtilization(Query):
280
281
  MATCH path = (
281
282
  (pfx)-[r_1:IS_RELATED]-(rl:Relationship)-[r_2:IS_RELATED]-(child:Node)
282
283
  -[r_attr:HAS_ATTRIBUTE]->(attr:Attribute)
283
- -[r_attr_val:HAS_VALUE]->(av:AttributeValue)
284
+ -[r_attr_val:HAS_VALUE]->(av:{PREFIX_ATTRIBUTE_LABEL}|{ADDRESS_ATTRIBUTE_LABEL})
284
285
  )
285
286
  WHERE %(id_func)s(r_1) = %(id_func)s(r_rel1)
286
287
  AND %(id_func)s(r_2) = %(id_func)s(r_rel2)
287
288
  AND ({rel_filter("r_attr")})
288
289
  AND ({rel_filter("r_attr_val")})
289
290
  AND attr.name IN ["prefix", "address"]
290
- AND any(l in labels(av) WHERE l in ["{PREFIX_ATTRIBUTE_LABEL}", "{ADDRESS_ATTRIBUTE_LABEL}"])
291
291
  WITH
292
292
  path,
293
293
  pfx,
@@ -478,6 +478,7 @@ class IPPrefixReconcileQuery(Query):
478
478
  -[pr2:IS_RELATED {status: "active"}]-(maybe_new_parent:%(ip_prefix_kind)s)
479
479
  -[har:HAS_ATTRIBUTE]->(:Attribute {name: "prefix"})
480
480
  -[hvr:HAS_VALUE]->(av:%(ip_prefix_attribute_kind)s)
481
+ USING INDEX av:%(ip_prefix_attribute_kind)s(binary_address)
481
482
  WHERE all(r IN relationships(parent_path) WHERE (%(branch_filter)s))
482
483
  AND av.version = $ip_version
483
484
  AND av.binary_address IN $possible_prefix_list
@@ -514,15 +515,16 @@ class IPPrefixReconcileQuery(Query):
514
515
  OPTIONAL MATCH child_path = (
515
516
  (ip_namespace)-[r1:IS_RELATED]
516
517
  -(ns_rel:Relationship)-[r2:IS_RELATED]
517
- -(maybe_new_child:Node)-[har:HAS_ATTRIBUTE]
518
+ -(maybe_new_child:%(ip_prefix_kind)s|%(ip_address_kind)s)-[har:HAS_ATTRIBUTE]
518
519
  ->(a:Attribute)-[hvr:HAS_VALUE]
519
- ->(av:AttributeValue)
520
+ ->(av:%(ip_prefix_attribute_kind)s|%(ip_address_attribute_kind)s)
520
521
  )
522
+ USING INDEX av:%(ip_prefix_attribute_kind)s(binary_address)
523
+ USING INDEX av:%(ip_address_attribute_kind)s(binary_address)
521
524
  WHERE $is_prefix // only prefix nodes can have children
522
525
  AND ns_rel.name IN ["ip_namespace__ip_prefix", "ip_namespace__ip_address"]
523
526
  AND any(child_kind IN [$ip_prefix_kind, $ip_address_kind] WHERE child_kind IN labels(maybe_new_child))
524
527
  AND a.name in ["prefix", "address"]
525
- AND any(attr_kind IN [$ip_prefix_attribute_kind, $ip_address_attribute_kind] WHERE attr_kind IN labels(av))
526
528
  AND (ip_node IS NULL OR maybe_new_child.uuid <> ip_node.uuid)
527
529
  AND (
528
530
  ($ip_prefix_kind IN labels(maybe_new_child) AND av.prefixlen > $prefixlen)
@@ -580,6 +582,10 @@ class IPPrefixReconcileQuery(Query):
580
582
  collect(new_child) as new_children
581
583
  """ % {
582
584
  "branch_filter": branch_filter,
585
+ "ip_prefix_kind": InfrahubKind.IPPREFIX,
586
+ "ip_address_kind": InfrahubKind.IPADDRESS,
587
+ "ip_prefix_attribute_kind": PREFIX_ATTRIBUTE_LABEL,
588
+ "ip_address_attribute_kind": ADDRESS_ATTRIBUTE_LABEL,
583
589
  }
584
590
  self.add_to_query(get_new_children_query)
585
591
  self.return_labels = ["ip_node", "current_parent", "current_children", "new_parent", "new_children"]
@@ -140,15 +140,19 @@ class NodeCreateAllQuery(NodeQuery):
140
140
  attributes: list[AttributeCreateData] = []
141
141
  attributes_iphost: list[AttributeCreateData] = []
142
142
  attributes_ipnetwork: list[AttributeCreateData] = []
143
+ attributes_indexed: list[AttributeCreateData] = []
143
144
 
144
145
  for attr_name in self.node._attributes:
145
146
  attr: BaseAttribute = getattr(self.node, attr_name)
146
147
  attr_data = attr.get_create_data()
148
+ node_type = attr.get_db_node_type()
147
149
 
148
- if attr_data.node_type == AttributeDBNodeType.IPHOST:
150
+ if AttributeDBNodeType.IPHOST in node_type:
149
151
  attributes_iphost.append(attr_data)
150
- elif attr_data.node_type == AttributeDBNodeType.IPNETWORK:
152
+ elif AttributeDBNodeType.IPNETWORK in node_type:
151
153
  attributes_ipnetwork.append(attr_data)
154
+ elif AttributeDBNodeType.INDEXED in node_type:
155
+ attributes_indexed.append(attr_data)
152
156
  else:
153
157
  attributes.append(attr_data)
154
158
 
@@ -182,6 +186,7 @@ class NodeCreateAllQuery(NodeQuery):
182
186
  relationships.append(rel_create_data)
183
187
 
184
188
  self.params["attrs"] = [attr.model_dump() for attr in attributes]
189
+ self.params["attrs_indexed"] = [attr.model_dump() for attr in attributes_indexed]
185
190
  self.params["attrs_iphost"] = [attr.model_dump() for attr in attributes_iphost]
186
191
  self.params["attrs_ipnetwork"] = [attr.model_dump() for attr in attributes_ipnetwork]
187
192
  self.params["rels_bidir"] = [
@@ -224,24 +229,59 @@ class NodeCreateAllQuery(NodeQuery):
224
229
  "binary_address": "attr.content.binary_address",
225
230
  "version": "attr.content.version",
226
231
  "prefixlen": "attr.content.prefixlen",
227
- # "num_addresses": "attr.content.num_addresses",
228
232
  }
229
233
  ipnetwork_prop_list = [f"{key}: {value}" for key, value in ipnetwork_prop.items()]
230
234
 
231
- attrs_query = """
235
+ attrs_nonindexed_query = """
232
236
  WITH distinct n
233
237
  UNWIND $attrs AS attr
238
+ // Try to find a matching vertex
239
+ OPTIONAL MATCH (existing_av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
240
+ WHERE NOT existing_av:AttributeValueIndexed
241
+ CALL (attr, existing_av) {
242
+ // If none found, create a new one
243
+ WITH existing_av
244
+ WHERE existing_av IS NULL
245
+ CREATE (:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
246
+ }
247
+ CALL (attr) {
248
+ MATCH (av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
249
+ WHERE NOT av:AttributeValueIndexed
250
+ RETURN av
251
+ LIMIT 1
252
+ }
253
+ CALL (n, attr, av) {
254
+ CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
255
+ CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
256
+ CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
257
+ MERGE (ip:Boolean { value: attr.is_protected })
258
+ MERGE (iv:Boolean { value: attr.is_visible })
259
+ WITH a, ip, iv
260
+ LIMIT 1
261
+ CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
262
+ CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
263
+ FOREACH ( prop IN attr.source_prop |
264
+ MERGE (peer:Node { uuid: prop.peer_id })
265
+ CREATE (a)-[:HAS_SOURCE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
266
+ )
267
+ FOREACH ( prop IN attr.owner_prop |
268
+ MERGE (peer:Node { uuid: prop.peer_id })
269
+ CREATE (a)-[:HAS_OWNER { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(peer)
270
+ )
271
+ }"""
272
+
273
+ attrs_indexed_query = """
274
+ WITH distinct n
275
+ UNWIND $attrs_indexed AS attr
234
276
  CALL (n, attr) {
235
277
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
236
278
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
237
- MERGE (av:AttributeValue { value: attr.content.value, is_default: attr.content.is_default })
279
+ MERGE (av:AttributeValue:AttributeValueIndexed { value: attr.content.value, is_default: attr.content.is_default })
238
280
  WITH av, a
239
281
  LIMIT 1
240
282
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
241
283
  MERGE (ip:Boolean { value: attr.is_protected })
242
284
  MERGE (iv:Boolean { value: attr.is_visible })
243
- WITH a, ip, iv
244
- LIMIT 1
245
285
  CREATE (a)-[:IS_PROTECTED { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(ip)
246
286
  CREATE (a)-[:IS_VISIBLE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(iv)
247
287
  FOREACH ( prop IN attr.source_prop |
@@ -260,7 +300,7 @@ class NodeCreateAllQuery(NodeQuery):
260
300
  CALL (n, attr) {
261
301
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
262
302
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
263
- MERGE (av:AttributeValue:AttributeIPHost { %(iphost_prop)s })
303
+ MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPHost { %(iphost_prop)s })
264
304
  WITH attr, av, a
265
305
  LIMIT 1
266
306
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
@@ -287,7 +327,7 @@ class NodeCreateAllQuery(NodeQuery):
287
327
  CALL (n, attr) {
288
328
  CREATE (a:Attribute { uuid: attr.uuid, name: attr.name, branch_support: attr.branch_support })
289
329
  CREATE (n)-[:HAS_ATTRIBUTE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(a)
290
- MERGE (av:AttributeValue:AttributeIPNetwork { %(ipnetwork_prop)s })
330
+ MERGE (av:AttributeValue:AttributeValueIndexed:AttributeIPNetwork { %(ipnetwork_prop)s })
291
331
  WITH attr, av, a
292
332
  LIMIT 1
293
333
  CREATE (a)-[:HAS_VALUE { branch: attr.branch, branch_level: attr.branch_level, status: attr.status, from: $at }]->(av)
@@ -424,7 +464,8 @@ class NodeCreateAllQuery(NodeQuery):
424
464
  MATCH (root:Root)
425
465
  CREATE (n:Node:%(labels)s $node_prop )
426
466
  CREATE (n)-[r:IS_PART_OF $node_branch_prop ]->(root)
427
- {attrs_query if self.params["attrs"] else ""}
467
+ {attrs_nonindexed_query if self.params["attrs"] else ""}
468
+ {attrs_indexed_query if self.params["attrs_indexed"] else ""}
428
469
  {attrs_iphost_query if self.params["attrs_iphost"] else ""}
429
470
  {attrs_ipnetwork_query if self.params["attrs_ipnetwork"] else ""}
430
471
  {rels_bidir_query if self.params["rels_bidir"] else ""}
@@ -140,7 +140,7 @@ class NumberPoolGetAllocated(Query):
140
140
  self.params.update(branch_params)
141
141
 
142
142
  query = """
143
- MATCH (n:%(node)s)-[ha:HAS_ATTRIBUTE]-(a:Attribute {name: $node_attribute})-[hv:HAS_VALUE]-(av:AttributeValue)
143
+ MATCH (n:%(node)s)-[ha:HAS_ATTRIBUTE]-(a:Attribute {name: $node_attribute})-[hv:HAS_VALUE]-(av:AttributeValueIndexed)
144
144
  MATCH (a)-[hs:HAS_SOURCE]-(pool:%(number_pool_kind)s)
145
145
  WHERE
146
146
  pool.uuid = $pool_id
@@ -306,7 +306,7 @@ class NumberPoolGetUsed(Query):
306
306
  self.params["attribute_name"] = self.pool.node_attribute.value
307
307
 
308
308
  query = """
309
- MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValue)
309
+ MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValueIndexed)
310
310
  WHERE toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
311
311
  CALL (pool, res, av) {
312
312
  MATCH (pool)-[res]->(av)<-[hv:HAS_VALUE]-(attr:Attribute)<-[ha:HAS_ATTRIBUTE]-(n:%(node)s)
@@ -371,7 +371,7 @@ class NumberPoolSetReserved(Query):
371
371
 
372
372
  query = """
373
373
  MATCH (pool:%(number_pool)s { uuid: $pool_id })
374
- MERGE (value:AttributeValue { value: $reserved, is_default: false })
374
+ MERGE (value:AttributeValue:AttributeValueIndexed { value: $reserved, is_default: false })
375
375
  WITH value, pool
376
376
  LIMIT 1
377
377
  CREATE (pool)-[rel:IS_RESERVED $rel_prop]->(value)
@@ -55,6 +55,14 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
55
55
  def is_profile_schema(self) -> bool:
56
56
  return False
57
57
 
58
+ @property
59
+ def is_ip_prefix(self) -> bool:
60
+ return False
61
+
62
+ @property
63
+ def is_ip_address(self) -> bool:
64
+ return False
65
+
58
66
  @property
59
67
  def kind(self) -> str:
60
68
  if self.namespace == "Attribute":