ALPHA: wire is a tool to deploy nixos systems
wire.althaea.zone/
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}