infrahub-server 1.2.11__py3-none-any.whl → 1.3.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 (211) hide show
  1. infrahub/actions/constants.py +130 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +243 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +393 -0
  6. infrahub/actions/tasks.py +119 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/branch/__init__.py +0 -0
  9. infrahub/branch/tasks.py +29 -0
  10. infrahub/branch/triggers.py +22 -0
  11. infrahub/cli/db.py +3 -4
  12. infrahub/computed_attribute/gather.py +3 -1
  13. infrahub/computed_attribute/tasks.py +23 -29
  14. infrahub/core/account.py +24 -47
  15. infrahub/core/attribute.py +13 -15
  16. infrahub/core/constants/__init__.py +10 -0
  17. infrahub/core/constants/database.py +1 -0
  18. infrahub/core/constants/infrahubkind.py +9 -0
  19. infrahub/core/constraint/node/runner.py +3 -1
  20. infrahub/core/convert_object_type/__init__.py +0 -0
  21. infrahub/core/convert_object_type/conversion.py +124 -0
  22. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  23. infrahub/core/diff/coordinator.py +8 -1
  24. infrahub/core/diff/query/all_conflicts.py +1 -5
  25. infrahub/core/diff/query/artifact.py +10 -20
  26. infrahub/core/diff/query/delete_query.py +8 -4
  27. infrahub/core/diff/query/diff_get.py +3 -6
  28. infrahub/core/diff/query/field_specifiers.py +1 -1
  29. infrahub/core/diff/query/field_summary.py +2 -4
  30. infrahub/core/diff/query/merge.py +72 -125
  31. infrahub/core/diff/query/save.py +83 -68
  32. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  33. infrahub/core/diff/query/time_range_query.py +0 -1
  34. infrahub/core/diff/repository/repository.py +4 -0
  35. infrahub/core/graph/__init__.py +1 -1
  36. infrahub/core/manager.py +14 -11
  37. infrahub/core/migrations/graph/__init__.py +6 -0
  38. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  39. infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
  40. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -6
  41. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  42. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  43. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  44. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  45. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  46. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
  47. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  48. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  49. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +662 -0
  50. infrahub/core/migrations/graph/m030_illegal_edges.py +82 -0
  51. infrahub/core/migrations/query/attribute_add.py +14 -11
  52. infrahub/core/migrations/query/attribute_rename.py +6 -11
  53. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  54. infrahub/core/migrations/query/node_duplicate.py +19 -21
  55. infrahub/core/migrations/query/relationship_duplicate.py +19 -18
  56. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  57. infrahub/core/migrations/schema/node_remove.py +19 -20
  58. infrahub/core/models.py +29 -2
  59. infrahub/core/node/__init__.py +131 -28
  60. infrahub/core/node/base.py +1 -1
  61. infrahub/core/node/create.py +211 -0
  62. infrahub/core/node/resource_manager/number_pool.py +31 -5
  63. infrahub/core/node/standard.py +6 -1
  64. infrahub/core/path.py +15 -1
  65. infrahub/core/protocols.py +57 -0
  66. infrahub/core/protocols_base.py +3 -0
  67. infrahub/core/query/__init__.py +2 -2
  68. infrahub/core/query/delete.py +3 -3
  69. infrahub/core/query/diff.py +19 -32
  70. infrahub/core/query/ipam.py +10 -20
  71. infrahub/core/query/node.py +29 -47
  72. infrahub/core/query/relationship.py +55 -34
  73. infrahub/core/query/resource_manager.py +1 -2
  74. infrahub/core/query/standard_node.py +19 -5
  75. infrahub/core/query/subquery.py +2 -4
  76. infrahub/core/relationship/constraints/count.py +10 -9
  77. infrahub/core/relationship/constraints/interface.py +2 -1
  78. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  79. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  80. infrahub/core/relationship/constraints/peer_relatives.py +72 -0
  81. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  82. infrahub/core/relationship/model.py +4 -1
  83. infrahub/core/schema/__init__.py +2 -1
  84. infrahub/core/schema/attribute_parameters.py +160 -0
  85. infrahub/core/schema/attribute_schema.py +130 -7
  86. infrahub/core/schema/basenode_schema.py +27 -3
  87. infrahub/core/schema/definitions/core/__init__.py +29 -1
  88. infrahub/core/schema/definitions/core/group.py +45 -0
  89. infrahub/core/schema/definitions/core/resource_pool.py +9 -0
  90. infrahub/core/schema/definitions/internal.py +43 -5
  91. infrahub/core/schema/generated/attribute_schema.py +16 -3
  92. infrahub/core/schema/generated/relationship_schema.py +11 -1
  93. infrahub/core/schema/manager.py +7 -2
  94. infrahub/core/schema/schema_branch.py +109 -12
  95. infrahub/core/validators/__init__.py +15 -2
  96. infrahub/core/validators/attribute/choices.py +1 -3
  97. infrahub/core/validators/attribute/enum.py +1 -3
  98. infrahub/core/validators/attribute/kind.py +1 -3
  99. infrahub/core/validators/attribute/length.py +13 -7
  100. infrahub/core/validators/attribute/min_max.py +118 -0
  101. infrahub/core/validators/attribute/number_pool.py +106 -0
  102. infrahub/core/validators/attribute/optional.py +1 -4
  103. infrahub/core/validators/attribute/regex.py +5 -6
  104. infrahub/core/validators/attribute/unique.py +1 -3
  105. infrahub/core/validators/determiner.py +18 -2
  106. infrahub/core/validators/enum.py +12 -0
  107. infrahub/core/validators/node/hierarchy.py +3 -6
  108. infrahub/core/validators/query.py +1 -3
  109. infrahub/core/validators/relationship/count.py +6 -12
  110. infrahub/core/validators/relationship/optional.py +2 -4
  111. infrahub/core/validators/relationship/peer.py +177 -12
  112. infrahub/core/validators/tasks.py +1 -1
  113. infrahub/core/validators/uniqueness/query.py +5 -9
  114. infrahub/database/__init__.py +12 -4
  115. infrahub/database/validation.py +100 -0
  116. infrahub/dependencies/builder/constraint/grouped/node_runner.py +4 -0
  117. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  118. infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
  119. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  120. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  121. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  122. infrahub/dependencies/registry.py +4 -0
  123. infrahub/events/group_action.py +1 -0
  124. infrahub/events/models.py +1 -1
  125. infrahub/git/base.py +5 -3
  126. infrahub/git/integrator.py +96 -5
  127. infrahub/git/tasks.py +1 -0
  128. infrahub/graphql/analyzer.py +139 -18
  129. infrahub/graphql/manager.py +4 -0
  130. infrahub/graphql/mutations/action.py +164 -0
  131. infrahub/graphql/mutations/convert_object_type.py +71 -0
  132. infrahub/graphql/mutations/main.py +25 -176
  133. infrahub/graphql/mutations/proposed_change.py +20 -17
  134. infrahub/graphql/mutations/relationship.py +32 -0
  135. infrahub/graphql/mutations/resource_manager.py +63 -7
  136. infrahub/graphql/queries/convert_object_type_mapping.py +34 -0
  137. infrahub/graphql/queries/resource_manager.py +7 -1
  138. infrahub/graphql/resolvers/many_relationship.py +1 -1
  139. infrahub/graphql/resolvers/resolver.py +2 -2
  140. infrahub/graphql/resolvers/single_relationship.py +1 -1
  141. infrahub/graphql/schema.py +6 -0
  142. infrahub/menu/menu.py +34 -2
  143. infrahub/message_bus/messages/__init__.py +0 -10
  144. infrahub/message_bus/operations/__init__.py +0 -8
  145. infrahub/message_bus/operations/refresh/registry.py +4 -7
  146. infrahub/patch/queries/delete_duplicated_edges.py +45 -39
  147. infrahub/pools/models.py +14 -0
  148. infrahub/pools/number.py +5 -3
  149. infrahub/pools/registration.py +22 -0
  150. infrahub/pools/tasks.py +126 -0
  151. infrahub/prefect_server/models.py +1 -19
  152. infrahub/proposed_change/models.py +68 -3
  153. infrahub/proposed_change/tasks.py +911 -34
  154. infrahub/schema/__init__.py +0 -0
  155. infrahub/schema/tasks.py +27 -0
  156. infrahub/schema/triggers.py +23 -0
  157. infrahub/task_manager/models.py +10 -6
  158. infrahub/trigger/catalogue.py +6 -0
  159. infrahub/trigger/models.py +23 -6
  160. infrahub/trigger/setup.py +26 -2
  161. infrahub/trigger/tasks.py +4 -2
  162. infrahub/types.py +6 -0
  163. infrahub/webhook/tasks.py +6 -9
  164. infrahub/workflows/catalogue.py +103 -1
  165. infrahub_sdk/client.py +43 -10
  166. infrahub_sdk/ctl/generator.py +4 -4
  167. infrahub_sdk/ctl/repository.py +1 -1
  168. infrahub_sdk/node/__init__.py +39 -0
  169. infrahub_sdk/node/attribute.py +122 -0
  170. infrahub_sdk/node/constants.py +21 -0
  171. infrahub_sdk/{node.py → node/node.py} +158 -803
  172. infrahub_sdk/node/parsers.py +15 -0
  173. infrahub_sdk/node/property.py +24 -0
  174. infrahub_sdk/node/related_node.py +266 -0
  175. infrahub_sdk/node/relationship.py +302 -0
  176. infrahub_sdk/protocols.py +112 -0
  177. infrahub_sdk/protocols_base.py +34 -2
  178. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  179. infrahub_sdk/query_groups.py +17 -5
  180. infrahub_sdk/schema/main.py +1 -0
  181. infrahub_sdk/schema/repository.py +16 -0
  182. infrahub_sdk/spec/object.py +1 -1
  183. infrahub_sdk/store.py +1 -1
  184. infrahub_sdk/testing/schemas/car_person.py +1 -0
  185. infrahub_sdk/utils.py +7 -20
  186. infrahub_sdk/yaml.py +6 -5
  187. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/METADATA +5 -5
  188. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/RECORD +197 -168
  189. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/WHEEL +1 -1
  190. infrahub_testcontainers/container.py +239 -65
  191. infrahub_testcontainers/docker-compose-cluster.test.yml +321 -0
  192. infrahub_testcontainers/docker-compose.test.yml +2 -1
  193. infrahub_testcontainers/helpers.py +23 -3
  194. infrahub_testcontainers/plugin.py +9 -0
  195. infrahub/message_bus/messages/check_generator_run.py +0 -26
  196. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  197. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  198. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  199. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  200. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  201. infrahub/message_bus/operations/check/__init__.py +0 -3
  202. infrahub/message_bus/operations/check/generator.py +0 -156
  203. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  204. infrahub/message_bus/operations/finalize/validator.py +0 -133
  205. infrahub/message_bus/operations/requests/__init__.py +0 -9
  206. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  207. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  208. infrahub/patch/queries/consolidate_duplicated_nodes.py +0 -109
  209. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  210. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/LICENSE.txt +0 -0
  211. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -28,7 +28,6 @@ INFRAHUB_SERVICES: dict[str, ContainerService] = {
28
28
  }
29
29
 
30
30
  PROJECT_ENV_VARIABLES: dict[str, str] = {
31
- "NEO4J_DOCKER_IMAGE": "neo4j:5.20.0-community",
32
31
  "MESSAGE_QUEUE_DOCKER_IMAGE": "rabbitmq:3.13.7-management",
33
32
  "CACHE_DOCKER_IMAGE": "redis:7.2.4",
34
33
  "INFRAHUB_TESTING_DOCKER_IMAGE": "registry.opsmill.io/opsmill/infrahub",
@@ -67,9 +66,12 @@ PROJECT_ENV_VARIABLES: dict[str, str] = {
67
66
  class InfrahubDockerCompose(DockerCompose):
68
67
  project_name: str | None = None
69
68
  env_vars: dict[str, str] = field(default_factory=dict)
69
+ deployment_type: str | None = None
70
70
 
71
71
  @classmethod
72
- def init(cls, directory: Path | None = None, version: str | None = None) -> Self:
72
+ def init(
73
+ cls, directory: Path | None = None, version: str | None = None, deployment_type: str | None = None
74
+ ) -> Self:
73
75
  if not directory:
74
76
  directory = Path.cwd()
75
77
 
@@ -80,7 +82,7 @@ class InfrahubDockerCompose(DockerCompose):
80
82
  if version == "local" and infrahub_image_version:
81
83
  version = infrahub_image_version
82
84
 
83
- compose = cls(project_name=cls.generate_project_name(), context=directory)
85
+ compose = cls(project_name=cls.generate_project_name(), context=directory, deployment_type=deployment_type)
84
86
  compose.create_docker_file(directory=directory)
85
87
  compose.create_env_file(directory=directory, version=version)
86
88
 
@@ -112,7 +114,10 @@ class InfrahubDockerCompose(DockerCompose):
112
114
 
113
115
  def create_docker_file(self, directory: Path) -> Path:
114
116
  current_directory = Path(__file__).resolve().parent
115
- compose_file = current_directory / "docker-compose.test.yml"
117
+ compose_file_name = (
118
+ "docker-compose-cluster.test.yml" if self.deployment_type == "cluster" else "docker-compose.test.yml"
119
+ )
120
+ compose_file = current_directory / compose_file_name
116
121
 
117
122
  test_compose_file = directory / "docker-compose.yml"
118
123
  test_compose_file.write_bytes(compose_file.read_bytes())
@@ -161,7 +166,7 @@ class InfrahubDockerCompose(DockerCompose):
161
166
  cmd.extend(self.services)
162
167
  self._run_command(cmd=cmd)
163
168
 
164
- def start_container(self, service_name: str) -> None:
169
+ def start_container(self, service_name: str | list[str]) -> None:
165
170
  """
166
171
  Starts a specific service of the docker compose environment.
167
172
 
@@ -171,7 +176,11 @@ class InfrahubDockerCompose(DockerCompose):
171
176
 
172
177
  # pull means running a separate command before starting
173
178
  if self.pull:
174
- pull_cmd = [*base_cmd, "pull", service_name]
179
+ pull_cmd = [*base_cmd, "pull"]
180
+ if isinstance(service_name, list):
181
+ pull_cmd.extend(service_name)
182
+ else:
183
+ pull_cmd.append(service_name)
175
184
  self._run_command(cmd=pull_cmd)
176
185
 
177
186
  up_cmd = [*base_cmd, "up"]
@@ -186,7 +195,10 @@ class InfrahubDockerCompose(DockerCompose):
186
195
  # we run in detached mode instead of blocking
187
196
  up_cmd.append("--detach")
188
197
 
189
- up_cmd.append(service_name)
198
+ if isinstance(service_name, list):
199
+ up_cmd.extend(service_name)
200
+ else:
201
+ up_cmd.append(service_name)
190
202
  self._run_command(cmd=up_cmd)
191
203
 
192
204
  # TODO would be good to the support for project_name upstream
@@ -234,7 +246,7 @@ class InfrahubDockerCompose(DockerCompose):
234
246
  dest_dir / backup_name,
235
247
  )
236
248
 
237
- def database_restore_backup(self, backup_file: Path) -> None:
249
+ def database_restore_backup(self, backup_file: Path) -> None: # noqa: PLR0915
238
250
  assert self.use_neo4j_enterprise
239
251
 
240
252
  shutil.copy(
@@ -243,52 +255,35 @@ class InfrahubDockerCompose(DockerCompose):
243
255
  )
244
256
  service_name = "database"
245
257
 
246
- # Ensure the database container is running otherwise start it
247
- try:
248
- self.get_container(service_name=service_name)
249
- except ContainerIsNotRunning:
250
- self.start_container(service_name=service_name)
251
-
252
- self.exec_in_container(
253
- command=["cypher-shell", "-u", "neo4j", "-p", "admin", "STOP DATABASE neo4j;"],
254
- service_name=service_name,
255
- )
258
+ if self.deployment_type != "cluster": # noqa: PLR1702
259
+ try:
260
+ self.get_container(service_name=service_name)
261
+ except ContainerIsNotRunning:
262
+ self.start_container(service_name=service_name)
256
263
 
257
- self.exec_in_container(
258
- command=[
259
- "neo4j-admin",
260
- "database",
261
- "restore",
262
- "--overwrite-destination",
263
- "--from-path",
264
- str(self.internal_backup_dir / backup_file.name),
265
- ],
266
- service_name=service_name,
267
- )
264
+ self.exec_in_container(
265
+ command=["cypher-shell", "-u", "neo4j", "-p", "admin", "STOP DATABASE neo4j;"],
266
+ service_name=service_name,
267
+ )
268
268
 
269
- self.exec_in_container(
270
- command=["chown", "-R", "neo4j:neo4j", "/data"],
271
- service_name=service_name,
272
- )
269
+ self.exec_in_container(
270
+ command=[
271
+ "neo4j-admin",
272
+ "database",
273
+ "restore",
274
+ "--overwrite-destination",
275
+ "--from-path",
276
+ str(self.internal_backup_dir / backup_file.name),
277
+ ],
278
+ service_name=service_name,
279
+ )
273
280
 
274
- (restore_output, _, _) = self.exec_in_container(
275
- command=[
276
- "cypher-shell",
277
- "--format",
278
- "plain",
279
- "-d",
280
- "system",
281
- "-u",
282
- "neo4j",
283
- "-p",
284
- "admin",
285
- "START DATABASE neo4j;",
286
- ],
287
- service_name=service_name,
288
- )
281
+ self.exec_in_container(
282
+ command=["chown", "-R", "neo4j:neo4j", "/data"],
283
+ service_name=service_name,
284
+ )
289
285
 
290
- for _ in range(3):
291
- (stdout, _, _) = self.exec_in_container(
286
+ (restore_output, _, _) = self.exec_in_container(
292
287
  command=[
293
288
  "cypher-shell",
294
289
  "--format",
@@ -299,26 +294,205 @@ class InfrahubDockerCompose(DockerCompose):
299
294
  "neo4j",
300
295
  "-p",
301
296
  "admin",
302
- "SHOW DATABASES WHERE name = 'neo4j' AND currentStatus = 'online';",
297
+ "START DATABASE neo4j;",
303
298
  ],
304
299
  service_name=service_name,
305
300
  )
306
- if stdout:
307
- break
308
- time.sleep(5)
301
+
302
+ for _ in range(3):
303
+ (stdout, _, _) = self.exec_in_container(
304
+ command=[
305
+ "cypher-shell",
306
+ "--format",
307
+ "plain",
308
+ "-d",
309
+ "system",
310
+ "-u",
311
+ "neo4j",
312
+ "-p",
313
+ "admin",
314
+ "SHOW DATABASES WHERE name = 'neo4j' AND currentStatus = 'online';",
315
+ ],
316
+ service_name=service_name,
317
+ )
318
+ if stdout:
319
+ break
320
+ time.sleep(5)
321
+ else:
322
+ (debug_logs, _, _) = self.exec_in_container(
323
+ command=["cat", "logs/debug.log"],
324
+ service_name=service_name,
325
+ )
326
+ raise Exception(f"Failed to restore database:\n{restore_output}\nDebug logs:\n{debug_logs}")
327
+
328
+ old_services = self.services
329
+ self.services = ["infrahub-server", "task-worker"]
330
+ self.stop(down=False)
331
+ try:
332
+ self.start()
333
+ except Exception as exc:
334
+ stdout, stderr = self.get_logs()
335
+ raise Exception(f"Failed to start docker compose:\nStdout:\n{stdout}\nStderr:\n{stderr}") from exc
336
+ self.services = old_services
309
337
  else:
310
- (debug_logs, _, _) = self.exec_in_container(
311
- command=["cat", "logs/debug.log"],
338
+ print("Cluster mode detected")
339
+ try:
340
+ self.get_container(service_name=service_name)
341
+ self.get_container(service_name="database-core2")
342
+ self.get_container(service_name="database-core3")
343
+ except ContainerIsNotRunning:
344
+ self.start_container("database", "database-core2", "database-core3")
345
+
346
+ # Waiting for cluster to stabilize...
347
+ time.sleep(10)
348
+
349
+ self.exec_in_container(
350
+ command=["cypher-shell", "-u", "neo4j", "-p", "admin", "DROP DATABASE neo4j;"],
351
+ service_name=service_name,
352
+ )
353
+
354
+ self.exec_in_container(
355
+ command=["rm", "-rf", "/data/databases/neo4j"],
356
+ service_name=service_name,
357
+ )
358
+ self.exec_in_container(
359
+ command=["rm", "-rf", "/data/transactions/neo4j"],
360
+ service_name=service_name,
361
+ )
362
+
363
+ self.exec_in_container(
364
+ command=[
365
+ "neo4j-admin",
366
+ "database",
367
+ "restore",
368
+ "--from-path",
369
+ str(self.internal_backup_dir / backup_file.name),
370
+ "neo4j",
371
+ ],
312
372
  service_name=service_name,
313
373
  )
314
- raise Exception(f"Failed to restore database:\n{restore_output}\nDebug logs:\n{debug_logs}")
315
374
 
316
- old_services = self.services
317
- self.services = ["infrahub-server", "task-worker"]
318
- self.stop(down=False)
319
- try:
375
+ cmd = self.compose_command_property[:]
376
+ cmd += ["restart", "database"]
377
+ self._run_command(cmd=cmd)
378
+
379
+ main_node = service_name
380
+ cluster_nodes = ["database", "database-core2", "database-core3"]
381
+
382
+ for attempt in range(3):
383
+ try:
384
+ (stdout, _, _) = self.exec_in_container(
385
+ command=[
386
+ "cypher-shell",
387
+ "--format",
388
+ "plain",
389
+ "-d",
390
+ "system",
391
+ "-u",
392
+ "neo4j",
393
+ "-p",
394
+ "admin",
395
+ "SHOW DATABASES YIELD name, address, currentStatus WHERE name = 'system' RETURN address, currentStatus",
396
+ ],
397
+ service_name=main_node,
398
+ )
399
+ except Exception:
400
+ time.sleep(10)
401
+ continue
402
+
403
+ raw_output = stdout
404
+ nodes_status = dict.fromkeys(cluster_nodes, False)
405
+ online_count = 0
406
+ total_entries = 0
407
+
408
+ try:
409
+ for line_raw in stdout.splitlines():
410
+ line = line_raw.strip()
411
+ if not line or line.startswith("address"):
412
+ continue
413
+
414
+ total_entries += 1
415
+ if "online" in line:
416
+ online_count += 1
417
+ for node in cluster_nodes:
418
+ node_pattern = f'"{node}:'
419
+ if node_pattern in line:
420
+ nodes_status[node] = True
421
+ break
422
+ if all(nodes_status.values()) and online_count == len(cluster_nodes):
423
+ break
424
+ except Exception as e:
425
+ print(f"Error parsing database status on attempt {attempt + 1}: {e}")
426
+
427
+ print(f"Waiting for all nodes to be online. Current status: {nodes_status}")
428
+ time.sleep(5)
429
+ else:
430
+ debug_logs = {}
431
+ for node in cluster_nodes:
432
+ try:
433
+ (logs, _, _) = self.exec_in_container(
434
+ command=["cat", "logs/debug.log"],
435
+ service_name=node,
436
+ )
437
+ debug_logs[node] = logs
438
+ except Exception as e:
439
+ debug_logs[node] = f"Could not retrieve logs: {str(e)}"
440
+
441
+ debug_info = f"Raw output from SHOW DATABASES command:\n{raw_output}\n\n"
442
+ debug_info += f"Final node status: {nodes_status}\n\n"
443
+
444
+ status_str = ", ".join(
445
+ [f"{node}: {'online' if status else 'offline'}" for node, status in nodes_status.items()]
446
+ )
447
+ logs_str = debug_info + "\n\n".join(
448
+ [f"--- {node} logs ---\n{logs}" for node, logs in debug_logs.items()]
449
+ )
450
+
451
+ raise Exception(
452
+ f"Failed to restore database cluster. Node status: {status_str}\nDebug logs:\n{logs_str}"
453
+ )
454
+
455
+ server_id = None
456
+ try:
457
+ stdout, _, _ = self.exec_in_container(
458
+ command=[
459
+ "cypher-shell",
460
+ "--format",
461
+ "plain",
462
+ "-d",
463
+ "system",
464
+ "-u",
465
+ "neo4j",
466
+ "-p",
467
+ "admin",
468
+ 'SHOW SERVERS YIELD name, address WHERE address = "database:7687" RETURN name;',
469
+ ],
470
+ service_name=service_name,
471
+ )
472
+
473
+ lines = stdout.splitlines()
474
+ for line_raw in lines:
475
+ line = line_raw.strip()
476
+ if not line or line == "name" or line.startswith("+"):
477
+ continue
478
+ server_id = line.strip('"')
479
+ break
480
+ except Exception as e:
481
+ print(f"Error retrieving server ID with direct query: {e}")
482
+
483
+ if server_id:
484
+ self.exec_in_container(
485
+ command=[
486
+ "cypher-shell",
487
+ "-d",
488
+ "system",
489
+ "-u",
490
+ "neo4j",
491
+ "-p",
492
+ "admin",
493
+ f"CREATE DATABASE neo4j TOPOLOGY 3 PRIMARIES OPTIONS {{ existingData: 'use', existingDataSeedInstance: '{server_id}' }};",
494
+ ],
495
+ service_name=service_name,
496
+ )
320
497
  self.start()
321
- except Exception as exc:
322
- stdout, stderr = self.get_logs()
323
- raise Exception(f"Failed to start docker compose:\nStdout:\n{stdout}\nStderr:\n{stderr}") from exc
324
- self.services = old_services
498
+ print("Database restored successfully")