1#![allow(dead_code, clippy::expect_fun_call)]
3
4pub mod errors;
5pub mod generators;
6pub mod network;
7pub mod network_helper;
8pub mod observability;
9pub mod tx_helper;
10
11mod custom_processes;
12mod network_spec;
13pub mod shared;
14mod spawner;
15mod utils;
16
17use std::{
18 collections::{HashMap, HashSet, VecDeque},
19 env,
20 net::IpAddr,
21 path::{Path, PathBuf},
22 sync::Arc,
23 time::{Duration, SystemTime},
24};
25
26use anyhow::anyhow;
27use configuration::{types::JsonOverrides, NetworkConfig, RegistrationStrategy};
28use errors::OrchestratorError;
29use generators::{core_assignment, errors::GeneratorError, jam_config};
30use network::{
31 node::{NetworkNode, SpawnedNode},
32 parachain::Parachain,
33 relaychain::Relaychain,
34 Network,
35};
36pub use network_spec::NetworkSpec;
38use network_spec::{jamchain::JamchainSpec, node::NodeSpec, parachain::ParachainSpec};
39use provider::{
40 shared::helpers::running_in_ci,
41 types::{GenerateFileCommand, NodeRole, ProviderCapabilities, TransferedFile},
42 DynNamespace, DynProvider,
43};
44use serde_json::json;
45use support::{
46 constants::{
47 GRAPH_CONTAINS_DEP, GRAPH_CONTAINS_NAME, INDEGREE_CONTAINS_NAME, QUEUE_NOT_EMPTY,
48 THIS_IS_A_BUG,
49 },
50 fs::{FileSystem, FileSystemError},
51 replacer::{get_tokens_to_replace, has_tokens},
52};
53use tokio::time::timeout;
54use tracing::{debug, info, trace, warn};
55
56use crate::{
57 network::{
58 jamchain::{Jamchain, RawJamchain},
59 node::{
60 jam::RawJamNetworkNode, process::RawCustomProcessNode, CustomProcessNode,
61 CustomProcessState, JamNetworkNode, RawNetworkNode,
62 },
63 parachain::RawParachain,
64 relaychain::RawRelaychain,
65 },
66 shared::types::RegisterParachainOptions,
67 spawner::SpawnNodeCtx,
68 utils::write_zombie_json,
69};
70pub struct Orchestrator<T>
71where
72 T: FileSystem + Sync + Send,
73{
74 filesystem: T,
75 provider: DynProvider,
76}
77
78impl<T> Orchestrator<T>
79where
80 T: FileSystem + Sync + Send + Clone,
81{
82 pub fn new(filesystem: T, provider: DynProvider) -> Self {
83 Self {
84 filesystem,
85 provider,
86 }
87 }
88
89 pub async fn spawn(
90 &self,
91 network_config: NetworkConfig,
92 ) -> Result<Network<T>, OrchestratorError> {
93 let global_timeout = network_config.global_settings().network_spawn_timeout();
94 let network_spec = NetworkSpec::from_config(&network_config).await?;
95
96 let res = timeout(
97 Duration::from_secs(global_timeout.into()),
98 self.spawn_inner(network_spec),
99 )
100 .await
101 .map_err(|_| OrchestratorError::GlobalTimeOut(global_timeout));
102 res?
103 }
104
105 pub async fn spawn_from_spec(
106 &self,
107 network_spec: NetworkSpec,
108 ) -> Result<Network<T>, OrchestratorError> {
109 let global_timeout = network_spec.global_settings.network_spawn_timeout();
110 let res = timeout(
111 Duration::from_secs(global_timeout as u64),
112 self.spawn_inner(network_spec),
113 )
114 .await
115 .map_err(|_| OrchestratorError::GlobalTimeOut(global_timeout));
116 res?
117 }
118
119 pub async fn attach_to_live(
120 &self,
121 zombie_json_path: &Path,
122 ) -> Result<Network<T>, OrchestratorError> {
123 info!("attaching to live network...");
124 info!("reading zombie.json from {:?}", zombie_json_path);
125
126 let zombie_json_content = self.filesystem.read_to_string(zombie_json_path).await?;
127 let zombie_json: serde_json::Value = serde_json::from_str(&zombie_json_content)?;
128
129 self.attach_to_live_from_json(&zombie_json).await
130 }
131
132 pub async fn attach_to_live_from_json(
135 &self,
136 zombie_json: &serde_json::Value,
137 ) -> Result<Network<T>, OrchestratorError> {
138 info!("recreating namespace...");
139 let ns: DynNamespace = self
140 .provider
141 .create_namespace_from_json(zombie_json)
142 .await?;
143
144 info!("recreating relaychain...");
145 let (relay, initial_spec) =
146 recreate_relaychain_from_json(zombie_json, ns.clone(), self.provider.name()).await?;
147 let relay_nodes = relay.nodes.clone();
148
149 let mut network =
150 Network::new_with_relay(relay, ns.clone(), self.filesystem.clone(), initial_spec);
151
152 for node in relay_nodes {
153 if node.is_responsive().await {
154 node.set_is_running(true);
155 }
156 network.insert_node(node);
157 }
158
159 info!("recreating parachains...");
160 let parachains_map =
161 recreate_parachains_from_json(zombie_json, ns.clone(), self.provider.name()).await?;
162 let para_nodes = parachains_map
163 .values()
164 .flat_map(|paras| paras.iter().flat_map(|para| para.collators.clone()))
165 .collect::<Vec<Arc<NetworkNode>>>();
166
167 network.set_parachains(parachains_map);
168 for node in para_nodes {
169 if node.is_responsive().await {
170 node.set_is_running(true);
171 }
172 network.insert_node(node);
173 }
174
175 if let Some(jamchain) =
176 recreate_jamchain_from_json(zombie_json, ns.clone(), self.provider.name()).await?
177 {
178 info!("recreating jamchain...");
179 let jam_nodes = jamchain.nodes.clone();
180 network.set_jamchain(jamchain);
181 for node in jam_nodes {
182 if node.is_responsive().await {
183 node.core().set_is_running(true);
184 }
185 network.insert_node(node);
186 }
187 }
188
189 let processes =
190 recreate_custom_processes_from_json(zombie_json, ns.clone(), self.provider.name())
191 .await?;
192 if !processes.is_empty() {
193 info!("recreating custom processes...");
194 for process in &processes {
195 if process.is_responsive().await {
198 process.core().set_is_running(true);
199 }
200 }
201 network.set_custom_processes(processes);
202 }
203
204 Ok(network)
205 }
206
207 async fn spawn_inner(
208 &self,
209 mut network_spec: NetworkSpec,
210 ) -> Result<Network<T>, OrchestratorError> {
211 debug!(network_spec = ?network_spec,"Network spec to spawn");
213
214 validate_spec_with_provider_capabilities(&network_spec, self.provider.capabilities())
216 .map_err(|err| {
217 OrchestratorError::InvalidConfigForProvider(
218 self.provider.name().into(),
219 err.to_string(),
220 )
221 })?;
222
223 let ns = if let Some(base_dir) = network_spec.global_settings.base_dir() {
225 self.provider
226 .create_namespace_with_base_dir(base_dir)
227 .await?
228 } else {
229 self.provider.create_namespace().await?
230 };
231
232 let (spawn_concurrency, limited_by_tokens) = calculate_concurrency(&network_spec)?;
234
235 let start_time = SystemTime::now();
236 info!("🧰 ns: {}", ns.name());
237 info!("🧰 base_dir: {:?}", ns.base_dir());
238 info!("🕰 start time: {:?}", start_time);
239 info!("⚙️ spawn concurrency: {spawn_concurrency} (limited by tokens: {limited_by_tokens})");
240
241 network_spec
242 .populate_nodes_available_args(ns.clone())
243 .await?;
244
245 if network_spec.try_relaychain().is_none() {
248 let jam_spec =
249 network_spec
250 .jamchain
251 .clone()
252 .ok_or(OrchestratorError::InvalidConfig(
253 "A network needs either a relaychain or a jamchain.".to_string(),
254 ))?;
255
256 return self
257 .spawn_jam(&jam_spec, &mut network_spec, ns, start_time)
258 .await;
259 }
260
261 let all_nodes: Vec<&NodeSpec> = network_spec
264 .relaychain()
265 .nodes
266 .iter()
267 .chain(
268 network_spec
269 .parachains_iter()
270 .flat_map(|p| p.collators.iter()),
271 )
272 .collect();
273 let resolved_db_snapshots =
274 generators::resolve_db_snapshots(all_nodes, &ns, &self.filesystem).await?;
275
276 let base_dir = ns.base_dir().to_string_lossy();
277 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
278
279 let mut relaychain = network_spec
283 .relaychain
284 .take()
285 .expect("checked to be `Some` above; qed");
286
287 relaychain.chain_spec.build(&ns, &scoped_fs).await?;
289
290 debug!("relaychain spec built!");
291 let relay_chain_id = relaychain.chain_spec.read_chain_id(&scoped_fs).await?;
293
294 let relay_chain_name = relaychain.chain.as_str().to_owned();
295 let base_dir_exists = network_spec.global_settings.base_dir().is_some();
296 network_spec
297 .build_parachain_artifacts(ns.clone(), &scoped_fs, &relay_chain_id, base_dir_exists)
298 .await?;
299
300 let (para_to_register_in_genesis, para_to_register_with_extrinsic): (
302 Vec<&ParachainSpec>,
303 Vec<&ParachainSpec>,
304 ) = network_spec
305 .parachains
306 .iter()
307 .filter(|para| para.registration_strategy != RegistrationStrategy::Manual)
308 .partition(|para| {
309 matches!(para.registration_strategy, RegistrationStrategy::InGenesis)
310 });
311
312 let mut para_artifacts = vec![];
313 for para in ¶_to_register_in_genesis {
314 let genesis_config = para.get_genesis_config()?;
315 para_artifacts.push(genesis_config)
316 }
317
318 relaychain
320 .chain_spec
321 .customize_relay(
322 &relaychain,
323 &network_spec.hrmp_channels,
324 para_artifacts,
325 &scoped_fs,
326 )
327 .await?;
328
329 if let Some(script_cmd) = relaychain.post_process_script.as_deref() {
331 relaychain
332 .chain_spec
333 .run_post_process_script(script_cmd, &scoped_fs)
334 .await?;
335 }
336
337 let num_cores = network_spec.parachains_iter().fold(0u32, |mut acc, para| {
339 if let Some(cores) = para.num_cores {
340 acc += cores;
341 } else if &RegistrationStrategy::InGenesis == para.registration_strategy() {
342 acc += 1;
344 }
345
346 acc
347 });
348
349 if num_cores > para_to_register_in_genesis.len() as u32 {
350 let num_cores_to_set = num_cores - para_to_register_in_genesis.len() as u32;
351 let overrides = json!({
353 "configuration": {
354 "config": {
355 "scheduler_params": {
356 "num_cores": num_cores_to_set,
357 "max_validators_per_core": 1
358 },
359 }
360 }
361 });
362
363 relaychain
364 .chain_spec
365 .apply_genesis_override(&scoped_fs, &overrides)
366 .await?;
367 }
368
369 relaychain
371 .chain_spec
372 .build_raw(&ns, &scoped_fs, None)
373 .await?;
374
375 if let Some(ref wasm_override) = relaychain.wasm_override {
377 relaychain
378 .chain_spec
379 .override_code(&scoped_fs, wasm_override)
380 .await?;
381 }
382
383 if let Some(ref raw_spec_override) = relaychain.raw_spec_override {
385 relaychain
386 .chain_spec
387 .override_raw_spec(&scoped_fs, raw_spec_override)
388 .await?;
389 }
390
391 if num_cores > para_to_register_in_genesis.len() as u32
393 || (relaychain.override_session_0 && !para_to_register_in_genesis.is_empty())
394 {
395 debug!("Raw overrides info: num_cores: {}, para_to_register_in_genesis_len: {:?}, override_session_0: {}", num_cores, para_to_register_in_genesis.len(), relaychain.override_session_0);
396 let mut core_index = 0u32;
397 let scheduler_key = core_assignment::get_parascheduler_storage_key();
400 let is_old = !relaychain
401 .chain_spec
402 .find_raw_key(&scoped_fs, &scheduler_key)
403 .await?;
404
405 let mut para_scheduler_value_parts: Vec<String> = vec![];
406 let mut raw_json_overrides = json!({});
407 for para in &network_spec.parachains {
409 let mut cores_for_para = 0_u32;
411 if let Some(cores) = para.num_cores {
412 if &RegistrationStrategy::InGenesis == para.registration_strategy() {
413 cores_for_para = cores;
414 }
415 } else {
416 if &RegistrationStrategy::InGenesis == para.registration_strategy()
419 && relaychain.override_session_0
420 {
421 cores_for_para += 1;
422 }
423 }
424
425 debug!(
426 "Assigning {cores_for_para} cores in raw spec for para {}. Using pallet {}",
427 para.id,
428 if is_old {
429 "CoretimeAssignmentProvider"
430 } else {
431 "ParaScheduler"
432 }
433 );
434 for _core in 0..cores_for_para {
435 if is_old {
436 let (core_assign_key, core_assign_value) =
437 core_assignment::generate_old(core_index, para.id);
438 raw_json_overrides[core_assign_key] = json!(core_assign_value);
439 } else {
440 let part = core_assignment::generate(core_index, para.id);
441 para_scheduler_value_parts.push(part);
442 }
443 core_index += 1;
444 }
445 }
446
447 if !is_old {
449 let count_prefix = format!("{:02x}", para_scheduler_value_parts.len() * 4);
450 let core_assign_value =
451 format!("{count_prefix}{}", para_scheduler_value_parts.join(""));
452 raw_json_overrides[scheduler_key] = json!(core_assign_value);
453 }
454
455 if relaychain.override_session_0 {
457 trace!("Overriding pallet ParaSessionInfo.session (0) to allow paras to produce blocks at first session.");
458 let raw_spec = relaychain.chain_spec.read_raw_spec(&scoped_fs).await?;
459 let overrides = generators::generate_session_0_overrides(&raw_spec, num_cores)?;
460
461 for (k, v) in overrides.as_object().ok_or(anyhow!(
462 "'generate_session_0_overrides' should be a valid json Object."
463 ))? {
464 raw_json_overrides[k] = v.clone();
465 }
466 }
467
468 debug!("Raw overrides keys: {:?}", raw_json_overrides);
469
470 relaychain
471 .chain_spec
472 .override_raw_spec(
473 &scoped_fs,
474 &JsonOverrides::Json(json!({
475 "genesis": {
476 "raw": {
477 "top": raw_json_overrides
478 }
479 }
480 })),
481 )
482 .await?;
483 }
484
485 network_spec.relaychain = Some(relaychain);
487
488 let (bootnodes, relaynodes) =
489 split_nodes_by_bootnodes(&network_spec.relaychain().nodes, false);
490
491 let mut ctx = SpawnNodeCtx {
493 chain_id: &relay_chain_id,
494 parachain_id: None,
495 chain: relay_chain_name.as_str(),
496 role: ZombieRole::Node,
497 ns: &ns,
498 scoped_fs: &scoped_fs,
499 parachain: None,
500 bootnodes_addr: &vec![],
501 wait_ready: false,
502 nodes_by_name: json!({}),
503 global_settings: &network_spec.global_settings,
504 resolved_db_snapshots: &resolved_db_snapshots,
505 };
506
507 let global_files_to_inject = vec![TransferedFile::new(
508 PathBuf::from(format!(
509 "{}/{relay_chain_name}.json",
510 ns.base_dir().to_string_lossy()
511 )),
512 PathBuf::from(format!("/cfg/{relay_chain_name}.json")),
513 )];
514
515 let r = Relaychain::new(
516 relay_chain_name.to_string(),
517 relay_chain_id.clone(),
518 PathBuf::from(network_spec.relaychain().chain_spec.raw_path().ok_or(
519 OrchestratorError::InvariantError("chain-spec raw path should be set now"),
520 )?),
521 );
522 let mut network =
523 Network::new_with_relay(r, ns.clone(), self.filesystem.clone(), network_spec.clone());
524
525 let mut node_ws_url: String = "".to_string();
527
528 let mut bootnodes_addr: Vec<String> = vec![];
530
531 for level in dependency_levels_among(&bootnodes)? {
532 let mut running_nodes_per_level = vec![];
533 for chunk in level.chunks(spawn_concurrency) {
534 let spawning_tasks = chunk
535 .iter()
536 .map(|node| spawner::spawn_node(node, global_files_to_inject.clone(), &ctx));
537
538 for node in futures::future::try_join_all(spawning_tasks).await? {
539 let bootnode_multiaddr = node.multiaddr();
540
541 bootnodes_addr.push(bootnode_multiaddr.to_string());
542
543 if node_ws_url.is_empty() {
545 node_ws_url.clone_from(&node.ws_uri)
546 }
547
548 running_nodes_per_level.push(node);
549 }
550 }
551 info!(
552 "🕰 waiting for level: {:?} to be up...",
553 level.iter().map(|n| n.name.clone()).collect::<Vec<_>>()
554 );
555
556 let waiting_tasks = running_nodes_per_level.iter().map(|node| {
558 node.wait_until_is_up(network_spec.global_settings.node_spawn_timeout())
559 });
560
561 let _ = futures::future::try_join_all(waiting_tasks).await?;
562
563 for node in running_nodes_per_level {
564 ctx.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
566 network.add_running_node(node, None).await;
567 }
568 }
569
570 network_spec
572 .relaychain()
573 .chain_spec
574 .add_bootnodes(&scoped_fs, &bootnodes_addr)
575 .await?;
576
577 ctx.bootnodes_addr = &bootnodes_addr;
578
579 upload_raw_spec(&ns, &network_spec.relaychain().chain_spec).await?;
581
582 for level in dependency_levels_among(&relaynodes)? {
583 let mut running_nodes_per_level = vec![];
584 for chunk in level.chunks(spawn_concurrency) {
585 let spawning_tasks = chunk
586 .iter()
587 .map(|node| spawner::spawn_node(node, global_files_to_inject.clone(), &ctx));
588
589 for node in futures::future::try_join_all(spawning_tasks).await? {
590 running_nodes_per_level.push(node);
591 }
592 }
593 info!(
594 "🕰 waiting for level: {:?} to be up...",
595 level.iter().map(|n| n.name.clone()).collect::<Vec<_>>()
596 );
597
598 let waiting_tasks = running_nodes_per_level.iter().map(|node| {
600 node.wait_until_is_up(network_spec.global_settings.network_spawn_timeout())
601 });
602
603 let _ = futures::future::try_join_all(waiting_tasks).await?;
604
605 for node in running_nodes_per_level {
606 ctx.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
607 network.add_running_node(node, None).await;
608 }
609 }
610
611 self.spawn_parachains(
613 &network_spec.parachains,
614 &ctx,
615 &global_files_to_inject,
616 spawn_concurrency,
617 &mut network,
618 )
619 .await?;
620
621 for para in para_to_register_with_extrinsic {
623 let register_para_options: RegisterParachainOptions = RegisterParachainOptions {
624 id: para.id,
625 wasm_path: para
627 .genesis_wasm
628 .artifact_path()
629 .ok_or(OrchestratorError::InvariantError(
630 "artifact path for wasm must be set at this point",
631 ))?
632 .to_path_buf(),
633 state_path: para
634 .genesis_state
635 .artifact_path()
636 .ok_or(OrchestratorError::InvariantError(
637 "artifact path for state must be set at this point",
638 ))?
639 .to_path_buf(),
640 node_ws_url: node_ws_url.clone(),
641 onboard_as_para: para.onboard_as_parachain,
642 seed: None, finalization: false,
644 };
645
646 Parachain::register(register_para_options, &scoped_fs).await?;
647 }
648
649 if network_spec.global_settings.observability().enabled() {
650 match network
651 .start_observability(network_spec.global_settings.observability())
652 .await
653 {
654 Ok(obs) => {
655 info!("📊 Prometheus URL: {}", obs.prometheus_url);
656 info!("📊 Grafana URL: {}", obs.grafana_url);
657 },
658 Err(e) => {
659 warn!("⚠️ Failed to spawn observability stack: {e}");
660 },
661 }
662 }
663
664 custom_processes::spawn_custom_processes(
665 &mut network,
666 &network_spec,
667 ns.clone(),
668 configured_concurrency(&network_spec),
669 )
670 .await?;
671
672 network.set_start_time_ts(start_time);
673
674 write_zombie_json(serde_json::to_value(&network)?, scoped_fs, ns.name()).await?;
675
676 if network_spec.global_settings.tear_down_on_failure() {
677 network.spawn_watching_task();
678 }
679
680 generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
681
682 Ok(network)
683 }
684
685 async fn spawn_parachains<'a>(
690 &self,
691 parachains: &[ParachainSpec],
692 ctx: &SpawnNodeCtx<'a, T>,
693 files_to_inject: &[TransferedFile],
694 spawn_concurrency: usize,
695 network: &mut Network<T>,
696 ) -> Result<(), OrchestratorError> {
697 let scoped_fs = ctx.scoped_fs;
698
699 for para in parachains {
700 let parachain = Parachain::from_spec(para, files_to_inject, scoped_fs).await?;
702 let parachain_id = parachain.chain_id.clone();
703
704 let (bootnodes, collators) =
705 split_nodes_by_bootnodes(¶.collators, para.no_default_bootnodes);
706
707 let mut ctx_para = SpawnNodeCtx {
709 parachain: Some(para),
710 parachain_id: parachain_id.as_deref(),
711 role: if para.is_cumulus_based {
712 ZombieRole::CumulusCollator
713 } else {
714 ZombieRole::Collator
715 },
716 bootnodes_addr: &vec![],
717 ..ctx.clone()
718 };
719
720 let mut bootnodes_addr: Vec<String> = vec![];
722 let mut running_nodes: Vec<NetworkNode> = vec![];
723
724 for level in dependency_levels_among(&bootnodes)? {
725 for node in self
726 .spawn_parachain_level(&level, ¶chain, &ctx_para, spawn_concurrency)
727 .await?
728 {
729 bootnodes_addr.push(node.multiaddr().to_string());
730 ctx_para.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
731 running_nodes.push(node);
732 }
733 }
734
735 if let Some(para_chain_spec) = para.chain_spec.as_ref() {
736 para_chain_spec
737 .add_bootnodes(scoped_fs, &bootnodes_addr)
738 .await?;
739
740 upload_raw_spec(ctx.ns, para_chain_spec).await?;
742 }
743
744 ctx_para.bootnodes_addr = &bootnodes_addr;
745
746 for level in dependency_levels_among(&collators)? {
748 for node in self
749 .spawn_parachain_level(&level, ¶chain, &ctx_para, spawn_concurrency)
750 .await?
751 {
752 ctx_para.nodes_by_name[node.name().to_owned()] = serde_json::to_value(&node)?;
753 running_nodes.push(node);
754 }
755 }
756
757 let running_para_id = parachain.para_id;
758 network.add_para(parachain);
759 for node in running_nodes {
760 network.add_running_node(node, Some(running_para_id)).await;
761 }
762 }
763
764 Ok(())
765 }
766
767 async fn spawn_parachain_level<'a>(
769 &self,
770 level: &[&NodeSpec],
771 parachain: &Parachain,
772 ctx: &SpawnNodeCtx<'a, T>,
773 spawn_concurrency: usize,
774 ) -> Result<Vec<NetworkNode>, OrchestratorError> {
775 let mut running_nodes = vec![];
776 for chunk in level.chunks(spawn_concurrency) {
777 let spawning_tasks = chunk
778 .iter()
779 .map(|node| spawner::spawn_node(node, parachain.files_to_inject.clone(), ctx));
780
781 running_nodes.extend(futures::future::try_join_all(spawning_tasks).await?);
782 }
783
784 info!(
785 "🕰 waiting for level: {:?} to be up...",
786 level.iter().map(|n| n.name.clone()).collect::<Vec<_>>()
787 );
788
789 let waiting_tasks = running_nodes
791 .iter()
792 .map(|node| node.wait_until_is_up(ctx.global_settings.network_spawn_timeout()));
793
794 let _ = futures::future::try_join_all(waiting_tasks).await?;
795
796 Ok(running_nodes)
797 }
798
799 async fn spawn_jam(
801 &self,
802 jam_spec: &JamchainSpec,
803 network_spec: &mut NetworkSpec,
804 ns: DynNamespace,
805 start_time: SystemTime,
806 ) -> Result<Network<T>, OrchestratorError> {
807 let base_dir = ns.base_dir().to_string_lossy().to_string();
808 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
809
810 let jam_config = jam_config::generate(jam_spec)?;
812 scoped_fs
814 .write(
815 "jam_config.json",
816 serde_json::to_string_pretty(&jam_config)?,
817 )
818 .await?;
819 let cmd_parts: Vec<&str> = jam_spec.chain_spec_command.split(" ").collect();
821 let cmd = cmd_parts
822 .first()
823 .expect("jam chain-spec generator cmd should be valid");
824 let jam_config_full_path = format!("{}/{}", base_dir, "jam_config.json");
825 let jam_spec_full_path = format!("{}/{}", base_dir, "jam_spec.json");
826 let args = vec![
827 "gen-spec",
828 jam_config_full_path.as_str(),
829 jam_spec_full_path.as_str(),
830 ];
831 let generate_command = GenerateFileCommand::new(cmd, jam_spec_full_path.clone()).args(args);
832 generators::chain_spec::build_locally(
833 generate_command,
834 &scoped_fs,
835 Some(&PathBuf::from(&jam_spec_full_path)),
836 )
837 .await?;
838
839 let base_dir_exists = network_spec.global_settings.base_dir().is_some();
842 network_spec
843 .build_parachain_artifacts(
844 ns.clone(),
845 &scoped_fs,
846 jam_spec.id.as_str(),
847 base_dir_exists,
848 )
849 .await?;
850
851 let collators: Vec<&NodeSpec> = network_spec
854 .parachains_iter()
855 .flat_map(|para| para.collators.iter())
856 .collect();
857 let resolved_db_snapshots =
858 generators::resolve_db_snapshots(collators, &ns, &self.filesystem).await?;
859
860 let (spawn_concurrency, _) = calculate_concurrency(network_spec)?;
861 let global_settings = &network_spec.global_settings;
862
863 let jam_ctx = SpawnNodeCtx {
865 chain_id: jam_spec.id.as_str(),
866 parachain_id: None,
867 chain: jam_spec.id.as_str(),
868 role: ZombieRole::Node,
869 ns: &ns,
870 scoped_fs: &scoped_fs,
871 parachain: None,
872 bootnodes_addr: &vec![],
873 wait_ready: false,
874 nodes_by_name: json!({}),
875 global_settings,
876 resolved_db_snapshots: &resolved_db_snapshots,
877 };
878
879 let jam_global_files_to_inject = vec![TransferedFile::new(
880 PathBuf::from(format!("{base_dir}/jam_spec.json")),
881 PathBuf::from("/cfg/jam_spec.json"),
882 )];
883
884 let mut network =
885 Network::new_without_relay(ns.clone(), self.filesystem.clone(), network_spec.clone());
886
887 network.set_jamchain(Jamchain::new(
888 jam_spec.id.as_str(),
889 PathBuf::from(&jam_spec_full_path),
890 ));
891
892 let mut bootnodes_addr = vec![];
893 let mut nodes_by_name = json!({});
895 for jam_node in &jam_spec.nodes {
896 let jam_ctx = SpawnNodeCtx {
897 bootnodes_addr: &bootnodes_addr,
898 nodes_by_name: nodes_by_name.clone(),
899 ..jam_ctx.clone()
900 };
901
902 let running_node =
903 spawner::spawn_jam_node(jam_node, jam_global_files_to_inject.clone(), &jam_ctx)
904 .await?;
905
906 running_node
907 .wait_until_is_up(global_settings.node_spawn_timeout() as u64)
908 .await
909 .map_err(|e| OrchestratorError::InvalidConfig(e.to_string()))?;
910
911 bootnodes_addr.push(running_node.peer_addr().to_string());
912 nodes_by_name[running_node.name().to_owned()] = serde_json::to_value(&running_node)?;
913 network.add_running_jam_node(running_node).await;
914 }
915
916 for para in network_spec.parachains.iter() {
918 if para.registration_strategy() != &RegistrationStrategy::Manual {
919 warn!(
920 "⚠️ Parachain {} can't be registered automatically on a JAM chain, it needs to be registered manually.",
921 para.id
922 );
923 }
924 }
925
926 let jam_ctx = SpawnNodeCtx {
928 nodes_by_name,
929 ..jam_ctx
930 };
931
932 self.spawn_parachains(
933 &network_spec.parachains,
934 &jam_ctx,
935 &jam_global_files_to_inject,
936 spawn_concurrency,
937 &mut network,
938 )
939 .await?;
940
941 custom_processes::spawn_custom_processes(
942 &mut network,
943 network_spec,
944 ns.clone(),
945 configured_concurrency(network_spec),
946 )
947 .await?;
948
949 network.set_start_time_ts(start_time);
950
951 write_zombie_json(serde_json::to_value(&network)?, scoped_fs, ns.name()).await?;
952
953 if network_spec.global_settings.tear_down_on_failure() {
954 network.spawn_watching_task();
955 }
956
957 generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
958
959 Ok(network)
960 }
961}
962
963fn validate_provider_tag(
968 inner: &serde_json::Value,
969 node_name: &str,
970 provider_name: &str,
971) -> Result<(), OrchestratorError> {
972 let provider_tag = inner
973 .get("provider_tag")
974 .and_then(|v| v.as_str())
975 .ok_or_else(|| {
976 OrchestratorError::InvalidConfig(format!(
977 "Node '{node_name}' is missing `provider_tag` in inner node JSON"
978 ))
979 })?;
980
981 if provider_tag != provider_name {
982 return Err(OrchestratorError::InvalidConfigForProvider(
983 provider_name.to_string(),
984 provider_tag.to_string(),
985 ));
986 }
987
988 Ok(())
989}
990
991async fn recreate_network_nodes_from_json(
992 nodes_json: &serde_json::Value,
993 ns: DynNamespace,
994 provider_name: &str,
995) -> Result<Vec<Arc<NetworkNode>>, OrchestratorError> {
996 let raw_nodes: Vec<RawNetworkNode> = serde_json::from_value(nodes_json.clone())?;
997
998 let mut nodes = Vec::with_capacity(raw_nodes.len());
999 for raw in raw_nodes {
1000 validate_provider_tag(&raw.inner, &raw.name, provider_name)?;
1001
1002 let inner = ns.spawn_node_from_json(&raw.inner).await?;
1003 let relay_node = NetworkNode::new(
1004 raw.name,
1005 raw.ws_uri,
1006 raw.internal_ws_uri,
1007 raw.prometheus_uri,
1008 raw.multiaddr,
1009 raw.spec,
1010 inner,
1011 raw.cmd_generator_opts,
1012 raw.context,
1013 );
1014 nodes.push(Arc::new(relay_node));
1015 }
1016
1017 Ok(nodes)
1018}
1019
1020async fn recreate_relaychain_from_json(
1021 zombie_json: &serde_json::Value,
1022 ns: DynNamespace,
1023 provider_name: &str,
1024) -> Result<(Relaychain, NetworkSpec), OrchestratorError> {
1025 let relay_json = zombie_json
1026 .get("relay")
1027 .ok_or(OrchestratorError::InvalidConfig(
1028 "Missing `relay` field in zombie.json".into(),
1029 ))?
1030 .clone();
1031
1032 let mut relay_raw: RawRelaychain = serde_json::from_value(relay_json)?;
1033
1034 let initial_spec: NetworkSpec = serde_json::from_value(
1035 zombie_json
1036 .get("initial_spec")
1037 .ok_or(OrchestratorError::InvalidConfig(
1038 "Missing `initial_spec` field in zombie.json".into(),
1039 ))?
1040 .clone(),
1041 )?;
1042
1043 let nodes =
1045 recreate_network_nodes_from_json(&relay_raw.nodes, ns.clone(), provider_name).await?;
1046 relay_raw.inner.nodes = nodes;
1047
1048 Ok((relay_raw.inner, initial_spec))
1049}
1050
1051async fn recreate_jamchain_from_json(
1053 zombie_json: &serde_json::Value,
1054 ns: DynNamespace,
1055 provider_name: &str,
1056) -> Result<Option<Jamchain>, OrchestratorError> {
1057 let Some(jamchain_json) = zombie_json.get("jamchain") else {
1058 return Ok(None);
1059 };
1060
1061 let mut jamchain_raw: RawJamchain = serde_json::from_value(jamchain_json.clone())?;
1062 let raw_nodes: Vec<RawJamNetworkNode> = serde_json::from_value(jamchain_raw.nodes.clone())?;
1063
1064 let mut nodes = Vec::with_capacity(raw_nodes.len());
1065 for raw in raw_nodes {
1066 validate_provider_tag(&raw.inner, &raw.name, provider_name)?;
1067
1068 let inner = ns.spawn_node_from_json(&raw.inner).await?;
1069 nodes.push(Arc::new(JamNetworkNode::new(
1070 raw.name,
1071 inner,
1072 raw.spec,
1073 raw.ip,
1074 raw.cmd_generator_opts,
1075 )));
1076 }
1077
1078 jamchain_raw.inner.nodes = nodes;
1079
1080 Ok(Some(jamchain_raw.inner))
1081}
1082
1083async fn recreate_custom_processes_from_json(
1086 zombie_json: &serde_json::Value,
1087 ns: DynNamespace,
1088 provider_name: &str,
1089) -> Result<Vec<Arc<CustomProcessNode>>, OrchestratorError> {
1090 let Some(processes_json) = zombie_json.get("custom_processes") else {
1091 return Ok(vec![]);
1092 };
1093
1094 let raw_processes: Vec<RawCustomProcessNode> = serde_json::from_value(processes_json.clone())?;
1095
1096 let mut processes = Vec::with_capacity(raw_processes.len());
1097 for raw in raw_processes {
1098 validate_provider_tag(&raw.inner, &raw.name, provider_name)?;
1099
1100 let inner = match ns.spawn_node_from_json(&raw.inner).await {
1104 Ok(inner) => inner,
1105 Err(err) => {
1106 warn!(
1107 "⚠️ {}: could not attach to the custom process, skipping it: {err}",
1108 raw.name
1109 );
1110 continue;
1111 },
1112 };
1113
1114 let mut ports = raw.ports;
1118 let state = raw.state.unwrap_or(CustomProcessState::Ready);
1119 if !running_in_ci() && !raw.spec.one_shot() {
1122 for (port_name, port) in ports.iter_mut() {
1123 let saved = std::mem::take(&mut port.external);
1124 port.external =
1125 spawner::forward_or_keep(&inner, &raw.name, port_name, port.port, saved).await;
1126 }
1127 }
1128
1129 processes.push(Arc::new(CustomProcessNode::new(
1130 raw.name, inner, raw.spec, raw.ip, ports, state,
1131 )));
1132 }
1133
1134 Ok(processes)
1135}
1136
1137async fn recreate_parachains_from_json(
1138 zombie_json: &serde_json::Value,
1139 ns: DynNamespace,
1140 provider_name: &str,
1141) -> Result<HashMap<u32, Vec<Parachain>>, OrchestratorError> {
1142 let paras_json = zombie_json
1143 .get("parachains")
1144 .ok_or(OrchestratorError::InvalidConfig(
1145 "Missing `parachains` field in zombie.json".into(),
1146 ))?
1147 .clone();
1148
1149 let raw_paras: HashMap<u32, Vec<RawParachain>> = serde_json::from_value(paras_json)?;
1150
1151 let mut parachains_map = HashMap::new();
1152
1153 for (id, parachain_entries) in raw_paras {
1154 let mut parsed_vec = Vec::with_capacity(parachain_entries.len());
1155
1156 for raw_para in parachain_entries {
1157 let mut para = raw_para.inner;
1158 para.collators =
1159 recreate_network_nodes_from_json(&raw_para.collators, ns.clone(), provider_name)
1160 .await?;
1161 parsed_vec.push(para);
1162 }
1163
1164 parachains_map.insert(id, parsed_vec);
1165 }
1166
1167 Ok(parachains_map)
1168}
1169
1170fn split_nodes_by_bootnodes(
1173 nodes: &[NodeSpec],
1174 no_default_bootnodes: bool,
1175) -> (Vec<&NodeSpec>, Vec<&NodeSpec>) {
1176 let mut bootnodes = vec![];
1178 let mut other_nodes = vec![];
1179 nodes.iter().for_each(|node| {
1180 if node.is_bootnode {
1181 bootnodes.push(node)
1182 } else {
1183 other_nodes.push(node)
1184 }
1185 });
1186
1187 if bootnodes.is_empty() && !no_default_bootnodes {
1188 bootnodes.push(other_nodes.remove(0))
1189 }
1190
1191 (bootnodes, other_nodes)
1192}
1193
1194fn generate_bootnode_addr(
1196 node: &NetworkNode,
1197 ip: &IpAddr,
1198 port: u16,
1199) -> Result<String, GeneratorError> {
1200 generators::generate_node_bootnode_addr(
1201 &node.spec.peer_id,
1202 ip,
1203 port,
1204 node.args().as_ref(),
1205 &node.spec.p2p_cert_hash,
1206 )
1207}
1208
1209async fn upload_raw_spec(
1214 ns: &DynNamespace,
1215 chain_spec: &generators::chain_spec::ChainSpec,
1216) -> Result<(), OrchestratorError> {
1217 if let Some(location) = chain_spec.raw_path() {
1218 ns.upload_to_fileserver(&ns.base_dir().join(location))
1219 .await?;
1220 }
1221
1222 Ok(())
1223}
1224
1225fn validate_spec_with_provider_capabilities(
1227 network_spec: &NetworkSpec,
1228 capabilities: &ProviderCapabilities,
1229) -> Result<(), anyhow::Error> {
1230 let mut errs: Vec<String> = vec![];
1231
1232 if capabilities.requires_image {
1233 if let Some(relaychain) = network_spec.try_relaychain() {
1235 if relaychain.default_image.is_none() {
1236 let nodes = &relaychain.nodes;
1238 if nodes.iter().any(|node| node.image.is_none()) {
1239 errs.push(String::from(
1240 "Missing image for node, and not default is set at relaychain",
1241 ));
1242 }
1243 }
1244 };
1245
1246 for para in &network_spec.parachains {
1248 if para.default_image.is_none() {
1249 let nodes = ¶.collators;
1250 if nodes.iter().any(|node| node.image.is_none()) {
1251 errs.push(format!(
1252 "Missing image for node, and not default is set at parachain {}",
1253 para.id
1254 ));
1255 }
1256 }
1257 }
1258 } else {
1259 let mut cmds: HashSet<&str> = Default::default();
1262 if let Some(relaychain) = network_spec.try_relaychain() {
1263 if let Some(cmd) = relaychain.default_command.as_ref() {
1264 cmds.insert(cmd.as_str());
1265 }
1266 for node in relaychain.nodes.iter() {
1267 cmds.insert(node.command());
1268 }
1269 }
1270
1271 if let Some(jamchain) = network_spec.jamchain.as_ref() {
1272 for node in jamchain.nodes.iter() {
1273 cmds.insert(node.command());
1274 }
1275 }
1276
1277 for para in &network_spec.parachains {
1279 if let Some(cmd) = para.default_command.as_ref() {
1280 cmds.insert(cmd.as_str());
1281 }
1282
1283 for node in para.collators.iter() {
1284 cmds.insert(node.command());
1285 }
1286 }
1287
1288 let path = std::env::var("PATH").unwrap_or_default(); trace!("current PATH: {path}");
1291 let parts: Vec<_> = path.split(":").collect();
1292 for cmd in cmds {
1293 let missing = if cmd.contains('/') {
1294 trace!("checking {cmd}");
1295 if std::fs::metadata(cmd).is_err() {
1296 true
1297 } else {
1298 info!("🔎 We will use the full path {cmd} to spawn nodes.");
1299 false
1300 }
1301 } else {
1302 !parts.iter().any(|part| {
1304 let path_to = format!("{part}/{cmd}");
1305 trace!("checking {path_to}");
1306 let check_result = std::fs::metadata(&path_to);
1307 trace!("result {:?}", check_result);
1308 if check_result.is_ok() {
1309 info!("🔎 We will use the cmd: '{cmd}' at path {path_to} to spawn nodes.");
1310 true
1311 } else {
1312 false
1313 }
1314 })
1315 };
1316
1317 if missing {
1318 errs.push(help_msg(cmd));
1319 }
1320 }
1321 }
1322
1323 if !errs.is_empty() {
1324 let msg = errs.join("\n");
1325 return Err(anyhow::anyhow!(format!("Invalid configuration: \n {msg}")));
1326 }
1327
1328 Ok(())
1329}
1330
1331fn help_msg(cmd: &str) -> String {
1332 match cmd {
1333 "parachain-template-node" | "solochain-template-node" | "minimal-template-node" => {
1334 format!("Missing binary {cmd}, compile by running: \n\tcargo build --package {cmd} --release")
1335 },
1336 "polkadot" => {
1337 format!("Missing binary {cmd}, compile by running (in the polkadot-sdk repo): \n\t cargo build --locked --release --features fast-runtime --bin {cmd} --bin polkadot-prepare-worker --bin polkadot-execute-worker")
1338 },
1339 "polkadot-parachain" => {
1340 format!("Missing binary {cmd}, compile by running (in the polkadot-sdk repo): \n\t cargo build --release --locked -p {cmd}-bin --bin {cmd}")
1341 },
1342 _ => {
1343 format!("Missing binary {cmd}, please compile it.")
1344 },
1345 }
1346}
1347
1348fn spawn_concurrency_from_env() -> Option<usize> {
1350 if let Ok(concurrency) = env::var("ZOMBIE_SPAWN_CONCURRENCY") {
1351 concurrency.parse::<usize>().ok()
1352 } else {
1353 None
1354 }
1355}
1356
1357fn configured_concurrency(spec: &NetworkSpec) -> usize {
1362 spawn_concurrency_from_env()
1363 .or(spec.global_settings.spawn_concurrency())
1364 .unwrap_or(20)
1365 .max(1)
1366}
1367
1368fn chains_have_tokens(spec: &NetworkSpec) -> Result<bool, anyhow::Error> {
1372 let chains = (&spec.relaychain, &spec.jamchain, &spec.parachains);
1373 Ok(has_tokens(&serde_json::to_string(&chains)?))
1374}
1375
1376fn calculate_concurrency(spec: &NetworkSpec) -> Result<(usize, bool), anyhow::Error> {
1378 let concurrency = configured_concurrency(spec);
1379 if concurrency > 1 && chains_have_tokens(spec)? {
1380 Ok((1, true))
1381 } else {
1382 Ok((concurrency, false))
1383 }
1384}
1385
1386fn dependency_levels_among<'a>(
1391 nodes: &'a [&'a NodeSpec],
1392) -> Result<Vec<Vec<&'a NodeSpec>>, OrchestratorError> {
1393 let by_name = nodes
1394 .iter()
1395 .map(|n| (n.name.as_str(), *n))
1396 .collect::<HashMap<_, _>>();
1397
1398 let mut graph = HashMap::with_capacity(nodes.len());
1399 let mut indegree = HashMap::with_capacity(nodes.len());
1400
1401 for node in nodes {
1402 graph.insert(node.name.as_str(), Vec::new());
1403 indegree.insert(node.name.as_str(), 0);
1404 }
1405
1406 for &node in nodes {
1408 if let Ok(args_json) = serde_json::to_string(&node.args) {
1409 let unique_deps = get_tokens_to_replace(&args_json)
1411 .into_iter()
1412 .filter(|dep| dep != &node.name)
1413 .filter_map(|dep| by_name.get(dep.as_str()))
1414 .map(|&dep_node| dep_node.name.as_str())
1415 .collect::<HashSet<_>>();
1416
1417 for dep_name in unique_deps {
1418 graph
1419 .get_mut(dep_name)
1420 .expect(&format!("{GRAPH_CONTAINS_DEP} {THIS_IS_A_BUG}"))
1421 .push(node);
1422 *indegree
1423 .get_mut(node.name.as_str())
1424 .expect(&format!("{INDEGREE_CONTAINS_NAME} {THIS_IS_A_BUG}")) += 1;
1425 }
1426 }
1427 }
1428
1429 let mut queue = nodes
1431 .iter()
1432 .filter(|n| {
1433 *indegree
1434 .get(n.name.as_str())
1435 .expect(&format!("{INDEGREE_CONTAINS_NAME} {THIS_IS_A_BUG}"))
1436 == 0
1437 })
1438 .copied()
1439 .collect::<VecDeque<_>>();
1440
1441 let mut processed_count = 0;
1442 let mut levels = Vec::new();
1443
1444 while !queue.is_empty() {
1446 let level_size = queue.len();
1447 let mut current_level = Vec::with_capacity(level_size);
1448
1449 for _ in 0..level_size {
1450 let n = queue
1451 .pop_front()
1452 .expect(&format!("{QUEUE_NOT_EMPTY} {THIS_IS_A_BUG}"));
1453 current_level.push(n);
1454 processed_count += 1;
1455
1456 for &neighbour in graph
1457 .get(n.name.as_str())
1458 .expect(&format!("{GRAPH_CONTAINS_NAME} {THIS_IS_A_BUG}"))
1459 {
1460 let neighbour_indegree = indegree
1461 .get_mut(neighbour.name.as_str())
1462 .expect(&format!("{INDEGREE_CONTAINS_NAME} {THIS_IS_A_BUG}"));
1463 *neighbour_indegree -= 1;
1464
1465 if *neighbour_indegree == 0 {
1466 queue.push_back(neighbour);
1467 }
1468 }
1469 }
1470
1471 current_level.sort_by_key(|n| &n.name);
1472 levels.push(current_level);
1473 }
1474
1475 if processed_count != nodes.len() {
1477 return Err(OrchestratorError::InvalidConfig(
1478 "Tokens have cyclical dependencies".to_string(),
1479 ));
1480 }
1481
1482 Ok(levels)
1483}
1484
1485#[derive(Clone, Debug)]
1493pub struct ScopedFilesystem<'a, FS: FileSystem> {
1494 fs: &'a FS,
1495 base_dir: &'a str,
1496}
1497
1498impl<'a, FS: FileSystem> ScopedFilesystem<'a, FS> {
1499 pub fn new(fs: &'a FS, base_dir: &'a str) -> Self {
1500 Self { fs, base_dir }
1501 }
1502
1503 async fn copy_files(&self, files: Vec<&TransferedFile>) -> Result<(), FileSystemError> {
1504 for file in files {
1505 let full_remote_path = PathBuf::from(format!(
1506 "{}/{}",
1507 self.base_dir,
1508 file.remote_path.to_string_lossy()
1509 ));
1510 trace!("coping file: {file}");
1511 self.fs
1512 .copy(file.local_path.as_path(), full_remote_path)
1513 .await?;
1514 }
1515 Ok(())
1516 }
1517
1518 async fn read(&self, file: impl AsRef<Path>) -> Result<Vec<u8>, FileSystemError> {
1519 let file = file.as_ref();
1520
1521 let full_path = if file.is_absolute() {
1522 file.to_owned()
1523 } else {
1524 PathBuf::from(format!("{}/{}", self.base_dir, file.to_string_lossy()))
1525 };
1526 let content = self.fs.read(full_path).await?;
1527 Ok(content)
1528 }
1529
1530 async fn read_to_string(&self, file: impl AsRef<Path>) -> Result<String, FileSystemError> {
1531 let file = file.as_ref();
1532
1533 let full_path = if file.is_absolute() {
1534 file.to_owned()
1535 } else {
1536 PathBuf::from(format!("{}/{}", self.base_dir, file.to_string_lossy()))
1537 };
1538 let content = self.fs.read_to_string(full_path).await?;
1539 Ok(content)
1540 }
1541
1542 async fn create_dir(&self, path: impl AsRef<Path>) -> Result<(), FileSystemError> {
1543 let path = PathBuf::from(format!(
1544 "{}/{}",
1545 self.base_dir,
1546 path.as_ref().to_string_lossy()
1547 ));
1548 self.fs.create_dir(path).await
1549 }
1550
1551 async fn create_dir_all(&self, path: impl AsRef<Path>) -> Result<(), FileSystemError> {
1552 let path = PathBuf::from(format!(
1553 "{}/{}",
1554 self.base_dir,
1555 path.as_ref().to_string_lossy()
1556 ));
1557 self.fs.create_dir_all(path).await
1558 }
1559
1560 async fn write(
1561 &self,
1562 path: impl AsRef<Path>,
1563 contents: impl AsRef<[u8]> + Send,
1564 ) -> Result<(), FileSystemError> {
1565 let path = path.as_ref();
1566
1567 let full_path = if path.is_absolute() {
1568 path.to_owned()
1569 } else {
1570 PathBuf::from(format!("{}/{}", self.base_dir, path.to_string_lossy()))
1571 };
1572
1573 self.fs.write(full_path, contents).await
1574 }
1575
1576 fn full_path(&self, path: impl AsRef<Path>) -> PathBuf {
1578 let path = path.as_ref();
1579
1580 let full_path = if path.is_absolute() {
1581 path.to_owned()
1582 } else {
1583 PathBuf::from(format!("{}/{}", self.base_dir, path.to_string_lossy()))
1584 };
1585
1586 full_path
1587 }
1588
1589 fn base_dir(&self) -> &str {
1591 self.base_dir
1592 }
1593}
1594
1595#[derive(Clone, Debug)]
1596pub enum ZombieRole {
1597 Temp,
1598 Node,
1599 Bootnode,
1600 Collator,
1601 CumulusCollator,
1602}
1603
1604impl ZombieRole {
1605 pub(crate) fn node_role(&self) -> NodeRole {
1606 match self {
1607 ZombieRole::Node => NodeRole::Node,
1608 ZombieRole::Bootnode => NodeRole::Bootnode,
1609 ZombieRole::Collator => NodeRole::Collator,
1610 ZombieRole::CumulusCollator => NodeRole::CumulusCollator,
1611 ZombieRole::Temp => NodeRole::Temp,
1612 }
1613 }
1614}
1615
1616pub use network::{AddCollatorOptions, AddNodeOptions};
1618pub use network_helper::metrics;
1619pub use sc_chain_spec;
1620
1621#[cfg(test)]
1622mod tests {
1623 use configuration::{GlobalSettingsBuilder, NetworkConfigBuilder};
1624 use lazy_static::lazy_static;
1625 use tokio::sync::Mutex;
1626
1627 use super::*;
1628
1629 const ENV_KEY: &str = "ZOMBIE_SPAWN_CONCURRENCY";
1630 lazy_static! {
1632 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
1633 }
1634
1635 fn set_env(concurrency: Option<u32>) {
1636 if let Some(value) = concurrency {
1637 env::set_var(ENV_KEY, value.to_string());
1638 } else {
1639 env::remove_var(ENV_KEY);
1640 }
1641 }
1642
1643 fn generate(
1644 with_image: bool,
1645 with_cmd: Option<&'static str>,
1646 ) -> Result<NetworkConfig, Vec<anyhow::Error>> {
1647 NetworkConfigBuilder::new()
1648 .with_relaychain(|r| {
1649 let mut relay = r
1650 .with_chain("rococo-local")
1651 .with_default_command(with_cmd.unwrap_or("polkadot"));
1652 if with_image {
1653 relay = relay.with_default_image("docker.io/parity/polkadot")
1654 }
1655
1656 relay
1657 .with_validator(|node| node.with_name("alice"))
1658 .with_validator(|node| node.with_name("bob"))
1659 })
1660 .with_parachain(|p| {
1661 p.with_id(2000).cumulus_based(true).with_collator(|n| {
1662 let node = n
1663 .with_name("collator")
1664 .with_command(with_cmd.unwrap_or("polkadot-parachain"));
1665 if with_image {
1666 node.with_image("docker.io/paritypr/test-parachain")
1667 } else {
1668 node
1669 }
1670 })
1671 })
1672 .build()
1673 }
1674
1675 fn get_node_with_dependencies(name: &str, dependencies: Option<Vec<&NodeSpec>>) -> NodeSpec {
1676 let mut spec = NodeSpec {
1677 name: name.to_string(),
1678 ..Default::default()
1679 };
1680 if let Some(dependencies) = dependencies {
1681 for node in dependencies {
1682 spec.args.push(
1683 format!("{{{{ZOMBIE:{}:someField}}}}", node.name)
1684 .as_str()
1685 .into(),
1686 );
1687 }
1688 }
1689 spec
1690 }
1691
1692 fn verify_levels(actual_levels: Vec<Vec<&NodeSpec>>, expected_levels: Vec<Vec<&str>>) {
1693 actual_levels
1694 .iter()
1695 .zip(expected_levels)
1696 .for_each(|(actual_level, expected_level)| {
1697 assert_eq!(actual_level.len(), expected_level.len());
1698 actual_level
1699 .iter()
1700 .zip(expected_level.iter())
1701 .for_each(|(node, expected_name)| assert_eq!(node.name, *expected_name));
1702 });
1703 }
1704
1705 #[tokio::test]
1706 async fn valid_config_with_image() {
1707 let network_config = generate(true, None).unwrap();
1708 let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1709 let caps = ProviderCapabilities {
1710 requires_image: true,
1711 has_resources: false,
1712 prefix_with_full_path: false,
1713 use_default_ports_in_cmd: false,
1714 };
1715
1716 let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1717 assert!(valid.is_ok())
1718 }
1719
1720 #[tokio::test]
1721 async fn invalid_config_without_image() {
1722 let network_config = generate(false, None).unwrap();
1723 let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1724 let caps = ProviderCapabilities {
1725 requires_image: true,
1726 has_resources: false,
1727 prefix_with_full_path: false,
1728 use_default_ports_in_cmd: false,
1729 };
1730
1731 let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1732 assert!(valid.is_err())
1733 }
1734
1735 #[tokio::test]
1736 async fn invalid_config_missing_cmd() {
1737 let network_config = generate(false, Some("other")).unwrap();
1738 let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1739 let caps = ProviderCapabilities {
1740 requires_image: false,
1741 has_resources: false,
1742 prefix_with_full_path: false,
1743 use_default_ports_in_cmd: false,
1744 };
1745
1746 let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1747 assert!(valid.is_err())
1748 }
1749
1750 #[tokio::test]
1751 async fn valid_config_present_cmd() {
1752 let network_config = generate(false, Some("cargo")).unwrap();
1753 let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1754 let caps = ProviderCapabilities {
1755 requires_image: false,
1756 has_resources: false,
1757 prefix_with_full_path: false,
1758 use_default_ports_in_cmd: false,
1759 };
1760
1761 let valid = validate_spec_with_provider_capabilities(&spec, &caps);
1762 println!("{valid:?}");
1763 assert!(valid.is_ok())
1764 }
1765
1766 #[tokio::test]
1767 async fn default_spawn_concurrency() {
1768 let _g = ENV_MUTEX.lock().await;
1769 set_env(None);
1770 let network_config = generate(false, Some("cargo")).unwrap();
1771 let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1772 let (concurrency, _) = calculate_concurrency(&spec).unwrap();
1773 assert_eq!(concurrency, 20);
1774 }
1775
1776 #[tokio::test]
1777 async fn set_spawn_concurrency() {
1778 let _g = ENV_MUTEX.lock().await;
1779 set_env(None);
1780
1781 let network_config = generate(false, Some("cargo")).unwrap();
1782 let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1783
1784 let global_settings = GlobalSettingsBuilder::new()
1785 .with_spawn_concurrency(4)
1786 .build()
1787 .unwrap();
1788
1789 spec.set_global_settings(global_settings);
1790 let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1791 assert_eq!(concurrency, 4);
1792 assert!(!limited);
1793 }
1794
1795 #[tokio::test]
1796 async fn set_spawn_concurrency_but_limited() {
1797 let _g = ENV_MUTEX.lock().await;
1798 set_env(None);
1799
1800 let network_config = generate(false, Some("cargo")).unwrap();
1801 let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1802
1803 let global_settings = GlobalSettingsBuilder::new()
1804 .with_spawn_concurrency(4)
1805 .build()
1806 .unwrap();
1807
1808 spec.set_global_settings(global_settings);
1809 let node = spec.relaychain_mut().nodes.first_mut().unwrap();
1810 node.args
1811 .push("--bootnodes {{ZOMBIE:bob:multiAddress')}}".into());
1812 let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1813 assert_eq!(concurrency, 1);
1814 assert!(limited);
1815 }
1816
1817 #[tokio::test]
1818 async fn custom_process_placeholders_do_not_limit_spawn_concurrency() {
1819 let _g = ENV_MUTEX.lock().await;
1820 set_env(None);
1821
1822 let network_config = generate(false, Some("cargo")).unwrap();
1823 let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1824
1825 let global_settings = GlobalSettingsBuilder::new()
1826 .with_spawn_concurrency(4)
1827 .build()
1828 .unwrap();
1829
1830 spec.set_global_settings(global_settings);
1831 spec.custom_processes.push(
1832 configuration::CustomProcessBuilder::new()
1833 .with_name("api")
1834 .with_command("api")
1835 .with_args(vec!["{{ZOMBIE:alice:ws_uri}}".into()])
1836 .build()
1837 .unwrap(),
1838 );
1839 let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1840 assert_eq!(concurrency, 4);
1841 assert!(!limited);
1842 }
1843
1844 #[tokio::test]
1845 async fn set_spawn_concurrency_from_env() {
1846 let _g = ENV_MUTEX.lock().await;
1847 set_env(Some(10));
1848
1849 let network_config = generate(false, Some("cargo")).unwrap();
1850 let spec = NetworkSpec::from_config(&network_config).await.unwrap();
1851 let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1852 assert_eq!(concurrency, 10);
1853 assert!(!limited);
1854 }
1855
1856 #[tokio::test]
1857 async fn set_spawn_concurrency_from_env_but_limited() {
1858 let _g = ENV_MUTEX.lock().await;
1859 set_env(Some(12));
1860
1861 let network_config = generate(false, Some("cargo")).unwrap();
1862 let mut spec = NetworkSpec::from_config(&network_config).await.unwrap();
1863 let node = spec.relaychain_mut().nodes.first_mut().unwrap();
1864 node.args
1865 .push("--bootnodes {{ZOMBIE:bob:multiAddress')}}".into());
1866 let (concurrency, limited) = calculate_concurrency(&spec).unwrap();
1867 assert_eq!(concurrency, 1);
1868 assert!(limited);
1869 }
1870
1871 #[test]
1872 fn dependency_levels_among_should_work() {
1873 assert!(dependency_levels_among(&[]).unwrap().is_empty());
1875
1876 let alice = get_node_with_dependencies("alice", None);
1878 let nodes = [&alice];
1879
1880 let levels = dependency_levels_among(&nodes).unwrap();
1881 let expected = vec![vec!["alice"]];
1882
1883 verify_levels(levels, expected);
1884
1885 let alice = get_node_with_dependencies("alice", None);
1887 let bob = get_node_with_dependencies("bob", None);
1888 let nodes = [&alice, &bob];
1889
1890 let levels = dependency_levels_among(&nodes).unwrap();
1891 let expected = vec![vec!["alice", "bob"]];
1892
1893 verify_levels(levels, expected);
1894
1895 let alice = get_node_with_dependencies("alice", None);
1897 let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1898 let charlie = get_node_with_dependencies("charlie", Some(vec![&bob]));
1899 let nodes = [&alice, &bob, &charlie];
1900
1901 let levels = dependency_levels_among(&nodes).unwrap();
1902 let expected = vec![vec!["alice"], vec!["bob"], vec!["charlie"]];
1903
1904 verify_levels(levels, expected);
1905
1906 let alice = get_node_with_dependencies("alice", None);
1910 let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1911 let charlie = get_node_with_dependencies("charlie", Some(vec![&alice]));
1912 let nodes = [&alice, &bob, &charlie];
1913
1914 let levels = dependency_levels_among(&nodes).unwrap();
1915 let expected = vec![vec!["alice"], vec!["bob", "charlie"]];
1916
1917 verify_levels(levels, expected);
1918
1919 let alice = get_node_with_dependencies("alice", None);
1923 let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1924 let charlie = get_node_with_dependencies("charlie", Some(vec![&alice]));
1925 let dave = get_node_with_dependencies("dave", Some(vec![&charlie, &bob]));
1926 let nodes = [&alice, &bob, &charlie, &dave];
1927
1928 let levels = dependency_levels_among(&nodes).unwrap();
1929 let expected = vec![vec!["alice"], vec!["bob", "charlie"], vec!["dave"]];
1930
1931 verify_levels(levels, expected);
1932 }
1933
1934 #[test]
1935 fn dependency_levels_among_should_detect_cycles() {
1936 let mut alice = get_node_with_dependencies("alice", None);
1937 let bob = get_node_with_dependencies("bob", Some(vec![&alice]));
1938 alice.args.push("{{ZOMBIE:bob:someField}}".into());
1939
1940 assert!(dependency_levels_among(&[&alice, &bob]).is_err())
1941 }
1942}