infrahub-server 1.7.0b0__py3-none-any.whl → 1.7.1__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.
- infrahub/api/exceptions.py +2 -2
- infrahub/api/schema.py +5 -0
- infrahub/cli/db.py +54 -24
- infrahub/core/account.py +12 -9
- infrahub/core/branch/models.py +11 -117
- infrahub/core/branch/tasks.py +7 -3
- infrahub/core/diff/branch_differ.py +1 -1
- infrahub/core/diff/conflict_transferer.py +1 -1
- infrahub/core/diff/data_check_synchronizer.py +1 -1
- infrahub/core/diff/enricher/cardinality_one.py +1 -1
- infrahub/core/diff/enricher/hierarchy.py +1 -1
- infrahub/core/diff/enricher/labels.py +1 -1
- infrahub/core/diff/merger/merger.py +6 -2
- infrahub/core/diff/repository/repository.py +3 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +1 -1
- infrahub/core/initialization.py +2 -1
- infrahub/core/ipam/reconciler.py +8 -6
- infrahub/core/ipam/utilization.py +8 -15
- infrahub/core/manager.py +1 -26
- infrahub/core/merge.py +1 -1
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +4 -4
- infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
- infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
- infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
- infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
- infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
- infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
- infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
- infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
- infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
- infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +4 -3
- infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
- infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
- infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
- infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
- infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +19 -4
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
- infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
- infrahub/core/migrations/query/__init__.py +2 -2
- infrahub/core/migrations/query/schema_attribute_update.py +1 -1
- infrahub/core/migrations/runner.py +6 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
- infrahub/core/migrations/schema/attribute_name_update.py +1 -1
- infrahub/core/migrations/schema/attribute_supports_profile.py +5 -10
- infrahub/core/migrations/schema/models.py +8 -0
- infrahub/core/migrations/schema/node_attribute_add.py +11 -14
- infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
- infrahub/core/migrations/schema/node_kind_update.py +1 -1
- infrahub/core/migrations/schema/tasks.py +7 -1
- infrahub/core/migrations/shared.py +37 -30
- infrahub/core/node/__init__.py +3 -2
- infrahub/core/node/base.py +9 -5
- infrahub/core/node/delete_validator.py +1 -1
- infrahub/core/order.py +30 -0
- infrahub/core/protocols.py +1 -0
- infrahub/core/protocols_base.py +4 -0
- infrahub/core/query/__init__.py +8 -5
- infrahub/core/query/attribute.py +3 -3
- infrahub/core/query/branch.py +1 -1
- infrahub/core/query/delete.py +1 -1
- infrahub/core/query/diff.py +3 -3
- infrahub/core/query/ipam.py +104 -43
- infrahub/core/query/node.py +454 -101
- infrahub/core/query/relationship.py +83 -26
- infrahub/core/query/resource_manager.py +107 -18
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/constraints/peer_kind.py +1 -1
- infrahub/core/relationship/constraints/peer_parent.py +1 -1
- infrahub/core/relationship/constraints/peer_relatives.py +1 -1
- infrahub/core/relationship/constraints/profiles_kind.py +1 -1
- infrahub/core/relationship/constraints/profiles_removal.py +1 -1
- infrahub/core/relationship/model.py +8 -2
- infrahub/core/schema/attribute_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +9 -15
- infrahub/core/schema/basenode_schema.py +3 -0
- infrahub/core/schema/definitions/core/__init__.py +8 -2
- infrahub/core/schema/definitions/core/account.py +10 -10
- infrahub/core/schema/definitions/core/artifact.py +14 -8
- infrahub/core/schema/definitions/core/check.py +10 -4
- infrahub/core/schema/definitions/core/generator.py +26 -6
- infrahub/core/schema/definitions/core/graphql_query.py +1 -1
- infrahub/core/schema/definitions/core/group.py +9 -2
- infrahub/core/schema/definitions/core/ipam.py +80 -10
- infrahub/core/schema/definitions/core/menu.py +41 -7
- infrahub/core/schema/definitions/core/permission.py +16 -2
- infrahub/core/schema/definitions/core/profile.py +16 -2
- infrahub/core/schema/definitions/core/propose_change.py +24 -4
- infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
- infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
- infrahub/core/schema/definitions/core/repository.py +10 -0
- infrahub/core/schema/definitions/core/resource_pool.py +8 -1
- infrahub/core/schema/definitions/core/template.py +19 -2
- infrahub/core/schema/definitions/core/transform.py +11 -5
- infrahub/core/schema/definitions/core/webhook.py +27 -9
- infrahub/core/schema/manager.py +50 -38
- infrahub/core/schema/schema_branch.py +68 -2
- infrahub/core/utils.py +3 -3
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/attribute/choices.py +1 -1
- infrahub/core/validators/attribute/enum.py +1 -1
- infrahub/core/validators/attribute/kind.py +6 -3
- infrahub/core/validators/attribute/length.py +1 -1
- infrahub/core/validators/attribute/min_max.py +1 -1
- infrahub/core/validators/attribute/number_pool.py +1 -1
- infrahub/core/validators/attribute/optional.py +1 -1
- infrahub/core/validators/attribute/regex.py +1 -1
- infrahub/core/validators/node/attribute.py +1 -1
- infrahub/core/validators/node/relationship.py +1 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/database/__init__.py +1 -1
- infrahub/git/utils.py +1 -1
- infrahub/graphql/app.py +2 -2
- infrahub/graphql/field_extractor.py +1 -1
- infrahub/graphql/manager.py +17 -3
- infrahub/graphql/mutations/account.py +1 -1
- infrahub/graphql/order.py +14 -0
- infrahub/graphql/queries/diff/tree.py +5 -5
- infrahub/graphql/queries/resource_manager.py +25 -24
- infrahub/graphql/resolvers/ipam.py +3 -3
- infrahub/graphql/resolvers/resolver.py +44 -3
- infrahub/graphql/types/standard_node.py +8 -4
- infrahub/lock.py +7 -0
- infrahub/menu/repository.py +1 -1
- infrahub/patch/queries/base.py +1 -1
- infrahub/pools/number.py +1 -8
- infrahub/profiles/node_applier.py +1 -1
- infrahub/profiles/queries/get_profile_data.py +1 -1
- infrahub/proposed_change/action_checker.py +1 -1
- infrahub/services/__init__.py +1 -1
- infrahub/services/adapters/cache/nats.py +1 -1
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/webhook/gather.py +1 -1
- infrahub/webhook/tasks.py +22 -6
- infrahub_sdk/analyzer.py +2 -2
- infrahub_sdk/branch.py +12 -39
- infrahub_sdk/checks.py +4 -4
- infrahub_sdk/client.py +36 -0
- infrahub_sdk/ctl/cli_commands.py +2 -1
- infrahub_sdk/ctl/graphql.py +15 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/enums.py +6 -0
- infrahub_sdk/graphql/renderers.py +21 -0
- infrahub_sdk/graphql/utils.py +85 -0
- infrahub_sdk/node/attribute.py +12 -2
- infrahub_sdk/node/constants.py +11 -0
- infrahub_sdk/node/metadata.py +69 -0
- infrahub_sdk/node/node.py +65 -14
- infrahub_sdk/node/property.py +3 -0
- infrahub_sdk/node/related_node.py +24 -1
- infrahub_sdk/node/relationship.py +10 -1
- infrahub_sdk/operation.py +2 -2
- infrahub_sdk/schema/repository.py +1 -2
- infrahub_sdk/transforms.py +2 -2
- infrahub_sdk/types.py +18 -2
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/METADATA +6 -6
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/RECORD +176 -172
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/entry_points.txt +0 -1
- infrahub_testcontainers/models.py +3 -3
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/graphql/models.py +0 -36
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
infrahub/api/exceptions.py
CHANGED
|
@@ -4,12 +4,12 @@ from infrahub.exceptions import Error
|
|
|
4
4
|
class QueryValidationError(Error):
|
|
5
5
|
HTTP_CODE = 400
|
|
6
6
|
|
|
7
|
-
def __init__(self, message: str):
|
|
7
|
+
def __init__(self, message: str) -> None:
|
|
8
8
|
self.message = message
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class SchemaNotValidError(Error):
|
|
12
12
|
HTTP_CODE = 422
|
|
13
13
|
|
|
14
|
-
def __init__(self, message: str):
|
|
14
|
+
def __init__(self, message: str) -> None:
|
|
15
15
|
self.message = message
|
infrahub/api/schema.py
CHANGED
|
@@ -36,6 +36,7 @@ from infrahub.core.schema import (
|
|
|
36
36
|
TemplateSchema,
|
|
37
37
|
)
|
|
38
38
|
from infrahub.core.schema.constants import SchemaNamespace # noqa: TC001
|
|
39
|
+
from infrahub.core.timestamp import Timestamp
|
|
39
40
|
from infrahub.core.validators.models.validate_migration import (
|
|
40
41
|
SchemaValidateMigrationData,
|
|
41
42
|
SchemaValidatorPathResponseData,
|
|
@@ -353,6 +354,8 @@ async def load_schema(
|
|
|
353
354
|
if error_messages:
|
|
354
355
|
raise SchemaNotValidError(",\n".join(error_messages))
|
|
355
356
|
|
|
357
|
+
schema_load_at = Timestamp()
|
|
358
|
+
|
|
356
359
|
# ----------------------------------------------------------
|
|
357
360
|
# Update the schema
|
|
358
361
|
# ----------------------------------------------------------
|
|
@@ -367,6 +370,7 @@ async def load_schema(
|
|
|
367
370
|
limit=result.diff.all,
|
|
368
371
|
update_db=True,
|
|
369
372
|
user_id=account_session.account_id,
|
|
373
|
+
at=schema_load_at,
|
|
370
374
|
)
|
|
371
375
|
branch.update_schema_hash()
|
|
372
376
|
log.info("Schema has been updated", branch=branch.name, hash=branch.active_schema_hash.main)
|
|
@@ -389,6 +393,7 @@ async def load_schema(
|
|
|
389
393
|
previous_schema=origin_schema,
|
|
390
394
|
migrations=result.migrations,
|
|
391
395
|
user_id=account_session.account_id,
|
|
396
|
+
at=schema_load_at,
|
|
392
397
|
)
|
|
393
398
|
migration_error_msgs = await service.workflow.execute_workflow(
|
|
394
399
|
workflow=SCHEMA_APPLY_MIGRATION,
|
infrahub/cli/db.py
CHANGED
|
@@ -40,10 +40,11 @@ from infrahub.core.migrations.exceptions import MigrationFailureError
|
|
|
40
40
|
from infrahub.core.migrations.graph import get_graph_migrations, get_migration_by_number
|
|
41
41
|
from infrahub.core.migrations.schema.models import SchemaApplyMigrationData
|
|
42
42
|
from infrahub.core.migrations.schema.tasks import schema_apply_migrations
|
|
43
|
-
from infrahub.core.migrations.shared import get_migration_console
|
|
43
|
+
from infrahub.core.migrations.shared import MigrationInput, get_migration_console
|
|
44
44
|
from infrahub.core.schema import SchemaRoot, core_models, internal_schema
|
|
45
45
|
from infrahub.core.schema.definitions.deprecated import deprecated_models
|
|
46
46
|
from infrahub.core.schema.manager import SchemaManager
|
|
47
|
+
from infrahub.core.timestamp import Timestamp
|
|
47
48
|
from infrahub.core.validators.models.validate_migration import SchemaValidateMigrationData
|
|
48
49
|
from infrahub.core.validators.tasks import schema_validate_migrations
|
|
49
50
|
from infrahub.database import DatabaseType
|
|
@@ -65,6 +66,7 @@ def get_timestamp_string() -> str:
|
|
|
65
66
|
if TYPE_CHECKING:
|
|
66
67
|
from infrahub.cli.context import CliContext
|
|
67
68
|
from infrahub.core.migrations.shared import MigrationTypes
|
|
69
|
+
from infrahub.core.root import Root
|
|
68
70
|
from infrahub.database import InfrahubDatabase
|
|
69
71
|
from infrahub.database.index import IndexManagerBase
|
|
70
72
|
|
|
@@ -93,12 +95,40 @@ def callback() -> None:
|
|
|
93
95
|
"""
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
async def do_migrate(
|
|
99
|
+
db: InfrahubDatabase,
|
|
100
|
+
root_node: Root,
|
|
101
|
+
check: bool = False,
|
|
102
|
+
migration_number: int | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Core migration logic that can be called independently of CLI.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
db: The database connection.
|
|
108
|
+
root_node: The root node containing the current graph version.
|
|
109
|
+
check: If True, only check which migrations need to run without applying them.
|
|
110
|
+
migration_number: If provided, run only this specific migration.
|
|
111
|
+
"""
|
|
112
|
+
migrations = await detect_migration_to_run(
|
|
113
|
+
current_graph_version=root_node.graph_version, migration_number=migration_number
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if check or not migrations:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
await migrate_database(
|
|
120
|
+
db=db, migrations=migrations, initialize=True, update_graph_version=(migration_number is None)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
96
124
|
@app.command(name="migrate")
|
|
97
125
|
async def migrate_cmd(
|
|
98
126
|
ctx: typer.Context,
|
|
99
127
|
check: bool = typer.Option(False, help="Check the state of the database without applying the migrations."),
|
|
100
128
|
config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
|
|
101
|
-
migration_number: int | None = typer.Option(
|
|
129
|
+
migration_number: int | None = typer.Option(
|
|
130
|
+
None, help="Apply a specific migration by number, regardless of current database version"
|
|
131
|
+
),
|
|
102
132
|
) -> None:
|
|
103
133
|
"""Check the current format of the internal graph and apply the necessary migrations"""
|
|
104
134
|
logging.getLogger("infrahub").setLevel(logging.WARNING)
|
|
@@ -111,14 +141,7 @@ async def migrate_cmd(
|
|
|
111
141
|
dbdriver = await context.init_db(retry=1)
|
|
112
142
|
|
|
113
143
|
root_node = await get_root_node(db=dbdriver)
|
|
114
|
-
|
|
115
|
-
current_graph_version=root_node.graph_version, migration_number=migration_number
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if check or not migrations:
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
await migrate_database(db=dbdriver, migrations=migrations, initialize=True)
|
|
144
|
+
await do_migrate(db=dbdriver, root_node=root_node, check=check, migration_number=migration_number)
|
|
122
145
|
|
|
123
146
|
await dbdriver.close()
|
|
124
147
|
|
|
@@ -292,18 +315,17 @@ async def detect_migration_to_run(
|
|
|
292
315
|
migration = get_migration_by_number(migration_number)
|
|
293
316
|
migrations.append(migration)
|
|
294
317
|
if current_graph_version > migration.minimum_version:
|
|
318
|
+
get_migration_console().log(f"Migration {migration_number} will be re-applied.")
|
|
319
|
+
else:
|
|
295
320
|
get_migration_console().log(
|
|
296
|
-
f"Migration {migration_number}
|
|
321
|
+
f"Migration {migration_number} needs to be applied. Run `infrahub db migrate` to apply all outstanding migrations."
|
|
297
322
|
)
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if not migrations:
|
|
305
|
-
get_migration_console().log(f"Database up-to-date (v{current_graph_version}), no migration to execute.")
|
|
306
|
-
return []
|
|
323
|
+
return migrations
|
|
324
|
+
|
|
325
|
+
migrations.extend(await get_graph_migrations(current_graph_version=current_graph_version))
|
|
326
|
+
if not migrations:
|
|
327
|
+
get_migration_console().log(f"Database up-to-date (v{current_graph_version}), no migration to execute.")
|
|
328
|
+
return []
|
|
307
329
|
|
|
308
330
|
get_migration_console().log(
|
|
309
331
|
f"Database needs to be updated (v{current_graph_version} -> v{GRAPH_VERSION}), {len(migrations)} migrations pending"
|
|
@@ -312,7 +334,10 @@ async def detect_migration_to_run(
|
|
|
312
334
|
|
|
313
335
|
|
|
314
336
|
async def migrate_database(
|
|
315
|
-
db: InfrahubDatabase,
|
|
337
|
+
db: InfrahubDatabase,
|
|
338
|
+
migrations: Sequence[MigrationTypes],
|
|
339
|
+
initialize: bool = False,
|
|
340
|
+
update_graph_version: bool = True,
|
|
316
341
|
) -> bool:
|
|
317
342
|
"""Apply the latest migrations to the database, this function will print the status directly in the console.
|
|
318
343
|
|
|
@@ -322,6 +347,7 @@ async def migrate_database(
|
|
|
322
347
|
db: The database object.
|
|
323
348
|
migrations: Sequence of migrations to apply.
|
|
324
349
|
initialize: Whether to initialize the registry before running migrations.
|
|
350
|
+
update_graph_version: Whether to update the graph version after each migration.
|
|
325
351
|
"""
|
|
326
352
|
if not migrations:
|
|
327
353
|
return True
|
|
@@ -332,15 +358,16 @@ async def migrate_database(
|
|
|
332
358
|
root_node = await get_root_node(db=db)
|
|
333
359
|
|
|
334
360
|
for migration in migrations:
|
|
335
|
-
execution_result = await migration.execute(db=db)
|
|
361
|
+
execution_result = await migration.execute(migration_input=MigrationInput(db=db))
|
|
336
362
|
validation_result = None
|
|
337
363
|
|
|
338
364
|
if execution_result.success:
|
|
339
365
|
validation_result = await migration.validate_migration(db=db)
|
|
340
366
|
if validation_result.success:
|
|
341
367
|
get_migration_console().log(f"Migration: {migration.name} {SUCCESS_BADGE}")
|
|
342
|
-
|
|
343
|
-
|
|
368
|
+
if update_graph_version:
|
|
369
|
+
root_node.graph_version = migration.minimum_version + 1
|
|
370
|
+
await root_node.save(db=db)
|
|
344
371
|
|
|
345
372
|
if not execution_result.success or (validation_result and not validation_result.success):
|
|
346
373
|
get_migration_console().log(f"Migration: {migration.name} {FAILED_BADGE}")
|
|
@@ -476,6 +503,7 @@ async def update_core_schema(db: InfrahubDatabase, initialize: bool = True, debu
|
|
|
476
503
|
schema_default_branch.process()
|
|
477
504
|
registry.schema.set_schema_branch(name=default_branch.name, schema=schema_default_branch)
|
|
478
505
|
|
|
506
|
+
update_at = Timestamp()
|
|
479
507
|
async with db.start_transaction() as dbt:
|
|
480
508
|
await registry.schema.update_schema_branch(
|
|
481
509
|
schema=candidate_schema,
|
|
@@ -484,6 +512,7 @@ async def update_core_schema(db: InfrahubDatabase, initialize: bool = True, debu
|
|
|
484
512
|
diff=result.diff,
|
|
485
513
|
limit=result.diff.all,
|
|
486
514
|
update_db=True,
|
|
515
|
+
at=update_at,
|
|
487
516
|
)
|
|
488
517
|
default_branch.update_schema_hash()
|
|
489
518
|
get_migration_console().log(
|
|
@@ -501,6 +530,7 @@ async def update_core_schema(db: InfrahubDatabase, initialize: bool = True, debu
|
|
|
501
530
|
new_schema=candidate_schema,
|
|
502
531
|
previous_schema=origin_schema,
|
|
503
532
|
migrations=result.migrations,
|
|
533
|
+
at=update_at,
|
|
504
534
|
)
|
|
505
535
|
migration_error_msgs = await schema_apply_migrations(message=apply_migration_data)
|
|
506
536
|
|
infrahub/core/account.py
CHANGED
|
@@ -54,11 +54,15 @@ class AccountGlobalPermissionQuery(Query):
|
|
|
54
54
|
name: str = "account_global_permissions"
|
|
55
55
|
type: QueryType = QueryType.READ
|
|
56
56
|
|
|
57
|
-
def __init__(
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
account_id: str,
|
|
60
|
+
branch: Branch | None = None,
|
|
61
|
+
branch_agnostic: bool = False,
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__(branch=branch, branch_agnostic=branch_agnostic)
|
|
58
64
|
self.account_id = account_id
|
|
59
|
-
super().__init__(**kwargs)
|
|
60
65
|
|
|
61
|
-
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
62
66
|
self.params["account_id"] = self.account_id
|
|
63
67
|
|
|
64
68
|
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
@@ -66,7 +70,6 @@ class AccountGlobalPermissionQuery(Query):
|
|
|
66
70
|
)
|
|
67
71
|
self.params.update(branch_params)
|
|
68
72
|
|
|
69
|
-
# ruff: noqa: E501
|
|
70
73
|
query = """
|
|
71
74
|
MATCH (account:%(generic_account_node)s)
|
|
72
75
|
WHERE account.uuid = $account_id
|
|
@@ -164,7 +167,7 @@ class AccountObjectPermissionQuery(Query):
|
|
|
164
167
|
name: str = "account_object_permissions"
|
|
165
168
|
type: QueryType = QueryType.READ
|
|
166
169
|
|
|
167
|
-
def __init__(self, account_id: str, **kwargs: Any):
|
|
170
|
+
def __init__(self, account_id: str, **kwargs: Any) -> None:
|
|
168
171
|
self.account_id = account_id
|
|
169
172
|
super().__init__(**kwargs)
|
|
170
173
|
|
|
@@ -293,7 +296,7 @@ class AccountObjectPermissionQuery(Query):
|
|
|
293
296
|
|
|
294
297
|
|
|
295
298
|
async def fetch_permissions(account_id: str, db: InfrahubDatabase, branch: Branch) -> AssignedPermissions:
|
|
296
|
-
query1 =
|
|
299
|
+
query1 = AccountGlobalPermissionQuery(branch=branch, account_id=account_id, branch_agnostic=True)
|
|
297
300
|
await query1.execute(db=db)
|
|
298
301
|
global_permissions = query1.get_permissions()
|
|
299
302
|
|
|
@@ -308,7 +311,7 @@ class AccountRoleGlobalPermissionQuery(Query):
|
|
|
308
311
|
name: str = "account_role_global_permissions"
|
|
309
312
|
type: QueryType = QueryType.READ
|
|
310
313
|
|
|
311
|
-
def __init__(self, role_id: str, **kwargs: Any):
|
|
314
|
+
def __init__(self, role_id: str, **kwargs: Any) -> None:
|
|
312
315
|
self.role_id = role_id
|
|
313
316
|
super().__init__(**kwargs)
|
|
314
317
|
|
|
@@ -394,7 +397,7 @@ class AccountRoleObjectPermissionQuery(Query):
|
|
|
394
397
|
name: str = "account_role_object_permissions"
|
|
395
398
|
type: QueryType = QueryType.READ
|
|
396
399
|
|
|
397
|
-
def __init__(self, role_id: str, **kwargs: Any):
|
|
400
|
+
def __init__(self, role_id: str, **kwargs: Any) -> None:
|
|
398
401
|
self.role_id = role_id
|
|
399
402
|
super().__init__(**kwargs)
|
|
400
403
|
|
|
@@ -515,7 +518,7 @@ class AccountTokenValidateQuery(Query):
|
|
|
515
518
|
name: str = "account_token_validate"
|
|
516
519
|
type: QueryType = QueryType.READ
|
|
517
520
|
|
|
518
|
-
def __init__(self, token: str, **kwargs: Any):
|
|
521
|
+
def __init__(self, token: str, **kwargs: Any) -> None:
|
|
519
522
|
self.token = token
|
|
520
523
|
super().__init__(**kwargs)
|
|
521
524
|
|
infrahub/core/branch/models.py
CHANGED
|
@@ -333,7 +333,7 @@ class Branch(StandardNode):
|
|
|
333
333
|
f"({rel}.branch IN $branch{idx} AND {rel}.from <= $time{idx} AND {rel}.to IS NULL)"
|
|
334
334
|
)
|
|
335
335
|
filters_per_rel.append(
|
|
336
|
-
f"({rel}.branch IN $branch{idx} AND {rel}.from <= $time{idx} AND {rel}.to
|
|
336
|
+
f"({rel}.branch IN $branch{idx} AND {rel}.from <= $time{idx} AND {rel}.to > $time{idx})"
|
|
337
337
|
)
|
|
338
338
|
|
|
339
339
|
if not include_outside_parentheses:
|
|
@@ -366,7 +366,9 @@ class Branch(StandardNode):
|
|
|
366
366
|
at = Timestamp(at)
|
|
367
367
|
at_str = at.to_string()
|
|
368
368
|
if branch_agnostic:
|
|
369
|
-
filter_str =
|
|
369
|
+
filter_str = (
|
|
370
|
+
f"{variable_name}.from < ${pp}time1 AND ({variable_name}.to IS NULL or {variable_name}.to > ${pp}time1)"
|
|
371
|
+
)
|
|
370
372
|
params[f"{pp}time1"] = at_str
|
|
371
373
|
return filter_str, params
|
|
372
374
|
|
|
@@ -380,132 +382,24 @@ class Branch(StandardNode):
|
|
|
380
382
|
for idx in range(len(branches_times)):
|
|
381
383
|
filters.append(
|
|
382
384
|
f"({variable_name}.branch IN ${pp}branch{idx} "
|
|
383
|
-
f"AND {variable_name}.from
|
|
385
|
+
f"AND {variable_name}.from <= ${pp}time{idx} AND {variable_name}.to IS NULL)"
|
|
384
386
|
)
|
|
385
387
|
filters.append(
|
|
386
388
|
f"({variable_name}.branch IN ${pp}branch{idx} "
|
|
387
|
-
f"AND {variable_name}.from
|
|
388
|
-
f"AND {variable_name}.to
|
|
389
|
+
f"AND {variable_name}.from <= ${pp}time{idx} "
|
|
390
|
+
f"AND {variable_name}.to > ${pp}time{idx})"
|
|
389
391
|
)
|
|
390
392
|
|
|
391
393
|
filter_str = "(" + "\n OR ".join(filters) + ")"
|
|
392
394
|
|
|
393
395
|
return filter_str, params
|
|
394
396
|
|
|
395
|
-
def
|
|
396
|
-
self,
|
|
397
|
-
|
|
398
|
-
start_time: Timestamp,
|
|
399
|
-
end_time: Timestamp,
|
|
400
|
-
include_outside_parentheses: bool = False,
|
|
401
|
-
include_global: bool = False,
|
|
402
|
-
) -> tuple[list, dict]:
|
|
403
|
-
"""Generate a CYPHER Query filter based on a list of relationships to query a range of values in the graph.
|
|
404
|
-
The goal is to return all the values that are valid during this timerange.
|
|
405
|
-
"""
|
|
406
|
-
|
|
407
|
-
filters = []
|
|
408
|
-
params = {}
|
|
409
|
-
|
|
410
|
-
if not isinstance(rel_labels, list):
|
|
411
|
-
raise TypeError(f"rel_labels must be a list, not a {type(rel_labels)}")
|
|
412
|
-
|
|
413
|
-
start_time = Timestamp(start_time)
|
|
414
|
-
end_time = Timestamp(end_time)
|
|
415
|
-
|
|
416
|
-
if include_global:
|
|
417
|
-
branches_times = self.get_branches_and_times_to_query_global(at=start_time)
|
|
418
|
-
else:
|
|
419
|
-
branches_times = self.get_branches_and_times_to_query(at=start_time)
|
|
420
|
-
|
|
421
|
-
params["branches"] = list({branch for branches in branches_times for branch in branches})
|
|
422
|
-
params["start_time"] = start_time.to_string()
|
|
423
|
-
params["end_time"] = end_time.to_string()
|
|
424
|
-
|
|
425
|
-
for rel in rel_labels:
|
|
426
|
-
filters_per_rel = [
|
|
427
|
-
f"({rel}.branch IN $branches AND {rel}.from <= $end_time AND {rel}.to IS NULL)",
|
|
428
|
-
f"({rel}.branch IN $branches AND ({rel}.from <= $end_time OR ({rel}.to >= $start_time AND {rel}.to <= $end_time)))",
|
|
429
|
-
]
|
|
430
|
-
|
|
431
|
-
if not include_outside_parentheses:
|
|
432
|
-
filters.append("\n OR ".join(filters_per_rel))
|
|
433
|
-
|
|
434
|
-
filters.append("(" + "\n OR ".join(filters_per_rel) + ")")
|
|
435
|
-
|
|
436
|
-
return filters, params
|
|
437
|
-
|
|
438
|
-
def get_query_filter_relationships_diff(
|
|
439
|
-
self, rel_labels: list, diff_from: Timestamp, diff_to: Timestamp
|
|
440
|
-
) -> tuple[list, dict]:
|
|
441
|
-
"""
|
|
442
|
-
Generate a CYPHER Query filter to query all events that are applicable to a given branch based
|
|
443
|
-
- The time when the branch as created
|
|
444
|
-
- The branched_from time of the branch
|
|
445
|
-
- The diff_to and diff_from time as provided
|
|
446
|
-
"""
|
|
447
|
-
|
|
448
|
-
if not isinstance(rel_labels, list):
|
|
449
|
-
raise TypeError(f"rel_labels must be a list, not a {type(rel_labels)}")
|
|
450
|
-
|
|
451
|
-
start_times, end_times = self.get_branches_and_times_for_range(start_time=diff_from, end_time=diff_to)
|
|
452
|
-
|
|
453
|
-
filters = []
|
|
454
|
-
params = {}
|
|
455
|
-
|
|
456
|
-
for idx, branch_name in enumerate(start_times.keys()):
|
|
457
|
-
params[f"branch{idx}"] = branch_name
|
|
458
|
-
params[f"start_time{idx}"] = start_times[branch_name]
|
|
459
|
-
params[f"end_time{idx}"] = end_times[branch_name]
|
|
460
|
-
|
|
461
|
-
for rel in rel_labels:
|
|
462
|
-
filters_per_rel = []
|
|
463
|
-
for idx in range(len(start_times)):
|
|
464
|
-
filters_per_rel.extend(
|
|
465
|
-
[
|
|
466
|
-
f"""({rel}.branch = $branch{idx}
|
|
467
|
-
AND {rel}.from >= $start_time{idx}
|
|
468
|
-
AND {rel}.from <= $end_time{idx}
|
|
469
|
-
AND ( r2.to is NULL or r2.to >= $end_time{idx}))""",
|
|
470
|
-
f"""({rel}.branch = $branch{idx} AND {rel}.from >= $start_time{idx}
|
|
471
|
-
AND {rel}.to <= $start_time{idx})""",
|
|
472
|
-
]
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
filters.append("(" + "\n OR ".join(filters_per_rel) + ")")
|
|
476
|
-
|
|
477
|
-
return filters, params
|
|
478
|
-
|
|
479
|
-
def get_query_filter_range(self, rel_label: list, start_time: Timestamp, end_time: Timestamp) -> tuple[list, dict]:
|
|
480
|
-
"""
|
|
481
|
-
Generate a CYPHER Query filter to query a range of values in the graph between start_time and end_time."""
|
|
482
|
-
|
|
483
|
-
filters = []
|
|
484
|
-
params = {}
|
|
485
|
-
|
|
486
|
-
start_time = Timestamp(start_time)
|
|
487
|
-
end_time = Timestamp(end_time)
|
|
488
|
-
|
|
489
|
-
params["branches"] = self.get_branches_in_scope()
|
|
490
|
-
params["start_time"] = start_time.to_string()
|
|
491
|
-
params["end_time"] = end_time.to_string()
|
|
492
|
-
|
|
493
|
-
filters_per_rel = [
|
|
494
|
-
f"""({rel_label}.branch IN $branches AND {rel_label}.from >= $start_time
|
|
495
|
-
AND {rel_label}.from <= $end_time AND {rel_label}.to IS NULL)""",
|
|
496
|
-
f"""({rel_label}.branch IN $branches AND (({rel_label}.from >= $start_time
|
|
497
|
-
AND {rel_label}.from <= $end_time) OR ({rel_label}.to >= $start_time
|
|
498
|
-
AND {rel_label}.to <= $end_time)))""",
|
|
499
|
-
]
|
|
500
|
-
|
|
501
|
-
filters.append("(" + "\n OR ".join(filters_per_rel) + ")")
|
|
502
|
-
|
|
503
|
-
return filters, params
|
|
504
|
-
|
|
505
|
-
async def rebase(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> None:
|
|
397
|
+
async def rebase(
|
|
398
|
+
self, db: InfrahubDatabase, at: Optional[Union[str, Timestamp]] = None, user_id: str = SYSTEM_USER_ID
|
|
399
|
+
) -> None:
|
|
506
400
|
"""Rebase the current Branch with its origin branch"""
|
|
507
401
|
|
|
508
|
-
at = Timestamp()
|
|
402
|
+
at = Timestamp(at)
|
|
509
403
|
|
|
510
404
|
await self.rebase_graph(db=db, at=at)
|
|
511
405
|
|
infrahub/core/branch/tasks.py
CHANGED
|
@@ -88,7 +88,7 @@ async def migrate_branch(branch: str, context: InfrahubContext, send_events: boo
|
|
|
88
88
|
|
|
89
89
|
try:
|
|
90
90
|
log.info(f"Running migrations for branch '{obj.name}'")
|
|
91
|
-
await migration_runner.run(db=db)
|
|
91
|
+
await migration_runner.run(db=db, at=Timestamp())
|
|
92
92
|
except MigrationFailureError as exc:
|
|
93
93
|
log.error(f"Failed to run migrations for branch '{obj.name}': {exc.errors}")
|
|
94
94
|
raise
|
|
@@ -170,7 +170,8 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
|
|
|
170
170
|
migrations = []
|
|
171
171
|
async with lock.registry.global_graph_lock():
|
|
172
172
|
async with db.start_transaction() as dbt:
|
|
173
|
-
|
|
173
|
+
rebase_at = Timestamp()
|
|
174
|
+
await obj.rebase(db=dbt, user_id=context.account.account_id, at=rebase_at)
|
|
174
175
|
log.info("Branch successfully rebased")
|
|
175
176
|
|
|
176
177
|
if obj.has_schema_changes:
|
|
@@ -199,6 +200,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
|
|
|
199
200
|
previous_schema=schema_in_main_before,
|
|
200
201
|
migrations=migrations,
|
|
201
202
|
user_id=context.account.account_id,
|
|
203
|
+
at=rebase_at,
|
|
202
204
|
)
|
|
203
205
|
)
|
|
204
206
|
for error in errors:
|
|
@@ -291,7 +293,8 @@ async def merge_branch(branch: str, context: InfrahubContext, proposed_change_id
|
|
|
291
293
|
diff_locker=DiffLocker(),
|
|
292
294
|
workflow=get_workflow(),
|
|
293
295
|
)
|
|
294
|
-
|
|
296
|
+
merge_at = Timestamp()
|
|
297
|
+
branch_diff = await merger.merge(at=merge_at)
|
|
295
298
|
await merger.update_schema()
|
|
296
299
|
|
|
297
300
|
changelog_collector = DiffChangelogCollector(diff=branch_diff, branch=obj, db=db)
|
|
@@ -304,6 +307,7 @@ async def merge_branch(branch: str, context: InfrahubContext, proposed_change_id
|
|
|
304
307
|
previous_schema=merger.initial_source_schema,
|
|
305
308
|
migrations=merger.migrations,
|
|
306
309
|
user_id=context.account.account_id,
|
|
310
|
+
at=merge_at,
|
|
307
311
|
)
|
|
308
312
|
)
|
|
309
313
|
for error in errors:
|
|
@@ -9,7 +9,7 @@ from .model.path import (
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class DiffConflictTransferer:
|
|
12
|
-
def __init__(self, diff_combiner: DiffCombiner):
|
|
12
|
+
def __init__(self, diff_combiner: DiffCombiner) -> None:
|
|
13
13
|
self.diff_combiner = diff_combiner
|
|
14
14
|
|
|
15
15
|
async def transfer(self, earlier: EnrichedDiffRoot, later: EnrichedDiffRoot) -> None:
|
|
@@ -29,7 +29,7 @@ class DiffDataCheckSynchronizer:
|
|
|
29
29
|
conflicts_extractor: DiffConflictsExtractor,
|
|
30
30
|
conflict_recorder: ObjectConflictValidatorRecorder,
|
|
31
31
|
diff_repository: DiffRepository,
|
|
32
|
-
):
|
|
32
|
+
) -> None:
|
|
33
33
|
self.db = db
|
|
34
34
|
self.conflicts_extractor = conflicts_extractor
|
|
35
35
|
self.conflict_recorder = conflict_recorder
|
|
@@ -31,7 +31,7 @@ class DiffCardinalityOneEnricher(DiffEnricherInterface):
|
|
|
31
31
|
- changes to properties (IS_PROTECTED, etc) of a cardinality=one relationship are consolidated as well
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
-
def __init__(self, db: InfrahubDatabase):
|
|
34
|
+
def __init__(self, db: InfrahubDatabase) -> None:
|
|
35
35
|
self.db = db
|
|
36
36
|
self._node_schema_map: dict[str, MainSchemaTypes] = {}
|
|
37
37
|
|
|
@@ -24,7 +24,7 @@ log = get_logger()
|
|
|
24
24
|
class DiffHierarchyEnricher(DiffEnricherInterface):
|
|
25
25
|
"""Add hierarchy and parent/component nodes to diff even if the higher-level nodes are unchanged"""
|
|
26
26
|
|
|
27
|
-
def __init__(self, db: InfrahubDatabase, parent_adder: DiffParentNodeAdder):
|
|
27
|
+
def __init__(self, db: InfrahubDatabase, parent_adder: DiffParentNodeAdder) -> None:
|
|
28
28
|
self.db = db
|
|
29
29
|
self.parent_adder = parent_adder
|
|
30
30
|
|
|
@@ -35,7 +35,7 @@ class DisplayLabelRequest:
|
|
|
35
35
|
class DiffLabelsEnricher(DiffEnricherInterface):
|
|
36
36
|
"""Add display labels for nodes and labels for relationships"""
|
|
37
37
|
|
|
38
|
-
def __init__(self, db: InfrahubDatabase):
|
|
38
|
+
def __init__(self, db: InfrahubDatabase) -> None:
|
|
39
39
|
self.db = db
|
|
40
40
|
self._base_branch_name: str | None = None
|
|
41
41
|
self._diff_branch_name: str | None = None
|
|
@@ -36,7 +36,7 @@ class DiffMerger:
|
|
|
36
36
|
destination_branch: Branch,
|
|
37
37
|
diff_repository: DiffRepository,
|
|
38
38
|
serializer: DiffMergeSerializer,
|
|
39
|
-
):
|
|
39
|
+
) -> None:
|
|
40
40
|
self.source_branch = source_branch
|
|
41
41
|
self.destination_branch = destination_branch
|
|
42
42
|
self.db = db
|
|
@@ -125,7 +125,11 @@ class DiffMerger:
|
|
|
125
125
|
)
|
|
126
126
|
await metadata_query.execute(db=self.db)
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
# set the branched_from time to the previous microsecond to prevent duplicated
|
|
129
|
+
# relationships on the branch after the merge
|
|
130
|
+
branched_from = at.subtract(microseconds=1)
|
|
131
|
+
|
|
132
|
+
self.source_branch.branched_from = branched_from.to_string()
|
|
129
133
|
await self.source_branch.save(db=self.db)
|
|
130
134
|
registry.branch[self.source_branch.name] = self.source_branch
|
|
131
135
|
return enriched_diff
|
|
@@ -49,7 +49,9 @@ log = get_logger()
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class DiffRepository:
|
|
52
|
-
def __init__(
|
|
52
|
+
def __init__(
|
|
53
|
+
self, db: InfrahubDatabase, deserializer: EnrichedDiffDeserializer, max_save_batch_size: int = 1000
|
|
54
|
+
) -> None:
|
|
53
55
|
self.db = db
|
|
54
56
|
self.deserializer = deserializer
|
|
55
57
|
self.max_save_batch_size = max_save_batch_size
|
infrahub/core/graph/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
GRAPH_VERSION =
|
|
1
|
+
GRAPH_VERSION = 51
|
|
@@ -165,7 +165,7 @@ class ConstraintManagerBase:
|
|
|
165
165
|
constraint_node_class: Optional[type[ConstraintItem]] = ConstraintItem
|
|
166
166
|
constraint_rel_class: Optional[type[ConstraintItem]] = ConstraintItem
|
|
167
167
|
|
|
168
|
-
def __init__(self, db: InfrahubDatabase):
|
|
168
|
+
def __init__(self, db: InfrahubDatabase) -> None:
|
|
169
169
|
self.db = db
|
|
170
170
|
|
|
171
171
|
self.nodes: list[ConstraintItem] = []
|
infrahub/core/initialization.py
CHANGED
|
@@ -30,6 +30,7 @@ from infrahub.core.protocols import CoreAccount, CoreAccountGroup, CoreAccountRo
|
|
|
30
30
|
from infrahub.core.root import Root
|
|
31
31
|
from infrahub.core.schema import SchemaRoot, core_models, internal_schema
|
|
32
32
|
from infrahub.core.schema.manager import SchemaManager
|
|
33
|
+
from infrahub.core.timestamp import Timestamp
|
|
33
34
|
from infrahub.database import InfrahubDatabase
|
|
34
35
|
from infrahub.database.memgraph import IndexManagerMemgraph
|
|
35
36
|
from infrahub.database.neo4j import IndexManagerNeo4j
|
|
@@ -527,7 +528,7 @@ async def first_time_initialization(db: InfrahubDatabase) -> None:
|
|
|
527
528
|
schema_branch = registry.schema.register_schema(schema=schema, branch=default_branch.name)
|
|
528
529
|
schema_branch.load_schema(schema=SchemaRoot(**core_models))
|
|
529
530
|
schema_branch.process()
|
|
530
|
-
await registry.schema.load_schema_to_db(schema=schema_branch, branch=default_branch, db=db)
|
|
531
|
+
await registry.schema.load_schema_to_db(schema=schema_branch, branch=default_branch, db=db, at=Timestamp())
|
|
531
532
|
registry.schema.set_schema_branch(name=default_branch.name, schema=schema_branch)
|
|
532
533
|
default_branch.update_schema_hash()
|
|
533
534
|
await default_branch.save(db=db)
|
infrahub/core/ipam/reconciler.py
CHANGED
|
@@ -101,16 +101,18 @@ class IpamReconciler:
|
|
|
101
101
|
)
|
|
102
102
|
await query.execute(db=self.db)
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
if not ip_node_uuid:
|
|
104
|
+
data = query.get_data()
|
|
105
|
+
if not data or not data.ip_node_uuid:
|
|
106
106
|
node_type = InfrahubKind.IPPREFIX
|
|
107
107
|
if isinstance(ip_value, ipaddress.IPv6Interface | ipaddress.IPv4Interface):
|
|
108
108
|
node_type = InfrahubKind.IPADDRESS
|
|
109
109
|
raise NodeNotFoundError(node_type=node_type, identifier=str(ip_value))
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
|
|
111
|
+
ip_node_uuid = data.ip_node_uuid
|
|
112
|
+
current_parent_uuid = data.current_parent_uuid
|
|
113
|
+
calculated_parent_uuid = data.calculated_parent_uuid
|
|
114
|
+
current_children_uuids = set(data.current_children_uuids)
|
|
115
|
+
calculated_children_uuids = set(data.calculated_children_uuids)
|
|
114
116
|
|
|
115
117
|
all_uuids: set[str] = set()
|
|
116
118
|
all_uuids = (all_uuids | {ip_node_uuid}) if ip_node_uuid else all_uuids
|