From 4f59dd41a1e6a7c39acb7843a6f9322e49f99d36 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 2 Jan 2018 17:59:01 -0600 Subject: [PATCH 1/7] refactoring wait_for_ready to be used in both VS and hardware managers --- SoftLayer/managers/hardware.py | 26 ++++++++++++++++ SoftLayer/managers/vs.py | 57 ++++++++++------------------------ SoftLayer/utils.py | 24 ++++++++++++++ tests/managers/vs_tests.py | 22 ++++++------- 4 files changed, 77 insertions(+), 52 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 97ef6cd67..072894660 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -7,6 +7,7 @@ """ import logging import socket +import time import SoftLayer from SoftLayer.decoration import retry @@ -587,6 +588,31 @@ def update_firmware(self, bool(ipmi), bool(raid_controller), bool(bios), bool(hard_drive), id=hardware_id) + @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + def wait_for_ready(self, instance_id, limit=14400, delay=10): + """Determine if a Server is ready. + + A server is ready when no transactions are running on it. + + :param int instance_id: The instance ID with the pending transaction + :param int limit: The maximum amount of seconds to wait. + :param int delay: The number of seconds to sleep before checks. Defaults to 10. + """ + now = time.time() + until = now + limit + mask = "mask[id, lastOperatingSystemReload[id], activeTransaction[id, keyName], provisionDate]" + instance = self.get_hardware(instance_id, mask=mask) + while now < until and not utils.is_ready(instance): + snooze = min(delay, until - now) + LOGGER.info("%d not ready. Auto retry in %ds", instance_id, snooze) + time.sleep(snooze) + instance = self.get_hardware(instance_id, mask=mask) + now = time.time() + if now >= until: + LOGGER.info("Waiting for %d expired.", instance_id) + return False + return True + def _get_extra_price_id(items, key_name, hourly, location): """Returns a price id attached to item with the given key_name.""" diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index c0456a87b..0aed8a0e2 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -6,9 +6,7 @@ :license: MIT, see LICENSE for more details. """ import datetime -import itertools import logging -import random import socket import time import warnings @@ -428,7 +426,8 @@ def wait_for_transaction(self, instance_id, limit, delay=10): return self.wait_for_ready(instance_id, limit, delay=delay, pending=True) - def wait_for_ready(self, instance_id, limit, delay=10, pending=False): + @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + def wait_for_ready(self, instance_id, limit=3600, delay=10, pending=False): """Determine if a VS is ready and available. In some cases though, that can mean that no transactions are running. @@ -439,7 +438,7 @@ def wait_for_ready(self, instance_id, limit, delay=10, pending=False): cancellations. :param int instance_id: The instance ID with the pending transaction - :param int limit: The maximum amount of time to wait. + :param int limit: The maximum amount of seconds to wait. :param int delay: The number of seconds to sleep before checks. Defaults to 10. :param bool pending: Wait for pending transactions not related to provisioning or reloads such as monitoring. @@ -449,44 +448,20 @@ def wait_for_ready(self, instance_id, limit, delay=10, pending=False): # Will return once vsi 12345 is ready, or after 10 checks ready = mgr.wait_for_ready(12345, 10) """ - until = time.time() + limit - for new_instance in itertools.repeat(instance_id): - mask = """id, - lastOperatingSystemReload.id, - activeTransaction.id,provisionDate""" - try: - instance = self.get_instance(new_instance, mask=mask) - last_reload = utils.lookup(instance, 'lastOperatingSystemReload', 'id') - active_transaction = utils.lookup(instance, 'activeTransaction', 'id') - - reloading = all(( - active_transaction, - last_reload, - last_reload == active_transaction, - )) - - # only check for outstanding transactions if requested - outstanding = False - if pending: - outstanding = active_transaction - - # return True if the instance has finished provisioning - # and isn't currently reloading the OS. - if all([instance.get('provisionDate'), - not reloading, - not outstanding]): - return True - LOGGER.info("%s not ready.", str(instance_id)) - except exceptions.SoftLayerAPIError as exception: - delay = (delay * 2) + random.randint(0, 9) - LOGGER.info('Exception: %s', str(exception)) - + now = time.time() + until = now + limit + mask = "mask[id, lastOperatingSystemReload[id], activeTransaction[id, keyName], provisionDate]" + instance = self.get_instance(instance_id, mask=mask) + while now < until and not utils.is_ready(instance, pending): + snooze = min(delay, until - now) + LOGGER.info("%d not ready. Auto retry in %ds", instance_id, snooze) + time.sleep(snooze) + instance = self.get_instance(instance_id, mask=mask) now = time.time() - if now >= until: - return False - LOGGER.info('Auto retry in %s seconds', str(min(delay, until - now))) - time.sleep(min(delay, until - now)) - return False + if now >= until: + LOGGER.info("Waiting for %d expired.", instance_id) + return False + return True def verify_create_instance(self, **kwargs): """Verifies an instance creation command. diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index f39ffc0fe..07eb72edb 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -185,3 +185,27 @@ def tzname(self, _): def dst(self, _): return datetime.timedelta(0) + + +def is_ready(instance, pending=False): + """Returns True if instance is ready to be used + + :param Object instance: Hardware or Virt with transaction data retrieved from the API + :param bool pending: Wait for ALL transactions to finish? + :returns bool: + """ + + last_reload = lookup(instance, 'lastOperatingSystemReload', 'id') + active_transaction = lookup(instance, 'activeTransaction', 'id') + + reloading = all(( + active_transaction, + last_reload, + last_reload == active_transaction, + )) + outstanding = False + if pending: + outstanding = active_transaction + if instance.get('provisionDate') and not reloading and not outstanding: + return True + return False diff --git a/tests/managers/vs_tests.py b/tests/managers/vs_tests.py index 16c96d44e..fddb49959 100644 --- a/tests/managers/vs_tests.py +++ b/tests/managers/vs_tests.py @@ -886,22 +886,22 @@ def test_iter_20_incomplete(self, _sleep, _time): _sleep.assert_has_calls([mock.call(10)]) + @mock.patch('SoftLayer.decoration.sleep') @mock.patch('SoftLayer.managers.vs.VSManager.get_instance') - @mock.patch('random.randint') @mock.patch('time.time') @mock.patch('time.sleep') - def test_exception_from_api(self, _sleep, _time, _random, _vs): + def test_exception_from_api(self, _sleep, _time, _vs, _dsleep): """Tests escalating scale back when an excaption is thrown""" + _dsleep.return_value = False self.guestObject.return_value = {'activeTransaction': {'id': 1}} - _vs.side_effect = exceptions.TransportError(104, "Its broken") + _vs.side_effect = [ + exceptions.TransportError(104, "Its broken"), + {'activeTransaction': {'id': 1}}, + {'provisionDate': 'aaa'} + ] # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. _time.side_effect = [0, 0, 0, 0, 2, 2, 2, 6, 6, 6, 14, 14, 14, 20, 20, 20, 100, 100, 100] - _random.side_effect = [0, 0, 0, 0, 0] value = self.vs.wait_for_ready(1, 20, delay=1) - _sleep.assert_has_calls([ - mock.call(2), - mock.call(4), - mock.call(8), - mock.call(6) - ]) - self.assertFalse(value) + _sleep.assert_has_calls([mock.call(1)]) + _dsleep.assert_called_once() + self.assertTrue(value) From 5a4508df258ac0a8ef4e8abc3b51a02d29df06b2 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Wed, 3 Jan 2018 18:13:41 -0600 Subject: [PATCH 2/7] some more refactoring of wait_for_ready, added slcli hw ready command. fixed an issue with the retry decorator being too greedy --- SoftLayer/CLI/__init__.py | 5 ++++ SoftLayer/CLI/routes.py | 1 + SoftLayer/CLI/virt/ready.py | 6 +---- SoftLayer/decoration.py | 11 +++++++- SoftLayer/exceptions.py | 12 +++------ SoftLayer/managers/hardware.py | 31 +++++++++++------------ SoftLayer/managers/vs.py | 31 ++++++++++++----------- SoftLayer/transports.py | 4 +-- tests/managers/vs_tests.py | 46 ++++++++++++++++------------------ 9 files changed, 76 insertions(+), 71 deletions(-) diff --git a/SoftLayer/CLI/__init__.py b/SoftLayer/CLI/__init__.py index 3d5d6bf79..31d8f4b88 100644 --- a/SoftLayer/CLI/__init__.py +++ b/SoftLayer/CLI/__init__.py @@ -6,5 +6,10 @@ :license: MIT, see LICENSE for more details. """ # pylint: disable=w0401, invalid-name +import logging from SoftLayer.CLI.helpers import * # NOQA + +logger = logging.getLogger() +logger.addHandler(logging.StreamHandler()) +logger.setLevel(logging.INFO) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 3fa115828..f191e73bc 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -223,6 +223,7 @@ ('hardware:credentials', 'SoftLayer.CLI.hardware.credentials:cli'), ('hardware:update-firmware', 'SoftLayer.CLI.hardware.update_firmware:cli'), ('hardware:rescue', 'SoftLayer.CLI.hardware.power:rescue'), + ('hardware:ready', 'SoftLayer.CLI.hardware.ready:cli'), ('securitygroup', 'SoftLayer.CLI.securitygroup'), ('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'), diff --git a/SoftLayer/CLI/virt/ready.py b/SoftLayer/CLI/virt/ready.py index 1be2e1b42..c41680436 100644 --- a/SoftLayer/CLI/virt/ready.py +++ b/SoftLayer/CLI/virt/ready.py @@ -11,11 +11,7 @@ @click.command() @click.argument('identifier') -@click.option('--wait', - default=0, - show_default=True, - type=click.INT, - help="Name of the image") +@click.option('--wait', default=0, show_default=True, type=click.INT, help="Seconds to wait") @environment.pass_env def cli(env, identifier, wait): """Check if a virtual server is ready.""" diff --git a/SoftLayer/decoration.py b/SoftLayer/decoration.py index 8fb759893..66ce62ccb 100644 --- a/SoftLayer/decoration.py +++ b/SoftLayer/decoration.py @@ -9,8 +9,17 @@ from random import randint from time import sleep +from SoftLayer import exceptions -def retry(ex, tries=4, delay=5, backoff=2, logger=None): +RETRIABLE = ( + exceptions.ServerError, + exceptions.ApplicationError, + exceptions.RemoteSystemError, + exceptions.TransportError +) + + +def retry(ex=RETRIABLE, tries=4, delay=5, backoff=2, logger=None): """Retry calling the decorated function using an exponential backoff. http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ diff --git a/SoftLayer/exceptions.py b/SoftLayer/exceptions.py index 13da8474b..450dc23e0 100644 --- a/SoftLayer/exceptions.py +++ b/SoftLayer/exceptions.py @@ -27,14 +27,10 @@ def __init__(self, fault_code, fault_string, *args): self.reason = self.faultString = fault_string def __repr__(self): - return '<%s(%s): %s>' % (self.__class__.__name__, - self.faultCode, - self.faultString) + return '<%s(%s): %s>' % (self.__class__.__name__, self.faultCode, self.faultString) def __str__(self): - return '%s(%s): %s' % (self.__class__.__name__, - self.faultCode, - self.faultString) + return '%s(%s): %s' % (self.__class__.__name__, self.faultCode, self.faultString) class ParseError(SoftLayerAPIError): @@ -78,12 +74,12 @@ class SpecViolation(ServerError): pass -class MethodNotFound(ServerError): +class MethodNotFound(SoftLayerAPIError): """Method name not found.""" pass -class InvalidMethodParameters(ServerError): +class InvalidMethodParameters(SoftLayerAPIError): """Invalid method paramters.""" pass diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 072894660..ae0a4a429 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -11,7 +11,6 @@ import SoftLayer from SoftLayer.decoration import retry -from SoftLayer import exceptions from SoftLayer.managers import ordering from SoftLayer import utils @@ -89,7 +88,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', immediate, False, cancel_reason, comment, id=billing_id) - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, domain=None, datacenter=None, nic_speed=None, public_ip=None, private_ip=None, **kwargs): @@ -177,7 +176,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, kwargs['filter'] = _filter.to_dict() return self.account.getHardware(**kwargs) - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def get_hardware(self, hardware_id, **kwargs): """Get details about a hardware device. @@ -344,7 +343,7 @@ def get_cancellation_reasons(self): 'moving': 'Moving to competitor', } - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def get_create_options(self): """Returns valid options for ordering hardware.""" @@ -405,7 +404,7 @@ def get_create_options(self): 'extras': extras, } - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def _get_package(self): """Get the package related to simple hardware ordering.""" mask = ''' @@ -585,11 +584,9 @@ def update_firmware(self, """ return self.hardware.createFirmwareUpdateTransaction( - bool(ipmi), bool(raid_controller), bool(bios), bool(hard_drive), - id=hardware_id) + bool(ipmi), bool(raid_controller), bool(bios), bool(hard_drive), id=hardware_id) - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) - def wait_for_ready(self, instance_id, limit=14400, delay=10): + def wait_for_ready(self, instance_id, limit=14400, delay=10, pending=False): """Determine if a Server is ready. A server is ready when no transactions are running on it. @@ -600,18 +597,20 @@ def wait_for_ready(self, instance_id, limit=14400, delay=10): """ now = time.time() until = now + limit - mask = "mask[id, lastOperatingSystemReload[id], activeTransaction[id, keyName], provisionDate]" + mask = "mask[id, lastOperatingSystemReload[id], activeTransaction, provisionDate]" instance = self.get_hardware(instance_id, mask=mask) - while now < until and not utils.is_ready(instance): + while now <= until: + if utils.is_ready(instance, pending): + return True + transaction = utils.lookup(instance, 'activeTransaction', 'transactionStatus', 'friendlyName') snooze = min(delay, until - now) - LOGGER.info("%d not ready. Auto retry in %ds", instance_id, snooze) + LOGGER.info("%s - %d not ready. Auto retry in %ds", transaction, instance_id, snooze) time.sleep(snooze) instance = self.get_hardware(instance_id, mask=mask) now = time.time() - if now >= until: - LOGGER.info("Waiting for %d expired.", instance_id) - return False - return True + + LOGGER.info("Waiting for %d expired.", instance_id) + return False def _get_extra_price_id(items, key_name, hourly, location): diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 0aed8a0e2..fa20ef86f 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -56,7 +56,7 @@ def __init__(self, client, ordering_manager=None): else: self.ordering_manager = ordering_manager - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, memory=None, hostname=None, domain=None, local_disk=None, datacenter=None, nic_speed=None, @@ -160,7 +160,7 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None, func = getattr(self.account, call) return func(**kwargs) - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def get_instance(self, instance_id, **kwargs): """Get details about a virtual server instance. @@ -235,7 +235,7 @@ def get_instance(self, instance_id, **kwargs): return self.guest.getObject(id=instance_id, **kwargs) - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def get_create_options(self): """Retrieves the available options for creating a VS. @@ -412,7 +412,7 @@ def _generate_create_dict( return data - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def wait_for_transaction(self, instance_id, limit, delay=10): """Waits on a VS transaction for the specified amount of time. @@ -426,7 +426,6 @@ def wait_for_transaction(self, instance_id, limit, delay=10): return self.wait_for_ready(instance_id, limit, delay=delay, pending=True) - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) def wait_for_ready(self, instance_id, limit=3600, delay=10, pending=False): """Determine if a VS is ready and available. @@ -450,18 +449,20 @@ def wait_for_ready(self, instance_id, limit=3600, delay=10, pending=False): """ now = time.time() until = now + limit - mask = "mask[id, lastOperatingSystemReload[id], activeTransaction[id, keyName], provisionDate]" - instance = self.get_instance(instance_id, mask=mask) - while now < until and not utils.is_ready(instance, pending): + mask = "mask[id, lastOperatingSystemReload[id], activeTransaction, provisionDate]" + + while now <= until: + instance = self.get_instance(instance_id, mask=mask) + if utils.is_ready(instance, pending): + return True + transaction = utils.lookup(instance, 'activeTransaction', 'transactionStatus', 'friendlyName') snooze = min(delay, until - now) - LOGGER.info("%d not ready. Auto retry in %ds", instance_id, snooze) + LOGGER.info("%s - %d not ready. Auto retry in %ds", transaction, instance_id, snooze) time.sleep(snooze) - instance = self.get_instance(instance_id, mask=mask) now = time.time() - if now >= until: - LOGGER.info("Waiting for %d expired.", instance_id) - return False - return True + + LOGGER.info("Waiting for %d expired.", instance_id) + return False def verify_create_instance(self, **kwargs): """Verifies an instance creation command. @@ -556,7 +557,7 @@ def create_instance(self, **kwargs): self.set_tags(tags, guest_id=inst['id']) return inst - @retry(exceptions.SoftLayerAPIError, logger=LOGGER) + @retry(logger=LOGGER) def set_tags(self, tags, guest_id): """Sets tags on a guest with a retry decorator diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 32bda03e7..5b79e3fa4 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -149,7 +149,7 @@ def __call__(self, request): verify = self.verify LOGGER.debug("=== REQUEST ===") - LOGGER.info('POST %s', url) + LOGGER.debug('POST %s', url) LOGGER.debug(request.transport_headers) LOGGER.debug(payload) @@ -271,7 +271,7 @@ def __call__(self, request): verify = self.verify LOGGER.debug("=== REQUEST ===") - LOGGER.info(url) + LOGGER.debug(url) LOGGER.debug(request.transport_headers) LOGGER.debug(raw_body) try: diff --git a/tests/managers/vs_tests.py b/tests/managers/vs_tests.py index fddb49959..3141d96f0 100644 --- a/tests/managers/vs_tests.py +++ b/tests/managers/vs_tests.py @@ -754,9 +754,7 @@ def test_wait_interface(self, ready): def test_active_not_provisioned(self): # active transaction and no provision date should be false - self.guestObject.side_effect = [ - {'activeTransaction': {'id': 1}}, - ] + self.guestObject.return_value = {'activeTransaction': {'id': 1}} value = self.vs.wait_for_ready(1, 0) self.assertFalse(value) @@ -769,11 +767,14 @@ def test_active_and_provisiondate(self): value = self.vs.wait_for_ready(1, 1) self.assertTrue(value) + def test_active_provision_pending(self): # active transaction and provision date # and pending should be false self.guestObject.side_effect = [ - {'activeTransaction': {'id': 1}, 'provisionDate': 'aaa'}, + {'activeTransaction': {'id': 1}}, + {'activeTransaction': {'id': 1}, + 'provisionDate': 'aaa'}, ] value = self.vs.wait_for_ready(1, 0, pending=True) self.assertFalse(value) @@ -781,6 +782,7 @@ def test_active_provision_pending(self): def test_active_reload(self): # actively running reload self.guestObject.side_effect = [ + {'activeTransaction': {'id': 1}}, { 'activeTransaction': {'id': 1}, 'provisionDate': 'aaa', @@ -792,19 +794,19 @@ def test_active_reload(self): def test_reload_no_pending(self): # reload complete, maintance transactions - self.guestObject.side_effect = [ - { + self.guestObject.return_value = { 'activeTransaction': {'id': 2}, 'provisionDate': 'aaa', 'lastOperatingSystemReload': {'id': 1}, - }, - ] + } + value = self.vs.wait_for_ready(1, 1) self.assertTrue(value) def test_reload_pending(self): # reload complete, pending maintance transactions self.guestObject.side_effect = [ + {'activeTransaction': {'id': 1}}, { 'activeTransaction': {'id': 2}, 'provisionDate': 'aaa', @@ -816,22 +818,17 @@ def test_reload_pending(self): @mock.patch('time.sleep') def test_ready_iter_once_incomplete(self, _sleep): - self.guestObject = self.client['Virtual_Guest'].getObject - # no iteration, false - self.guestObject.side_effect = [ - {'activeTransaction': {'id': 1}}, - ] + self.guestObject.return_value = {'activeTransaction': {'id': 1}} value = self.vs.wait_for_ready(1, 0, delay=1) self.assertFalse(value) - self.assertFalse(_sleep.called) + _sleep.assert_has_calls([mock.call(0)]) + @mock.patch('time.sleep') def test_iter_once_complete(self, _sleep): # no iteration, true - self.guestObject.side_effect = [ - {'provisionDate': 'aaa'}, - ] + self.guestObject.return_value = {'provisionDate': 'aaa'} value = self.vs.wait_for_ready(1, 1, delay=1) self.assertTrue(value) self.assertFalse(_sleep.called) @@ -859,15 +856,16 @@ def test_iter_four_complete(self, _sleep): def test_iter_two_incomplete(self, _sleep, _time): # test 2 iterations, with no matches self.guestObject.side_effect = [ + {'activeTransaction': {'id': 1}}, {'activeTransaction': {'id': 1}}, {'activeTransaction': {'id': 1}}, {'provisionDate': 'aaa'} ] # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. - _time.side_effect = [0, 0, 1, 1, 2, 2, 2] + _time.side_effect = [0, 1, 2, 3, 4, 5, 6] value = self.vs.wait_for_ready(1, 2, delay=1) self.assertFalse(value) - _sleep.assert_called_once_with(1) + _sleep.assert_has_calls([mock.call(1), mock.call(0)]) self.guestObject.assert_has_calls([ mock.call(id=1, mask=mock.ANY), mock.call(id=1, mask=mock.ANY), @@ -887,21 +885,21 @@ def test_iter_20_incomplete(self, _sleep, _time): _sleep.assert_has_calls([mock.call(10)]) @mock.patch('SoftLayer.decoration.sleep') - @mock.patch('SoftLayer.managers.vs.VSManager.get_instance') + @mock.patch('SoftLayer.transports.FixtureTransport.__call__') @mock.patch('time.time') @mock.patch('time.sleep') def test_exception_from_api(self, _sleep, _time, _vs, _dsleep): """Tests escalating scale back when an excaption is thrown""" _dsleep.return_value = False - self.guestObject.return_value = {'activeTransaction': {'id': 1}} - _vs.side_effect = [ + + self.guestObject.side_effect = [ exceptions.TransportError(104, "Its broken"), {'activeTransaction': {'id': 1}}, {'provisionDate': 'aaa'} ] # logging calls time.time as of pytest3.3, not sure if there is a better way of getting around that. - _time.side_effect = [0, 0, 0, 0, 2, 2, 2, 6, 6, 6, 14, 14, 14, 20, 20, 20, 100, 100, 100] + _time.side_effect = [0, 1, 2, 3, 4] value = self.vs.wait_for_ready(1, 20, delay=1) - _sleep.assert_has_calls([mock.call(1)]) + _sleep.assert_called_once() _dsleep.assert_called_once() self.assertTrue(value) From b5402dacc26071746a64630b8830608fe3b4c90b Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 5 Jan 2018 14:18:57 -0600 Subject: [PATCH 3/7] final fixes --- tests/managers/vs_tests.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/managers/vs_tests.py b/tests/managers/vs_tests.py index 3141d96f0..d45e631a3 100644 --- a/tests/managers/vs_tests.py +++ b/tests/managers/vs_tests.py @@ -767,7 +767,6 @@ def test_active_and_provisiondate(self): value = self.vs.wait_for_ready(1, 1) self.assertTrue(value) - def test_active_provision_pending(self): # active transaction and provision date # and pending should be false @@ -795,10 +794,10 @@ def test_active_reload(self): def test_reload_no_pending(self): # reload complete, maintance transactions self.guestObject.return_value = { - 'activeTransaction': {'id': 2}, - 'provisionDate': 'aaa', - 'lastOperatingSystemReload': {'id': 1}, - } + 'activeTransaction': {'id': 2}, + 'provisionDate': 'aaa', + 'lastOperatingSystemReload': {'id': 1}, + } value = self.vs.wait_for_ready(1, 1) self.assertTrue(value) @@ -824,7 +823,6 @@ def test_ready_iter_once_incomplete(self, _sleep): self.assertFalse(value) _sleep.assert_has_calls([mock.call(0)]) - @mock.patch('time.sleep') def test_iter_once_complete(self, _sleep): # no iteration, true From 59db25158f07742e693d2300e7e13bb68c47c411 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 5 Jan 2018 14:28:04 -0600 Subject: [PATCH 4/7] actually adding the ready cli command --- SoftLayer/CLI/hardware/ready.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 SoftLayer/CLI/hardware/ready.py diff --git a/SoftLayer/CLI/hardware/ready.py b/SoftLayer/CLI/hardware/ready.py new file mode 100644 index 000000000..ec2cf9940 --- /dev/null +++ b/SoftLayer/CLI/hardware/ready.py @@ -0,0 +1,25 @@ +"""Check if a virtual server is ready.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@click.option('--wait', default=0, show_default=True, type=click.INT, help="Seconds to wait") +@environment.pass_env +def cli(env, identifier, wait): + """Check if a virtual server is ready.""" + + compute = SoftLayer.HardwareManager(env.client) + compute_id = helpers.resolve_id(compute.resolve_ids, identifier, 'hardware') + ready = compute.wait_for_ready(compute_id, wait) + if ready: + env.fout("READY") + else: + raise exceptions.CLIAbort("Instance %s not ready" % compute_id) From 3d413be31788c97728751fcc3c39297aed1b9b0a Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 5 Jan 2018 15:41:39 -0600 Subject: [PATCH 5/7] more test coverage for t he utils classes --- tests/CLI/core_tests.py | 3 +-- tests/basic_tests.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/CLI/core_tests.py b/tests/CLI/core_tests.py index 534fcccca..bf5879c45 100644 --- a/tests/CLI/core_tests.py +++ b/tests/CLI/core_tests.py @@ -19,8 +19,7 @@ class CoreTests(testing.TestCase): def test_load_all(self): - for path, cmd in recursive_subcommand_loader(core.cli, - current_path='root'): + for path, cmd in recursive_subcommand_loader(core.cli, current_path='root'): try: cmd.main(args=['--help']) except SystemExit as ex: diff --git a/tests/basic_tests.py b/tests/basic_tests.py index cfaf6e259..b430a3d5e 100644 --- a/tests/basic_tests.py +++ b/tests/basic_tests.py @@ -5,6 +5,7 @@ :license: MIT, see LICENSE for more details. """ +import datetime import SoftLayer from SoftLayer import testing @@ -59,6 +60,25 @@ def test_query_filter(self): result = SoftLayer.utils.query_filter(10) self.assertEqual({'operation': 10}, result) + def test_query_filter_date(self): + result = SoftLayer.utils.query_filter_date("2018-01-01", "2018-01-02") + expected = { + 'operation': 'betweenDate', + 'options': [ + {'name': 'startDate', 'value': ['1/1/2018 0:0:0']}, + {'name': 'endDate', 'value': ['1/2/2018 0:0:0']} + ] + } + self.assertEqual(expected, result) + + def test_timezone(self): + utc = SoftLayer.utils.UTC() + time = datetime.datetime(2018, 1, 1, tzinfo=utc) + self.assertEqual('2018-01-01 00:00:00+00:00', time.__str__()) + self.assertEqual('UTC', time.tzname()) + self.assertEqual(datetime.timedelta(0), time.dst()) + self.assertEqual(datetime.timedelta(0), time.utcoffset()) + class TestNestedDict(testing.TestCase): From 8bebfcc905d82fffbd3c7e469c15a90f08961bec Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 5 Jan 2018 16:10:19 -0600 Subject: [PATCH 6/7] transport testing to 100 --- SoftLayer/transports.py | 6 +-- tests/transport_tests.py | 85 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 5b79e3fa4..da6b2da03 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -339,13 +339,11 @@ def __call__(self, call): module_path = 'SoftLayer.fixtures.%s' % call.service module = importlib.import_module(module_path) except ImportError: - raise NotImplementedError('%s fixture is not implemented' - % call.service) + raise NotImplementedError('%s fixture is not implemented' % call.service) try: return getattr(module, call.method) except AttributeError: - raise NotImplementedError('%s::%s fixture is not implemented' - % (call.service, call.method)) + raise NotImplementedError('%s::%s fixture is not implemented' % (call.service, call.method)) def _proxies_dict(proxy): diff --git a/tests/transport_tests.py b/tests/transport_tests.py index 690882f69..724a136ac 100644 --- a/tests/transport_tests.py +++ b/tests/transport_tests.py @@ -510,6 +510,32 @@ def test_with_mask(self, request): proxies=None, timeout=None) + @mock.patch('requests.request') + def test_with_limit_offset(self, request): + request().text = '{}' + + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'getObject' + req.identifier = 2 + req.limit = 10 + req.offset = 5 + + resp = self.transport(req) + + self.assertEqual(resp, {}) + request.assert_called_with( + 'GET', + 'http://something.com/SoftLayer_Service/2/getObject.json', + headers=mock.ANY, + auth=None, + data=None, + params={'limit': 10, 'offset': 5}, + verify=True, + cert=None, + proxies=None, + timeout=None) + @mock.patch('requests.request') def test_unknown_error(self, request): e = requests.RequestException('error') @@ -523,3 +549,62 @@ def test_unknown_error(self, request): req.method = 'getObject' self.assertRaises(SoftLayer.TransportError, self.transport, req) + + + @mock.patch('requests.request') + @mock.patch('requests.auth.HTTPBasicAuth') + def test_with_special_auth(self, auth, request): + request().text = '{}' + + user = 'asdf' + password = 'zxcv' + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'getObject' + req.identifier = 2 + req.transport_user = user + req.transport_password = password + + + resp = self.transport(req) + auth.assert_called_with(user, password) + request.assert_called_with( + 'GET', + 'http://something.com/SoftLayer_Service/2/getObject.json', + headers=mock.ANY, + auth=mock.ANY, + data=None, + params={}, + verify=True, + cert=None, + proxies=None, + timeout=None) + + +class TestFixtureTransport(testing.TestCase): + + def set_up(self): + self.transport = transports.FixtureTransport() + + def test_basic(self): + + req = transports.Request() + req.service = 'SoftLayer_Account' + req.method = 'getObject' + resp = self.transport(req) + + self.assertEqual(resp['cdnAccounts'][0]['accountId'], 1234) + + def test_no_module(self): + + req = transports.Request() + req.service = 'Doesnt_Exist' + req.method = 'getObject' + self.assertRaises(NotImplementedError, self.transport, req) + + def test_no_method(self): + + req = transports.Request() + req.service = 'SoftLayer_Account' + req.method = 'getObjectzzzz' + self.assertRaises(NotImplementedError, self.transport, req) From ed1802325fb684a345aa677e209de720809b593d Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Fri, 5 Jan 2018 16:55:34 -0600 Subject: [PATCH 7/7] ready vs and hw tests added --- tests/CLI/modules/server_tests.py | 47 +++++++++++++++++++++++++++++++ tests/CLI/modules/vs_tests.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index 083798bd5..5f8c85d31 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -462,3 +462,50 @@ def test_server_rescue_negative(self, confirm_mock): self.assertEqual(result.exit_code, 2) self.assertIsInstance(result.exception, exceptions.CLIAbort) + + def test_ready(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = { + "provisionDate": "2017-10-17T11:21:53-07:00", + "id": 41957081 + } + result = self.run_command(['hw', 'ready', '100']) + self.assert_no_fail(result) + self.assertEqual(result.output, '"READY"\n') + + def test_not_ready(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + not_ready = { + 'activeTransaction': { + 'transactionStatus': {'friendlyName': 'Attach Primary Disk'} + }, + 'provisionDate': '', + 'id': 47392219 + } + ready = { + "provisionDate": "2017-10-17T11:21:53-07:00", + "id": 41957081 + } + mock.side_effect = [not_ready, ready] + result = self.run_command(['hw', 'ready', '100']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + @mock.patch('time.sleep') + def test_going_ready(self, _sleep): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + not_ready = { + 'activeTransaction': { + 'transactionStatus': {'friendlyName': 'Attach Primary Disk'} + }, + 'provisionDate': '', + 'id': 47392219 + } + ready = { + "provisionDate": "2017-10-17T11:21:53-07:00", + "id": 41957081 + } + mock.side_effect = [not_ready, ready] + result = self.run_command(['hw', 'ready', '100', '--wait=100']) + self.assert_no_fail(result) + self.assertEqual(result.output, '"READY"\n') diff --git a/tests/CLI/modules/vs_tests.py b/tests/CLI/modules/vs_tests.py index dced520e0..e9b8cf46c 100644 --- a/tests/CLI/modules/vs_tests.py +++ b/tests/CLI/modules/vs_tests.py @@ -606,3 +606,50 @@ def test_edit(self): args=(100,), identifier=100, ) + + def test_ready(self): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + mock.return_value = { + "provisionDate": "2017-10-17T11:21:53-07:00", + "id": 41957081 + } + result = self.run_command(['vs', 'ready', '100']) + self.assert_no_fail(result) + self.assertEqual(result.output, '"READY"\n') + + def test_not_ready(self): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + not_ready = { + 'activeTransaction': { + 'transactionStatus': {'friendlyName': 'Attach Primary Disk'} + }, + 'provisionDate': '', + 'id': 47392219 + } + ready = { + "provisionDate": "2017-10-17T11:21:53-07:00", + "id": 41957081 + } + mock.side_effect = [not_ready, ready] + result = self.run_command(['vs', 'ready', '100']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) + + @mock.patch('time.sleep') + def test_going_ready(self, _sleep): + mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject') + not_ready = { + 'activeTransaction': { + 'transactionStatus': {'friendlyName': 'Attach Primary Disk'} + }, + 'provisionDate': '', + 'id': 47392219 + } + ready = { + "provisionDate": "2017-10-17T11:21:53-07:00", + "id": 41957081 + } + mock.side_effect = [not_ready, ready] + result = self.run_command(['vs', 'ready', '100', '--wait=100']) + self.assert_no_fail(result) + self.assertEqual(result.output, '"READY"\n')