diff --git a/doc/topics/cloud/map.rst b/doc/topics/cloud/map.rst index a37bca4eb0..af3def61f5 100644 --- a/doc/topics/cloud/map.rst +++ b/doc/topics/cloud/map.rst @@ -92,25 +92,27 @@ Any top level data element from your profile may be overridden in the map file: - web2: size: t2.nano -Nested elements cannot be specified individually. Instead, the complete -definition for any top level data element must be repeated. In this example a -separate subnet is assigned to each ec2 instance: +As of Salt Nitrogen, nested elements are merged, and can can be specified +individually without having to repeat the complete definition for each top +level data element. In this example a separate MAC is assigned to each VMware +instance while inheriting device parameters for for disk and network +configuration: .. code-block:: yaml - fedora_small_aws: - - web1: - network_interfaces: - - DeviceIndex: 0 - SubnetId: subnet-3bf94a72 - SecurityGroupId: - - sg-9f644fe5 - - web2: - network_interfaces: - - DeviceIndex: 0 - SubnetId: subnet-c3ad9fa4 - SecurityGroupId: - - sg-9f644fe5 + nyc-vm: + - db1: + devices: + network: + Network Adapter 1: + mac: '44:44:44:44:44:41' + - db2: + devices: + network: + Network Adapter 1: + mac: '44:44:44:44:44:42' + + A map file may also be used with the various query options: diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 678f03b9e8..f4bb02b4eb 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -31,6 +31,7 @@ import salt.client import salt.loader import salt.utils import salt.utils.cloud +import salt.utils.dictupdate import salt.utils.files import salt.syspaths from salt.utils import reinit_crypto @@ -1957,7 +1958,7 @@ class Map(Cloud): if len(overrides['minion']) == 0: del overrides['minion'] - nodedata.update(overrides) + nodedata = salt.utils.dictupdate.update(nodedata, overrides) # Add the computed information to the return data ret['create'][nodename] = nodedata # Add the node name to the defined set @@ -1992,7 +1993,7 @@ class Map(Cloud): break log.warning("'{0}' already exists, removing from " - 'the create map.'.format(name)) + 'the create map.'.format(name)) if 'existing' not in ret: ret['existing'] = {} diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 34506ff7fb..2815fbfe85 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -1709,8 +1709,8 @@ def _validate_opts(opts): # We don't expect the user to know this, so we will fix up their path for # them if it isn't compliant. if (salt.utils.is_windows() and opts.get('transport') == 'raet' and - 'sock_dir' in opts and - not opts['sock_dir'].startswith('\\\\.\\mailslot\\')): + 'sock_dir' in opts and + not opts['sock_dir'].startswith('\\\\.\\mailslot\\')): opts['sock_dir'] = ( '\\\\.\\mailslot\\' + opts['sock_dir'].replace(':', '')) @@ -2515,9 +2515,10 @@ def apply_vm_profiles_config(providers, overrides, defaults=None): vms.pop(profile) continue - extended = vms.get(extends).copy() + extended = deepcopy(vms.get(extends)) extended.pop('profile') - extended.update(details) + # Merge extended configuration with base profile + extended = salt.utils.dictupdate.update(extended, details) if ':' not in extended['provider']: if extended['provider'] not in providers: @@ -2757,7 +2758,7 @@ def apply_cloud_providers_config(overrides, defaults=None): # Grab a copy of what should be extended extended = providers.get(ext_alias).get(ext_driver).copy() # Merge the data to extend with the details - extended.update(details) + extended = salt.utils.dictupdate.update(extended, details) # Update the providers dictionary with the merged data providers[alias][driver] = extended # Update name of the driver, now that it's populated with extended information diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 4f13341a86..4e713c38cc 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -552,6 +552,33 @@ class ConfigTestCase(TestCase, integration.AdaptedConfigurationTestCaseMixIn): overrides, defaults=DEFAULT), ret) + def test_apply_vm_profiles_config_extend_override_success(self): + ''' + Tests profile extends and recursively merges data elements + ''' + self.maxDiff = None + providers = {'test-config': {'ec2': {'profiles': {}, 'driver': 'ec2'}}} + overrides = {'Fedora': {'image': 'test-image-2', + 'extends': 'dev-instances', + 'minion': {'grains': {'stage': 'experimental'}}}, + 'conf_file': PATH, + 'dev-instances': {'ssh_username': 'test_user', + 'provider': 'test-config', + 'minion': {'grains': {'role': 'webserver'}}}} + ret = {'Fedora': {'profile': 'Fedora', + 'ssh_username': 'test_user', + 'image': 'test-image-2', + 'minion': {'grains': {'role': 'webserver', + 'stage': 'experimental'}}, + 'provider': 'test-config:ec2'}, + 'dev-instances': {'profile': 'dev-instances', + 'ssh_username': 'test_user', + 'minion': {'grains': {'role': 'webserver'}}, + 'provider': 'test-config:ec2'}} + self.assertEqual(sconfig.apply_vm_profiles_config(providers, + overrides, + defaults=DEFAULT), ret) + # apply_cloud_providers_config tests def test_apply_cloud_providers_config_same_providers(self): diff --git a/tests/unit/map_conf_test.py b/tests/unit/map_conf_test.py new file mode 100644 index 0000000000..041793c3e6 --- /dev/null +++ b/tests/unit/map_conf_test.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Eric Radman ` +''' + +# Import Python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from salttesting import skipIf, TestCase +from salttesting.helpers import ensure_in_syspath +from salttesting.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON, +) + +ensure_in_syspath('../') + +# Import Salt libs +import salt.cloud + +EXAMPLE_PROVIDERS = { + 'nyc_vcenter': {'vmware': {'driver': 'vmware', + 'password': '123456', + 'profiles': {'nyc-vm': {'cluster': 'nycvirt', + 'datastore': 'datastore1', + 'devices': {'disk': {'Hard disk 1': {'controller': 'SCSI controller 1', + 'size': 20}}, + 'network': {'Network Adapter 1': {'mac': '44:44:44:44:44:42', + 'name': 'vlan50', + 'switch_type': 'standard'}}, + 'scsi': {'SCSI controller 1': {'type': 'paravirtual'}}}, + 'extra_config': {'mem.hotadd': 'yes'}, + 'folder': 'coreinfra', + 'image': 'rhel6_64Guest', + 'memory': '8GB', + 'num_cpus': 2, + 'power_on': True, + 'profile': 'nyc-vm', + 'provider': 'nyc_vcenter:vmware', + 'resourcepool': 'Resources'}}, + 'url': 'vca1.saltstack.com', + 'user': 'root'}} +} + +EXAMPLE_PROFILES = { + 'nyc-vm': {'cluster': 'nycvirt', + 'datastore': 'datastore1', + 'devices': {'disk': {'Hard disk 1': {'controller': 'SCSI controller 1', + 'size': 20}}, + 'network': {'Network Adapter 1': {'mac': '44:44:44:44:44:42', + 'name': 'vlan50', + 'switch_type': 'standard'}}, + 'scsi': {'SCSI controller 1': {'type': 'paravirtual'}}}, + 'extra_config': {'mem.hotadd': 'yes'}, + 'folder': 'coreinfra', + 'image': 'rhel6_64Guest', + 'memory': '8GB', + 'num_cpus': 2, + 'power_on': True, + 'profile': 'nyc-vm', + 'provider': 'nyc_vcenter:vmware', + 'resourcepool': 'Resources'} +} + +EXAMPLE_MAP = { + 'nyc-vm': {'db1': {'cpus': 4, + 'devices': {'disk': {'Hard disk 1': {'size': 40}}, + 'network': {'Network Adapter 1': {'mac': '22:4a:b2:92:b3:eb'}}}, + 'memory': '16GB', + 'name': 'db1'}} +} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class MapConfTest(TestCase): + ''' + Validate evaluation of salt-cloud map configuration + ''' + + @patch('salt.cloud.Map.read', MagicMock(return_value=EXAMPLE_MAP)) + def test_cloud_map_merge_conf(self): + self.maxDiff = None + ''' + Ensure that nested values can be selectivly overridden in a map file + ''' + opts = {'extension_modules': '/var/cache/salt/master/extmods', + 'providers': EXAMPLE_PROVIDERS, 'profiles': EXAMPLE_PROFILES} + cloud_map = salt.cloud.Map(opts) + merged_profile = { + 'create': {'db1': {'cluster': 'nycvirt', + 'cpus': 4, + 'datastore': 'datastore1', + 'devices': {'disk': {'Hard disk 1': {'controller': 'SCSI controller 1', + 'size': 40}}, + 'network': {'Network Adapter 1': {'mac': '22:4a:b2:92:b3:eb', + 'name': 'vlan50', + 'switch_type': 'standard'}}, + 'scsi': {'SCSI controller 1': {'type': 'paravirtual'}}}, + 'driver': 'vmware', + 'extra_config': {'mem.hotadd': 'yes'}, + 'folder': 'coreinfra', + 'image': 'rhel6_64Guest', + 'memory': '16GB', + 'name': 'db1', + 'num_cpus': 2, + 'password': '123456', + 'power_on': True, + 'profile': 'nyc-vm', + 'provider': 'nyc_vcenter:vmware', + 'resourcepool': 'Resources', + 'url': 'vca1.saltstack.com', + 'user': 'root'}} + } + self.assertEqual(cloud_map.map_data(), merged_profile) + + +if __name__ == '__main__': + from integration import run_tests + run_tests(MapConfTest, needs_daemon=False)