diff --git a/cloudbaseinit/metadata/services/base.py b/cloudbaseinit/metadata/services/base.py index f704f446..ac4508e4 100644 --- a/cloudbaseinit/metadata/services/base.py +++ b/cloudbaseinit/metadata/services/base.py @@ -16,7 +16,6 @@ import abc import collections import time -import warnings from oslo.config import cfg @@ -42,6 +41,7 @@ LOG = logging.getLogger(__name__) NetworkDetails = collections.namedtuple( "NetworkDetails", [ + "name", "mac", "address", "netmask", @@ -98,7 +98,7 @@ class BaseMetadataService(object): pass def get_content(self, name): - pass + """Get raw content within a service.""" def get_user_data(self): pass @@ -109,11 +109,6 @@ class BaseMetadataService(object): def get_public_keys(self): pass - def get_network_config(self): - """Deprecated, use `get_network_details` instead.""" - warnings.warn("deprecated method, use `get_network_details`", - DeprecationWarning) - def get_network_details(self): """Return a list of `NetworkDetails` objects. diff --git a/cloudbaseinit/metadata/services/baseopenstackservice.py b/cloudbaseinit/metadata/services/baseopenstackservice.py index 7973df0b..3b183337 100644 --- a/cloudbaseinit/metadata/services/baseopenstackservice.py +++ b/cloudbaseinit/metadata/services/baseopenstackservice.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. + import json import posixpath @@ -19,9 +20,11 @@ from oslo.config import cfg from cloudbaseinit.metadata.services import base from cloudbaseinit.openstack.common import log as logging +from cloudbaseinit.utils import debiface from cloudbaseinit.utils import encoding from cloudbaseinit.utils import x509constants + opts = [ cfg.StrOpt('metadata_base_url', default='http://169.254.169.254/', help='The base URL where the service looks for metadata'), @@ -63,8 +66,19 @@ class BaseOpenStackService(base.BaseMetadataService): if public_keys: return public_keys.values() - def get_network_config(self): - return self._get_meta_data().get('network_config') + def get_network_details(self): + network_config = self._get_meta_data().get('network_config') + if not network_config: + return None + key = "content_path" + if key not in network_config: + return None + + content_name = network_config[key].rsplit("/", 1)[-1] + content = self.get_content(content_name) + content = encoding.get_as_string(content) + + return debiface.parse(content) def get_admin_password(self): meta_data = self._get_meta_data() diff --git a/cloudbaseinit/metadata/services/ec2service.py b/cloudbaseinit/metadata/services/ec2service.py index 6b8ace82..7165a7ab 100644 --- a/cloudbaseinit/metadata/services/ec2service.py +++ b/cloudbaseinit/metadata/services/ec2service.py @@ -101,6 +101,6 @@ class EC2Service(base.BaseMetadataService): return ssh_keys - def get_network_config(self): - # TODO(alexpilotti): add static network support + def get_network_details(self): + # TODO(cpoieana): add static network config support pass diff --git a/cloudbaseinit/metadata/services/opennebulaservice.py b/cloudbaseinit/metadata/services/opennebulaservice.py index 0985fd3d..6a71a34f 100644 --- a/cloudbaseinit/metadata/services/opennebulaservice.py +++ b/cloudbaseinit/metadata/services/opennebulaservice.py @@ -31,6 +31,8 @@ LOG = logging.getLogger(__name__) CONTEXT_FILE = "context.sh" INSTANCE_ID = "iid-dsopennebula" +# interface name default template +IF_FORMAT = "eth{iid}" # metadata identifiers HOST_NAME = ["SET_HOSTNAME", "HOSTNAME"] @@ -211,15 +213,21 @@ class OpenNebulaService(base.BaseMetadataService): # get existing values mac = self._get_cache_data(MAC, iid=iid).upper() address = self._get_cache_data(ADDRESS, iid=iid) - gateway = self._get_cache_data(GATEWAY, iid=iid) # try to find/predict and compute the rest + try: + gateway = self._get_cache_data(GATEWAY, iid=iid) + except base.NotExistingMetadataException: + gateway = None try: netmask = self._get_cache_data(NETMASK, iid=iid) except base.NotExistingMetadataException: + if not gateway: + raise netmask = self._calculate_netmask(address, gateway) broadcast = self._compute_broadcast(address, netmask) # gather them as namedtuple objects details = base.NetworkDetails( + IF_FORMAT.format(iid=iid), mac, address, netmask, diff --git a/cloudbaseinit/osutils/windows.py b/cloudbaseinit/osutils/windows.py index 5eccf575..59f41d2b 100644 --- a/cloudbaseinit/osutils/windows.py +++ b/cloudbaseinit/osutils/windows.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. + import ctypes from ctypes import wintypes import os @@ -541,19 +542,21 @@ class WindowsUtils(base.BaseOSUtils): "Cannot set static IP address on network adapter") reboot_required = (ret_val == 1) - LOG.debug("Setting static gateways") - (ret_val,) = adapter_config.SetGateways([gateway], [1]) - if ret_val > 1: - raise exception.CloudbaseInitException( - "Cannot set gateway on network adapter") - reboot_required = reboot_required or ret_val == 1 + if gateway: + LOG.debug("Setting static gateways") + (ret_val,) = adapter_config.SetGateways([gateway], [1]) + if ret_val > 1: + raise exception.CloudbaseInitException( + "Cannot set gateway on network adapter") + reboot_required = reboot_required or ret_val == 1 - LOG.debug("Setting static DNS servers") - (ret_val,) = adapter_config.SetDNSServerSearchOrder(dnsnameservers) - if ret_val > 1: - raise exception.CloudbaseInitException( - "Cannot set DNS on network adapter") - reboot_required = reboot_required or ret_val == 1 + if dnsnameservers: + LOG.debug("Setting static DNS servers") + (ret_val,) = adapter_config.SetDNSServerSearchOrder(dnsnameservers) + if ret_val > 1: + raise exception.CloudbaseInitException( + "Cannot set DNS on network adapter") + reboot_required = reboot_required or ret_val == 1 return reboot_required diff --git a/cloudbaseinit/plugins/windows/networkconfig.py b/cloudbaseinit/plugins/windows/networkconfig.py index 1d9d60cd..88036f1f 100644 --- a/cloudbaseinit/plugins/windows/networkconfig.py +++ b/cloudbaseinit/plugins/windows/networkconfig.py @@ -13,111 +13,102 @@ # under the License. -import re - -from oslo.config import cfg - from cloudbaseinit import exception from cloudbaseinit.metadata.services import base as service_base from cloudbaseinit.openstack.common import log as logging from cloudbaseinit.osutils import factory as osutils_factory from cloudbaseinit.plugins import base as plugin_base -from cloudbaseinit.utils import encoding LOG = logging.getLogger(__name__) -opts = [ - cfg.StrOpt('network_adapter', default=None, help='Network adapter to ' - 'configure. If not specified, the first available ethernet ' - 'adapter will be chosen.'), -] +# Mandatory network details are marked with True. And +# if the key is a tuple, then at least one field must exists. +NET_REQUIRE = { + ("name", "mac"): True, + "address": True, + "netmask": True, + "broadcast": False, # currently not used + "gateway": False, + "dnsnameservers": False +} -CONF = cfg.CONF -CONF.register_opts(opts) + +def _preprocess_nics(network_details, network_adapters): + """Check NICs and fill missing data if possible.""" + # initial checks + if not network_adapters: + raise exception.CloudbaseInitException( + "no network adapters available") + # Sort VM adapters by name (assuming that those + # from the context are in correct order). + network_adapters = sorted(network_adapters, key=lambda arg: arg[0]) + _network_details = [] # store here processed data + # check and update every NetworkDetails object + ind = 0 + total = len(network_adapters) + for nic in network_details: + if not isinstance(nic, service_base.NetworkDetails): + raise exception.CloudbaseInitException( + "invalid NetworkDetails object {!r}" + .format(type(nic)) + ) + # check requirements + final_status = True + for fields, status in NET_REQUIRE.items(): + if not status: + continue # skip 'not required' entries + if not isinstance(fields, tuple): + fields = (fields,) + final_status = any([getattr(nic, field) for field in fields]) + if not final_status: + LOG.error("Incomplete NetworkDetails object %s", nic) + break + if final_status: + # Complete hardware address if missing by selecting + # the corresponding MAC in terms of naming, then ordering. + if not nic.mac: + mac = None + # by name + macs = [adapter[1] for adapter in network_adapters + if adapter[0] == nic.name] + mac = macs[0] if macs else None + # or by order + if not mac and ind < total: + mac = network_adapters[ind][1] + nic = service_base.NetworkDetails( + nic.name, + mac, + nic.address, + nic.netmask, + nic.broadcast, + nic.gateway, + nic.dnsnameservers + ) + _network_details.append(nic) + ind += 1 + return _network_details class NetworkConfigPlugin(plugin_base.BasePlugin): def execute(self, service, shared_data): - # FIXME(cpoieana): `network_config` is deprecated - # * refactor all services by providing NetworkDetails objects * - # Also, the old method is not supporting multiple NICs. - osutils = osutils_factory.get_os_utils() network_details = service.get_network_details() if not network_details: - network_config = service.get_network_config() - if not network_config: - return (plugin_base.PLUGIN_EXECUTION_DONE, False) + return (plugin_base.PLUGIN_EXECUTION_DONE, False) - # ---- BEGIN deprecated code ---- - if not network_details: - if 'content_path' not in network_config: - return (plugin_base.PLUGIN_EXECUTION_DONE, False) - - content_path = network_config['content_path'] - content_name = content_path.rsplit('/', 1)[-1] - debian_network_conf = service.get_content(content_name) - debian_network_conf = encoding.get_as_string(debian_network_conf) - - LOG.debug('network config content:\n%s' % debian_network_conf) - - # TODO(alexpilotti): implement a proper grammar - m = re.search(r'iface eth0 inet static\s+' - r'address\s+(?P
[^\s]+)\s+' - r'netmask\s+(?P[^\s]+)\s+' - r'broadcast\s+(?P[^\s]+)\s+' - r'gateway\s+(?P[^\s]+)\s+' - r'dns\-nameservers\s+' - r'(?P[^\r\n]+)\s+', - debian_network_conf) - if not m: - raise exception.CloudbaseInitException( - "network_config format not recognized") - - mac = None - network_adapters = osutils.get_network_adapters() - if network_adapters: - adapter_name = CONF.network_adapter - if adapter_name: - # configure with the specified one - for network_adapter in network_adapters: - if network_adapter[0] == adapter_name: - mac = network_adapter[1] - break - else: - # configure with the first one - mac = network_adapters[0][1] - network_details = [ - service_base.NetworkDetails( - mac, - m.group('address'), - m.group('netmask'), - m.group('broadcast'), - m.group('gateway'), - m.group('dnsnameservers').strip().split(' ') - ) - ] - # ---- END deprecated code ---- - - # check NICs' type and save them by MAC + # check and save NICs by MAC + network_adapters = osutils.get_network_adapters() + network_details = _preprocess_nics(network_details, + network_adapters) macnics = {} for nic in network_details: - if not isinstance(nic, service_base.NetworkDetails): - raise exception.CloudbaseInitException( - "invalid NetworkDetails object {!r}" - .format(type(nic)) - ) # assuming that the MAC address is unique macnics[nic.mac] = nic + # try configuring all the available adapters - adapter_macs = [pair[1] for pair in - osutils.get_network_adapters()] - if not adapter_macs: - raise exception.CloudbaseInitException( - "no network adapters available") - # configure each one + adapter_macs = [pair[1] for pair in network_adapters] reboot_required = False configured = False for mac in adapter_macs: diff --git a/cloudbaseinit/tests/metadata/fake_json_response.py b/cloudbaseinit/tests/metadata/fake_json_response.py index 452cff0a..0111ebff 100644 --- a/cloudbaseinit/tests/metadata/fake_json_response.py +++ b/cloudbaseinit/tests/metadata/fake_json_response.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013 Cloudbase Solutions Srl # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -15,43 +13,103 @@ # under the License. +NAME0 = "eth0" +MAC0 = "fa:16:3e:2d:ec:cd" +ADDRESS0 = "10.0.0.15" +NETMASK0 = "255.255.255.0" +BROADCAST0 = "10.0.0.255" +GATEWAY0 = "10.0.0.1" +DNSNS0 = "208.67.220.220 208.67.222.222" + +NAME1 = "eth1" +ADDRESS1 = "10.1.0.2" +NETMASK1 = "255.255.255.0" +BROADCAST1 = "10.1.0.255" +GATEWAY1 = "10.1.0.1" + + def get_fake_metadata_json(version): - if version == '2013-04-04': - return {"random_seed": - "Wn51FGjZa3vlZtTxJuPr96oCf+X8jqbA9U2XR5wNdnApy1fz" - "/2NNssUwPoNzG6etw9RBn+XiZ0zKWnFzMsTopaN7WwYjWTnIsVw3cpIk" - "Td579wQgoEr1ANqhfO3qTvkOVNMhzTAw1ps+wqRmkLxH+1qYJnX06Gcd" - "KRRGkWTaOSlTkieA0LO2oTGFlbFDWcOW2vT5BvSBmqP7vNLzbLDMTc7M" - "IWRBzwmtcVPC17QL6EhZJTUcZ0mTz7l0R0DocLmFwHEXFEEr+q4WaJjt" - "1ejOOxVM3tiT7D8YpRZnnGNPfvEhq1yVMUoi8yv9pFmMmXicNBhm6zDK" - "VjcWk0gfbvaQcMnnOLrrE1VxAAzyNyPIXBI/H7AAHz2ECz7dgd2/4ocv" - "3bmTRY3hhcUKtNuat2IOvSGgMBUGdWnLorQGFz8t0/bcYhE0Dve35U6H" - "mtj78ydV/wmQWG0iq49NX6hk+VUmZtSZztlkbsaa7ajNjZ+Md9oZtlhX" - "Z5vJuhRXnHiCm7dRNO8Xo6HffEBH5A4smQ1T2Kda+1c18DZrY7+iQJRi" - "fa6witPCw0tXkQ6nlCLqL2weJD1XMiTZLSM/XsZFGGSkKCKvKLEqQrI/" - "XFUq/TA6B4aLGFlmmhOO/vMJcht06O8qVU/xtd5Mv/MRFzYaSG568Z/m" - "hk4vYLYdQYAA+pXRW9A=", - "uuid": "4b32ddf7-7941-4c36-a854-a1f5ac45b318", - "availability_zone": "nova", - "hostname": "windows.novalocal", - "launch_index": 0, - "public_keys": {"key": "ssh-rsa " - "AAAAB3NzaC1yc2EAAAADAQABAAABA" - "QDf7kQHq7zvBod3yIZs0tB/AOOZz5pab7qt/h" - "78VF7yi6qTsFdUnQxRue43R/75wa9EEyokgYR" - "LKIN+Jq2A5tXNMcK+rNOCzLJFtioAwEl+S6VL" - "G9jfkbUv++7zoSMOsanNmEDvG0B79MpyECFCl" - "th2DsdE4MQypify35U5ri5Qi7E6PEYAsU65LF" - "MG2boeCIB29BEooE6AgPr2DuJeJ+2uw+YScF9" - "FV3og4Wyz5zipPVh8YpVev6dlg0tRWUrCtZF9" - "IODpCTrT3vsPRG3xz7CppR+vGi/1gLXHtJCRj" - "frHwkY6cXyhypNmkU99K/wMqSv30vsDwdnsQ1" - "q3YhLarMHB Generated by Nova\n", - 0: "windows"}, - "network_config": {"content_path": "network", - 'debian_config': 'iface eth0 inet static' - 'address 10.11.12.13' - 'broadcast 0.0.0.0' - 'netmask 255.255.255.255' - 'gateway 1.2.3.4' - 'dns-nameserver 8.8.8.8'}} + data1 = { + "random_seed": "Wn51FGjZa3vlZtTxJuPr96oCf+X8jqbA9U2XR5wNdnApy1fz" + "/2NNssUwPoNzG6etw9RBn+XiZ0zKWnFzMsTopaN7WwYjWTnI" + "sVw3cpIkTd579wQgoEr1ANqhfO3qTvkOVNMhzTAw1ps+wqRm" + "kLxH+1qYJnX06GcdKRRGkWTaOSlTkieA0LO2oTGFlbFDWcOW" + "2vT5BvSBmqP7vNLzbLDMTc7MIWRBzwmtcVPC17QL6EhZJTUc" + "Z0mTz7l0R0DocLmFwHEXFEEr+q4WaJjt1ejOOxVM3tiT7D8Y" + "pRZnnGNPfvEhq1yVMUoi8yv9pFmMmXicNBhm6zDKVjcWk0gf" + "bvaQcMnnOLrrE1VxAAzyNyPIXBI/H7AAHz2ECz7dgd2/4ocv" + "3bmTRY3hhcUKtNuat2IOvSGgMBUGdWnLorQGFz8t0/bcYhE0" + "Dve35U6Hmtj78ydV/wmQWG0iq49NX6hk+VUmZtSZztlkbsaa" + "7ajNjZ+Md9oZtlhXZ5vJuhRXnHiCm7dRNO8Xo6HffEBH5A4s" + "mQ1T2Kda+1c18DZrY7+iQJRifa6witPCw0tXkQ6nlCLqL2we" + "JD1XMiTZLSM/XsZFGGSkKCKvKLEqQrI/XFUq/TA6B4aLGFlm" + "mhOO/vMJcht06O8qVU/xtd5Mv/MRFzYaSG568Z/mhk4vYLYd" + "QYAA+pXRW9A=", + "uuid": "4b32ddf7-7941-4c36-a854-a1f5ac45b318", + "availability_zone": "nova", + "hostname": "windows.novalocal", + "launch_index": 0, + "public_keys": { + "key": + "ssh-rsa " + "AAAAB3NzaC1yc2EAAAADAQABAAABA" + "QDf7kQHq7zvBod3yIZs0tB/AOOZz5pab7qt/h" + "78VF7yi6qTsFdUnQxRue43R/75wa9EEyokgYR" + "LKIN+Jq2A5tXNMcK+rNOCzLJFtioAwEl+S6VL" + "G9jfkbUv++7zoSMOsanNmEDvG0B79MpyECFCl" + "th2DsdE4MQypify35U5ri5Qi7E6PEYAsU65LF" + "MG2boeCIB29BEooE6AgPr2DuJeJ+2uw+YScF9" + "FV3og4Wyz5zipPVh8YpVev6dlg0tRWUrCtZF9" + "IODpCTrT3vsPRG3xz7CppR+vGi/1gLXHtJCRj" + "frHwkY6cXyhypNmkU99K/wMqSv30vsDwdnsQ1" + "q3YhLarMHB Generated by Nova\n", + 0: "windows" + }, + "network_config": { + "content_path": "network", + "debian_config": (""" +# Injected by Nova on instance boot +# +# This file describes the network interfaces available on your system +# and how to activate them. For more information, see interfaces(5). + +# The loopback network interface +auto lo +iface lo inet loopback + +auto {name0} +iface {name0} inet static + hwaddress ether {mac0} + address {address0} + netmask {netmask0} + broadcast {broadcast0} + gateway {gateway0} + dns-nameservers {dnsns0} + +auto {name1} +iface {name1} inet static + address {address1} + netmask {netmask1} + broadcast {broadcast1} + gateway {gateway1} + """).format(name0=NAME0, # eth0 (IPv4) + mac0=MAC0, + address0=ADDRESS0, + broadcast0=BROADCAST0, + netmask0=NETMASK0, + gateway0=GATEWAY0, + dnsns0=DNSNS0, + # eth1 (IPv4) + name1=NAME1, + address1=ADDRESS1, + broadcast1=BROADCAST1, + netmask1=NETMASK1, + gateway1=GATEWAY1) + } + } + + datadict = { + "2013-04-04": data1 + } + + return datadict.get(version) diff --git a/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py b/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py index bc2b41f7..0569e4bc 100644 --- a/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py +++ b/cloudbaseinit/tests/metadata/services/test_baseopenstackservice.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2014 Cloudbase Solutions Srl # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,24 +12,41 @@ # License for the specific language governing permissions and limitations # under the License. -import mock + +import functools import posixpath import unittest +import mock +from oslo.config import cfg + from cloudbaseinit.metadata.services import base from cloudbaseinit.metadata.services import baseopenstackservice +from cloudbaseinit.tests.metadata import fake_json_response from cloudbaseinit.utils import x509constants -from oslo.config import cfg + CONF = cfg.CONF +MODPATH = "cloudbaseinit.metadata.services.baseopenstackservice" + + +class TestBaseOpenStackService(unittest.TestCase): -class BaseOpenStackServiceTest(unittest.TestCase): def setUp(self): - CONF.set_override('retry_count_interval', 0) + CONF.set_override("retry_count_interval", 0) self._service = baseopenstackservice.BaseOpenStackService() + date = "2013-04-04" + fake_metadata = fake_json_response.get_fake_metadata_json(date) + self._fake_network_config = fake_metadata["network_config"] + self._fake_content = self._fake_network_config["debian_config"] + self._partial_test_get_network_details = functools.partial( + self._test_get_network_details, + network_config=self._fake_network_config, + content=self._fake_content + ) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_cache_data") def test_get_content(self, mock_get_cache_data): response = self._service.get_content('fake name') @@ -39,7 +54,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): mock_get_cache_data.assert_called_once_with(path) self.assertEqual(mock_get_cache_data.return_value, response) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_cache_data") def test_get_user_data(self, mock_get_cache_data): response = self._service.get_user_data() @@ -47,7 +62,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): mock_get_cache_data.assert_called_once_with(path) self.assertEqual(mock_get_cache_data.return_value, response) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_cache_data") def test_get_meta_data(self, mock_get_cache_data): mock_get_cache_data.return_value = b'{"fake": "data"}' @@ -57,7 +72,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): mock_get_cache_data.assert_called_with(path) self.assertEqual({"fake": "data"}, response) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_meta_data") def test_get_instance_id(self, mock_get_meta_data): response = self._service.get_instance_id() @@ -66,7 +81,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): self.assertEqual(mock_get_meta_data.return_value.get.return_value, response) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_meta_data") def test_get_host_name(self, mock_get_meta_data): response = self._service.get_host_name() @@ -75,7 +90,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): self.assertEqual(mock_get_meta_data.return_value.get.return_value, response) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_meta_data") def test_get_public_keys(self, mock_get_meta_data): response = self._service.get_public_keys() @@ -83,15 +98,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): mock_get_meta_data().get.assert_called_once_with('public_keys') self.assertEqual(mock_get_meta_data().get().values(), response) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" - ".BaseOpenStackService._get_meta_data") - def test_get_network_config(self, mock_get_meta_data): - response = self._service.get_network_config() - mock_get_meta_data.assert_called_once_with() - mock_get_meta_data().get.assert_called_once_with('network_config') - self.assertEqual(response, mock_get_meta_data().get()) - - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_meta_data") def _test_get_admin_password(self, mock_get_meta_data, meta_data): mock_get_meta_data.return_value = meta_data @@ -102,7 +109,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): elif meta_data and 'admin_pass' in meta_data.get('meta'): self.assertEqual(meta_data.get('meta')['admin_pass'], response) else: - self.assertEqual(None, response) + self.assertIsNone(response) def test_get_admin_pass(self): self._test_get_admin_password(meta_data={'admin_pass': 'fake pass'}) @@ -114,9 +121,9 @@ class BaseOpenStackServiceTest(unittest.TestCase): def test_get_admin_pass_no_pass(self): self._test_get_admin_password(meta_data={}) - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService._get_meta_data") - @mock.patch("cloudbaseinit.metadata.services.baseopenstackservice" + @mock.patch(MODPATH + ".BaseOpenStackService.get_user_data") def _test_get_client_auth_certs(self, mock_get_user_data, mock_get_meta_data, meta_data, @@ -132,7 +139,7 @@ class BaseOpenStackServiceTest(unittest.TestCase): mock_get_user_data.assert_called_once_with() self.assertEqual([ret_value], response) elif ret_value is base.NotExistingMetadataException: - self.assertEqual(None, response) + self.assertIsNone(response) def test_get_client_auth_certs(self): self._test_get_client_auth_certs( @@ -145,3 +152,75 @@ class BaseOpenStackServiceTest(unittest.TestCase): def test_get_client_auth_certs_no_cert_data_exception(self): self._test_get_client_auth_certs( meta_data={}, ret_value=base.NotExistingMetadataException) + + @mock.patch(MODPATH + + ".BaseOpenStackService.get_content") + @mock.patch(MODPATH + + ".BaseOpenStackService._get_meta_data") + def _test_get_network_details(self, + mock_get_meta_data, + mock_get_content, + network_config=None, + content=None, + search_fail=False, + no_path=False): + # mock obtained data + mock_get_meta_data().get.return_value = network_config + mock_get_content.return_value = content + # actual tests + if search_fail: + ret = self._service.get_network_details() + self.assertFalse(ret) + return + ret = self._service.get_network_details() + mock_get_meta_data().get.assert_called_once_with("network_config") + if network_config and not no_path: + mock_get_content.assert_called_once_with("network") + if not network_config: + self.assertIsNone(ret) + return + if no_path: + self.assertIsNone(ret) + return + # check returned NICs details + nic1 = base.NetworkDetails( + fake_json_response.NAME0, + fake_json_response.MAC0.upper(), + fake_json_response.ADDRESS0, + fake_json_response.NETMASK0, + fake_json_response.BROADCAST0, + fake_json_response.GATEWAY0, + fake_json_response.DNSNS0.split() + ) + nic2 = base.NetworkDetails( + fake_json_response.NAME1, + None, + fake_json_response.ADDRESS1, + fake_json_response.NETMASK1, + fake_json_response.BROADCAST1, + fake_json_response.GATEWAY1, + None + ) + self.assertEqual([nic1, nic2], ret) + + def test_get_network_details_no_config(self): + self._partial_test_get_network_details( + network_config=None + ) + + def test_get_network_details_no_path(self): + self._fake_network_config.pop("content_path", None) + self._partial_test_get_network_details( + network_config=self._fake_network_config, + no_path=True + ) + + def test_get_network_details_search_fail(self): + self._fake_content = "invalid format" + self._partial_test_get_network_details( + content=self._fake_content, + search_fail=True + ) + + def test_get_network_details(self): + self._partial_test_get_network_details() diff --git a/cloudbaseinit/tests/metadata/services/test_opennebulaservice.py b/cloudbaseinit/tests/metadata/services/test_opennebulaservice.py index a9a2a2a6..86dd495f 100644 --- a/cloudbaseinit/tests/metadata/services/test_opennebulaservice.py +++ b/cloudbaseinit/tests/metadata/services/test_opennebulaservice.py @@ -97,8 +97,9 @@ ETH1_MAC='{mac}' ) -def _get_nic_details(): +def _get_nic_details(iid=0): details = base.NetworkDetails( + opennebulaservice.IF_FORMAT.format(iid=iid), MAC, ADDRESS, NETMASK, @@ -302,8 +303,9 @@ class TestLoadedOpenNebulaService(_TestOpenNebulaService): def test_multiple_nics(self): self.load_context(context=CONTEXT2) - details = _get_nic_details() - network_details = [details] * 2 + nic1 = _get_nic_details(iid=0) + nic2 = _get_nic_details(iid=1) + network_details = [nic1, nic2] self.assertEqual( network_details, self._service.get_network_details() diff --git a/cloudbaseinit/tests/plugins/windows/test_networkconfig.py b/cloudbaseinit/tests/plugins/windows/test_networkconfig.py index 18801154..879e496b 100644 --- a/cloudbaseinit/tests/plugins/windows/test_networkconfig.py +++ b/cloudbaseinit/tests/plugins/windows/test_networkconfig.py @@ -13,163 +13,226 @@ # under the License. -import re +import functools import unittest import mock -from oslo.config import cfg from cloudbaseinit import exception from cloudbaseinit.metadata.services import base as service_base from cloudbaseinit.plugins import base as plugin_base from cloudbaseinit.plugins.windows import networkconfig -from cloudbaseinit.tests.metadata import fake_json_response - - -CONF = cfg.CONF class TestNetworkConfigPlugin(unittest.TestCase): def setUp(self): - self._network_plugin = networkconfig.NetworkConfigPlugin() - self.fake_data = fake_json_response.get_fake_metadata_json( - '2013-04-04') + self._setUp() - @mock.patch('cloudbaseinit.osutils.factory.get_os_utils') + @mock.patch("cloudbaseinit.osutils.factory.get_os_utils") def _test_execute(self, mock_get_os_utils, - search_result=mock.MagicMock(), - no_adapter_name=False, no_adapters=False, - using_content=0, details_list=None, - missing_content_path=False): - fake_adapter = ("fake_name_0", "fake_mac_0") + network_adapters=None, + network_details=None, + invalid_details=False, + missed_adapters=[], + extra_network_details=[]): + # prepare mock environment mock_service = mock.MagicMock() + mock_shared_data = mock.Mock() mock_osutils = mock.MagicMock() - mock_ndetails = mock.Mock() - re.search = mock.MagicMock(return_value=search_result) - fake_shared_data = 'fake shared data' - network_config = self.fake_data['network_config'] - if not details_list: - details_list = [None] * 6 - details_list[0] = fake_adapter[1] # set MAC for matching - if no_adapter_name: # nothing provided in the config file - CONF.set_override("network_adapter", None) - else: - CONF.set_override("network_adapter", fake_adapter[0]) - mock_osutils.get_network_adapters.return_value = [ - fake_adapter, - # and other adapters - ("name1", "mac1"), - ("name2", "mac2") - ] + mock_service.get_network_details.return_value = network_details mock_get_os_utils.return_value = mock_osutils - mock_osutils.set_static_network_config.return_value = False - # service method setup - methods = ["get_network_config", "get_content", "get_network_details"] - for method in methods: - mock_method = getattr(mock_service, method) - mock_method.return_value = None - if using_content == 1: - mock_service.get_network_config.return_value = network_config - mock_service.get_content.return_value = search_result - - elif using_content == 2: - mock_service.get_network_details.return_value = [mock_ndetails] + mock_osutils.get_network_adapters.return_value = network_adapters + mock_osutils.set_static_network_config.return_value = True + network_execute = functools.partial( + self._network_plugin.execute, + mock_service, mock_shared_data + ) # actual tests - if search_result is None and using_content == 1: - self.assertRaises(exception.CloudbaseInitException, - self._network_plugin.execute, - mock_service, fake_shared_data) + if not network_details: + ret = network_execute() + self.assertEqual((plugin_base.PLUGIN_EXECUTION_DONE, False), ret) return - if no_adapters: - mock_osutils.get_network_adapters.return_value = [] - self.assertRaises(exception.CloudbaseInitException, - self._network_plugin.execute, - mock_service, fake_shared_data) - return - attrs = [ - "address", - "netmask", - "broadcast", - "gateway", - "dnsnameservers", - ] - if using_content == 0: - response = self._network_plugin.execute(mock_service, - fake_shared_data) - elif using_content == 1: - if missing_content_path: - mock_service.get_network_config.return_value.pop( - "content_path", None - ) - response = self._network_plugin.execute(mock_service, - fake_shared_data) - if not missing_content_path: - mock_service.get_network_config.assert_called_once_with() - mock_service.get_content.assert_called_once_with( - network_config['content_path']) - adapters = mock_osutils.get_network_adapters() - if CONF.network_adapter: - mac = [pair[1] for pair in adapters - if pair == fake_adapter][0] - else: - mac = adapters[0][1] - ( - address, - netmask, - broadcast, - gateway, - dnsnameserver - ) = map(search_result.group, attrs) - dnsnameservers = dnsnameserver.strip().split(" ") - elif using_content == 2: + if invalid_details: with self.assertRaises(exception.CloudbaseInitException): - self._network_plugin.execute(mock_service, - fake_shared_data) - mock_service.get_network_details.reset_mock() - mock_ndetails = service_base.NetworkDetails(*details_list) - mock_service.get_network_details.return_value = [mock_ndetails] - response = self._network_plugin.execute(mock_service, - fake_shared_data) - mock_service.get_network_details.assert_called_once_with() - mac = mock_ndetails.mac - ( - address, - netmask, - broadcast, - gateway, - dnsnameservers - ) = map(lambda attr: getattr(mock_ndetails, attr), attrs) - if using_content in (1, 2) and not missing_content_path: - mock_osutils.set_static_network_config.assert_called_once_with( - mac, - address, - netmask, - broadcast, - gateway, - dnsnameservers + network_execute() + return + if not network_adapters: + with self.assertRaises(exception.CloudbaseInitException): + network_execute() + return + # good to go for the configuration process + ret = network_execute() + calls = [] + for adapter in set(network_adapters) - set(missed_adapters): + nics = [nic for nic in (network_details + + extra_network_details) + if nic.mac == adapter[1]] + self.assertTrue(nics) # missed_adapters should do the job + nic = nics[0] + call = mock.call( + nic.mac, + nic.address, + nic.netmask, + nic.broadcast, + nic.gateway, + nic.dnsnameservers ) - self.assertEqual((plugin_base.PLUGIN_EXECUTION_DONE, False), - response) + calls.append(call) + mock_osutils.set_static_network_config.assert_has_calls( + calls, any_order=True) + reboot = len(missed_adapters) != self._count + self.assertEqual((plugin_base.PLUGIN_EXECUTION_DONE, reboot), ret) - def test_execute(self): - self._test_execute(using_content=1) + def _setUp(self, same_names=True): + # Generate fake pairs of NetworkDetails objects and + # local ethernet network adapters. + self._count = 3 + details_names = [ + "eth0", + "eth1", + "eth2" + ] + if same_names: + adapters_names = details_names[:] + else: + adapters_names = ["vm " + name for name in details_names] + macs = [ + "54:EE:75:19:F4:61", + "54:EE:75:19:F4:62", + "54:EE:75:19:F4:63" + ] + addresses = [ + "192.168.122.101", + "192.168.103.104", + "192.168.122.105", + ] + netmasks = [ + "255.255.255.0", + "255.255.0.0", + "255.255.255.128", + ] + broadcasts = [ + "192.168.122.255", + "192.168.255.255", + "192.168.122.127", + ] + gateway = [ + "192.168.122.1", + "192.168.122.16", + "192.168.122.32", + ] + dnsnses = [ + "8.8.8.8", + "8.8.8.8 8.8.4.4", + "8.8.8.8 0.0.0.0", + ] + self._network_adapters = [] + self._network_details = [] + for ind in range(self._count): + adapter = (adapters_names[ind], macs[ind]) + nic = service_base.NetworkDetails( + details_names[ind], + macs[ind], + addresses[ind], + netmasks[ind], + broadcasts[ind], + gateway[ind], + dnsnses[ind].split() + ) + self._network_adapters.append(adapter) + self._network_details.append(nic) + # get the network config plugin + self._network_plugin = networkconfig.NetworkConfigPlugin() + # execution wrapper + self._partial_test_execute = functools.partial( + self._test_execute, + network_adapters=self._network_adapters, + network_details=self._network_details + ) - def test_execute_missing_content_path(self): - self._test_execute(using_content=1, missing_content_path=True) + def test_execute_no_network_details(self): + self._network_details[:] = [] + self._partial_test_execute() - def test_execute_no_debian(self): - self._test_execute(search_result=None, using_content=1) + def test_execute_no_network_adapters(self): + self._network_adapters[:] = [] + self._partial_test_execute() - def test_execute_no_adapter_name(self): - self._test_execute(no_adapter_name=True, using_content=1) + def test_execute_invalid_network_details(self): + self._network_details.append([None] * 6) + self._partial_test_execute(invalid_details=True) - def test_execute_no_adapter_name_or_adapters(self): - self._test_execute(no_adapter_name=True, no_adapters=True, - using_content=1) + def test_execute_single(self): + for _ in range(self._count - 1): + self._network_adapters.pop() + self._network_details.pop() + self._partial_test_execute() - def test_execute_network_details(self): - self._test_execute(using_content=2) + def test_execute_multiple(self): + self._partial_test_execute() - def test_execute_no_config_or_details(self): - self._test_execute(using_content=0) + def test_execute_missing_one(self): + self.assertGreater(self._count, 1) + self._network_details.pop(0) + adapter = self._network_adapters[0] + self._partial_test_execute(missed_adapters=[adapter]) + + def test_execute_missing_all(self): + nic = self._network_details[0] + nic = service_base.NetworkDetails( + nic.name, + "00" + nic.mac[2:], + nic.address, + nic.netmask, + nic.broadcast, + nic.gateway, + nic.dnsnameservers + ) + self._network_details[:] = [nic] + self._partial_test_execute(missed_adapters=self._network_adapters) + + def _test_execute_missing_smth(self, name=False, mac=False, + address=False, gateway=False, + fail=False): + ind = self._count - 1 + nic = self._network_details[ind] + nic2 = service_base.NetworkDetails( + None if name else nic.name, + None if mac else nic.mac, + None if address else nic.address, + nic.netmask, + nic.broadcast, + None if gateway else nic.gateway, + nic.dnsnameservers + ) + self._network_details[ind] = nic2 + # excluding address and gateway switches... + if not fail: + # even this way, all adapters should be configured + missed_adapters = [] + extra_network_details = [nic] + else: + # both name and MAC are missing, so we can't make the match + missed_adapters = [self._network_adapters[ind]] + extra_network_details = [] + self._partial_test_execute( + missed_adapters=missed_adapters, + extra_network_details=extra_network_details + ) + + def test_execute_missing_mac(self): + self._test_execute_missing_smth(mac=True) + + def test_execute_missing_mac2(self): + self._setUp(same_names=False) + self._test_execute_missing_smth(mac=True) + + def test_execute_missing_name_mac(self): + self._test_execute_missing_smth(name=True, mac=True, fail=True) + + def test_execute_missing_address(self): + self._test_execute_missing_smth(address=True, fail=True) + + def test_execute_missing_gateway(self): + self._test_execute_missing_smth(gateway=True) diff --git a/cloudbaseinit/tests/utils/test_debiface.py b/cloudbaseinit/tests/utils/test_debiface.py new file mode 100644 index 00000000..f72ed180 --- /dev/null +++ b/cloudbaseinit/tests/utils/test_debiface.py @@ -0,0 +1,63 @@ +# Copyright 2014 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 unittest + +from cloudbaseinit.metadata.services import base as service_base +from cloudbaseinit.tests.metadata import fake_json_response +from cloudbaseinit.utils import debiface + + +class TestInterfacesParser(unittest.TestCase): + + def setUp(self): + date = "2013-04-04" + content = fake_json_response.get_fake_metadata_json(date) + self.data = content["network_config"]["debian_config"] + + def _test_parse_nics(self, no_nics=False): + nics = debiface.parse(self.data) + if no_nics: + self.assertFalse(nics) + return + # check what we've got + nic0 = service_base.NetworkDetails( + fake_json_response.NAME0, + fake_json_response.MAC0.upper(), + fake_json_response.ADDRESS0, + fake_json_response.NETMASK0, + fake_json_response.BROADCAST0, + fake_json_response.GATEWAY0, + fake_json_response.DNSNS0.split() + ) + nic1 = service_base.NetworkDetails( + fake_json_response.NAME1, + None, + fake_json_response.ADDRESS1, + fake_json_response.NETMASK1, + fake_json_response.BROADCAST1, + fake_json_response.GATEWAY1, + None + ) + self.assertEqual([nic0, nic1], nics) + + def test_nothing_to_parse(self): + invalid = [None, "", 324242, ("dasd", "dsa")] + for data in invalid: + self.data = data + self._test_parse_nics(no_nics=True) + + def test_parse(self): + self._test_parse_nics() diff --git a/cloudbaseinit/utils/debiface.py b/cloudbaseinit/utils/debiface.py new file mode 100644 index 00000000..4efd24cf --- /dev/null +++ b/cloudbaseinit/utils/debiface.py @@ -0,0 +1,100 @@ +# Copyright 2014 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 re + +import six + +from cloudbaseinit.metadata.services import base as service_base +from cloudbaseinit.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + +NAME = "name" +MAC = "mac" +ADDRESS = "address" +NETMASK = "netmask" +BROADCAST = "broadcast" +GATEWAY = "gateway" +DNSNS = "dnsnameservers" +# fields of interest (order and regexp) +FIELDS = { + NAME: (0, re.compile(r"iface\s+(?P<{}>\S+)" + r"\s+inet\s+static".format(NAME))), + MAC: (1, re.compile(r"hwaddress\s+ether\s+" + r"(?P<{}>\S+)".format(MAC))), + ADDRESS: (2, re.compile(r"address\s+" + r"(?P<{}>\S+)".format(ADDRESS))), + NETMASK: (3, re.compile(r"netmask\s+" + r"(?P<{}>\S+)".format(NETMASK))), + BROADCAST: (4, re.compile(r"broadcast\s+" + r"(?P<{}>\S+)".format(BROADCAST))), + GATEWAY: (5, re.compile(r"gateway\s+" + r"(?P<{}>\S+)".format(GATEWAY))), + DNSNS: (6, re.compile(r"dns-nameservers\s+(?P<{}>.+)".format(DNSNS))) +} +IFACE_TEMPLATE = dict.fromkeys(range(len(FIELDS))) + + +def _get_field(line): + for field, (index, regex) in FIELDS.items(): + match = regex.match(line) + if match: + return index, match.group(field) + + +def _add_nic(iface, nics): + if not iface: + return + details = [iface[key] for key in sorted(iface)] + LOG.debug("Found new interface: %s", details) + # each missing detail is marked as None + nic = service_base.NetworkDetails(*details) + nics.append(nic) + + +def parse(data): + """Parse the received content and obtain network details.""" + # TODO(cpoieana): support IPv6 flavors + if not data or not isinstance(data, six.string_types): + LOG.error("Invalid debian config to parse:\n%s", data) + return + LOG.info("Parsing debian config...\n%s", data) + nics = [] # list of NetworkDetails objects + iface = {} + # take each line and process it + for line in data.split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + ret = _get_field(line) + if not ret: + continue + # save the detail + index = ret[0] + if index == 0: + # we found a new interface + _add_nic(iface, nics) + iface = IFACE_TEMPLATE.copy() + value = ret[1] + if index == 1: + value = value.upper() + elif index == 6: + value = value.strip().split() + iface[index] = value + # also add the last one + _add_nic(iface, nics) + return nics