From 5da9a6797747b4b133c319b7f5ce00f15a5f5104 Mon Sep 17 00:00:00 2001 From: Lucas Quinney Date: Mon, 27 Jul 2026 16:08:33 -0700 Subject: [PATCH] oracle: add IMDSv2 functional test resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an OCI metadata fixture and a local HTTP server that enforces the IMDSv2 Authorization: Bearer Oracle header. Add functional coverage proving that unauthorized requests are rejected and that Cloudbase-Init retrieves, decodes, and executes OCI user data. Document how to run the Oracle functional tests. AI-assisted contribution: I reviewed the change and submit it under the DCO terms at https://github.com/cloudbase/cloudbase-init/blob/master/DCO#L16 (clause (a) — the contribution is mine). Signed-off-by: Lucas Quinney --- README.md | 29 ++++++ functional-tests/CloudbaseInit.Tests.ps1 | 97 ++++++++++++++++++- oracle/cloudbase-init.conf | 9 ++ oracle/metadata/opc/v2/instance/id | 1 + .../opc/v2/instance/metadata/user_data | 1 + oracle/mock_metadata_server.py | 48 +++++++++ 6 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 oracle/cloudbase-init.conf create mode 100644 oracle/metadata/opc/v2/instance/id create mode 100644 oracle/metadata/opc/v2/instance/metadata/user_data create mode 100644 oracle/mock_metadata_server.py diff --git a/README.md b/README.md index d49c782..549ea95 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/functional-tests/CloudbaseInit.Tests.ps1 b/functional-tests/CloudbaseInit.Tests.ps1 index be2ae99..315b069 100644 --- a/functional-tests/CloudbaseInit.Tests.ps1 +++ b/functional-tests/CloudbaseInit.Tests.ps1 @@ -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) { @@ -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) { @@ -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 } diff --git a/oracle/cloudbase-init.conf b/oracle/cloudbase-init.conf new file mode 100644 index 0000000..b97d73a --- /dev/null +++ b/oracle/cloudbase-init.conf @@ -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 diff --git a/oracle/metadata/opc/v2/instance/id b/oracle/metadata/opc/v2/instance/id new file mode 100644 index 0000000..013ece3 --- /dev/null +++ b/oracle/metadata/opc/v2/instance/id @@ -0,0 +1 @@ +b9517879-4e93-4a1a-9073-4ae0ddfac27c diff --git a/oracle/metadata/opc/v2/instance/metadata/user_data b/oracle/metadata/opc/v2/instance/metadata/user_data new file mode 100644 index 0000000..52918dc --- /dev/null +++ b/oracle/metadata/opc/v2/instance/metadata/user_data @@ -0,0 +1 @@ +I2Nsb3VkLWNvbmZpZwp3cml0ZV9maWxlczoKICAtIHBhdGg6ICJDOlxcb3JhY2xlLWltZHN2Mi1mdW5jdGlvbmFsLXRlc3QudHh0IgogICAgY29udGVudDogIk9yYWNsZSBDbG91ZCBJTURTdjIgZnVuY3Rpb25hbCB0ZXN0IHBhc3NlZFxuIgogICAgcGVybWlzc2lvbnM6ICIwNjQ0Igo= diff --git a/oracle/mock_metadata_server.py b/oracle/mock_metadata_server.py new file mode 100644 index 0000000..0784ca8 --- /dev/null +++ b/oracle/mock_metadata_server.py @@ -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()