Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider: add boot check-in on azure and packet #147

Merged
merged 5 commits into from
Mar 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use reqwest::header;
use serde_json;

error_chain!{
links {
Expand All @@ -24,6 +25,7 @@ error_chain!{
XmlDeserialize(::serde_xml_rs::Error);
Base64Decode(::base64::DecodeError);
Io(::std::io::Error);
Json(serde_json::Error);
Reqwest(::reqwest::Error);
OpensslStack(::openssl::error::ErrorStack);
HeaderValue(header::InvalidHeaderValue);
Expand Down
11 changes: 11 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const CMDLINE_OEM_FLAG: &str = "coreos.oem.id";
struct Config {
provider: String,
attributes_file: Option<String>,
check_in: bool,
ssh_keys_user: Option<String>,
hostname_file: Option<String>,
network_units_dir: Option<String>,
Expand Down Expand Up @@ -110,6 +111,12 @@ fn run() -> Result<()> {
.map_or(Ok(()), |x| metadata.write_network_units(x))
.chain_err(|| "writing network units")?;

// perform boot check-in.
if config.check_in {
metadata.boot_checkin()
.chain_err(|| "checking-in instance boot to cloud provider")?;
}

debug!("Done!");

Ok(())
Expand Down Expand Up @@ -142,6 +149,9 @@ fn init() -> Result<Config> {
.long("attributes")
.help("The file into which the metadata attributes are written")
.takes_value(true))
.arg(Arg::with_name("check-in")
lucab marked this conversation as resolved.
Show resolved Hide resolved
.long("check-in")
.help("Check-in this instance boot with the cloud provider"))
.arg(Arg::with_name("cmdline")
.long("cmdline")
.help("Read the cloud provider from the kernel cmdline"))
Expand Down Expand Up @@ -174,6 +184,7 @@ fn init() -> Result<Config> {
}
},
attributes_file: matches.value_of("attributes").map(String::from),
check_in: matches.is_present("check-in"),
ssh_keys_user: matches.value_of("ssh-keys").map(String::from),
hostname_file: matches.value_of("hostname").map(String::from),
network_units_dir: matches.value_of("network-units").map(String::from),
Expand Down
82 changes: 82 additions & 0 deletions src/providers/azure/mock_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use mockito::{self, Matcher};
use providers::{azure, MetadataProvider};

#[test]
fn test_boot_checkin() {
let fab_version = "/?comp=versions";
let ver_body = r#"<?xml version="1.0" encoding="utf-8"?>
<Versions>
<Preferred>
<Version>2015-04-05</Version>
</Preferred>
<Supported>
<Version>2015-04-05</Version>
<Version>2012-11-30</Version>
<Version>2012-09-15</Version>
<Version>2012-05-15</Version>
<Version>2011-12-31</Version>
<Version>2011-10-15</Version>
<Version>2011-08-31</Version>
<Version>2011-04-07</Version>
<Version>2010-12-15</Version>
<Version>2010-28-10</Version>
</Supported>
</Versions>"#;
let m_version = mockito::mock("GET", fab_version)
.with_body(ver_body)
.with_status(200)
.create();

let fab_goalstate = "/machine/?comp=goalstate";
let gs_body = r#"<?xml version="1.0" encoding="utf-8"?>
<GoalState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="goalstate10.xsd">
<Version>2012-11-30</Version>
<Incarnation>1</Incarnation>
<Machine>
<ExpectedState>Started</ExpectedState>
<StopRolesDeadlineHint>300000</StopRolesDeadlineHint>
<LBProbePorts>
<Port>16001</Port>
</LBProbePorts>
<ExpectHealthReport>FALSE</ExpectHealthReport>
</Machine>
<Container>
<ContainerId>a511aa6d-29e7-4f53-8788-55655dfe848f</ContainerId>
<RoleInstanceList>
<RoleInstance>
<InstanceId>f6cd1d7ef1644557b9059345e5ba890c.lars-test-1</InstanceId>
<State>Started</State>
<Configuration>
<HostingEnvironmentConfig>http://100.115.176.3:80/machine/a511aa6d-29e7-4f53-8788-55655dfe848f/f6cd1d7ef1644557b9059345e5ba890c.lars%2Dtest%2D1?comp=config&amp;type=hostingEnvironmentConfig&amp;incarnation=1</HostingEnvironmentConfig>
<SharedConfig>http://100.115.176.3:80/machine/a511aa6d-29e7-4f53-8788-55655dfe848f/f6cd1d7ef1644557b9059345e5ba890c.lars%2Dtest%2D1?comp=config&amp;type=sharedConfig&amp;incarnation=1</SharedConfig>
<ExtensionsConfig>http://100.115.176.3:80/machine/a511aa6d-29e7-4f53-8788-55655dfe848f/f6cd1d7ef1644557b9059345e5ba890c.lars%2Dtest%2D1?comp=config&amp;type=extensionsConfig&amp;incarnation=1</ExtensionsConfig>
<FullConfig>http://100.115.176.3:80/machine/a511aa6d-29e7-4f53-8788-55655dfe848f/f6cd1d7ef1644557b9059345e5ba890c.lars%2Dtest%2D1?comp=config&amp;type=fullConfig&amp;incarnation=1</FullConfig>
<Certificates>http://100.115.176.3:80/machine/a511aa6d-29e7-4f53-8788-55655dfe848f/f6cd1d7ef1644557b9059345e5ba890c.lars%2Dtest%2D1?comp=certificates&amp;incarnation=1</Certificates>
<ConfigName>f6cd1d7ef1644557b9059345e5ba890c.0.f6cd1d7ef1644557b9059345e5ba890c.0.lars-test-1.1.xml</ConfigName>
</Configuration>
</RoleInstance>
</RoleInstanceList>
</Container>
</GoalState>
"#;
let m_goalstate = mockito::mock("GET", fab_goalstate)
.with_body(gs_body)
.with_status(200)
.create();

let fab_health = "/machine/?comp=health";
let m_health = mockito::mock("POST", fab_health)
.match_header("content-type", Matcher::Regex("text/xml".to_string()))
.match_header("x-ms-version", Matcher::Regex("2012-11-30".to_string()))
.match_body(Matcher::Regex("<State>Ready</State>".to_string()))
.with_status(200)
.create();

let provider = azure::Azure::try_new();
let r = provider.unwrap().boot_checkin();

m_version.assert();
m_goalstate.assert();
m_health.assert();
r.unwrap();
}
82 changes: 75 additions & 7 deletions src/providers/azure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ use errors::*;
use network;
use providers::MetadataProvider;
use retry;
use util;

#[cfg(test)]
mod mock_tests;

static HDR_AGENT_NAME: &str = "x-ms-agent-name";
static HDR_VERSION: &str = "x-ms-version";
static HDR_CIPHER_NAME: &str = "x-ms-cipher-name";
static HDR_CERT: &str = "x-ms-guest-agent-public-x509-cert";

const OPTION_245: &str = "OPTION_245";
const MS_AGENT_NAME: &str = "com.coreos.metadata";
const MS_VERSION: &str = "2012-11-30";
const SMIME_HEADER: &str = "\
Expand All @@ -48,8 +49,31 @@ Content-Transfer-Encoding: base64

/// This is a known working wireserver endpoint within Azure.
/// See: https://blogs.msdn.microsoft.com/mast/2015/05/18/what-is-the-ip-address-168-63-129-16/
#[cfg(not(test))]
const FALLBACK_WIRESERVER_ADDR: [u8; 4] = [168, 63, 129, 16]; // for grep: 168.63.129.16

macro_rules! ready_state {
($container:expr, $instance:expr) => {
format!(r#"<?xml version="1.0" encoding="utf-8"?>
<Health xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<GoalStateIncarnation>1</GoalStateIncarnation>
<Container>
<ContainerId>{}</ContainerId>
<RoleInstanceList>
<Role>
<InstanceId>{}</InstanceId>
<Health>
<State>Ready</State>
</Health>
</Role>
</RoleInstanceList>
</Container>
</Health>
"#,
$container, $instance)
}
}

#[derive(Debug, Deserialize, Clone, Default)]
struct GoalState {
#[serde(rename = "Container")]
Expand All @@ -58,8 +82,10 @@ struct GoalState {

#[derive(Debug, Deserialize, Clone, Default)]
struct Container {
#[serde(rename = "ContainerId")]
pub container_id: String,
#[serde(rename = "RoleInstanceList")]
pub role_instance_list: RoleInstanceList
pub role_instance_list: RoleInstanceList,
}

#[derive(Debug, Deserialize, Clone, Default)]
Expand All @@ -71,7 +97,9 @@ struct RoleInstanceList {
#[derive(Debug, Deserialize, Clone)]
struct RoleInstance {
#[serde(rename = "Configuration")]
pub configuration: Configuration
pub configuration: Configuration,
#[serde(rename = "InstanceId")]
pub instance_id: String,
}

#[derive(Debug, Deserialize, Clone)]
Expand Down Expand Up @@ -177,11 +205,12 @@ impl Azure {
}

fn get_goal_state(&self) -> Result<GoalState> {
self.client.get(retry::Xml, format!("http://{}/machine/?comp=goalstate", self.endpoint)).send()
self.client.get(retry::Xml, format!("{}/machine/?comp=goalstate", self.fabric_base_url())).send()
.chain_err(|| "failed to get goal state")?
.ok_or_else(|| "failed to get goal state: not found response".into())
}

#[cfg(not(test))]
fn get_fabric_address() -> IpAddr {
// try to fetch from dhcp, else use fallback; this is similar to what WALinuxAgent does
Azure::get_fabric_address_from_dhcp().unwrap_or_else(|e| {
Expand All @@ -191,8 +220,9 @@ impl Azure {
})
}

#[cfg(not(test))]
fn get_fabric_address_from_dhcp() -> Result<IpAddr> {
let v = util::dns_lease_key_lookup(OPTION_245)?;
let v = ::util::dns_lease_key_lookup("OPTION_245")?;
// value is an 8 digit hex value. convert it to u32 and
// then parse that into an ip. Ipv4Addr::from(u32)
// performs conversion from big-endian
Expand All @@ -202,8 +232,24 @@ impl Azure {
Ok(IpAddr::V4(dec.into()))
}

#[cfg(not(test))]
fn fabric_base_url(&self) -> String {
format!("http://{}", self.endpoint)
}

#[cfg(test)]
fn get_fabric_address() -> IpAddr {
use std::net::Ipv4Addr;
IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))
}

#[cfg(test)]
fn fabric_base_url(&self) -> String {
::mockito::server_url().to_string()
}

fn is_fabric_compatible(&self, version: &str) -> Result<()> {
let versions: Versions = self.client.get(retry::Xml, format!("http://{}/?comp=versions", self.endpoint)).send()
let versions: Versions = self.client.get(retry::Xml, format!("{}/?comp=versions", self.fabric_base_url())).send()
.chain_err(|| "failed to get versions")?
.ok_or_else(|| "failed to get versions: not found")?;

Expand Down Expand Up @@ -294,6 +340,20 @@ impl Azure {

Ok(attributes)
}

/// Return this instance `ContainerId`.
pub(crate) fn container_id(&self) -> &str {
&self.goal_state.container.container_id
}

/// Return this instance `InstanceId`.
pub(crate) fn instance_id(&self) -> Result<&str> {
Ok(&self.goal_state.container
.role_instance_list
.role_instances.get(0)
.ok_or_else(|| "empty RoleInstanceList".to_string())?
.instance_id)
}
}

impl MetadataProvider for Azure {
Expand Down Expand Up @@ -328,4 +388,12 @@ impl MetadataProvider for Azure {
fn network_devices(&self) -> Result<Vec<network::Device>> {
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
let body = ready_state!(self.container_id(), self.instance_id()?);
let url = self.fabric_base_url() + "/machine/?comp=health";
self.client.post(retry::Xml, url, Some(body.into()))
.dispatch_post()?;
Ok(())
}
}
5 changes: 5 additions & 0 deletions src/providers/cloudstack/configdrive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ impl MetadataProvider for ConfigDrive {
fn network_devices(&self) -> Result<Vec<network::Device>> {
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
}
}

impl ::std::ops::Drop for ConfigDrive {
Expand Down
5 changes: 5 additions & 0 deletions src/providers/cloudstack/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,9 @@ impl MetadataProvider for CloudstackNetwork {
fn network_devices(&self) -> Result<Vec<network::Device>> {
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
}
}
5 changes: 5 additions & 0 deletions src/providers/digitalocean/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,9 @@ impl MetadataProvider for DigitalOceanProvider {
fn network_devices(&self) -> Result<Vec<network::Device>> {
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
}
}
5 changes: 5 additions & 0 deletions src/providers/ec2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,9 @@ impl MetadataProvider for Ec2Provider {
fn network_devices(&self) -> Result<Vec<network::Device>> {
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
}
}
5 changes: 5 additions & 0 deletions src/providers/gce/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,9 @@ impl MetadataProvider for GceProvider {
fn network_devices(&self) -> Result<Vec<network::Device>> {
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
}
}
1 change: 1 addition & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub trait MetadataProvider {
fn ssh_keys(&self) -> Result<Vec<AuthorizedKeyEntry>>;
fn networks(&self) -> Result<Vec<network::Interface>>;
fn network_devices(&self) -> Result<Vec<network::Device>>;
fn boot_checkin(&self) -> Result<()>;

fn write_attributes(&self, attributes_file_path: String) -> Result<()> {
let mut attributes_file = create_file(&attributes_file_path)?;
Expand Down
5 changes: 5 additions & 0 deletions src/providers/openstack/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,9 @@ impl MetadataProvider for OpenstackProvider {
fn network_devices(&self) -> Result<Vec<network::Device>> {
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
}
}
33 changes: 33 additions & 0 deletions src/providers/packet/mock_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use mockito::{self, Matcher};
use providers::{packet, MetadataProvider};

#[test]
fn test_boot_checkin() {
let data = packet::PacketData {
id: String::new(),
hostname: String::new(),
iqn: String::new(),
plan: String::new(),
facility: String::new(),
tags: vec![],
ssh_keys: vec![],
network: packet::PacketNetworkInfo {
interfaces: vec![],
addresses: vec![],
bonding: packet::PacketBondingMode { mode: 0 },
},
error: None,
phone_home_url: mockito::server_url(),
};
let provider = packet::PacketProvider { data };

let mock = mockito::mock("POST", "/")
.match_header("content-type", Matcher::Regex("application/json".to_string()))
.match_body("")
.with_status(200)
.create();

let r = provider.boot_checkin();
mock.assert();
r.unwrap();
}
Loading