diff --git a/src/errors.rs b/src/errors.rs index cf3b44cb..e2271c1c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -15,23 +15,23 @@ #![allow(deprecated)] use error_chain::error_chain; -use reqwest::header; -use serde_json; error_chain! { links { - PublicKey(::openssh_keys::errors::Error, ::openssh_keys::errors::ErrorKind); AuthorizedKeys(::update_ssh_keys::errors::Error, ::update_ssh_keys::errors::ErrorKind) #[cfg(feature = "cl-legacy")]; + PublicKey(::openssh_keys::errors::Error, ::openssh_keys::errors::ErrorKind); } foreign_links { - Log(::slog::Error); - XmlDeserialize(::serde_xml_rs::Error); Base64Decode(::base64::DecodeError); + HeaderValue(reqwest::header::InvalidHeaderValue); Io(::std::io::Error); + IpNetwork(ipnetwork::IpNetworkError); Json(serde_json::Error); - Reqwest(::reqwest::Error); + Log(::slog::Error); + MacAddr(pnet_base::ParseMacAddrErr); OpensslStack(::openssl::error::ErrorStack); - HeaderValue(header::InvalidHeaderValue); + Reqwest(::reqwest::Error); + XmlDeserialize(::serde_xml_rs::Error); } errors { UnknownProvider(p: String) { diff --git a/src/network.rs b/src/network.rs index 2401d595..15acffde 100644 --- a/src/network.rs +++ b/src/network.rs @@ -51,6 +51,12 @@ pub fn bonding_mode_to_string(mode: u32) -> Result { Err(format!("no such bonding mode: {}", mode).into()) } +/// Try to parse an IP+netmask pair into a CIDR network. +pub fn try_parse_cidr(address: IpAddr, netmask: IpAddr) -> Result { + let prefix = ipnetwork::ip_mask_to_prefix(netmask)?; + IpNetwork::new(address, prefix).chain_err(|| "failed to parse network") +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct NetworkRoute { pub destination: IpNetwork, diff --git a/src/providers/ibmcloud/classic.rs b/src/providers/ibmcloud/classic.rs index a2669dc8..d2dd8280 100644 --- a/src/providers/ibmcloud/classic.rs +++ b/src/providers/ibmcloud/classic.rs @@ -11,15 +11,16 @@ //! //! configdrive: https://cloudinit.readthedocs.io/en/latest/topics/datasources/configdrive.html -use std::collections::HashMap; -use std::fs::File; -use std::io::{BufReader, Read}; -use std::path::{Path, PathBuf}; - use error_chain::bail; use openssh_keys::PublicKey; +use pnet_base::MacAddr; use serde::Deserialize; use slog_scope::warn; +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::net::IpAddr; +use std::path::{Path, PathBuf}; use tempdir::TempDir; use crate::errors::*; @@ -57,6 +58,60 @@ pub struct MetaDataJSON { pub public_keys: HashMap, } +/// Partial object for `network_data.json` +#[derive(Debug, Deserialize)] +pub struct NetworkDataJSON { + pub links: Vec, + pub networks: Vec, + pub services: Vec, +} + +/// JSON entry in `links` array. +#[derive(Debug, Deserialize)] +pub struct NetLinkJSON { + pub name: String, + pub id: String, + #[serde(rename = "ethernet_mac_address")] + pub mac_addr: String, +} + +/// JSON entry in `networks` array. +#[derive(Debug, Deserialize)] +pub struct NetNetworkJSON { + /// Unique network ID. + pub id: String, + /// Network type (e.g. `ipv4`) + #[serde(rename = "type")] + pub kind: String, + /// Reference to the underlying interface (see `NetLinkJSON.id`) + pub link: String, + /// IP network address. + pub ip_address: IpAddr, + /// IP network mask. + pub netmask: IpAddr, + /// Routable networks. + pub routes: Vec, +} + +/// JSON entry in `networks.routes` array. +#[derive(Debug, Deserialize)] +pub struct NetRouteJSON { + /// Route network address. + pub network: IpAddr, + /// Route netmask. + pub netmask: IpAddr, + /// Route gateway. + pub gateway: IpAddr, +} + +/// JSON entry in `services` array. +#[derive(Debug, Deserialize)] +pub struct NetServiceJSON { + #[serde(rename = "type")] + pub kind: String, + pub address: IpAddr, +} + impl ClassicProvider { /// Try to build a new provider client. /// @@ -120,6 +175,86 @@ impl ClassicProvider { }; Ok(attrs) } + + /// Read and parse network configuration. + fn read_network_data(&self) -> Result { + let filename = self.metadata_dir().join("network_data.json"); + let file = + File::open(&filename).chain_err(|| format!("failed to open file '{:?}'", filename))?; + let bufrd = BufReader::new(file); + Self::parse_network_data(bufrd) + } + + /// Parse network configuration. + /// + /// Network configuration file contains a JSON object, corresponding to `NetworkDataJSON`. + fn parse_network_data(input: BufReader) -> Result { + serde_json::from_reader(input).chain_err(|| "failed parse JSON network data") + } + + /// Transform network JSON data into a set of interface configurations. + fn network_interfaces(input: NetworkDataJSON) -> Result> { + use std::str::FromStr; + + // Validate links and parse them into a map, keyed by id. + let mut devices: HashMap = + HashMap::with_capacity(input.links.len()); + for dev in input.links { + let mac = MacAddr::from_str(&dev.mac_addr)?; + devices.insert(dev.id, (dev.name, mac)); + } + + // Parse resolvers. + let nameservers: Vec = input + .services + .into_iter() + .filter_map(|svc| { + if svc.kind == "dns" { + Some(svc.address) + } else { + None + } + }) + .collect(); + + let mut output = Vec::with_capacity(input.networks.len()); + for net in input.networks { + // Ensure that the referenced link exists. + let (name, mac_addr) = match devices.get(&net.link) { + Some(dev) => (dev.0.clone(), dev.1), + None => continue, + }; + + // Assemble network CIDR. + let ip_net = network::try_parse_cidr(net.ip_address, net.netmask)?; + + // Parse network routes. + let mut routes = Vec::with_capacity(net.routes.len()); + for entry in net.routes { + let destination = network::try_parse_cidr(entry.network, entry.netmask)?; + let route = network::NetworkRoute { + destination, + gateway: entry.gateway, + }; + routes.push(route); + } + + let iface = network::Interface { + name: Some(name), + mac_address: Some(mac_addr), + priority: 10, + nameservers: nameservers.clone(), + ip_addresses: vec![ip_net], + routes, + bond: None, + unmanaged: false, + }; + output.push(iface); + } + + output.shrink_to_fit(); + Ok(output) + } } impl MetadataProvider for ClassicProvider { @@ -144,8 +279,9 @@ impl MetadataProvider for ClassicProvider { } fn networks(&self) -> Result> { - warn!("network metadata requested, but not supported on this platform"); - Ok(vec![]) + let data = self.read_network_data()?; + let interfaces = Self::network_interfaces(data)?; + Ok(interfaces) } fn virtual_network_devices(&self) -> Result> { @@ -217,4 +353,20 @@ mod tests { assert!(!parsed.local_hostname.is_empty()); assert!(!parsed.public_keys.is_empty()); } + + #[test] + fn test_parse_network_data_json() { + let fixture = File::open("./tests/fixtures/ibmcloud/classic/network_data.json").unwrap(); + let bufrd = BufReader::new(fixture); + let parsed = ClassicProvider::parse_network_data(bufrd).unwrap(); + + let interfaces = ClassicProvider::network_interfaces(parsed).unwrap(); + assert_eq!(interfaces.len(), 2); + assert_eq!(interfaces[0].routes.len(), 3); + assert_eq!(interfaces[1].routes.len(), 1); + + for entry in interfaces { + assert_eq!(entry.nameservers.len(), 2); + } + } } diff --git a/tests/fixtures/ibmcloud/classic/network_data.json b/tests/fixtures/ibmcloud/classic/network_data.json new file mode 100644 index 00000000..c797e1f4 --- /dev/null +++ b/tests/fixtures/ibmcloud/classic/network_data.json @@ -0,0 +1,68 @@ +{ + "links": [ + { + "id": "interface_58965010", + "name": "eth0", + "mtu": null, + "type": "phy", + "ethernet_mac_address": "06:52:db:01:ff:d9" + }, + { + "id": "interface_58965014", + "name": "eth1", + "mtu": null, + "type": "phy", + "ethernet_mac_address": "06:f6:71:3b:64:01" + } + ], + "networks": [ + { + "id": "network_142213448", + "link": "interface_58965010", + "type": "ipv4", + "ip_address": "10.135.202.146", + "netmask": "255.255.255.192", + "routes": [ + { + "network": "10.0.0.0", + "netmask": "255.0.0.0", + "gateway": "10.135.202.129" + }, + { + "network": "161.26.0.0", + "netmask": "255.255.0.0", + "gateway": "10.135.202.129" + }, + { + "network": "166.8.0.0", + "netmask": "255.252.0.0", + "gateway": "10.135.202.129" + } + ] + }, + { + "id": "network_142211974", + "link": "interface_58965014", + "type": "ipv4", + "ip_address": "133.133.209.229", + "netmask": "255.255.255.240", + "routes": [ + { + "network": "0.0.0.0", + "netmask": "0.0.0.0", + "gateway": "133.133.209.225" + } + ] + } + ], + "services": [ + { + "type": "dns", + "address": "10.0.80.11" + }, + { + "type": "dns", + "address": "10.0.80.12" + } + ] +}