diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 48f5f01a6..57f66d356 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -49,7 +49,7 @@ from SoftLayer import Client, TimedClient, SoftLayerError, SoftLayerAPIError from SoftLayer.consts import VERSION -from .helpers import CLIAbort, ArgumentError, format_output, KeyValueTable +from .helpers import CLIAbort, ArgumentError, InvalidInput, format_output, KeyValueTable from .environment import Environment, InvalidCommand, InvalidModule diff --git a/SoftLayer/CLI/exceptions.py b/SoftLayer/CLI/exceptions.py index 903485466..6caace3de 100644 --- a/SoftLayer/CLI/exceptions.py +++ b/SoftLayer/CLI/exceptions.py @@ -23,3 +23,8 @@ class ArgumentError(CLIAbort): def __init__(self, msg, *args): super(ArgumentError, self).__init__(msg, *args) self.message = "Argument Error: %s" % msg + +class InvalidInput(CLIAbort): + def _init_(self, msg, *args): + super(InvalidInput, self).__init__(msg, *args) + self.message = "InvalidInput: %s" % msg diff --git a/SoftLayer/CLI/helpers.py b/SoftLayer/CLI/helpers.py index 6c9e928ad..9fbf21da9 100644 --- a/SoftLayer/CLI/helpers.py +++ b/SoftLayer/CLI/helpers.py @@ -8,7 +8,7 @@ from SoftLayer.utils import NestedDict from SoftLayer.CLI.environment import CLIRunnable -from .exceptions import CLIHalt, CLIAbort, ArgumentError +from .exceptions import CLIHalt, CLIAbort, ArgumentError, InvalidInput from .formatting import ( Table, KeyValueTable, FormattedItem, SequentialOutput, confirm, no_going_back, mb_to_gb, gb, listing, blank, format_output, @@ -19,7 +19,7 @@ # Core/Misc 'CLIRunnable', 'NestedDict', 'FALSE_VALUES', 'resolve_id', # Exceptions - 'CLIAbort', 'CLIHalt', 'ArgumentError', + 'CLIAbort', 'CLIHalt', 'ArgumentError', 'InvalidInput', # Formatting 'Table', 'KeyValueTable', 'FormattedItem', 'SequentialOutput', 'valid_response', 'confirm', 'no_going_back', 'mb_to_gb', 'gb', diff --git a/SoftLayer/CLI/modules/iscsi.py b/SoftLayer/CLI/modules/iscsi.py index c4f8ceb45..c7c9d5204 100644 --- a/SoftLayer/CLI/modules/iscsi.py +++ b/SoftLayer/CLI/modules/iscsi.py @@ -4,17 +4,32 @@ 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.utils import lookup +from SoftLayer.CLI import ( + CLIRunnable, Table, no_going_back, confirm, mb_to_gb, listing, + FormattedItem) +from SoftLayer.CLI.helpers import ( + CLIAbort, ArgumentError, InvalidInput, NestedDict, blank, resolve_id, KeyValueTable, + update_with_template_args, FALSE_VALUES, export_to_template, + active_txn, transaction_status) +from SoftLayer import iSCSIManager class ListISCSI(CLIRunnable): + """ -usage: sl iscsi list [options] + usage: sl iscsi list [options] List iSCSI accounts """ @@ -48,3 +63,265 @@ def execute(self, args): 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) + + size, location = self._parse_create_args(args) + location = self._get_location_id(location) + items = iscsi.find_items(int(size)) + + iscsi.order_iscsi(items, size, location) + + 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 InvalidInput('Inavlid datacenter name: %s' + % location) + + +class CanceliSCSI(CLIRunnable): + + """ +usage: sl iscsi [--immediate] [--reason] cancel [options] + +Cancel iSCSI Storage + +options : +--immediate Cancels the iSCSI immediately (instead of on the billing + anniversary) +--reason Reason for cancellation + +Prompt Options: +-y, --really Confirm all prompt actions +""" + 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', str('No longer needed')) + + 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. + +""" + action = 'create_snapshot' + + def execute(self, args): + iscsi = iSCSIManager(self.client) + iscsi_id = resolve_id(iscsi.resolve_ids, + args.get(''), + 'iSCSI') + iscsi.create_snapshot(iscsi_id) + + +class OrderIscsiSpace(CLIRunnable): + + """ +usage: sl iscsi order_snapshot_space [--capacity=Capacity...] [options] + +Order iSCSI snapshot space. + +Required : +--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') + item_price = iscsi.find_space(int(args['--capacity'][0])) + result = iscsi.get_iscsi( + iscsi_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}], + 'quantity': 1, + 'volumeId': iscsi_id} + iscsi.Order_snapshot_space(**snapshotSpaceOrder) + + +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) + snapshot_id = resolve_id( + iscsi.resolve_ids, + args.get(''), + 'Snapshot') + result = iscsi.get_iscsi(snapshot_id,mask='mask[parentPartnerships.volumeId]') + volume_id = result['parentPartnerships'][0]['volumeId'] + 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) + print snapshots + snapshots = [NestedDict(n) for n in snapshots] + + t = Table([ + 'id', + 'createDate', + 'name', + 'description', + ]) + + 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..2f3f8260c 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -18,7 +18,7 @@ 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..c8295ec60 --- /dev/null +++ b/SoftLayer/managers/iscsi.py @@ -0,0 +1,151 @@ +import socket +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['SoftLayer_Product_Order'] + self.account = self.client['Account'] + + def find_items(self, size): + items = [] + _filter = NestedDict({}) + _filter[ + 'itemPrices'][ + 'item'][ + 'description'] = query_filter( + '~GB iSCSI SAN Storage') + _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.get('recurringFee', 0))) + iscsi_item_prices = sorted( + iscsi_item_prices, + key=lambda x: float(x['item']['capacity'])) + for price in iscsi_item_prices: + items.append(price['id']) + return items + + def find_space(self, size): + _filter = NestedDict({}) + _filter[ + 'itemPrices'][ + 'item'][ + 'description'] = query_filter( + '~iSCSI SAN Snapshot Space') + _filter['itemPrices']['item']['capacity'] = query_filter('>=%s' % size) + item_prices = self.client['Product_Package'].getItemPrices( + id=0, + mask='mask[id,item[capacity]]', + filter=_filter.to_dict()) + item_prices = sorted( + item_prices, + key=lambda x: int(x['item']['capacity'])) + if len(item_prices) == 0: + return None + return item_prices[0]['id'] + + def build_order(self, item, location): + order = { + 'complexType': + 'SoftLayer_Container_Product_Order_Network_Storage_Iscsi', + 'location': location, + 'packageId': 0, # storage package + 'prices': [{'id': item}], + 'quantity': 1 + } + return order + + def order_iscsi(self, items, size, location): + """Places an order for iSCSI volume + """ + + for item in items: + iscsi_order = self.build_order(item, location) + try: + self.product_order.verifyOrder(iscsi_order) + order = self.product_order.placeOrder(iscsi_order) + except Exception as e: + continue + return + + 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. + """ + iscsi = self.get_iscsi( + volume_id, + mask='mask[id,capacityGb,username,password,billingItem[id]]') + billingItemId = iscsi['billingItem']['id'] + self.client['SoftLayer_Billing_Item'].cancelItem( + immediate, + True, + reason, + id=billingItemId) + + def create_snapshot(self, volume_id): + """ Orders a snapshot for given volume + """ + + self.iscsi.createSnapshot('', id=volume_id) + + def Order_snapshot_space(self, **snapshotSpaceOrder): + """ Orders a snapshot space for given volume + """ + + self.product_order.verifyOrder(snapshotSpaceOrder) + order = 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: integer snapshot_id: the snapshot ID + """ + self.iscsi.restoreFromSnapshot(snapshot_id, id = volume_id)