Skip to content

Commit

Permalink
Merge pull request #957 from allmightyspiff/issues914
Browse files Browse the repository at this point in the history
added a way to try to cancel monthly bare metal immediately
  • Loading branch information
allmightyspiff authored Mar 29, 2018
2 parents 6007e39 + 4ca237a commit 45c6d6e
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 36 deletions.
6 changes: 2 additions & 4 deletions SoftLayer/CLI/hardware/cancel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
45 changes: 32 additions & 13 deletions SoftLayer/managers/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import socket
import time


import SoftLayer
from SoftLayer.decoration import retry
from SoftLayer.managers import ordering
Expand Down Expand Up @@ -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::
Expand All @@ -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,
Expand Down
65 changes: 46 additions & 19 deletions tests/managers/hardware_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import mock


import SoftLayer
from SoftLayer import fixtures
from SoftLayer import managers
Expand Down Expand Up @@ -246,51 +247,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)
Expand Down

0 comments on commit 45c6d6e

Please sign in to comment.