Skip to content

Commit

Permalink
Merge pull request #982 from xinau/xinau/add-support-for-scaleway
Browse files Browse the repository at this point in the history
providers: add support for scaleway
  • Loading branch information
prestist authored Sep 21, 2023
2 parents 296d08d + b5215c2 commit ebd3186
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ The following platforms are supported, with a different set of features availabl
* powervs
- Attributes
- SSH keys
* scaleway
- Attributes
- Boot check-in
- SSH keys
* vmware
- Custom network command-line arguments
* vultr
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ nav_order: 8

Major changes:

- Add support for Scaleway

Minor changes:

Expand Down
8 changes: 8 additions & 0 deletions docs/usage/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ Cloud providers with supported metadata endpoints and their respective attribute
* powervs
- AFTERBURN_POWERVS_INSTANCE_ID
- AFTERBURN_POWERVS_LOCAL_HOSTNAME
* scaleway
- AFTERBURN_SCALEWAY_HOSTNAME
- AFTERBURN_SCALEWAY_INSTANCE_ID
- AFTERBURN_SCALEWAY_INSTANCE_TYPE
- AFTERBURN_SCALEWAY_PRIVATE_IPV4
- AFTERBURN_SCALEWAY_PUBLIC_IPV4
- AFTERBURN_SCALEWAY_PUBLIC_IPV6
- AFTERBURN_SCALEWAY_ZONE_ID
* vultr
- AFTERBURN_VULTR_HOSTNAME
- AFTERBURN_VULTR_INSTANCE_ID
Expand Down
1 change: 1 addition & 0 deletions dracut/30afterburn/afterburn-hostname.service
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ConditionKernelCommandLine=|ignition.platform.id=digitalocean
ConditionKernelCommandLine=|ignition.platform.id=exoscale
ConditionKernelCommandLine=|ignition.platform.id=hetzner
ConditionKernelCommandLine=|ignition.platform.id=ibmcloud
ConditionKernelCommandLine=|ignition.platform.id=scaleway
ConditionKernelCommandLine=|ignition.platform.id=vultr

# We order this service after sysroot has been mounted
Expand Down
2 changes: 2 additions & 0 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::providers::openstack;
use crate::providers::openstack::network::OpenstackProviderNetwork;
use crate::providers::packet::PacketProvider;
use crate::providers::powervs::PowerVSProvider;
use crate::providers::scaleway::ScalewayProvider;
use crate::providers::vmware::VmwareProvider;
use crate::providers::vultr::VultrProvider;

Expand Down Expand Up @@ -67,6 +68,7 @@ pub fn fetch_metadata(provider: &str) -> Result<Box<dyn providers::MetadataProvi
"openstack-metadata" => box_result!(OpenstackProviderNetwork::try_new()?),
"packet" => box_result!(PacketProvider::try_new()?),
"powervs" => box_result!(PowerVSProvider::try_new()?),
"scaleway" => box_result!(ScalewayProvider::try_new()?),
"vmware" => box_result!(VmwareProvider::try_new()?),
"vultr" => box_result!(VultrProvider::try_new()?),
_ => bail!("unknown provider '{}'", provider),
Expand Down
1 change: 1 addition & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod microsoft;
pub mod openstack;
pub mod packet;
pub mod powervs;
pub mod scaleway;
pub mod vmware;
pub mod vultr;

Expand Down
71 changes: 71 additions & 0 deletions src/providers/scaleway/mock_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use mockito::{self, Matcher};

use crate::providers::scaleway::ScalewayProvider;
use crate::providers::MetadataProvider;

#[test]
fn test_attributes() {
let metadata = r#"{
"commercial_type": "GP1-M",
"hostname": "frontend-0",
"id": "11111111-1111-1111-1111-111111111111",
"ipv6": {
"address": "2001:db8::1"
},
"location": {
"zone_id": "par1"
},
"private_ip": "10.0.0.2",
"public_ip": {
"address": "93.184.216.34"
},
"ssh_public_keys": []
}"#;

let want = maplit::hashmap! {
"SCALEWAY_INSTANCE_ID".to_string() => "11111111-1111-1111-1111-111111111111".to_string(),
"SCALEWAY_INSTANCE_TYPE".to_string() => "GP1-M".to_string(),
"SCALEWAY_HOSTNAME".to_string() => "frontend-0".to_string(),
"SCALEWAY_IPV4_PRIVATE".to_string() => "10.0.0.2".to_string(),
"SCALEWAY_IPV4_PUBLIC".to_string() => "93.184.216.34".to_string(),
"SCALEWAY_IPV6_PUBLIC".to_string() => "2001:db8::1".to_string(),
"SCALEWAY_ZONE_ID".to_string() => "par1".to_string(),
};

let mut server = mockito::Server::new();
server
.mock("GET", "/conf?format=json")
.with_status(200)
.with_body(metadata)
.create();

let mut provider = ScalewayProvider::try_new().unwrap();
provider.client = provider.client.max_retries(0).mock_base_url(server.url());
let got = provider.attributes().unwrap();

assert_eq!(got, want);

server.reset();
}

#[test]
fn test_boot_checkin() {
let mut server = mockito::Server::new();
let mock = server
.mock("PATCH", "/state")
.match_header(
"content-type",
Matcher::Regex("application/json".to_string()),
)
.match_body(r#"{"state_detail":"booted"}"#)
.with_status(200)
.create();

let mut provider = ScalewayProvider::try_new().unwrap();
provider.client = provider.client.max_retries(0).mock_base_url(server.url());

provider.boot_checkin().unwrap();
mock.assert();

server.reset();
}
162 changes: 162 additions & 0 deletions src/providers/scaleway/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2023 CoreOS, Inc.
//
// 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
//
// http://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.

//! Metadata fetcher for Scaleway.
//!
//! The metadata API specification follows the instance one described
//! [their docs](https://www.scaleway.com/en/developers/api/instance/#path-instances-get-an-instance)
//!
//! An implementation for the metadata retrival and boot check-in can be found
//! in the image-tools
//! [`scw-metadata-json`](https://github.com/scaleway/image-tools/blob/cloud-init-18.3%2B24.gf6249277/bases/overlay-common/usr/local/bin/scw-metadata-json)
//! and
//! [`scw-signal-state`](https://github.com/scaleway/image-tools/blob/cloud-init-18.3%2B24.gf6249277/bases/overlay-common/usr/local/sbin/scw-signal-state)

use std::collections::HashMap;

use anyhow::{anyhow, Result};
use openssh_keys::PublicKey;
use serde::Deserialize;

use crate::providers::MetadataProvider;
use crate::retry;

#[cfg(test)]
mod mock_tests;

#[derive(Clone, Deserialize)]
struct ScalewaySSHPublicKey {
key: String,
}

#[derive(Clone, Deserialize)]
struct ScalwayInterfaces {
private_ip: Option<String>,
public_ip: Option<ScalewayIPv4Public>,
ipv6: Option<ScalewayIPv6Public>,
}

#[derive(Clone, Deserialize)]
struct ScalewayIPv4Public {
address: String,
}

#[derive(Clone, Deserialize)]
struct ScalewayIPv6Public {
address: String,
}

#[derive(Clone, Deserialize)]
struct ScalewayLocation {
zone_id: String,
}

#[derive(Clone, Deserialize)]
struct ScalewayInstanceMetadata {
commercial_type: String,
hostname: String,
id: String,
#[serde(flatten)]
interfaces: ScalwayInterfaces,
location: ScalewayLocation,
ssh_public_keys: Vec<ScalewaySSHPublicKey>,
}

pub struct ScalewayProvider {
client: retry::Client,
}

impl ScalewayProvider {
pub fn try_new() -> Result<ScalewayProvider> {
let client = retry::Client::try_new()?;
Ok(ScalewayProvider { client })
}

fn fetch_metadata(&self) -> Result<ScalewayInstanceMetadata> {
let data: ScalewayInstanceMetadata = self
.client
.get(
retry::Json,
"http://169.254.42.42/conf?format=json".to_string(),
)
.send()?
.ok_or_else(|| anyhow!("not found"))?;

Ok(data)
}

fn parse_attrs(&self) -> Result<Vec<(String, String)>> {
let data = self.fetch_metadata()?;

let instance_type = data.commercial_type;
let zone_id = data.location.zone_id;

let mut attrs = vec![
("SCALEWAY_HOSTNAME".to_string(), data.hostname.clone()),
("SCALEWAY_INSTANCE_ID".to_string(), data.id.clone()),
("SCALEWAY_INSTANCE_TYPE".to_string(), instance_type.clone()),
("SCALEWAY_ZONE_ID".to_string(), zone_id.clone()),
];

if let Some(ref ip) = data.interfaces.private_ip {
attrs.push(("SCALEWAY_IPV4_PRIVATE".to_string(), ip.clone()));
}

if let Some(ref ip) = data.interfaces.public_ip {
attrs.push(("SCALEWAY_IPV4_PUBLIC".to_string(), ip.address.clone()));
}

if let Some(ref ip) = data.interfaces.ipv6 {
attrs.push(("SCALEWAY_IPV6_PUBLIC".to_string(), ip.address.clone()));
}

Ok(attrs)
}
}

impl MetadataProvider for ScalewayProvider {
fn attributes(&self) -> Result<HashMap<String, String>> {
let attrs = self.parse_attrs()?;
Ok(attrs.into_iter().collect())
}

fn boot_checkin(&self) -> Result<()> {
self.client
.patch(
retry::Json,
"http://169.254.42.42/state".to_string(),
Some(r#"{"state_detail":"booted"}"#.into()),
)
.dispatch_patch()?;
Ok(())
}

fn hostname(&self) -> Result<Option<String>> {
let data = self.fetch_metadata()?;
Ok(Some(data.hostname.clone()))
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
let mut out = Vec::new();

let data = self.fetch_metadata()?;

for key in data.ssh_public_keys {
let key = PublicKey::parse(&key.key)?;
out.push(key);
}

Ok(out)
}
}
44 changes: 44 additions & 0 deletions src/retry/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,23 @@ impl Client {
}
}

pub fn patch<D>(&self, d: D, url: String, body: Option<Cow<str>>) -> RequestBuilder<D>
where
D: Deserializer,
{
RequestBuilder {
url,
body: body.map(Cow::into_owned),
d,
client: self.client.clone(),
headers: self.headers.clone(),
retry: self.retry.clone(),
return_on_404: self.return_on_404,
#[cfg(test)]
mock_base_url: self.mock_base_url.clone(),
}
}

pub fn post<D>(&self, d: D, url: String, body: Option<Cow<str>>) -> RequestBuilder<D>
where
D: Deserializer,
Expand Down Expand Up @@ -257,6 +274,33 @@ where
})
}

pub fn dispatch_patch(self) -> Result<reqwest::StatusCode> {
let url = self.parse_url()?;

self.retry.clone().retry(|attempt| {
let mut builder = blocking::Client::new()
.patch(url.clone())
.headers(self.headers.clone())
.header(header::CONTENT_TYPE, self.d.content_type());
if let Some(ref content) = self.body {
builder = builder.body(content.clone());
};
let req = builder.build().context("failed to build PATCH request")?;

info!("Patching {}: Attempt #{}", req.url(), attempt + 1);
let status = self
.client
.execute(req)
.context("failed to PATCH request")?
.status();
if status.is_success() {
Ok(status)
} else {
Err(anyhow!("PATCH failed: {}", status))
}
})
}

pub fn dispatch_put<T>(self) -> Result<Option<T>>
where
T: for<'de> serde::Deserialize<'de>,
Expand Down
1 change: 1 addition & 0 deletions systemd/afterburn-checkin.service
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Description=Afterburn (Check In)
Documentation=https://coreos.github.io/afterburn/
ConditionKernelCommandLine=|ignition.platform.id=azure
ConditionKernelCommandLine=|ignition.platform.id=azurestack
ConditionKernelCommandLine=|ignition.platform.id=scaleway
After=network.target
After=multi-user.target boot-complete.target

Expand Down
1 change: 1 addition & 0 deletions systemd/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ConditionKernelCommandLine=|ignition.platform.id=gcp
ConditionKernelCommandLine=|ignition.platform.id=hetzner
ConditionKernelCommandLine=|ignition.platform.id=ibmcloud
ConditionKernelCommandLine=|ignition.platform.id=openstack
ConditionKernelCommandLine=|ignition.platform.id=scaleway
ConditionKernelCommandLine=|ignition.platform.id=packet
ConditionKernelCommandLine=|ignition.platform.id=powervs
ConditionKernelCommandLine=|ignition.platform.id=vultr
Expand Down

0 comments on commit ebd3186

Please sign in to comment.