Skip to content
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,32 @@ if ($errors -or !$pluginExecution) {
# These tests make sure that the environment is changed according to the specifications
Invoke-Pester test-resources/functional-tests -Output Detailed -FullNameFilter TestVerifyAfterAllPlugins
```

### Oracle Cloud IMDSv2

The `oracle` fixture runs a local metadata server on `127.0.0.1:18080` and
serves example OCI metadata from `oracle/metadata/opc/v2`. The server returns
HTTP 401 unless every request includes the IMDSv2 header:

```text
Authorization: Bearer Oracle
```

The precondition tests explicitly verify that a request without this header is
rejected with HTTP 401 and that a request with the header returns the expected
instance ID. The end-to-end execution then verifies that `OracleCloudService`
adds the header itself and successfully processes the returned user data.

Run it through the Cloudbase-Init functional-test workflow with:

```powershell
$ENV:CLOUD = "oracle"
$ENV:TEST_ARCHITECTURE = "x64"
Invoke-Pester functional-tests -Output Detailed -FullNameFilter TestVerifyBeforeAllPlugins

& cloudbase-init.exe `
--noreset_service_password `
--config-file ./oracle/cloudbase-init.conf

Invoke-Pester functional-tests -Output Detailed -FullNameFilter TestVerifyAfterAllPlugins
```
97 changes: 93 additions & 4 deletions functional-tests/CloudbaseInit.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,56 @@ function after.cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin {
# which is not available on the test environment
}

function before.cloudbaseinit.plugins.common.userdata.UserDataPlugin {
It "UserDataPlugin has a clean environment" {
function before.cloudbaseinit.plugins.common.userdata.UserDataPlugin {
if ($env:CLOUD -eq "oracle") {
It "OCI IMDSv2 rejects requests without the Oracle authorization header" {
$statusCode = $null
try {
$response = Invoke-WebRequest `
-Uri "http://127.0.0.1:18080/opc/v2/instance/id" `
-UseBasicParsing `
-ErrorAction Stop
$statusCode = [int]$response.StatusCode
} catch {
if (!$_.Exception.Response) {
throw
}
$statusCode = [int]$_.Exception.Response.StatusCode
}

$statusCode | Should -Be 401
}

It "OCI IMDSv2 accepts requests with the Oracle authorization header" {
$response = Invoke-WebRequest `
-Uri "http://127.0.0.1:18080/opc/v2/instance/id" `
-Headers @{ Authorization = "Bearer Oracle" } `
-UseBasicParsing `
-ErrorAction Stop

[int]$response.StatusCode | Should -Be 200
# Windows PowerShell 5.1 returns application/octet-stream response
# bodies as Byte[]; newer PowerShell versions can return a String.
$content = if ($response.Content -is [byte[]]) {
[System.Text.Encoding]::UTF8.GetString($response.Content)
} else {
[string]$response.Content
}
$content.Trim() |
Should -BeExactly "b9517879-4e93-4a1a-9073-4ae0ddfac27c"
}

It "Oracle IMDSv2 user data has a clean environment" {
{
Get-Content -Raw `
-Path "C:\oracle-imdsv2-functional-test.txt" `
-ErrorAction Stop
} | Should -Throw
}
return
}

It "UserDataPlugin has a clean environment" {
{
$out = net.exe localgroup "windows" 2>&1
if ($LASTEXITCODE) {
Expand All @@ -123,8 +171,19 @@ function before.cloudbaseinit.plugins.common.userdata.UserDataPlugin {
{ Get-ChildItem -Path "c:\runcmd" -ErrorAction Stop } | Should -Throw
}
}
function after.cloudbaseinit.plugins.common.userdata.UserDataPlugin {
It "UserDataPlugin created windows group" {
function after.cloudbaseinit.plugins.common.userdata.UserDataPlugin {
if ($env:CLOUD -eq "oracle") {
It "Oracle IMDSv2 user data was decoded and processed" {
$content = Get-Content -Raw `
-Path "C:\oracle-imdsv2-functional-test.txt" `
-ErrorAction Stop
$content.Trim() |
Should -BeExactly "Oracle Cloud IMDSv2 functional test passed"
}
return
}

It "UserDataPlugin created windows group" {
{
$out = net.exe localgroup "windows" 2>&1
if ($LASTEXITCODE) {
Expand Down Expand Up @@ -283,6 +342,36 @@ function prepare.openstack-http {
prepare.openstack
}

function prepare.oracle {
$metadataUrl = "http://127.0.0.1:18080/opc/v2/instance/id"
$headers = @{ Authorization = "Bearer Oracle" }

try {
Invoke-WebRequest -Uri $metadataUrl -Headers $headers `
-UseBasicParsing -ErrorAction Stop | Out-Null
return
} catch {}

$serverScript = (
Resolve-Path "$here/../oracle/mock_metadata_server.py"
).Path
Start-Process -FilePath "python.exe" `
-ArgumentList "`"$serverScript`"" `
-WindowStyle Hidden | Out-Null

for ($attempt = 0; $attempt -lt 20; $attempt++) {
try {
Invoke-WebRequest -Uri $metadataUrl -Headers $headers `
-UseBasicParsing -ErrorAction Stop | Out-Null
return
} catch {
Start-Sleep -Milliseconds 250
}
}

throw "Oracle metadata test server did not become ready"
}

function before.cloudbaseinit.plugins.windows.bootconfig.BCDConfigPlugin {
# TBD
}
Expand Down
9 changes: 9 additions & 0 deletions oracle/cloudbase-init.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[DEFAULT]
debug = true
stop_service_on_exit = false
metadata_services = cloudbaseinit.metadata.services.oraclecloudservice.OracleCloudService
plugins = cloudbaseinit.plugins.common.userdata.UserDataPlugin

[oraclecloud]
metadata_base_url = http://127.0.0.1:18080/
add_metadata_private_ip_route = false
1 change: 1 addition & 0 deletions oracle/metadata/opc/v2/instance/id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b9517879-4e93-4a1a-9073-4ae0ddfac27c
1 change: 1 addition & 0 deletions oracle/metadata/opc/v2/instance/metadata/user_data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
I2Nsb3VkLWNvbmZpZwp3cml0ZV9maWxlczoKICAtIHBhdGg6ICJDOlxcb3JhY2xlLWltZHN2Mi1mdW5jdGlvbmFsLXRlc3QudHh0IgogICAgY29udGVudDogIk9yYWNsZSBDbG91ZCBJTURTdjIgZnVuY3Rpb25hbCB0ZXN0IHBhc3NlZFxuIgogICAgcGVybWlzc2lvbnM6ICIwNjQ0Igo=
48 changes: 48 additions & 0 deletions oracle/mock_metadata_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (c) 2026, Oracle and/or its affiliates.
#
# 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

import http.server
import pathlib


METADATA_ROOT = pathlib.Path(__file__).parent / "metadata"
EXPECTED_AUTHORIZATION = "Bearer Oracle"


class OracleMetadataRequestHandler(http.server.BaseHTTPRequestHandler):

def do_GET(self):
if self.headers.get("Authorization") != EXPECTED_AUTHORIZATION:
self.send_error(
http.HTTPStatus.UNAUTHORIZED,
"OCI IMDSv2 requires Authorization: Bearer Oracle",
)
return

metadata_path = (METADATA_ROOT / self.path.lstrip("/")).resolve()
if METADATA_ROOT.resolve() not in metadata_path.parents:
self.send_error(http.HTTPStatus.NOT_FOUND)
return
try:
metadata = metadata_path.read_bytes().rstrip(b"\r\n")
except (FileNotFoundError, IsADirectoryError):
self.send_error(http.HTTPStatus.NOT_FOUND)
return

self.send_response(http.HTTPStatus.OK)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(metadata)))
self.end_headers()
self.wfile.write(metadata)


if __name__ == "__main__":
server = http.server.ThreadingHTTPServer(
("127.0.0.1", 18080), OracleMetadataRequestHandler
)
server.serve_forever()