Repository: softlayer/softlayer-python
Pull request #1249 (2020-03-26 21:50:48)

Storage feature for virtual and hardware servers
Storage feature for virtual and hardware servers https://github.com/softlayer/softlayer-python/issues/1241.


Patch:
diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py
index 89f0cb0e..d48a7e5e 100644
--- a/SoftLayer/CLI/hardware/detail.py
+++ b/SoftLayer/CLI/hardware/detail.py
@@ -27,6 +27,7 @@ def cli(env, identifier, passwords, price):
     hardware_id = helpers.resolve_id(hardware.resolve_ids, identifier, 'hardware')
     result = hardware.get_hardware(hardware_id)
     result = utils.NestedDict(result)
+    hard_drives = hardware.get_hard_drives(hardware_id)
 
     operating_system = utils.lookup(result, 'operatingSystem', 'softwareLicense', 'softwareDescription') or {}
     memory = formatting.gb(result.get('memoryCapacity', 0))
@@ -34,6 +35,15 @@ def cli(env, identifier, passwords, price):
     if utils.lookup(result, 'billingItem') != []:
         owner = utils.lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username')
 
+    table_hard_drives = formatting.Table(['Name', 'Capacity', 'Serial #'])
+    for drives in hard_drives:
+        name = drives['hardwareComponentModel']['manufacturer'] + " " + drives['hardwareComponentModel']['name']
+        capacity = str(drives['hardwareComponentModel']['hardwareGenericComponentModel']['capacity']) + " " + str(
+            drives['hardwareComponentModel']['hardwareGenericComponentModel']['units'])
+        serial = drives['serialNumber']
+
+        table_hard_drives.add_row([name, capacity, serial])
+
     table.add_row(['id', result['id']])
     table.add_row(['guid', result['globalIdentifier'] or formatting.blank()])
     table.add_row(['hostname', result['hostname']])
@@ -43,6 +53,7 @@ def cli(env, identifier, passwords, price):
     table.add_row(['datacenter', result['datacenter']['name'] or formatting.blank()])
     table.add_row(['cores', result['processorPhysicalCoreAmount']])
     table.add_row(['memory', memory])
+    table.add_row(['drives', table_hard_drives])
     table.add_row(['public_ip', result['primaryIpAddress'] or formatting.blank()])
     table.add_row(['private_ip', result['primaryBackendIpAddress'] or formatting.blank()])
     table.add_row(['ipmi_ip', result['networkManagementIpAddress'] or formatting.blank()])
diff --git a/SoftLayer/CLI/hardware/storage.py b/SoftLayer/CLI/hardware/storage.py
new file mode 100644
index 00000000..9af2f387
--- /dev/null
+++ b/SoftLayer/CLI/hardware/storage.py
@@ -0,0 +1,59 @@
+"""Get storage details for a hardware server."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+from SoftLayer.CLI import helpers
+
+
+@click.command()
+@click.argument('identifier')
+@environment.pass_env
+def cli(env, identifier):
+    """Get storage details for a hardware server."""
+
+    hardware = SoftLayer.HardwareManager(env.client)
+    hardware_id = helpers.resolve_id(hardware.resolve_ids, identifier, 'hardware')
+    iscsi_storage_data = hardware.get_storage_details(hardware_id, "ISCSI")
+    nas_storage_data = hardware.get_storage_details(hardware_id, "NAS")
+    storage_credentials = hardware.get_storage_credentials(hardware_id)
+    hard_drives = hardware.get_hard_drives(hardware_id)
+
+    table_credentials = formatting.Table(['Username', 'Password', 'IQN'], title="Block Storage Details \n iSCSI")
+    if storage_credentials:
+        table_credentials.add_row([storage_credentials['credential']['username'],
+                                   storage_credentials['credential']['password'],
+                                   storage_credentials['name']])
+
+    table_iscsi = formatting.Table(['LUN name', 'capacity', 'Target address', 'Location', 'Notes'])
+    for iscsi in iscsi_storage_data:
+        table_iscsi.add_row([iscsi['username'], iscsi['capacityGb'],
+                             iscsi['serviceResourceBackendIpAddress'],
+                             iscsi['allowedHardware'][0]['datacenter']['longName'],
+                             iscsi.get('notes', None)])
+
+    table_nas = formatting.Table(['Volume name', 'capacity', 'Host Name', 'Location', 'Notes'],
+                                 title="File Storage Details")
+    for nas in nas_storage_data:
+        table_nas.add_row([nas['username'], nas['capacityGb'],
+                           nas['serviceResourceBackendIpAddress'],
+                           nas['allowedHardware'][0]['datacenter']['longName'],
+                           nas.get('notes', None)])
+
+    table_hard_drives = formatting.Table(['Type', 'Name', 'Capacity', 'Serial #'], title="Other storage details")
+    for drives in hard_drives:
+        type_drive = drives['hardwareComponentModel']['hardwareGenericComponentModel']['hardwareComponentType']['type']
+        name = drives['hardwareComponentModel']['manufacturer'] + " " + drives['hardwareComponentModel']['name']
+        capacity = str(drives['hardwareComponentModel']['hardwareGenericComponentModel']['capacity']) + " " + str(
+            drives['hardwareComponentModel']['hardwareGenericComponentModel']['units'])
+        serial = drives['serialNumber']
+
+        table_hard_drives.add_row([type_drive, name, capacity, serial])
+
+    env.fout(table_credentials)
+    env.fout(table_iscsi)
+    env.fout(table_nas)
+    env.fout(table_hard_drives)
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index 8f302f67..452ee0f9 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -40,6 +40,7 @@
     ('virtual:ready', 'SoftLayer.CLI.virt.ready:cli'),
     ('virtual:reboot', 'SoftLayer.CLI.virt.power:reboot'),
     ('virtual:reload', 'SoftLayer.CLI.virt.reload:cli'),
+    ('virtual:storage', 'SoftLayer.CLI.virt.storage:cli'),
     ('virtual:upgrade', 'SoftLayer.CLI.virt.upgrade:cli'),
     ('virtual:usage', 'SoftLayer.CLI.virt.usage:cli'),
     ('virtual:credentials', 'SoftLayer.CLI.virt.credentials:cli'),
@@ -248,6 +249,7 @@
     ('hardware:ready', 'SoftLayer.CLI.hardware.ready:cli'),
     ('hardware:toggle-ipmi', 'SoftLayer.CLI.hardware.toggle_ipmi:cli'),
     ('hardware:dns-sync', 'SoftLayer.CLI.hardware.dns:cli'),
+    ('hardware:storage', 'SoftLayer.CLI.hardware.storage:cli'),
 
     ('securitygroup', 'SoftLayer.CLI.securitygroup'),
     ('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'),
diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py
index 131117df..d17e489f 100644
--- a/SoftLayer/CLI/virt/detail.py
+++ b/SoftLayer/CLI/virt/detail.py
@@ -32,6 +32,13 @@ def cli(env, identifier, passwords=False, price=False):
     vs_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS')
     result = vsi.get_instance(vs_id)
     result = utils.NestedDict(result)
+    local_disks = vsi.get_local_disks(vs_id)
+
+    table_local_disks = formatting.Table(['Type', 'Name', 'Capacity'])
+    for disks in local_disks:
+        if 'diskImage' in disks:
+            table_local_disks.add_row([get_local_type(disks), disks['mountType'],
+                                       str(disks['diskImage']['capacity']) + " " + str(disks['diskImage']['units'])])
 
     table.add_row(['id', result['id']])
     table.add_row(['guid', result['globalIdentifier']])
@@ -57,6 +64,7 @@ def cli(env, identifier, passwords=False, price=False):
     table.add_row(['os_version', operating_system.get('version', '-')])
     table.add_row(['cores', result['maxCpu']])
     table.add_row(['memory', formatting.mb_to_gb(result['maxMemory'])])
+    table.add_row(['drives', table_local_disks])
     table.add_row(['public_ip', result.get('primaryIpAddress', '-')])
     table.add_row(['private_ip', result.get('primaryBackendIpAddress', '-')])
     table.add_row(['private_only', result['privateNetworkOnlyFlag']])
@@ -192,3 +200,15 @@ def _get_security_table(result):
         return secgroup_table
     else:
         return None
+
+
+def get_local_type(disks):
+    """Returns the virtual server local disk type.
+
+    :param disks: virtual serve local disks.
+    """
+    disk_type = 'System'
+    if 'SWAP' in disks['diskImage']['description']:
+        disk_type = 'Swap'
+
+    return disk_type
diff --git a/SoftLayer/CLI/virt/storage.py b/SoftLayer/CLI/virt/storage.py
new file mode 100644
index 00000000..8d1b6585
--- /dev/null
+++ b/SoftLayer/CLI/virt/storage.py
@@ -0,0 +1,73 @@
+"""Get storage details for a virtual server."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+from SoftLayer.CLI import helpers
+
+
+@click.command()
+@click.argument('identifier')
+@environment.pass_env
+def cli(env, identifier):
+    """Get storage details for a virtual server."""
+
+    vsi = SoftLayer.VSManager(env.client)
+    vsi_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS')
+    iscsi_storage_data = vsi.get_storage_details(vsi_id, "ISCSI")
+    nas_storage_data = vsi.get_storage_details(vsi_id, "NAS")
+    storage_credentials = vsi.get_storage_credentials(vsi_id)
+    portable_storage = vsi.get_portable_storage(vsi_id)
+    local_disks = vsi.get_local_disks(vsi_id)
+
+    table_credentials = formatting.Table(['Username', 'Password', 'IQN'], title="Block Storage Details \n iSCSI")
+    if storage_credentials:
+        table_credentials.add_row([storage_credentials['credential']['username'],
+                                   storage_credentials['credential']['password'],
+                                   storage_credentials['name']])
+
+    table_iscsi = formatting.Table(['LUN name', 'capacity', 'Target address', 'Location', 'Notes'])
+    for iscsi in iscsi_storage_data:
+        table_iscsi.add_row([iscsi['username'], iscsi['capacityGb'],
+                             iscsi['serviceResourceBackendIpAddress'],
+                             iscsi['allowedVirtualGuests'][0]['datacenter']['longName'],
+                             iscsi.get('notes', None)])
+
+    table_portable = formatting.Table(['Description', 'Capacity'], title="Portable Storage")
+    for portable in portable_storage:
+        table_portable.add_row([portable.get('description', None), portable.get('capacity', None)])
+
+    table_nas = formatting.Table(['Volume name', 'capacity', 'Host Name', 'Location', 'Notes'],
+                                 title="File Storage Details")
+    for nas in nas_storage_data:
+        table_nas.add_row([nas['username'], nas['capacityGb'],
+                           nas['serviceResourceBackendIpAddress'],
+                           nas['allowedVirtualGuests'][0]['datacenter']['longName'],
+                           nas.get('notes', None)])
+
+    table_local_disks = formatting.Table(['Type', 'Name', 'Capacity'], title="Other storage details")
+    for disks in local_disks:
+        if 'diskImage' in disks:
+            table_local_disks.add_row([get_local_type(disks), disks['mountType'],
+                                       str(disks['diskImage']['capacity']) + " " + str(disks['diskImage']['units'])])
+
+    env.fout(table_credentials)
+    env.fout(table_iscsi)
+    env.fout(table_portable)
+    env.fout(table_nas)
+    env.fout(table_local_disks)
+
+
+def get_local_type(disks):
+    """Returns the virtual server local disk type.
+
+    :param disks: virtual serve local disks.
+    """
+    disk_type = 'System'
+    if 'SWAP' in disks['diskImage']['description']:
+        disk_type = 'Swap'
+
+    return disk_type
diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py
index 9524c61c..95692ef0 100644
--- a/SoftLayer/fixtures/SoftLayer_Account.py
+++ b/SoftLayer/fixtures/SoftLayer_Account.py
@@ -853,6 +853,22 @@
     }
 ]
 
+
+getPortableStorageVolumes = [
+    {
+        "capacity": 200,
+        "createDate": "2018-10-06T04:27:59-06:00",
+        "description": "Disk 2",
+        "id": 11111,
+        "modifyDate": "",
+        "name": "Disk 2",
+        "parentId": "",
+        "storageRepositoryId": 22222,
+        "typeId": 241,
+        "units": "GB",
+        "uuid": "fd477feb-bf32-408e-882f-02540gghgh111"
+    }
+]
 getAllTopLevelBillingItems = [
     {
         "allowCancellationFlag": 1,
diff --git a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
index 47e9a1bc..e9028875 100644
--- a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
+++ b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
@@ -149,3 +149,96 @@
 ]
 
 getMetricTrackingObjectId = 1000
+
+getAttachedNetworkStorages = [
+    {
+        "accountId": 11111,
+        "capacityGb": 20,
+        "createDate": "2018-04-05T05:15:49-06:00",
+        "id": 22222,
+        "nasType": "NAS",
+        "serviceProviderId": 1,
+        "storageTypeId": "13",
+        "username": "SL02SEV311111_11",
+        "allowedHardware": [
+            {
+                "id": 12345,
+                "datacenter": {
+                    "id": 449506,
+                    "longName": "Frankfurt 2",
+                    "name": "fra02",
+                    "statusId": 2
+                }
+            }
+        ],
+        "serviceResourceBackendIpAddress": "fsn-fra0201a-fz.service.softlayer.com",
+        "serviceResourceName": "Storage Type 02 File Aggregate stfm-fra0201a"
+    },
+    {
+        "accountId": 11111,
+        "capacityGb": 12000,
+        "createDate": "2018-01-28T04:57:30-06:00",
+        "id": 3777111,
+        "nasType": "ISCSI",
+        "notes": "BlockStorage12T",
+        "password": "",
+        "serviceProviderId": 1,
+        "storageTypeId": "7",
+        "username": "SL02SEL32222-9",
+        "allowedHardware": [
+            {
+                "id": 629222,
+                "datacenter": {
+                    "id": 449506,
+                    "longName": "Frankfurt 2",
+                    "name": "fra02",
+                    "statusId": 2
+                }
+            }
+        ],
+        "serviceResourceBackendIpAddress": "10.31.95.152",
+        "serviceResourceName": "Storage Type 02 Block Aggregate stbm-fra0201a"
+    }
+]
+
+getAllowedHost = {
+    "accountId": 11111,
+    "credentialId": 22222,
+    "id": 33333,
+    "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
+    "resourceTableId": 6291111,
+    "resourceTableName": "VIRTUAL_GUEST",
+    "credential": {
+        "accountId": "11111",
+        "createDate": "2020-03-20T13:35:47-06:00",
+        "id": 44444,
+        "nasCredentialTypeId": 2,
+        "password": "SjFDCpHrjskfj",
+        "username": "SL02SU11111-V62941551"
+    }
+}
+
+getHardDrives = [
+    {
+        "id": 11111,
+        "serialNumber": "z1w4sdf",
+        "serviceProviderId": 1,
+        "hardwareComponentModel": {
+            "capacity": "1000",
+            "description": "SATAIII:2000:8300:Constellation",
+            "id": 111,
+            "manufacturer": "Seagate",
+            "name": "Constellation ES",
+            "hardwareGenericComponentModel": {
+                "capacity": "1000",
+                "units": "GB",
+                "hardwareComponentType": {
+                    "id": 1,
+                    "keyName": "HARD_DRIVE",
+                    "type": "Hard Drive",
+                    "typeParentId": 5
+                }
+            }
+        }
+    }
+]
diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage_Iscsi.py b/SoftLayer/fixtures/SoftLayer_Network_Storage_Iscsi.py
new file mode 100644
index 00000000..04c22847
--- /dev/null
+++ b/SoftLayer/fixtures/SoftLayer_Network_Storage_Iscsi.py
@@ -0,0 +1,23 @@
+getObject = {
+    "id": 11111,
+    "allowedVirtualGuests": [
+        {
+            "id": 22222,
+            "allowedHost": {
+                "accountId": 12345,
+                "id": 18311111,
+                "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
+                "resourceTableId": 6222222,
+                "resourceTableName": "VIRTUAL_GUEST",
+                "credential": {
+                    "accountId": "12345",
+                    "createDate": "2020-03-20T13:35:47-06:00",
+                    "id": 1522222,
+                    "nasCredentialTypeId": 2,
+                    "password": "SjFDCpHrmKewos",
+                    "username": "SL02SU322222-V62922222"
+                }
+            }
+        }
+    ]
+}
diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
index 270ecf2a..c42963c8 100644
--- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
+++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
@@ -670,3 +670,100 @@
         }
     }
 ]
+
+getAttachedNetworkStorages = [
+    {
+        "accountId": 11111,
+        "capacityGb": 20,
+        "createDate": "2018-04-05T05:15:49-06:00",
+        "id": 22222,
+        "nasType": "NAS",
+        "serviceProviderId": 1,
+        "storageTypeId": "13",
+        "username": "SL02SEV311111_11",
+        "allowedVirtualGuests": [
+            {
+                "id": 12345,
+                "datacenter": {
+                    "id": 449506,
+                    "longName": "Frankfurt 2",
+                    "name": "fra02",
+                    "statusId": 2
+                }
+            }
+        ],
+        "serviceResourceBackendIpAddress": "fsn-fra0201a-fz.service.softlayer.com",
+        "serviceResourceName": "Storage Type 02 File Aggregate stfm-fra0201a"
+    },
+    {
+        "accountId": 11111,
+        "capacityGb": 12000,
+        "createDate": "2018-01-28T04:57:30-06:00",
+        "id": 3777111,
+        "nasType": "ISCSI",
+        "notes": "BlockStorage12T",
+        "password": "",
+        "serviceProviderId": 1,
+        "storageTypeId": "7",
+        "username": "SL02SEL32222-9",
+        "allowedVirtualGuests": [
+            {
+                "id": 629222,
+                "datacenter": {
+                    "id": 449506,
+                    "longName": "Frankfurt 2",
+                    "name": "fra02",
+                    "statusId": 2
+                }
+            }
+        ],
+        "serviceResourceBackendIpAddress": "10.31.95.152",
+        "serviceResourceName": "Storage Type 02 Block Aggregate stbm-fra0201a"
+    }
+]
+
+getAllowedHost = {
+    "accountId": 11111,
+    "credentialId": 22222,
+    "id": 33333,
+    "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
+    "resourceTableId": 6291111,
+    "resourceTableName": "VIRTUAL_GUEST",
+    "credential": {
+        "accountId": "11111",
+        "createDate": "2020-03-20T13:35:47-06:00",
+        "id": 44444,
+        "nasCredentialTypeId": 2,
+        "password": "SjFDCpHrjskfj",
+        "username": "SL02SU11111-V62941551"
+    }
+}
+
+getBlockDevices = [
+    {
+        "createDate": "2018-10-06T04:27:35-06:00",
+        "device": "0",
+        "id": 11111,
+        "mountType": "Disk",
+        "diskImage": {
+            "capacity": 100,
+            "description": "adns.vmware.com",
+            "id": 72222,
+            "name": "adns.vmware.com",
+            "units": "GB",
+        }
+    },
+    {
+        "device": "1",
+        "id": 22222,
+        "mountType": "Disk",
+        "statusId": 1,
+        "diskImage": {
+            "capacity": 2,
+            "description": "6211111-SWAP",
+            "id": 33333,
+            "name": "6211111-SWAP",
+            "units": "GB",
+        }
+    }
+]
diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py
index fa4ee42a..99ab45b9 100644
--- a/SoftLayer/managers/hardware.py
+++ b/SoftLayer/managers/hardware.py
@@ -702,6 +702,31 @@ def get_bandwidth_allocation(self, instance_id):
             return {'allotment': allotment.get('allocation'), 'usage': usage}
         return {'allotment': allotment, 'usage': usage}
 
+    def get_storage_details(self, instance_id, nas_type):
+        """Returns the hardware server attached network storage.
+
+        :param int instance_id: Id of the hardware server
+        :param nas_type: storage type.
+        """
+        mask = 'mask[id,username,capacityGb,notes,serviceResourceBackendIpAddress,' \
+               'allowedHardware[id,datacenter]]'
+        return self.hardware.getAttachedNetworkStorages(nas_type, mask=mask, id=instance_id)
+
+    def get_storage_credentials(self, instance_id):
+        """Returns the hardware server storage credentials.
+
+        :param int instance_id: Id of the hardware server
+        """
+        mask = 'mask[credential]'
+        return self.hardware.getAllowedHost(mask=mask, id=instance_id)
+
+    def get_hard_drives(self, instance_id):
+        """Returns the hardware server hard drives.
+
+        :param int instance_id: Id of the hardware server
+        """
+        return self.hardware.getHardDrives(id=instance_id)
+
 
 def _get_extra_price_id(items, key_name, hourly, location):
     """Returns a price id attached to item with the given key_name."""
diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py
index b1391bc3..e63d7a80 100644
--- a/SoftLayer/managers/vs.py
+++ b/SoftLayer/managers/vs.py
@@ -51,6 +51,7 @@ def __init__(self, client, ordering_manager=None):
         self.account = client['Account']
         self.guest = client['Virtual_Guest']
         self.package_svc = client['Product_Package']
+        self.storage_iscsi = client['SoftLayer_Network_Storage_Iscsi']
         self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname]
         if ordering_manager is None:
             self.ordering_manager = ordering.OrderingManager(client)
@@ -1123,3 +1124,37 @@ def _get_price_id_for_upgrade(self, package_items, option, value, public=True):
                             return price['id']
                     else:
                         return price['id']
+
+    def get_storage_details(self, instance_id, nas_type):
+        """Returns the virtual server attached network storage.
+
+        :param int instance_id: Id of the virtual server
+        :param nas_type: storage type.
+        """
+        mask = 'mask[id,username,capacityGb,notes,serviceResourceBackendIpAddress,' \
+               'allowedVirtualGuests[id,datacenter]]'
+        return self.guest.getAttachedNetworkStorages(nas_type, mask=mask, id=instance_id)
+
+    def get_storage_credentials(self, instance_id):
+        """Returns the virtual server storage credentials.
+
+        :param int instance_id: Id of the virtual server
+        """
+        mask = 'mask[credential]'
+        return self.guest.getAllowedHost(mask=mask, id=instance_id)
+
+    def get_portable_storage(self, instance_id):
+        """Returns the virtual server portable storage.
+
+        :param int instance_id: Id of the virtual server
+        """
+        object_filter = {"portableStorageVolumes": {"blockDevices": {"guest": {"id": {"operation": instance_id}}}}}
+        return self.account.getPortableStorageVolumes(filter=object_filter)
+
+    def get_local_disks(self, instance_id):
+        """Returns the virtual server local disks.
+
+        :param int instance_id: Id of the virtual server
+        """
+        mask = 'mask[diskImage]'
+        return self.guest.getBlockDevices(mask=mask, id=instance_id)
diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst
index 514f94b1..cd97c9d0 100644
--- a/docs/cli/hardware.rst
+++ b/docs/cli/hardware.rst
@@ -109,3 +109,7 @@ This function updates the firmware of a server. If already at the latest version
 .. click:: SoftLayer.CLI.hardware.dns:cli
    :prog: hardware dns-sync
    :show-nested:
+
+.. click:: SoftLayer.CLI.hardware.storage:cli
+   :prog: hw storage
+   :show-nested:
diff --git a/docs/cli/vs.rst b/docs/cli/vs.rst
index 37daed98..49b99e09 100644
--- a/docs/cli/vs.rst
+++ b/docs/cli/vs.rst
@@ -245,16 +245,17 @@ If no timezone is specified, IMS local time (CST) will be assumed, which might n
    :prog: virtual usage
    :show-nested:
 
+.. click:: SoftLayer.CLI.virt.storage:cli
+   :prog: virtual storage
+
 .. click:: SoftLayer.CLI.virt.billing:cli
-   :prog: vs billing
+   :prog: virtual billing
    :show-nested:
 
-
 .. click:: SoftLayer.CLI.virt.detail:cli
    :prog: virtual detail
    :show-nested:
 
-
 .. click:: SoftLayer.CLI.virt.reload:cli
    :prog: virtual reload
    :show-nested:


Test patch:
diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py
index 588ed4d1..44e2bc81 100644
--- a/tests/CLI/modules/server_tests.py
+++ b/tests/CLI/modules/server_tests.py
@@ -139,6 +139,39 @@ def test_detail_empty_allotment(self):
             '-',
         )
 
+    def test_detail_drives(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getHardDrives')
+        mock.return_value = [
+            {
+                "id": 11111,
+                "serialNumber": "z1w4sdf",
+                "hardwareComponentModel": {
+                    "capacity": "1000",
+                    "description": "SATAIII:2000:8300:Constellation",
+                    "id": 111,
+                    "manufacturer": "Seagate",
+                    "name": "Constellation ES",
+                    "hardwareGenericComponentModel": {
+                        "capacity": "1000",
+                        "units": "GB",
+                        "hardwareComponentType": {
+                            "id": 1,
+                            "keyName": "HARD_DRIVE",
+                            "type": "Hard Drive",
+                            "typeParentId": 5
+                        }
+                    }
+                }
+            }
+        ]
+        result = self.run_command(['server', 'detail', '100'])
+
+        self.assert_no_fail(result)
+        output = json.loads(result.output)
+        self.assertEqual(output['drives'][0]['Capacity'], '1000 GB')
+        self.assertEqual(output['drives'][0]['Name'], 'Seagate Constellation ES')
+        self.assertEqual(output['drives'][0]['Serial #'], 'z1w4sdf')
+
     def test_list_servers(self):
         result = self.run_command(['server', 'list', '--tag=openstack'])
 
@@ -827,6 +860,12 @@ def test_dns_sync_misc_exception(self, confirm_mock):
         self.assertEqual(result.exit_code, 2)
         self.assertIsInstance(result.exception, exceptions.CLIAbort)
 
+    def test_hardware_storage(self):
+        result = self.run_command(
+            ['hw', 'storage', '100'])
+
+        self.assert_no_fail(result)
+
     def test_billing(self):
         result = self.run_command(['hw', 'billing', '123456'])
         billing_json = {
diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py
index 9fbe699c..c6188338 100644
--- a/tests/CLI/modules/vs/vs_tests.py
+++ b/tests/CLI/modules/vs/vs_tests.py
@@ -216,6 +216,56 @@ def test_detail_vs_empty_allotment(self):
             '-',
         )
 
+    def test_detail_drives_system(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getBlockDevices')
+        mock.return_value = [
+            {
+                "createDate": "2018-10-06T04:27:35-06:00",
+                "device": "0",
+                "id": 11111,
+                "mountType": "Disk",
+                "diskImage": {
+                    "capacity": 100,
+                    "description": "adns.vmware.com",
+                    "id": 72222,
+                    "name": "adns.vmware.com",
+                    "units": "GB",
+                }
+            }
+        ]
+        result = self.run_command(['vs', 'detail', '100'])
+
+        self.assert_no_fail(result)
+        output = json.loads(result.output)
+        self.assertEqual(output['drives'][0]['Capacity'], '100 GB')
+        self.assertEqual(output['drives'][0]['Name'], 'Disk')
+        self.assertEqual(output['drives'][0]['Type'], 'System')
+
+    def test_detail_drives_swap(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getBlockDevices')
+        mock.return_value = [
+            {
+                "device": "1",
+                "id": 22222,
+                "mountType": "Disk",
+                "statusId": 1,
+                "diskImage": {
+                    "capacity": 2,
+                    "description": "6211111-SWAP",
+                    "id": 33333,
+                    "name": "6211111-SWAP",
+                    "units": "GB",
+                }
+            }
+        ]
+        result = self.run_command(['vs', 'detail', '100'])
+
+        self.assert_no_fail(result)
+        output = json.loads(result.output)
+        self.assertEqual(output['drives'][0]['Capacity'], '2 GB')
+        self.assertEqual(output['drives'][0]['Name'], 'Disk')
+        self.assertEqual(output['drives'][0]['Type'], 'Swap')
+
     def test_detail_vs_dedicated_host_not_found(self):
         ex = SoftLayerAPIError('SoftLayer_Exception', 'Not found')
         mock = self.set_mock('SoftLayer_Virtual_DedicatedHost', 'getObject')
@@ -741,6 +791,12 @@ def test_bandwidth_vs_quite(self):
         self.assertEqual(output_summary[2]['Max GB'], 0.1172)
         self.assertEqual(output_summary[3]['Sum GB'], 0.0009)
 
+    def test_vs_storage(self):
+        result = self.run_command(
+            ['vs', 'storage', '100'])
+
+        self.assert_no_fail(result)
+
     def test_billing(self):
         result = self.run_command(['vs', 'billing', '123456'])
         vir_billing = {
diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py
index 5710dd0a..9ac63c22 100644
--- a/tests/managers/hardware_tests.py
+++ b/tests/managers/hardware_tests.py
@@ -9,7 +9,6 @@
 import mock
 
 import SoftLayer
-
 from SoftLayer import fixtures
 from SoftLayer import managers
 from SoftLayer import testing
@@ -467,6 +466,166 @@ def test_get_bandwidth_allocation_no_allotment(self):
 
         self.assertEqual(None, result['allotment'])
 
+    def test_get_storage_iscsi_details(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getAttachedNetworkStorages')
+        mock.return_value = [
+            {
+                "accountId": 11111,
+                "capacityGb": 12000,
+                "id": 3777123,
+                "nasType": "ISCSI",
+                "username": "SL02SEL31111-9",
+            }
+        ]
+
+        result = self.hardware.get_storage_details(1234, 'ISCSI')
+
+        self.assertEqual([{
+            "accountId": 11111,
+            "capacityGb": 12000,
+            "id": 3777123,
+            "nasType": "ISCSI",
+            "username": "SL02SEL31111-9",
+        }], result)
+
+    def test_get_storage_iscsi_empty_details(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getAttachedNetworkStorages')
+        mock.return_value = []
+
+        result = self.hardware.get_storage_details(1234, 'ISCSI')
+
+        self.assertEqual([], result)
+
+    def test_get_storage_nas_details(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getAttachedNetworkStorages')
+        mock.return_value = [
+            {
+                "accountId": 11111,
+                "capacityGb": 12000,
+                "id": 3777111,
+                "nasType": "NAS",
+                "username": "SL02SEL32222-9",
+            }
+        ]
+
+        result = self.hardware.get_storage_details(1234, 'NAS')
+
+        self.assertEqual([{
+            "accountId": 11111,
+            "capacityGb": 12000,
+            "id": 3777111,
+            "nasType": "NAS",
+            "username": "SL02SEL32222-9",
+        }], result)
+
+    def test_get_storage_nas_empty_details(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getAttachedNetworkStorages')
+        mock.return_value = []
+
+        result = self.hardware.get_storage_details(1234, 'NAS')
+
+        self.assertEqual([], result)
+
+    def test_get_storage_credentials(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getAllowedHost')
+        mock.return_value = {
+            "accountId": 11111,
+            "id": 33333,
+            "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
+            "resourceTableName": "HARDWARE",
+            "credential": {
+                "accountId": "11111",
+                "id": 44444,
+                "password": "SjFDCpHrjskfj",
+                "username": "SL02SU11111-V62941551"
+            }
+        }
+
+        result = self.hardware.get_storage_credentials(1234)
+
+        self.assertEqual({
+            "accountId": 11111,
+            "id": 33333,
+            "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
+            "resourceTableName": "HARDWARE",
+            "credential": {
+                "accountId": "11111",
+                "id": 44444,
+                "password": "SjFDCpHrjskfj",
+                "username": "SL02SU11111-V62941551"
+            }
+        }, result)
+
+    def test_get_none_storage_credentials(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getAllowedHost')
+        mock.return_value = None
+
+        result = self.hardware.get_storage_credentials(1234)
+
+        self.assertEqual(None, result)
+
+    def test_get_hard_drives(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getHardDrives')
+        mock.return_value = [
+            {
+                "id": 11111,
+                "serialNumber": "z1w4sdf",
+                "serviceProviderId": 1,
+                "hardwareComponentModel": {
+                    "capacity": "1000",
+                    "description": "SATAIII:2000:8300:Constellation",
+                    "id": 111,
+                    "manufacturer": "Seagate",
+                    "name": "Constellation ES",
+                    "hardwareGenericComponentModel": {
+                        "capacity": "1000",
+                        "units": "GB",
+                        "hardwareComponentType": {
+                            "id": 1,
+                            "keyName": "HARD_DRIVE",
+                            "type": "Hard Drive",
+                            "typeParentId": 5
+                        }
+                    }
+                }
+            }
+        ]
+
+        result = self.hardware.get_hard_drives(1234)
+
+        self.assertEqual([
+            {
+                "id": 11111,
+                "serialNumber": "z1w4sdf",
+                "serviceProviderId": 1,
+                "hardwareComponentModel": {
+                    "capacity": "1000",
+                    "description": "SATAIII:2000:8300:Constellation",
+                    "id": 111,
+                    "manufacturer": "Seagate",
+                    "name": "Constellation ES",
+                    "hardwareGenericComponentModel": {
+                        "capacity": "1000",
+                        "units": "GB",
+                        "hardwareComponentType": {
+                            "id": 1,
+                            "keyName": "HARD_DRIVE",
+                            "type": "Hard Drive",
+                            "typeParentId": 5
+                        }
+                    }
+                }
+            }
+        ], result)
+
+    def test_get_hard_drive_empty(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getHardDrives')
+        mock.return_value = []
+
+        result = self.hardware.get_hard_drives(1234)
+
+        self.assertEqual([], result)
+
 
 class HardwareHelperTests(testing.TestCase):
     def test_get_extra_price_id_no_items(self):
diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py
index 47fcaf20..40fb3063 100644
--- a/tests/managers/vs/vs_tests.py
+++ b/tests/managers/vs/vs_tests.py
@@ -925,3 +925,210 @@ def test_get_bandwidth_allocation_with_allotment(self):
         result = self.vs.get_bandwidth_allocation(1234)
 
         self.assertEqual(2000, int(result['allotment']['amount']))
+
+    def test_get_storage_iscsi_details(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getAttachedNetworkStorages')
+        mock.return_value = [
+            {
+                "accountId": 11111,
+                "capacityGb": 12000,
+                "id": 3777123,
+                "nasType": "ISCSI",
+                "username": "SL02SEL31111-9",
+            }
+        ]
+
+        result = self.vs.get_storage_details(1234, 'ISCSI')
+
+        self.assertEqual([{
+            "accountId": 11111,
+            "capacityGb": 12000,
+            "id": 3777123,
+            "nasType": "ISCSI",
+            "username": "SL02SEL31111-9",
+        }], result)
+
+    def test_get_storage_iscsi_empty_details(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getAttachedNetworkStorages')
+        mock.return_value = []
+
+        result = self.vs.get_storage_details(1234, 'ISCSI')
+
+        self.assertEqual([], result)
+
+    def test_get_storage_nas_details(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getAttachedNetworkStorages')
+        mock.return_value = [
+            {
+                "accountId": 11111,
+                "capacityGb": 12000,
+                "id": 3777111,
+                "nasType": "NAS",
+                "username": "SL02SEL32222-9",
+            }
+        ]
+
+        result = self.vs.get_storage_details(1234, 'NAS')
+
+        self.assertEqual([{
+            "accountId": 11111,
+            "capacityGb": 12000,
+            "id": 3777111,
+            "nasType": "NAS",
+            "username": "SL02SEL32222-9",
+        }], result)
+
+    def test_get_storage_nas_empty_details(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getAttachedNetworkStorages')
+        mock.return_value = []
+
+        result = self.vs.get_storage_details(1234, 'NAS')
+
+        self.assertEqual([], result)
+
+    def test_get_storage_credentials(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getAllowedHost')
+        mock.return_value = {
+            "accountId": 11111,
+            "id": 33333,
+            "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
+            "resourceTableName": "VIRTUAL_GUEST",
+            "credential": {
+                "accountId": "11111",
+                "id": 44444,
+                "password": "SjFDCpHrjskfj",
+                "username": "SL02SU11111-V62941551"
+            }
+        }
+
+        result = self.vs.get_storage_credentials(1234)
+
+        self.assertEqual({
+            "accountId": 11111,
+            "id": 33333,
+            "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
+            "resourceTableName": "VIRTUAL_GUEST",
+            "credential": {
+                "accountId": "11111",
+                "id": 44444,
+                "password": "SjFDCpHrjskfj",
+                "username": "SL02SU11111-V62941551"
+            }
+        }, result)
+
+    def test_get_none_storage_credentials(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getAllowedHost')
+        mock.return_value = None
+
+        result = self.vs.get_storage_credentials(1234)
+
+        self.assertEqual(None, result)
+
+    def test_get_portable_storage(self):
+        result = self.vs.get_portable_storage(1234)
+        self.assert_called_with('SoftLayer_Account',
+                                'getPortableStorageVolumes')
+
+        self.assertEqual([
+            {
+                "capacity": 200,
+                "createDate": "2018-10-06T04:27:59-06:00",
+                "description": "Disk 2",
+                "id": 11111,
+                "modifyDate": "",
+                "name": "Disk 2",
+                "parentId": "",
+                "storageRepositoryId": 22222,
+                "typeId": 241,
+                "units": "GB",
+                "uuid": "fd477feb-bf32-408e-882f-02540gghgh111"
+            }
+        ], result)
+
+    def test_get_portable_storage_empty(self):
+        mock = self.set_mock('SoftLayer_Account', 'getPortableStorageVolumes')
+        mock.return_value = []
+
+        result = self.vs.get_portable_storage(1234)
+
+        self.assertEqual([], result)
+
+    def test_get_local_disks_system(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getBlockDevices')
+        mock.return_value = [
+            {
+                "createDate": "2018-10-06T04:27:35-06:00",
+                "device": "0",
+                "id": 11111,
+                "mountType": "Disk",
+                "diskImage": {
+                    "capacity": 100,
+                    "description": "adns.vmware.com",
+                    "id": 72222,
+                    "name": "adns.vmware.com",
+                    "units": "GB",
+                }
+            }
+        ]
+
+        result = self.vs.get_local_disks(1234)
+
+        self.assertEqual([
+            {
+                "createDate": "2018-10-06T04:27:35-06:00",
+                "device": "0",
+                "id": 11111,
+                "mountType": "Disk",
+                "diskImage": {
+                    "capacity": 100,
+                    "description": "adns.vmware.com",
+                    "id": 72222,
+                    "name": "adns.vmware.com",
+                    "units": "GB",
+                }
+            }
+        ], result)
+
+    def test_get_local_disks_empty(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getBlockDevices')
+        mock.return_value = []
+
+        result = self.vs.get_local_disks(1234)
+
+        self.assertEqual([], result)
+
+    def test_get_local_disks_swap(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getBlockDevices')
+        mock.return_value = [
+            {
+                "device": "1",
+                "id": 22222,
+                "mountType": "Disk",
+                "statusId": 1,
+                "diskImage": {
+                    "capacity": 2,
+                    "description": "6211111-SWAP",
+                    "id": 33333,
+                    "name": "6211111-SWAP",
+                    "units": "GB",
+                }
+            }
+        ]
+
+        result = self.vs.get_local_disks(1234)
+
+        self.assertEqual([
+            {
+                "device": "1",
+                "id": 22222,
+                "mountType": "Disk",
+                "statusId": 1,
+                "diskImage": {
+                    "capacity": 2,
+                    "description": "6211111-SWAP",
+                    "id": 33333,
+                    "name": "6211111-SWAP",
+                    "units": "GB",
+                }
+            }
+        ], result)