This is an automated email from the ASF dual-hosted git repository.

DaanHoogland pushed a commit to branch apple-container-extension
in repository https://gitbox.apache.org/repos/asf/cloudstack-extensions.git


The following commit(s) were added to refs/heads/apple-container-extension by 
this push:
     new d349c24  apple containers: add service to monitor power states
d349c24 is described below

commit d349c245c9b6e7aa1545bb7478a7be20c1520558
Author: Daan Hoogland <[email protected]>
AuthorDate: Thu Oct 8 18:08:14 2026 +0200

    apple containers: add service to monitor power states
---
 apple-container/README.txt  |  71 +++++++++++--
 apple-container/src/main.rs | 251 +++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 300 insertions(+), 22 deletions(-)

diff --git a/apple-container/README.txt b/apple-container/README.txt
index deb1f46..92c8888 100644
--- a/apple-container/README.txt
+++ b/apple-container/README.txt
@@ -57,12 +57,19 @@ Supported operations
                   count, memory (rounded up to whole MiB), one network
                   attachment per NIC with the CloudStack MAC address, then
                   start it. The container is removed again when it fails
-                  to start.
+                  to start. See Networks for which network each NIC uses.
   start, stop, reboot, delete, status, statuses
                   stop sends SIGTERM and kills the container after
                   wait_timeout seconds; statuses reports only containers
-                  carrying the CloudStack label
+                  carrying the CloudStack label. Containers stop together
+                  with the container system service, so when the service is
+                  not running it is started first (see start_service) and
+                  its containers are reported as stopped, instead of
+                  CloudStack seeing an unknown power state.
   getconsole      Not supported, Apple container has no VNC endpoint.
+                  Other operations CloudStack passes through, such as its
+                  periodic GetVmIpAddressCommand, are answered with
+                  "Operation not supported".
 
 Custom actions (register them with addCustomAction):
   GetIpAddresses  List the IPv4/IPv6 addresses of the instance's network
@@ -86,10 +93,13 @@ Extension or host details (host details win):
   network           Optional, comma separated container networks, one per
                     NIC in device order; the last one is used for further
                     NICs. Default: default. Create other networks with
-                    "container network create <name>".
+                    "container network create <name>". Not used for NICs on
+                    vmnet extension networks (see Networks).
   wait_timeout      Optional, seconds a stop waits before killing the
                     container, and the limit for other commands, default 60
   pull_timeout      Optional, seconds an image pull may take, default 900
+  start_service     Optional, "true" (default) starts the container system
+                    service when a command finds it not running
 
 Template, service offering or instance details (instance details win; they
 can also be set on the host or extension as defaults):
@@ -101,6 +111,26 @@ can also be set on the host or extension as defaults):
                     that reaps zombies and forwards signals
   rosetta           Optional, "true" enables Rosetta, for linux/amd64 images
 
+Networks
+--------
+
+A NIC on a CloudStack network of the vmnet network extension (an L2 network
+whose broadcast URI looks like vs://cs-net-42?mode=nat, see the vmnet
+extension's README.txt) is attached to the container network of that name.
+That network gets one vmnet network per CloudStack network, so instances on
+different CloudStack networks are isolated from each other:
+
+  - It is created with "container network create", labelled
+    org.apache.cloudstack.managed=true and with --internal for mode
+    internal, before an instance using it is created, started or rebooted.
+    A network of that name not created by CloudStack is not used, and the
+    instance fails to deploy.
+  - It is deleted when an instance using it is deleted and no other
+    container uses it any more.
+
+Any other NIC uses the network detail: the container network at its
+position in the list, the last one for further NICs.
+
 Setup
 -----
 
@@ -109,6 +139,31 @@ Setup
 
      container system start --enable-kernel-install
 
+   The service does not start again by itself after a reboot or logout.
+   The extension starts it when it finds it down, but only once CloudStack
+   asks for a status (about once a minute), and starting it over SSH is not
+   verified yet. To start it at login instead, create a launchd
+   agent ~/Library/LaunchAgents/org.apache.cloudstack.container.plist:
+
+     <?xml version="1.0" encoding="UTF-8"?>
+     <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
+       "http://www.apple.com/DTDs/PropertyList-1.0.dtd";>
+     <plist version="1.0">
+     <dict>
+       <key>Label</key><string>org.apache.cloudstack.container</string>
+       <key>ProgramArguments</key>
+       <array>
+         <string>/usr/local/bin/container</string>
+         <string>system</string>
+         <string>start</string>
+         <string>--disable-kernel-install</string>
+       </array>
+       <key>RunAtLoad</key><true/>
+     </dict>
+     </plist>
+
+   and load it with: launchctl load 
~/Library/LaunchAgents/org.apache.cloudstack.container.plist
+
 2. Build the binary on (or for) the management server's platform, e.g. on
    a Linux management server:
 
@@ -151,7 +206,8 @@ Limitations
     running command, a command detail such as "sleep infinity", or an image
     with an init system.
   - Networking is limited to container (vmnet) networks on the Mac: no VLAN
-    isolation, and CloudStack does not manage the IP addresses. Apple
+    isolation, only networks of the vmnet network extension or ones
+    created by hand, and CloudStack does not manage the IP addresses. Apple
     container assigns them, possibly a different IPv4 address on every
     start; GetIpAddresses reports the current ones. The IPv6 address is
     derived from the MAC address and stays the same.
@@ -163,5 +219,8 @@ Limitations
   - Tested on macOS 26.6 with Apple container 1.5.0, locally (url
     localhost): deploy, stop, start, reboot and destroy from CloudStack,
     the CloudStack MAC address inside the guest, GetIpAddresses and GetLogs
-    with alpine:3.22 and command "sleep infinity". SSH to a remote Mac and
-    multiple NICs still need verification.
+    with alpine:3.22 and command "sleep infinity"; and with a vmnet
+    extension network in mode internal: two instances reach each other but
+    not the internet, the network is created with the first instance and
+    deleted with the last. SSH to a remote Mac and multiple NICs still need
+    verification.
diff --git a/apple-container/src/main.rs b/apple-container/src/main.rs
index 7f2990a..e505f73 100644
--- a/apple-container/src/main.rs
+++ b/apple-container/src/main.rs
@@ -26,7 +26,11 @@
 //!
 //! The management server reaches the Mac over SSH (or runs locally when the
 //! management server itself runs on the Mac). Each NIC of the instance is
-//! attached to a container network with the CloudStack MAC address.
+//! attached to a container network with the CloudStack MAC address. A NIC on
+//! a network of the vmnet network extension carries a broadcast URI such as
+//! vs://cs-net-42?mode=nat; it is attached to the container network of that
+//! name, which is created on first use and deleted with its last container.
+//! Other NICs use the networks of the network detail.
 //!
 //! Details (host details override extension details):
 //!   url                   Mac hostname/IP, or "localhost" to run locally
@@ -39,10 +43,13 @@
 //!   container_path        optional, defaults to /usr/local/bin/container
 //!   network               optional, comma separated container networks, one
 //!                         per NIC in device order, the last one repeating for
-//!                         further NICs; defaults to "default"
+//!                         further NICs; defaults to "default". Not used for
+//!                         NICs on vmnet extension networks
 //!   wait_timeout          optional, seconds a stop waits before the container
 //!                         is killed and to wait for other commands (60)
 //!   pull_timeout          optional, seconds to wait for an image pull (900)
+//!   start_service         optional, "true" (default) starts the container
+//!                         system service when it is not running
 //!
 //! Template, service offering or instance details (instance wins), which may
 //! also be set on the host or extension as defaults:
@@ -68,6 +75,13 @@ const DEFAULT_NETWORK: &str = "default";
 const MANAGED_LABEL: &str = "org.apache.cloudstack.managed";
 const LOCAL_HOSTS: [&str; 3] = ["localhost", "127.0.0.1", "::1"];
 const DEFAULT_LOG_LINES: u64 = 100;
+/// Broadcast URI scheme of the networks named by the vmnet network extension.
+const VMNET_SCHEME: &str = "vs://";
+/// Hint the container tool prints when its system service is not running.
+const SERVICE_DOWN_HINT: &str = "container system start";
+const OPERATIONS: [&str; 10] = [
+    "create", "start", "stop", "reboot", "delete", "status", "statuses", 
"getconsole", "getipaddresses", "getlogs",
+];
 
 type Result<T> = std::result::Result<T, String>;
 
@@ -107,6 +121,37 @@ fn memory_argument(bytes: u64) -> String {
     format!("{}M", bytes.div_ceil(1024 * 1024).max(1))
 }
 
+/// A container network named by the vmnet network extension.
+#[derive(Clone, Debug, PartialEq)]
+struct VmnetNetwork {
+    name: String,
+    internal: bool,
+}
+
+/// The vmnet network of a NIC broadcast URI like vs://cs-net-42?mode=internal,
+/// or None for NICs on other networks.
+fn vmnet_network(broadcast_uri: &str) -> Result<Option<VmnetNetwork>> {
+    let Some(rest) = broadcast_uri.strip_prefix(VMNET_SCHEME) else {
+        return Ok(None);
+    };
+    let (name, query) = rest.split_once('?').unwrap_or((rest, ""));
+    if name.is_empty() || !name.chars().all(|c| c.is_ascii_lowercase() || 
c.is_ascii_digit() || c == '-') {
+        return Err(format!("Invalid vmnet network name in broadcast URI 
'{broadcast_uri}'"));
+    }
+    let mode = query.split('&').find_map(|p| 
p.strip_prefix("mode=")).unwrap_or("nat");
+    let internal = match mode {
+        "nat" => false,
+        "internal" => true,
+        _ => return Err(format!("Invalid vmnet network mode in broadcast URI 
'{broadcast_uri}'")),
+    };
+    Ok(Some(VmnetNetwork { name: name.to_string(), internal }))
+}
+
+struct Nic {
+    mac: String,
+    vmnet: Option<VmnetNetwork>,
+}
+
 /// The container network for each NIC, the last configured network repeating 
for further NICs.
 fn nic_networks(networks: &[String], nic_count: usize) -> Vec<String> {
     let last = networks.last().cloned().unwrap_or_else(|| 
DEFAULT_NETWORK.to_string());
@@ -131,10 +176,30 @@ fn container_networks(snapshot: &Value) -> Vec<Value> {
         .unwrap_or_default()
 }
 
+/// Names of the networks a container is attached to, running or not; only
+/// running containers report their network status.
+fn network_names(snapshot: &Value) -> Vec<String> {
+    let configured = snapshot
+        .get("configuration")
+        .and_then(|c| c.get("networks"))
+        .and_then(Value::as_array)
+        .cloned()
+        .unwrap_or_default();
+    let mut names: Vec<String> = Vec::new();
+    for network in 
configured.iter().chain(container_networks(snapshot).iter()) {
+        let name = value_text(network.get("network"));
+        if !name.is_empty() && !names.contains(&name) {
+            names.push(name);
+        }
+    }
+    names
+}
+
 fn container_id(snapshot: &Value) -> String {
     value_text(snapshot.get("configuration").and_then(|c| c.get("id")))
 }
 
+/// Whether a container or network carries the CloudStack label.
 fn is_managed(snapshot: &Value) -> bool {
     snapshot
         .get("configuration")
@@ -179,6 +244,7 @@ struct Config {
     networks: Vec<String>,
     wait_timeout: u64,
     pull_timeout: u64,
+    start_service: bool,
     image: String,
     command: Vec<String>,
     platform: String,
@@ -187,7 +253,7 @@ struct Config {
     vmname: String,
     cpus: Option<u64>,
     memory: Option<u64>,
-    macs: Vec<String>,
+    nics: Vec<Nic>,
     parameters: Map<String, Value>,
 }
 
@@ -231,11 +297,15 @@ impl Config {
         let vm_details = section(json_data.get("cloudstack.vm.details"));
         let mut nics: Vec<&Value> = 
vm_details.get("nics").and_then(Value::as_array).map(|n| 
n.iter().collect()).unwrap_or_default();
         nics.sort_by_key(|nic| 
nic.get("deviceId").and_then(Value::as_i64).unwrap_or(0));
-        let macs = nics
+        let nics = nics
             .iter()
-            .map(|nic| value_text(nic.get("mac")))
-            .filter(|mac| !mac.is_empty())
-            .collect();
+            .map(|nic| {
+                Ok(Nic {
+                    mac: value_text(nic.get("mac")),
+                    vmnet: 
vmnet_network(&value_text(nic.get("broadcastUri")))?,
+                })
+            })
+            .collect::<Result<Vec<_>>>()?;
 
         Ok(Config {
             url,
@@ -249,6 +319,7 @@ impl Config {
             networks,
             wait_timeout: number("wait_timeout", "60")?,
             pull_timeout: number("pull_timeout", "900")?,
+            start_service: detail("start_service", "true").to_lowercase() == 
"true",
             image: vm_detail("image", ""),
             command: vm_detail("command", 
"").split_whitespace().map(String::from).collect(),
             platform: vm_detail("platform", ""),
@@ -257,7 +328,7 @@ impl Config {
             vmname: value_text(vm_details.get("name")),
             cpus: value_u64(vm_details.get("cpus")),
             memory: value_u64(vm_details.get("minRam")),
-            macs,
+            nics,
             parameters: section(json_data.get("parameters")),
         })
     }
@@ -350,9 +421,26 @@ impl ContainerManager {
         Ok(())
     }
 
-    /// All containers, running or not, keyed by container ID.
+    /// Start the container system service, without installing a kernel as 
that prompts.
+    fn start_service(&self) -> Result<()> {
+        let timeout = self.config.wait_timeout.to_string();
+        self.container(&["system", "start", "--disable-kernel-install", 
"--timeout", &timeout])
+            .map(|_| ())
+            .map_err(|e| format!("The container system service is not running 
and failed to start: {e}"))
+    }
+
+    /// All containers, running or not, keyed by container ID. Containers do
+    /// not survive the system service, so when it is down it is started to
+    /// report them as stopped instead of failing.
     fn list_containers(&self) -> Result<BTreeMap<String, Value>> {
-        let output = self.container(&["list", "--all", "--format", "json"])?;
+        let args = ["list", "--all", "--format", "json"];
+        let output = match self.container(&args) {
+            Err(e) if self.config.start_service && 
e.contains(SERVICE_DOWN_HINT) => {
+                self.start_service()?;
+                self.container(&args)?
+            }
+            result => result?,
+        };
         let containers: Value = serde_json::from_str(output.trim())
             .map_err(|e| format!("Failed to parse the container list: {e}"))?;
         Ok(containers
@@ -380,10 +468,12 @@ impl ContainerManager {
         if let Some(memory) = c.memory {
             args.extend(["--memory".to_string(), memory_argument(memory)]);
         }
-        let networks = nic_networks(&c.networks, c.macs.len());
+        let networks = nic_networks(&c.networks, c.nics.len());
         for (i, network) in networks.iter().enumerate() {
-            let spec = match c.macs.get(i) {
-                Some(mac) => format!("{network},mac={}", mac.to_lowercase()),
+            let nic = c.nics.get(i);
+            let network = nic.and_then(|n| n.vmnet.as_ref()).map(|v| 
&v.name).unwrap_or(network);
+            let spec = match nic.filter(|n| !n.mac.is_empty()) {
+                Some(nic) => format!("{network},mac={}", 
nic.mac.to_lowercase()),
                 None => network.clone(),
             };
             args.extend(["--network".to_string(), spec]);
@@ -402,6 +492,75 @@ impl ContainerManager {
         args
     }
 
+    /// The distinct vmnet extension networks of the instance's NICs.
+    fn vmnet_networks(&self) -> Vec<&VmnetNetwork> {
+        let mut networks: Vec<&VmnetNetwork> = Vec::new();
+        for network in self.config.nics.iter().filter_map(|n| 
n.vmnet.as_ref()) {
+            if !networks.iter().any(|n| n.name == network.name) {
+                networks.push(network);
+            }
+        }
+        networks
+    }
+
+    /// All container networks keyed by name.
+    fn list_networks(&self) -> Result<BTreeMap<String, Value>> {
+        let output = self.container(&["network", "list", "--format", "json"])?;
+        let networks: Value = serde_json::from_str(output.trim())
+            .map_err(|e| format!("Failed to parse the network list: {e}"))?;
+        Ok(networks
+            .as_array()
+            .map(|list| list.iter().map(|n| (value_text(n.get("id")), 
n.clone())).collect())
+            .unwrap_or_default())
+    }
+
+    /// Create the vmnet extension networks the instance needs and that do not
+    /// exist yet. An existing network is only used when CloudStack created it.
+    fn ensure_networks(&self) -> Result<()> {
+        let wanted = self.vmnet_networks();
+        if wanted.is_empty() {
+            return Ok(());
+        }
+        let existing = self.list_networks()?;
+        for network in wanted {
+            match existing.get(&network.name) {
+                Some(found) if is_managed(found) => continue,
+                Some(_) => {
+                    return Err(format!("Container network '{}' exists but was 
not created by CloudStack", network.name))
+                }
+                None => {}
+            }
+            let label = format!("{MANAGED_LABEL}=true");
+            let mut args = vec!["network", "create", "--label", 
label.as_str()];
+            if network.internal {
+                args.push("--internal");
+            }
+            args.push(&network.name);
+            if let Err(e) = self.container(&args) {
+                // Another instance may have created it meanwhile.
+                if 
!self.list_networks()?.get(&network.name).is_some_and(is_managed) {
+                    return Err(format!("Failed to create container network 
'{}': {e}", network.name));
+                }
+            }
+        }
+        Ok(())
+    }
+
+    /// Delete the given networks when CloudStack created them and no other
+    /// container uses them; the container tool refuses to delete networks in 
use.
+    fn delete_unused_networks(&self, names: &[String]) -> Result<()> {
+        if names.is_empty() {
+            return Ok(());
+        }
+        let existing = self.list_networks()?;
+        for name in names {
+            if existing.get(name).is_some_and(is_managed) {
+                let _ = self.container(&["network", "delete", name]);
+            }
+        }
+        Ok(())
+    }
+
     fn pull_image(&self) -> Result<()> {
         let c = &self.config;
         let mut args = vec!["image", "pull", "--progress", "none"];
@@ -440,6 +599,7 @@ impl ContainerManager {
             return Err(format!("A container named '{name}' already exists"));
         }
         self.pull_image()?;
+        self.ensure_networks()?;
         let args = self.create_arguments();
         self.container(&args.iter().map(String::as_str).collect::<Vec<_>>())?;
         if let Err(e) = self.start_container() {
@@ -452,6 +612,7 @@ impl ContainerManager {
     fn start(&mut self) -> Result<Value> {
         self.require_vmname()?;
         if container_state(&self.require_container()?) != "running" {
+            self.ensure_networks()?;
             self.start_container()?;
         }
         Ok(success_message("Instance started"))
@@ -472,14 +633,19 @@ impl ContainerManager {
         if container_state(&self.require_container()?) != "stopped" {
             self.stop_container()?;
         }
+        self.ensure_networks()?;
         self.start_container()?;
         Ok(success_message("Instance rebooted"))
     }
 
     fn delete(&mut self) -> Result<Value> {
         self.require_vmname()?;
-        if self.find_container()?.is_some() {
+        if let Some(snapshot) = self.find_container()? {
+            // The expunge payload has no NICs, so take the networks from the 
container.
+            let networks = network_names(&snapshot);
             self.delete_container()?;
+            // Best effort: a leftover network is reused or deleted with a 
later instance.
+            let _ = self.delete_unused_networks(&networks);
         }
         Ok(success_message("Instance deleted"))
     }
@@ -523,6 +689,11 @@ impl ContainerManager {
 }
 
 fn execute(operation: &str, json_file_path: &str) -> Result<Value> {
+    // CloudStack also passes commands it has no extension operation for, such 
as
+    // com.cloud.agent.api.GetVmIpAddressCommand, without host details.
+    if !OPERATIONS.contains(&operation) {
+        return Err(format!("Operation not supported: {operation}"));
+    }
     let content = fs::read_to_string(json_file_path).map_err(|e| match 
e.kind() {
         std::io::ErrorKind::NotFound => format!("JSON file not found: 
{json_file_path}"),
         _ => format!("Failed to read {json_file_path}: {e}"),
@@ -541,7 +712,7 @@ fn execute(operation: &str, json_file_path: &str) -> 
Result<Value> {
         "getconsole" => Err("Operation not supported".into()),
         "getipaddresses" => manager.get_ip_addresses(),
         "getlogs" => manager.get_logs(),
-        _ => Err("Invalid action".into()),
+        _ => unreachable!("operation {operation} is checked above"),
     }
 }
 
@@ -613,11 +784,14 @@ mod tests {
         assert!(c.local);
         assert_eq!(c.container_path, DEFAULT_CONTAINER_PATH);
         assert_eq!(c.networks, vec!["default", "isolated"]);
-        assert_eq!(c.macs, vec!["02:00:0a:00:00:01", "02:00:0A:00:00:02", 
"02:00:0a:00:00:03"]);
+        let macs: Vec<&str> = c.nics.iter().map(|n| n.mac.as_str()).collect();
+        assert_eq!(macs, vec!["02:00:0a:00:00:01", "02:00:0A:00:00:02", 
"02:00:0a:00:00:03"]);
+        assert!(c.nics.iter().all(|n| n.vmnet.is_none()));
         assert_eq!(c.command, vec!["sleep", "infinity"]);
         assert!(c.init);
         assert!(!c.rosetta);
         assert_eq!(c.wait_timeout, 60);
+        assert!(c.start_service);
         assert!(Config::parse(&json!({"externaldetails": {"extension": {"url": 
"mac"}}})).is_err());
         assert!(Config::parse(&json!({"externaldetails": {"extension": {"url": 
"localhost", "wait_timeout": "x"}}})).is_err());
     }
@@ -650,6 +824,37 @@ mod tests {
         );
     }
 
+    #[test]
+    fn parses_vmnet_broadcast_uris() {
+        let net = |name: &str, internal| Some(VmnetNetwork { name: 
name.into(), internal });
+        assert_eq!(vmnet_network("vs://cs-net-42?mode=nat").unwrap(), 
net("cs-net-42", false));
+        assert_eq!(vmnet_network("vs://cs-net-7?mode=internal").unwrap(), 
net("cs-net-7", true));
+        assert_eq!(vmnet_network("vs://cs-net-7").unwrap(), net("cs-net-7", 
false));
+        assert_eq!(vmnet_network("vlan://300").unwrap(), None);
+        assert_eq!(vmnet_network("").unwrap(), None);
+        assert!(vmnet_network("vs://cs-net-7?mode=bridged").is_err());
+        assert!(vmnet_network("vs://Bad_Name").is_err());
+        assert!(vmnet_network("vs://").is_err());
+    }
+
+    #[test]
+    fn attaches_vmnet_nics_to_their_own_networks() {
+        let mut data = base(json!({"image": "alpine:3.22"}));
+        data["cloudstack.vm.details"]["nics"] = json!([
+            {"deviceId": 0, "mac": "02:00:0a:00:00:01", "broadcastUri": 
"vs://cs-net-42?mode=internal"},
+            {"deviceId": 1, "mac": "02:00:0a:00:00:02", "broadcastUri": 
"vlan://300"},
+            {"deviceId": 2, "mac": "02:00:0a:00:00:03", "broadcastUri": 
"vs://cs-net-42?mode=internal"},
+        ]);
+        let manager = ContainerManager { config: config(data) };
+        assert_eq!(manager.vmnet_networks(), vec![&VmnetNetwork { name: 
"cs-net-42".into(), internal: true }]);
+        let args = manager.create_arguments();
+        let networks: Vec<&str> = args.iter().zip(args.iter().skip(1))
+            .filter(|(flag, _)| *flag == "--network").map(|(_, spec)| 
spec.as_str()).collect();
+        assert_eq!(networks, vec![
+            "cs-net-42,mac=02:00:0a:00:00:01", 
"isolated,mac=02:00:0a:00:00:02", "cs-net-42,mac=02:00:0a:00:00:03",
+        ]);
+    }
+
     #[test]
     fn rejects_unsafe_instance_names() {
         let mut data = base(json!({}));
@@ -659,6 +864,13 @@ mod tests {
         }
     }
 
+    #[test]
+    fn rejects_unsupported_operations_before_reading_details() {
+        let error = execute("com.cloud.agent.api.getvmipaddresscommand", 
"/nonexistent.json").unwrap_err();
+        assert_eq!(error, "Operation not supported: 
com.cloud.agent.api.getvmipaddresscommand");
+        assert!(execute("statuses", 
"/nonexistent.json").unwrap_err().starts_with("JSON file not found"));
+    }
+
     #[test]
     fn reads_container_snapshots() {
         let current = json!({
@@ -672,6 +884,13 @@ mod tests {
         assert_eq!(power_state(&container_state(&current)), "poweron");
         assert!(is_managed(&current));
         assert_eq!(ip_addresses(&current), vec!["192.168.64.3", "fd00::3"]);
+        assert_eq!(network_names(&current), vec!["default"]);
+        let stopped = json!({
+            "configuration": {"id": "i-2-11-VM", "networks": [{"network": 
"cs-net-42", "options": {}},
+                                                              {"network": 
"default", "options": {}}]},
+            "status": "stopped"
+        });
+        assert_eq!(network_names(&stopped), vec!["cs-net-42", "default"]);
 
         let legacy = json!({
             "configuration": {"id": "buildkit", "labels": {}},

Reply via email to