diff --git a/cloudbaseinit/metadata/services/nocloudservice.py b/cloudbaseinit/metadata/services/nocloudservice.py new file mode 100644 index 00000000..32b11b1e --- /dev/null +++ b/cloudbaseinit/metadata/services/nocloudservice.py @@ -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) diff --git a/cloudbaseinit/tests/metadata/services/test_nocloudservice.py b/cloudbaseinit/tests/metadata/services/test_nocloudservice.py new file mode 100644 index 00000000..763ad843 --- /dev/null +++ b/cloudbaseinit/tests/metadata/services/test_nocloudservice.py @@ -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) diff --git a/doc/source/services.rst b/doc/source/services.rst index ef10e9cd..d5935110 100644 --- a/doc/source/services.rst +++ b/doc/source/services.rst @@ -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 `_. + Amazon EC2 ----------