From a1de4a137cec4610a1ef13ac1918a2d3166e5a96 Mon Sep 17 00:00:00 2001 From: Seth House Date: Wed, 22 Apr 2015 14:59:27 -0600 Subject: [PATCH 01/15] Add a method to NetapiClient to check if the master is running --- salt/netapi/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/salt/netapi/__init__.py b/salt/netapi/__init__.py index 032c5a51cd..6412b6d128 100644 --- a/salt/netapi/__init__.py +++ b/salt/netapi/__init__.py @@ -31,6 +31,17 @@ class NetapiClient(object): def __init__(self, opts): self.opts = opts + def is_master_running(self): + ''' + Perform a lightweight check to see if the master daemon is running + + Note, this will return an invalid success if the master crashed or was + not shut down cleanly. + ''' + return os.path.exists(os.path.join( + self.opts['sock_dir'], + 'workers.ipc')) + def run(self, low): ''' Execute the specified function in the specified client by passing the From deaa323acd9113f45e143bd6383bd82197547e56 Mon Sep 17 00:00:00 2001 From: Seth House Date: Wed, 22 Apr 2015 18:10:03 -0600 Subject: [PATCH 02/15] Add new exception subclass for when a required daemon is not running --- salt/exceptions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/salt/exceptions.py b/salt/exceptions.py index c9d8b5bad2..944f658347 100644 --- a/salt/exceptions.py +++ b/salt/exceptions.py @@ -182,6 +182,13 @@ class AuthorizationError(SaltException): ''' +class SaltDaemonNotRunning(SaltException): + ''' + Throw when a running master/minion/syndic is not running but is needed to + perform the requested operation (e.g., eauth). + ''' + + class SaltRunnerError(SaltException): ''' Problem in runner From 036c14cc59923002d432268ce13d9777fa667293 Mon Sep 17 00:00:00 2001 From: Seth House Date: Wed, 22 Apr 2015 18:10:46 -0600 Subject: [PATCH 03/15] Check if the Salt Master is running before trying to run a command --- salt/netapi/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/salt/netapi/__init__.py b/salt/netapi/__init__.py index 6412b6d128..60d154da10 100644 --- a/salt/netapi/__init__.py +++ b/salt/netapi/__init__.py @@ -16,7 +16,7 @@ import salt.syspaths import salt.wheel import salt.utils import salt.client.ssh.client -from salt.exceptions import SaltException, EauthAuthenticationError +import salt.exceptions class NetapiClient(object): @@ -47,11 +47,18 @@ class NetapiClient(object): Execute the specified function in the specified client by passing the lowstate ''' + # Eauth currently requires a running daemon and commands run through + # this method require eauth so perform a quick check to raise a + # more meaningful error. + if not self.is_master_running(): + raise salt.exceptions.SaltDaemonNotRunning( + 'Salt Master is not available.') + if 'client' not in low: - raise SaltException('No client specified') + raise salt.exceptions.SaltException('No client specified') if not ('token' in low or 'eauth' in low) and low['client'] != 'ssh': - raise EauthAuthenticationError( + raise salt.exceptions.EauthAuthenticationError( 'No authentication credentials given') l_fun = getattr(self, low['client']) From e83f73d3e87c569766fa4c8381e044ec0c698948 Mon Sep 17 00:00:00 2001 From: Seth House Date: Wed, 22 Apr 2015 18:22:17 -0600 Subject: [PATCH 04/15] Return a 503 when the Salt master is down; for commands and auths --- salt/netapi/rest_cherrypy/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/salt/netapi/rest_cherrypy/app.py b/salt/netapi/rest_cherrypy/app.py index b623caaadc..8f26999107 100644 --- a/salt/netapi/rest_cherrypy/app.py +++ b/salt/netapi/rest_cherrypy/app.py @@ -394,6 +394,9 @@ def hypermedia_handler(*args, **kwargs): except (salt.exceptions.EauthAuthenticationError, salt.exceptions.TokenAuthenticationError): raise cherrypy.HTTPError(401) + except (salt.exceptions.SaltDaemonNotRunning, + salt.exceptions.SaltReqTimeoutError) as exc: + raise cherrypy.HTTPError(503, exc.strerror) except cherrypy.CherryPyException: raise except Exception as exc: @@ -1361,6 +1364,10 @@ class Login(LowDataAdapter): ] }} ''' + if not self.api.is_master_running(): + raise salt.exceptions.SaltDaemonNotRunning( + 'Salt Master is not available.') + # the urlencoded_processor will wrap this in a list if isinstance(cherrypy.serving.request.lowstate, list): creds = cherrypy.serving.request.lowstate[0] From 4328b1839dcd15775c40de8b03f0d046ca269170 Mon Sep 17 00:00:00 2001 From: Seth House Date: Wed, 22 Apr 2015 21:54:13 -0600 Subject: [PATCH 05/15] Added 'once' support to the scheduler --- salt/modules/schedule.py | 5 +++- salt/utils/schedule.py | 64 +++++++++++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/salt/modules/schedule.py b/salt/modules/schedule.py index b14ad5ffc5..81096f4164 100644 --- a/salt/modules/schedule.py +++ b/salt/modules/schedule.py @@ -33,6 +33,8 @@ SCHEDULE_CONF = [ 'splay', 'range', 'when', + 'once', + 'once_fmt', 'returner', 'jid_include', 'args', @@ -259,7 +261,8 @@ def build_schedule_item(name, **kwargs): else: schedule[name]['splay'] = kwargs['splay'] - for item in ['range', 'when', 'cron', 'returner', 'return_config']: + for item in ['range', 'when', 'once', 'once_fmt', 'cron', 'returner', + 'return_config']: if item in kwargs: schedule[name][item] = kwargs[item] diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index bde010cb3c..12b8772376 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -85,6 +85,18 @@ localtime. - Thursday 3:00pm - Friday 5:00pm +This will schedule a job to run once on the specified date. The default date +format is ISO 8601 but can be overridden by also specifying the ``once_fmt`` +option. + +.. code-block:: yaml + + schedule: + job1: + function: test.ping + once: 2015-04-22T20:21:00 + once_fmt: '%Y-%m-%dT%H:%M:%S' + This will schedule the command: state.sls httpd test=True at 5pm on Monday, Wednesday and Friday, and 3pm on Tuesday and Thursday. @@ -212,6 +224,7 @@ from __future__ import absolute_import import os import time import datetime +import itertools import multiprocessing import threading import sys @@ -620,32 +633,55 @@ class Schedule(object): seconds = 0 cron = 0 now = int(time.time()) - time_conflict = False - for item in ['seconds', 'minutes', 'hours', 'days']: - if item in data and 'when' in data: - time_conflict = True - if item in data and 'cron' in data: - time_conflict = True + # Used for quick lookups when detecting invalid option combinations. + schedule_keys = set(data.keys()) - if time_conflict: - log.error('Unable to use "seconds", "minutes",' - '"hours", or "days" with ' - '"when" or "cron" options. Ignoring.') + time_elements = ('seconds', 'minutes', 'hours', 'days') + scheduling_elements = ('when', 'cron', 'once') + + invalid_sched_combos = [set(i) + for i in itertools.combinations(scheduling_elements, 2)] + + if any(i <= schedule_keys for i in invalid_sched_combos): + log.error('Unable to use "{0}" options together. Ignoring.' + .format('", "'.join(scheduling_elements))) continue - if 'when' in data and 'cron' in data: - log.error('Unable to use "when" and "cron" options together.' - 'Ignoring.') + invalid_time_combos = [] + for item in scheduling_elements: + all_items = itertools.chain([item], time_elements) + invalid_time_combos.append( + set(itertools.combinations(all_items, 2))) + + if any(set(x) <= schedule_keys for x in invalid_time_combos): + log.error('Unable to use "{0}" with "{1}" options. Ignoring' + .format('", "'.join(time_elements), + '", "'.join(scheduling_elements))) continue - time_elements = ['seconds', 'minutes', 'hours', 'days'] if True in [True for item in time_elements if item in data]: # Add up how many seconds between now and then seconds += int(data.get('seconds', 0)) seconds += int(data.get('minutes', 0)) * 60 seconds += int(data.get('hours', 0)) * 3600 seconds += int(data.get('days', 0)) * 86400 + elif 'once' in data: + once_fmt = data.get('once_fmt', '%Y-%m-%dT%H:%M:%S') + + try: + once = datetime.datetime.strptime(data['once'], once_fmt) + once = int(time.mktime(once.timetuple())) + except (TypeError, ValueError): + log.error('Date string could not be parsed: %s, %s', + data['once'], once_fmt) + continue + + if now != once: + continue + else: + seconds = 1 + elif 'when' in data: if not _WHEN_SUPPORTED: log.error('Missing python-dateutil.' From b24cbc68d58b51bd4fda9102ca85d282fdaf6568 Mon Sep 17 00:00:00 2001 From: Seth House Date: Thu, 23 Apr 2015 01:58:33 -0600 Subject: [PATCH 06/15] Added rest_cherrypy tool to serve HTML the root URL --- salt/netapi/rest_cherrypy/app.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/salt/netapi/rest_cherrypy/app.py b/salt/netapi/rest_cherrypy/app.py index 8f26999107..e92a5f845c 100644 --- a/salt/netapi/rest_cherrypy/app.py +++ b/salt/netapi/rest_cherrypy/app.py @@ -274,6 +274,41 @@ except ImportError: HAS_WEBSOCKETS = False +def html_override_tool(): + ''' + Bypass the normal handler and serve HTML for all URLs + + The ``app_path`` setting must be non-empty and the request must ask for + ``text/html`` in the ``Accept`` header. + ''' + apiopts = cherrypy.config['apiopts'] + request = cherrypy.request + + url_blacklist = ( + apiopts.get('app_path', '/app'), + apiopts.get('static_path', '/static'), + ) + + if 'app' not in cherrypy.config['apiopts']: + return + + if request.path_info.startswith(url_blacklist): + return + + if request.headers.get('Accept') == '*/*': + return + + try: + wants_html = cherrypy.lib.cptools.accept('text/html') + except cherrypy.HTTPError: + return + else: + if wants_html != 'text/html': + return + + raise cherrypy.InternalRedirect(apiopts.get('app_path', '/app')) + + def salt_token_tool(): ''' If the custom authentication header is supplied, put it in the cookie dict @@ -577,6 +612,8 @@ def lowdata_fmt(): cherrypy.serving.request.lowstate = data +cherrypy.tools.html_override = cherrypy.Tool('on_start_resource', + html_override_tool, priority=53) cherrypy.tools.salt_token = cherrypy.Tool('on_start_resource', salt_token_tool, priority=55) cherrypy.tools.salt_auth = cherrypy.Tool('before_request_body', @@ -2219,6 +2256,7 @@ class API(object): 'tools.cpstats.on': self.apiopts.get('collect_stats', False), + 'tools.html_override.on': True, 'tools.cors_tool.on': True, }, } From de976756d5da89cf0b51c7927b51790393d41000 Mon Sep 17 00:00:00 2001 From: Seth House Date: Thu, 23 Apr 2015 02:00:51 -0600 Subject: [PATCH 07/15] Switch is_master_running() to be a private method The list of available clients is generated from the public methods on the class. --- salt/netapi/__init__.py | 4 ++-- salt/netapi/rest_cherrypy/app.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/netapi/__init__.py b/salt/netapi/__init__.py index 60d154da10..ee093f233c 100644 --- a/salt/netapi/__init__.py +++ b/salt/netapi/__init__.py @@ -31,7 +31,7 @@ class NetapiClient(object): def __init__(self, opts): self.opts = opts - def is_master_running(self): + def _is_master_running(self): ''' Perform a lightweight check to see if the master daemon is running @@ -50,7 +50,7 @@ class NetapiClient(object): # Eauth currently requires a running daemon and commands run through # this method require eauth so perform a quick check to raise a # more meaningful error. - if not self.is_master_running(): + if not self._is_master_running(): raise salt.exceptions.SaltDaemonNotRunning( 'Salt Master is not available.') diff --git a/salt/netapi/rest_cherrypy/app.py b/salt/netapi/rest_cherrypy/app.py index e92a5f845c..28855ffae6 100644 --- a/salt/netapi/rest_cherrypy/app.py +++ b/salt/netapi/rest_cherrypy/app.py @@ -1401,7 +1401,7 @@ class Login(LowDataAdapter): ] }} ''' - if not self.api.is_master_running(): + if not self.api._is_master_running(): raise salt.exceptions.SaltDaemonNotRunning( 'Salt Master is not available.') From eceacad9c48ed504da81341f3d0a1af00de306f7 Mon Sep 17 00:00:00 2001 From: Seth House Date: Thu, 23 Apr 2015 02:12:12 -0600 Subject: [PATCH 08/15] Added favicon support to avoid 404s --- salt/netapi/rest_cherrypy/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/salt/netapi/rest_cherrypy/app.py b/salt/netapi/rest_cherrypy/app.py index 28855ffae6..b4f91cc8a6 100644 --- a/salt/netapi/rest_cherrypy/app.py +++ b/salt/netapi/rest_cherrypy/app.py @@ -2261,6 +2261,12 @@ class API(object): }, } + if 'favicon' in self.apiopts: + conf['/favicon.ico'] = { + 'tools.staticfile.on': True, + 'tools.staticfile.filename': self.apiopts['favicon'], + } + if self.apiopts.get('debug', False) is False: conf['global']['environment'] = 'production' From 73178d7a6198cea01bf36178a43b57bb25a4baa1 Mon Sep 17 00:00:00 2001 From: Mathieu Le Marec - Pasquet Date: Thu, 23 Apr 2015 11:39:52 +0200 Subject: [PATCH 09/15] Fix file.managed absent source error --- salt/modules/file.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index 19d2d2f2f6..69537a1d18 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2765,7 +2765,9 @@ def get_managed( source_sum = {} if template and source: sfn = __salt__['cp.cache_file'](source, saltenv) - if not os.path.exists(sfn): + # exists doesn't play nice with sfn as bool + # but if cache failed, sfn == False + if not sfn or not os.path.exists(sfn): return sfn, {}, 'Source file {0} not found'.format(source) if sfn == name: raise SaltInvocationError( From 2c03692d04898acb0c5c42ce9404f33b1b4af03f Mon Sep 17 00:00:00 2001 From: Jayesh Kariya Date: Thu, 23 Apr 2015 17:27:28 +0530 Subject: [PATCH 10/15] adding states/boto_elasticache unit test case --- tests/unit/states/boto_elasticache_test.py | 120 +++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/unit/states/boto_elasticache_test.py diff --git a/tests/unit/states/boto_elasticache_test.py b/tests/unit/states/boto_elasticache_test.py new file mode 100644 index 0000000000..a026960a8c --- /dev/null +++ b/tests/unit/states/boto_elasticache_test.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Jayesh Kariya ` +''' +# Import Python libs +from __future__ import absolute_import + +# Import Salt Testing Libs +from salttesting import skipIf, TestCase +from salttesting.mock import ( + NO_MOCK, + NO_MOCK_REASON, + MagicMock, + patch) + +from salttesting.helpers import ensure_in_syspath + +ensure_in_syspath('../../') + +# Import Salt Libs +from salt.states import boto_elasticache + +boto_elasticache.__salt__ = {} +boto_elasticache.__opts__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BotoElasticacheTestCase(TestCase): + ''' + Test cases for salt.states.boto_elasticache + ''' + # 'present' function tests: 1 + + def test_present(self): + ''' + Test to ensure the cache cluster exists. + ''' + name = 'myelasticache' + + ret = {'name': name, + 'result': None, + 'changes': {}, + 'comment': ''} + + mock = MagicMock(side_effect=[None, False, False, True]) + mock_bool = MagicMock(return_value=False) + with patch.dict(boto_elasticache.__salt__, + {'boto_elasticache.get_config': mock, + 'boto_elasticache.create': mock_bool}): + comt = ('Failed to retrieve cache cluster info from AWS.') + ret.update({'comment': comt}) + self.assertDictEqual(boto_elasticache.present(name), ret) + + with patch.dict(boto_elasticache.__opts__, {'test': True}): + comt = ('Cache cluster {0} is set to be created.'.format(name)) + ret.update({'comment': comt}) + self.assertDictEqual(boto_elasticache.present(name), ret) + + with patch.dict(boto_elasticache.__opts__, {'test': False}): + comt = ('Failed to create {0} cache cluster.'.format(name)) + ret.update({'comment': comt, 'result': False}) + self.assertDictEqual(boto_elasticache.present(name), ret) + + comt = ('Cache cluster {0} is present.'.format(name)) + ret.update({'comment': comt, 'result': True}) + self.assertDictEqual(boto_elasticache.present(name), ret) + + # 'absent' function tests: 1 + + def test_absent(self): + ''' + Test to ensure the named elasticache cluster is deleted. + ''' + name = 'new_table' + + ret = {'name': name, + 'result': True, + 'changes': {}, + 'comment': ''} + + mock = MagicMock(side_effect=[False, True]) + with patch.dict(boto_elasticache.__salt__, + {'boto_elasticache.exists': mock}): + comt = ('{0} does not exist in None.'.format(name)) + ret.update({'comment': comt}) + self.assertDictEqual(boto_elasticache.absent(name), ret) + + with patch.dict(boto_elasticache.__opts__, {'test': True}): + comt = ('Cache cluster {0} is set to be removed.'.format(name)) + ret.update({'comment': comt, 'result': None}) + self.assertDictEqual(boto_elasticache.absent(name), ret) + + # 'creategroup' function tests: 1 + + def test_creategroup(self): + ''' + Test to ensure the a replication group is create. + ''' + name = 'new_table' + primary_cluster_id = 'A' + replication_group_description = 'my description' + + ret = {'name': name, + 'result': True, + 'changes': {}, + 'comment': ''} + + mock = MagicMock(return_value=True) + with patch.dict(boto_elasticache.__salt__, + {'boto_elasticache.group_exists': mock}): + comt = ('{0} replication group exists .'.format(name)) + ret.update({'comment': comt}) + self.assertDictEqual(boto_elasticache.creategroup + (name, primary_cluster_id, + replication_group_description), ret) + + +if __name__ == '__main__': + from integration import run_tests + run_tests(BotoElasticacheTestCase, needs_daemon=False) From 6ef51d1b2d6dde49ca8d9d268ecdabd267e8d5e0 Mon Sep 17 00:00:00 2001 From: Jayesh Kariya Date: Thu, 23 Apr 2015 17:29:19 +0530 Subject: [PATCH 11/15] adding states/boto_elb unit test case --- tests/unit/states/boto_elb_test.py | 163 +++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 tests/unit/states/boto_elb_test.py diff --git a/tests/unit/states/boto_elb_test.py b/tests/unit/states/boto_elb_test.py new file mode 100644 index 0000000000..49b208acc7 --- /dev/null +++ b/tests/unit/states/boto_elb_test.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Jayesh Kariya ` +''' +# Import Python libs +from __future__ import absolute_import +import copy + +# Import Salt Testing Libs +from salttesting import skipIf, TestCase +from salttesting.mock import ( + NO_MOCK, + NO_MOCK_REASON, + MagicMock, + patch) + +from salttesting.helpers import ensure_in_syspath + +ensure_in_syspath('../../') + +# Import Salt Libs +from salt.states import boto_elb + +boto_elb.__salt__ = {} +boto_elb.__opts__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BotoElbTestCase(TestCase): + ''' + Test cases for salt.states.boto_elb + ''' + # 'present' function tests: 1 + + def test_present(self): + ''' + Test to ensure the IAM role exists. + ''' + name = 'myelb' + listeners = [{'elb_port': 'ELBPORT', 'instance_port': 'PORT', + 'elb_protocol': 'HTTPS', 'certificate': 'A'}] + attributes = {'alarm_actions': ['arn:aws:sns:us-east-1:12345:myalarm'], + 'insufficient_data_actions': [], + 'ok_actions': ['arn:aws:sns:us-east-1:12345:myalarm']} + avail_zones = ['us-east-1a', 'us-east-1c', 'us-east-1d'] + alarms = {'alarm_actions': {'name': name, + 'attributes': {'description': 'A'}}} + + ret = {'name': name, + 'result': False, + 'changes': {}, + 'comment': ''} + ret1 = copy.deepcopy(ret) + + mock = MagicMock(return_value={}) + mock_bool = MagicMock(return_value=False) + with patch.dict(boto_elb.__salt__, + {'config.option': mock, + 'boto_elb.exists': mock_bool, + 'boto_elb.create': mock_bool, + 'boto_elb.get_attributes': mock}): + with patch.dict(boto_elb.__opts__, {'test': False}): + comt = (' Failed to create myelb ELB.') + ret.update({'comment': comt}) + self.assertDictEqual(boto_elb.present + (name, listeners, attributes=attributes, + availability_zones=avail_zones), ret) + + mock = MagicMock(return_value={}) + mock_ret = MagicMock(return_value={'result': {'result': False}}) + comt1 = (' Failed to retrieve health_check for ELB myelb.') + with patch.dict(boto_elb.__salt__, + {'config.option': mock, + 'boto_elb.get_attributes': mock, + 'boto_elb.get_health_check': mock, + 'boto_elb.get_elb_config': mock, + 'state.single': mock_ret}): + with patch.dict(boto_elb.__opts__, {'test': False}): + ret1.update({'result': True}) + mock_elb_present = MagicMock(return_value=ret1) + with patch.object(boto_elb, '_elb_present', mock_elb_present): + comt = (' Failed to retrieve attributes for ELB myelb.') + ret.update({'comment': comt}) + self.assertDictEqual(boto_elb.present + (name, listeners), ret) + + with patch.object(boto_elb, '_attributes_present', + mock_elb_present): + ret.update({'comment': comt1}) + self.assertDictEqual(boto_elb.present + (name, listeners), ret) + + with patch.object(boto_elb, '_health_check_present', + mock_elb_present): + comt = (' Failed to retrieve ELB myelb.') + ret.update({'comment': comt}) + self.assertDictEqual(boto_elb.present + (name, listeners), ret) + + with patch.object(boto_elb, '_cnames_present', + mock_elb_present): + comt = (' ') + ret.update({'comment': comt}) + self.assertDictEqual(boto_elb.present + (name, listeners, + alarms=alarms), ret) + + with patch.object(boto_elb, '_alarms_present', + mock_elb_present): + ret.update({'result': True}) + self.assertDictEqual(boto_elb.present + (name, listeners, + alarms=alarms), ret) + + # 'register_instances' function tests: 1 + + def test_register_instances(self): + ''' + Test to add instance/s to load balancer + ''' + name = 'myelb' + instances = ['instance-id1', 'instance-id2'] + + ret = {'name': name, + 'result': None, + 'changes': {}, + 'comment': ''} + + mock_bool = MagicMock(return_value=False) + with patch.dict(boto_elb.__salt__, {'boto_elb.exists': mock_bool}): + comt = ('Could not find lb {0}'.format(name)) + ret.update({'comment': comt}) + self.assertDictEqual(boto_elb.register_instances(name, + instances), ret) + + # 'absent' function tests: 1 + + def test_absent(self): + ''' + Test to ensure the IAM role is deleted. + ''' + name = 'new_table' + + ret = {'name': name, + 'result': True, + 'changes': {}, + 'comment': ''} + + mock = MagicMock(side_effect=[False, True]) + with patch.dict(boto_elb.__salt__, {'boto_elb.exists': mock}): + comt = ('{0} ELB does not exist.'.format(name)) + ret.update({'comment': comt}) + self.assertDictEqual(boto_elb.absent(name), ret) + + with patch.dict(boto_elb.__opts__, {'test': True}): + comt = ('ELB {0} is set to be removed.'.format(name)) + ret.update({'comment': comt, 'result': None}) + self.assertDictEqual(boto_elb.absent(name), ret) + + +if __name__ == '__main__': + from integration import run_tests + run_tests(BotoElbTestCase, needs_daemon=False) From 06bf61822d086de7e1e5694593103d156e809c30 Mon Sep 17 00:00:00 2001 From: Jayesh Kariya Date: Thu, 23 Apr 2015 17:56:04 +0530 Subject: [PATCH 12/15] changes as per 2015.2 branch --- tests/unit/states/boto_elasticache_test.py | 41 +++++++--------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/tests/unit/states/boto_elasticache_test.py b/tests/unit/states/boto_elasticache_test.py index a026960a8c..ed6d52bdc3 100644 --- a/tests/unit/states/boto_elasticache_test.py +++ b/tests/unit/states/boto_elasticache_test.py @@ -36,6 +36,8 @@ class BotoElasticacheTestCase(TestCase): Test to ensure the cache cluster exists. ''' name = 'myelasticache' + engine = 'redis' + cache_node_type = 'cache.t1.micro' ret = {'name': name, 'result': None, @@ -49,21 +51,28 @@ class BotoElasticacheTestCase(TestCase): 'boto_elasticache.create': mock_bool}): comt = ('Failed to retrieve cache cluster info from AWS.') ret.update({'comment': comt}) - self.assertDictEqual(boto_elasticache.present(name), ret) + self.assertDictEqual(boto_elasticache.present(name, engine, + cache_node_type), ret) with patch.dict(boto_elasticache.__opts__, {'test': True}): comt = ('Cache cluster {0} is set to be created.'.format(name)) ret.update({'comment': comt}) - self.assertDictEqual(boto_elasticache.present(name), ret) + self.assertDictEqual(boto_elasticache.present(name, engine, + cache_node_type), + ret) with patch.dict(boto_elasticache.__opts__, {'test': False}): comt = ('Failed to create {0} cache cluster.'.format(name)) ret.update({'comment': comt, 'result': False}) - self.assertDictEqual(boto_elasticache.present(name), ret) + self.assertDictEqual(boto_elasticache.present(name, engine, + cache_node_type), + ret) comt = ('Cache cluster {0} is present.'.format(name)) ret.update({'comment': comt, 'result': True}) - self.assertDictEqual(boto_elasticache.present(name), ret) + self.assertDictEqual(boto_elasticache.present(name, engine, + cache_node_type), + ret) # 'absent' function tests: 1 @@ -90,30 +99,6 @@ class BotoElasticacheTestCase(TestCase): ret.update({'comment': comt, 'result': None}) self.assertDictEqual(boto_elasticache.absent(name), ret) - # 'creategroup' function tests: 1 - - def test_creategroup(self): - ''' - Test to ensure the a replication group is create. - ''' - name = 'new_table' - primary_cluster_id = 'A' - replication_group_description = 'my description' - - ret = {'name': name, - 'result': True, - 'changes': {}, - 'comment': ''} - - mock = MagicMock(return_value=True) - with patch.dict(boto_elasticache.__salt__, - {'boto_elasticache.group_exists': mock}): - comt = ('{0} replication group exists .'.format(name)) - ret.update({'comment': comt}) - self.assertDictEqual(boto_elasticache.creategroup - (name, primary_cluster_id, - replication_group_description), ret) - if __name__ == '__main__': from integration import run_tests From d347bb4e3b3f182bdf336f1c55c4405859ae47b1 Mon Sep 17 00:00:00 2001 From: Jayesh Kariya Date: Thu, 23 Apr 2015 17:58:08 +0530 Subject: [PATCH 13/15] changes as per 2015.2 branch --- tests/unit/states/boto_elb_test.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/tests/unit/states/boto_elb_test.py b/tests/unit/states/boto_elb_test.py index 49b208acc7..eb21a741ac 100644 --- a/tests/unit/states/boto_elb_test.py +++ b/tests/unit/states/boto_elb_test.py @@ -112,27 +112,6 @@ class BotoElbTestCase(TestCase): (name, listeners, alarms=alarms), ret) - # 'register_instances' function tests: 1 - - def test_register_instances(self): - ''' - Test to add instance/s to load balancer - ''' - name = 'myelb' - instances = ['instance-id1', 'instance-id2'] - - ret = {'name': name, - 'result': None, - 'changes': {}, - 'comment': ''} - - mock_bool = MagicMock(return_value=False) - with patch.dict(boto_elb.__salt__, {'boto_elb.exists': mock_bool}): - comt = ('Could not find lb {0}'.format(name)) - ret.update({'comment': comt}) - self.assertDictEqual(boto_elb.register_instances(name, - instances), ret) - # 'absent' function tests: 1 def test_absent(self): From b9188aea6a0044e721153892d3f781ff9c061f57 Mon Sep 17 00:00:00 2001 From: Jayesh Kariya Date: Thu, 23 Apr 2015 18:13:27 +0530 Subject: [PATCH 14/15] adding qemu_img unit test case --- tests/unit/modules/qemu_img_test.py | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/unit/modules/qemu_img_test.py diff --git a/tests/unit/modules/qemu_img_test.py b/tests/unit/modules/qemu_img_test.py new file mode 100644 index 0000000000..ae1541e9bc --- /dev/null +++ b/tests/unit/modules/qemu_img_test.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Rupesh Tare ` +''' + +# Import Python Libs +from __future__ import absolute_import + +# Import Salt Testing Libs +from salttesting import TestCase, skipIf +from salttesting.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +from salttesting.helpers import ensure_in_syspath + +ensure_in_syspath('../../') + +# Import Salt Libs +from salt.modules import qemu_img +import os + +# Globals +qemu_img.__salt__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class QemuimgTestCase(TestCase): + ''' + Test cases for salt.modules.qemu_img + ''' + def test_make_image(self): + ''' + Test for create a blank virtual machine image file + of the specified size in megabytes + ''' + with patch.object(os.path, 'isabs', + MagicMock(side_effect=[False, True, True, True])): + self.assertEqual(qemu_img.make_image('location', 'size', 'fmt'), '') + + with patch.object(os.path, 'isdir', + MagicMock(side_effect=[False, True, True])): + self.assertEqual(qemu_img.make_image('location', 'size', 'fmt'), + '') + + with patch.dict(qemu_img.__salt__, + {'cmd.retcode': MagicMock(side_effect=[False, + True])}): + self.assertEqual(qemu_img.make_image('location', 'size', + 'fmt'), 'location') + + self.assertEqual(qemu_img.make_image('location', 'size', + 'fmt'), '') + + +if __name__ == '__main__': + from integration import run_tests + run_tests(QemuimgTestCase, needs_daemon=False) From 34453b314e2c2ca1ee4baf60a763346453a1b9a0 Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Thu, 23 Apr 2015 08:09:00 -0600 Subject: [PATCH 15/15] fix failing apache state unit test --- tests/unit/states/apache_test.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/unit/states/apache_test.py b/tests/unit/states/apache_test.py index 8f96e2706c..cc652da972 100644 --- a/tests/unit/states/apache_test.py +++ b/tests/unit/states/apache_test.py @@ -39,28 +39,34 @@ class ApacheTestCase(TestCase): Test to allows for inputting a yaml dictionary into a file for apache configuration files. ''' - name = 'yaml' + name = '/etc/distro/specific/apache.conf' config = 'VirtualHost: this: "*:80"' + new_config = 'LiteralHost: that: "*:79"' ret = {'name': name, 'result': True, 'changes': {}, 'comment': ''} - mock = MagicMock(side_effect=[config, '', '']) with patch.object(salt.utils, 'fopen', mock_open(read_data=config)): - with patch.dict(apache.__salt__, - {'apache.config': mock}): + mock_config = MagicMock(return_value=config) + with patch.dict(apache.__salt__, {'apache.config': mock_config}): ret.update({'comment': 'Configuration is up to date.'}) self.assertDictEqual(apache.configfile(name, config), ret) + with patch.object(salt.utils, 'fopen', mock_open(read_data=config)): + mock_config = MagicMock(return_value=new_config) + with patch.dict(apache.__salt__, {'apache.config': mock_config}): ret.update({'comment': 'Configuration will update.', - 'changes': {'new': '', - 'old': 'VirtualHost: this: "*:80"'}, + 'changes': {'new': new_config, + 'old': config}, 'result': None}) with patch.dict(apache.__opts__, {'test': True}): - self.assertDictEqual(apache.configfile(name, config), ret) + self.assertDictEqual(apache.configfile(name, new_config), ret) + with patch.object(salt.utils, 'fopen', mock_open(read_data=config)): + mock_config = MagicMock(return_value=new_config) + with patch.dict(apache.__salt__, {'apache.config': mock_config}): ret.update({'comment': 'Successfully created configuration.', 'result': True}) with patch.dict(apache.__opts__, {'test': False}):