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:
Adrian Vladu 2020-02-07 19:47:25 +02:00
parent fcb68a4dc7
commit 4b0d94cd0f
3 changed files with 215 additions and 0 deletions

View File

@ -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)

View File

@ -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)

View File

@ -113,6 +113,61 @@ Config options for `config_drive` section:
* 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
----------