Repository: softlayer/softlayer-python
Pull request #1281 (2020-06-10 21:41:06)

Tags
#1230 

Adds the following commands.

### SLCLI tags
*  list
* delete
* set
* taggable
* cleanup
* details


Patch:
diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py
index fe86f714..7257c59d 100644
--- a/SoftLayer/CLI/core.py
+++ b/SoftLayer/CLI/core.py
@@ -35,6 +35,7 @@
 PROG_NAME = "slcli (SoftLayer Command-line)"
 VALID_FORMATS = ['table', 'raw', 'json', 'jsonraw']
 DEFAULT_FORMAT = 'raw'
+
 if sys.stdout.isatty():
     DEFAULT_FORMAT = 'table'
 
@@ -157,6 +158,7 @@ def cli(env,
 
     logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG))
     env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport)
+    env.vars['verbose'] = verbose
     env.client.transport = env.vars['_timings']
 
 
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index 452ee0f9..cc67a7d2 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -290,6 +290,14 @@
     ('subnet:list', 'SoftLayer.CLI.subnet.list:cli'),
     ('subnet:lookup', 'SoftLayer.CLI.subnet.lookup:cli'),
 
+    ('tags', 'SoftLayer.CLI.tags'),
+    ('tags:cleanup', 'SoftLayer.CLI.tags.cleanup:cli'),
+    ('tags:list', 'SoftLayer.CLI.tags.list:cli'),
+    ('tags:set', 'SoftLayer.CLI.tags.set:cli'),
+    ('tags:details', 'SoftLayer.CLI.tags.details:cli'),
+    ('tags:delete', 'SoftLayer.CLI.tags.delete:cli'),
+    ('tags:taggable', 'SoftLayer.CLI.tags.taggable:cli'),
+
     ('ticket', 'SoftLayer.CLI.ticket'),
     ('ticket:create', 'SoftLayer.CLI.ticket.create:cli'),
     ('ticket:detail', 'SoftLayer.CLI.ticket.detail:cli'),
diff --git a/SoftLayer/CLI/tags/__init__.py b/SoftLayer/CLI/tags/__init__.py
new file mode 100644
index 00000000..f8dd3783
--- /dev/null
+++ b/SoftLayer/CLI/tags/__init__.py
@@ -0,0 +1,1 @@
+"""Manage Tags"""
diff --git a/SoftLayer/CLI/tags/cleanup.py b/SoftLayer/CLI/tags/cleanup.py
new file mode 100644
index 00000000..26ddea7e
--- /dev/null
+++ b/SoftLayer/CLI/tags/cleanup.py
@@ -0,0 +1,26 @@
+"""Removes unused Tags"""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+from SoftLayer.CLI import environment
+from SoftLayer.managers.tags import TagManager
+
+
+@click.command()
+@click.option('--dry-run', '-d', is_flag=True, default=False,
+              help="Don't delete, just show what will be deleted.")
+@environment.pass_env
+def cli(env, dry_run):
+    """Removes all empty tags."""
+
+    tag_manager = TagManager(env.client)
+    empty_tags = tag_manager.get_unattached_tags()
+
+    for tag in empty_tags:
+        if dry_run:
+            click.secho("(Dry Run) Removing {}".format(tag.get('name')), fg='yellow')
+        else:
+            result = tag_manager.delete_tag(tag.get('name'))
+            color = 'green' if result else 'red'
+            click.secho("Removing {}".format(tag.get('name')), fg=color)
diff --git a/SoftLayer/CLI/tags/delete.py b/SoftLayer/CLI/tags/delete.py
new file mode 100644
index 00000000..f3bb1e70
--- /dev/null
+++ b/SoftLayer/CLI/tags/delete.py
@@ -0,0 +1,29 @@
+"""Delete Tags."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+from SoftLayer.CLI import environment
+from SoftLayer.managers.tags import TagManager
+
+
+@click.command()
+@click.argument('identifier')
+@click.option('--name', required=False, default=False, is_flag=True, show_default=False,
+              help='Assume identifier is a tag name. Useful if your tag name is a number.')
+@environment.pass_env
+def cli(env, identifier, name):
+    """Delete a Tag. Tag names that contain spaces need to be encased in quotes"""
+
+    tag_manager = TagManager(env.client)
+    tag_name = identifier
+    # If the identifier is a int, and user didn't tell us it was a name.
+    if str.isdigit(identifier) and not name:
+        tag = tag_manager.get_tag(identifier)
+        tag_name = tag.get('name', None)
+
+    result = tag_manager.delete_tag(tag_name)
+    if result:
+        click.secho("Tag {} has been removed".format(tag_name), fg='green')
+    else:
+        click.secho("Failed to remove tag {}".format(tag_name), fg='red')
diff --git a/SoftLayer/CLI/tags/details.py b/SoftLayer/CLI/tags/details.py
new file mode 100644
index 00000000..7c397f43
--- /dev/null
+++ b/SoftLayer/CLI/tags/details.py
@@ -0,0 +1,27 @@
+"""Details of a Tag."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+from SoftLayer.CLI import environment
+from SoftLayer.CLI.tags.list import detailed_table
+from SoftLayer.managers.tags import TagManager
+
+
+@click.command()
+@click.argument('identifier')
+@click.option('--name', required=False, default=False, is_flag=True, show_default=False,
+              help='Assume identifier is a tag name. Useful if your tag name is a number.')
+@environment.pass_env
+def cli(env, identifier, name):
+    """Get details for a Tag. Identifier can be either a name or tag-id"""
+
+    tag_manager = TagManager(env.client)
+
+    # If the identifier is a int, and user didn't tell us it was a name.
+    if str.isdigit(identifier) and not name:
+        tags = [tag_manager.get_tag(identifier)]
+    else:
+        tags = tag_manager.get_tag_by_name(identifier)
+    table = detailed_table(tag_manager, tags)
+    env.fout(table)
diff --git a/SoftLayer/CLI/tags/list.py b/SoftLayer/CLI/tags/list.py
new file mode 100644
index 00000000..bc866276
--- /dev/null
+++ b/SoftLayer/CLI/tags/list.py
@@ -0,0 +1,75 @@
+"""List Tags."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+from SoftLayer.exceptions import SoftLayerAPIError
+from SoftLayer.managers.tags import TagManager
+from SoftLayer import utils
+
+# pylint: disable=unnecessary-lambda
+
+
+@click.command()
+@click.option('--detail', '-d', is_flag=True, default=False,
+              help="Show information about the resources using this tag.")
+@environment.pass_env
+def cli(env, detail):
+    """List Tags."""
+
+    tag_manager = TagManager(env.client)
+
+    if detail:
+        tables = detailed_table(tag_manager, tag_manager.get_attached_tags())
+        for table in tables:
+            env.fout(table)
+    else:
+        table = simple_table(tag_manager)
+        env.fout(table)
+    # pp(tags.list_tags())
+
+
+def tag_row(tag):
+    """Format a tag table row"""
+    return [tag.get('id'), tag.get('name'), tag.get('referenceCount', 0)]
+
+
+def detailed_table(tag_manager, tags):
+    """Creates a table for each tag, with details about resources using it"""
+    tables = []
+    for tag in tags:
+        references = tag_manager.get_tag_references(tag.get('id'))
+        # pp(references)
+        new_table = formatting.Table(['Id', 'Type', 'Resource'], title=tag.get('name'))
+        for reference in references:
+            tag_type = utils.lookup(reference, 'tagType', 'keyName')
+            resource_id = reference.get('resourceTableId')
+            resource_row = get_resource_name(tag_manager, resource_id, tag_type)
+            new_table.add_row([resource_id, tag_type, resource_row])
+        tables.append(new_table)
+
+    return tables
+
+
+def simple_table(tag_manager):
+    """Just tags and how many resources on each"""
+    tags = tag_manager.list_tags()
+    table = formatting.Table(['Id', 'Tag', 'Count'], title='Tags')
+    for tag in tags.get('attached', []):
+        table.add_row(tag_row(tag))
+    for tag in tags.get('unattached', []):
+        table.add_row(tag_row(tag))
+    return table
+
+
+def get_resource_name(tag_manager, resource_id, tag_type):
+    """Returns a string to identify a resource"""
+    name = None
+    try:
+        resource = tag_manager.reference_lookup(resource_id, tag_type)
+        name = tag_manager.get_resource_name(resource, tag_type)
+    except SoftLayerAPIError as exception:
+        name = "{}".format(exception.reason)
+    return name
diff --git a/SoftLayer/CLI/tags/set.py b/SoftLayer/CLI/tags/set.py
new file mode 100644
index 00000000..ed409fb9
--- /dev/null
+++ b/SoftLayer/CLI/tags/set.py
@@ -0,0 +1,26 @@
+"""Set Tags."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+from SoftLayer.CLI import environment
+from SoftLayer.managers.tags import TagManager
+
+
+@click.command()
+@click.option('--tags', '-t', type=click.STRING, required=True,
+              help='Comma seperated list of tags, enclosed in quotes. "tag1, tag2"')
+@click.option('--key-name', '-k', type=click.STRING, required=True,
+              help="Key name of a tag type e.g. GUEST, HARDWARE. See slcli tags taggable output.")
+@click.option('--resource-id', '-r', type=click.INT, required=True, help="ID of the object being tagged")
+@environment.pass_env
+def cli(env, tags, key_name, resource_id):
+    """Set Tags."""
+
+    tag_manager = TagManager(env.client)
+    tags = tag_manager.set_tags(tags, key_name, resource_id)
+
+    if tags:
+        click.secho("Set tags successfully", fg='green')
+    else:
+        click.secho("Failed to set tags", fg='red')
diff --git a/SoftLayer/CLI/tags/taggable.py b/SoftLayer/CLI/tags/taggable.py
new file mode 100644
index 00000000..0c08acdb
--- /dev/null
+++ b/SoftLayer/CLI/tags/taggable.py
@@ -0,0 +1,27 @@
+"""List everything that could be tagged."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+from SoftLayer.managers.tags import TagManager
+
+
+@click.command()
+@environment.pass_env
+def cli(env):
+    """List everything that could be tagged."""
+
+    tag_manager = TagManager(env.client)
+    tag_types = tag_manager.get_all_tag_types()
+    for tag_type in tag_types:
+        title = "{} ({})".format(tag_type['description'], tag_type['keyName'])
+        table = formatting.Table(['Id', 'Name'], title=title)
+        resources = tag_manager.taggable_by_type(tag_type['keyName'])
+        for resource in resources:
+            table.add_row([
+                resource['resource']['id'],
+                tag_manager.get_resource_name(resource['resource'], tag_type['keyName'])
+            ])
+        env.fout(table)
diff --git a/SoftLayer/fixtures/SoftLayer_Hardware.py b/SoftLayer/fixtures/SoftLayer_Hardware.py
new file mode 100644
index 00000000..cb902b55
--- /dev/null
+++ b/SoftLayer/fixtures/SoftLayer_Hardware.py
@@ -0,0 +1,59 @@
+getObject = {
+    'id': 1234,
+    'globalIdentifier': 'xxxxc-asd',
+    'datacenter': {'id': 12, 'name': 'DALLAS21',
+                   'description': 'Dallas 21'},
+    'billingItem': {
+        'id': 6327,
+        'recurringFee': 1.54,
+        'nextInvoiceTotalRecurringAmount': 16.08,
+        'children': [
+            {'description': 'test', 'nextInvoiceTotalRecurringAmount': 1},
+        ],
+        'orderItem': {
+            'order': {
+                'userRecord': {
+                    'username': 'bob',
+                }
+            }
+        }
+    },
+    'primaryIpAddress': '4.4.4.4',
+    'hostname': 'testtest1',
+    'domain': 'test.sftlyr.ws',
+    'bareMetalInstanceFlag': True,
+    'fullyQualifiedDomainName': 'testtest1.test.sftlyr.ws',
+    'processorPhysicalCoreAmount': 4,
+    'memoryCapacity': 4,
+    'primaryBackendIpAddress': '10.4.4.4',
+    'networkManagementIpAddress': '10.4.4.4',
+    'hardwareStatus': {'status': 'ACTIVE'},
+    'primaryNetworkComponent': {'maxSpeed': 1000, 'speed': 1000},
+    'provisionDate': '2020-08-01 15:23:45',
+    'notes': 'NOTES NOTES NOTES',
+    'operatingSystem': {
+        'softwareLicense': {
+            'softwareDescription': {
+                'referenceCode': 'UBUNTU_20_64',
+                'name': 'Ubuntu',
+                'version': 'Ubuntu 20.04 LTS',
+            }
+        },
+        'passwords': [
+            {'username': 'root', 'password': 'xxxxxxxxxxxx'}
+        ],
+    },
+    'remoteManagementAccounts': [
+        {'username': 'root', 'password': 'zzzzzzzzzzzzzz'}
+    ],
+    'networkVlans': [
+        {
+            'networkSpace': 'PRIVATE',
+            'vlanNumber': 1234,
+            'id': 11111
+        },
+    ],
+    'tagReferences': [
+        {'tag': {'name': 'a tag'}}
+    ],
+}
diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage_Allowed_Host.py b/SoftLayer/fixtures/SoftLayer_Network_Storage_Allowed_Host.py
index 5bf8c335..923147a5 100644
--- a/SoftLayer/fixtures/SoftLayer_Network_Storage_Allowed_Host.py
+++ b/SoftLayer/fixtures/SoftLayer_Network_Storage_Allowed_Host.py
@@ -32,22 +32,22 @@
 getObject = TEST_ALLOWED_HOST
 
 getSubnetsInAcl = [{
-        'id': 12345678,
-        'accountId': 1234,
-        'networkIdentifier': '10.11.12.13',
-        'cidr': '14',
-        'billingRecordId': None,
-        'parentId': None,
-        'networkVlanId': None,
-        'createDate': '2020-01-02 00:00:01',
-        'modifyDate': None,
-        'subnetType': 'SECONDARY_ON_VLAN',
-        'restrictAllocationFlag': 0,
-        'leafFlag': 1,
-        'ownerId': 1,
-        'ipAddressBegin': 129123,
-        'ipAddressEnd': 129145,
-        'purgeFlag': 0
+    'id': 12345678,
+    'accountId': 1234,
+    'networkIdentifier': '10.11.12.13',
+    'cidr': '14',
+    'billingRecordId': None,
+    'parentId': None,
+    'networkVlanId': None,
+    'createDate': '2020-01-02 00:00:01',
+    'modifyDate': None,
+    'subnetType': 'SECONDARY_ON_VLAN',
+    'restrictAllocationFlag': 0,
+    'leafFlag': 1,
+    'ownerId': 1,
+    'ipAddressBegin': 129123,
+    'ipAddressEnd': 129145,
+    'purgeFlag': 0
 }]
 
 assignSubnetsToAcl = [
diff --git a/SoftLayer/fixtures/SoftLayer_Search.py b/SoftLayer/fixtures/SoftLayer_Search.py
new file mode 100644
index 00000000..ccb45fe5
--- /dev/null
+++ b/SoftLayer/fixtures/SoftLayer_Search.py
@@ -0,0 +1,23 @@
+advancedSearch = [
+    {
+        "relevanceScore": "4",
+        "resourceType": "SoftLayer_Hardware",
+        "resource": {
+            "accountId": 307608,
+            "domain": "vmware.test.com",
+            "fullyQualifiedDomainName": "host14.vmware.test.com",
+            "hardwareStatusId": 5,
+            "hostname": "host14",
+            "id": 123456,
+            "manufacturerSerialNumber": "AAAAAAAAA",
+            "notes": "A test notes",
+            "provisionDate": "2018-08-24T12:32:10-06:00",
+            "serialNumber": "SL12345678",
+            "serviceProviderId": 1,
+            "hardwareStatus": {
+                "id": 5,
+                "status": "ACTIVE"
+            }
+        }
+    }
+]
diff --git a/SoftLayer/fixtures/SoftLayer_Tag.py b/SoftLayer/fixtures/SoftLayer_Tag.py
new file mode 100644
index 00000000..9f6aeaec
--- /dev/null
+++ b/SoftLayer/fixtures/SoftLayer_Tag.py
@@ -0,0 +1,31 @@
+getUnattachedTagsForCurrentUser = [{'id': 287895, 'name': 'coreos', 'referenceCount': 0}]
+getAttachedTagsForCurrentUser = [{'id': 1286571, 'name': 'bs_test_instance', 'referenceCount': 5}]
+getReferences = [
+    {
+        'id': 73009305,
+        'resourceTableId': 33488921,
+        'tag': {
+            'id': 1286571,
+            'name': 'bs_test_instance',
+        },
+        'tagId': 1286571,
+        'tagType': {'description': 'CCI', 'keyName': 'GUEST'},
+        'tagTypeId': 2,
+        'usrRecordId': 6625205
+    }
+]
+
+deleteTag = True
+
+setTags = True
+
+getObject = getAttachedTagsForCurrentUser[0]
+
+getTagByTagName = getAttachedTagsForCurrentUser
+
+getAllTagTypes = [
+    {
+        "description": "Hardware",
+        "keyName": "HARDWARE"
+    }
+]
diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py
index 5c489345..8053ec70 100644
--- a/SoftLayer/managers/__init__.py
+++ b/SoftLayer/managers/__init__.py
@@ -24,6 +24,7 @@
 from SoftLayer.managers.ordering import OrderingManager
 from SoftLayer.managers.sshkey import SshKeyManager
 from SoftLayer.managers.ssl import SSLManager
+from SoftLayer.managers.tags import TagManager
 from SoftLayer.managers.ticket import TicketManager
 from SoftLayer.managers.user import UserManager
 from SoftLayer.managers.vs import VSManager
@@ -50,6 +51,7 @@
     'PlacementManager',
     'SshKeyManager',
     'SSLManager',
+    'TagManager',
     'TicketManager',
     'UserManager',
     'VSManager',
diff --git a/SoftLayer/managers/tags.py b/SoftLayer/managers/tags.py
new file mode 100644
index 00000000..818b0547
--- /dev/null
+++ b/SoftLayer/managers/tags.py
@@ -0,0 +1,230 @@
+"""
+    SoftLayer.tags
+    ~~~~~~~~~~~~
+    Tag Manager
+
+    :license: MIT, see LICENSE for more details.
+"""
+import re
+
+from SoftLayer.exceptions import SoftLayerAPIError
+
+
+class TagManager(object):
+    """Manager for Tag functions."""
+
+    def __init__(self, client):
+        self.client = client
+
+    def list_tags(self, mask=None):
+        """Returns a list of all tags for the Current User
+
+        :param str mask: Object mask to use if you do not want the default.
+        """
+        if mask is None:
+            mask = "mask[id,name,referenceCount]"
+        unattached = self.get_unattached_tags(mask)
+        attached = self.get_attached_tags(mask)
+        return {'attached': attached, 'unattached': unattached}
+
+    def get_unattached_tags(self, mask=None):
+        """Calls SoftLayer_Tag::getUnattachedTagsForCurrentUser()
+
+        :params string mask: Mask to use.
+        """
+        return self.client.call('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser',
+                                mask=mask, iter=True)
+
+    def get_attached_tags(self, mask=None):
+        """Calls SoftLayer_Tag::getAttachedTagsForCurrentUser()
+
+        :params string mask: Mask to use.
+        """
+        return self.client.call('SoftLayer_Tag', 'getAttachedTagsForCurrentUser',
+                                mask=mask, iter=True)
+
+    def get_tag_references(self, tag_id, mask=None):
+        """Calls SoftLayer_Tag::getReferences(id=tag_id)
+
+        :params int tag_id: Tag id to get references from
+        :params string mask: Mask to use.
+        """
+        if mask is None:
+            mask = "mask[tagType]"
+        return self.client.call('SoftLayer_Tag', 'getReferences', id=tag_id, mask=mask, iter=True)
+
+    def get_tag(self, tag_id, mask=None):
+        """Calls SoftLayer_Tag::getObject(id=tag_id)
+
+        :params int tag_id: Tag id to get object from
+        :params string mask: Mask to use.
+        """
+        if mask is None:
+            mask = "mask[id,name]"
+        result = self.client.call('SoftLayer_Tag', 'getObject', id=tag_id, mask=mask)
+        return result
+
+    def get_tag_by_name(self, tag_name, mask=None):
+        """Calls SoftLayer_Tag::getTagByTagName(tag_name)
+
+        :params string tag_name: Tag name to get object from
+        :params string mask: Mask to use.
+        """
+        if mask is None:
+            mask = "mask[id,name]"
+        result = self.client.call('SoftLayer_Tag', 'getTagByTagName', tag_name, mask=mask)
+        return result
+
+    def reference_lookup(self, resource_table_id, tag_type):
+        """Returns the SoftLayer Service for the corresponding type
+
+        :param int resource_table_id: Tag_Reference::resourceTableId
+        :param string tag_type: Tag_Reference->tagType->keyName
+
+        From  SoftLayer_Tag::getAllTagTypes()
+
+        |Type                             |Service |
+        | -----------------------------   | ------ |
+        |Hardware                         |HARDWARE|
+        |CCI                              |GUEST|
+        |Account Document                 |ACCOUNT_DOCUMENT|
+        |Ticket                           |TICKET|
+        |Vlan Firewall                    |NETWORK_VLAN_FIREWALL|
+        |Contract                         |CONTRACT|
+        |Image Template                   |IMAGE_TEMPLATE|
+        |Application Delivery Controller  |APPLICATION_DELIVERY_CONTROLLER|
+        |Vlan                             |NETWORK_VLAN|
+        |Dedicated Host                   |DEDICATED_HOST|
+        """
+        service = self.type_to_service(tag_type)
+        if service is None:
+            raise SoftLayerAPIError(404, "Unable to lookup {} types".format(tag_type))
+        return self.client.call(service, 'getObject', id=resource_table_id)
+
+    def delete_tag(self, name):
+        """Calls SoftLayer_Tag::deleteTag
+
+        :param string name: tag name to delete
+        """
+        return self.client.call('SoftLayer_Tag', 'deleteTag', name)
+
+    def set_tags(self, tags, key_name, resource_id):
+        """Calls SoftLayer_Tag::setTags()
+
+        :param string tags: List of tags.
+        :param string key_name: Key name of a tag type.
+        :param int resource_id: ID of the object being tagged.
+        """
+        return self.client.call('SoftLayer_Tag', 'setTags', tags, key_name, resource_id)
+
+    def get_all_tag_types(self):
+        """Calls SoftLayer_Tag::getAllTagTypes()"""
+        types = self.client.call('SoftLayer_Tag', 'getAllTagTypes')
+        useable_types = []
+        for tag_type in types:
+            service = self.type_to_service(tag_type['keyName'])
+            # Mostly just to remove the types that are not user taggable.
+            if service is not None:
+                temp_type = tag_type
+                temp_type['service'] = service
+                useable_types.append(temp_type)
+        return useable_types
+
+    def taggable_by_type(self, tag_type):
+        """Returns a list of resources that can be tagged, that are of the given type
+
+        :param string tag_type: Key name of a tag type. See SoftLayer_Tag::getAllTagTypes
+        """
+        service = self.type_to_service(tag_type)
+        search_term = "_objectType:SoftLayer_{}".format(service)
+        if tag_type == 'TICKET':
+            search_term = "{} status.name: open".format(search_term)
+        elif tag_type == 'IMAGE_TEMPLATE':
+            mask = "mask[id,accountId,name,globalIdentifier,parentId,publicFlag,flexImageFlag,imageType]"
+            resources = self.client.call('SoftLayer_Account', 'getPrivateBlockDeviceTemplateGroups',
+                                         mask=mask, iter=True)
+            to_return = []
+            # Fake search result output
+            for resource in resources:
+                to_return.append({'resourceType': service, 'resource': resource})
+            return to_return
+        elif tag_type == 'NETWORK_SUBNET':
+            resources = self.client.call('SoftLayer_Account', 'getSubnets', iter=True)
+            to_return = []
+            # Fake search result output
+            for resource in resources:
+                to_return.append({'resourceType': service, 'resource': resource})
+            return to_return
+        resources = self.client.call('SoftLayer_Search', 'advancedSearch', search_term, iter=True)
+        return resources
+
+    @staticmethod
+    def type_to_service(tag_type):
+        """Returns the SoftLayer service for the given tag_type"""
+        service = None
+        if tag_type in ['ACCOUNT_DOCUMENT', 'CONTRACT']:
+            return None
+
+        if tag_type == 'APPLICATION_DELIVERY_CONTROLLER':
+            service = 'Network_Application_Delivery_Controller'
+        elif tag_type == 'GUEST':
+            service = 'Virtual_Guest'
+        elif tag_type == 'DEDICATED_HOST':
+            service = 'Virtual_DedicatedHost'
+        elif tag_type == 'IMAGE_TEMPLATE':
+            service = 'Virtual_Guest_Block_Device_Template_Group'
+        else:
+
+            tag_type = tag_type.lower()
+            # Sets the First letter, and any letter preceeded by a '_' to uppercase
+            # HARDWARE -> Hardware, NETWORK_VLAN -> Network_Vlan for example.
+            service = re.sub(r'(^[a-z]|\_[a-z])', lambda x: x.group().upper(), tag_type)
+        return service
+
+    @staticmethod
+    def get_resource_name(resource, tag_type):
+        """Returns a string that names a resource
+
+        :param dict resource: A SoftLayer datatype for the given tag_type
+        :param string tag_type: Key name for the tag_type
+        """
+        if tag_type == 'NETWORK_VLAN_FIREWALL':
+            return resource.get('primaryIpAddress')
+        elif tag_type == 'NETWORK_VLAN':
+            return "{} ({})".format(resource.get('vlanNumber'), resource.get('name'))
+        elif tag_type == 'IMAGE_TEMPLATE' or tag_type == 'APPLICATION_DELIVERY_CONTROLLER':
+            return resource.get('name')
+        elif tag_type == 'TICKET':
+            return resource.get('subjet')
+        elif tag_type == 'NETWORK_SUBNET':
+            return resource.get('networkIdentifier')
+        else:
+            return resource.get('fullyQualifiedDomainName')
+
+    # @staticmethod
+    # def type_to_datatype(tag_type):
+    #     """Returns the SoftLayer datatye for the given tag_type"""
+    #     datatye = None
+    #     if tag_type in ['ACCOUNT_DOCUMENT', 'CONTRACT']:
+    #         return None
+
+    #     if tag_type == 'APPLICATION_DELIVERY_CONTROLLER':
+    #         datatye = 'adcLoadBalancers'
+    #     elif tag_type == 'GUEST':
+    #         datatye = 'virtualGuests'
+    #     elif tag_type == 'DEDICATED_HOST':
+    #         datatye = 'dedicatedHosts'
+    #     elif tag_type == 'HARDWARE':
+    #         datatye = 'hardware'
+    #     elif tag_type == 'TICKET':
+    #         datatye = 'openTickets'
+    #     elif tag_type == 'NETWORK_SUBNET':
+    #         datatye = 'subnets'
+    #     elif tag_type == 'NETWORK_VLAN':
+    #         datatye = 'networkVlans'
+    #     elif tag_type == 'NETWORK_VLAN_FIREWALL':
+    #         datatye = 'networkVlans'
+    #     elif tag_type == 'IMAGE_TEMPLATE':
+    #         datatye = 'blockDeviceTemplateGroups'
+
+    #     return datatye
diff --git a/docs/cli/tags.rst b/docs/cli/tags.rst
new file mode 100644
index 00000000..a5a99b69
--- /dev/null
+++ b/docs/cli/tags.rst
@@ -0,0 +1,30 @@
+.. _cli_tags:
+
+Tag Commands
+============
+
+These commands will allow you to interact with the **IMS** provier tagging service. The `IBM Global Search and Tagging API <https://cloud.ibm.com/apidocs/tagging#related-apis>`_  can be used to interact with both the **GHOST** provider and **IMS** provider. The **GHOST** provider will handle tags for things outside of the Classic Infrastructure (aka SoftLayer) space.
+
+.. click:: SoftLayer.CLI.tags.list:cli
+    :prog: tags list
+    :show-nested:
+
+.. click:: SoftLayer.CLI.tags.set:cli
+    :prog: tags set
+    :show-nested:
+
+.. click:: SoftLayer.CLI.tags.details:cli
+    :prog: tags details
+    :show-nested:
+
+.. click:: SoftLayer.CLI.tags.delete:cli
+    :prog: tags delete
+    :show-nested:
+
+.. click:: SoftLayer.CLI.tags.taggable:cli
+    :prog: tags taggable
+    :show-nested:
+
+.. click:: SoftLayer.CLI.tags.cleanup:cli
+    :prog: tags cleanup
+    :show-nested:


Test patch:
diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py
index b39face1..e5f6ba8c 100644
--- a/tests/CLI/modules/block_tests.py
+++ b/tests/CLI/modules/block_tests.py
@@ -112,7 +112,7 @@ def test_volume_detail_name_identifier(self):
                     'keyName': {'operation': '*= BLOCK_STORAGE'}
                 },
                 'username': {'operation': '_= SL-12345'}
-                }
+            }
         }
 
         self.assert_called_with('SoftLayer_Account', 'getIscsiNetworkStorage', filter=expected_filter)
diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py
index 157acfcc..a7fd4e90 100644
--- a/tests/CLI/modules/server_tests.py
+++ b/tests/CLI/modules/server_tests.py
@@ -694,19 +694,19 @@ def test_dns_sync_both(self, confirm_mock):
                                            'getResourceRecords')
         getResourceRecords.return_value = []
         createAargs = ({
-                           'type': 'a',
-                           'host': 'hardware-test1',
-                           'domainId': 12345,  # from SoftLayer_Account::getDomains
-                           'data': '172.16.1.100',
-                           'ttl': 7200
-                       },)
+            'type': 'a',
+            'host': 'hardware-test1',
+            'domainId': 12345,  # from SoftLayer_Account::getDomains
+            'data': '172.16.1.100',
+            'ttl': 7200
+        },)
         createPTRargs = ({
-                             'type': 'ptr',
-                             'host': '100',
-                             'domainId': 123456,
-                             'data': 'hardware-test1.test.sftlyr.ws',
-                             'ttl': 7200
-                         },)
+            'type': 'ptr',
+            'host': '100',
+            'domainId': 123456,
+            'data': 'hardware-test1.test.sftlyr.ws',
+            'ttl': 7200
+        },)
 
         result = self.run_command(['hw', 'dns-sync', '1000'])
 
@@ -749,12 +749,12 @@ def test_dns_sync_v6(self, confirm_mock):
             }
         }
         createV6args = ({
-                            'type': 'aaaa',
-                            'host': 'hardware-test1',
-                            'domainId': 12345,  # from SoftLayer_Account::getDomains
-                            'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004',
-                            'ttl': 7200
-                        },)
+            'type': 'aaaa',
+            'host': 'hardware-test1',
+            'domainId': 12345,  # from SoftLayer_Account::getDomains
+            'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004',
+            'ttl': 7200
+        },)
         server.return_value = test_server
         result = self.run_command(['hw', 'dns-sync', '--aaaa-record', '1000'])
         self.assert_no_fail(result)
diff --git a/tests/CLI/modules/tag_tests.py b/tests/CLI/modules/tag_tests.py
new file mode 100644
index 00000000..b2e29721
--- /dev/null
+++ b/tests/CLI/modules/tag_tests.py
@@ -0,0 +1,113 @@
+"""
+    SoftLayer.tests.CLI.modules.tag_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Tests for the user cli command
+"""
+import mock
+
+from SoftLayer.exceptions import SoftLayerAPIError
+from SoftLayer import testing
+
+
+class TagCLITests(testing.TestCase):
+
+    def test_list(self):
+        result = self.run_command(['tags', 'list'])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser')
+        self.assert_called_with('SoftLayer_Tag', 'getAttachedTagsForCurrentUser')
+        self.assertIn('coreos', result.output)
+
+    def test_list_detail(self):
+        result = self.run_command(['tags', 'list', '-d'])
+        self.assert_no_fail(result)
+        self.assertIn('"vs-test1.test.sftlyr.ws', result.output)  # From fixtures/virutal_guest.getObject
+        # self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser')
+        self.assert_called_with('SoftLayer_Tag', 'getAttachedTagsForCurrentUser')
+        self.assert_called_with('SoftLayer_Tag', 'getReferences', identifier=1286571)
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'getObject', identifier=33488921)
+
+    def test_list_detail_ungettable(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject')
+        mock.side_effect = SoftLayerAPIError(404, "TEST ERROR")
+        result = self.run_command(['tags', 'list', '-d'])
+        self.assert_no_fail(result)
+        self.assertIn("TEST ERROR", result.output)  # From fixtures/virutal_guest.getObject
+        # self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser')
+        self.assert_called_with('SoftLayer_Tag', 'getAttachedTagsForCurrentUser')
+        self.assert_called_with('SoftLayer_Tag', 'getReferences', identifier=1286571)
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'getObject', identifier=33488921)
+
+    @mock.patch('SoftLayer.CLI.tags.set.click')
+    def test_set_tags(self, click):
+        result = self.run_command(['tags', 'set', '--tags=tag1,tag2', '--key-name=GUEST', '--resource-id=100'])
+        click.secho.assert_called_with('Set tags successfully', fg='green')
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'setTags',  args=("tag1,tag2", "GUEST", 100), )
+
+    @mock.patch('SoftLayer.CLI.tags.set.click')
+    def test_set_tags_failure(self, click):
+        mock = self.set_mock('SoftLayer_Tag', 'setTags')
+        mock.return_value = False
+        result = self.run_command(['tags', 'set', '--tags=tag1,tag2', '--key-name=GUEST', '--resource-id=100'])
+        click.secho.assert_called_with('Failed to set tags', fg='red')
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'setTags', args=("tag1,tag2", "GUEST", 100), )
+
+    def test_details_by_name(self):
+        tag_name = 'bs_test_instance'
+        result = self.run_command(['tags', 'details', tag_name])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'getTagByTagName', args=(tag_name,))
+
+    def test_details_by_id(self):
+        tag_id = '1286571'
+        result = self.run_command(['tags', 'details', tag_id])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'getObject', identifier=tag_id)
+
+    def test_deleteTags_by_name(self):
+        result = self.run_command(['tags', 'delete', 'test'])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'deleteTag', args=('test',))
+
+    def test_deleteTags_by_id(self):
+        result = self.run_command(['tags', 'delete', '123456'])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'getObject', identifier='123456')
+        self.assert_called_with('SoftLayer_Tag', 'deleteTag', args=('bs_test_instance',))
+
+    def test_deleteTags_by_number_name(self):
+        result = self.run_command(['tags', 'delete', '123456', '--name'])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'deleteTag', args=('123456',))
+
+    @mock.patch('SoftLayer.CLI.tags.delete.click')
+    def test_deleteTags_fail(self, click):
+        mock = self.set_mock('SoftLayer_Tag', 'deleteTag')
+        mock.return_value = False
+        result = self.run_command(['tags', 'delete', '123456', '--name'])
+        click.secho.assert_called_with('Failed to remove tag 123456', fg='red')
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'deleteTag', args=('123456',))
+
+    def test_taggable(self):
+        result = self.run_command(['tags', 'taggable'])
+        self.assert_no_fail(result)
+        self.assertIn('"host14.vmware.test.com', result.output)
+        self.assert_called_with('SoftLayer_Tag', 'getAllTagTypes')
+        self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=('_objectType:SoftLayer_Hardware',))
+
+    def test_cleanup(self):
+        result = self.run_command(['tags', 'cleanup'])
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser')
+        self.assert_called_with('SoftLayer_Tag', 'deleteTag', args=('coreos',))
+
+    def test_cleanup_dry(self):
+        result = self.run_command(['tags', 'cleanup', '-d'])
+        self.assert_no_fail(result)
+        self.assertIn('(Dry Run)', result.output)
+        self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser')
+        self.assertEqual([], self.calls(service='SoftLayer_Tag',  method='deleteTag'))
diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py
index c6188338..a4bf2850 100644
--- a/tests/CLI/modules/vs/vs_tests.py
+++ b/tests/CLI/modules/vs/vs_tests.py
@@ -370,19 +370,19 @@ def test_dns_sync_both(self, confirm_mock):
                                            'getResourceRecords')
         getResourceRecords.return_value = []
         createAargs = ({
-                           'type': 'a',
-                           'host': 'vs-test1',
-                           'domainId': 12345,  # from SoftLayer_Account::getDomains
-                           'data': '172.16.240.2',
-                           'ttl': 7200
-                       },)
+            'type': 'a',
+            'host': 'vs-test1',
+            'domainId': 12345,  # from SoftLayer_Account::getDomains
+            'data': '172.16.240.2',
+            'ttl': 7200
+        },)
         createPTRargs = ({
-                             'type': 'ptr',
-                             'host': '2',
-                             'domainId': 123456,
-                             'data': 'vs-test1.test.sftlyr.ws',
-                             'ttl': 7200
-                         },)
+            'type': 'ptr',
+            'host': '2',
+            'domainId': 123456,
+            'data': 'vs-test1.test.sftlyr.ws',
+            'ttl': 7200
+        },)
 
         result = self.run_command(['vs', 'dns-sync', '100'])
 
@@ -425,12 +425,12 @@ def test_dns_sync_v6(self, confirm_mock):
             }
         }
         createV6args = ({
-                            'type': 'aaaa',
-                            'host': 'vs-test1',
-                            'domainId': 12345,
-                            'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004',
-                            'ttl': 7200
-                        },)
+            'type': 'aaaa',
+            'host': 'vs-test1',
+            'domainId': 12345,
+            'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004',
+            'ttl': 7200
+        },)
         guest.return_value = test_guest
         result = self.run_command(['vs', 'dns-sync', '--aaaa-record', '100'])
         self.assert_no_fail(result)
diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py
index f504dba9..69e7ae52 100644
--- a/tests/managers/hardware_tests.py
+++ b/tests/managers/hardware_tests.py
@@ -374,10 +374,10 @@ def test_edit(self):
         self.assert_called_with('SoftLayer_Hardware_Server',
                                 'editObject',
                                 args=({
-                                          'hostname': 'new-host',
-                                          'domain': 'new.sftlyr.ws',
-                                          'notes': 'random notes',
-                                      },),
+                                    'hostname': 'new-host',
+                                    'domain': 'new.sftlyr.ws',
+                                    'notes': 'random notes',
+                                },),
                                 identifier=100)
 
     def test_rescue(self):
diff --git a/tests/managers/tag_tests.py b/tests/managers/tag_tests.py
new file mode 100644
index 00000000..67c817a6
--- /dev/null
+++ b/tests/managers/tag_tests.py
@@ -0,0 +1,208 @@
+"""
+    SoftLayer.tests.managers.tag_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :license: MIT, see LICENSE for more details.
+"""
+
+from SoftLayer.exceptions import SoftLayerAPIError
+from SoftLayer.managers import tags
+from SoftLayer import testing
+
+
+class TagTests(testing.TestCase):
+
+    def set_up(self):
+        self.tag_manager = tags.TagManager(self.client)
+        self.test_mask = "mask[id]"
+
+    def test_list_tags(self):
+        result = self.tag_manager.list_tags()
+        self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser')
+        self.assert_called_with('SoftLayer_Tag', 'getAttachedTagsForCurrentUser')
+        self.assertIn('attached', result.keys())
+        self.assertIn('unattached', result.keys())
+
+    def test_list_tags_mask(self):
+        result = self.tag_manager.list_tags(mask=self.test_mask)
+        self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser', mask=self.test_mask)
+        self.assert_called_with('SoftLayer_Tag', 'getAttachedTagsForCurrentUser', mask=self.test_mask)
+        self.assertIn('attached', result.keys())
+        self.assertIn('unattached', result.keys())
+
+    def test_unattached_tags(self):
+        result = self.tag_manager.get_unattached_tags()
+        self.assertEqual('coreos', result[0].get('name'))
+        self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser', mask=None)
+
+    def test_unattached_tags_mask(self):
+        result = self.tag_manager.get_unattached_tags(mask=self.test_mask)
+        self.assertEqual('coreos', result[0].get('name'))
+        self.assert_called_with('SoftLayer_Tag', 'getUnattachedTagsForCurrentUser', mask=self.test_mask)
+
+    def test_attached_tags(self):
+        result = self.tag_manager.get_attached_tags()
+        self.assertEqual('bs_test_instance', result[0].get('name'))
+        self.assert_called_with('SoftLayer_Tag', 'getAttachedTagsForCurrentUser', mask=None)
+
+    def test_attached_tags_mask(self):
+        result = self.tag_manager.get_attached_tags(mask=self.test_mask)
+        self.assertEqual('bs_test_instance', result[0].get('name'))
+        self.assert_called_with('SoftLayer_Tag', 'getAttachedTagsForCurrentUser', mask=self.test_mask)
+
+    def test_get_tag_references(self):
+        tag_id = 1286571
+        result = self.tag_manager.get_tag_references(tag_id)
+        self.assertEqual(tag_id, result[0].get('tagId'))
+        self.assert_called_with('SoftLayer_Tag', 'getReferences', identifier=tag_id)
+
+    def test_get_tag_references_mask(self):
+        tag_id = 1286571
+        result = self.tag_manager.get_tag_references(tag_id, mask=self.test_mask)
+        self.assertEqual(tag_id, result[0].get('tagId'))
+        self.assert_called_with('SoftLayer_Tag', 'getReferences', identifier=tag_id, mask=self.test_mask)
+
+    def test_reference_lookup_hardware(self):
+        resource_id = 12345
+        tag_type = 'HARDWARE'
+
+        self.tag_manager.reference_lookup(resource_id, tag_type)
+        self.assert_called_with('SoftLayer_Hardware', 'getObject', identifier=resource_id)
+
+    def test_reference_lookup_guest(self):
+        resource_id = 12345
+        tag_type = 'GUEST'
+
+        self.tag_manager.reference_lookup(resource_id, tag_type)
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'getObject', identifier=resource_id)
+
+    def test_reference_lookup_app_delivery(self):
+        resource_id = 12345
+        tag_type = 'APPLICATION_DELIVERY_CONTROLLER'
+
+        self.tag_manager.reference_lookup(resource_id, tag_type)
+        self.assert_called_with('SoftLayer_Network_Application_Delivery_Controller',
+                                'getObject', identifier=resource_id)
+
+    def test_reference_lookup_dedicated(self):
+        resource_id = 12345
+        tag_type = 'DEDICATED_HOST'
+
+        self.tag_manager.reference_lookup(resource_id, tag_type)
+        self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'getObject', identifier=resource_id)
+
+    def test_reference_lookup_document(self):
+        resource_id = 12345
+        tag_type = 'ACCOUNT_DOCUMENT'
+
+        exception = self.assertRaises(
+            SoftLayerAPIError,
+            self.tag_manager.reference_lookup,
+            resource_id,
+            tag_type
+        )
+        self.assertEqual(exception.faultCode, 404)
+        self.assertEqual(exception.reason, "Unable to lookup ACCOUNT_DOCUMENT types")
+
+    def test_set_tags(self):
+        tags = "tag1,tag2"
+        key_name = "GUEST"
+        resource_id = 100
+
+        self.tag_manager.set_tags(tags, key_name, resource_id)
+        self.assert_called_with('SoftLayer_Tag', 'setTags')
+
+    def test_get_tag(self):
+        tag_id = 1286571
+        result = self.tag_manager.get_tag(tag_id)
+        self.assertEqual(tag_id, result.get('id'))
+        self.assert_called_with('SoftLayer_Tag', 'getObject', identifier=tag_id)
+
+    def test_get_tag_mask(self):
+        tag_id = 1286571
+        result = self.tag_manager.get_tag(tag_id, mask=self.test_mask)
+        self.assertEqual(tag_id, result.get('id'))
+        self.assert_called_with('SoftLayer_Tag', 'getObject', identifier=tag_id, mask=self.test_mask)
+
+    def test_get_tag_by_name(self):
+        tag_name = 'bs_test_instance'
+        result = self.tag_manager.get_tag_by_name(tag_name)
+        args = (tag_name,)
+        self.assertEqual(tag_name, result[0].get('name'))
+        self.assert_called_with('SoftLayer_Tag', 'getTagByTagName', args=args)
+
+    def test_get_tag_by_name_mask(self):
+        tag_name = 'bs_test_instance'
+        result = self.tag_manager.get_tag_by_name(tag_name, mask=self.test_mask)
+        args = (tag_name,)
+        self.assertEqual(tag_name, result[0].get('name'))
+        self.assert_called_with('SoftLayer_Tag', 'getTagByTagName', mask=self.test_mask, args=args)
+
+    def test_taggable_by_type_main(self):
+        result = self.tag_manager.taggable_by_type("HARDWARE")
+        self.assertEqual("SoftLayer_Hardware", result[0].get('resourceType'))
+        self.assert_called_with('SoftLayer_Search', 'advancedSearch', args=('_objectType:SoftLayer_Hardware',))
+
+    def test_taggable_by_type_ticket(self):
+        mock = self.set_mock('SoftLayer_Search', 'advancedSearch')
+        mock.return_value = [
+            {
+                "resourceType": "SoftLayer_Ticket",
+                "resource": {
+                    "domain": "vmware.test.com",
+                }
+            }
+        ]
+
+        result = self.tag_manager.taggable_by_type("TICKET")
+        self.assertEqual("SoftLayer_Ticket", result[0].get('resourceType'))
+        self.assert_called_with('SoftLayer_Search', 'advancedSearch',
+                                args=('_objectType:SoftLayer_Ticket status.name: open',))
+
+    def test_taggable_by_type_image_template(self):
+        result = self.tag_manager.taggable_by_type("IMAGE_TEMPLATE")
+        self.assertEqual("Virtual_Guest_Block_Device_Template_Group", result[0].get('resourceType'))
+        self.assert_called_with('SoftLayer_Account', 'getPrivateBlockDeviceTemplateGroups')
+
+    def test_taggable_by_type_network_subnet(self):
+        result = self.tag_manager.taggable_by_type("NETWORK_SUBNET")
+        self.assertEqual("Network_Subnet", result[0].get('resourceType'))
+        self.assert_called_with('SoftLayer_Account', 'getSubnets')
+
+    def test_type_to_service(self):
+        in_out = [
+            {'input': 'ACCOUNT_DOCUMENT', 'output': None},
+            {'input': 'APPLICATION_DELIVERY_CONTROLLER', 'output': 'Network_Application_Delivery_Controller'},
+            {'input': 'GUEST', 'output': 'Virtual_Guest'},
+            {'input': 'DEDICATED_HOST', 'output': 'Virtual_DedicatedHost'},
+            {'input': 'IMAGE_TEMPLATE', 'output': 'Virtual_Guest_Block_Device_Template_Group'},
+            {'input': 'HARDWARE', 'output': 'Hardware'},
+            {'input': 'NETWORK_VLAN', 'output': 'Network_Vlan'},
+        ]
+
+        for test in in_out:
+            result = self.tag_manager.type_to_service(test.get('input'))
+            self.assertEqual(test.get('output'), result)
+
+    def test_get_resource_name(self):
+        resource = {
+            'primaryIpAddress': '4.4.4.4',
+            'vlanNumber': '12345',
+            'name': 'testName',
+            'subject': 'TEST SUBJECT',
+            'networkIdentifier': '127.0.0.1',
+            'fullyQualifiedDomainName': 'test.test.com'
+        }
+        in_out = [
+            {'input': 'NETWORK_VLAN_FIREWALL', 'output': resource.get('primaryIpAddress')},
+            {'input': 'NETWORK_VLAN', 'output': "{} ({})".format(resource.get('vlanNumber'), resource.get('name'))},
+            {'input': 'IMAGE_TEMPLATE', 'output': resource.get('name')},
+            {'input': 'APPLICATION_DELIVERY_CONTROLLER', 'output': resource.get('name')},
+            {'input': 'TICKET', 'output': resource.get('subjet')},
+            {'input': 'NETWORK_SUBNET', 'output': resource.get('networkIdentifier')},
+            {'input': 'HARDWARE', 'output': resource.get('fullyQualifiedDomainName')},
+        ]
+
+        for test in in_out:
+            result = self.tag_manager.get_resource_name(resource, test.get('input'))
+            self.assertEqual(test.get('output'), result)
diff --git a/tests/managers/user_tests.py b/tests/managers/user_tests.py
index b75a5a77..b0ab015f 100644
--- a/tests/managers/user_tests.py
+++ b/tests/managers/user_tests.py
@@ -231,7 +231,7 @@ def test_vpn_subnet_add(self):
         subnet_id = 1234
         expected_args = (
             [{"userId": user_id, "subnetId": subnet_id}],
-            )
+        )
         self.manager.vpn_subnet_add(user_id, [subnet_id])
         self.assert_called_with('SoftLayer_Network_Service_Vpn_Overrides', 'createObjects', args=expected_args)
         self.assert_called_with('SoftLayer_User_Customer', 'updateVpnUser', identifier=user_id)
@@ -242,7 +242,7 @@ def test_vpn_subnet_remove(self):
         overrides = [{'id': 3661234, 'subnetId': subnet_id}]
         expected_args = (
             overrides,
-            )
+        )
         self.manager.vpn_subnet_remove(user_id, [subnet_id])
         self.assert_called_with('SoftLayer_Network_Service_Vpn_Overrides', 'deleteObjects', args=expected_args)
         self.assert_called_with('SoftLayer_User_Customer', 'updateVpnUser', identifier=user_id)