Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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)
Expand Down