diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000000..cba5839c7cba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +# This is only for bloomberg, we can make Jenkins run the unit tests inside of a container and remove this later +version: '2.4' +services: + salt-proper-tests: + image: "artprod.dev.bloomberg.com/bb-inf/salt-minion:2018.3.3" + volumes: + - .:/srv/sysca-salt + - ./tests/bbcpu/:/bb/bin/ diff --git a/requirements/dev_bloomberg.txt b/requirements/dev_bloomberg.txt index fbc73386b080..af67a9586488 100644 --- a/requirements/dev_bloomberg.txt +++ b/requirements/dev_bloomberg.txt @@ -5,9 +5,10 @@ pytest>=3.5.0 git+https://e904693b4b9f451885e6a05696ab0a60c3be959c:@bbgithub.dev.bloomberg.com/saltstack/pytest-salt.git@master#egg=pytest-salt testinfra>=1.7.0 bloomberg.hostinfo + # httpretty Needs to be here for now even though it's a dependency of boto. # A pip install on a fresh system will decide to target httpretty 0.8.10 to # satisfy other requirements, and httpretty 0.8.10 has bugs in setup.py that # prevent it from being successfully installed (at least on Python 3.4). httpretty; python_version >= '3.4' -pylint==1.6.5 \ No newline at end of file +pylint==1.6.5 diff --git a/salt/fileserver/__init__.py b/salt/fileserver/__init__.py index 9c99fd2a2fbb..c7049e2a4855 100644 --- a/salt/fileserver/__init__.py +++ b/salt/fileserver/__init__.py @@ -493,7 +493,7 @@ def update_intervals(self, back=None): ret[fsb] = self.servers[fstr]() return ret - def envs(self, back=None, sources=False): + def envs(self, back=None, sources=False, **kwargs): ''' Return the environments for the named backend or all backends ''' diff --git a/salt/master.py b/salt/master.py index 13174dc9d935..bb07fce8fae9 100644 --- a/salt/master.py +++ b/salt/master.py @@ -1209,7 +1209,7 @@ def _file_envs(self, load=None): if any('environments' in ext for ext in self.opts['ext_pillar']): return self.pillars['environments'](load.get('id'), {}) else: - return self.fs_.envs() + return self.fs_.envs(**load) def __verify_minion(self, id_, token): ''' diff --git a/salt/pillar/environments.py b/salt/pillar/environments.py index 6a695a063e54..8591b56f1194 100644 --- a/salt/pillar/environments.py +++ b/salt/pillar/environments.py @@ -104,13 +104,11 @@ def __get_by_node(self, node): def tenancy_groups_set(): groups = IndexedSet() - # assume config is a sane schema - # scalar = tag, dict = 'tag' key is tag for tenancy in __opts__['evaporator']['tenancies']: - if isinstance(tenancy, dict): - groups.add(tenancy['environment']) - else: - groups.add(tenancy) + try: + groups.add(tenancy['environment']['groups']) + except KeyError: + pass return groups @@ -118,17 +116,19 @@ def tenancy_groups_set(): def global_tenancy_groups_set(): groups = IndexedSet() - # assume config is a sane schema - # scalar = tag, dict = 'tag' key is tag for tenancy in __opts__['evaporator']['tenancies']: - if isinstance(tenancy, dict) and tenancy.get('global'): - groups.add(tenancy['environment']) + try: + if tenancy['global']: + groups.add(tenancy['environment']['groups']) + except KeyError: + pass return groups # first try node-id if it exists in grains, then try the minion_id def resolve_node(minion_id): + if __grains__.get('bb', {}).get('node-id'): node_id = __grains__['bb']['node-id'] try: @@ -145,6 +145,18 @@ def resolve_node(minion_id): # if we've gotten this far its an unknown node return None +def stage_envs(stage, envs): + """ + Takes in an iterable of env names and prepends a stage to the beginning. + Example: + env = [salt-core, natm] + stage = 'sn2' + + >> {'environments': ['salt-core-sn2', 'natm-sn2']} + """ + staged_envs = ['{}-{}'.format(env, stage) for env in envs] + return {'environments': staged_envs} + # the goal here is to # 1. if a local top for the given repo exists, include it @@ -161,13 +173,19 @@ def ext_pillar(minion_id, pillar): :return: a dictionary which is included by the salt master in the pillars returned to the minion """ - node = resolve_node(minion_id) + global_tenancy_groups = global_tenancy_groups_set() + # hostinfo resolving a node that is None will throw an error + if not minion_id: + return stage_envs('nostage', global_tenancy_groups) + + node = resolve_node(minion_id) + if node is None: - return [] + return stage_envs('nostage', global_tenancy_groups) # any matching tenancy_group is a 1 to 1 association with environment # we use an IndexedSet to ensure global roots are always highest priority - environments = IndexedSet(global_tenancy_groups_set() | ( node.groups_set() & tenancy_groups_set())) + environments = IndexedSet(global_tenancy_groups | ( node.groups_set() & tenancy_groups_set())) - return {'environments': list(environments)} + return stage_envs(node.stage, environments) diff --git a/tests/unit/test_master.py b/tests/unit/test_master.py index 5a680c4cc904..471334dc06f5 100644 --- a/tests/unit/test_master.py +++ b/tests/unit/test_master.py @@ -14,12 +14,94 @@ MagicMock, ) -@skipIf(True, 'bb test was failing when ran in Jenkins') + +# These tests require a working /bb/bin/bbcpu.lst/alias. +# I ran them inside a docker container using the docker-compose.yml in root +class AESFuncsTestCase(TestCase): + ''' + TestCase for salt.master.AESFuncs class + ''' + def test__file_envs_no_matching_node(self): + # Default master opts + opts = salt.config.master_config(None) + opts['ext_pillar'] = [ + {'environments': ['word']} + ] + + self.aes_funcs = salt.master.AESFuncs(opts) + res = self.aes_funcs._file_envs({"id": "pytest_minion_1"}) + self.assertEqual(res, []) + + def test__file_envs_load_is_none(self): + # Default master opts + opts = salt.config.master_config(None) + opts['evaporator'] = {'tenancies': [{'name': 'salt-core', 'groups': ['salt'], 'global': True}]} + opts['ext_pillar'] = [ + {'environments': ['word']} + ] + + self.aes_funcs = salt.master.AESFuncs(opts) + res = self.aes_funcs._file_envs() + self.assertEqual(res, []) + + def test__file_envs_node_is_found(self): + # Default master opts + opts = salt.config.master_config(None) + opts['ext_pillar'] = [ + {'environments': ['word']} + ] + opts['evaporator'] = { + 'tenancies': [ + {"name": "sltdm", "groups": ["salt"], "global": False}, + {"name": "salt-native", "groups": ["salt"], "global": True}, + {"name": "salt-water", "groups": ["salt"], "global": False}, + ] + } + + self.aes_funcs = salt.master.AESFuncs(opts) + res = self.aes_funcs._file_envs({"id": "sltdm-rr-005"}) + self.assertEqual(res, {u'environments': ['salt-native', "sltdm"]}) + + def test__file_envs_node_no_environment(self): + # Default master opts + opts = salt.config.master_config(None) + opts['evaporator'] = { + 'tenancies': [ + {"name": "sltdm", "groups": ["salt"], "global": False}, + {"name": "salt-native", "groups": ["salt"], "global": True}, + {"name": "salt-water", "groups": ["salt"], "global": False}, + ] + } + + self.aes_funcs = salt.master.AESFuncs(opts) + res = self.aes_funcs._file_envs({"id": "sltdm-rr-005"}) + self.assertEqual(res, ["base"]) + + def test_master_opts_ext_pillar_environments(self): + opts = salt.config.master_config(None) + opts['ext_pillar'] = [ + {'environments': ['word']} + ] + opts['evaporator'] = {} + opts['evaporator']['tenancies'] = [ + {"environment": "sltdm", "global": False}, + {"environment": "salt-native", "global": True}, + {"environment": "salt-water", "global": False}, + ] + + self.aes_funcs = salt.master.AESFuncs(opts) + + res = self.aes_funcs._master_opts({ + "id": "sltdm-rr-005", + "env_only": True, + }) + self.assertEqual(res, {u'default_top': u'base', u'env_order': [], u'ext_pillar': [{'environments': ['word']}], u'top_file_merging_strategy': u'merge', u'file_roots': {u'environments': []},}) + + class ClearFuncsTestCase(TestCase): ''' TestCase for salt.master.ClearFuncs class ''' - def setUp(self): opts = salt.config.master_config(None) self.clear_funcs = salt.master.ClearFuncs(opts, {}) @@ -31,7 +113,7 @@ def test_runner_token_not_authenticated(self): Asserts that a TokenAuthenticationError is returned when the token can't authenticate. ''' mock_ret = {'error': {'name': 'TokenAuthenticationError', - 'message': 'Authentication failure of type "token" occurred.'}} + 'message': 'Authentication failure of type "token" occurred.'}} ret = self.clear_funcs.runner({'token': 'asdfasdfasdfasdf'}) self.assertDictEqual(mock_ret, ret) @@ -44,11 +126,11 @@ def test_runner_token_authorization_error(self): clear_load = {'token': token, 'fun': 'test.arg'} mock_token = {'token': token, 'eauth': 'foo', 'name': 'test'} mock_ret = {'error': {'name': 'TokenAuthenticationError', - 'message': 'Authentication failure of type "token" occurred ' - 'for user test.'}} + 'message': 'Authentication failure of type "token" occurred ' + 'for user test.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): ret = self.clear_funcs.runner(clear_load) self.assertDictEqual(mock_ret, ret) @@ -62,10 +144,10 @@ def test_runner_token_salt_invocation_error(self): clear_load = {'token': token, 'fun': 'badtestarg'} mock_token = {'token': token, 'eauth': 'foo', 'name': 'test'} mock_ret = {'error': {'name': 'SaltInvocationError', - 'message': 'A command invocation error occurred: Check syntax.'}} + 'message': 'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.runner(clear_load) self.assertDictEqual(mock_ret, ret) @@ -75,8 +157,8 @@ def test_runner_eauth_not_authenticated(self): Asserts that an EauthAuthenticationError is returned when the user can't authenticate. ''' mock_ret = {'error': {'name': 'EauthAuthenticationError', - 'message': 'Authentication failure of type "eauth" occurred for ' - 'user UNKNOWN.'}} + 'message': 'Authentication failure of type "eauth" occurred for ' + 'user UNKNOWN.'}} ret = self.clear_funcs.runner({'eauth': 'foo'}) self.assertDictEqual(mock_ret, ret) @@ -87,10 +169,10 @@ def test_runner_eauth_authorization_error(self): ''' clear_load = {'eauth': 'foo', 'username': 'test', 'fun': 'test.arg'} mock_ret = {'error': {'name': 'EauthAuthenticationError', - 'message': 'Authentication failure of type "eauth" occurred for ' - 'user test.'}} + 'message': 'Authentication failure of type "eauth" occurred for ' + 'user test.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): ret = self.clear_funcs.runner(clear_load) self.assertDictEqual(mock_ret, ret) @@ -102,9 +184,9 @@ def test_runner_eauth_salt_invocation_error(self): ''' clear_load = {'eauth': 'foo', 'username': 'test', 'fun': 'bad.test.arg.func'} mock_ret = {'error': {'name': 'SaltInvocationError', - 'message': 'A command invocation error occurred: Check syntax.'}} + 'message': 'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.runner(clear_load) self.assertDictEqual(mock_ret, ret) @@ -114,7 +196,7 @@ def test_runner_user_not_authenticated(self): Asserts that an UserAuthenticationError is returned when the user can't authenticate. ''' mock_ret = {'error': {'name': 'UserAuthenticationError', - 'message': 'Authentication failure of type "user" occurred'}} + 'message': 'Authentication failure of type "user" occurred'}} ret = self.clear_funcs.runner({}) self.assertDictEqual(mock_ret, ret) @@ -125,7 +207,7 @@ def test_wheel_token_not_authenticated(self): Asserts that a TokenAuthenticationError is returned when the token can't authenticate. ''' mock_ret = {'error': {'name': 'TokenAuthenticationError', - 'message': 'Authentication failure of type "token" occurred.'}} + 'message': 'Authentication failure of type "token" occurred.'}} ret = self.clear_funcs.wheel({'token': 'asdfasdfasdfasdf'}) self.assertDictEqual(mock_ret, ret) @@ -138,11 +220,11 @@ def test_wheel_token_authorization_error(self): clear_load = {'token': token, 'fun': 'test.arg'} mock_token = {'token': token, 'eauth': 'foo', 'name': 'test'} mock_ret = {'error': {'name': 'TokenAuthenticationError', - 'message': 'Authentication failure of type "token" occurred ' - 'for user test.'}} + 'message': 'Authentication failure of type "token" occurred ' + 'for user test.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): ret = self.clear_funcs.wheel(clear_load) self.assertDictEqual(mock_ret, ret) @@ -156,10 +238,10 @@ def test_wheel_token_salt_invocation_error(self): clear_load = {'token': token, 'fun': 'badtestarg'} mock_token = {'token': token, 'eauth': 'foo', 'name': 'test'} mock_ret = {'error': {'name': 'SaltInvocationError', - 'message': 'A command invocation error occurred: Check syntax.'}} + 'message': 'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_token', MagicMock(return_value=mock_token)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.wheel(clear_load) self.assertDictEqual(mock_ret, ret) @@ -170,8 +252,8 @@ def test_wheel_eauth_not_authenticated(self): Asserts that an EauthAuthenticationError is returned when the user can't authenticate. ''' mock_ret = {'error': {'name': 'EauthAuthenticationError', - 'message': 'Authentication failure of type "eauth" occurred for ' - 'user UNKNOWN.'}} + 'message': 'Authentication failure of type "eauth" occurred for ' + 'user UNKNOWN.'}} ret = self.clear_funcs.wheel({'eauth': 'foo'}) self.assertDictEqual(mock_ret, ret) @@ -182,10 +264,10 @@ def test_wheel_eauth_authorization_error(self): ''' clear_load = {'eauth': 'foo', 'username': 'test', 'fun': 'test.arg'} mock_ret = {'error': {'name': 'EauthAuthenticationError', - 'message': 'Authentication failure of type "eauth" occurred for ' - 'user test.'}} + 'message': 'Authentication failure of type "eauth" occurred for ' + 'user test.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=[])): ret = self.clear_funcs.wheel(clear_load) self.assertDictEqual(mock_ret, ret) @@ -197,9 +279,9 @@ def test_wheel_eauth_salt_invocation_error(self): ''' clear_load = {'eauth': 'foo', 'username': 'test', 'fun': 'bad.test.arg.func'} mock_ret = {'error': {'name': 'SaltInvocationError', - 'message': 'A command invocation error occurred: Check syntax.'}} + 'message': 'A command invocation error occurred: Check syntax.'}} with patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ - patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): + patch('salt.auth.LoadAuth.get_auth_list', MagicMock(return_value=['testing'])): ret = self.clear_funcs.wheel(clear_load) self.assertDictEqual(mock_ret, ret) @@ -209,7 +291,7 @@ def test_wheel_user_not_authenticated(self): Asserts that an UserAuthenticationError is returned when the user can't authenticate. ''' mock_ret = {'error': {'name': 'UserAuthenticationError', - 'message': 'Authentication failure of type "user" occurred'}} + 'message': 'Authentication failure of type "user" occurred'}} ret = self.clear_funcs.wheel({}) self.assertDictEqual(mock_ret, ret) @@ -220,7 +302,7 @@ def test_publish_user_is_blacklisted(self): Asserts that an AuthorizationError is returned when the user has been blacklisted. ''' mock_ret = {'error': {'name': 'AuthorizationError', - 'message': 'Authorization error occurred.'}} + 'message': 'Authorization error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=True)): self.assertEqual(mock_ret, self.clear_funcs.publish({'user': 'foo', 'fun': 'test.arg'})) @@ -229,7 +311,7 @@ def test_publish_cmd_blacklisted(self): Asserts that an AuthorizationError is returned when the command has been blacklisted. ''' mock_ret = {'error': {'name': 'AuthorizationError', - 'message': 'Authorization error occurred.'}} + 'message': 'Authorization error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.acl.PublisherACL.cmd_is_blacklisted', MagicMock(return_value=True)): self.assertEqual(mock_ret, self.clear_funcs.publish({'user': 'foo', 'fun': 'test.arg'})) @@ -239,7 +321,7 @@ def test_publish_token_not_authenticated(self): Asserts that an AuthenticationError is returned when the token can't authenticate. ''' mock_ret = {'error': {'name': 'AuthenticationError', - 'message': 'Authentication error occurred.'}} + 'message': 'Authentication error occurred.'}} load = {'user': 'foo', 'fun': 'test.arg', 'tgt': 'test_minion', 'kwargs': {'token': 'asdfasdfasdfasdf'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ @@ -256,7 +338,7 @@ def test_publish_token_authorization_error(self): 'arg': 'bar', 'kwargs': {'token': token}} mock_token = {'token': token, 'eauth': 'foo', 'name': 'test'} mock_ret = {'error': {'name': 'AuthorizationError', - 'message': 'Authorization error occurred.'}} + 'message': 'Authorization error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.acl.PublisherACL.cmd_is_blacklisted', MagicMock(return_value=False)), \ @@ -271,7 +353,7 @@ def test_publish_eauth_not_authenticated(self): load = {'user': 'test', 'fun': 'test.arg', 'tgt': 'test_minion', 'kwargs': {'eauth': 'foo'}} mock_ret = {'error': {'name': 'AuthenticationError', - 'message': 'Authentication error occurred.'}} + 'message': 'Authentication error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.acl.PublisherACL.cmd_is_blacklisted', MagicMock(return_value=False)): self.assertEqual(mock_ret, self.clear_funcs.publish(load)) @@ -284,7 +366,7 @@ def test_publish_eauth_authorization_error(self): load = {'user': 'test', 'fun': 'test.arg', 'tgt': 'test_minion', 'kwargs': {'eauth': 'foo'}, 'arg': 'bar'} mock_ret = {'error': {'name': 'AuthorizationError', - 'message': 'Authorization error occurred.'}} + 'message': 'Authorization error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.acl.PublisherACL.cmd_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.auth.LoadAuth.authenticate_eauth', MagicMock(return_value=True)), \ @@ -297,7 +379,7 @@ def test_publish_user_not_authenticated(self): ''' load = {'user': 'test', 'fun': 'test.arg', 'tgt': 'test_minion'} mock_ret = {'error': {'name': 'AuthenticationError', - 'message': 'Authentication error occurred.'}} + 'message': 'Authentication error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.acl.PublisherACL.cmd_is_blacklisted', MagicMock(return_value=False)): self.assertEqual(mock_ret, self.clear_funcs.publish(load)) @@ -310,7 +392,7 @@ def test_publish_user_authenticated_missing_auth_list(self): load = {'user': 'test', 'fun': 'test.arg', 'tgt': 'test_minion', 'kwargs': {'user': 'test'}, 'arg': 'foo'} mock_ret = {'error': {'name': 'AuthenticationError', - 'message': 'Authentication error occurred.'}} + 'message': 'Authentication error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.acl.PublisherACL.cmd_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.auth.LoadAuth.authenticate_key', MagicMock(return_value='fake-user-key')), \ @@ -325,10 +407,12 @@ def test_publish_user_authorization_error(self): load = {'user': 'test', 'fun': 'test.arg', 'tgt': 'test_minion', 'kwargs': {'user': 'test'}, 'arg': 'foo'} mock_ret = {'error': {'name': 'AuthorizationError', - 'message': 'Authorization error occurred.'}} + 'message': 'Authorization error occurred.'}} with patch('salt.acl.PublisherACL.user_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.acl.PublisherACL.cmd_is_blacklisted', MagicMock(return_value=False)), \ patch('salt.auth.LoadAuth.authenticate_key', MagicMock(return_value='fake-user-key')), \ patch('salt.utils.master.get_values_of_matching_keys', MagicMock(return_value=['test'])), \ patch('salt.utils.minions.CkMinions.auth_check', MagicMock(return_value=False)): self.assertEqual(mock_ret, self.clear_funcs.publish(load)) + + diff --git a/tests/unit/test_pillar.py b/tests/unit/test_pillar.py index cec3b15bfbe2..b2b053058f55 100644 --- a/tests/unit/test_pillar.py +++ b/tests/unit/test_pillar.py @@ -828,3 +828,21 @@ def test_pillar_send_extra_minion_data_from_config(self): 'pillar_override': {}, 'extra_minion_data': {'path_to_add': 'fake_data'}}, dictkey='pillar') + +class Pillar(TestCase): + def test__get_envs(self): + opts = salt.config.master_config(None) + opts['ext_pillar'] = [ + {'environments': ['word']} + ] + + opts['evaporator'] = {} + opts['evaporator']['tenancies'] = [ + {"environment": "sltdm", "global": False}, + {"environment": "salt-native", "global": True}, + {"environment": "salt-water", "global": False}, + ] + + pillar = salt.pillar.Pillar(opts, {}, 'sltdm-rr-005', 'base') + res = pillar._get_envs() + self.assertEqual(res, {u'environments': ['salt-native', 'sltdm']}) diff --git a/tests/unit/utils/test_state.py b/tests/unit/utils/test_state.py index d076e7d00436..d4677a3f1ac5 100644 --- a/tests/unit/utils/test_state.py +++ b/tests/unit/utils/test_state.py @@ -686,3 +686,11 @@ def test_merge_empty_comments(self): res = salt.utils.state.merge_subreturn(m, s) self.assertEqual(res['comment'], [sub_comment_1, sub_comment_2]) self.assertEqual('\n'.join(res['comment']), final_comment) + +class UtilStateGetSlsOptsTestcase(TestCase): + ''' + Test cases for salt.utils.state.merge_subreturn function. + ''' + def test_get_sls_opts(self): + opts = salt.utils.state.get_sls_opts({'orig':'inal'}, {"environments": ['base', 'scuba-diving']}) + self.assertEqual(opts, {u'pillarenv': [u'base', u'scuba-diving'], u'saltenv': [u'base', u'scuba-diving'], u'orig': u'inal'})