Skip to content
Draft
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
3 changes: 3 additions & 0 deletions build_library/oem/outscale/grub.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Flatcar GRUB settings

set oem_id="outscale"
6 changes: 6 additions & 0 deletions build_library/vm_image_util.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ VALID_IMG_TYPES=(
openstack
openstack_mini
oraclecloud
outscale
parallels
proxmoxve
pxe
Expand Down Expand Up @@ -161,6 +162,11 @@ IMG_oraclecloud_DISK_FORMAT=qcow2
IMG_oraclecloud_DISK_LAYOUT=vm
IMG_oraclecloud_OEM_SYSEXT=oraclecloud

## Outscale
IMG_outscale_DISK_FORMAT=qcow2
IMG_outscale_DISK_LAYOUT=vm
IMG_outscale_OEM_SYSEXT=outscale
Comment on lines +165 to +168

## pxe, which is an cpio image
IMG_pxe_DISK_FORMAT=cpio
IMG_pxe_PARTITIONED_IMG=0
Expand Down
2 changes: 1 addition & 1 deletion ci-automation/vms.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ function _vm_build_impl() {
COMPRESSION_FORMAT="zip"
elif [[ "${format}" =~ ^(scaleway|kubevirt|proxmoxve|stackit|exoscale|oraclecloud)$ ]];then
COMPRESSION_FORMAT="none"
elif [[ "${format}" =~ ^(akamai)$ ]];then
elif [[ "${format}" =~ ^(akamai|outscale)$ ]];then
COMPRESSION_FORMAT="gz"
fi
./run_sdk_container -n "${vms_container}" -C "${packages_image}" \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ ConditionKernelCommandLine=|coreos.oem.id=openstack

ConditionKernelCommandLine=|flatcar.oem.id=oraclecloud

ConditionKernelCommandLine=|flatcar.oem.id=outscale

ConditionKernelCommandLine=|flatcar.oem.id=scaleway

ConditionKernelCommandLine=|flatcar.oem.id=hetzner
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2026 Flatcar Maintainers. All rights reserved.
Comment thread
Copilot marked this conversation as resolved.
# Distributed under the terms of the GNU General Public License v2

EAPI=8

DESCRIPTION="OEM suite for Outscale"
HOMEPAGE="https://en.outscale.com/cloud-experience/cloud-services/"

LICENSE="metapackage"
SLOT="0"
KEYWORDS="amd64 arm64"

SYSEXT_NAME="Outscale"
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
From 716b1359dbfb40334c631681d53dbf18be78979f Mon Sep 17 00:00:00 2001
From: Mohammad AMLA <mohammad.amla@numspot.com>
Date: Wed, 12 Aug 2026 17:11:31 +0200
Subject: [PATCH] providers: Add Outscale support

---
dracut/30afterburn/afterburn-hostname.service | 1 +
src/metadata.rs | 2 +
src/providers/mod.rs | 1 +
src/providers/outscale/mock_tests.rs | 118 ++++++++++++++++
src/providers/outscale/mod.rs | 132 ++++++++++++++++++
systemd/afterburn-sshkeys@.service.in | 1 +
9 files changed, 268 insertions(+)
create mode 100644 src/providers/outscale/mock_tests.rs
create mode 100644 src/providers/outscale/mod.rs

diff --git a/dracut/30afterburn/afterburn-hostname.service b/dracut/30afterburn/afterburn-hostname.service
index 9b0c6eae9..45356b7e5 100644
--- a/dracut/30afterburn/afterburn-hostname.service
+++ b/dracut/30afterburn/afterburn-hostname.service
@@ -13,6 +13,7 @@ ConditionKernelCommandLine=|ignition.platform.id=hetzner
ConditionKernelCommandLine=|ignition.platform.id=ibmcloud
ConditionKernelCommandLine=|ignition.platform.id=kubevirt
ConditionKernelCommandLine=|ignition.platform.id=oraclecloud
+ConditionKernelCommandLine=|ignition.platform.id=outscale
ConditionKernelCommandLine=|ignition.platform.id=proxmoxve
ConditionKernelCommandLine=|ignition.platform.id=scaleway
ConditionKernelCommandLine=|ignition.platform.id=upcloud
diff --git a/src/metadata.rs b/src/metadata.rs
index f43f94210..34fca8216 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -32,6 +32,7 @@ use crate::providers::microsoft::azurestack::AzureStack;
use crate::providers::openstack;
use crate::providers::openstack::network::OpenstackProviderNetwork;
use crate::providers::oraclecloud::OracleCloudProvider;
+use crate::providers::outscale::OutscaleProvider;
use crate::providers::packet::PacketProvider;
use crate::providers::powervs::PowerVSProvider;
use crate::providers::proxmoxve;
@@ -71,6 +72,7 @@ pub fn fetch_metadata(provider: &str) -> Result<Box<dyn providers::MetadataProvi
"kubevirt" => kubevirt::try_new_provider_else_noop(),
"openstack" => openstack::try_config_drive_else_network(),
"openstack-metadata" => box_result!(OpenstackProviderNetwork::try_new()?),
+ "outscale" => box_result!(OutscaleProvider::try_new()?),
"oraclecloud" => box_result!(OracleCloudProvider::try_new()?),
"packet" => box_result!(PacketProvider::try_new()?),
"powervs" => box_result!(PowerVSProvider::try_new()?),
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index 0fdacd657..5511d0168 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -38,6 +38,7 @@ pub mod microsoft;
pub mod noop;
pub mod openstack;
pub mod oraclecloud;
+pub mod outscale;
pub mod packet;
pub mod powervs;
pub mod proxmoxve;
diff --git a/src/providers/outscale/mock_tests.rs b/src/providers/outscale/mock_tests.rs
new file mode 100644
index 000000000..1f69263b9
--- /dev/null
+++ b/src/providers/outscale/mock_tests.rs
@@ -0,0 +1,118 @@
+use crate::providers::outscale::OutscaleProvider;
+use crate::providers::MetadataProvider;
+use mockito;
+
+#[test]
+fn test_hostname() {
+ let ep = "/latest/meta-data/hostname";
+ let hostname = "test-hostname";
+
+ let mut server = mockito::Server::new();
+ let mut provider = OutscaleProvider::try_new().unwrap();
+ provider.client = provider.client.max_retries(0).mock_base_url(server.url());
+
+ server.mock("GET", ep).with_status(503).create();
+ provider.hostname().unwrap_err();
+
+ server
+ .mock("GET", ep)
+ .with_status(200)
+ .with_body(hostname)
+ .create();
+ let v = provider.hostname().unwrap();
+ assert_eq!(v, Some(hostname.to_string()));
+
+ server.mock("GET", ep).with_status(404).create();
+ let v = provider.hostname().unwrap();
+ assert_eq!(v, None);
+
+ server
+ .mock("GET", ep)
+ .with_status(200)
+ .with_body("")
+ .create();
+ let v = provider.hostname().unwrap();
+ assert_eq!(v, None);
+
+ server.reset();
+ provider.hostname().unwrap_err();
+}
+
+#[test]
+fn test_pubkeys() {
+ let mut server = mockito::Server::new();
+ let mut provider = OutscaleProvider::try_new().unwrap();
+ provider.client = provider.client.max_retries(0).mock_base_url(server.url());
+
+ server.mock("GET", "/latest/meta-data/public-keys/0/openssh-key")
+ .with_status(200)
+ .with_body("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCsXe6CfHl45kCIzMF92VhDf2NpBWUyS1+IiTtxm5a83mT9730Hb8xim7GYeJu47kiESw2DAN8vNJ/Irg0apZ217ah2rXXjPQuWYSXuEuap8yLBSjqw8exgqVj/kzW+YqmnHASxI13eoFDxTQQGzyqbqowvxu/5gQmDwBmNAa9bT809ziB/qmpS1mD6qyyFDpR23kUwu3TkgAbwMXBDoqK+pdwfaF9uo9XaLHNEH8lD5BZuG2BeDafm2o76DhNSo83MvcCPNXKLxu3BbX/FCMFO6O8RRqony4i91fEV1b8TbXrbJz1bwEYEnJRvmjnqI/389tQFeYvplXR2WdT9PCKyEAG+j8y6XgecIcdTqV/7gFfak1mp2S7mYHZDnXixsn3MjCP/cIxxJVDitKusnj1TdFqtSXl4tqGccbg/5Sqnt/EVSK4bGwwBxv/YmE0P9cbXLxuEVI0JYzgrQvC8TtUgd8kUu2jqi1/Yj9IWm3aFsl/hhh8YwYrv/gm8PV0TxkM= root@example1\n
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDj6FBVgkTt7/DB93VVLk6304Nx7WUjLBJDSCh38zjCimHUpeo9uYDxflfu2N1CLtrSImIKBVP/JRy9g7K4zmRAH/wXw2UxYziX+hZoFIpbW3GmYQqhjx2lDvIRXJI7blhHhTUNWX5f10lFAYOLqA9J859AB1w7ND09+MS3jQgSazCx17h+QZ0qQ6kLSfnXw9PMUOE1Xba9hD1nYj14ryTVj9jrFPMFuUfXdb/G9lsDJ+cGvdE2/RMuPfDmEdo04zvZ5fQJJKvS7OyAuYev4Y+JC8MhEr756ITDZ17yq4BEMo/8rNPxZ5Von/8xnvry+8/2C3ep9rZyHtCwpRb6WT6TndV2ddXKhEIneyd1XiOcWPJguHj5vSoMN3mo8k2PvznGauvxBstvpjUSFLQu869/ZQwyMnbQi3wnkJk5CpLXePXn1J9njocJjt8+SKGijmmIAsmYosx8gmmu3H1mvq9Wi0qqWDITMm+J24AZBEPBhwVrjhLZb5MKxylF6JFJJBs= root@example2")
+ .create();
+
+ let keys = provider.ssh_keys().unwrap();
+ assert_ne!(keys, vec![]);
+ assert_eq!(keys.len(), 2);
+
+ assert_eq!(keys[0].options, None);
+ assert_eq!(keys[0].comment, Some("root@example1".to_string()));
+
+ assert_eq!(keys[1].options, None);
+ assert_eq!(keys[1].comment, Some("root@example2".to_string()));
+
+ server.reset();
+ provider.ssh_keys().unwrap_err();
+}
+
+#[test]
+fn test_attributes() {
+ let instance_id = "i-be1dcf1a";
+ let ami_id = "ami-12345678";
+ let instance_type = "c4.large";
+ let availability_zone = "eu-west-2a";
+ let local_ipv4 = "10.0.0.1";
+ let reservation_id = "r-12345678";
+ let hostname = "ip-10-0-0-1.eu-west-2.compute.outscale.com";
+
+ let endpoints = maplit::btreemap! {
+ "/latest/meta-data/instance-id" => instance_id,
+ "/latest/meta-data/ami-id" => ami_id,
+ "/latest/meta-data/instance-type" => instance_type,
+ "/latest/meta-data/placement/availability-zone" => availability_zone,
+ "/latest/meta-data/local-ipv4" => local_ipv4,
+ "/latest/meta-data/reservation-id" => reservation_id,
+ "/latest/meta-data/hostname" => hostname,
+ };
+
+ let mut server = mockito::Server::new();
+ for (endpoint, body) in endpoints {
+ server
+ .mock("GET", endpoint)
+ .with_status(200)
+ .with_body(body)
+ .create();
+ }
+
+ let attributes = maplit::hashmap! {
+ "OUTSCALE_INSTANCE_ID".to_string() => instance_id.to_string(),
+ "OUTSCALE_AMI_ID".to_string() => ami_id.to_string(),
+ "OUTSCALE_INSTANCE_TYPE".to_string() => instance_type.to_string(),
+ "OUTSCALE_AVAILABILITY_ZONE".to_string() => availability_zone.to_string(),
+ "OUTSCALE_LOCAL_IPV4".to_string() => local_ipv4.to_string(),
+ "OUTSCALE_RESERVATION_ID".to_string() => reservation_id.to_string(),
+ "OUTSCALE_HOSTNAME".to_string() => hostname.to_string(),
+ };
+
+ let client = crate::retry::Client::try_new()
+ .unwrap()
+ .max_retries(0)
+ .return_on_404(true)
+ .mock_base_url(server.url());
+ let provider = OutscaleProvider { client };
+
+ let v = provider.attributes().unwrap();
+ assert_eq!(v, attributes);
+
+ server.reset();
+ provider.attributes().unwrap_err();
+}
diff --git a/src/providers/outscale/mod.rs b/src/providers/outscale/mod.rs
new file mode 100644
index 000000000..8e925df1a
--- /dev/null
+++ b/src/providers/outscale/mod.rs
@@ -0,0 +1,132 @@
+// Copyright 2025 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.
+
+//! Outscale provider metadata fetcher
+//! This provider is selected via the platform ID `outscale`.
+//! The metadata endpoint is documented at https://docs.outscale.com/en/userguide/Accessing-the-Metadata-and-User-Data-of-a-VM.html.
+
+use anyhow::Result;
+use openssh_keys::PublicKey;
+use slog_scope::error;
+use std::collections::HashMap;
+
+use crate::providers::MetadataProvider;
+use crate::retry;
+
+#[cfg(test)]
+mod mock_tests;
+
+#[derive(Clone, Debug)]
+pub struct OutscaleProvider {
+ client: retry::Client,
+}
+
+impl OutscaleProvider {
+ pub fn try_new() -> Result<OutscaleProvider> {
+ let client = retry::Client::try_new()?.return_on_404(true);
+
+ Ok(OutscaleProvider { client })
+ }
+
+ fn endpoint_for(name: &str) -> String {
+ format!("http://169.254.169.254/latest/meta-data/{name}")
+ }
+
+ fn fetch_attribute(
+ &self,
+ map: &mut HashMap<String, String>,
+ key: &str,
+ endpoint: &str,
+ ) -> Result<()> {
+ let content: Option<String> = self
+ .client
+ .get(retry::Raw, Self::endpoint_for(endpoint))
+ .send()?;
+
+ if let Some(value) = content {
+ if !value.is_empty() {
+ map.insert(key.to_string(), value);
+ }
+ }
+
+ Ok(())
+ }
+
+ fn fetch_ssh_keys(&self) -> Result<Vec<String>> {
+ let entries: Option<String> = self
+ .client
+ .get(retry::Raw, Self::endpoint_for("public-keys/0/openssh-key"))
+ .send()?;
+ let keys_list = entries.unwrap_or_default();
+
+ let mut keys = Vec::new();
+ for key in keys_list.lines() {
+ let key = key.to_string();
+ keys.push(key)
+ }
+
+ Ok(keys)
+ }
+
+ fn fetch_hostname(&self) -> Result<Option<String>> {
+ let value: Option<String> = self
+ .client
+ .get(retry::Raw, Self::endpoint_for("hostname"))
+ .send()?;
+
+ let hostname = value.unwrap_or_default();
+ if hostname.is_empty() {
+ return Ok(None);
+ }
+
+ Ok(Some(hostname))
+ }
+}
+
+impl MetadataProvider for OutscaleProvider {
+ fn attributes(&self) -> Result<HashMap<String, String>> {
+ let mut out = HashMap::with_capacity(7);
+
+ self.fetch_attribute(&mut out, "OUTSCALE_INSTANCE_ID", "instance-id")?;
+ self.fetch_attribute(&mut out, "OUTSCALE_AMI_ID", "ami-id")?;
+ self.fetch_attribute(&mut out, "OUTSCALE_INSTANCE_TYPE", "instance-type")?;
+ self.fetch_attribute(
+ &mut out,
+ "OUTSCALE_AVAILABILITY_ZONE",
+ "placement/availability-zone",
+ )?;
+ self.fetch_attribute(&mut out, "OUTSCALE_LOCAL_IPV4", "local-ipv4")?;
+ self.fetch_attribute(&mut out, "OUTSCALE_RESERVATION_ID", "reservation-id")?;
+ self.fetch_attribute(&mut out, "OUTSCALE_HOSTNAME", "hostname")?;
+
+ Ok(out)
+ }
+
+ fn hostname(&self) -> Result<Option<String>> {
+ self.fetch_hostname()
+ }
+
+ fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
+ let keys = self.fetch_ssh_keys()?;
+ let mut out = Vec::with_capacity(keys.len());
+ for key in keys {
+ match PublicKey::parse(&key) {
+ Ok(pk) => out.push(pk),
+ Err(e) => error!("failed to parse SSH Public Key: {}", e),
+ };
+ }
+
+ Ok(out)
+ }
+}
diff --git a/systemd/afterburn-sshkeys@.service.in b/systemd/afterburn-sshkeys@.service.in
index eec5d3410..e96c43bc3 100644
--- a/systemd/afterburn-sshkeys@.service.in
+++ b/systemd/afterburn-sshkeys@.service.in
@@ -16,6 +16,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=outscale
ConditionKernelCommandLine=|ignition.platform.id=oraclecloud
ConditionKernelCommandLine=|ignition.platform.id=proxmoxve
ConditionKernelCommandLine=|ignition.platform.id=scaleway
Loading
Loading