Add NoCloudConfigDriveService metadata provider
Add support for NoCloud metadata provider, where the metadata is provided on a config-drive (vfat or iso9660) with the label cidata or CIDATA. The folder structure for NoCloud is: * /user-data * /meta-data The user-data and meta-data files respect the EC2 metadata service format. Supported features for the NoCloud metadata service: * instance id * hostname * plublic keys * static network configuration (Debian format) * user data More information: cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html Change-Id: Ib434cf2b2b21bf9faa58e05ba40eb0135385c9ea Implements: blueprint nocloud-metadata-support
This commit is contained in:
parent
fcb68a4dc7
commit
4b0d94cd0f
|
@ -0,0 +1,70 @@
|
||||||
|
# Copyright 2020 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_log import log as oslo_logging
|
||||||
|
|
||||||
|
from cloudbaseinit import conf as cloudbaseinit_conf
|
||||||
|
from cloudbaseinit.metadata.services import base
|
||||||
|
from cloudbaseinit.metadata.services import baseconfigdrive
|
||||||
|
from cloudbaseinit.utils import debiface
|
||||||
|
from cloudbaseinit.utils import serialization
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cloudbaseinit_conf.CONF
|
||||||
|
LOG = oslo_logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NoCloudConfigDriveService(baseconfigdrive.BaseConfigDriveService):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(NoCloudConfigDriveService, self).__init__(
|
||||||
|
'cidata', 'meta-data')
|
||||||
|
self._meta_data = {}
|
||||||
|
|
||||||
|
def get_user_data(self):
|
||||||
|
return self._get_cache_data("user-data")
|
||||||
|
|
||||||
|
def _get_meta_data(self):
|
||||||
|
if self._meta_data:
|
||||||
|
return self._meta_data
|
||||||
|
|
||||||
|
raw_meta_data = self._get_cache_data("meta-data", decode=True)
|
||||||
|
try:
|
||||||
|
self._meta_data = (
|
||||||
|
serialization.parse_json_yaml(raw_meta_data))
|
||||||
|
except base.YamlParserConfigError as ex:
|
||||||
|
LOG.error("Metadata could not be parsed")
|
||||||
|
LOG.exception(ex)
|
||||||
|
|
||||||
|
return self._meta_data
|
||||||
|
|
||||||
|
def get_host_name(self):
|
||||||
|
return self._get_meta_data().get('local-hostname')
|
||||||
|
|
||||||
|
def get_instance_id(self):
|
||||||
|
return self._get_meta_data().get('instance-id')
|
||||||
|
|
||||||
|
def get_public_keys(self):
|
||||||
|
raw_ssh_keys = self._get_meta_data().get('public-keys')
|
||||||
|
if not raw_ssh_keys:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [raw_ssh_keys[key].get('openssh-key') for key in raw_ssh_keys]
|
||||||
|
|
||||||
|
def get_network_details(self):
|
||||||
|
debian_net_config = self._get_meta_data().get('network-interfaces')
|
||||||
|
if not debian_net_config:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return debiface.parse(debian_net_config)
|
|
@ -0,0 +1,90 @@
|
||||||
|
# Copyright 2020 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
try:
|
||||||
|
import unittest.mock as mock
|
||||||
|
except ImportError:
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cloudbaseinit.tests import testutils
|
||||||
|
|
||||||
|
MODULE_PATH = "cloudbaseinit.metadata.services.nocloudservice"
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoCloudConfigDriveService(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._win32com_mock = mock.MagicMock()
|
||||||
|
self._ctypes_mock = mock.MagicMock()
|
||||||
|
self._ctypes_util_mock = mock.MagicMock()
|
||||||
|
self._win32com_client_mock = mock.MagicMock()
|
||||||
|
self._pywintypes_mock = mock.MagicMock()
|
||||||
|
|
||||||
|
self._module_patcher = mock.patch.dict(
|
||||||
|
'sys.modules',
|
||||||
|
{'win32com': self._win32com_mock,
|
||||||
|
'ctypes': self._ctypes_mock,
|
||||||
|
'ctypes.util': self._ctypes_util_mock,
|
||||||
|
'win32com.client': self._win32com_client_mock,
|
||||||
|
'pywintypes': self._pywintypes_mock})
|
||||||
|
self._module_patcher.start()
|
||||||
|
self.addCleanup(self._module_patcher.stop)
|
||||||
|
|
||||||
|
self.configdrive_module = importlib.import_module(MODULE_PATH)
|
||||||
|
self._config_drive = (
|
||||||
|
self.configdrive_module.NoCloudConfigDriveService())
|
||||||
|
self.snatcher = testutils.LogSnatcher(MODULE_PATH)
|
||||||
|
|
||||||
|
@mock.patch('os.path.normpath')
|
||||||
|
@mock.patch('os.path.join')
|
||||||
|
def test_get_data(self, mock_join, mock_normpath):
|
||||||
|
fake_path = os.path.join('fake', 'path')
|
||||||
|
with mock.patch('six.moves.builtins.open',
|
||||||
|
mock.mock_open(read_data='fake data'), create=True):
|
||||||
|
response = self._config_drive._get_data(fake_path)
|
||||||
|
self.assertEqual('fake data', response)
|
||||||
|
mock_join.assert_called_with(
|
||||||
|
self._config_drive._metadata_path, fake_path)
|
||||||
|
mock_normpath.assert_called_once_with(mock_join.return_value)
|
||||||
|
|
||||||
|
@mock.patch('shutil.rmtree')
|
||||||
|
def test_cleanup(self, mock_rmtree):
|
||||||
|
fake_path = os.path.join('fake', 'path')
|
||||||
|
self._config_drive._metadata_path = fake_path
|
||||||
|
mock_mgr = mock.Mock()
|
||||||
|
self._config_drive._mgr = mock_mgr
|
||||||
|
mock_mgr.target_path = fake_path
|
||||||
|
self._config_drive.cleanup()
|
||||||
|
mock_rmtree.assert_called_once_with(fake_path,
|
||||||
|
ignore_errors=True)
|
||||||
|
self.assertEqual(None, self._config_drive._metadata_path)
|
||||||
|
|
||||||
|
@mock.patch(MODULE_PATH + '.NoCloudConfigDriveService._get_meta_data')
|
||||||
|
def test_get_public_keys(self, mock_get_metadata):
|
||||||
|
fake_key = 'fake key'
|
||||||
|
expected_result = [fake_key]
|
||||||
|
mock_get_metadata.return_value = {
|
||||||
|
'public-keys': {
|
||||||
|
'0': {
|
||||||
|
'openssh-key': fake_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = self._config_drive.get_public_keys()
|
||||||
|
self.assertEqual(result, expected_result)
|
|
@ -113,6 +113,61 @@ Config options for `config_drive` section:
|
||||||
* locations (list: ["cdrom", "hdd", "partition"])
|
* locations (list: ["cdrom", "hdd", "partition"])
|
||||||
|
|
||||||
|
|
||||||
|
.. _nocloudconfigdrive:
|
||||||
|
|
||||||
|
NoCloud configuration drive
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. class:: cloudbaseinit.metadata.services.nocloudservice.NoCloudConfigDriveService
|
||||||
|
|
||||||
|
NoCloudConfigDriveService is similar to OpenStack config drive metadata in terms of
|
||||||
|
the medium on which the data is provided (as an attached ISO, partition or disk) and
|
||||||
|
similar to the EC2 metadata in terms of how the metadata files are named and structured.
|
||||||
|
|
||||||
|
The metadata is provided on a config-drive (vfat or iso9660) with the label cidata or CIDATA.
|
||||||
|
|
||||||
|
The folder structure for NoCloud is:
|
||||||
|
|
||||||
|
* /user-data
|
||||||
|
* /meta-data
|
||||||
|
|
||||||
|
The user-data and meta-data files respect the EC2 metadata service format.
|
||||||
|
|
||||||
|
Capabilities:
|
||||||
|
|
||||||
|
* instance id
|
||||||
|
* hostname
|
||||||
|
* public keys
|
||||||
|
* static network configuration (Debian format)
|
||||||
|
* user data
|
||||||
|
|
||||||
|
Config options for `config_drive` section:
|
||||||
|
|
||||||
|
* raw_hdd (bool: True)
|
||||||
|
* cdrom (bool: True)
|
||||||
|
* vfat (bool: True)
|
||||||
|
* types (list: ["vfat", "iso"])
|
||||||
|
* locations (list: ["cdrom", "hdd", "partition"])
|
||||||
|
|
||||||
|
Example metadata:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
instance-id: windows1
|
||||||
|
network-interfaces: |
|
||||||
|
iface Ethernet0 inet static
|
||||||
|
address 10.0.0.2
|
||||||
|
network 10.0.0.0
|
||||||
|
netmask 255.255.255.0
|
||||||
|
broadcast 10.0.0.255
|
||||||
|
gateway 10.0.0.1
|
||||||
|
hwaddress ether 00:11:22:33:44:55
|
||||||
|
hostname: windowshost1
|
||||||
|
|
||||||
|
|
||||||
|
More information on the NoCloud metadata service specifications can be found
|
||||||
|
`here <https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html>`_.
|
||||||
|
|
||||||
Amazon EC2
|
Amazon EC2
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue