guest_cli/
list.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::platform::PlatformServices;
6use anyhow::{anyhow, Error};
7use fidl_fuchsia_virtualization::{GuestManagerProxy, GuestStatus};
8use guest_cli_args as arguments;
9use prettytable::format::consts::FORMAT_CLEAN;
10use prettytable::{cell, row, Table};
11use std::fmt;
12
13fn guest_status_to_string(status: GuestStatus) -> &'static str {
14    match status {
15        GuestStatus::NotStarted => "Not started",
16        GuestStatus::Starting => "Starting",
17        GuestStatus::Running => "Running",
18        GuestStatus::Stopping => "Stopping",
19        GuestStatus::Stopped => "Stopped",
20        GuestStatus::VmmUnexpectedTermination => "VMM Unexpectedly Terminated",
21    }
22}
23
24fn uptime_to_string(uptime_nanos: Option<i64>) -> String {
25    match uptime_nanos {
26        Some(uptime) => {
27            if uptime < 0 {
28                "Invalid negative uptime!".to_string()
29            } else {
30                let uptime = std::time::Duration::from_nanos(uptime as u64);
31                let seconds = uptime.as_secs() % 60;
32                let minutes = (uptime.as_secs() / 60) % 60;
33                let hours = uptime.as_secs() / 3600;
34                format!("{:0>2}:{:0>2}:{:0>2} HH:MM:SS", hours, minutes, seconds)
35            }
36        }
37        None => "--:--:-- HH:MM:SS".to_string(),
38    }
39}
40
41#[derive(Default, serde::Serialize, serde::Deserialize)]
42pub struct GuestDetails {
43    pub package_url: String,
44    pub status: String,
45    pub uptime_nanos: i64,
46    pub stop_reason: Option<String>,
47    pub cpu_count: Option<u8>,
48    pub memory_bytes: Option<u64>,
49    pub device_counts: Vec<(String, u32)>,
50    pub problems: Vec<String>,
51}
52
53impl fmt::Display for GuestDetails {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        let mut table = Table::new();
56        table.set_format(*FORMAT_CLEAN);
57        table.add_row(row!["Guest package:", self.package_url]);
58        table.add_row(row!["Guest status:", self.status]);
59        table.add_row(row!["Guest uptime:", uptime_to_string(Some(self.uptime_nanos))]);
60
61        if self.status == "Not started" {
62            write!(f, "{}", table)?;
63            return Ok(());
64        }
65
66        table.add_empty_row();
67
68        if let Some(stop_reason) = &self.stop_reason {
69            table.add_row(row!["Stop reason:", stop_reason]);
70        }
71        if let Some(cpu_count) = self.cpu_count {
72            table.add_row(row!["CPU count:", cpu_count]);
73        }
74        if let Some(memory_bytes) = self.memory_bytes {
75            let gib = f64::trunc(memory_bytes as f64 / (1 << 30) as f64) * 100.0 / 100.0;
76            table.add_row(row!["Guest memory:", format!("{} GiB ({} bytes)", gib, memory_bytes)]);
77        }
78
79        if self.cpu_count.is_some() && self.memory_bytes.is_some() {
80            table.add_empty_row();
81
82            let mut active = Table::new();
83            active.set_format(*FORMAT_CLEAN);
84            let mut inactive = Table::new();
85            inactive.set_format(*FORMAT_CLEAN);
86
87            for (device, count) in self.device_counts.iter() {
88                if *count == 0 {
89                    inactive.add_row(row![device]);
90                } else if *count == 1 {
91                    active.add_row(row![device]);
92                } else {
93                    active.add_row(row![format!("{} ({} devices)", device, *count)]);
94                }
95            }
96
97            if active.len() == 0 {
98                active.add_row(row!["None"]);
99            }
100
101            if inactive.len() == 0 {
102                inactive.add_row(row!["None"]);
103            }
104
105            table.add_row(row!["Active devices:", active]);
106            table.add_empty_row();
107            table.add_row(row!["Inactive devices:", inactive]);
108        }
109        write!(f, "{}", table)?;
110
111        if !self.problems.is_empty() {
112            let mut problem_table = Table::new();
113            problem_table.set_format(*FORMAT_CLEAN);
114            problem_table.add_empty_row();
115            problem_table.add_row(row![
116                format!(
117                    "{} problem{} detected:",
118                    self.problems.len(),
119                    if self.problems.len() > 1 { "s" } else { "" }
120                ),
121                " "
122            ]);
123            for problem in self.problems.iter() {
124                problem_table.add_row(row![format!("* {}", problem), " "]);
125            }
126            write!(f, "{}", problem_table)?;
127        }
128        return Ok(());
129    }
130}
131
132async fn get_detailed_information(
133    guest_type: arguments::GuestType,
134    manager: GuestManagerProxy,
135) -> Result<GuestDetails, Error> {
136    let guest_info = manager.get_info().await;
137    if let Err(_) = guest_info {
138        return Err(anyhow!("Failed to query guest information: {}", guest_type.to_string()));
139    }
140    let guest_info = guest_info.unwrap();
141    let guest_status = guest_info.guest_status.expect("guest status should always be set");
142
143    let mut details: GuestDetails = Default::default();
144    details.package_url = guest_type.package_url().to_string();
145    details.status = guest_status_to_string(guest_status).to_string();
146    details.uptime_nanos = guest_info.uptime.unwrap_or(0);
147
148    if guest_status == GuestStatus::NotStarted {
149        return Ok(details);
150    }
151
152    if guest_status == GuestStatus::Stopped {
153        let stop_reason = guest_info
154            .stop_error
155            .map_or_else(|| "Clean shutdown".to_string(), |err| format!("{:?}", err));
156        details.stop_reason = Some(stop_reason);
157    } else {
158        if let Some(config) = guest_info.guest_descriptor {
159            details.cpu_count = config.num_cpus;
160            details.memory_bytes = config.guest_memory;
161
162            let add_to_table =
163                |device: &str, is_active: Option<bool>, table: &mut Vec<(String, u32)>| -> () {
164                    let count = is_active.map(|b| b as u32).unwrap_or(0);
165                    table.push((device.to_string(), count));
166                };
167
168            add_to_table("balloon", config.balloon, &mut details.device_counts);
169            add_to_table("console", config.console, &mut details.device_counts);
170            add_to_table("gpu", config.gpu, &mut details.device_counts);
171            add_to_table("rng", config.rng, &mut details.device_counts);
172            add_to_table("vsock", config.vsock, &mut details.device_counts);
173            add_to_table("sound", config.sound, &mut details.device_counts);
174            let networks = config.networks.map(|v| v.len()).unwrap_or(0);
175            details.device_counts.push(("network".to_string(), networks as u32));
176        }
177    }
178
179    if let Some(problems) = guest_info.detected_problems {
180        details.problems = problems;
181    }
182    Ok(details)
183}
184
185#[derive(serde::Serialize, serde::Deserialize)]
186pub struct GuestOverview {
187    name: String,
188    status: String,
189    uptime_nanos: Option<i64>,
190}
191
192#[derive(serde::Serialize, serde::Deserialize)]
193pub struct GuestSummary {
194    guests: Vec<GuestOverview>,
195}
196
197impl fmt::Display for GuestSummary {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        let mut table = Table::new();
200        table.set_titles(row!["Guest", "Status", "Uptime"]);
201        for guest in &self.guests {
202            table.add_row(row![guest.name, guest.status, uptime_to_string(guest.uptime_nanos)]);
203        }
204        write!(f, "{}", table)
205    }
206}
207
208async fn get_environment_summary(
209    managers: Vec<(String, GuestManagerProxy)>,
210) -> Result<GuestSummary, Error> {
211    let mut summary = GuestSummary { guests: Vec::new() };
212    for (name, manager) in managers {
213        match manager.get_info().await {
214            Ok(guest_info) => summary.guests.push(GuestOverview {
215                name,
216                status: guest_status_to_string(
217                    guest_info.guest_status.expect("guest status should always be set"),
218                )
219                .to_string(),
220                uptime_nanos: guest_info.uptime,
221            }),
222            Err(_) => summary.guests.push(GuestOverview {
223                name,
224                status: "Unavailable".to_string(),
225                uptime_nanos: None,
226            }),
227        }
228    }
229    Ok(summary)
230}
231
232#[derive(serde::Serialize, serde::Deserialize)]
233pub enum GuestList {
234    Summary(GuestSummary),
235    Details(GuestDetails),
236}
237
238impl fmt::Display for GuestList {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        match self {
241            GuestList::Summary(summary) => write!(f, "{}", summary)?,
242            GuestList::Details(details) => write!(f, "{}", details)?,
243        }
244        Ok(())
245    }
246}
247
248pub async fn handle_list<P: PlatformServices>(
249    services: &P,
250    args: &arguments::list_args::ListArgs,
251) -> Result<GuestList, Error> {
252    match args.guest_type {
253        Some(guest_type) => {
254            let manager = services.connect_to_manager(guest_type).await?;
255            Ok(GuestList::Details(get_detailed_information(guest_type, manager).await?))
256        }
257        None => {
258            let mut managers = Vec::new();
259            for guest_type in arguments::GuestType::all_guests() {
260                let manager = services.connect_to_manager(guest_type).await?;
261                managers.push((guest_type.to_string(), manager));
262            }
263            Ok(GuestList::Summary(get_environment_summary(managers).await?))
264        }
265    }
266}
267
268#[cfg(test)]
269mod test {
270    use super::*;
271    use fidl::endpoints::create_proxy_and_stream;
272    use fidl_fuchsia_net::MacAddress;
273    use fidl_fuchsia_virtualization::{
274        GuestDescriptor, GuestError, GuestInfo, GuestManagerMarker, NetSpec,
275    };
276    use fuchsia_async as fasync;
277    use futures::StreamExt;
278
279    fn serve_mock_manager(response: Option<GuestInfo>) -> GuestManagerProxy {
280        let (proxy, mut stream) = create_proxy_and_stream::<GuestManagerMarker>();
281        fasync::Task::local(async move {
282            let responder = stream
283                .next()
284                .await
285                .expect("mock manager expected a request")
286                .unwrap()
287                .into_get_info()
288                .expect("unexpected call to mock manager");
289
290            if let Some(guest_info) = response {
291                responder.send(&guest_info).expect("failed to send mock response");
292            } else {
293                drop(responder);
294            }
295        })
296        .detach();
297
298        proxy
299    }
300
301    #[fasync::run_until_stalled(test)]
302    async fn negative_uptime() {
303        // Note that a negative duration should never happen as we're measuring duration
304        // monotonically from a single process.
305        let duration = -5;
306        let actual = uptime_to_string(Some(duration));
307        let expected = "Invalid negative uptime!";
308
309        assert_eq!(actual, expected);
310    }
311
312    #[fasync::run_until_stalled(test)]
313    async fn very_large_uptime() {
314        let hours = std::time::Duration::from_secs(123 * 60 * 60);
315        let minutes = std::time::Duration::from_secs(45 * 60);
316        let seconds = std::time::Duration::from_secs(54);
317        let duration = hours + minutes + seconds;
318
319        let actual = uptime_to_string(Some(duration.as_nanos() as i64));
320        let expected = "123:45:54 HH:MM:SS";
321
322        assert_eq!(actual, expected);
323    }
324
325    #[fasync::run_until_stalled(test)]
326    async fn summarize_existing_managers() {
327        let managers = vec![
328            ("zircon".to_string(), serve_mock_manager(None)),
329            ("termina".to_string(), serve_mock_manager(None)),
330            (
331                "debian".to_string(),
332                serve_mock_manager(Some(GuestInfo {
333                    guest_status: Some(GuestStatus::Running),
334                    uptime: Some(std::time::Duration::from_secs(123).as_nanos() as i64),
335                    ..Default::default()
336                })),
337            ),
338        ];
339
340        let actual = format!("{}", get_environment_summary(managers).await.unwrap());
341        let expected = concat!(
342            "+---------+-------------+-------------------+\n",
343            "| Guest   | Status      | Uptime            |\n",
344            "+=========+=============+===================+\n",
345            "| zircon  | Unavailable | --:--:-- HH:MM:SS |\n",
346            "+---------+-------------+-------------------+\n",
347            "| termina | Unavailable | --:--:-- HH:MM:SS |\n",
348            "+---------+-------------+-------------------+\n",
349            "| debian  | Running     | 00:02:03 HH:MM:SS |\n",
350            "+---------+-------------+-------------------+\n"
351        );
352
353        assert_eq!(actual, expected);
354    }
355
356    #[fasync::run_until_stalled(test)]
357    async fn get_detailed_info_stopped_clean() {
358        let manager = serve_mock_manager(Some(GuestInfo {
359            guest_status: Some(GuestStatus::Stopped),
360            uptime: Some(std::time::Duration::from_secs(5).as_nanos() as i64),
361            ..Default::default()
362        }));
363
364        let actual = format!(
365            "{}",
366            get_detailed_information(arguments::GuestType::Termina, manager).await.unwrap()
367        );
368        let expected = concat!(
369            " Guest package:  fuchsia-pkg://fuchsia.com/termina_guest#meta/termina_guest.cm \n",
370            " Guest status:   Stopped \n",
371            " Guest uptime:   00:00:05 HH:MM:SS \n",
372            "                  \n",
373            " Stop reason:    Clean shutdown \n",
374        );
375
376        assert_eq!(actual, expected);
377    }
378
379    #[fasync::run_until_stalled(test)]
380    async fn get_detailed_info_stopped_guest_failure() {
381        let manager = serve_mock_manager(Some(GuestInfo {
382            guest_status: Some(GuestStatus::Stopped),
383            uptime: Some(std::time::Duration::from_secs(65).as_nanos() as i64),
384            stop_error: Some(GuestError::InternalError),
385            ..Default::default()
386        }));
387
388        let actual = format!(
389            "{}",
390            get_detailed_information(arguments::GuestType::Zircon, manager).await.unwrap()
391        );
392        let expected = concat!(
393            " Guest package:  fuchsia-pkg://fuchsia.com/zircon_guest#meta/zircon_guest.cm \n",
394            " Guest status:   Stopped \n",
395            " Guest uptime:   00:01:05 HH:MM:SS \n",
396            "                  \n",
397            " Stop reason:    InternalError \n",
398        );
399
400        assert_eq!(actual, expected);
401    }
402
403    #[fasync::run_until_stalled(test)]
404    async fn get_detailed_info_running_guest() {
405        let manager = serve_mock_manager(Some(GuestInfo {
406            guest_status: Some(GuestStatus::Running),
407            uptime: Some(std::time::Duration::from_secs(125 * 60).as_nanos() as i64),
408            guest_descriptor: Some(GuestDescriptor {
409                num_cpus: Some(4),
410                guest_memory: Some(1073741824),
411                networks: Some(vec![
412                    NetSpec {
413                        mac_address: MacAddress { octets: [0u8; 6] },
414                        enable_bridge: true,
415                    };
416                    2
417                ]),
418                balloon: Some(true),
419                console: Some(true),
420                gpu: Some(false),
421                rng: Some(true),
422                vsock: Some(true),
423                sound: Some(false),
424                ..Default::default()
425            }),
426            detected_problems: Some(vec![
427                "Host is experiencing heavy memory pressure".to_string(),
428                "No bridge between guest and host network interaces".to_string(),
429            ]),
430            ..Default::default()
431        }));
432
433        let actual = format!(
434            "{}",
435            get_detailed_information(arguments::GuestType::Debian, manager).await.unwrap()
436        );
437        let expected = concat!(
438            " Guest package:     fuchsia-pkg://fuchsia.com/debian_guest#meta/debian_guest.cm \n",
439            " Guest status:      Running \n",
440            " Guest uptime:      02:05:00 HH:MM:SS \n",
441            "                     \n",
442            " CPU count:         4 \n",
443            " Guest memory:      1 GiB (1073741824 bytes) \n",
444            "                     \n",
445            " Active devices:     balloon  \n",
446            "                     console  \n",
447            "                     rng  \n",
448            "                     vsock  \n",
449            "                     network (2 devices)  \n",
450            "                     \n",
451            " Inactive devices:   gpu  \n",
452            "                     sound  \n",
453            "                                                        \n",
454            " 2 problems detected:                                    \n",
455            " * Host is experiencing heavy memory pressure            \n",
456            " * No bridge between guest and host network interaces    \n",
457        );
458
459        assert_eq!(actual, expected);
460    }
461}