diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py index 70a41c0c0..ecfd5c4d5 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -62,12 +62,10 @@ def cli(env, volume_id): if block_volume['activeTransactions']: for trans in block_volume['activeTransactions']: - table.add_row([ - 'Ongoing Transactions', - trans['transactionStatus']['friendlyName']]) + if 'transactionStatus' in trans and 'friendlyName' in trans['transactionStatus']: + table.add_row(['Ongoing Transaction', trans['transactionStatus']['friendlyName']]) - table.add_row(['Replicant Count', "%u" - % block_volume['replicationPartnerCount']]) + table.add_row(['Replicant Count', "%u" % block_volume.get('replicationPartnerCount', 0)]) if block_volume['replicationPartnerCount'] > 0: # This if/else temporarily handles a bug in which the SL API @@ -102,12 +100,12 @@ def cli(env, volume_id): table.add_row(['Replicant Volumes', replicant_list]) if block_volume.get('originalVolumeSize'): - duplicate_info = formatting.Table(['Original Volume Name', - block_volume['originalVolumeName']]) - duplicate_info.add_row(['Original Volume Size', - block_volume['originalVolumeSize']]) - duplicate_info.add_row(['Original Snapshot Name', - block_volume['originalSnapshotName']]) - table.add_row(['Duplicate Volume Properties', duplicate_info]) + original_volume_info = formatting.Table(['Property', 'Value']) + original_volume_info.add_row(['Original Volume Size', block_volume['originalVolumeSize']]) + if block_volume.get('originalVolumeName'): + original_volume_info.add_row(['Original Volume Name', block_volume['originalVolumeName']]) + if block_volume.get('originalSnapshotName'): + original_volume_info.add_row(['Original Snapshot Name', block_volume['originalSnapshotName']]) + table.add_row(['Original Volume Properties', original_volume_info]) env.fout(table) diff --git a/SoftLayer/CLI/block/duplicate.py b/SoftLayer/CLI/block/duplicate.py index 0ecf59110..ec728f87c 100644 --- a/SoftLayer/CLI/block/duplicate.py +++ b/SoftLayer/CLI/block/duplicate.py @@ -22,9 +22,7 @@ 'the origin volume will be used.***\n' 'Potential Sizes: [20, 40, 80, 100, 250, ' '500, 1000, 2000, 4000, 8000, 12000] ' - 'Minimum: [the size of the origin volume] ' - 'Maximum: [the minimum of 12000 GB or ' - '10*(origin volume size)]') + 'Minimum: [the size of the origin volume]') @click.option('--duplicate-iops', '-i', type=int, help='Performance Storage IOPS, between 100 and 6000 in ' diff --git a/SoftLayer/CLI/block/modify.py b/SoftLayer/CLI/block/modify.py new file mode 100644 index 000000000..3697ddd79 --- /dev/null +++ b/SoftLayer/CLI/block/modify.py @@ -0,0 +1,57 @@ +"""Modify an existing block storage volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions + + +CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()} + + +@click.command(context_settings=CONTEXT_SETTINGS) +@click.argument('volume-id') +@click.option('--new-size', '-c', + type=int, + help='New Size of block volume in GB. ***If no size is given, the original size of volume is used.***\n' + 'Potential Sizes: [20, 40, 80, 100, 250, 500, 1000, 2000, 4000, 8000, 12000]\n' + 'Minimum: [the original size of the volume]') +@click.option('--new-iops', '-i', + type=int, + help='Performance Storage IOPS, between 100 and 6000 in multiples of 100 [only for performance volumes] ' + '***If no IOPS value is specified, the original IOPS value of the volume will be used.***\n' + 'Requirements: [If original IOPS/GB for the volume is less than 0.3, new IOPS/GB must also be ' + 'less than 0.3. If original IOPS/GB for the volume is greater than or equal to 0.3, new IOPS/GB ' + 'for the volume must also be greater than or equal to 0.3.]') +@click.option('--new-tier', '-t', + help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] ' + '***If no tier is specified, the original tier of the volume will be used.***\n' + 'Requirements: [If original IOPS/GB for the volume is 0.25, new IOPS/GB for the volume must also ' + 'be 0.25. If original IOPS/GB for the volume is greater than 0.25, new IOPS/GB for the volume ' + 'must also be greater than 0.25.]', + type=click.Choice(['0.25', '2', '4', '10'])) +@environment.pass_env +def cli(env, volume_id, new_size, new_iops, new_tier): + """Modify an existing block storage volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + + if new_tier is not None: + new_tier = float(new_tier) + + try: + order = block_manager.order_modified_volume( + volume_id, + new_size=new_size, + new_iops=new_iops, + new_tier_level=new_tier, + ) + except ValueError as ex: + raise exceptions.ArgumentError(str(ex)) + + if 'placedOrder' in order.keys(): + click.echo("Order #{0} placed successfully!".format(order['placedOrder']['id'])) + for item in order['placedOrder']['items']: + click.echo(" > %s" % item['description']) + else: + click.echo("Order could not be placed! Please verify your options and try again.") diff --git a/SoftLayer/CLI/file/detail.py b/SoftLayer/CLI/file/detail.py index 96437dc36..cb712dc97 100644 --- a/SoftLayer/CLI/file/detail.py +++ b/SoftLayer/CLI/file/detail.py @@ -78,12 +78,10 @@ def cli(env, volume_id): if file_volume['activeTransactions']: for trans in file_volume['activeTransactions']: - table.add_row([ - 'Ongoing Transactions', - trans['transactionStatus']['friendlyName']]) + if 'transactionStatus' in trans and 'friendlyName' in trans['transactionStatus']: + table.add_row(['Ongoing Transaction', trans['transactionStatus']['friendlyName']]) - table.add_row(['Replicant Count', "%u" - % file_volume['replicationPartnerCount']]) + table.add_row(['Replicant Count', "%u" % file_volume.get('replicationPartnerCount', 0)]) if file_volume['replicationPartnerCount'] > 0: # This if/else temporarily handles a bug in which the SL API @@ -118,12 +116,12 @@ def cli(env, volume_id): table.add_row(['Replicant Volumes', replicant_list]) if file_volume.get('originalVolumeSize'): - duplicate_info = formatting.Table(['Original Volume Name', - file_volume['originalVolumeName']]) - duplicate_info.add_row(['Original Volume Size', - file_volume['originalVolumeSize']]) - duplicate_info.add_row(['Original Snapshot Name', - file_volume['originalSnapshotName']]) - table.add_row(['Duplicate Volume Properties', duplicate_info]) + original_volume_info = formatting.Table(['Property', 'Value']) + original_volume_info.add_row(['Original Volume Size', file_volume['originalVolumeSize']]) + if file_volume.get('originalVolumeName'): + original_volume_info.add_row(['Original Volume Name', file_volume['originalVolumeName']]) + if file_volume.get('originalSnapshotName'): + original_volume_info.add_row(['Original Snapshot Name', file_volume['originalSnapshotName']]) + table.add_row(['Original Volume Properties', original_volume_info]) env.fout(table) diff --git a/SoftLayer/CLI/file/modify.py b/SoftLayer/CLI/file/modify.py new file mode 100644 index 000000000..5e0c097b3 --- /dev/null +++ b/SoftLayer/CLI/file/modify.py @@ -0,0 +1,57 @@ +"""Modify an existing file storage volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions + + +CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()} + + +@click.command(context_settings=CONTEXT_SETTINGS) +@click.argument('volume-id') +@click.option('--new-size', '-c', + type=int, + help='New Size of file volume in GB. ***If no size is given, the original size of volume is used.***\n' + 'Potential Sizes: [20, 40, 80, 100, 250, 500, 1000, 2000, 4000, 8000, 12000]\n' + 'Minimum: [the original size of the volume]') +@click.option('--new-iops', '-i', + type=int, + help='Performance Storage IOPS, between 100 and 6000 in multiples of 100 [only for performance volumes] ' + '***If no IOPS value is specified, the original IOPS value of the volume will be used.***\n' + 'Requirements: [If original IOPS/GB for the volume is less than 0.3, new IOPS/GB must also be ' + 'less than 0.3. If original IOPS/GB for the volume is greater than or equal to 0.3, new IOPS/GB ' + 'for the volume must also be greater than or equal to 0.3.]') +@click.option('--new-tier', '-t', + help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] ' + '***If no tier is specified, the original tier of the volume will be used.***\n' + 'Requirements: [If original IOPS/GB for the volume is 0.25, new IOPS/GB for the volume must also ' + 'be 0.25. If original IOPS/GB for the volume is greater than 0.25, new IOPS/GB for the volume ' + 'must also be greater than 0.25.]', + type=click.Choice(['0.25', '2', '4', '10'])) +@environment.pass_env +def cli(env, volume_id, new_size, new_iops, new_tier): + """Modify an existing file storage volume.""" + file_manager = SoftLayer.FileStorageManager(env.client) + + if new_tier is not None: + new_tier = float(new_tier) + + try: + order = file_manager.order_modified_volume( + volume_id, + new_size=new_size, + new_iops=new_iops, + new_tier_level=new_tier, + ) + except ValueError as ex: + raise exceptions.ArgumentError(str(ex)) + + if 'placedOrder' in order.keys(): + click.echo("Order #{0} placed successfully!".format(order['placedOrder']['id'])) + for item in order['placedOrder']['items']: + click.echo(" > %s" % item['description']) + else: + click.echo("Order could not be placed! Please verify your options and try again.") diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 2ff3379ac..3b406ec25 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -80,6 +80,7 @@ ('block:volume-detail', 'SoftLayer.CLI.block.detail:cli'), ('block:volume-duplicate', 'SoftLayer.CLI.block.duplicate:cli'), ('block:volume-list', 'SoftLayer.CLI.block.list:cli'), + ('block:volume-modify', 'SoftLayer.CLI.block.modify:cli'), ('block:volume-order', 'SoftLayer.CLI.block.order:cli'), ('block:volume-set-lun-id', 'SoftLayer.CLI.block.lun:cli'), @@ -105,6 +106,7 @@ ('file:volume-detail', 'SoftLayer.CLI.file.detail:cli'), ('file:volume-duplicate', 'SoftLayer.CLI.file.duplicate:cli'), ('file:volume-list', 'SoftLayer.CLI.file.list:cli'), + ('file:volume-modify', 'SoftLayer.CLI.file.modify:cli'), ('file:volume-order', 'SoftLayer.CLI.file.order:cli'), ('firewall', 'SoftLayer.CLI.firewall'), diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py index b4dd0b751..80996cd73 100644 --- a/SoftLayer/fixtures/SoftLayer_Network_Storage.py +++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py @@ -39,8 +39,10 @@ getObject = { 'accountId': 1234, - 'activeTransactionCount': 0, - 'activeTransactions': None, + 'activeTransactionCount': 1, + 'activeTransactions': [{ + 'transactionStatus': {'friendlyName': 'This is a buffer time in which the customer may cancel the server'} + }], 'allowedHardware': [{ 'allowedHost': { 'credential': {'username': 'joe', 'password': '12345'}, @@ -104,8 +106,8 @@ 'lunId': 2, 'nasType': 'ISCSI', 'notes': """{'status': 'available'}""", - 'originalSnapshotName': 'test-origin-snapshot-name', - 'originalVolumeName': 'test-origin-volume-name', + 'originalSnapshotName': 'test-original-snapshot-name', + 'originalVolumeName': 'test-original-volume-name', 'originalVolumeSize': '20', 'osType': {'keyName': 'LINUX'}, 'parentVolume': {'snapshotSizeBytes': 1024}, diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py index 766312012..1e5931fce 100644 --- a/SoftLayer/managers/block.py +++ b/SoftLayer/managers/block.py @@ -303,6 +303,35 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None, return self.client.call('Product_Order', 'placeOrder', order) + def order_modified_volume(self, volume_id, new_size=None, new_iops=None, new_tier_level=None): + """Places an order for modifying an existing block volume. + + :param volume_id: The ID of the volume to be modified + :param new_size: The new size/capacity for the volume + :param new_iops: The new IOPS for the volume + :param new_tier_level: The new tier level for the volume + :return: Returns a SoftLayer_Container_Product_Order_Receipt + """ + + mask_items = [ + 'id', + 'billingItem', + 'storageType[keyName]', + 'capacityGb', + 'provisionedIops', + 'storageTierLevel', + 'staasVersion', + 'hasEncryptionAtRest', + ] + block_mask = ','.join(mask_items) + volume = self.get_block_volume_details(volume_id, mask=block_mask) + + order = storage_utils.prepare_modify_order_object( + self, volume, new_iops, new_tier_level, new_size + ) + + return self.client.call('Product_Order', 'placeOrder', order) + def delete_snapshot(self, snapshot_id): """Deletes the specified snapshot object. diff --git a/SoftLayer/managers/file.py b/SoftLayer/managers/file.py index 2ba39cca4..e4a5ac238 100644 --- a/SoftLayer/managers/file.py +++ b/SoftLayer/managers/file.py @@ -283,6 +283,35 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None, return self.client.call('Product_Order', 'placeOrder', order) + def order_modified_volume(self, volume_id, new_size=None, new_iops=None, new_tier_level=None): + """Places an order for modifying an existing file volume. + + :param volume_id: The ID of the volume to be modified + :param new_size: The new size/capacity for the volume + :param new_iops: The new IOPS for the volume + :param new_tier_level: The new tier level for the volume + :return: Returns a SoftLayer_Container_Product_Order_Receipt + """ + + mask_items = [ + 'id', + 'billingItem', + 'storageType[keyName]', + 'capacityGb', + 'provisionedIops', + 'storageTierLevel', + 'staasVersion', + 'hasEncryptionAtRest', + ] + file_mask = ','.join(mask_items) + volume = self.get_file_volume_details(volume_id, mask=file_mask) + + order = storage_utils.prepare_modify_order_object( + self, volume, new_iops, new_tier_level, new_size + ) + + return self.client.call('Product_Order', 'placeOrder', order) + def delete_snapshot(self, snapshot_id): """Deletes the specified snapshot object. diff --git a/SoftLayer/managers/storage_utils.py b/SoftLayer/managers/storage_utils.py index 5040078e6..b6e1a3d46 100644 --- a/SoftLayer/managers/storage_utils.py +++ b/SoftLayer/managers/storage_utils.py @@ -947,8 +947,7 @@ def prepare_duplicate_order_object(manager, origin_volume, iops, tier, if iops is None: iops = int(origin_volume.get('provisionedIops', 0)) if iops <= 0: - raise exceptions.SoftLayerError( - "Cannot find origin volume's provisioned IOPS") + raise exceptions.SoftLayerError("Cannot find origin volume's provisioned IOPS") # Set up the price array for the order prices = [ find_price_by_category(package, 'storage_as_a_service'), @@ -1001,6 +1000,85 @@ def prepare_duplicate_order_object(manager, origin_volume, iops, tier, return duplicate_order +def prepare_modify_order_object(manager, volume, new_iops, new_tier, new_size): + """Prepare the modification order to submit to SoftLayer_Product::placeOrder() + + :param manager: The File or Block manager calling this function + :param volume: The volume which is being modified + :param new_iops: The new IOPS for the volume (performance) + :param new_tier: The new tier level for the volume (endurance) + :param new_size: The requested new size for the volume + :return: Returns the order object to be passed to the placeOrder() method of the Product_Order service + """ + + # Verify that the origin volume has not been cancelled + if 'billingItem' not in volume: + raise exceptions.SoftLayerError("The volume has been cancelled; unable to modify volume.") + + # Ensure the origin volume is STaaS v2 or higher and supports Encryption at Rest + if not _staas_version_is_v2_or_above(volume): + raise exceptions.SoftLayerError("This volume cannot be modified since it does not support Encryption at Rest.") + + # Get the appropriate package for the order ('storage_as_a_service' is currently used for modifying volumes) + package = get_package(manager, 'storage_as_a_service') + + # Based on volume storage type, ensure at least one volume property is being modified, + # use current values if some are not specified, and lookup price codes for the order + volume_storage_type = volume['storageType']['keyName'] + if 'PERFORMANCE' in volume_storage_type: + volume_is_performance = True + if new_size is None and new_iops is None: + raise exceptions.SoftLayerError("A size or IOPS value must be given to modify this performance volume.") + + if new_size is None: + new_size = volume['capacityGb'] + elif new_iops is None: + new_iops = int(volume.get('provisionedIops', 0)) + if new_iops <= 0: + raise exceptions.SoftLayerError("Cannot find volume's provisioned IOPS.") + + # Set up the prices array for the order + prices = [ + find_price_by_category(package, 'storage_as_a_service'), + find_saas_perform_space_price(package, new_size), + find_saas_perform_iops_price(package, new_size, new_iops), + ] + + elif 'ENDURANCE' in volume_storage_type: + volume_is_performance = False + if new_size is None and new_tier is None: + raise exceptions.SoftLayerError("A size or tier value must be given to modify this endurance volume.") + + if new_size is None: + new_size = volume['capacityGb'] + elif new_tier is None: + new_tier = find_endurance_tier_iops_per_gb(volume) + + # Set up the prices array for the order + prices = [ + find_price_by_category(package, 'storage_as_a_service'), + find_saas_endurance_space_price(package, new_size, new_tier), + find_saas_endurance_tier_price(package, new_tier), + ] + + else: + raise exceptions.SoftLayerError("Volume does not have a valid storage type (with an appropriate " + "keyName to indicate the volume is a PERFORMANCE or an ENDURANCE volume).") + + modify_order = { + 'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': package['id'], + 'prices': prices, + 'volume': {'id': volume['id']}, + 'volumeSize': new_size + } + + if volume_is_performance: + modify_order['iops'] = new_iops + + return modify_order + + def _has_category(categories, category_code): return any( True diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py index d7ad2c3c1..352871b15 100644 --- a/tests/CLI/modules/block_tests.py +++ b/tests/CLI/modules/block_tests.py @@ -71,7 +71,8 @@ def test_volume_detail(self): 'Data Center': 'dal05', 'Type': 'ENDURANCE', 'ID': 100, - '# of Active Transactions': '0', + '# of Active Transactions': '1', + 'Ongoing Transaction': 'This is a buffer time in which the customer may cancel the server', 'Replicant Count': '1', 'Replication Status': 'Replicant Volume Provisioning ' 'has completed.', @@ -86,11 +87,13 @@ def test_volume_detail(self): {'Replicant ID': 'Data Center', '1785': 'dal01'}, {'Replicant ID': 'Schedule', '1785': 'REPLICATION_DAILY'}, ]], - 'Duplicate Volume Properties': [ - {'Original Volume Name': 'Original Volume Size', - 'test-origin-volume-name': '20'}, - {'Original Volume Name': 'Original Snapshot Name', - 'test-origin-volume-name': 'test-origin-snapshot-name'} + 'Original Volume Properties': [ + {'Property': 'Original Volume Size', + 'Value': '20'}, + {'Property': 'Original Volume Name', + 'Value': 'test-original-volume-name'}, + {'Property': 'Original Snapshot Name', + 'Value': 'test-original-snapshot-name'} ] }, json.loads(result.output)) @@ -599,6 +602,37 @@ def test_duplicate_order_hourly_billing(self, order_mock): 'Order #24602 placed successfully!\n' ' > Storage as a Service\n') + @mock.patch('SoftLayer.BlockStorageManager.order_modified_volume') + def test_modify_order_exception_caught(self, order_mock): + order_mock.side_effect = ValueError('order attempt failed, noooo!') + + result = self.run_command(['block', 'volume-modify', '102', '--new-size=1000']) + + self.assertEqual(2, result.exit_code) + self.assertEqual('Argument Error: order attempt failed, noooo!', result.exception.message) + + @mock.patch('SoftLayer.BlockStorageManager.order_modified_volume') + def test_modify_order_order_not_placed(self, order_mock): + order_mock.return_value = {} + + result = self.run_command(['block', 'volume-modify', '102', '--new-iops=1400']) + + self.assert_no_fail(result) + self.assertEqual('Order could not be placed! Please verify your options and try again.\n', result.output) + + @mock.patch('SoftLayer.BlockStorageManager.order_modified_volume') + def test_modify_order(self, order_mock): + order_mock.return_value = {'placedOrder': {'id': 24602, 'items': [{'description': 'Storage as a Service'}, + {'description': '1000 GBs'}, + {'description': '4 IOPS per GB'}]}} + + result = self.run_command(['block', 'volume-modify', '102', '--new-size=1000', '--new-tier=4']) + + order_mock.assert_called_with('102', new_size=1000, new_iops=None, new_tier_level=4) + self.assert_no_fail(result) + self.assertEqual('Order #24602 placed successfully!\n > Storage as a Service\n > 1000 GBs\n > 4 IOPS per GB\n', + result.output) + def test_set_password(self): result = self.run_command(['block', 'access-password', '1234', '--password=AAAAA']) self.assert_no_fail(result) diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py index 6aad4564d..6614e115f 100644 --- a/tests/CLI/modules/file_tests.py +++ b/tests/CLI/modules/file_tests.py @@ -111,7 +111,8 @@ def test_volume_detail(self): 'Data Center': 'dal05', 'Type': 'ENDURANCE', 'ID': 100, - '# of Active Transactions': '0', + '# of Active Transactions': '1', + 'Ongoing Transaction': 'This is a buffer time in which the customer may cancel the server', 'Replicant Count': '1', 'Replication Status': 'Replicant Volume Provisioning ' 'has completed.', @@ -126,11 +127,13 @@ def test_volume_detail(self): {'Replicant ID': 'Data Center', '1785': 'dal01'}, {'Replicant ID': 'Schedule', '1785': 'REPLICATION_DAILY'}, ]], - 'Duplicate Volume Properties': [ - {'Original Volume Name': 'Original Volume Size', - 'test-origin-volume-name': '20'}, - {'Original Volume Name': 'Original Snapshot Name', - 'test-origin-volume-name': 'test-origin-snapshot-name'} + 'Original Volume Properties': [ + {'Property': 'Original Volume Size', + 'Value': '20'}, + {'Property': 'Original Volume Name', + 'Value': 'test-original-volume-name'}, + {'Property': 'Original Snapshot Name', + 'Value': 'test-original-snapshot-name'} ] }, json.loads(result.output)) @@ -577,3 +580,34 @@ def test_duplicate_order_hourly_billing(self, order_mock): self.assertEqual(result.output, 'Order #24602 placed successfully!\n' ' > Storage as a Service\n') + + @mock.patch('SoftLayer.FileStorageManager.order_modified_volume') + def test_modify_order_exception_caught(self, order_mock): + order_mock.side_effect = ValueError('order attempt failed, noooo!') + + result = self.run_command(['file', 'volume-modify', '102', '--new-size=1000']) + + self.assertEqual(2, result.exit_code) + self.assertEqual('Argument Error: order attempt failed, noooo!', result.exception.message) + + @mock.patch('SoftLayer.FileStorageManager.order_modified_volume') + def test_modify_order_order_not_placed(self, order_mock): + order_mock.return_value = {} + + result = self.run_command(['file', 'volume-modify', '102', '--new-iops=1400']) + + self.assert_no_fail(result) + self.assertEqual('Order could not be placed! Please verify your options and try again.\n', result.output) + + @mock.patch('SoftLayer.FileStorageManager.order_modified_volume') + def test_modify_order(self, order_mock): + order_mock.return_value = {'placedOrder': {'id': 24602, 'items': [{'description': 'Storage as a Service'}, + {'description': '1000 GBs'}, + {'description': '4 IOPS per GB'}]}} + + result = self.run_command(['file', 'volume-modify', '102', '--new-size=1000', '--new-tier=4']) + + order_mock.assert_called_with('102', new_size=1000, new_iops=None, new_tier_level=4) + self.assert_no_fail(result) + self.assertEqual('Order #24602 placed successfully!\n > Storage as a Service\n > 1000 GBs\n > 4 IOPS per GB\n', + result.output) diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py index d3ebe922a..2f6cf2abc 100644 --- a/tests/managers/block_tests.py +++ b/tests/managers/block_tests.py @@ -832,6 +832,50 @@ def test_order_block_duplicate_endurance(self): 'useHourlyPricing': False },)) + def test_order_block_modified_performance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE' + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = mock_volume + + result = self.block.order_modified_volume(102, new_size=1000, new_iops=2000, new_tier_level=None) + + self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) + self.assert_called_with( + 'SoftLayer_Product_Order', + 'placeOrder', + args=({'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}], + 'volume': {'id': 102}, + 'volumeSize': 1000, + 'iops': 2000},) + ) + + def test_order_block_modified_endurance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = mock_volume + + result = self.block.order_modified_volume(102, new_size=1000, new_iops=None, new_tier_level=4) + + self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) + self.assert_called_with( + 'SoftLayer_Product_Order', + 'placeOrder', + args=({'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}], + 'volume': {'id': 102}, + 'volumeSize': 1000},) + ) + def test_setCredentialPassword(self): mock = self.set_mock('SoftLayer_Network_Storage_Allowed_Host', 'setCredentialPassword') mock.return_value = True diff --git a/tests/managers/file_tests.py b/tests/managers/file_tests.py index 6a6d14d82..389682cdc 100644 --- a/tests/managers/file_tests.py +++ b/tests/managers/file_tests.py @@ -785,3 +785,48 @@ def test_order_file_duplicate_endurance(self): 'duplicateOriginSnapshotId': 470, 'useHourlyPricing': False },)) + + def test_order_file_modified_performance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE' + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = mock_volume + + result = self.file.order_modified_volume(102, new_size=1000, new_iops=2000, new_tier_level=None) + + self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) + self.assert_called_with( + 'SoftLayer_Product_Order', + 'placeOrder', + args=({'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}], + 'volume': {'id': 102}, + 'volumeSize': 1000, + 'iops': 2000},) + ) + + def test_order_file_modified_endurance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE' + mock = self.set_mock('SoftLayer_Network_Storage', 'getObject') + mock.return_value = mock_volume + + result = self.file.order_modified_volume(102, new_size=1000, new_iops=None, new_tier_level=4) + + self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result) + self.assert_called_with( + 'SoftLayer_Product_Order', + 'placeOrder', + args=({'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}], + 'volume': {'id': 102}, + 'volumeSize': 1000},) + ) diff --git a/tests/managers/storage_utils_tests.py b/tests/managers/storage_utils_tests.py index c3676d9cb..7c32e0832 100644 --- a/tests/managers/storage_utils_tests.py +++ b/tests/managers/storage_utils_tests.py @@ -3851,3 +3851,187 @@ def test_prep_duplicate_order_invalid_origin_storage_type(self): "Origin volume does not have a valid storage type " "(with an appropriate keyName to indicate the " "volume is a PERFORMANCE or an ENDURANCE volume)") + + # --------------------------------------------------------------------- + # Tests for prepare_modify_order_object() + # --------------------------------------------------------------------- + def test_prep_modify_order_origin_volume_cancelled(self): + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + del mock_volume['billingItem'] + + exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object, + self.block, mock_volume, None, None, None) + + self.assertEqual("The volume has been cancelled; unable to modify volume.", str(exception)) + + def test_prep_modify_order_origin_volume_staas_version_below_v2(self): + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['staasVersion'] = 1 + + exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object, + self.block, mock_volume, None, None, None) + + self.assertEqual("This volume cannot be modified since it does not support Encryption at Rest.", + str(exception)) + + def test_prep_modify_order_performance_values_not_given(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE' + + exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object, + self.block, mock_volume, None, None, None) + + self.assertEqual("A size or IOPS value must be given to modify this performance volume.", str(exception)) + + def test_prep_modify_order_performance_iops_not_found(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE' + del mock_volume['provisionedIops'] + + exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object, + self.block, mock_volume, None, None, 40) + + self.assertEqual("Cannot find volume's provisioned IOPS.", str(exception)) + + def test_prep_modify_order_performance_use_existing_iops(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE' + + expected_object = { + 'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}], + 'volume': {'id': 102}, + 'volumeSize': 1000, + 'iops': 1000 + } + + result = storage_utils.prepare_modify_order_object(self.file, mock_volume, None, None, 1000) + self.assertEqual(expected_object, result) + + def test_prep_modify_order_performance_use_existing_size(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE' + + expected_object = { + 'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 189993}, {'id': 190053}], + 'volume': {'id': 102}, + 'volumeSize': 500, + 'iops': 2000 + } + + result = storage_utils.prepare_modify_order_object(self.block, mock_volume, 2000, None, None) + self.assertEqual(expected_object, result) + + def test_prep_modify_order_performance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE' + + expected_object = { + 'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}], + 'volume': {'id': 102}, + 'volumeSize': 1000, + 'iops': 2000 + } + + result = storage_utils.prepare_modify_order_object(self.file, mock_volume, 2000, None, 1000) + self.assertEqual(expected_object, result) + + def test_prep_modify_order_endurance_values_not_given(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'ENDURANCE_BLOCK_STORAGE' + + exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object, + self.block, mock_volume, None, None, None) + + self.assertEqual("A size or tier value must be given to modify this endurance volume.", str(exception)) + + def test_prep_modify_order_endurance_use_existing_tier(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE' + + expected_object = { + 'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 193433}, {'id': 193373}], + 'volume': {'id': 102}, + 'volumeSize': 1000 + } + + result = storage_utils.prepare_modify_order_object(self.file, mock_volume, None, None, 1000) + self.assertEqual(expected_object, result) + + def test_prep_modify_order_endurance_use_existing_size(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'ENDURANCE_BLOCK_STORAGE' + + expected_object = { + 'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}], + 'volume': {'id': 102}, + 'volumeSize': 500 + } + + result = storage_utils.prepare_modify_order_object(self.block, mock_volume, None, 4, None) + self.assertEqual(expected_object, result) + + def test_prep_modify_order_endurance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE' + + expected_object = { + 'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade', + 'packageId': 759, + 'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}], + 'volume': {'id': 102}, + 'volumeSize': 1000 + } + + result = storage_utils.prepare_modify_order_object(self.file, mock_volume, None, 4, 1000) + self.assertEqual(expected_object, result) + + def test_prep_modify_order_invalid_volume_storage_type(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE] + + mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME) + mock_volume['storageType']['keyName'] = 'NINJA_PENGUINS' + + exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object, + self.block, mock_volume, None, None, None) + + self.assertEqual("Volume does not have a valid storage type (with an appropriate " + "keyName to indicate the volume is a PERFORMANCE or an ENDURANCE volume).", + str(exception))