souschef/chef/
node.rs

1use crate::{client, config::KnifeConfig};
2use colored::Colorize;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::{error::Error, process::Stdio};
6
7#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
8pub struct ChefNode {
9    pub automatic: ChefNodeAutomatic,
10    pub chef_environment: String,
11    pub name: String,
12    pub run_list: Vec<String>,
13}
14
15/// Collects relevant fields from the `automatic` key from Chef API response
16#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
17pub struct ChefNodeAutomatic {
18    #[serde(default)]
19    pub ipaddress: String,
20
21    #[serde(default)]
22    pub macaddress: String,
23
24    #[serde(default)]
25    pub hostname: String,
26
27    #[serde(default)]
28    pub os: String,
29
30    #[serde(default)]
31    pub os_version: String,
32
33    #[serde(default)]
34    pub machinename: String,
35
36    #[serde(default)]
37    pub fqdn: String,
38
39    #[serde(default)]
40    pub platform: String,
41
42    #[serde(default)]
43    pub platform_family: String,
44
45    #[serde(default)]
46    pub platform_version: String,
47
48    #[serde(default)]
49    pub recipes: Vec<String>,
50
51    #[serde(default)]
52    pub roles: Vec<String>,
53}
54
55impl ChefNode {
56    pub fn display(&self) {
57        println!("{}:        {}", "Node name".green().bold(), self.name);
58        println!(
59            "{}:       {}",
60            "IP Address".green().bold(),
61            self.automatic.ipaddress
62        );
63        println!(
64            "{}: {}",
65            "Chef Environment".green().bold(),
66            self.chef_environment
67        );
68        println!(
69            "{}:            {}",
70            "Roles".green().bold(),
71            self.automatic.roles.join(", ")
72        );
73        println!(
74            "{}:         {}",
75            "Run List".green().bold(),
76            self.run_list.join(", ")
77        );
78
79        println!(
80            "{}:          {}",
81            "Recipes".green().bold(),
82            self.automatic.recipes.join(", ")
83        );
84
85        println!(
86            "{}:          {}",
87            "Platform".green().bold(),
88            self.automatic.platform_family
89        );
90
91        println!(
92            "{}:  {}",
93            "Platform version".green().bold(),
94            self.automatic.platform_version
95        );
96        println!("\n");
97    }
98}
99
100pub async fn node_list(config: &KnifeConfig) -> Result<(), Box<dyn Error>> {
101    let request_path = format!("/organizations/{}/nodes", config.organization);
102
103    match client::request::get(config, &request_path, "").await {
104        Ok(n) => {
105            let nodes: Value = serde_json::from_str(&n.body)?;
106
107            for (k, _) in nodes.as_object().unwrap() {
108                println!("{k}");
109            }
110            Ok(())
111        }
112        Err(e) => Err(format!("node list: {e}").into()),
113    }
114}
115
116// node_show queries Chef server to display information about the node object
117pub async fn node_show(config: &KnifeConfig, node_id: &str) -> Result<(), Box<dyn Error>> {
118    let request_path = format!("/organizations/{}/nodes/{}", config.organization, node_id);
119
120    match client::request::get(config, &request_path, "").await {
121        Ok(n) => {
122            match n.status {
123                404 => {
124                    println!("Node not found: {}", node_id);
125                }
126                200 => {
127                    let node: ChefNode = serde_json::from_str(&n.body)?;
128                    node.display();
129                }
130                _ => {
131                    println!("Unkown status code: {}", n.status)
132                }
133            }
134
135            Ok(())
136        }
137        Err(e) => Err(format!("node show: {e}").into()),
138    }
139}
140
141/// node_ssh - handles the CLI call for opening a SSH connection
142pub async fn node_ssh(
143    config: &KnifeConfig,
144    node_id: &str,
145    user: Option<String>,
146) -> Result<(), Box<dyn Error>> {
147    let request_path: String = format!("/organizations/{}/nodes/{}", config.organization, node_id);
148
149    match client::request::get(config, &request_path, "").await {
150        Ok(n) => {
151            match n.status {
152                200 => {
153                    let node: ChefNode = serde_json::from_str(&n.body)?;
154
155                    open_ssh_connection(node.automatic.ipaddress, user).await?;
156                }
157                404 => {
158                    println!("Node not found: {}", node_id);
159                }
160                _ => {
161                    println!("Unknown status code: {}", n.status)
162                }
163            }
164            Ok(())
165        }
166        Err(e) => Err(format!("node show: {}", e).into()),
167    }
168}
169
170/// open_ssh_connection - Opens a SSH client and establishes and SSH connection
171/// Wraps around the ~/.ssh/config that uses the set username in
172async fn open_ssh_connection(
173    node_ipaddress: String,
174    user: Option<String>,
175) -> Result<(), Box<dyn Error>> {
176    let ssh_command = match user {
177        Some(u) => format!("{}@{}", u, node_ipaddress),
178        None => node_ipaddress,
179    };
180
181    let mut child = std::process::Command::new("ssh")
182        .arg(ssh_command)
183        .stdin(Stdio::inherit())
184        .stdout(Stdio::inherit())
185        .stderr(Stdio::inherit())
186        .spawn()
187        .unwrap();
188
189    let status = child.wait().unwrap();
190    println!("SSH process existed with: {}", status);
191    Ok(())
192}