Skip to content

Commit c897382

Browse files
Merge pull request #1437 from FernandoOjeda/ft/upgrade_hardware
Add upgrade option to slcli hw.
2 parents 197c675 + ac8a4b4 commit c897382

File tree

7 files changed

+341
-1
lines changed

7 files changed

+341
-1
lines changed

SoftLayer/CLI/hardware/upgrade.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Upgrade a hardware server."""
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import click
5+
6+
import SoftLayer
7+
from SoftLayer.CLI import environment
8+
from SoftLayer.CLI import exceptions
9+
from SoftLayer.CLI import formatting
10+
from SoftLayer.CLI import helpers
11+
12+
13+
@click.command()
14+
@click.argument('identifier')
15+
@click.option('--memory', type=click.INT, help="Memory Size in GB")
16+
@click.option('--network', help="Network port speed in Mbps",
17+
default=None,
18+
type=click.Choice(['100', '100 Redundant', '100 Dual',
19+
'1000', '1000 Redundant', '1000 Dual',
20+
'10000', '10000 Redundant', '10000 Dual'])
21+
)
22+
@click.option('--drive-controller',
23+
help="Drive Controller",
24+
default=None,
25+
type=click.Choice(['Non-RAID', 'RAID']))
26+
@click.option('--public-bandwidth', type=click.INT, help="Public Bandwidth in GB")
27+
@click.option('--test', is_flag=True, default=False, help="Do not actually upgrade the hardware server")
28+
@environment.pass_env
29+
def cli(env, identifier, memory, network, drive_controller, public_bandwidth, test):
30+
"""Upgrade a Hardware Server."""
31+
32+
mgr = SoftLayer.HardwareManager(env.client)
33+
34+
if not any([memory, network, drive_controller, public_bandwidth]):
35+
raise exceptions.ArgumentError("Must provide "
36+
" [--memory], [--network], [--drive-controller], or [--public-bandwidth]")
37+
38+
hw_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'Hardware')
39+
if not test:
40+
if not (env.skip_confirmations or formatting.confirm(
41+
"This action will incur charges on your account. Continue?")):
42+
raise exceptions.CLIAbort('Aborted')
43+
44+
if not mgr.upgrade(hw_id, memory=memory, nic_speed=network, drive_controller=drive_controller,
45+
public_bandwidth=public_bandwidth, test=test):
46+
raise exceptions.CLIAbort('Hardware Server Upgrade Failed')
47+
env.fout('Successfully Upgraded.')

SoftLayer/CLI/routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@
258258
('hardware:toggle-ipmi', 'SoftLayer.CLI.hardware.toggle_ipmi:cli'),
259259
('hardware:dns-sync', 'SoftLayer.CLI.hardware.dns:cli'),
260260
('hardware:storage', 'SoftLayer.CLI.hardware.storage:cli'),
261+
('hardware:upgrade', 'SoftLayer.CLI.hardware.upgrade:cli'),
261262

262263
('securitygroup', 'SoftLayer.CLI.securitygroup'),
263264
('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'),

SoftLayer/fixtures/SoftLayer_Hardware_Server.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
'billingItem': {
77
'id': 6327,
88
'recurringFee': 1.54,
9+
'package': {
10+
'id': 911
11+
},
912
'nextInvoiceTotalRecurringAmount': 16.08,
1013
'children': [
1114
{'description': 'test', 'nextInvoiceTotalRecurringAmount': 1},
@@ -262,3 +265,76 @@
262265
}
263266
}
264267
}
268+
269+
getUpgradeItemPrices = [
270+
{
271+
"id": 21525,
272+
"recurringFee": "0",
273+
"categories": [
274+
{
275+
"categoryCode": "port_speed",
276+
"id": 26,
277+
"name": "Uplink Port Speeds",
278+
}
279+
],
280+
"item": {
281+
"capacity": "10000",
282+
"description": "10 Gbps Redundant Public & Private Network Uplinks",
283+
"id": 4342,
284+
"keyName": "10_GBPS_REDUNDANT_PUBLIC_PRIVATE_NETWORK_UPLINKS"
285+
}
286+
},
287+
{
288+
"hourlyRecurringFee": ".247",
289+
"id": 209391,
290+
"recurringFee": "164",
291+
"categories": [
292+
{
293+
"categoryCode": "ram",
294+
"id": 3,
295+
"name": "RAM"
296+
}
297+
],
298+
"item": {
299+
"capacity": "32",
300+
"description": "32 GB RAM",
301+
"id": 11291,
302+
"keyName": "RAM_32_GB_DDR4_2133_ECC_NON_REG"
303+
}
304+
},
305+
{
306+
"hourlyRecurringFee": ".068",
307+
"id": 22482,
308+
"recurringFee": "50",
309+
"categories": [
310+
{
311+
"categoryCode": "disk_controller",
312+
"id": 11,
313+
"name": "Disk Controller",
314+
}
315+
],
316+
"item": {
317+
"capacity": "0",
318+
"description": "RAID",
319+
"id": 4478,
320+
"keyName": "DISK_CONTROLLER_RAID",
321+
}
322+
},
323+
{
324+
"id": 50357,
325+
"recurringFee": "0",
326+
"categories": [
327+
{
328+
"categoryCode": "bandwidth",
329+
"id": 10,
330+
"name": "Public Bandwidth",
331+
}
332+
],
333+
"item": {
334+
"capacity": "500",
335+
"description": "500 GB Bandwidth Allotment",
336+
"id": 6177,
337+
"keyName": "BANDWIDTH_500_GB"
338+
}
339+
}
340+
]

SoftLayer/managers/hardware.py

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
66
:license: MIT, see LICENSE for more details.
77
"""
8+
import datetime
89
import logging
910
import socket
1011
import time
1112

1213
from SoftLayer.decoration import retry
14+
from SoftLayer import exceptions
1315
from SoftLayer.exceptions import SoftLayerError
1416
from SoftLayer.managers import ordering
1517
from SoftLayer.managers.ticket import TicketManager
@@ -18,7 +20,7 @@
1820
LOGGER = logging.getLogger(__name__)
1921

2022
# Invalid names are ignored due to long method names and short argument names
21-
# pylint: disable=invalid-name, no-self-use
23+
# pylint: disable=invalid-name, no-self-use, too-many-lines
2224

2325
EXTRA_CATEGORIES = ['pri_ipv6_addresses',
2426
'static_ipv6_addresses',
@@ -786,6 +788,146 @@ def get_hardware_item_prices(self, location):
786788
return self.client.call('SoftLayer_Product_Package', 'getItemPrices', mask=object_mask, filter=object_filter,
787789
id=package['id'])
788790

791+
def upgrade(self, instance_id, memory=None,
792+
nic_speed=None, drive_controller=None,
793+
public_bandwidth=None, test=False):
794+
"""Upgrades a hardware server instance.
795+
796+
:param int instance_id: Instance id of the hardware server to be upgraded.
797+
:param int memory: Memory size.
798+
:param string nic_speed: Network Port Speed data.
799+
:param string drive_controller: Drive Controller data.
800+
:param int public_bandwidth: Public keyName data.
801+
:param bool test: Test option to verify the request.
802+
803+
:returns: bool
804+
"""
805+
upgrade_prices = self._get_upgrade_prices(instance_id)
806+
prices = []
807+
data = {}
808+
809+
if memory:
810+
data['memory'] = memory
811+
if nic_speed:
812+
data['nic_speed'] = nic_speed
813+
if drive_controller:
814+
data['disk_controller'] = drive_controller
815+
if public_bandwidth:
816+
data['bandwidth'] = public_bandwidth
817+
818+
server_response = self.get_instance(instance_id)
819+
package_id = server_response['billingItem']['package']['id']
820+
821+
maintenance_window = datetime.datetime.now(utils.UTC())
822+
order = {
823+
'complexType': 'SoftLayer_Container_Product_Order_Hardware_Server_Upgrade',
824+
'properties': [{
825+
'name': 'MAINTENANCE_WINDOW',
826+
'value': maintenance_window.strftime("%Y-%m-%d %H:%M:%S%z")
827+
}],
828+
'hardware': [{'id': int(instance_id)}],
829+
'packageId': package_id
830+
}
831+
832+
for option, value in data.items():
833+
price_id = self._get_prices_for_upgrade_option(upgrade_prices, option, value)
834+
if not price_id:
835+
# Every option provided is expected to have a price
836+
raise exceptions.SoftLayerError(
837+
"Unable to find %s option with value %s" % (option, value))
838+
839+
prices.append({'id': price_id})
840+
841+
order['prices'] = prices
842+
843+
if prices:
844+
if test:
845+
self.client['Product_Order'].verifyOrder(order)
846+
else:
847+
self.client['Product_Order'].placeOrder(order)
848+
return True
849+
return False
850+
851+
@retry(logger=LOGGER)
852+
def get_instance(self, instance_id):
853+
"""Get details about a hardware server instance.
854+
855+
:param int instance_id: the instance ID
856+
:returns: A dictionary containing a large amount of information about
857+
the specified instance.
858+
"""
859+
mask = [
860+
'billingItem[id,package[id,keyName]]'
861+
]
862+
mask = "mask[%s]" % ','.join(mask)
863+
864+
return self.hardware.getObject(id=instance_id, mask=mask)
865+
866+
def _get_upgrade_prices(self, instance_id, include_downgrade_options=True):
867+
"""Following Method gets all the price ids related to upgrading a Hardware Server.
868+
869+
:param int instance_id: Instance id of the Hardware Server to be upgraded.
870+
871+
:returns: list
872+
"""
873+
mask = [
874+
'id',
875+
'locationGroupId',
876+
'categories[name,id,categoryCode]',
877+
'item[keyName,description,capacity,units]'
878+
]
879+
mask = "mask[%s]" % ','.join(mask)
880+
return self.hardware.getUpgradeItemPrices(include_downgrade_options, id=instance_id, mask=mask)
881+
882+
@staticmethod
883+
def _get_prices_for_upgrade_option(upgrade_prices, option, value):
884+
"""Find the price id for the option and value to upgrade. This
885+
886+
:param list upgrade_prices: Contains all the prices related to a
887+
hardware server upgrade.
888+
:param string option: Describes type of parameter to be upgraded
889+
890+
:return: int.
891+
"""
892+
price_id = None
893+
option_category = {
894+
'memory': 'ram',
895+
'nic_speed': 'port_speed',
896+
'disk_controller': 'disk_controller',
897+
'bandwidth': 'bandwidth'
898+
}
899+
category_code = option_category.get(option)
900+
901+
for price in upgrade_prices:
902+
if price.get('categories') is None or price.get('item') is None:
903+
continue
904+
905+
product = price.get('item')
906+
for category in price.get('categories'):
907+
if not category.get('categoryCode') == category_code:
908+
continue
909+
910+
if option == 'disk_controller':
911+
if value == product.get('description'):
912+
price_id = price.get('id')
913+
elif option == 'nic_speed':
914+
if value.isdigit():
915+
if str(product.get('capacity')) == str(value):
916+
price_id = price.get('id')
917+
else:
918+
split_nic_speed = value.split(" ")
919+
if str(product.get('capacity')) == str(split_nic_speed[0]) and \
920+
split_nic_speed[1] in product.get("description"):
921+
price_id = price.get('id')
922+
elif option == 'bandwidth':
923+
if str(product.get('capacity')) == str(value):
924+
price_id = price.get('id')
925+
else:
926+
if str(product.get('capacity')) == str(value):
927+
price_id = price.get('id')
928+
929+
return price_id
930+
789931

790932
def _get_bandwidth_key(items, hourly=True, no_public=False, location=None):
791933
"""Picks a valid Bandwidth Item, returns the KeyName"""

docs/cli/hardware.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,7 @@ This function updates the firmware of a server. If already at the latest version
119119
.. click:: SoftLayer.CLI.hardware.guests:cli
120120
:prog: hardware guests
121121
:show-nested:
122+
123+
.. click:: SoftLayer.CLI.hardware.upgrade:cli
124+
:prog: hardware upgrade
125+
:show-nested:

tests/CLI/modules/server_tests.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,3 +903,29 @@ def test_hardware_guests_empty(self):
903903
result = self.run_command(['hw', 'guests', '123456'])
904904
self.assertEqual(result.exit_code, 2)
905905
self.assertIsInstance(result.exception, exceptions.CLIAbort)
906+
907+
def test_upgrade_no_options(self, ):
908+
result = self.run_command(['hw', 'upgrade', '100'])
909+
self.assertEqual(result.exit_code, 2)
910+
self.assertIsInstance(result.exception, exceptions.ArgumentError)
911+
912+
@mock.patch('SoftLayer.CLI.formatting.confirm')
913+
def test_upgrade_aborted(self, confirm_mock):
914+
confirm_mock.return_value = False
915+
result = self.run_command(['hw', 'upgrade', '100', '--memory=1'])
916+
self.assertEqual(result.exit_code, 2)
917+
self.assertIsInstance(result.exception, exceptions.CLIAbort)
918+
919+
@mock.patch('SoftLayer.CLI.formatting.confirm')
920+
def test_upgrade_test(self, confirm_mock):
921+
confirm_mock.return_value = True
922+
result = self.run_command(['hw', 'upgrade', '100', '--test', '--memory=32', '--public-bandwidth=500',
923+
'--drive-controller=RAID', '--network=10000 Redundant'])
924+
self.assert_no_fail(result)
925+
926+
@mock.patch('SoftLayer.CLI.formatting.confirm')
927+
def test_upgrade(self, confirm_mock):
928+
confirm_mock.return_value = True
929+
result = self.run_command(['hw', 'upgrade', '100', '--memory=32', '--public-bandwidth=500',
930+
'--drive-controller=RAID', '--network=10000 Redundant'])
931+
self.assert_no_fail(result)

0 commit comments

Comments
 (0)