Skip to content

Commit

Permalink
Merge pull request #316 from lucab/ups/ibmcloud-classic-network-metadata
Browse files Browse the repository at this point in the history
ibmcloud/classic: source network configuration from metadata
  • Loading branch information
Luca Bruno authored Nov 29, 2019
2 parents c8db329 + 011f395 commit e8af47e
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 14 deletions.
14 changes: 7 additions & 7 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ pub fn bonding_mode_to_string(mode: u32) -> Result<String> {
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<IpNetwork> {
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,
Expand Down
166 changes: 159 additions & 7 deletions src/providers/ibmcloud/classic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -57,6 +58,60 @@ pub struct MetaDataJSON {
pub public_keys: HashMap<String, String>,
}

/// Partial object for `network_data.json`
#[derive(Debug, Deserialize)]
pub struct NetworkDataJSON {
pub links: Vec<NetLinkJSON>,
pub networks: Vec<NetNetworkJSON>,
pub services: Vec<NetServiceJSON>,
}

/// 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<NetRouteJSON>,
}

/// 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.
///
Expand Down Expand Up @@ -120,6 +175,86 @@ impl ClassicProvider {
};
Ok(attrs)
}

/// Read and parse network configuration.
fn read_network_data(&self) -> Result<NetworkDataJSON> {
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<T: Read>(input: BufReader<T>) -> Result<NetworkDataJSON> {
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<Vec<network::Interface>> {
use std::str::FromStr;

// Validate links and parse them into a map, keyed by id.
let mut devices: HashMap<String, (String, MacAddr)> =
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<IpAddr> = 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 {
Expand All @@ -144,8 +279,9 @@ impl MetadataProvider for ClassicProvider {
}

fn networks(&self) -> Result<Vec<network::Interface>> {
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<Vec<network::VirtualNetDev>> {
Expand Down Expand Up @@ -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);
}
}
}
68 changes: 68 additions & 0 deletions tests/fixtures/ibmcloud/classic/network_data.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}

0 comments on commit e8af47e

Please sign in to comment.