From 4ca237aba4957e6bbf300fc4f79ebbb74df80a9d Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 27 Mar 2018 16:55:39 -0500 Subject: [PATCH] added a way to try to cancel montly bare metal immediately --- SoftLayer/CLI/hardware/cancel.py | 6 +-- SoftLayer/managers/hardware.py | 45 +++++++++++++++------- tests/managers/hardware_tests.py | 65 ++++++++++++++++++++++---------- 3 files changed, 80 insertions(+), 36 deletions(-) diff --git a/SoftLayer/CLI/hardware/cancel.py b/SoftLayer/CLI/hardware/cancel.py index 0b6650fc3..b56179b02 100644 --- a/SoftLayer/CLI/hardware/cancel.py +++ b/SoftLayer/CLI/hardware/cancel.py @@ -15,13 +15,11 @@ @click.option('--immediate', is_flag=True, default=False, - help="""Cancels the server immediately (instead of on the billing - anniversary)""") + help="Cancels the server immediately (instead of on the billing anniversary)") @click.option('--comment', help="An optional comment to add to the cancellation ticket") @click.option('--reason', - help="""An optional cancellation reason. See cancel-reasons for - a list of available options""") + help="An optional cancellation reason. See cancel-reasons for a list of available options") @environment.pass_env def cli(env, identifier, immediate, comment, reason): """Cancel a dedicated server.""" diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 02f67f746..b6b922b7a 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -9,6 +9,7 @@ import socket import time + import SoftLayer from SoftLayer.decoration import retry from SoftLayer.managers import ordering @@ -56,8 +57,7 @@ def __init__(self, client, ordering_manager=None): else: self.ordering_manager = ordering_manager - def cancel_hardware(self, hardware_id, reason='unneeded', comment='', - immediate=False): + def cancel_hardware(self, hardware_id, reason='unneeded', comment='', immediate=False): """Cancels the specified dedicated server. Example:: @@ -66,27 +66,46 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', result = mgr.cancel_hardware(hardware_id=1234) :param int hardware_id: The ID of the hardware to be cancelled. - :param string reason: The reason code for the cancellation. This should - come from :func:`get_cancellation_reasons`. - :param string comment: An optional comment to include with the - cancellation. + :param string reason: The reason code for the cancellation. This should come from + :func:`get_cancellation_reasons`. + :param string comment: An optional comment to include with the cancellation. + :param bool immediate: If set to True, will automatically update the cancelation ticket to request + the resource be reclaimed asap. This request still has to be reviewed by a human + :returns: True on success or an exception """ # Get cancel reason reasons = self.get_cancellation_reasons() cancel_reason = reasons.get(reason, reasons['unneeded']) + ticket_mgr = SoftLayer.TicketManager(self.client) + mask = 'mask[id, hourlyBillingFlag, billingItem[id], openCancellationTicket[id]]' + hw_billing = self.get_hardware(hardware_id, mask=mask) - hw_billing = self.get_hardware(hardware_id, - mask='mask[id, billingItem.id]') if 'billingItem' not in hw_billing: - raise SoftLayer.SoftLayerError( - "No billing item found for hardware") + raise SoftLayer.SoftLayerError("Ticket #%s already exists for this server" % + hw_billing['openCancellationTicket']['id']) billing_id = hw_billing['billingItem']['id'] - return self.client.call('Billing_Item', 'cancelItem', - immediate, False, cancel_reason, comment, - id=billing_id) + if immediate and not hw_billing['hourlyBillingFlag']: + LOGGER.warning("Immediate cancelation of montly servers is not guaranteed. " + + "Please check the cancelation ticket for updates.") + + result = self.client.call('Billing_Item', 'cancelItem', + False, False, cancel_reason, comment, id=billing_id) + hw_billing = self.get_hardware(hardware_id, mask=mask) + ticket_number = hw_billing['openCancellationTicket']['id'] + cancel_message = "Please reclaim this server ASAP, it is no longer needed. Thankyou." + ticket_mgr.update_ticket(ticket_number, cancel_message) + LOGGER.info("Cancelation ticket #%s has been updated requesting immediate reclaim", ticket_number) + else: + result = self.client.call('Billing_Item', 'cancelItem', + immediate, False, cancel_reason, comment, id=billing_id) + hw_billing = self.get_hardware(hardware_id, mask=mask) + ticket_number = hw_billing['openCancellationTicket']['id'] + LOGGER.info("Cancelation ticket #%s has been created", ticket_number) + + return result @retry(logger=LOGGER) def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None, diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index d9d432be8..2206b1fa9 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -8,6 +8,7 @@ import mock + import SoftLayer from SoftLayer import fixtures from SoftLayer import managers @@ -240,51 +241,77 @@ def test_place_order(self, create_dict): def test_cancel_hardware_without_reason(self): mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') - mock.return_value = {'id': 987, 'billingItem': {'id': 1234}} + mock.return_value = {'id': 987, 'billingItem': {'id': 1234}, + 'openCancellationTicket': {'id': 1234}} result = self.hardware.cancel_hardware(987) self.assertEqual(result, True) reasons = self.hardware.get_cancellation_reasons() args = (False, False, reasons['unneeded'], '') - self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', - identifier=1234, - args=args) + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', identifier=1234, args=args) def test_cancel_hardware_with_reason_and_comment(self): mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') - mock.return_value = {'id': 987, 'billingItem': {'id': 1234}} + mock.return_value = {'id': 987, 'billingItem': {'id': 1234}, + 'openCancellationTicket': {'id': 1234}} - result = self.hardware.cancel_hardware(6327, - reason='sales', - comment='Test Comment') + result = self.hardware.cancel_hardware(6327, reason='sales', comment='Test Comment') self.assertEqual(result, True) reasons = self.hardware.get_cancellation_reasons() args = (False, False, reasons['sales'], 'Test Comment') - self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', - identifier=1234, - args=args) + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', identifier=1234, args=args) def test_cancel_hardware(self): - + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = {'id': 987, 'billingItem': {'id': 6327}, + 'openCancellationTicket': {'id': 4567}} result = self.hardware.cancel_hardware(6327) self.assertEqual(result, True) - self.assert_called_with('SoftLayer_Billing_Item', - 'cancelItem', - identifier=6327, - args=(False, False, 'No longer needed', '')) + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', + identifier=6327, args=(False, False, 'No longer needed', '')) def test_cancel_hardware_no_billing_item(self): mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') - mock.return_value = {'id': 987} + mock.return_value = {'id': 987, 'openCancellationTicket': {'id': 1234}, + 'openCancellationTicket': {'id': 1234}} ex = self.assertRaises(SoftLayer.SoftLayerError, self.hardware.cancel_hardware, 6327) - self.assertEqual("No billing item found for hardware", - str(ex)) + self.assertEqual("Ticket #1234 already exists for this server", str(ex)) + + def test_cancel_hardware_monthly_now(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = {'id': 987, 'billingItem': {'id': 1234}, + 'openCancellationTicket': {'id': 4567}, + 'hourlyBillingFlag': False} + with self.assertLogs('SoftLayer.managers.hardware', level='INFO') as logs: + result = self.hardware.cancel_hardware(987, immediate=True) + # should be 2 infom essages here + self.assertEqual(len(logs.records), 2) + + self.assertEqual(result, True) + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', + identifier=1234, args=(False, False, 'No longer needed', '')) + cancel_message = "Please reclaim this server ASAP, it is no longer needed. Thankyou." + self.assert_called_with('SoftLayer_Ticket', 'addUpdate', + identifier=4567, args=({'entry': cancel_message},)) + + def test_cancel_hardware_monthly_whenever(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') + mock.return_value = {'id': 987, 'billingItem': {'id': 6327}, + 'openCancellationTicket': {'id': 4567}} + + with self.assertLogs('SoftLayer.managers.hardware', level='INFO') as logs: + result = self.hardware.cancel_hardware(987, immediate=False) + # should be 2 infom essages here + self.assertEqual(len(logs.records), 1) + self.assertEqual(result, True) + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', + identifier=6327, args=(False, False, 'No longer needed', '')) def test_change_port_speed_public(self): self.hardware.change_port_speed(2, True, 100)