diff --git a/src/metadata.rs b/src/metadata.rs index d3ac4499..738d791b 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -24,6 +24,7 @@ use crate::providers::exoscale::ExoscaleProvider; use crate::providers::gcp::GcpProvider; use crate::providers::ibmcloud::IBMGen2Provider; use crate::providers::ibmcloud_classic::IBMClassicProvider; +use crate::providers::opennebula::ContextDrive; use crate::providers::openstack; use crate::providers::openstack::network::OpenstackProviderNetwork; use crate::providers::packet::PacketProvider; @@ -63,6 +64,7 @@ pub fn fetch_metadata(provider: &str) -> errors::Result box_result!(IBMGen2Provider::try_new()?), // IBM Cloud - Classic infrastructure. "ibmcloud-classic" => box_result!(IBMClassicProvider::try_new()?), + "one" => box_result!(ContextDrive::try_new()?), "openstack" => openstack::try_config_drive_else_network(), "openstack-metadata" => box_result!(OpenstackProviderNetwork::try_new()?), "packet" => box_result!(PacketProvider::try_new()?), diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 8249b1df..a0cce387 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -32,6 +32,7 @@ pub mod exoscale; pub mod gcp; pub mod ibmcloud; pub mod ibmcloud_classic; +pub mod opennebula; pub mod openstack; pub mod packet; #[cfg(feature = "cl-legacy")] diff --git a/src/providers/opennebula/mod.rs b/src/providers/opennebula/mod.rs new file mode 100644 index 00000000..29ff81ef --- /dev/null +++ b/src/providers/opennebula/mod.rs @@ -0,0 +1,265 @@ +/* +Copyright 2020 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; +use slog_scope::warn; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::path::{Path, PathBuf}; + +use nix::mount; +use openssh_keys::PublicKey; +use tempfile; + +use crate::errors::*; +use crate::network; +use crate::providers::MetadataProvider; +use pnet_base::MacAddr; + +#[cfg(test)] +mod tests; + +const ENV_PREFIX: &str = "ONE_"; +const CONTEXT_DRIVE_LABEL: &str = "CONTEXT"; +const CONTEXT_SCRIPT_NAME: &str = "context.sh"; + +#[derive(Debug)] +pub struct ContextDrive { + contents: String, + attributes: HashMap, + device: Option, + mount_point: Option, +} + +impl ContextDrive { + pub fn try_new() -> Result { + // Mount disk by label to a new tempdir + let target = tempfile::Builder::new() + .prefix("afterburn-") + .tempdir() + .chain_err(|| "failed to create temporary directory")?; + let device = Path::new("/dev/disk/by-label/").join(CONTEXT_DRIVE_LABEL); + ContextDrive::mount_ro(&device, target.path(), "iso9660")?; + let filename = target.path().join(CONTEXT_SCRIPT_NAME); + let mut file = + File::open(&filename).chain_err(|| format!("failed to open file '{:?}'", filename))?; + let mut contents = String::new(); + file.read_to_string(&mut contents) + .chain_err(|| format!("failed to read from file '{:?}'", filename))?; + Ok(ContextDrive { + contents: contents.to_string(), + attributes: ContextDrive::fetch_all_values(contents.to_string()), + device: Some(device.to_owned()), + mount_point: Some(target.path().to_owned()), + }) + } + + #[allow(dead_code)] + pub fn try_new_from_string(contents: &String) -> Result { + Ok(ContextDrive { + contents: contents.to_owned(), + attributes: ContextDrive::fetch_all_values(contents.to_string()), + device: None, + mount_point: None, + }) + } + + fn fetch_all_values(contents: String) -> HashMap { + let mut res = HashMap::new(); + for line in contents.lines() { + let l = line.trim(); + if !l.starts_with("#") && l.len() > 2 { + let v: Vec<&str> = l.split("=").collect(); + if v.len() == 2 { + // Line are formatted as KEY='value', for bash-usability. This should extract + // them fairly safely by stripping off surrounding ' marks and trimming + res.insert( + v[0].to_string(), + v[1].to_string() + .strip_prefix("'") + .unwrap_or("") + .strip_suffix("'") + .unwrap_or("") + .trim() + .to_string(), + ); + } + } + } + res + } + + fn fetch_value(&self, key: &str) -> Option<&String> { + self.attributes.get(key) + } + + fn fetch_publickeys(&self) -> Result> { + let val = self.fetch_value("SSH_PUBLIC_KEY"); + if val.is_none() { + return Ok(vec![]); + } + ContextDrive::parse_publickeys(val.unwrap()) + } + + fn parse_publickeys(s: &str) -> Result> { + let res = PublicKey::parse(s)?; + Ok(vec![res]) + } + + fn fetch_networks(&self) -> Result> { + let mut interfaces: HashMap = HashMap::new(); + for (k, v) in self.attributes.iter() { + let chunks: Vec<&str> = k.splitn(2, "_").collect(); + let name = chunks[0].to_string(); + if name.starts_with("ETH") { + if !interfaces.contains_key(&name) { + interfaces.insert( + name.to_string(), + network::Interface { + name: None, + mac_address: None, + nameservers: vec![], + ip_addresses: vec![], + routes: vec![], + bond: None, + priority: 10, + unmanaged: false, + }, + ); + } + let int = interfaces.get_mut(chunks[0]).unwrap(); + match chunks[1] { + "MAC" => { + int.mac_address = Some(v.parse::().unwrap()); + } + "IP" => { + // Break out the mask value into a prefix-length from a different attribute + let mask_attr_name = &(name.clone() + "_MASK"); + let prefix_length = ipnetwork::ip_mask_to_prefix( + self.fetch_value(mask_attr_name) + .unwrap() + .parse::() + .unwrap(), + ) + .unwrap(); + let address = IpNetwork::V4( + Ipv4Network::new(v.parse::().unwrap(), prefix_length) + .unwrap(), + ); + int.ip_addresses.push(address); + } + "GATEWAY" => int.routes.push(network::NetworkRoute { + destination: IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(0, 0, 0, 0), 0).unwrap(), + ), + gateway: v.parse().unwrap(), + }), + "IP6" => { + let mask_attr_name = &(name.clone() + "_IP6_PREFIX_LENGTH"); + let prefix_length = self + .fetch_value(mask_attr_name) + .unwrap() + .parse::() + .unwrap(); + let address = IpNetwork::V6( + Ipv6Network::new(v.parse::().unwrap(), prefix_length) + .unwrap(), + ); + int.ip_addresses.push(address); + } + "DNS" => { + let nameservers: Vec = + v.split(" ").map(|d| d.parse::().unwrap()).collect(); + int.nameservers.extend_from_slice(&nameservers); + } + _ => {} + }; + } + } + let mut res: Vec = Vec::new(); + for v in interfaces.values() { + res.push(v.to_owned()); + } + Ok(res) + } + + fn mount_ro(source: &Path, target: &Path, fstype: &str) -> Result<()> { + mount::mount( + Some(source), + target, + Some(fstype), + mount::MsFlags::MS_RDONLY, + None::<&str>, + ) + .chain_err(|| { + format!( + "failed to read-only mount source '{:?}' to target '{:?}' with filetype '{}'", + source, target, fstype + ) + }) + } + + fn unmount(target: &Path) -> Result<()> { + mount::umount(target).chain_err(|| format!("failed to unmount target '{:?}'", target)) + } +} + +impl MetadataProvider for ContextDrive { + fn attributes(&self) -> Result> { + let mut res: HashMap = HashMap::new(); + for (k, v) in self.attributes.clone() { + res.insert(format!("{}{}", ENV_PREFIX, k), v); + } + Ok(res) + } + + fn hostname(&self) -> Result> { + let hostname = self.fetch_value("SET_HOSTNAME"); + if hostname.is_some() { + return Ok(Some(hostname.unwrap().to_owned())); + } + Ok(None) + } + + fn ssh_keys(&self) -> Result> { + self.fetch_publickeys() + } + + fn networks(&self) -> Result> { + self.fetch_networks() + } + + fn virtual_network_devices(&self) -> Result> { + warn!("virtual network devices metadata requested, but not supported on this platform"); + Ok(vec![]) + } + + fn boot_checkin(&self) -> Result<()> { + warn!("boot check-in requested, but not supported on this platform"); + Ok(()) + } +} + +impl ::std::ops::Drop for ContextDrive { + fn drop(&mut self) { + if self.mount_point.is_some() { + let path = self.mount_point.as_ref(); + ContextDrive::unmount(&path.unwrap()).unwrap(); + } + } +} diff --git a/src/providers/opennebula/tests.rs b/src/providers/opennebula/tests.rs new file mode 100644 index 00000000..e1eb4219 --- /dev/null +++ b/src/providers/opennebula/tests.rs @@ -0,0 +1,150 @@ +/* +Copyright 2020 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use crate::network; +use crate::providers::opennebula::ContextDrive; +use crate::providers::MetadataProvider; +use ipnetwork::{IpNetwork, Ipv4Network}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[test] +fn test_new() { + let input = " + # Context variables generated by OpenNebula + NETWORK='YES' + SSH_PUBLIC_KEY='ssh-rsa AAAABc123' + " + .to_string(); + ContextDrive::try_new_from_string(&input).unwrap(); +} + +#[test] +fn test_ssh_keys() { + let input = " + # Context variables generated by OpenNebula + NETWORK='YES' + SSH_PUBLIC_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKR89FoY+zBJX0i0V0MIL3BrTjcSllSO4bKKQ2ozB5R3' + ".to_string(); + let drive = ContextDrive::try_new_from_string(&input).unwrap(); + let keys = drive.ssh_keys().unwrap(); + assert_eq!( + keys[0].fingerprint(), + "KA5yzDbmpEQtDpz0sXVYbsqv6mM9cX0CVqojc9eQiFM" + ) +} + +#[test] +fn test_networks() { + let input = " + DISK_ID='1' + ETH0_CONTEXT_FORCE_IPV4='' + ETH0_DNS='8.8.8.8 8.8.4.4' + ETH0_EXTERNAL='' + ETH0_GATEWAY='192.168.2.1' + ETH0_GATEWAY6='' + ETH0_IP='192.168.2.50' + ETH0_IP6='2001:db8:1234:5678::abcd' + ETH0_IP6_PREFIX_LENGTH='64' + ETH0_IP6_ULA='' + ETH0_MAC='02:00:c0:a8:02:32' + ETH0_MASK='255.255.255.0' + ETH0_METRIC='' + ETH0_METRIC6='' + ETH0_MTU='' + ETH0_NETWORK='192.168.2.0' + ETH0_SEARCH_DOMAIN='' + ETH0_VLAN_ID='1026' + ETH0_VROUTER_IP='' + ETH0_VROUTER_IP6='' + ETH0_VROUTER_MANAGEMENT='' + NETWORK='YES' + " + .to_string(); + let drive = ContextDrive::try_new_from_string(&input).unwrap(); + let networks = drive.networks().unwrap(); + println!("{:?}", networks[0]); + // Basic checks + assert_eq!(networks.clone().len(), 1); + assert_eq!(networks.clone()[0].ip_addresses.len(), 2); + // MAC + assert_eq!( + networks.clone()[0].mac_address.unwrap().to_string(), + "02:00:c0:a8:02:32" + ); + // Nameservers + assert_eq!(networks.clone()[0].nameservers.len(), 2); + assert_eq!( + networks.clone()[0].nameservers[0], + Ipv4Addr::new(8, 8, 8, 8) + ); + assert_eq!( + networks.clone()[0].nameservers[1], + Ipv4Addr::new(8, 8, 4, 4) + ); + // IPv4 + + assert!(networks.clone()[0] + .ip_addresses + .contains(&IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 2, 50)), 24).unwrap())); + assert_eq!( + networks.clone()[0].routes[0], + network::NetworkRoute { + destination: IpNetwork::V4(Ipv4Network::new(Ipv4Addr::new(0, 0, 0, 0), 0).unwrap()), + gateway: IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1)), + } + ); + // IPv6 + assert!(networks.clone()[0].ip_addresses.contains( + &IpNetwork::new( + IpAddr::V6(Ipv6Addr::new(8193, 3512, 4660, 22136, 0, 0, 0, 43981)), + 64 + ) + .unwrap() + )); +} + +#[test] +fn test_hostname() { + let input = " + SET_HOSTNAME='flatcar-test' + " + .to_string(); + let drive = ContextDrive::try_new_from_string(&input).unwrap(); + assert_eq!(drive.hostname().unwrap().unwrap(), "flatcar-test"); +} + +#[test] +fn test_hostname_none() { + let input = " + NETWORK='YES' + " + .to_string(); + let drive = ContextDrive::try_new_from_string(&input).unwrap(); + assert_eq!(drive.hostname().unwrap(), None); +} + +#[test] +fn test_attributes() { + let input = " + IS_SPECIAL='YES' + " + .to_string(); + let drive = ContextDrive::try_new_from_string(&input).unwrap(); + assert_eq!( + drive.attributes().unwrap().get("ONE_IS_SPECIAL").unwrap(), + "YES" + ) +} diff --git a/systemd/afterburn-sshkeys@.service.in b/systemd/afterburn-sshkeys@.service.in index 9735cc5f..7ad489f7 100644 --- a/systemd/afterburn-sshkeys@.service.in +++ b/systemd/afterburn-sshkeys@.service.in @@ -11,6 +11,7 @@ ConditionKernelCommandLine=|ignition.platform.id=azure ConditionKernelCommandLine=|ignition.platform.id=digitalocean ConditionKernelCommandLine=|ignition.platform.id=exoscale ConditionKernelCommandLine=|ignition.platform.id=gcp +ConditionKernelCommandLine=|ignition.platform.id=one ConditionKernelCommandLine=|ignition.platform.id=packet ConditionKernelCommandLine=|ignition.platform.id=vultr