Repository: softlayer/softlayer-python

Issue:
Add Vlan trunks to hardware view page
Similar to:

![image](https://github.com/softlayer/softlayer-python/assets/7408017/a0fc0ca4-6eac-4cb7-a6fa-789ac5e4361a)


Related to #1719 


Also, improve performance of get_hardware() by adding some multithreading.

Discussion:

Patch:
diff --git a/.secrets.baseline b/.secrets.baseline
index cafb8b61..c3027509 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,7 +3,7 @@
     "files": "^.secrets.baseline$",
     "lines": null
   },
-  "generated_at": "2023-09-27T14:21:34Z",
+  "generated_at": "2023-10-13T20:28:05Z",
   "plugins_used": [
     {
       "name": "AWSKeyDetector"
@@ -238,7 +238,7 @@
         "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 259,
+        "line_number": 274,
         "type": "Secret Keyword",
         "verified_result": null
       }
@@ -529,23 +529,22 @@
         "verified_result": null
       }
     ],
-    "tests/CLI/modules/securitygroup_tests.py": [
+    "tests/CLI/modules/hardware/hardware_basic_tests.py": [
       {
-        "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc",
-        "is_secret": false,
+        "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee",
         "is_verified": false,
-        "line_number": 339,
-        "type": "Hex High Entropy String",
+        "line_number": 57,
+        "type": "Secret Keyword",
         "verified_result": null
       }
     ],
-    "tests/CLI/modules/server_tests.py": [
+    "tests/CLI/modules/securitygroup_tests.py": [
       {
-        "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee",
+        "hashed_secret": "bc553d847e40dd6f3f63638f16f57b28ce1425cc",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 57,
-        "type": "Secret Keyword",
+        "line_number": 339,
+        "type": "Hex High Entropy String",
         "verified_result": null
       }
     ],
@@ -594,7 +593,7 @@
         "hashed_secret": "fb5f2f1b65d1f2bc130ce9d5729b38d12f2b444e",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 737,
+        "line_number": 673,
         "type": "Secret Keyword",
         "verified_result": null
       }
diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py
index 17731fe2..9c32318d 100644
--- a/SoftLayer/CLI/formatting.py
+++ b/SoftLayer/CLI/formatting.py
@@ -318,6 +318,22 @@ def __init__(self, columns, title=None, align=None):
         self.align = align or {}
         self.sortby = None
         self.title = title
+        # Used to print a message if the table is empty
+        self.empty_message = None
+
+    def __bool__(self):
+        """Useful for seeing if the table has any rows"""
+        return len(self.rows) > 0
+
+    def set_empty_message(self, message):
+        """Sets the empty message for this table for env.fout
+
+        Set this message if you want to print a message instead of a table to the user
+        but still want the json output to print an empty list `[]`
+
+        :param message str: Message to print if the table has no rows
+        """
+        self.empty_message = message
 
     def add_row(self, row):
         """Add a row to the table.
@@ -337,6 +353,10 @@ def to_python(self):
 
     def prettytable(self, fmt='table', theme=None):
         """Returns a RICH table instance."""
+
+        # Used to print a message instead of a bad looking empty table
+        if not self and self.empty_message:
+            return self.empty_message
         box_style = box.SQUARE
         if fmt == 'raw':
             box_style = None
diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py
index ba4c8f3f..b25c4ad6 100644
--- a/SoftLayer/CLI/hardware/detail.py
+++ b/SoftLayer/CLI/hardware/detail.py
@@ -29,7 +29,7 @@ def cli(env, identifier, passwords, price, components):
     table.align['value'] = 'l'
 
     hardware_id = helpers.resolve_id(hardware.resolve_ids, identifier, 'hardware')
-    result = hardware.get_hardware(hardware_id)
+    result = hardware.get_hardware_fast(hardware_id)
     result = utils.NestedDict(result)
     hard_drives = hardware.get_hard_drives(hardware_id)
 
@@ -72,11 +72,30 @@ def cli(env, identifier, passwords, price, components):
     table.add_row(['last_transaction', last_transaction])
     table.add_row(['billing', 'Hourly' if result['hourlyBillingFlag'] else 'Monthly'])
 
-    vlan_table = formatting.Table(['type', 'number', 'id', 'name', 'netmask'])
+    vlan_table = formatting.Table(['Network', 'Number', 'Id', 'Name', 'Type'])
     for vlan in result['networkVlans']:
-        vlan_table.add_row([vlan['networkSpace'], vlan['vlanNumber'],
-                            vlan['id'], vlan['fullyQualifiedName'],
-                            vlan['primarySubnets'][0]['netmask']])
+        vlan_table.add_row([
+            vlan.get('networkSpace'),
+            vlan.get('vlanNumber'),
+            vlan['id'],
+            vlan['fullyQualifiedName'],
+            'Primary'
+        ])
+
+    # Shows any VLANS trunked/tagged on this server
+    for component in result.get('networkComponents', []):
+        # These are the Primary network components
+        if component.get('primaryIpAddress', False):
+            uplink = component.get('uplinkComponent', {})
+            for trunk in uplink.get('networkVlanTrunks', []):
+                trunk_vlan = trunk.get('networkVlan')
+                vlan_table.add_row([
+                    trunk_vlan.get('networkSpace'),
+                    trunk_vlan.get('vlanNumber'),
+                    trunk_vlan.get('id'),
+                    trunk_vlan.get('fullyQualifiedName'),
+                    'Trunked'
+                ])
 
     table.add_row(['vlans', vlan_table])
 
diff --git a/SoftLayer/CLI/hardware/vlan_add.py b/SoftLayer/CLI/hardware/vlan_add.py
new file mode 100644
index 00000000..3109d207
--- /dev/null
+++ b/SoftLayer/CLI/hardware/vlan_add.py
@@ -0,0 +1,60 @@
+"""Trunk a VLAN to this server."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import helpers
+
+
+@click.command(cls=SoftLayer.CLI.command.SLCommand, )
+@click.argument('hardware', nargs=1)
+@click.argument('vlans', nargs=-1)
+@environment.pass_env
+def cli(env, hardware, vlans):
+    """Trunk a VLAN to this server.
+
+    HARDWARE is the id of the server
+    VLANS is the ID, name, or number of the VLANs you want to add. Multiple vlans can be added at the same time.
+    It is recommended to use the vlan ID, especially if you have multiple vlans with the same name/number.
+    """
+
+    if not vlans:
+        raise exceptions.ArgumentError("Error: Missing argument 'VLANS'.")
+    h_mgr = SoftLayer.HardwareManager(env.client)
+    n_mgr = SoftLayer.NetworkManager(env.client)
+    hw_id = helpers.resolve_id(h_mgr.resolve_ids, hardware, 'hardware')
+    # Enclosing in quotes is required for any input that has a space in it.
+    # "Public DAL10" for example needs to be sent to search as \"Public DAL10\"
+    sl_vlans = n_mgr.search_for_vlan(" ".join(f"\"{v}\"" for v in vlans))
+    if not sl_vlans:
+        raise exceptions.ArgumentError(f"No vlans found matching {' '.join(vlans)}")
+    add_vlans = parse_vlans(sl_vlans)
+    component_mask = "mask[id, name, port, macAddress, primaryIpAddress]"
+    # NEXT: Add nice output / exception handling
+    if len(add_vlans['public']) > 0:
+        components = h_mgr.get_network_components(hw_id, mask=component_mask, space='public')
+        for c in components:
+            if c.get('primaryIpAddress'):
+                h_mgr.trunk_vlan(c.get('id'), add_vlans['public'])
+    if len(add_vlans['private']) > 0:
+        components = h_mgr.get_network_components(hw_id, mask=component_mask, space='private')
+        for c in components:
+            if c.get('primaryIpAddress'):
+                h_mgr.trunk_vlan(c.get('id'), add_vlans['private'])
+
+
+def parse_vlans(vlans):
+    """returns a dictionary mapping for public / private vlans"""
+
+    pub_vlan = []
+    pri_vlan = []
+    for vlan in vlans:
+        print(f"{vlan.get('networkSpace')} | {vlan.get('id')} -> {vlan.get('vlanNumber')}")
+        if vlan.get('networkSpace') == "PUBLIC":
+            pub_vlan.append(vlan)
+        else:
+            pri_vlan.append(vlan)
+    return {"public": pub_vlan, "private": pri_vlan}
diff --git a/SoftLayer/CLI/hardware/vlan_remove.py b/SoftLayer/CLI/hardware/vlan_remove.py
new file mode 100644
index 00000000..36f3bd1f
--- /dev/null
+++ b/SoftLayer/CLI/hardware/vlan_remove.py
@@ -0,0 +1,65 @@
+"""Remove VLANs trunked to this server."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import helpers
+
+
+@click.command(cls=SoftLayer.CLI.command.SLCommand, )
+@click.argument('hardware', nargs=1)
+@click.argument('vlans', nargs=-1)
+@click.option('--all', 'all_vlans', is_flag=True, default=False, help="Remove ALL trunked vlans from this server.")
+@environment.pass_env
+def cli(env, hardware, vlans, all_vlans):
+    """Remove VLANs trunked to this server.
+
+    HARDWARE is the id of the server
+    VLANS is the ID, name, or number of the VLANs you want to remove. Multiple vlans can be removed at the same time.
+    It is recommended to use the vlan ID, especially if you have multiple vlans with the same name/number.
+    """
+    if not vlans and not all_vlans:
+        raise exceptions.ArgumentError("Error: Missing argument 'VLANS'.")
+    h_mgr = SoftLayer.HardwareManager(env.client)
+    n_mgr = SoftLayer.NetworkManager(env.client)
+    hw_id = helpers.resolve_id(h_mgr.resolve_ids, hardware, 'hardware')
+
+    if all_vlans:
+        h_mgr.clear_vlan(hw_id)
+        env.fout("Done.")
+        return
+
+    # Enclosing in quotes is required for any input that has a space in it.
+    # "Public DAL10" for example needs to be sent to search as \"Public DAL10\"
+    sl_vlans = n_mgr.search_for_vlan(" ".join(f"\"{v}\"" for v in vlans))
+    if not sl_vlans:
+        raise exceptions.ArgumentError(f"No vlans found matching {' '.join(vlans)}")
+    del_vlans = parse_vlans(sl_vlans)
+    component_mask = "mask[id, name, port, macAddress, primaryIpAddress]"
+    # NEXT: Add nice output / exception handling
+    if len(del_vlans['public']) > 0:
+        components = h_mgr.get_network_components(hw_id, mask=component_mask, space='public')
+        for c in components:
+            if c.get('primaryIpAddress'):
+                h_mgr.remove_vlan(c.get('id'), del_vlans['public'])
+    if len(del_vlans['private']) > 0:
+        components = h_mgr.get_network_components(hw_id, mask=component_mask, space='private')
+        for c in components:
+            if c.get('primaryIpAddress'):
+                h_mgr.remove_vlan(c.get('id'), del_vlans['private'])
+
+
+def parse_vlans(vlans):
+    """returns a dictionary mapping for public / private vlans"""
+
+    pub_vlan = []
+    pri_vlan = []
+    for vlan in vlans:
+        if vlan.get('networkSpace') == "PUBLIC":
+            pub_vlan.append(vlan)
+        else:
+            pri_vlan.append(vlan)
+    return {"public": pub_vlan, "private": pri_vlan}
diff --git a/SoftLayer/CLI/hardware/vlan_trunkable.py b/SoftLayer/CLI/hardware/vlan_trunkable.py
new file mode 100644
index 00000000..670a8df0
--- /dev/null
+++ b/SoftLayer/CLI/hardware/vlan_trunkable.py
@@ -0,0 +1,35 @@
+"""List VLANs this server can be attached to."""
+# :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(cls=SoftLayer.CLI.command.SLCommand, )
+@click.argument('hardware')
+@environment.pass_env
+def cli(env, hardware):
+    """List VLANs this server can be attached to."""
+
+    mgr = SoftLayer.HardwareManager(env.client)
+    hw_id = helpers.resolve_id(mgr.resolve_ids, hardware, 'hardware')
+    mask = (
+        "mask[id,primaryIpAddress,"
+        "networkVlansTrunkable[id,name,vlanNumber,fullyQualifiedName,networkSpace]]"
+    )
+    table = formatting.Table(["ID", "VLAN", "Name", "Space"])
+    table.set_empty_message("No trunkable vlans found.")
+    hw_components = env.client.call('SoftLayer_Hardware_Server', 'getNetworkComponents', id=hw_id, mask=mask)
+
+    for component in hw_components:
+        if component.get('primaryIpAddress'):
+            for vlan in component.get('networkVlansTrunkable', []):
+                table.add_row([
+                    vlan.get('id'), vlan.get('fullyQualifiedName'), vlan.get('name'), vlan.get('networkSpace')
+                ])
+
+    env.fout(table)
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index dbaf1cfe..f6ba2a8c 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -315,6 +315,9 @@
     ('hardware:notification-add', 'SoftLayer.CLI.hardware.notification_add:cli'),
     ('hardware:notification-delete', 'SoftLayer.CLI.hardware.notification_delete:cli'),
     ('hardware:create-credential', 'SoftLayer.CLI.hardware.create_credential:cli'),
+    ('hardware:vlan-trunkable', 'SoftLayer.CLI.hardware.vlan_trunkable:cli'),
+    ('hardware:vlan-add', 'SoftLayer.CLI.hardware.vlan_add:cli'),
+    ('hardware:vlan-remove', 'SoftLayer.CLI.hardware.vlan_remove:cli'),
 
     ('securitygroup', 'SoftLayer.CLI.securitygroup'),
     ('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'),
diff --git a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
index 8b1f3c90..150b8d8b 100644
--- a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
+++ b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
@@ -112,6 +112,7 @@
         }
     ]
 }
+
 editObject = True
 setTags = True
 setPrivateNetworkInterfaceSpeed = True
@@ -130,37 +131,51 @@
 getReverseDomainRecords = [
     {'resourceRecords': [{'data': '2.0.1.10.in-addr.arpa'}]}]
 bootToRescueLayer = True
-getFrontendNetworkComponents = [
+
+getNetworkComponents = [
     {'maxSpeed': 100},
     {
         'maxSpeed': 1000,
         'networkComponentGroup': {
             'groupTypeId': 2,
             'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}]
-        }
+        },
+        'primaryIpAddress': '192.168.1.1',
+        'id': 998877,
+        'uplinkComponent': {}
     },
     {
         'maxSpeed': 1000,
         'networkComponentGroup': {
             'groupTypeId': 2,
             'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}]
-        }
+        },
+        'id': 665544,
+        'uplinkComponent': {}
     },
     {
         'maxSpeed': 1000,
         'networkComponentGroup': {
             'groupTypeId': 2,
             'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}]
-        }
+        },
+        'id': 112233,
+        'uplinkComponent': {}
     },
     {
         'maxSpeed': 1000,
         'networkComponentGroup': {
             'groupTypeId': 2,
             'networkComponents': [{'maxSpeed': 1000}, {'maxSpeed': 1000}]
-        }
+        },
+        'primaryIpAddress': '10.0.0.1',
+        'id': 123456,
+        'uplinkComponent': {}
     }
 ]
+# This splits out the network components into 2 sections so they are different enough for tests
+getFrontendNetworkComponents = getNetworkComponents[:2]
+getBackendNetworkComponents = getNetworkComponents[3:]
 
 getBandwidthAllotmentDetail = {
     'allocationId': 25465663,
@@ -410,4 +425,73 @@
             {
                 "createDate": "2019-10-14T16:51:12-06:00",
                 "version": "5.10"
-            }]}}]
+            }
+        ]
+    }
+}]
+getActiveComponents = getComponents
+getActiveTransaction = getObject['activeTransaction']
+getOperatingSystem = getObject['operatingSystem']
+getSoftwareComponents = [
+    {
+        "hardwareId": 1907356,
+        "id": 59003868,
+        "manufacturerLicenseInstance": "",
+        "softwareLicense": {
+            "id": 20658,
+            "softwareDescriptionId": 2888,
+            "softwareDescription": {
+                "controlPanel": 0,
+                "id": 2888,
+                "licenseTermValue": 0,
+                "longDescription": "Juniper vSRX 1G 19.4R2-S3 Standard 19.4.2.3",
+                "manufacturer": "Juniper",
+                "name": "vSRX 1G 19.4R2-S3 Standard",
+                "operatingSystem": 1,
+                "referenceCode": "UBUNTU_18_64",
+                "upgradeSoftwareDescriptionId": None,
+                "upgradeSwDescId": None,
+                "version": "19.4.2.3",
+                "virtualLicense": 0,
+                "virtualizationPlatform": 0,
+                "requiredUser": "root"
+            }
+        }
+    },
+    {
+        "hardwareId": 1907356,
+        "id": 59003870,
+        "manufacturerLicenseInstance": "",
+        "softwareLicense": {
+            "id": 147,
+            "softwareDescriptionId": 148,
+            "softwareDescription": {
+                "controlPanel": 0,
+                "id": 148,
+                "licenseTermValue": None,
+                "longDescription": "Passmark Suite Latest",
+                "manufacturer": "Passmark",
+                "name": "Passmark Suite",
+                "operatingSystem": 0,
+                "upgradeSoftwareDescriptionId": None,
+                "upgradeSwDescId": None,
+                "version": "Latest",
+                "virtualLicense": 0,
+                "virtualizationPlatform": 0
+            }
+        }
+    }
+]
+getBillingItem = getObject['billingItem']
+getTagReferences = getObject['tagReferences']
+getNetworkVlans = getObject['networkVlans']
+getRemoteManagementAccounts = getObject['remoteManagementAccounts']
+
+
+# Setup for hardwareManager.clear_vlan related tests
+getObjectVlanClear = {
+    'backendNetworkComponent': getBackendNetworkComponents,
+    'frontendNetworkComponent': getFrontendNetworkComponents
+}
+getObjectVlanClear['backendNetworkComponent'][1]['networkVlanTrunks'] = [{'id': 99}]
+getObjectVlanClear['frontendNetworkComponent'][1]['networkVlanTrunks'] = [{'id': 11}]
diff --git a/SoftLayer/fixtures/SoftLayer_Network_Component.py b/SoftLayer/fixtures/SoftLayer_Network_Component.py
new file mode 100644
index 00000000..5927a323
--- /dev/null
+++ b/SoftLayer/fixtures/SoftLayer_Network_Component.py
@@ -0,0 +1,3 @@
+addNetworkVlanTrunks = True
+clearNetworkVlanTrunks = True
+removeNetworkVlanTrunks = True
diff --git a/SoftLayer/fixtures/SoftLayer_Search.py b/SoftLayer/fixtures/SoftLayer_Search.py
index c7ebab1d..181bd2ed 100644
--- a/SoftLayer/fixtures/SoftLayer_Search.py
+++ b/SoftLayer/fixtures/SoftLayer_Search.py
@@ -49,3 +49,37 @@
                            "type": "string"
                        }]
                    }]
+
+
+advancedSearchVlan = [
+    {
+        "matchedTerms": [
+            "name:|IBMPrivate|",
+            "name.sort:|IBMPrivate|"
+        ],
+        "relevanceScore": "9.05",
+        "resource": {
+            "fullyQualifiedName": "dal10.bcr03.0000",
+            "id": 11111,
+            "name": "IBMPrivate",
+            "vlanNumber": 0000,
+            "networkSpace": "PRIVATE"
+        },
+        "resourceType": "SoftLayer_Network_Vlan"
+    },
+    {
+        "matchedTerms": [
+            "name:|IBMPublic|",
+            "name.sort:|IBMPublic|"
+        ],
+        "relevanceScore": "9.01",
+        "resource": {
+            "fullyQualifiedName": "dal10.bcr03.11111",
+            "id": 999999,
+            "name": "IBMPublic",
+            "vlanNumber": 11111,
+            "networkSpace": "PUBLIC"
+        },
+        "resourceType": "SoftLayer_Network_Vlan"
+    }
+]
diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py
index 4cff9675..a59e7244 100644
--- a/SoftLayer/managers/hardware.py
+++ b/SoftLayer/managers/hardware.py
@@ -5,6 +5,7 @@
 
     :license: MIT, see LICENSE for more details.
 """
+import concurrent.futures as cf
 import datetime
 import logging
 import socket
@@ -20,7 +21,7 @@
 LOGGER = logging.getLogger(__name__)
 
 # Invalid names are ignored due to long method names and short argument names
-# pylint: disable=invalid-name, too-many-lines
+# pylint: disable=invalid-name, too-many-lines, too-many-public-methods
 
 EXTRA_CATEGORIES = ['pri_ipv6_addresses',
                     'static_ipv6_addresses',
@@ -276,6 +277,97 @@ def get_hardware(self, hardware_id, **kwargs):
 
         return self.hardware.getObject(id=hardware_id, **kwargs)
 
+    @retry(logger=LOGGER)
+    def get_hardware_fast(self, hardware_id):
+        """Get details about a hardware device. Similar to get_hardware() but this uses threads
+
+        :param integer id: the hardware ID
+        :returns: A dictionary containing a large amount of information about the specified server.
+        """
+
+        hw_mask = (
+            'id, globalIdentifier, fullyQualifiedDomainName, hostname, domain,'
+            'provisionDate, hardwareStatus, bareMetalInstanceFlag, processorPhysicalCoreAmount,'
+            'memoryCapacity, notes, privateNetworkOnlyFlag, primaryBackendIpAddress,'
+            'primaryIpAddress, networkManagementIpAddress, userData, datacenter, hourlyBillingFlag,'
+            'lastTransaction[transactionGroup], hardwareChassis[id,name]'
+        )
+        server = self.client.call('SoftLayer_Hardware_Server', 'getObject', id=hardware_id, mask=hw_mask)
+        with cf.ThreadPoolExecutor(max_workers=10) as executor:
+            networkComponentsMask = (
+                "id, status, speed, maxSpeed, name, ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,"
+                "port, primarySubnet[id, netmask, broadcastAddress, networkIdentifier, gateway],"
+                "uplinkComponent[networkVlanTrunks[networkVlan[networkSpace]]]"
+            )
+            networkComponents = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getNetworkComponents',
+                id=hardware_id, mask=networkComponentsMask
+            )
+            activeComponentsMask = (
+               'id,hardwareComponentModel[hardwareGenericComponentModel[id,hardwareComponentType[keyName]]]'
+            )
+            activeComponents = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getActiveComponents',
+                id=hardware_id, mask=activeComponentsMask
+            )
+
+            activeTransaction = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getActiveTransaction',
+                id=hardware_id, mask="id, transactionStatus[friendlyName,name]"
+            )
+
+            operatingSystemMask = (
+                'softwareLicense[softwareDescription[manufacturer, name, version, referenceCode]],'
+                'passwords[id,username,password]'
+            )
+            operatingSystem = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getOperatingSystem',
+                id=hardware_id, mask=operatingSystemMask
+            )
+
+            # Intentionally reusing the operatingSystemMask here. They are both softwareComponents
+            softwareComponents = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getSoftwareComponents',
+                id=hardware_id, mask=operatingSystemMask
+            )
+
+            billingItemMask = (
+                'id,nextInvoiceTotalRecurringAmount,'
+                'nextInvoiceChildren[nextInvoiceTotalRecurringAmount],'
+                'orderItem.order.userRecord[username]'
+            )
+            billingItem = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getBillingItem',
+                id=hardware_id, mask=billingItemMask
+            )
+
+            tagReferences = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getTagReferences',
+                id=hardware_id, mask="id,tag[name,id]"
+            )
+
+            networkVlans = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getNetworkVlans',
+                id=hardware_id, mask="id,vlanNumber,networkSpace,fullyQualifiedName,primarySubnets[ipAddresses]"
+            )
+
+            remoteManagementAccounts = executor.submit(
+                self.client.call, 'SoftLayer_Hardware_Server', 'getRemoteManagementAccounts',
+                id=hardware_id, mask="username,password"
+            )
+
+            server['networkComponents'] = networkComponents.result()
+            server['activeComponents'] = activeComponents.result()
+            server['activeTransaction'] = activeTransaction.result()
+            server['operatingSystem'] = operatingSystem.result()
+            server['softwareComponents'] = softwareComponents.result()
+            server['billingItem'] = billingItem.result()
+            server['networkVlans'] = networkVlans.result()
+            server['remoteManagementAccounts'] = remoteManagementAccounts.result()
+            server['tagReferences'] = tagReferences.result()
+
+        return server
+
     def reload(self, hardware_id, post_uri=None, ssh_keys=None, lvm=False):
         """Perform an OS reload of a server with its current configuration.
 
@@ -1072,22 +1164,71 @@ def get_components(self, hardware_id, mask=None, filter_component=None):
                         "createDate": {
                             "operation": "orderBy",
                             "options": [
-                                {
-                                    "name": "sort",
-                                    "value": [
-                                        "DESC"
-                                    ]
-                                },
-                                {
-                                    "name": "sortOrder",
-                                    "value": [
-                                        1
-                                    ]}]}
-                    }}}}
+                                {"name": "sort", "value": ["DESC"]},
+                                {"name": "sortOrder", "value": [1]}
+                            ]
+                        }
+                    }
+                }}}
 
         return self.client.call('Hardware_Server', 'getComponents',
                                 mask=mask, filter=filter_component, id=hardware_id)
 
+    def get_network_components(self, hardware_id, mask=None, space=None):
+        """Calls SoftLayer_Hardware_Server::getNetworkComponents()
+
+        :param int hardware_id: SoftLayer_Hardware_Server id
+        :param string mask: The object mask to use if you do not want the default
+        :param string space: 'public', 'private', or None for both.
+        :returns: https://sldn.softlayer.com/reference/datatypes/SoftLayer_Network_Component/
+        """
+
+        if mask is None:
+            mask = "mask[uplinkComponent, router, redundancyEnabledFlag, redundancyCapableFlag]"
+        method = "getNetworkComponents"
+        if space == "public":
+            method = "getFrontendNetworkComponents"
+        elif space == "private":
+            method = "getBackendNetworkComponents"
+        return self.client.call("SoftLayer_Hardware_Server", method, id=hardware_id, mask=mask)
+
+    def trunk_vlan(self, component_id, vlans):
+        """Calls SoftLayer_Network_Component::addNetworkVlanTrunks()
+
+        :param int component_id: SoftLayer_Network_Component id
+        :param list vlans: list of SoftLayer_Network_Vlan objects to add. Each object needs at least id or vlanNumber
+        """
+        return self.client.call('SoftLayer_Network_Component', 'addNetworkVlanTrunks', vlans, id=component_id)
+
+    def remove_vlan(self, component_id, vlans):
+        """Calls SoftLayer_Network_Component::removeNetworkVlanTrunks()
+
+        :param int component_id: SoftLayer_Network_Component id
+        :param list vlans: list of SoftLayer_Network_Vlan objects to remove. Each object needs at least id or vlanNumber
+        """
+        return self.client.call('SoftLayer_Network_Component', 'removeNetworkVlanTrunks', vlans, id=component_id)
+
+    def clear_vlan(self, hardware_id):
+        """Clears all vlan trunks from a hardware_id
+
+        :param int hardware_id: server to clear vlans from
+        """
+        component_mask = (
+            "mask[id, "
+            "backendNetworkComponents[id,networkVlanTrunks[networkVlanId]], "
+            "frontendNetworkComponents[id,networkVlanTrunks[networkVlanId]]"
+            "]"
+        )
+        components = self.client.call('SoftLayer_Hardware_Server', 'getObject', id=hardware_id, mask=component_mask)
+        # We only want to call this API on components with actual trunks.
+        # Calling this on the primary and redundant components might cause exceptions.
+        for c in components.get('backendNetworkComponent', []):
+            if len(c.get('networkVlanTrunks', [])):
+                self.client.call('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', id=c.get('id'))
+        for c in components.get('frontendNetworkComponent', []):
+            if len(c.get('networkVlanTrunks', [])):
+                self.client.call('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', id=c.get('id'))
+
     def get_sensors(self, hardware_id):
         """Returns Hardware sensor data"""
         return self.client.call('Hardware', 'getSensorData', id=hardware_id)
diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py
index ac720109..11554bee 100644
--- a/SoftLayer/managers/network.py
+++ b/SoftLayer/managers/network.py
@@ -81,6 +81,7 @@ def __init__(self, client):
         self.subnet = client['Network_Subnet']
         self.network_storage = self.client['Network_Storage']
         self.security_group = self.client['Network_SecurityGroup']
+        self.resolvers = [self.search_for_vlan]
 
     def add_global_ip(self, version=4, test_order=False):
         """Adds a global IP address to the account.
@@ -887,3 +888,17 @@ def clear_route(self, identifier):
         returns  true or false.
         """
         return self.client.call('SoftLayer_Network_Subnet', 'clearRoute', id=identifier)
+
+    def search_for_vlan(self, vlan):
+        """Returns a list of matching VLAN objects.
+
+        :param string vlan: Could be either vlan name, number, id, or fully qualified name
+        :return list: List of SoftLayer_Network_Vlan objects
+        """
+
+        query = f"_objectType:SoftLayer_Network_Vlan {vlan}"
+        mask = "mask[resource(SoftLayer_Network_Vlan)[id,name,vlanNumber,fullyQualifiedName,networkSpace]]"
+
+        results = self.client.call('SoftLayer_Search', 'advancedSearch', query, mask=mask)
+        # This returns JUST the Network_Vlan information, none of the search information.
+        return [result.get('resource') for result in results]
diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst
index ce55f2cc..535dd2d3 100644
--- a/docs/cli/hardware.rst
+++ b/docs/cli/hardware.rst
@@ -147,3 +147,15 @@ This function updates the firmware of a server. If already at the latest version
 .. click:: SoftLayer.CLI.hardware.create_credential:cli
    :prog: hardware create-credential
    :show-nested:
+
+.. click:: SoftLayer.CLI.hardware.vlan_add:cli
+   :prog: hardware vlan-add
+   :show-nested:
+
+.. click:: SoftLayer.CLI.hardware.vlan_remove:cli
+   :prog: hardware vlan-remove
+   :show-nested:
+
+.. click:: SoftLayer.CLI.hardware.vlan_trunkable:cli
+   :prog: hardware vlan-trunkable
+   :show-nested:
\ No newline at end of file


Test patch:
diff --git a/tests/CLI/formatting_table_tests.py b/tests/CLI/formatting_table_tests.py
new file mode 100644
index 00000000..11766707
--- /dev/null
+++ b/tests/CLI/formatting_table_tests.py
@@ -0,0 +1,62 @@
+"""
+    SoftLayer.tests.CLI.formatting_table_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :license: MIT, see LICENSE for more details.
+"""
+
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import formatting
+from SoftLayer import testing
+
+
+class TestTable(testing.TestCase):
+
+    def test_table_with_duplicated_columns(self):
+        self.assertRaises(exceptions.CLIHalt, formatting.Table, ['col', 'col'])
+
+    def test_boolean_table(self):
+        table = formatting.Table(["column1"], title="Test Title")
+        self.assertFalse(table)
+        table.add_row(["entry1"])
+        self.assertTrue(table)
+
+
+class IterToTableTests(testing.TestCase):
+
+    def test_format_api_dict(self):
+        result = formatting._format_dict({'key': 'value'})
+
+        self.assertIsInstance(result, formatting.Table)
+        self.assertEqual(result.columns, ['name', 'value'])
+        self.assertEqual(result.rows, [['key', 'value']])
+
+    def test_format_api_list(self):
+        result = formatting._format_list([{'key': 'value'}])
+
+        self.assertIsInstance(result, formatting.Table)
+        self.assertEqual(result.columns, ['key'])
+        self.assertEqual(result.rows, [['value']])
+
+    def test_format_api_list_non_objects(self):
+        result = formatting._format_list(['a', 'b', 'c'])
+
+        self.assertIsInstance(result, formatting.Table)
+        self.assertEqual(result.columns, ['value'])
+        self.assertEqual(result.rows, [['a'], ['b'], ['c']])
+
+    def test_format_api_list_with_none_value(self):
+        result = formatting._format_list([{'key': [None, 'value']}, None])
+
+        self.assertIsInstance(result, formatting.Table)
+        self.assertEqual(result.columns, ['key'])
+
+    def test_format_api_list_with_empty_array(self):
+        result = formatting.iter_to_table([{'id': 130224450, 'activeTickets': []}])
+        self.assertIsInstance(result, formatting.Table)
+        self.assertIn('id', result.columns)
+        self.assertIn('activeTickets', result.columns)
+        formatted = formatting.format_output(result, "table")
+        # No good ways to test whats actually in a Rich.Table without going through the hassel of
+        # printing it out. As long as this didn't throw and exception it should be fine.
+        self.assertEqual(formatted.row_count, 1)
diff --git a/tests/CLI/helper_tests.py b/tests/CLI/helper_tests.py
index 1c8c2d91..73b727be 100644
--- a/tests/CLI/helper_tests.py
+++ b/tests/CLI/helper_tests.py
@@ -255,12 +255,6 @@ def test_resolve_id_multiple(self):
             exceptions.CLIAbort, helpers.resolve_id, lambda r: [12345, 54321], 'test')
 
 
-class TestTable(testing.TestCase):
-
-    def test_table_with_duplicated_columns(self):
-        self.assertRaises(exceptions.CLIHalt, formatting.Table, ['col', 'col'])
-
-
 class TestFormatOutput(testing.TestCase):
 
     def test_format_output_string(self):
@@ -454,43 +448,3 @@ def test_export_to_template(self):
                 self.assertEqual(len(data.splitlines()), 2)
                 self.assertIn('datacenter=ams01\n', data)
                 self.assertIn('disk=disk1,disk2\n', data)
-
-
-class IterToTableTests(testing.TestCase):
-
-    def test_format_api_dict(self):
-        result = formatting._format_dict({'key': 'value'})
-
-        self.assertIsInstance(result, formatting.Table)
-        self.assertEqual(result.columns, ['name', 'value'])
-        self.assertEqual(result.rows, [['key', 'value']])
-
-    def test_format_api_list(self):
-        result = formatting._format_list([{'key': 'value'}])
-
-        self.assertIsInstance(result, formatting.Table)
-        self.assertEqual(result.columns, ['key'])
-        self.assertEqual(result.rows, [['value']])
-
-    def test_format_api_list_non_objects(self):
-        result = formatting._format_list(['a', 'b', 'c'])
-
-        self.assertIsInstance(result, formatting.Table)
-        self.assertEqual(result.columns, ['value'])
-        self.assertEqual(result.rows, [['a'], ['b'], ['c']])
-
-    def test_format_api_list_with_none_value(self):
-        result = formatting._format_list([{'key': [None, 'value']}, None])
-
-        self.assertIsInstance(result, formatting.Table)
-        self.assertEqual(result.columns, ['key'])
-
-    def test_format_api_list_with_empty_array(self):
-        result = formatting.iter_to_table([{'id': 130224450, 'activeTickets': []}])
-        self.assertIsInstance(result, formatting.Table)
-        self.assertIn('id', result.columns)
-        self.assertIn('activeTickets', result.columns)
-        formatted = formatting.format_output(result, "table")
-        # No good ways to test whats actually in a Rich.Table without going through the hassel of
-        # printing it out. As long as this didn't throw and exception it should be fine.
-        self.assertEqual(formatted.row_count, 1)
diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/hardware/hardware_basic_tests.py
similarity index 91%
rename from tests/CLI/modules/server_tests.py
rename to tests/CLI/modules/hardware/hardware_basic_tests.py
index 4b4e6eda..61135fa4 100644
--- a/tests/CLI/modules/server_tests.py
+++ b/tests/CLI/modules/hardware/hardware_basic_tests.py
@@ -20,7 +20,7 @@
 from SoftLayer import utils
 
 
-class ServerCLITests(testing.TestCase):
+class HardwareCLITests(testing.TestCase):
 
     def test_server_cancel_reasons(self):
         result = self.run_command(['server', 'cancel-reasons'])
@@ -105,10 +105,7 @@ def test_server_credentials_exception_password_not_found(self):
 
         result = self.run_command(['hardware', 'credentials', '12345'])
 
-        self.assertEqual(
-            'None',
-            str(result.exception)
-        )
+        self.assertEqual('None', str(result.exception))
 
     def test_server_details(self):
         result = self.run_command(['server', 'detail', '1234', '--passwords', '--price'])
@@ -119,28 +116,17 @@ def test_server_details(self):
         self.assertEqual(output['prices'][0]['Recurring Price'], 16.08)
         self.assertEqual(output['remote users'][0]['password'], 'abc123')
         self.assertEqual(output['users'][0]['username'], 'root')
-        self.assertEqual(output['vlans'][0]['number'], 1800)
+        self.assertEqual(output['vlans'][0]['Number'], 1800)
         self.assertEqual(output['owner'], 'chechu')
         self.assertEqual(output['Bandwidth'][0]['Allotment'], '250')
 
     def test_detail_vs_empty_tag(self):
-        mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
-        mock.return_value = {
-            'id': 100,
-            'processorPhysicalCoreAmount': 2,
-            'memoryCapacity': 2,
-            'tagReferences': [
-                {'tag': {'name': 'example-tag'}},
-                {},
-            ],
-        }
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getTagReferences')
+        mock.return_value = [{'tag': {'name': 'example-tag'}}, {}]
         result = self.run_command(['server', 'detail', '100'])
 
         self.assert_no_fail(result)
-        self.assertEqual(
-            json.loads(result.output)['tags'],
-            ['example-tag'],
-        )
+        self.assertEqual(json.loads(result.output)['tags'], ['example-tag'])
 
     def test_detail_empty_allotment(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getBandwidthAllotmentDetail')
@@ -148,10 +134,7 @@ def test_detail_empty_allotment(self):
         result = self.run_command(['server', 'detail', '100'])
 
         self.assert_no_fail(result)
-        self.assertEqual(
-            json.loads(result.output)['Bandwidth'][0]['Allotment'],
-            '-',
-        )
+        self.assertEqual(json.loads(result.output)['Bandwidth'][0]['Allotment'], '-')
 
     def test_detail_drives(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getHardDrives')
@@ -255,15 +238,13 @@ def test_cancel_server(self, cancel_mock, ngb_mock):
         ngb_mock.return_value = False
 
         # Check the positive case
-        result = self.run_command(['--really', 'server', 'cancel', '12345',
-                                   '--reason=Test', '--comment=Test'])
+        result = self.run_command(['--really', 'server', 'cancel', '12345', '--reason=Test', '--comment=Test'])
 
         self.assert_no_fail(result)
         cancel_mock.assert_called_with(12345, "Test", "Test", False)
 
         # Test
-        result = self.run_command(['server', 'cancel', '12345',
-                                   '--reason=Test', '--comment=Test'])
+        result = self.run_command(['server', 'cancel', '12345', '--reason=Test', '--comment=Test'])
 
         self.assertEqual(result.exit_code, 2)
         self.assertIsInstance(result.exception, exceptions.CLIAbort)
@@ -273,8 +254,7 @@ def test_server_power_off(self, confirm_mock):
         # Check the positive case
         result = self.run_command(['--really', 'server', 'power-off', '12345'])
 
-        self.assert_called_with('SoftLayer_Hardware_Server', 'powerOff',
-                                identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'powerOff', identifier=12345)
 
         # Now check to make sure we properly call CLIAbort in the negative case
         confirm_mock.return_value = False
@@ -287,24 +267,19 @@ def test_server_reboot_default(self):
         result = self.run_command(['--really', 'server', 'reboot', '12345'])
 
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Hardware_Server', 'rebootDefault',
-                                identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'rebootDefault', identifier=12345)
 
     def test_server_reboot_soft(self):
-        result = self.run_command(['--really', 'server', 'reboot', '12345',
-                                   '--soft'])
+        result = self.run_command(['--really', 'server', 'reboot', '12345', '--soft'])
 
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Hardware_Server', 'rebootSoft',
-                                identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'rebootSoft', identifier=12345)
 
     def test_server_reboot_hard(self):
-        result = self.run_command(['--really', 'server', 'reboot', '12345',
-                                   '--hard'])
+        result = self.run_command(['--really', 'server', 'reboot', '12345', '--hard'])
 
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Hardware_Server', 'rebootHard',
-                                identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'rebootHard',  identifier=12345)
 
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_server_reboot_negative(self, confirm_mock):
@@ -318,8 +293,7 @@ def test_server_power_on(self):
         result = self.run_command(['--really', 'server', 'power-on', '12345'])
 
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Hardware_Server', 'powerOn',
-                                identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'powerOn', identifier=12345)
 
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_harware_power_on_force(self, confirm_mock):
@@ -336,12 +310,10 @@ def test_harware_power_off_force(self, confirm_mock):
         self.assertEqual('Aborted.', result.exception.message)
 
     def test_server_power_cycle(self):
-        result = self.run_command(['--really', 'server', 'power-cycle',
-                                   '12345'])
+        result = self.run_command(['--really', 'server', 'power-cycle', '12345'])
 
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Hardware_Server', 'powerCycle',
-                                identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'powerCycle', identifier=12345)
 
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_server_power_cycle_negative(self, confirm_mock):
@@ -498,8 +470,7 @@ def test_edit_server_userdata(self):
         self.assert_no_fail(result)
         self.assertEqual(result.output, "")
         self.assert_called_with('SoftLayer_Hardware_Server', 'editObject',
-                                args=({'domain': 'test.sftlyr.ws',
-                                       'hostname': 'hardware-test1'},),
+                                args=({'domain': 'test.sftlyr.ws', 'hostname': 'hardware-test1'},),
                                 identifier=1000)
 
     @mock.patch('SoftLayer.HardwareManager.edit')
@@ -512,10 +483,7 @@ def test_edit_server_failed(self, edit_mock):
 
         self.assertEqual(result.exit_code, 2)
         self.assertEqual(result.output, "")
-        edit_mock.assert_called_with(1000,
-                                     userdata='My data',
-                                     domain='test.sftlyr.ws',
-                                     hostname='hardware-test1')
+        edit_mock.assert_called_with(1000, userdata='My data', domain='test.sftlyr.ws', hostname='hardware-test1')
 
     def test_edit_server_userfile(self):
         if (sys.platform.startswith("win")):
@@ -523,15 +491,12 @@ def test_edit_server_userfile(self):
         with tempfile.NamedTemporaryFile() as userfile:
             userfile.write(b"some data")
             userfile.flush()
-            result = self.run_command(['server', 'edit', '1000',
-                                       '--userfile=%s' % userfile.name])
+            result = self.run_command(['server', 'edit', '1000', '--userfile=%s' % userfile.name])
 
             self.assert_no_fail(result)
             self.assertEqual(result.output, "")
-            self.assert_called_with('SoftLayer_Hardware_Server',
-                                    'setUserMetadata',
-                                    args=(['some data'],),
-                                    identifier=1000)
+            self.assert_called_with('SoftLayer_Hardware_Server', 'setUserMetadata',
+                                    args=(['some data'],), identifier=1000)
 
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_update_firmware(self, confirm_mock):
@@ -540,8 +505,7 @@ def test_update_firmware(self, confirm_mock):
 
         self.assert_no_fail(result)
         self.assertEqual(result.output, "")
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'createFirmwareUpdateTransaction',
+        self.assert_called_with('SoftLayer_Hardware_Server', 'createFirmwareUpdateTransaction',
                                 args=((1, 1, 1, 1)), identifier=1000)
 
     @mock.patch('SoftLayer.CLI.formatting.confirm')
@@ -551,8 +515,7 @@ def test_reflash_firmware(self, confirm_mock):
 
         self.assert_no_fail(result)
         self.assertEqual(result.output, 'Successfully device firmware reflashed\n')
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'createFirmwareReflashTransaction',
+        self.assert_called_with('SoftLayer_Hardware_Server', 'createFirmwareReflashTransaction',
                                 args=((1, 1, 1)), identifier=1000)
 
     def test_edit(self):
@@ -715,8 +678,7 @@ def test_bandwidth_hw_quite(self):
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_dns_sync_both(self, confirm_mock):
         confirm_mock.return_value = True
-        getReverseDomainRecords = self.set_mock('SoftLayer_Hardware_Server',
-                                                'getReverseDomainRecords')
+        getReverseDomainRecords = self.set_mock('SoftLayer_Hardware_Server', 'getReverseDomainRecords')
         getReverseDomainRecords.return_value = [{
             'networkAddress': '172.16.1.100',
             'name': '2.240.16.172.in-addr.arpa',
@@ -727,8 +689,7 @@ def test_dns_sync_both(self, confirm_mock):
             'serial': 1234665663,
             'id': 123456,
         }]
-        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain',
-                                           'getResourceRecords')
+        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', 'getResourceRecords')
         getResourceRecords.return_value = []
         createAargs = ({
                            'type': 'a',
@@ -749,20 +710,14 @@ def test_dns_sync_both(self, confirm_mock):
 
         self.assert_no_fail(result)
         self.assert_called_with('SoftLayer_Dns_Domain', 'getResourceRecords')
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'getReverseDomainRecords')
-        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord',
-                                'createObject',
-                                args=createAargs)
-        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord',
-                                'createObject',
-                                args=createPTRargs)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getReverseDomainRecords')
+        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'createObject', args=createAargs)
+        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'createObject', args=createPTRargs)
 
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_dns_sync_v6(self, confirm_mock):
         confirm_mock.return_value = True
-        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain',
-                                           'getResourceRecords')
+        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', 'getResourceRecords')
         getResourceRecords.return_value = []
         server = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
         test_server = {
@@ -795,9 +750,7 @@ def test_dns_sync_v6(self, confirm_mock):
         server.return_value = test_server
         result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000'])
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord',
-                                'createObject',
-                                args=createV6args)
+        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'createObject', args=createV6args)
 
         v6Record = {
             'id': 1,
@@ -807,18 +760,14 @@ def test_dns_sync_v6(self, confirm_mock):
             'type': 'aaaa'
         }
 
-        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain',
-                                           'getResourceRecords')
+        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', 'getResourceRecords')
         getResourceRecords.return_value = [v6Record]
         editArgs = (v6Record,)
         result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000'])
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord',
-                                'editObject',
-                                args=editArgs)
+        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'editObject', args=editArgs)
 
-        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain',
-                                           'getResourceRecords')
+        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', 'getResourceRecords')
         getResourceRecords.return_value = [v6Record, v6Record]
         result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000'])
         self.assertEqual(result.exit_code, 1)
@@ -827,8 +776,7 @@ def test_dns_sync_v6(self, confirm_mock):
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_dns_sync_edit_a(self, confirm_mock):
         confirm_mock.return_value = True
-        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain',
-                                           'getResourceRecords')
+        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', 'getResourceRecords')
         getResourceRecords.return_value = [
             {'id': 1, 'ttl': 7200, 'data': '1.1.1.1',
              'host': 'hardware-test1', 'type': 'a'}
@@ -839,12 +787,9 @@ def test_dns_sync_edit_a(self, confirm_mock):
         )
         result = self.run_command(['hw', 'dns-sync', '-a', '1000'])
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord',
-                                'editObject',
-                                args=editArgs)
+        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'editObject', args=editArgs)
 
-        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain',
-                                           'getResourceRecords')
+        getResourceRecords = self.set_mock('SoftLayer_Dns_Domain', 'getResourceRecords')
         getResourceRecords.return_value = [
             {'id': 1, 'ttl': 7200, 'data': '1.1.1.1',
              'host': 'hardware-test1', 'type': 'a'},
@@ -858,8 +803,7 @@ def test_dns_sync_edit_a(self, confirm_mock):
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_dns_sync_edit_ptr(self, confirm_mock):
         confirm_mock.return_value = True
-        getReverseDomainRecords = self.set_mock('SoftLayer_Hardware_Server',
-                                                'getReverseDomainRecords')
+        getReverseDomainRecords = self.set_mock('SoftLayer_Hardware_Server', 'getReverseDomainRecords')
         getReverseDomainRecords.return_value = [{
             'networkAddress': '172.16.1.100',
             'name': '100.1.16.172.in-addr.arpa',
@@ -870,13 +814,10 @@ def test_dns_sync_edit_ptr(self, confirm_mock):
             'serial': 1234665663,
             'id': 123456,
         }]
-        editArgs = ({'host': '100', 'data': 'hardware-test1.test.sftlyr.ws',
-                     'id': 123, 'ttl': 7200},)
+        editArgs = ({'host': '100', 'data': 'hardware-test1.test.sftlyr.ws', 'id': 123, 'ttl': 7200},)
         result = self.run_command(['hw', 'dns-sync', '--ptr', '1000'])
         self.assert_no_fail(result)
-        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord',
-                                'editObject',
-                                args=editArgs)
+        self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'editObject', args=editArgs)
 
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_dns_sync_misc_exception(self, confirm_mock):
@@ -900,9 +841,7 @@ def test_dns_sync_misc_exception(self, confirm_mock):
         self.assertIsInstance(result.exception, exceptions.CLIAbort)
 
     def test_hardware_storage(self):
-        result = self.run_command(
-            ['hw', 'storage', '100'])
-
+        result = self.run_command(['hw', 'storage', '100'])
         self.assert_no_fail(result)
 
     def test_billing(self):
@@ -1083,3 +1022,11 @@ def test_list_hw_search_noargs_domain(self):
         self.assert_no_fail(result)
         self.assert_called_with('SoftLayer_Search', 'advancedSearch',
                                 args=('_objectType:SoftLayer_Hardware  domain: *test*',))
+
+    @mock.patch('SoftLayer.CLI.formatting.confirm')
+    def test_hardware_cancel_no_force(self, confirm_mock):
+        confirm_mock.return_value = False
+        result = self.run_command(['hardware', 'cancel', '102'])
+
+        self.assertEqual(2, result.exit_code)
+        self.assertEqual('Aborted', result.exception.message)
diff --git a/tests/CLI/modules/hardware/hardware_vlan_tests.py b/tests/CLI/modules/hardware/hardware_vlan_tests.py
new file mode 100644
index 00000000..59a19207
--- /dev/null
+++ b/tests/CLI/modules/hardware/hardware_vlan_tests.py
@@ -0,0 +1,128 @@
+"""
+    SoftLayer.tests.CLI.modules.hardware.hardware_vlan_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    These tests are for any commands that work with the vlans on hardware objects.
+
+    :license: MIT, see LICENSE for more details.
+"""
+
+import json
+
+from SoftLayer.fixtures import SoftLayer_Hardware_Server
+from SoftLayer.fixtures import SoftLayer_Search
+from SoftLayer import testing
+
+
+class HardwareVlanCLITests(testing.TestCase):
+
+    # slcli hardware vlan-trunkable
+    def test_hardware_vlan_trunkable_no_hardware(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getNetworkComponents')
+        mock.return_value = []
+        result = self.run_command(['hardware', 'vlan-trunkable'])
+        self.assertEqual(2, result.exit_code)
+        self.assertIn("Missing argument 'HARDWARE'.", result.output)
+
+    def test_hardware_vlan_trunkable_happypath(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getNetworkComponents')
+        mock.return_value = [
+            {
+                'maxSpeed': 1000,
+                'networkVlansTrunkable': [{
+                    'id': 5555,
+                    'fullyQualifiedName': 'test01.ibm99.1234',
+                    'name': 'IBMTEst',
+                    'networkSpace': 'PUBLIC'
+                }],
+                'primaryIpAddress': '192.168.1.1',
+                'id': 998877,
+            }
+        ]
+        result = self.run_command(['hardware', 'vlan-trunkable', '12345'])
+        self.assert_no_fail(result)
+        output = json.loads(result.output)
+        self.assertEqual(len(output), 1)
+        self.assertEqual(output[0]['ID'], 5555)
+
+    def test_hardware_vlan_trunkable_no_vlans(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getNetworkComponents')
+        mock.return_value = []
+        result = self.run_command(['--format=table', 'hardware', 'vlan-trunkable', '12345'])
+        print(result.output)
+        self.assert_no_fail(result)
+        self.assertIn("No trunkable vlans found.", result.output)
+
+    def test_hardware_vlan_trunkable_no_vlans_json(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getNetworkComponents')
+        mock.return_value = []
+        result = self.run_command(['hardware', 'vlan-trunkable', '12345'])
+        output = json.loads(result.output)
+        self.assert_no_fail(result)
+        self.assertEqual([], output)
+
+    # slcli hardware vlan-remove
+    def test_hardware_vlan_remove(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = SoftLayer_Search.advancedSearchVlan
+        result = self.run_command(['hardware', 'vlan-remove', '12345', '5555'])
+        self.assert_no_fail(result)
+        search_args = '_objectType:SoftLayer_Network_Vlan "5555"'
+        self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=(search_args,))
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getFrontendNetworkComponents', identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getBackendNetworkComponents', identifier=12345)
+        self.assert_called_with('SoftLayer_Network_Component', 'removeNetworkVlanTrunks', identifier=998877)
+        self.assert_called_with('SoftLayer_Network_Component', 'removeNetworkVlanTrunks', identifier=123456)
+
+    def test_hardware_vlan_remove_two_vlans(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = SoftLayer_Search.advancedSearchVlan
+        result = self.run_command(['hardware', 'vlan-remove', '12345', '5555', 'testVlan'])
+        self.assert_no_fail(result)
+        search_args = '_objectType:SoftLayer_Network_Vlan "5555" "testVlan"'
+        self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=(search_args,))
+
+    def test_hardware_vlan_remove_no_vlans(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = SoftLayer_Search.advancedSearchVlan
+        result = self.run_command(['hardware', 'vlan-remove', '12345'])
+        self.assertEqual(2, result.exit_code)
+        self.assertEqual("Argument Error: Error: Missing argument 'VLANS'.", result.exception.message)
+
+    def test_hardware_vlan_remove_all_vlans(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = SoftLayer_Search.advancedSearchVlan
+        hardware_mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
+        hardware_mock.return_value = SoftLayer_Hardware_Server.getObjectVlanClear
+        result = self.run_command(['hardware', 'vlan-remove', '12345', '--all'])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', identifier=998877)
+        self.assert_called_with('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', identifier=123456)
+
+    # slcli hardware vlan-add
+    def test_hardware_vlan_add(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = SoftLayer_Search.advancedSearchVlan
+        result = self.run_command(['hardware', 'vlan-add', '12345', '5555'])
+        self.assert_no_fail(result)
+        search_args = '_objectType:SoftLayer_Network_Vlan "5555"'
+        self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=(search_args,))
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getFrontendNetworkComponents', identifier=12345)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getBackendNetworkComponents', identifier=12345)
+        self.assert_called_with('SoftLayer_Network_Component', 'addNetworkVlanTrunks', identifier=998877)
+        self.assert_called_with('SoftLayer_Network_Component', 'addNetworkVlanTrunks', identifier=123456)
+
+    def test_hardware_vlan_add_two_vlans(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = SoftLayer_Search.advancedSearchVlan
+        result = self.run_command(['hardware', 'vlan-add', '12345', '5555', 'testVlan'])
+        self.assert_no_fail(result)
+        search_args = '_objectType:SoftLayer_Network_Vlan "5555" "testVlan"'
+        self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=(search_args,))
+
+    def test_hardware_vlan_add_no_vlans(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = SoftLayer_Search.advancedSearchVlan
+        result = self.run_command(['hardware', 'vlan-add', '12345'])
+        self.assertEqual(2, result.exit_code)
+        self.assertEqual("Argument Error: Error: Missing argument 'VLANS'.", result.exception.message)
diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py
index 05790469..c6175d07 100644
--- a/tests/managers/hardware_tests.py
+++ b/tests/managers/hardware_tests.py
@@ -275,10 +275,7 @@ def test_get_hardware_item_prices(self):
                             "longName": "Montreal 1",
                             "name": "mon01",
                             "regions": [
-                                {
-                                    "description": "MON01 - Montreal",
-                                    "keyname": "MONTREAL",
-                                }
+                                {"description": "MON01 - Montreal",  "keyname": "MONTREAL"}
                             ]
                         }
                     ]
@@ -303,8 +300,7 @@ def test_generate_create_dict_no_regions(self):
         packages_copy[0]['regions'] = []
         packages.return_value = packages_copy
 
-        ex = self.assertRaises(SoftLayer.SoftLayerError,
-                               self.hardware._generate_create_dict,
+        ex = self.assertRaises(SoftLayer.SoftLayerError, self.hardware._generate_create_dict,
                                **MINIMAL_TEST_CREATE_ARGS)
         self.assertIn("Could not find valid location for: 'wdc01'", str(ex))
 
@@ -372,16 +368,8 @@ def test_generate_create_dict_by_router_network_component(self):
             'hardware': [{
                 'domain': 'giggles.woo',
                 'hostname': 'unicorn',
-                'primaryNetworkComponent': {
-                    "router": {
-                        "id": 1111
-                    }
-                },
-                'primaryBackendNetworkComponent': {
-                    "router": {
-                        "id": 1234
-                    }
-                }
+                'primaryNetworkComponent': {"router": {"id": 1111}},
+                'primaryBackendNetworkComponent': {"router": {"id": 1234}}
             }]
         }
 
@@ -429,8 +417,7 @@ def test_place_order(self, create_dict, place_order):
 
     def test_cancel_hardware_without_reason(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
-        mock.return_value = {'id': 987, 'billingItem': {'id': 1234},
-                             'openCancellationTicket': {'id': 1234}}
+        mock.return_value = {'id': 987, 'billingItem': {'id': 1234}, 'openCancellationTicket': {'id': 1234}}
 
         result = self.hardware.cancel_hardware(987)
 
@@ -441,8 +428,7 @@ def test_cancel_hardware_without_reason(self):
 
     def test_cancel_hardware_with_reason_and_comment(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
-        mock.return_value = {'id': 987, 'billingItem': {'id': 1234},
-                             'openCancellationTicket': {'id': 1234}}
+        mock.return_value = {'id': 987, 'billingItem': {'id': 1234}, 'openCancellationTicket': {'id': 1234}}
 
         result = self.hardware.cancel_hardware(6327, reason='sales', comment='Test Comment')
 
@@ -465,18 +451,14 @@ def test_cancel_hardware_no_billing_item(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
         mock.return_value = {'id': 987, 'openCancellationTicket': {'id': 1234}}
 
-        ex = self.assertRaises(SoftLayer.SoftLayerError,
-                               self.hardware.cancel_hardware,
-                               6327)
+        ex = self.assertRaises(SoftLayer.SoftLayerError, self.hardware.cancel_hardware, 6327)
         self.assertEqual("Ticket #1234 already exists for this server", str(ex))
 
     def test_cancel_hardwareno_billing_item_or_ticket(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
         mock.return_value = {'id': 987}
 
-        ex = self.assertRaises(SoftLayer.SoftLayerError,
-                               self.hardware.cancel_hardware,
-                               6327)
+        ex = self.assertRaises(SoftLayer.SoftLayerError, self.hardware.cancel_hardware, 6327)
         self.assertEqual("Cannot locate billing for the server. The server may already be cancelled.", str(ex))
 
     def test_cancel_hardware_monthly_now(self):
@@ -513,34 +495,25 @@ def test_cancel_running_transaction(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
         mock.return_value = {'id': 987, 'billingItem': {'id': 6327},
                              'activeTransaction': {'id': 4567}}
-        self.assertRaises(SoftLayer.SoftLayerError,
-                          self.hardware.cancel_hardware,
-                          12345)
+        self.assertRaises(SoftLayer.SoftLayerError, self.hardware.cancel_hardware, 12345)
 
     def test_change_port_speed_public(self):
         self.hardware.change_port_speed(2, True, 100, 'degraded')
 
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'setPublicNetworkInterfaceSpeed',
-                                identifier=2,
-                                args=([100, 'degraded'],))
+        self.assert_called_with('SoftLayer_Hardware_Server', 'setPublicNetworkInterfaceSpeed',
+                                identifier=2, args=([100, 'degraded'],))
 
     def test_change_port_speed_private(self):
         self.hardware.change_port_speed(2, False, 10, 'redundant')
 
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'setPrivateNetworkInterfaceSpeed',
-                                identifier=2,
-                                args=([10, 'redundant'],))
+        self.assert_called_with('SoftLayer_Hardware_Server', 'setPrivateNetworkInterfaceSpeed',
+                                identifier=2, args=([10, 'redundant'],))
 
     def test_edit_meta(self):
         # Test editing user data
         self.hardware.edit(100, userdata='my data')
 
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'setUserMetadata',
-                                args=(['my data'],),
-                                identifier=100)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'setUserMetadata', args=(['my data'],), identifier=100)
 
     def test_edit_blank(self):
         # Now test a blank edit
@@ -549,13 +522,9 @@ def test_edit_blank(self):
 
     def test_edit(self):
         # Finally, test a full edit
-        self.hardware.edit(100,
-                           hostname='new-host',
-                           domain='new.sftlyr.ws',
-                           notes='random notes')
+        self.hardware.edit(100, hostname='new-host', domain='new.sftlyr.ws', notes='random notes')
 
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'editObject',
+        self.assert_called_with('SoftLayer_Hardware_Server', 'editObject',
                                 args=({
                                           'hostname': 'new-host',
                                           'domain': 'new.sftlyr.ws',
@@ -567,44 +536,34 @@ def test_rescue(self):
         result = self.hardware.rescue(1234)
 
         self.assertEqual(result, True)
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'bootToRescueLayer',
-                                identifier=1234)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'bootToRescueLayer', identifier=1234)
 
     def test_update_firmware(self):
         result = self.hardware.update_firmware(100)
 
         self.assertEqual(result, True)
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'createFirmwareUpdateTransaction',
+        self.assert_called_with('SoftLayer_Hardware_Server', 'createFirmwareUpdateTransaction',
                                 identifier=100, args=(1, 1, 1, 1))
 
     def test_update_firmware_selective(self):
-        result = self.hardware.update_firmware(100,
-                                               ipmi=False,
-                                               hard_drive=False)
+        result = self.hardware.update_firmware(100, ipmi=False, hard_drive=False)
 
         self.assertEqual(result, True)
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'createFirmwareUpdateTransaction',
+        self.assert_called_with('SoftLayer_Hardware_Server', 'createFirmwareUpdateTransaction',
                                 identifier=100, args=(0, 1, 1, 0))
 
     def test_reflash_firmware(self):
         result = self.hardware.reflash_firmware(100)
 
         self.assertEqual(result, True)
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'createFirmwareReflashTransaction',
+        self.assert_called_with('SoftLayer_Hardware_Server', 'createFirmwareReflashTransaction',
                                 identifier=100, args=(1, 1, 1))
 
     def test_reflash_firmware_selective(self):
-        result = self.hardware.reflash_firmware(100,
-                                                raid_controller=False,
-                                                bios=False)
+        result = self.hardware.reflash_firmware(100, raid_controller=False, bios=False)
 
         self.assertEqual(result, True)
-        self.assert_called_with('SoftLayer_Hardware_Server',
-                                'createFirmwareReflashTransaction',
+        self.assert_called_with('SoftLayer_Hardware_Server', 'createFirmwareReflashTransaction',
                                 identifier=100, args=(1, 0, 0))
 
     def test_get_tracking_id(self):
@@ -614,10 +573,8 @@ def test_get_tracking_id(self):
 
     def test_get_bandwidth_data(self):
         result = self.hardware.get_bandwidth_data(1234, '2019-01-01', '2019-02-01', 'public', 1000)
-        self.assert_called_with('SoftLayer_Metric_Tracking_Object',
-                                'getBandwidthData',
-                                args=('2019-01-01', '2019-02-01', 'public', 1000),
-                                identifier=1000)
+        self.assert_called_with('SoftLayer_Metric_Tracking_Object', 'getBandwidthData',
+                                args=('2019-01-01', '2019-02-01', 'public', 1000), identifier=1000)
         self.assertEqual(result[0]['type'], 'cpu0')
 
     def test_get_bandwidth_allocation(self):
@@ -681,7 +638,7 @@ def test_get_storage_iscsi_empty_details(self):
 
     def test_get_storage_nas_details(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getAttachedNetworkStorages')
-        mock.return_value = [
+        getAttachedNetworkStorages = [
             {
                 "accountId": 11111,
                 "capacityGb": 12000,
@@ -691,15 +648,9 @@ def test_get_storage_nas_details(self):
             }
         ]
 
+        mock.return_value = getAttachedNetworkStorages
         result = self.hardware.get_storage_details(1234, 'NAS')
-
-        self.assertEqual([{
-            "accountId": 11111,
-            "capacityGb": 12000,
-            "id": 3777111,
-            "nasType": "NAS",
-            "username": "SL02SEL32222-9",
-        }], result)
+        self.assertEqual(getAttachedNetworkStorages, result)
 
     def test_get_storage_nas_empty_details(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getAttachedNetworkStorages')
@@ -711,7 +662,7 @@ def test_get_storage_nas_empty_details(self):
 
     def test_get_storage_credentials(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getAllowedHost')
-        mock.return_value = {
+        getAllowedHost = {
             "accountId": 11111,
             "id": 33333,
             "name": "iqn.2020-03.com.ibm:sl02su11111-v62941551",
@@ -723,33 +674,19 @@ def test_get_storage_credentials(self):
                 "username": "SL02SU11111-V62941551"
             }
         }
-
+        mock.return_value = getAllowedHost
         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)
+        self.assertEqual(getAllowedHost, 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 = [
+        hd_return = [
             {
                 "id": 11111,
                 "serialNumber": "z1w4sdf",
@@ -773,33 +710,9 @@ def test_get_hard_drives(self):
                 }
             }
         ]
-
+        mock.return_value = hd_return
         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)
+        self.assertEqual(hd_return, result)
 
     def test_get_hard_drive_empty(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getHardDrives')
@@ -817,9 +730,7 @@ def test_authorize_storage(self):
     def test_authorize_storage_empty(self):
         mock = self.set_mock('SoftLayer_Account', 'getNetworkStorage')
         mock.return_value = []
-        self.assertRaises(SoftLayer.exceptions.SoftLayerError,
-                          self.hardware.authorize_storage,
-                          1234, "#")
+        self.assertRaises(SoftLayer.exceptions.SoftLayerError, self.hardware.authorize_storage, 1234, "#")
 
     def test_get_price_id_memory_capacity(self):
         upgrade_prices = [
@@ -945,14 +856,56 @@ def test_create_credential(self):
             "software": {
                 "hardwareId": 123456,
                 "softwareLicense": {
-                    "softwareDescription": {
-                        "name": 'system'
-                    }
+                    "softwareDescription": {"name": 'system'}
                 }
-            }}
+            }
+        }
         self.hardware.create_credential(template)
         self.assert_called_with('SoftLayer_Software_Component_Password', 'createObject')
 
+    def test_get_hardware_fast(self):
+        result = self.hardware.get_hardware_fast(1234)
+        self.assertIn('networkComponents', result)
+        self.assertIn('activeComponents', result)
+        self.assertIn('activeTransaction', result)
+        self.assertIn('operatingSystem', result)
+        self.assertIn('softwareComponents', result)
+        self.assertIn('billingItem', result)
+        self.assertIn('networkVlans', result)
+        self.assertIn('remoteManagementAccounts', result)
+        self.assertIn('tagReferences', result)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getNetworkComponents')
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getActiveComponents')
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getOperatingSystem')
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getSoftwareComponents')
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getBillingItem')
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getTagReferences')
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getNetworkVlans')
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getRemoteManagementAccounts')
+
+    def test_trunk_vlan(self):
+        vlans = [{'id': 9999}]
+        vlan_id = 1234
+        self.hardware.trunk_vlan(vlan_id, vlans)
+        self.assert_called_with('SoftLayer_Network_Component', 'addNetworkVlanTrunks',
+                                identifier=vlan_id, args=(vlans,))
+
+    def test_remove_vlan(self):
+        vlans = [{'id': 9999}]
+        vlan_id = 1234
+        self.hardware.remove_vlan(vlan_id, vlans)
+        self.assert_called_with('SoftLayer_Network_Component', 'removeNetworkVlanTrunks',
+                                identifier=vlan_id, args=(vlans,))
+
+    def test_clear_vlan(self):
+        hardware_mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
+        hardware_mock.return_value = fixtures.SoftLayer_Hardware_Server.getObjectVlanClear
+        server_id = 1234
+        self.hardware.clear_vlan(server_id)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getObject', identifier=server_id)
+        self.assert_called_with('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', identifier=998877)
+        self.assert_called_with('SoftLayer_Network_Component', 'clearNetworkVlanTrunks', identifier=123456)
+
 
 class HardwareHelperTests(testing.TestCase):
 
@@ -1052,11 +1005,3 @@ def test_is_private(self):
         item_public = {'attributes': [{'attributeTypeKeyName': 'NOT_PRIVATE_NETWORK_ONLY'}]}
         self.assertTrue(managers.hardware._is_private_port_speed_item(item_private))
         self.assertFalse(managers.hardware._is_private_port_speed_item(item_public))
-
-    @mock.patch('SoftLayer.CLI.formatting.confirm')
-    def test_hardware_cancel_no_force(self, confirm_mock):
-        confirm_mock.return_value = False
-        result = self.run_command(['hardware', 'cancel', '102'])
-
-        self.assertEqual(2, result.exit_code)
-        self.assertEqual('Aborted', result.exception.message)