ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/
at stable 28 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2// Copyright 2024-2025 wire Contributors 3 4#![allow(clippy::missing_errors_doc)] 5use enum_dispatch::enum_dispatch; 6use gethostname::gethostname; 7use serde::{Deserialize, Serialize}; 8use std::assert_matches::debug_assert_matches; 9use std::fmt::Display; 10use std::sync::Arc; 11use std::sync::atomic::AtomicBool; 12use tokio::sync::oneshot; 13use tracing::{Instrument, Level, Span, debug, error, event, instrument, trace}; 14 15use crate::commands::builder::CommandStringBuilder; 16use crate::commands::common::evaluate_hive_attribute; 17use crate::commands::{CommandArguments, WireCommandChip, run_command}; 18use crate::errors::NetworkError; 19use crate::hive::HiveLocation; 20use crate::hive::steps::build::Build; 21use crate::hive::steps::cleanup::CleanUp; 22use crate::hive::steps::evaluate::Evaluate; 23use crate::hive::steps::keys::{Key, Keys, PushKeyAgent, UploadKeyAt}; 24use crate::hive::steps::ping::Ping; 25use crate::hive::steps::push::{PushBuildOutput, PushEvaluatedOutput}; 26use crate::status::STATUS; 27use crate::{EvalGoal, StrictHostKeyChecking, SubCommandModifiers}; 28 29use super::HiveLibError; 30use super::steps::activate::SwitchToConfiguration; 31 32#[derive( 33 Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, derive_more::Display, 34)] 35pub struct Name(pub Arc<str>); 36 37#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] 38pub struct Target { 39 pub hosts: Vec<Arc<str>>, 40 pub user: Arc<str>, 41 pub port: u32, 42 43 #[serde(skip)] 44 current_host: usize, 45} 46 47impl Target { 48 #[instrument(ret(level = tracing::Level::DEBUG), skip_all)] 49 pub fn create_ssh_opts( 50 &self, 51 modifiers: SubCommandModifiers, 52 master: bool, 53 ) -> Result<String, HiveLibError> { 54 self.create_ssh_args(modifiers, false, master) 55 .map(|x| x.join(" ")) 56 } 57 58 #[instrument(ret(level = tracing::Level::DEBUG))] 59 pub fn create_ssh_args( 60 &self, 61 modifiers: SubCommandModifiers, 62 non_interactive_forced: bool, 63 master: bool, 64 ) -> Result<Vec<String>, HiveLibError> { 65 let mut vector = vec![ 66 "-l".to_string(), 67 self.user.to_string(), 68 "-p".to_string(), 69 self.port.to_string(), 70 ]; 71 let mut options = vec![ 72 format!( 73 "StrictHostKeyChecking={}", 74 match modifiers.ssh_accept_host { 75 StrictHostKeyChecking::AcceptNew => "accept-new", 76 StrictHostKeyChecking::No => "no", 77 } 78 ) 79 .to_string(), 80 ]; 81 82 options.extend(["PasswordAuthentication=no".to_string()]); 83 options.extend(["KbdInteractiveAuthentication=no".to_string()]); 84 85 vector.push("-o".to_string()); 86 vector.extend(options.into_iter().intersperse("-o".to_string())); 87 88 Ok(vector) 89 } 90} 91 92#[cfg(test)] 93impl Default for Target { 94 fn default() -> Self { 95 Target { 96 hosts: vec!["NAME".into()], 97 user: "root".into(), 98 port: 22, 99 current_host: 0, 100 } 101 } 102} 103 104#[cfg(test)] 105impl<'a> Context<'a> { 106 fn create_test_context( 107 hive_location: HiveLocation, 108 name: &'a Name, 109 node: &'a mut Node, 110 ) -> Self { 111 Context { 112 name, 113 node, 114 hive_location: Arc::new(hive_location), 115 modifiers: SubCommandModifiers::default(), 116 objective: Objective::Apply(ApplyObjective { 117 goal: Goal::SwitchToConfiguration(SwitchToConfigurationGoal::Switch), 118 no_keys: false, 119 reboot: false, 120 should_apply_locally: false, 121 substitute_on_destination: false, 122 handle_unreachable: HandleUnreachable::default(), 123 }), 124 state: StepState::default(), 125 should_quit: Arc::new(AtomicBool::new(false)), 126 } 127 } 128} 129 130impl Target { 131 pub fn get_preferred_host(&self) -> Result<&Arc<str>, HiveLibError> { 132 self.hosts 133 .get(self.current_host) 134 .ok_or(HiveLibError::NetworkError(NetworkError::HostsExhausted)) 135 } 136 137 pub const fn host_failed(&mut self) { 138 self.current_host += 1; 139 } 140 141 #[cfg(test)] 142 #[must_use] 143 pub fn from_host(host: &str) -> Self { 144 Target { 145 hosts: vec![host.into()], 146 ..Default::default() 147 } 148 } 149} 150 151impl Display for Target { 152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 153 let hosts = itertools::Itertools::join( 154 &mut self 155 .hosts 156 .iter() 157 .map(|host| format!("{}@{host}:{}", self.user, self.port)), 158 ", ", 159 ); 160 161 write!(f, "{hosts}") 162 } 163} 164 165#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] 166pub struct Node { 167 #[serde(rename = "target")] 168 pub target: Target, 169 170 #[serde(rename = "buildOnTarget")] 171 pub build_remotely: bool, 172 173 #[serde(rename = "allowLocalDeployment")] 174 pub allow_local_deployment: bool, 175 176 #[serde(default)] 177 pub tags: im::HashSet<String>, 178 179 #[serde(rename(deserialize = "_keys", serialize = "keys"))] 180 pub keys: im::Vector<Key>, 181 182 #[serde(rename(deserialize = "_hostPlatform", serialize = "host_platform"))] 183 pub host_platform: Arc<str>, 184 185 #[serde(rename( 186 deserialize = "privilegeEscalationCommand", 187 serialize = "privilege_escalation_command" 188 ))] 189 pub privilege_escalation_command: im::Vector<Arc<str>>, 190} 191 192#[cfg(test)] 193impl Default for Node { 194 fn default() -> Self { 195 Node { 196 target: Target::default(), 197 keys: im::Vector::new(), 198 tags: im::HashSet::new(), 199 privilege_escalation_command: vec!["sudo".into(), "--".into()].into(), 200 allow_local_deployment: true, 201 build_remotely: false, 202 host_platform: "x86_64-linux".into(), 203 } 204 } 205} 206 207impl Node { 208 #[cfg(test)] 209 #[must_use] 210 pub fn from_host(host: &str) -> Self { 211 Node { 212 target: Target::from_host(host), 213 ..Default::default() 214 } 215 } 216 217 /// Tests the connection to a node 218 pub async fn ping(&self, modifiers: SubCommandModifiers) -> Result<(), HiveLibError> { 219 let host = self.target.get_preferred_host()?; 220 221 let mut command_string = CommandStringBuilder::new("ssh"); 222 command_string.arg(format!("{}@{host}", self.target.user)); 223 command_string.arg(self.target.create_ssh_opts(modifiers, true)?); 224 command_string.arg("exit"); 225 226 let output = run_command( 227 &CommandArguments::new(command_string, modifiers) 228 .log_stdout() 229 .mode(crate::commands::ChildOutputMode::Interactive), 230 ) 231 .await?; 232 233 output.wait_till_success().await.map_err(|source| { 234 HiveLibError::NetworkError(NetworkError::HostUnreachable { 235 host: host.to_string(), 236 source, 237 }) 238 })?; 239 240 Ok(()) 241 } 242} 243 244#[must_use] 245pub fn should_apply_locally(allow_local_deployment: bool, name: &str) -> bool { 246 *name == *gethostname() && allow_local_deployment 247} 248 249#[derive(derive_more::Display)] 250pub enum Push<'a> { 251 Derivation(&'a Derivation), 252 Path(&'a String), 253} 254 255#[derive(Deserialize, Clone, Debug)] 256pub struct Derivation(String); 257 258impl Display for Derivation { 259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 260 self.0.fmt(f).and_then(|()| write!(f, "^*")) 261 } 262} 263 264#[derive(derive_more::Display, Debug, Clone, Copy)] 265pub enum SwitchToConfigurationGoal { 266 Switch, 267 Boot, 268 Test, 269 DryActivate, 270} 271 272#[derive(derive_more::Display, Clone, Copy)] 273pub enum Goal { 274 SwitchToConfiguration(SwitchToConfigurationGoal), 275 Build, 276 Push, 277 Keys, 278} 279 280// TODO: Get rid of this allow and resolve it 281#[allow(clippy::struct_excessive_bools)] 282#[derive(Clone, Copy)] 283pub struct ApplyObjective { 284 pub goal: Goal, 285 pub no_keys: bool, 286 pub reboot: bool, 287 pub should_apply_locally: bool, 288 pub substitute_on_destination: bool, 289 pub handle_unreachable: HandleUnreachable, 290} 291 292#[derive(Clone, Copy)] 293pub enum Objective { 294 Apply(ApplyObjective), 295 BuildLocally, 296} 297 298#[enum_dispatch] 299pub(crate) trait ExecuteStep: Send + Sync + Display + std::fmt::Debug { 300 async fn execute(&self, ctx: &mut Context<'_>) -> Result<(), HiveLibError>; 301 302 fn should_execute(&self, context: &Context) -> bool; 303} 304 305// may include other options such as FailAll in the future 306#[non_exhaustive] 307#[derive(Clone, Copy, Default)] 308pub enum HandleUnreachable { 309 Ignore, 310 #[default] 311 FailNode, 312} 313 314#[derive(Default)] 315pub struct StepState { 316 pub evaluation: Option<Derivation>, 317 pub evaluation_rx: Option<oneshot::Receiver<Result<Derivation, HiveLibError>>>, 318 pub build: Option<String>, 319 pub key_agent_directory: Option<String>, 320} 321 322pub struct Context<'a> { 323 pub name: &'a Name, 324 pub node: &'a mut Node, 325 pub hive_location: Arc<HiveLocation>, 326 pub modifiers: SubCommandModifiers, 327 pub state: StepState, 328 pub should_quit: Arc<AtomicBool>, 329 pub objective: Objective, 330} 331 332#[enum_dispatch(ExecuteStep)] 333#[derive(Debug, PartialEq)] 334enum Step { 335 Ping, 336 PushKeyAgent, 337 Keys, 338 Evaluate, 339 PushEvaluatedOutput, 340 Build, 341 PushBuildOutput, 342 SwitchToConfiguration, 343 CleanUp, 344} 345 346impl Display for Step { 347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 348 match self { 349 Self::Ping(step) => step.fmt(f), 350 Self::PushKeyAgent(step) => step.fmt(f), 351 Self::Keys(step) => step.fmt(f), 352 Self::Evaluate(step) => step.fmt(f), 353 Self::PushEvaluatedOutput(step) => step.fmt(f), 354 Self::Build(step) => step.fmt(f), 355 Self::PushBuildOutput(step) => step.fmt(f), 356 Self::SwitchToConfiguration(step) => step.fmt(f), 357 Self::CleanUp(step) => step.fmt(f), 358 } 359 } 360} 361 362pub struct GoalExecutor<'a> { 363 steps: Vec<Step>, 364 context: Context<'a>, 365} 366 367/// returns Err if the application should shut down. 368fn app_shutdown_guard(context: &Context) -> Result<(), HiveLibError> { 369 if context 370 .should_quit 371 .load(std::sync::atomic::Ordering::Relaxed) 372 { 373 return Err(HiveLibError::Sigint); 374 } 375 376 Ok(()) 377} 378 379impl<'a> GoalExecutor<'a> { 380 #[must_use] 381 pub fn new(context: Context<'a>) -> Self { 382 Self { 383 steps: vec![ 384 Step::Ping(Ping), 385 Step::PushKeyAgent(PushKeyAgent), 386 Step::Keys(Keys { 387 filter: UploadKeyAt::NoFilter, 388 }), 389 Step::Keys(Keys { 390 filter: UploadKeyAt::PreActivation, 391 }), 392 Step::Evaluate(super::steps::evaluate::Evaluate), 393 Step::PushEvaluatedOutput(super::steps::push::PushEvaluatedOutput), 394 Step::Build(super::steps::build::Build), 395 Step::PushBuildOutput(super::steps::push::PushBuildOutput), 396 Step::SwitchToConfiguration(SwitchToConfiguration), 397 Step::Keys(Keys { 398 filter: UploadKeyAt::PostActivation, 399 }), 400 ], 401 context, 402 } 403 } 404 405 #[instrument(skip_all, name = "eval")] 406 async fn evaluate_task( 407 tx: oneshot::Sender<Result<Derivation, HiveLibError>>, 408 hive_location: Arc<HiveLocation>, 409 name: Name, 410 modifiers: SubCommandModifiers, 411 ) { 412 let output = 413 evaluate_hive_attribute(&hive_location, &EvalGoal::GetTopLevel(&name), modifiers) 414 .await 415 .map(|output| { 416 serde_json::from_str::<Derivation>(&output).expect("failed to parse derivation") 417 }); 418 419 debug!(output = ?output, done = true); 420 421 let _ = tx.send(output); 422 } 423 424 #[instrument(skip_all, fields(node = %self.context.name))] 425 pub async fn execute(mut self) -> Result<(), HiveLibError> { 426 app_shutdown_guard(&self.context)?; 427 428 let (tx, rx) = oneshot::channel(); 429 self.context.state.evaluation_rx = Some(rx); 430 431 // The name of this span should never be changed without updating 432 // `wire/cli/tracing_setup.rs` 433 debug_assert_matches!(Span::current().metadata().unwrap().name(), "execute"); 434 // This span should always have a `node` field by the same file 435 debug_assert!( 436 Span::current() 437 .metadata() 438 .unwrap() 439 .fields() 440 .field("node") 441 .is_some() 442 ); 443 444 let spawn_evaluator = match self.context.objective { 445 Objective::Apply(apply_objective) => !matches!(apply_objective.goal, Goal::Keys), 446 Objective::BuildLocally => true, 447 }; 448 449 if spawn_evaluator { 450 tokio::spawn( 451 GoalExecutor::evaluate_task( 452 tx, 453 self.context.hive_location.clone(), 454 self.context.name.clone(), 455 self.context.modifiers, 456 ) 457 .in_current_span(), 458 ); 459 } 460 461 let steps = self 462 .steps 463 .iter() 464 .filter(|step| step.should_execute(&self.context)) 465 .inspect(|step| { 466 trace!("Will execute step `{step}` for {}", self.context.name); 467 }) 468 .collect::<Vec<_>>(); 469 let length = steps.len(); 470 471 for (position, step) in steps.iter().enumerate() { 472 app_shutdown_guard(&self.context)?; 473 474 event!( 475 Level::INFO, 476 step = step.to_string(), 477 progress = format!("{}/{length}", position + 1) 478 ); 479 480 STATUS 481 .lock() 482 .set_node_step(self.context.name, step.to_string()); 483 484 if let Err(err) = step.execute(&mut self.context).await.inspect_err(|_| { 485 error!("Failed to execute `{step}`"); 486 }) { 487 // discard error from cleanup 488 let _ = CleanUp.execute(&mut self.context).await; 489 490 if let Objective::Apply(apply_objective) = self.context.objective 491 && matches!(step, Step::Ping(..)) 492 && matches!( 493 apply_objective.handle_unreachable, 494 HandleUnreachable::Ignore, 495 ) 496 { 497 return Ok(()); 498 } 499 500 STATUS.lock().mark_node_failed(self.context.name); 501 502 return Err(err); 503 } 504 } 505 506 STATUS.lock().mark_node_succeeded(self.context.name); 507 508 Ok(()) 509 } 510} 511 512#[cfg(test)] 513mod tests { 514 use rand::distr::Alphabetic; 515 516 use super::*; 517 use crate::{ 518 function_name, get_test_path, 519 hive::{Hive, get_hive_location}, 520 location, 521 }; 522 use std::{assert_matches::assert_matches, path::PathBuf}; 523 use std::{collections::HashMap, env}; 524 525 fn get_steps(goal_executor: GoalExecutor) -> std::vec::Vec<Step> { 526 goal_executor 527 .steps 528 .into_iter() 529 .filter(|step| step.should_execute(&goal_executor.context)) 530 .collect::<Vec<_>>() 531 } 532 533 #[tokio::test] 534 #[cfg_attr(feature = "no_web_tests", ignore)] 535 async fn default_values_match() { 536 let mut path = get_test_path!(); 537 538 let location = 539 get_hive_location(path.display().to_string(), SubCommandModifiers::default()) 540 .await 541 .unwrap(); 542 let hive = Hive::new_from_path(&location, None, SubCommandModifiers::default()) 543 .await 544 .unwrap(); 545 546 let node = Node::default(); 547 548 let mut nodes = HashMap::new(); 549 nodes.insert(Name("NAME".into()), node); 550 551 path.push("hive.nix"); 552 553 assert_eq!( 554 hive, 555 Hive { 556 nodes, 557 schema: Hive::SCHEMA_VERSION 558 } 559 ); 560 } 561 562 #[tokio::test] 563 async fn order_build_locally() { 564 let location = location!(get_test_path!()); 565 let mut node = Node { 566 build_remotely: false, 567 ..Default::default() 568 }; 569 let name = &Name(function_name!().into()); 570 let executor = GoalExecutor::new(Context::create_test_context(location, name, &mut node)); 571 let steps = get_steps(executor); 572 573 assert_eq!( 574 steps, 575 vec![ 576 Ping.into(), 577 PushKeyAgent.into(), 578 Keys { 579 filter: UploadKeyAt::PreActivation 580 } 581 .into(), 582 crate::hive::steps::evaluate::Evaluate.into(), 583 crate::hive::steps::build::Build.into(), 584 crate::hive::steps::push::PushBuildOutput.into(), 585 SwitchToConfiguration.into(), 586 Keys { 587 filter: UploadKeyAt::PostActivation 588 } 589 .into(), 590 ] 591 ); 592 } 593 594 #[tokio::test] 595 async fn order_keys_only() { 596 let location = location!(get_test_path!()); 597 let mut node = Node::default(); 598 let name = &Name(function_name!().into()); 599 let mut context = Context::create_test_context(location, name, &mut node); 600 601 let Objective::Apply(ref mut apply_objective) = context.objective else { 602 unreachable!() 603 }; 604 605 apply_objective.goal = Goal::Keys; 606 607 let executor = GoalExecutor::new(context); 608 let steps = get_steps(executor); 609 610 assert_eq!( 611 steps, 612 vec![ 613 Ping.into(), 614 PushKeyAgent.into(), 615 Keys { 616 filter: UploadKeyAt::NoFilter 617 } 618 .into(), 619 ] 620 ); 621 } 622 623 #[tokio::test] 624 async fn order_build() { 625 let location = location!(get_test_path!()); 626 let mut node = Node::default(); 627 let name = &Name(function_name!().into()); 628 let mut context = Context::create_test_context(location, name, &mut node); 629 630 let Objective::Apply(ref mut apply_objective) = context.objective else { 631 unreachable!() 632 }; 633 apply_objective.goal = Goal::Build; 634 635 let executor = GoalExecutor::new(context); 636 let steps = get_steps(executor); 637 638 assert_eq!( 639 steps, 640 vec![ 641 Ping.into(), 642 crate::hive::steps::evaluate::Evaluate.into(), 643 crate::hive::steps::build::Build.into(), 644 crate::hive::steps::push::PushBuildOutput.into(), 645 ] 646 ); 647 } 648 649 #[tokio::test] 650 async fn order_push_only() { 651 let location = location!(get_test_path!()); 652 let mut node = Node::default(); 653 let name = &Name(function_name!().into()); 654 let mut context = Context::create_test_context(location, name, &mut node); 655 656 let Objective::Apply(ref mut apply_objective) = context.objective else { 657 unreachable!() 658 }; 659 apply_objective.goal = Goal::Push; 660 661 let executor = GoalExecutor::new(context); 662 let steps = get_steps(executor); 663 664 assert_eq!( 665 steps, 666 vec![ 667 Ping.into(), 668 crate::hive::steps::evaluate::Evaluate.into(), 669 crate::hive::steps::push::PushEvaluatedOutput.into(), 670 ] 671 ); 672 } 673 674 #[tokio::test] 675 async fn order_remote_build() { 676 let location = location!(get_test_path!()); 677 let mut node = Node { 678 build_remotely: true, 679 ..Default::default() 680 }; 681 682 let name = &Name(function_name!().into()); 683 let executor = GoalExecutor::new(Context::create_test_context(location, name, &mut node)); 684 let steps = get_steps(executor); 685 686 assert_eq!( 687 steps, 688 vec![ 689 Ping.into(), 690 PushKeyAgent.into(), 691 Keys { 692 filter: UploadKeyAt::PreActivation 693 } 694 .into(), 695 crate::hive::steps::evaluate::Evaluate.into(), 696 crate::hive::steps::push::PushEvaluatedOutput.into(), 697 crate::hive::steps::build::Build.into(), 698 SwitchToConfiguration.into(), 699 Keys { 700 filter: UploadKeyAt::PostActivation 701 } 702 .into(), 703 ] 704 ); 705 } 706 707 #[tokio::test] 708 async fn order_nokeys() { 709 let location = location!(get_test_path!()); 710 let mut node = Node::default(); 711 712 let name = &Name(function_name!().into()); 713 let mut context = Context::create_test_context(location, name, &mut node); 714 715 let Objective::Apply(ref mut apply_objective) = context.objective else { 716 unreachable!() 717 }; 718 apply_objective.no_keys = true; 719 720 let executor = GoalExecutor::new(context); 721 let steps = get_steps(executor); 722 723 assert_eq!( 724 steps, 725 vec![ 726 Ping.into(), 727 crate::hive::steps::evaluate::Evaluate.into(), 728 crate::hive::steps::build::Build.into(), 729 crate::hive::steps::push::PushBuildOutput.into(), 730 SwitchToConfiguration.into(), 731 ] 732 ); 733 } 734 735 #[tokio::test] 736 async fn order_should_apply_locally() { 737 let location = location!(get_test_path!()); 738 let mut node = Node::default(); 739 740 let name = &Name(function_name!().into()); 741 let mut context = Context::create_test_context(location, name, &mut node); 742 743 let Objective::Apply(ref mut apply_objective) = context.objective else { 744 unreachable!() 745 }; 746 apply_objective.no_keys = true; 747 apply_objective.should_apply_locally = true; 748 749 let executor = GoalExecutor::new(context); 750 let steps = get_steps(executor); 751 752 assert_eq!( 753 steps, 754 vec![ 755 crate::hive::steps::evaluate::Evaluate.into(), 756 crate::hive::steps::build::Build.into(), 757 SwitchToConfiguration.into(), 758 ] 759 ); 760 } 761 762 #[tokio::test] 763 async fn order_build_only() { 764 let location = location!(get_test_path!()); 765 let mut node = Node::default(); 766 767 let name = &Name(function_name!().into()); 768 let mut context = Context::create_test_context(location, name, &mut node); 769 770 context.objective = Objective::BuildLocally; 771 772 let executor = GoalExecutor::new(context); 773 let steps = get_steps(executor); 774 775 assert_eq!( 776 steps, 777 vec![ 778 crate::hive::steps::evaluate::Evaluate.into(), 779 crate::hive::steps::build::Build.into() 780 ] 781 ); 782 } 783 784 #[test] 785 fn target_fails_increments() { 786 let mut target = Target::from_host("localhost"); 787 788 assert_eq!(target.current_host, 0); 789 790 for i in 0..100 { 791 target.host_failed(); 792 assert_eq!(target.current_host, i + 1); 793 } 794 } 795 796 #[test] 797 fn get_preferred_host_fails() { 798 let mut target = Target { 799 hosts: vec![ 800 "un.reachable.1".into(), 801 "un.reachable.2".into(), 802 "un.reachable.3".into(), 803 "un.reachable.4".into(), 804 "un.reachable.5".into(), 805 ], 806 ..Default::default() 807 }; 808 809 assert_ne!( 810 target.get_preferred_host().unwrap().to_string(), 811 "un.reachable.5" 812 ); 813 814 for i in 1..=5 { 815 assert_eq!( 816 target.get_preferred_host().unwrap().to_string(), 817 format!("un.reachable.{i}") 818 ); 819 target.host_failed(); 820 } 821 822 for _ in 0..5 { 823 assert_matches!( 824 target.get_preferred_host(), 825 Err(HiveLibError::NetworkError(NetworkError::HostsExhausted)) 826 ); 827 } 828 } 829 830 #[test] 831 fn test_ssh_opts() { 832 let target = Target::from_host("hello-world"); 833 let subcommand_modifiers = SubCommandModifiers { 834 non_interactive: false, 835 ..Default::default() 836 }; 837 let tmp = format!( 838 "/tmp/{}", 839 rand::distr::SampleString::sample_string(&Alphabetic, &mut rand::rng(), 10) 840 ); 841 842 std::fs::create_dir(&tmp).unwrap(); 843 844 unsafe { env::set_var("XDG_RUNTIME_DIR", &tmp) } 845 846 let args = [ 847 "-l".to_string(), 848 target.user.to_string(), 849 "-p".to_string(), 850 target.port.to_string(), 851 "-o".to_string(), 852 "StrictHostKeyChecking=accept-new".to_string(), 853 "-o".to_string(), 854 "PasswordAuthentication=no".to_string(), 855 "-o".to_string(), 856 "KbdInteractiveAuthentication=no".to_string(), 857 ]; 858 859 assert_eq!( 860 target 861 .create_ssh_args(subcommand_modifiers, false, false) 862 .unwrap(), 863 args 864 ); 865 assert_eq!( 866 target.create_ssh_opts(subcommand_modifiers, false).unwrap(), 867 args.join(" ") 868 ); 869 870 assert_eq!( 871 target 872 .create_ssh_args(subcommand_modifiers, false, true) 873 .unwrap(), 874 [ 875 "-l".to_string(), 876 target.user.to_string(), 877 "-p".to_string(), 878 target.port.to_string(), 879 "-o".to_string(), 880 "StrictHostKeyChecking=accept-new".to_string(), 881 "-o".to_string(), 882 "PasswordAuthentication=no".to_string(), 883 "-o".to_string(), 884 "KbdInteractiveAuthentication=no".to_string(), 885 ] 886 ); 887 888 assert_eq!( 889 target 890 .create_ssh_args(subcommand_modifiers, true, true) 891 .unwrap(), 892 [ 893 "-l".to_string(), 894 target.user.to_string(), 895 "-p".to_string(), 896 target.port.to_string(), 897 "-o".to_string(), 898 "StrictHostKeyChecking=accept-new".to_string(), 899 "-o".to_string(), 900 "PasswordAuthentication=no".to_string(), 901 "-o".to_string(), 902 "KbdInteractiveAuthentication=no".to_string(), 903 ] 904 ); 905 906 // forced non interactive is the same as --non-interactive 907 assert_eq!( 908 target 909 .create_ssh_args(subcommand_modifiers, true, false) 910 .unwrap(), 911 target 912 .create_ssh_args( 913 SubCommandModifiers { 914 non_interactive: true, 915 ..Default::default() 916 }, 917 false, 918 false 919 ) 920 .unwrap() 921 ); 922 } 923 924 #[tokio::test] 925 async fn context_quits_sigint() { 926 let location = location!(get_test_path!()); 927 let mut node = Node::default(); 928 929 let name = &Name(function_name!().into()); 930 let context = Context::create_test_context(location, name, &mut node); 931 context 932 .should_quit 933 .store(true, std::sync::atomic::Ordering::Relaxed); 934 let executor = GoalExecutor::new(context); 935 let status = executor.execute().await; 936 937 assert_matches!(status, Err(HiveLibError::Sigint)); 938 } 939}