Skip to content

Commit 7e153cd

Browse files
chl117ader1990
authored andcommitted
metadata: add Oracle Cloud IMDSv2 service
OCI IMDSv2 exposes instance metadata under /opc/v2 and requires an Authorization: Bearer Oracle header on every request [1]. Cloudbase-Init does not have an OCI-specific metadata provider, so it cannot use the OCI IMDSv2 endpoints when legacy IMDSv1 access is disabled. Add OracleCloudService to retrieve the instance ID, hostname, and base64-encoded user data. Add the required authorization header through the service's HTTP request override, following the existing GCE and MaaS patterns. Register the related oraclecloud configuration options, add unit-test coverage for service loading, request headers, and metadata retrieval, and enable the Oracle IMDSv2 functional tests. [1] https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm 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 <lucas.quinney@oracle.com>
1 parent c0b1c39 commit 7e153cd

5 files changed

Lines changed: 230 additions & 1 deletion

File tree

.github/workflows/cloudbase_init_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
matrix:
6363
python-version: [ "3.14"]
6464
architecture: ["x64", "x86"]
65-
cloud: ["empty", "nocloud", "vmwareguest"]
65+
cloud: ["empty", "nocloud", "vmwareguest", "oracle"]
6666

6767
steps:
6868
- name: Checkout cloudbase-init repository

cloudbaseinit/conf/factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
'cloudbaseinit.conf.ec2.EC2Options',
2222
'cloudbaseinit.conf.maas.MAASOptions',
2323
'cloudbaseinit.conf.openstack.OpenStackOptions',
24+
'cloudbaseinit.conf.oraclecloud.OracleCloudOptions',
2425
'cloudbaseinit.conf.azure.AzureOptions',
2526
'cloudbaseinit.conf.ovf.OvfOptions',
2627
'cloudbaseinit.conf.packet.PacketOptions',

cloudbaseinit/conf/oraclecloud.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright (c) 2026, Oracle and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
"""Config options available for the Oracle Cloud metadata service."""
16+
17+
from oslo_config import cfg
18+
19+
from cloudbaseinit.conf import base as conf_base
20+
21+
22+
class OracleCloudOptions(conf_base.Options):
23+
24+
"""Config options available for the OpenStack metadata service."""
25+
26+
def __init__(self, config):
27+
super(OracleCloudOptions, self).__init__(config, group="oraclecloud")
28+
self._options = [
29+
cfg.StrOpt(
30+
"metadata_base_url", default="http://169.254.169.254/",
31+
help="The base URL where the service looks for metadata",
32+
deprecated_group="DEFAULT"),
33+
cfg.BoolOpt(
34+
"add_metadata_private_ip_route", default=True,
35+
help="Add a route for the metadata ip address to the gateway",
36+
deprecated_group="DEFAULT"),
37+
cfg.BoolOpt(
38+
"https_allow_insecure", default=False,
39+
help="Whether to disable the validation of HTTPS "
40+
"certificates."),
41+
cfg.StrOpt(
42+
"https_ca_bundle", default=None,
43+
help="The path to a CA_BUNDLE file or directory with "
44+
"certificates of trusted CAs."),
45+
]
46+
47+
def register(self):
48+
"""Register the current options to the global ConfigOpts object."""
49+
group = cfg.OptGroup(self.group_name, title='OracleCloud Options')
50+
self._config.register_group(group)
51+
self._config.register_opts(self._options, group=group)
52+
53+
def list(self):
54+
"""Return a list which contains all the available options."""
55+
return self._options
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (c) 2026, Oracle and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from base64 import b64decode
16+
17+
from oslo_log import log as oslo_logging
18+
19+
from cloudbaseinit import conf as cloudbaseinit_conf
20+
from cloudbaseinit.metadata.services import base
21+
from cloudbaseinit.utils import network
22+
23+
CONF = cloudbaseinit_conf.CONF
24+
LOG = oslo_logging.getLogger(__name__)
25+
26+
27+
class OracleCloudService(base.BaseHTTPMetadataService):
28+
_metadata_version = 'v2'
29+
_headers = {"Authorization": "Bearer Oracle"}
30+
31+
def __init__(self):
32+
super(OracleCloudService, self).__init__(
33+
base_url=CONF.oraclecloud.metadata_base_url,
34+
https_allow_insecure=CONF.oraclecloud.https_allow_insecure,
35+
https_ca_bundle=CONF.oraclecloud.https_ca_bundle)
36+
self._enable_retry = True
37+
38+
def _http_request(self, url, data=None, headers=None, method=None):
39+
headers = dict(headers or {})
40+
headers.update(self._headers)
41+
42+
return super(OracleCloudService, self)._http_request(
43+
url, data, headers, method)
44+
45+
def load(self):
46+
super(OracleCloudService, self).load()
47+
if CONF.oraclecloud.add_metadata_private_ip_route:
48+
network.check_metadata_ip_route(CONF.oraclecloud.metadata_base_url)
49+
50+
try:
51+
self.get_instance_id()
52+
return True
53+
except Exception as ex:
54+
LOG.exception(ex)
55+
LOG.debug('Metadata not found at URL \'%s\'' %
56+
CONF.oraclecloud.metadata_base_url)
57+
return False
58+
59+
def get_instance_id(self):
60+
return self._get_cache_data('opc/%s/instance/id' %
61+
self._metadata_version, decode=True)
62+
63+
def get_user_data(self):
64+
return b64decode(self._get_cache_data(
65+
'opc/%s/instance/metadata/user_data' % self._metadata_version))
66+
67+
def get_host_name(self):
68+
return self._get_cache_data('opc/%s/instance/hostname' %
69+
self._metadata_version, decode=True)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright (c) 2026, Oracle and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
import unittest
16+
17+
try:
18+
import unittest.mock as mock
19+
except ImportError:
20+
import mock
21+
22+
from cloudbaseinit import conf as cloudbaseinit_conf
23+
from cloudbaseinit.metadata.services import oraclecloudservice
24+
from cloudbaseinit.tests import testutils
25+
26+
CONF = cloudbaseinit_conf.CONF
27+
28+
29+
class OracleCloudServiceTest(unittest.TestCase):
30+
31+
def setUp(self):
32+
self._service = oraclecloudservice.OracleCloudService()
33+
34+
@mock.patch('cloudbaseinit.utils.network.check_metadata_ip_route')
35+
@mock.patch('cloudbaseinit.metadata.services.oraclecloudservice.'
36+
'OracleCloudService.get_instance_id')
37+
def _test_load(self, mock_get_instance_id, mock_check_metadata_ip_route,
38+
side_effect):
39+
mock_get_instance_id.side_effect = [side_effect]
40+
with testutils.LogSnatcher('cloudbaseinit.metadata.services.'
41+
'oraclecloudservice'):
42+
response = self._service.load()
43+
44+
mock_check_metadata_ip_route.assert_called_once_with(
45+
CONF.oraclecloud.metadata_base_url)
46+
mock_get_instance_id.assert_called_once_with()
47+
if side_effect is Exception:
48+
self.assertFalse(response)
49+
else:
50+
self.assertTrue(response)
51+
52+
def test_load(self):
53+
self._test_load(side_effect=None)
54+
55+
def test_load_exception(self):
56+
self._test_load(side_effect=Exception)
57+
58+
@mock.patch('cloudbaseinit.metadata.services.base.'
59+
'BaseHTTPMetadataService._http_request')
60+
def test_http_request(self, mock_http_request):
61+
headers = {'Test-Header': 'test-value'}
62+
expected_headers = dict(headers)
63+
expected_headers.update(self._service._headers)
64+
65+
response = self._service._http_request(
66+
mock.sentinel.url, data=mock.sentinel.data, headers=headers,
67+
method=mock.sentinel.method)
68+
69+
mock_http_request.assert_called_once_with(
70+
mock.sentinel.url, mock.sentinel.data, expected_headers,
71+
mock.sentinel.method)
72+
self.assertEqual(mock_http_request.return_value, response)
73+
self.assertEqual({'Test-Header': 'test-value'}, headers)
74+
75+
@mock.patch('cloudbaseinit.metadata.services.oraclecloudservice.'
76+
'OracleCloudService._get_cache_data')
77+
def test_get_instance_id(self, mock_get_cache_data):
78+
mock_get_cache_data.return_value = 'test-instance-id'
79+
response = self._service.get_instance_id()
80+
mock_get_cache_data.assert_called_once_with(
81+
'opc/%s/instance/id' % self._service._metadata_version,
82+
decode=True)
83+
self.assertEqual(mock_get_cache_data.return_value, response)
84+
85+
@mock.patch('cloudbaseinit.metadata.services.oraclecloudservice.'
86+
'OracleCloudService._get_cache_data')
87+
def test_get_user_data(self, mock_get_cache_data):
88+
mock_get_cache_data.return_value = (
89+
'VGVzdGluZyBvdXIgY29kZSBpcyBnb29kCg==')
90+
response = self._service.get_user_data()
91+
mock_get_cache_data.assert_called_once_with(
92+
'opc/%s/instance/metadata/user_data' %
93+
self._service._metadata_version)
94+
self.assertEqual(response.decode(), "Testing our code is good\n")
95+
96+
@mock.patch('cloudbaseinit.metadata.services.oraclecloudservice.'
97+
'OracleCloudService._get_cache_data')
98+
def test_get_host_name(self, mock_get_cache_data):
99+
mock_get_cache_data.return_value = 'test-hostname'
100+
response = self._service.get_host_name()
101+
mock_get_cache_data.assert_called_once_with(
102+
'opc/%s/instance/hostname' % self._service._metadata_version,
103+
decode=True)
104+
self.assertEqual(mock_get_cache_data.return_value, response)

0 commit comments

Comments
 (0)