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("wayland", config.wayland, &mut details.device_counts);
169            add_to_table("balloon", config.balloon, &mut details.device_counts);
170            add_to_table("console", config.console, &mut details.device_counts);
171            add_to_table("gpu", config.gpu, &mut details.device_counts);
172            add_to_table("rng", config.rng, &mut details.device_counts);
173            add_to_table("vsock", config.vsock, &mut details.device_counts);
174            add_to_table("sound", config.sound, &mut details.device_counts);
175            let networks = config.networks.map(|v| v.len()).unwrap_or(0);
176            details.device_counts.push(("network".to_string(), networks as u32));
177        }
178    }
179
180    if let Some(problems) = guest_info.detected_problems {
181        details.problems = problems;
182    }
183    Ok(details)
184}
185
186#[derive(serde::Serialize, serde::Deserialize)]
187pub struct GuestOverview {
188    name: String,
189    status: String,
190    uptime_nanos: Option<i64>,
191}
192
193#[derive(serde::Serialize, serde::Deserialize)]
194pub struct GuestSummary {
195    guests: Vec<GuestOverview>,
196}
197
198impl fmt::Display for GuestSummary {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        let mut table = Table::new();
201        table.set_titles(row!["Guest", "Status", "Uptime"]);
202        for guest in &self.guests {
203            table.add_row(row![guest.name, guest.status, uptime_to_string(guest.uptime_nanos)]);
204        }
205        write!(f, "{}", table)
206    }
207}
208
209async fn get_environment_summary(
210    managers: Vec<(String, GuestManagerProxy)>,
211) -> Result<GuestSummary, Error> {
212    let mut summary = GuestSummary { guests: Vec::new() };
213    for (name, manager) in managers {
214        match manager.get_info().await {
215            Ok(guest_info) => summary.guests.push(GuestOverview {
216                name,
217                status: guest_status_to_string(
218                    guest_info.guest_status.expect("guest status should always be set"),
219                )
220                .to_string(),
221                uptime_nanos: guest_info.uptime,
222            }),
223            Err(_) => summary.guests.push(GuestOverview {
224                name,
225                status: "Unavailable".to_string(),
226                uptime_nanos: None,
227            }),
228        }
229    }
230    Ok(summary)
231}
232
233#[derive(serde::Serialize, serde::Deserialize)]
234pub enum GuestList {
235    Summary(GuestSummary),
236    Details(GuestDetails),
237}
238
239impl fmt::Display for GuestList {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        match self {
242            GuestList::Summary(summary) => write!(f, "{}", summary)?,
243            GuestList::Details(details) => write!(f, "{}", details)?,
244        }
245        Ok(())
246    }
247}
248
249pub async fn handle_list<P: PlatformServices>(
250    services: &P,
251    args: &arguments::list_args::ListArgs,
252) -> Result<GuestList, Error> {
253    match args.guest_type {
254        Some(guest_type) => {
255            let manager = services.connect_to_manager(guest_type).await?;
256            Ok(GuestList::Details(get_detailed_information(guest_type, manager).await?))
257        }
258        None => {
259            let mut managers = Vec::new();
260            for guest_type in arguments::GuestType::all_guests() {
261                let manager = services.connect_to_manager(guest_type).await?;
262                managers.push((guest_type.to_string(), manager));
263            }
264            Ok(GuestList::Summary(get_environment_summary(managers).await?))
265        }
266    }
267}
268
269#[cfg(test)]
270mod test {
271    use super::*;
272    use fidl::endpoints::create_proxy_and_stream;
273    use fidl_fuchsia_net::MacAddress;
274    use fidl_fuchsia_virtualization::{
275        GuestDescriptor, GuestError, GuestInfo, GuestManagerMarker, NetSpec,
276    };
277    use fuchsia_async as fasync;
278    use futures::StreamExt;
279
280    fn serve_mock_manager(response: Option<GuestInfo>) -> GuestManagerProxy {
281        let (proxy, mut stream) = create_proxy_and_stream::<GuestManagerMarker>();
282        fasync::Task::local(async move {
283            let responder = stream
284                .next()
285                .await
286                .expect("mock manager expected a request")
287                .unwrap()
288                .into_get_info()
289                .expect("unexpected call to mock manager");
290
291            if let Some(guest_info) = response {
292                responder.send(&guest_info).expect("failed to send mock response");
293            } else {
294                drop(responder);
295            }
296        })
297        .detach();
298
299        proxy
300    }
301
302    #[fasync::run_until_stalled(test)]
303    async fn negative_uptime() {
304        // Note that a negative duration should never happen as we're measuring duration
305        // monotonically from a single process.
306        let duration = -5;
307        let actual = uptime_to_string(Some(duration));
308        let expected = "Invalid negative uptime!";
309
310        assert_eq!(actual, expected);
311    }
312
313    #[fasync::run_until_stalled(test)]
314    async fn very_large_uptime() {
315        let hours = std::time::Duration::from_secs(123 * 60 * 60);
316        let minutes = std::time::Duration::from_secs(45 * 60);
317        let seconds = std::time::Duration::from_secs(54);
318        let duration = hours + minutes + seconds;
319
320        let actual = uptime_to_string(Some(duration.as_nanos() as i64));
321        let expected = "123:45:54 HH:MM:SS";
322
323        assert_eq!(actual, expected);
324    }
325
326    #[fasync::run_until_stalled(test)]
327    async fn summarize_existing_managers() {
328        let managers = vec![
329            ("zircon".to_string(), serve_mock_manager(None)),
330            ("termina".to_string(), serve_mock_manager(None)),
331            (
332                "debian".to_string(),
333                serve_mock_manager(Some(GuestInfo {
334                    guest_status: Some(GuestStatus::Running),
335                    uptime: Some(std::time::Duration::from_secs(123).as_nanos() as i64),
336                    ..Default::default()
337                })),
338            ),
339        ];
340
341        let actual = format!("{}", get_environment_summary(managers).await.unwrap());
342        let expected = concat!(
343            "+---------+-------------+-------------------+\n",
344            "| Guest   | Status      | Uptime            |\n",
345            "+=========+=============+===================+\n",
346            "| zircon  | Unavailable | --:--:-- HH:MM:SS |\n",
347            "+---------+-------------+-------------------+\n",
348            "| termina | Unavailable | --:--:-- HH:MM:SS |\n",
349            "+---------+-------------+-------------------+\n",
350            "| debian  | Running     | 00:02:03 HH:MM:SS |\n",
351            "+---------+-------------+-------------------+\n"
352        );
353
354        assert_eq!(actual, expected);
355    }
356
357    #[fasync::run_until_stalled(test)]
358    async fn get_detailed_info_stopped_clean() {
359        let manager = serve_mock_manager(Some(GuestInfo {
360            guest_status: Some(GuestStatus::Stopped),
361            uptime: Some(std::time::Duration::from_secs(5).as_nanos() as i64),
362            ..Default::default()
363        }));
364
365        let actual = format!(
366            "{}",
367            get_detailed_information(arguments::GuestType::Termina, manager).await.unwrap()
368        );
369        let expected = concat!(
370            " Guest package:  fuchsia-pkg://fuchsia.com/termina_guest#meta/termina_guest.cm \n",
371            " Guest status:   Stopped \n",
372            " Guest uptime:   00:00:05 HH:MM:SS \n",
373            "                  \n",
374            " Stop reason:    Clean shutdown \n",
375        );
376
377        assert_eq!(actual, expected);
378    }
379
380    #[fasync::run_until_stalled(test)]
381    async fn get_detailed_info_stopped_guest_failure() {
382        let manager = serve_mock_manager(Some(GuestInfo {
383            guest_status: Some(GuestStatus::Stopped),
384            uptime: Some(std::time::Duration::from_secs(65).as_nanos() as i64),
385            stop_error: Some(GuestError::InternalError),
386            ..Default::default()
387        }));
388
389        let actual = format!(
390            "{}",
391            get_detailed_information(arguments::GuestType::Zircon, manager).await.unwrap()
392        );
393        let expected = concat!(
394            " Guest package:  fuchsia-pkg://fuchsia.com/zircon_guest#meta/zircon_guest.cm \n",
395            " Guest status:   Stopped \n",
396            " Guest uptime:   00:01:05 HH:MM:SS \n",
397            "                  \n",
398            " Stop reason:    InternalError \n",
399        );
400
401        assert_eq!(actual, expected);
402    }
403
404    #[fasync::run_until_stalled(test)]
405    async fn get_detailed_info_running_guest() {
406        let manager = serve_mock_manager(Some(GuestInfo {
407            guest_status: Some(GuestStatus::Running),
408            uptime: Some(std::time::Duration::from_secs(125 * 60).as_nanos() as i64),
409            guest_descriptor: Some(GuestDescriptor {
410                num_cpus: Some(4),
411                guest_memory: Some(1073741824),
412                wayland: Some(false),
413                networks: Some(vec![
414                    NetSpec {
415                        mac_address: MacAddress { octets: [0u8; 6] },
416                        enable_bridge: true,
417                    };
418                    2
419                ]),
420                balloon: Some(true),
421                console: Some(true),
422                gpu: Some(false),
423                rng: Some(true),
424                vsock: Some(true),
425                sound: Some(false),
426                ..Default::default()
427            }),
428            detected_problems: Some(vec![
429                "Host is experiencing heavy memory pressure".to_string(),
430                "No bridge between guest and host network interaces".to_string(),
431            ]),
432            ..Default::default()
433        }));
434
435        let actual = format!(
436            "{}",
437            get_detailed_information(arguments::GuestType::Debian, manager).await.unwrap()
438        );
439        let expected = concat!(
440            " Guest package:     fuchsia-pkg://fuchsia.com/debian_guest#meta/debian_guest.cm \n",
441            " Guest status:      Running \n",
442            " Guest uptime:      02:05:00 HH:MM:SS \n",
443            "                     \n",
444            " CPU count:         4 \n",
445            " Guest memory:      1 GiB (1073741824 bytes) \n",
446            "                     \n",
447            " Active devices:     balloon  \n",
448            "                     console  \n",
449            "                     rng  \n",
450            "                     vsock  \n",
451            "                     network (2 devices)  \n",
452            "                     \n",
453            " Inactive devices:   wayland  \n",
454            "                     gpu  \n",
455            "                     sound  \n",
456            "                                                        \n",
457            " 2 problems detected:                                    \n",
458            " * Host is experiencing heavy memory pressure            \n",
459            " * No bridge between guest and host network interaces    \n",
460        );
461
462        assert_eq!(actual, expected);
463    }
464}