diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 481f69feb..48f5f01a6 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -17,7 +17,6 @@ sshkey Manage SSH keys on your account Networking: - cdn Content Delivery Network service management dns Domain Name System firewall Firewall rule and security management globalip Global IP address management @@ -33,7 +32,6 @@ General: config View and edit configuration for this tool - ticket Manage account tickets summary Display an overall summary of your account help Show help @@ -66,25 +64,18 @@ class CommandParser(object): - """ Helper class to parse commands - - :param env: Environment instance - """ def __init__(self, env): self.env = env def get_main_help(self): - """ Get main help text """ return __doc__.strip() def get_module_help(self, module_name): - """ Get help text for a module """ module = self.env.load_module(module_name) arg_doc = module.__doc__ return arg_doc.strip() def get_command_help(self, module_name, command_name): - """ Get help text for a specific command """ command = self.env.get_command(module_name, command_name) default_format = 'raw' @@ -102,18 +93,16 @@ def get_command_help(self, module_name, command_name): if '[options]' in arg_doc: arg_doc += """ Standard Options: - --format=ARG Output format. [Options: table, raw] [Default: %s] - -C FILE --config=FILE Config file location. [Default: ~/.softlayer] - --debug=LEVEL Specifies the debug noise level + --format=ARG Output format. [Options: table, raw] [Default: %s] + -C FILE --config=FILE Config file location. [Default: ~/.softlayer] + --debug=LEVEL Specifies the debug noise level 1=warn, 2=info, 3=debug - --timings Time each API call and display after results - --proxy=PROTO:PROXY_URL HTTP[s] proxy to be use to make API calls - -h --help Show this screen + --timings Time each API call and display after results + -h --help Show this screen """ % default_format return arg_doc.strip() def parse_main_args(self, args): - """ Parse root arguments """ main_help = self.get_main_help() arguments = docopt( main_help, @@ -124,7 +113,6 @@ def parse_main_args(self, args): return arguments def parse_module_args(self, module_name, args): - """ Parse module arguments """ arg_doc = self.get_module_help(module_name) arguments = docopt( arg_doc, @@ -134,14 +122,12 @@ def parse_module_args(self, module_name, args): return arguments def parse_command_args(self, module_name, command_name, args): - """ Parse command arguments """ command = self.env.get_command(module_name, command_name) arg_doc = self.get_command_help(module_name, command_name) arguments = docopt(arg_doc, version=VERSION, argv=[module_name] + args) return command, arguments def parse(self, args): - """ Parse entire tree of arguments """ # handle `sl ...` main_args = self.parse_main_args(args) module_name = main_args[''] @@ -173,18 +159,14 @@ def main(args=sys.argv[1:], env=Environment()): debug_level = command_args.get('--debug') if debug_level: logger = logging.getLogger() - handler = logging.StreamHandler() - logger.addHandler(handler) + h = logging.StreamHandler() + logger.addHandler(h) logger.setLevel(DEBUG_LOGGING_MAP.get(debug_level, logging.DEBUG)) - kwargs = { - 'proxy': command_args.get('--proxy'), - 'config_file': command_args.get('--config') - } if command_args.get('--timings'): - client = TimedClient(**kwargs) + client = TimedClient(config_file=command_args.get('--config')) else: - client = Client(**kwargs) + client = Client(config_file=command_args.get('--config')) # Do the thing runnable = command(client=client, env=env) @@ -193,56 +175,56 @@ def main(args=sys.argv[1:], env=Environment()): out_format = command_args.get('--format', 'table') if out_format not in VALID_FORMATS: raise ArgumentError('Invalid format "%s"' % out_format) - output = format_output(data, fmt=out_format) - if output: - env.out(output) + s = format_output(data, fmt=out_format) + if s: + env.out(s) if command_args.get('--timings'): out_format = command_args.get('--format', 'table') api_calls = client.get_last_calls() - timing_table = KeyValueTable(['call', 'time']) + t = KeyValueTable(['call', 'time']) for call, _, duration in api_calls: - timing_table.add_row([call, duration]) + t.add_row([call, duration]) - env.err(format_output(timing_table, fmt=out_format)) + env.err(format_output(t, fmt=out_format)) - except InvalidCommand as ex: - env.err(resolver.get_module_help(ex.module_name)) - if ex.command_name: + except InvalidCommand as e: + env.err(resolver.get_module_help(e.module_name)) + if e.command_name: env.err('') - env.err(str(ex)) + env.err(str(e)) exit_status = 1 - except InvalidModule as ex: + except InvalidModule as e: env.err(resolver.get_main_help()) - if ex.module_name: + if e.module_name: env.err('') - env.err(str(ex)) + env.err(str(e)) exit_status = 1 - except DocoptExit as ex: - env.err(ex.usage) + except DocoptExit as e: + env.err(e.usage) env.err( '\nUnknown argument(s), use -h or --help for available options') exit_status = 127 except KeyboardInterrupt: env.out('') exit_status = 1 - except CLIAbort as ex: - env.err(str(ex.message)) - exit_status = ex.code - except SystemExit as ex: - exit_status = ex.code - except SoftLayerAPIError as ex: - if 'invalid api token' in ex.faultString.lower(): + except CLIAbort as e: + env.err(str(e.message)) + exit_status = e.code + except SystemExit as e: + exit_status = e.code + except SoftLayerAPIError as e: + if 'invalid api token' in e.faultString.lower(): env.out("Authentication Failed: To update your credentials, use " "'sl config setup'") else: - env.err(str(ex)) + env.err(str(e)) exit_status = 1 - except SoftLayerError as ex: - env.err(str(ex)) + except SoftLayerError as e: + env.err(str(e)) exit_status = 1 - except Exception: + except Exception as e: import traceback env.err(traceback.format_exc()) exit_status = 1 diff --git a/SoftLayer/CLI/exceptions.py b/SoftLayer/CLI/exceptions.py index 100966945..903485466 100644 --- a/SoftLayer/CLI/exceptions.py +++ b/SoftLayer/CLI/exceptions.py @@ -8,21 +8,18 @@ class CLIHalt(SystemExit): - """ Smoothly halt the execution of the command. No error """ def __init__(self, code=0, *args): super(CLIHalt, self).__init__(*args) self.code = code class CLIAbort(CLIHalt): - """ Halt the execution of the command. Gives an exit code of 2 """ def __init__(self, msg, *args): super(CLIAbort, self).__init__(code=2, *args) self.message = msg class ArgumentError(CLIAbort): - """ Halt the execution of the command because of invalid arguments. """ def __init__(self, msg, *args): super(ArgumentError, self).__init__(msg, *args) self.message = "Argument Error: %s" % msg diff --git a/SoftLayer/CLI/modules/iscsi.py b/SoftLayer/CLI/modules/iscsi.py index e8df62968..887f15540 100644 --- a/SoftLayer/CLI/modules/iscsi.py +++ b/SoftLayer/CLI/modules/iscsi.py @@ -4,17 +4,27 @@ Manage iSCSI targets The available commands are: - list List iSCSI targets -""" -# :license: MIT, see LICENSE for more details. + list List iSCSI targets + create Create iSCSI target + detail Output details about iSCSI + cancel cancel iSCSI target + order_snapshot_space Orders space for snapshots + create_snapshot Create snapshot of iSCSI + delete_snapshot Delete iSCSI snapshot + restore_volume Restores volume from existing snapshot + list_snapshots List Snapshots of given iscsi -from SoftLayer.CLI import CLIRunnable, Table, FormattedItem -from SoftLayer.CLI.helpers import NestedDict, blank +""" +from SoftLayer.CLI import (CLIRunnable, Table, no_going_back, FormattedItem) +from SoftLayer.CLI.helpers import ( + CLIAbort, ArgumentError, NestedDict, blank, resolve_id, KeyValueTable) +from SoftLayer import ISCSIManager class ListISCSI(CLIRunnable): + """ -usage: sl iscsi list [options] + usage: sl iscsi list [options] List iSCSI accounts """ @@ -23,11 +33,11 @@ class ListISCSI(CLIRunnable): def execute(self, args): account = self.client['Account'] - iscsi_list = account.getIscsiNetworkStorage( + iscsi = account.getIscsiNetworkStorage( mask='eventCount,serviceResource[datacenter.name]') - iscsi_list = [NestedDict(iscsi) for iscsi in iscsi_list] + iscsi = [NestedDict(n) for n in iscsi] - table = Table([ + t = Table([ 'id', 'datacenter', 'size', @@ -36,15 +46,269 @@ def execute(self, args): 'server' ]) - for iscsi in iscsi_list: - table.add_row([ - iscsi['id'], - iscsi['serviceResource']['datacenter'].get('name', blank()), + for n in iscsi: + t.add_row([ + n['id'], + n['serviceResource']['datacenter'].get('name', blank()), FormattedItem( - iscsi.get('capacityGb', blank()), - "%dGB" % iscsi.get('capacityGb', 0)), - iscsi.get('username', blank()), - iscsi.get('password', blank()), - iscsi.get('serviceResourceBackendIpAddress', blank())]) + n.get('capacityGb', blank()), + "%dGB" % n.get('capacityGb', 0)), + n.get('username', blank()), + n.get('password', blank()), + n.get('serviceResourceBackendIpAddress', blank())]) + + return t + + +class CreateiSCSI(CLIRunnable): + + """ + usage: sl iscsi create --size=SIZE --dc=DC [options] + + Order/create an iSCSI storage. + + Required: + --size=SIZE Size + --dc=DC Datacenter + """ + action = 'create' + options = ['confirm'] + required_params = ['--size', '--dc'] + + def execute(self, args): + iscsi = ISCSIManager(self.client) + + self._validate_create_args(args) + order = { + 'size': int(args['--size']), + } + location = self._get_location_id(args['--dc']) + order['dc'] = location + iscsi.order_iscsi(**order) + + def _validate_create_args(self, args): + invalid_args = [k for k in self.required_params if args.get(k) is None] + if invalid_args: + raise ArgumentError('Missing required options: %s' + % ','.join(invalid_args)) + + def _parse_create_args(self, args): + + size = int(args['--size']) + location = str(args['--dc']) + return size, location + + def _get_location_id(self, location): + datacenters = self.client['Location_Datacenter'].getDatacenters( + mask='mask[longName,id,name]') + for dc in datacenters: + if dc['name'] == location: + self.location = dc['id'] + return self.location + raise ArgumentError('Invalid datacenter name: %s' % location) + + +class CanceliSCSI(CLIRunnable): + + """ +usage: sl iscsi cancel [options] + +Cancel iSCSI Storage + +options : +--immediate Cancels the iSCSI immediately (instead of on the billing + anniversary) +--reason=REASON An optional cancellation reason. + +""" + action = 'cancel' + options = ['confirm'] + + def execute(self, args): + iscsi = ISCSIManager(self.client) + iscsi_id = resolve_id( + iscsi.resolve_ids, + args.get(''), + 'iSCSI') + + immediate = args.get('--immediate', False) + + reason = args.get('--reason') + if args['--really'] or no_going_back(iscsi_id): + iscsi.cancel_iscsi(iscsi_id, reason, immediate) + else: + CLIAbort('Aborted') + + +class IscsiDetails(CLIRunnable): + + """ +usage: sl iscsi detail [--passwords] [options] + +Get details for a iSCSI + +Options: + --passwords Show passwords + + +""" + action = 'detail' + + def execute(self, args): + iscsi = ISCSIManager(self.client) + t = KeyValueTable(['Name', 'Value']) + t.align['Name'] = 'r' + t.align['Value'] = 'l' + + iscsi_id = resolve_id( + iscsi.resolve_ids, + args.get(''), + 'iSCSI') + result = iscsi.get_iscsi(iscsi_id) + result = NestedDict(result) + + t.add_row(['id', result['id']]) + t.add_row(['serviceResourceName', result['serviceResourceName']]) + t.add_row(['createDate', result['createDate']]) + t.add_row(['nasType', result['nasType']]) + t.add_row(['capacityGb', result['capacityGb']]) + if result['snapshotCapacityGb']: + t.add_row(['snapshotCapacityGb', result['snapshotCapacityGb']]) + t.add_row(['mountableFlag', result['mountableFlag']]) + t.add_row( + ['serviceResourceBackendIpAddress', + result['serviceResourceBackendIpAddress']]) + t.add_row(['price', result['billingItem']['recurringFee']]) + t.add_row(['BillingItemId', result['billingItem']['id']]) + if result.get('notes'): + t.add_row(['notes', result['notes']]) + + if args.get('--passwords'): + pass_table = Table(['username', 'password']) + pass_table.add_row([result['username'], result['password']]) + t.add_row(['users', pass_table]) + + return t + + +class IscsiCreateSnapshot(CLIRunnable): + + """ +usage: sl iscsi create_snapshot [options] + +create an iSCSI snapshot. + +Options: +--notes=NOTE An optional note + +""" + action = 'create_snapshot' + + def execute(self, args): + iscsi = ISCSIManager(self.client) + iscsi_id = resolve_id(iscsi.resolve_ids, + args.get(''), + 'iSCSI') + notes = args.get('--notes') + iscsi.create_snapshot(iscsi_id, notes) + + +class OrderIscsiSpace(CLIRunnable): + + """ +usage: sl iscsi order_snapshot_space [options] + +Order iSCSI snapshot space. + +Required : +--capacity=Capacity Snapshot Capacity +""" + + action = 'order_snapshot_space' + required_params = ['--capacity'] + + def execute(self, args): + iscsi = ISCSIManager(self.client) + invalid_args = [k for k in self.required_params if args.get(k) is None] + if invalid_args: + raise ArgumentError('Missing required options: %s' + % ','.join(invalid_args)) + iscsi_id = resolve_id( + iscsi.resolve_ids, + args.get(''), + 'iSCSI') + capacity = args.get('--capacity') + iscsi.order_snapshot_space(iscsi_id, capacity) + + +class IscsiDeleteSnapshot(CLIRunnable): + + """ +usage: sl iscsi delete_snapshot [options] + +Delete iSCSI snapshot. + +""" + action = 'delete_snapshot' + + def execute(self, args): + iscsi = ISCSIManager(self.client) + snapshot_id = resolve_id( + iscsi.resolve_ids, + args.get(''), + 'Snapshot') + iscsi.delete_snapshot(snapshot_id) + + +class RestoreVolumefromSnapshot(CLIRunnable): + + """ +usage: sl iscsi restore_volume + +restores volume from existing snapshot. + +""" + action = 'restore_volume' + + def execute(self, args): + iscsi = ISCSIManager(self.client) + volume_id = resolve_id( + iscsi.resolve_ids, args.get(''), 'iSCSI') + snapshot_id = resolve_id( + iscsi.resolve_ids, args.get(''), 'Snapshot') + iscsi.restore_from_snapshot(volume_id, snapshot_id) + + +class ListISCSISnapshots(CLIRunnable): + + """ + usage: sl iscsi list_snapshots + +List iSCSI Snapshots +""" + action = 'list_snapshots' + + def execute(self, args): + mgr = ISCSIManager(self.client) + iscsi_id = resolve_id( + mgr.resolve_ids, args.get(''), 'iSCSI') + iscsi = self.client['Network_Storage_Iscsi'] + snapshots = iscsi.getPartnerships( + mask='volumeId,partnerVolumeId,createDate,type', id=iscsi_id) + snapshots = [NestedDict(n) for n in snapshots] + + t = Table([ + 'id', + 'createDate', + 'name', + 'description', + ]) - return table + for n in snapshots: + t.add_row([ + n['partnerVolumeId'], + n['createDate'], + n['type']['name'], + n['type']['description'], + ]) + return t diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index 9db06a1d3..4561639df 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -18,7 +18,8 @@ from SoftLayer.managers.sshkey import SshKeyManager from SoftLayer.managers.ssl import SSLManager from SoftLayer.managers.ticket import TicketManager - +from SoftLayer.managers.iscsi import ISCSIManager __all__ = ['CCIManager', 'DNSManager', 'FirewallManager', 'HardwareManager', 'ImageManager', 'MessagingManager', 'MetadataManager', - 'NetworkManager', 'SshKeyManager', 'SSLManager', 'TicketManager'] + 'NetworkManager', 'SshKeyManager', 'SSLManager', + 'TicketManager', 'ISCSIManager'] diff --git a/SoftLayer/managers/iscsi.py b/SoftLayer/managers/iscsi.py new file mode 100644 index 000000000..b80db863f --- /dev/null +++ b/SoftLayer/managers/iscsi.py @@ -0,0 +1,146 @@ +from SoftLayer.utils import NestedDict, query_filter, IdentifierMixin + + +class ISCSIManager(IdentifierMixin, object): + + """ + Manages iSCSI storages. + + :param SoftLayer.API.Client client: an API client instance + """ + + def __init__(self, client): + self.configuration = {} + self.client = client + self.iscsi = self.client['Network_Storage_Iscsi'] + self.product_order = self.client['Product_Order'] + self.account = self.client['Account'] + + def _find_item_prices(self, size, query): + item_prices = [] + _filter = NestedDict({}) + _filter[ + 'itemPrices'][ + 'item'][ + 'description'] = query_filter( + query) + _filter['itemPrices']['item']['capacity'] = query_filter('%s' % size) + iscsi_item_prices = self.client['Product_Package'].getItemPrices( + id=0, + filter=_filter.to_dict()) + iscsi_item_prices = sorted( + iscsi_item_prices, + key=lambda x: + (float(x['item']['capacity']), + float(x.get('recurringFee', 0)))) + for price in iscsi_item_prices: + item_prices.append(price['id']) + return item_prices + + def build_order(self, item_price, dc): + order = { + 'complexType': + 'SoftLayer_Container_Product_Order_Network_Storage_Iscsi', + 'location': dc, + 'packageId': 0, # storage package + 'prices': [{'id': item_price[-1]}], + 'quantity': 1 + } + return order + + def order_iscsi(self, **kwargs): + """Places an order for iSCSI volume + """ + size = kwargs.get('size') + dc = kwargs.get('dc') + item_price = self._find_item_prices(size, '~GB iSCSI SAN Storage') + iscsi_order = self.build_order(item_price, dc) + self.product_order.verifyOrder(iscsi_order) + self.product_order.placeOrder(iscsi_order) + + def get_iscsi(self, volume_id, **kwargs): + """ Get details about a iSCSI storage + + :param integer volume_id: the volume ID + :returns: A dictionary containing a large amount of information about + the specified storage. + + """ + + if 'mask' not in kwargs: + items = set([ + 'id', + 'serviceResourceName', + 'createDate', + 'nasType', + 'capacityGb', + 'snapshotCapacityGb', + 'mountableFlag', + 'serviceResourceBackendIpAddress', + 'billingItem', + 'notes', + 'username', + 'password' + ]) + kwargs['mask'] = "mask[%s]" % ','.join(items) + return self.iscsi.getObject(id=volume_id, **kwargs) + + def cancel_iscsi(self, volume_id, reason='unNeeded', immediate=False): + """Cancels the given iSCSI volume. + + :param integer volume_id: the volume ID + """ + iscsi = self.get_iscsi( + volume_id, + mask='mask[id,capacityGb,username,password,billingItem[id]]') + billingItemId = iscsi['billingItem']['id'] + self.client['Billing_Item'].cancelItem( + immediate, + True, + reason, + id=billingItemId) + + def create_snapshot(self, volume_id, notes='unNeeded'): + """ Orders a snapshot for given volume + + :param integer volume_id: the volume ID + """ + + self.iscsi.createSnapshot(notes, id=volume_id) + + def order_snapshot_space(self, volume_id, capacity): + """ Orders a snapshot space for given volume + + :param integer volume_id: the volume ID + :param integer capacity: capacity in ~GB + """ + item_price = self._find_item_prices( + int(capacity), '~iSCSI SAN Snapshot Space') + result = self.get_iscsi( + volume_id, mask='mask[id,capacityGb,serviceResource[datacenter]]') + snapshotSpaceOrder = { + 'complexType': + 'SoftLayer_Container_Product_Order_\ +Network_Storage_Iscsi_SnapshotSpace', + 'location': result['serviceResource']['datacenter']['id'], + 'packageId': 0, + 'prices': [{'id': item_price[0]}], + 'quantity': 1, + 'volumeId': volume_id} + self.product_order.verifyOrder(snapshotSpaceOrder) + self.product_order.placeOrder(snapshotSpaceOrder) + + def delete_snapshot(self, snapshot_id): + """ Deletes the snapshot + + :params: integer snapshot_id: the snapshot ID + """ + + self.iscsi.deleteObject(id=snapshot_id) + + def restore_from_snapshot(self, volume_id, snapshot_id): + """ Restore the volume to snapshot's contents + :params: imteger volume_id: the volume ID + :params: integer snapshot_id: the snapshot ID + """ + self.iscsi.restoreFromSnapshot(snapshot_id, id=volume_id) diff --git a/SoftLayer/tests/fixtures/Billing_Item.py b/SoftLayer/tests/fixtures/Billing_Item.py index 620cf14b5..36a07fbed 100644 --- a/SoftLayer/tests/fixtures/Billing_Item.py +++ b/SoftLayer/tests/fixtures/Billing_Item.py @@ -1,2 +1,3 @@ cancelService = {} cancelServiceOnAnniversaryDate = {} +cancelItem = {} diff --git a/SoftLayer/tests/fixtures/Network_Storage_Iscsi.py b/SoftLayer/tests/fixtures/Network_Storage_Iscsi.py new file mode 100644 index 000000000..da7b76610 --- /dev/null +++ b/SoftLayer/tests/fixtures/Network_Storage_Iscsi.py @@ -0,0 +1,69 @@ +getObject = { + 'accountId': 278184, + 'billingItem': {'id': 6327}, + 'capacityGb': 20, + 'createDate': '2014-03-14T06:50:15-04:00', + 'guestId': '', + 'hardwareId': '', + 'hostId': '', + 'id': 100, + 'nasType': 'ISCSI', + 'notes': """{'status': 'available', 'name': u'IBMI278184-201', 'id': +u'702dc07d-b04b-49eb-9ffc-cb4493470df9'}""", + 'password': 'YAQSb9s3FbEz', + 'serviceProviderId': 1, + 'serviceResource': {'datacenter': {'id': 138124}}, + 'serviceResourceBackendIpAddress': '10.2.37.21', + 'serviceResourceName': 'storagesng0101', + 'username': 'IBMI278184-201' +} + +createSnapshot = { + 'accountId': 278184, + 'capacityGb': 20, + 'createDate': '2014-03-27T03:51:11-04:00', + 'guestId': '', + 'hardwareId': '', + 'hostId': '', + 'id': 101, + 'nasType': 'ISCSI_SNAPSHOT', + 'parentVolume': { + 'accountId': 278184, + 'capacityGb': 20, + 'createDate': '2014-03-27T03:38:47-04:00', + 'guestId': '', + 'hardwareId': '', + 'hostId': '', + 'id': 100, + 'nasType': 'ISCSI', + 'password': 'L8ta7MRXELwg', + 'properties': [ + {'createDate': '2014-03-27T03:40:22-04:00', + 'modifyDate': '', + 'type': { + 'description': + 'Percent of reserved snapshot space that is available', + 'keyname': 'SNAPSHOT_RESERVE_AVAILABLE', + 'name': 'Snaphot Reserve Available'}, + + 'value': '100', + 'volumeId': 2678430}], + + 'propertyCount': 0, + 'serviceProviderId': 1, + 'name': 'storagedal0506', + 'snapshotCapacityGb': '40', + 'username': 'IBMI278184-211'}, + 'password': 'L8ta7MRXELwg', + 'serviceProviderId': 1, + 'serviceResource': {'backendIpAddress': '10.1.145.26', + 'name': 'storagedal0506', + 'type': {'type': 'ISCSI'}}, + 'serviceResourceBackendIpAddress': '10.1.145.26', + 'serviceResourceName': 'storagedal0506', + 'username': 'IBMI278184-211'} + +restoreFromSnapshot = True +editObject = True +createObject = getObject +deleteObject = True diff --git a/SoftLayer/tests/fixtures/Product_Package.py b/SoftLayer/tests/fixtures/Product_Package.py index e2b70a8f7..31e513479 100644 --- a/SoftLayer/tests/fixtures/Product_Package.py +++ b/SoftLayer/tests/fixtures/Product_Package.py @@ -1071,3 +1071,69 @@ def get_bmc_categories_mock(): 'itemCategory': {'categoryCode': 'global_ipv6'}, 'prices': [{'id': 611}], }] +getItemPrices = [ + { + 'currentPriceFlag': '', + 'id': 2152, + 'item': { + 'capacity': '1', + 'description': '1 GB iSCSI SAN Storage', + 'id': 1150, + 'softwareDescriptionId': '', + 'units': 'GB', + 'upgradeItemId': 548}, + 'itemId': 1150, + 'laborFee': '0', + 'onSaleFlag': '', + 'oneTimeFee': '0', + 'packageReferences': [{'id': 46626, + 'itemPriceId': 2152, 'packageId': 0}], + 'quantity': '', + 'recurringFee': '.35', + 'setupFee': '0', + 'sort': 0 + }, + { + 'currentPriceFlag': '', + 'id': 22501, + 'item': {'capacity': '1', + 'description': '1 GB iSCSI SAN Storage', + 'id': 1150, + 'softwareDescriptionId': '', + 'units': 'GB', + 'upgradeItemId': 548}, + 'itemId': 1150, + 'laborFee': '0', + 'onSaleFlag': '', + 'oneTimeFee': '0', + 'packageReferences': [{ + 'id': 252983, + 'itemPriceId': 22501, 'packageId': 0 + }], + 'quantity': '', + 'recurringFee': '0', + 'setupFee': '0', + 'sort': 0 + }, + { + 'currentPriceFlag': '', + 'id': 22441, + 'item': { + 'capacity': '1', + 'description': '1 GB iSCSI SAN Storage', + 'id': 1150, + 'softwareDescriptionId': '', + 'units': 'GB', + 'upgradeItemId': 548 + }, + 'itemId': 1150, + 'laborFee': '0', + 'onSaleFlag': '', + 'oneTimeFee': '0', + 'packageReferences': [{'id': 250326, + 'itemPriceId': 22441, 'packageId': 0}], + 'quantity': '', + 'recurringFee': '15', + 'setupFee': '0', + 'sort': 0 + }] diff --git a/SoftLayer/tests/managers/iscsi_tests.py b/SoftLayer/tests/managers/iscsi_tests.py new file mode 100644 index 000000000..303a709a6 --- /dev/null +++ b/SoftLayer/tests/managers/iscsi_tests.py @@ -0,0 +1,87 @@ +""" + SoftLayer.tests.managers.iscsi_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +from SoftLayer import ISCSIManager +from SoftLayer.tests import unittest, FixtureClient +from SoftLayer.tests.fixtures import Network_Storage_Iscsi + +from mock import ANY + + +class ISCSITests(unittest.TestCase): + def setUp(self): + self.client = FixtureClient() + self.iscsi = ISCSIManager(self.client) + + def test_get_iscsi(self): + result = self.iscsi.get_iscsi(100) + self.client['Network_Storage_Iscsi'].getObject.assert_called_once_with( + id=100, mask=ANY) + self.assertEqual(Network_Storage_Iscsi.getObject, result) + + def test_cancel_iscsi_immediately(self): + iscsi_id = 6327 + self.iscsi.cancel_iscsi(iscsi_id, immediate=True) + f = self.client['Billing_Item'].cancelItem + f.assert_called_once_with(True, True, 'unNeeded', id=iscsi_id) + + def test_cancel_iscsi_without_reason(self): + iscsi_id = 6327 + self.iscsi.cancel_iscsi(iscsi_id) + f = self.client['Billing_Item'].cancelItem + f.assert_called_once_with(False, True, 'unNeeded', id=iscsi_id) + + def test_cancel_iscsi_with_reason(self): + iscsi_id = 6327 + reason = 'Network Performance' + self.iscsi.cancel_iscsi(iscsi_id, reason) + f = self.client['Billing_Item'].cancelItem + f.assert_called_once_with(False, True, reason, id=iscsi_id) + + def test_order_iscsi_without_recurringFee(self): + self.iscsi.order_iscsi(test=1, verify=1) + f = self.client['Product_Order'].placeOrder + f.assert_called_once_with( + {'prices': [{'id': 22441}], + 'quantity': 1, + 'location': None, + 'packageId': 0, + 'complexType': + 'SoftLayer_Container_Product_Order_Network_Storage_Iscsi'}) + + def test_delete_snapshot(self): + self.iscsi.delete_snapshot(1) + self.client[ + 'Network_Storage_Iscsi'].deleteObject.assert_called_once_with(id=1) + + def test_create_snapshot(self): + iscsi_id = 100 + self.iscsi.create_snapshot(iscsi_id, 'unNeeded') + f = self.client['Network_Storage_Iscsi'].createSnapshot + f.assert_called_once_with('unNeeded', id=iscsi_id) + + def test_order_snapshot_space(self): + iscsi_id = 100 + capacity = 20 + self.iscsi.order_snapshot_space(iscsi_id, capacity) + f = self.client['Product_Order'].placeOrder + f.assert_called_once_with( + {'volumeId': 100, + 'location': 138124, + 'packageId': 0, + 'complexType': + 'SoftLayer_Container_\ +Product_Order_Network_Storage_Iscsi_SnapshotSpace', + 'prices': [{'id': 22501}], + 'quantity': 1 + }) + + def test_restore_from_snapshot(self): + volume_id = 100 + snapshot_id = 101 + self.iscsi.restore_from_snapshot(volume_id, snapshot_id) + f = self.client['Network_Storage_Iscsi'].restoreFromSnapshot + f.assert_called_once_with(101, id=100)