From 57e5840710c3b2b74d31bfd6a0da739e0fc747ed Mon Sep 17 00:00:00 2001
From: Akihiro Motoki <amotoki@gmail.com>
Date: Sat, 29 Apr 2017 00:32:32 +0000
Subject: [PATCH] Network tag support

Neutron tag mechanism now supports network, subnet, port,
subnetpool and router. Tag support for more resources is planned.

This commit introduces a common mixin class to implement
tag operation and individual resource consumes it.

To support tag remove, network unset command is added.

Implements blueprint neutron-client-tag
Change-Id: Iad59d052f46896d27d73c22d6d4bb3df889f2352
---
 doc/source/cli/command-objects/network.rst    |  79 ++++++++
 doc/source/cli/command-objects/port.rst       |  47 +++++
 doc/source/cli/command-objects/router.rst     |  47 +++++
 .../cli/command-objects/subnet-pool.rst       |  47 +++++
 doc/source/cli/command-objects/subnet.rst     |  47 +++++
 openstackclient/network/v2/_tag.py            | 134 ++++++++++++
 openstackclient/network/v2/network.py         |  43 +++-
 openstackclient/network/v2/port.py            |  24 ++-
 openstackclient/network/v2/router.py          |  19 +-
 openstackclient/network/v2/subnet.py          |  21 +-
 openstackclient/network/v2/subnet_pool.py     |  21 +-
 .../tests/functional/network/v2/common.py     |  81 ++++++++
 .../functional/network/v2/test_network.py     |   4 +-
 .../tests/functional/network/v2/test_port.py  |  13 +-
 .../functional/network/v2/test_router.py      |   4 +-
 .../functional/network/v2/test_subnet.py      |  10 +-
 .../functional/network/v2/test_subnet_pool.py |   8 +-
 .../tests/unit/network/v2/_test_tag.py        | 190 ++++++++++++++++++
 .../tests/unit/network/v2/fakes.py            |   5 +
 .../tests/unit/network/v2/test_network.py     |  83 +++++++-
 .../tests/unit/network/v2/test_port.py        | 115 +++++++----
 .../tests/unit/network/v2/test_router.py      |  62 +++++-
 .../tests/unit/network/v2/test_subnet.py      |  80 ++++++--
 .../tests/unit/network/v2/test_subnet_pool.py |  70 ++++++-
 openstackclient/tests/unit/utils.py           |   6 +
 ...p-neutron-client-tag-ff24d13e5c70e052.yaml |  13 ++
 setup.cfg                                     |   1 +
 27 files changed, 1172 insertions(+), 102 deletions(-)
 create mode 100644 openstackclient/network/v2/_tag.py
 create mode 100644 openstackclient/tests/unit/network/v2/_test_tag.py
 create mode 100644 releasenotes/notes/bp-neutron-client-tag-ff24d13e5c70e052.yaml

diff --git a/doc/source/cli/command-objects/network.rst b/doc/source/cli/command-objects/network.rst
index ed9fd13d1d..5f20dc3884 100644
--- a/doc/source/cli/command-objects/network.rst
+++ b/doc/source/cli/command-objects/network.rst
@@ -32,6 +32,7 @@ Create new network
         [--provider-segment <provider-segment>]
         [--qos-policy <qos-policy>]
         [--transparent-vlan | --no-transparent-vlan]
+        [--tag <tag> | --no-tag]
         <name>
 
 .. option:: --project <project>
@@ -165,6 +166,18 @@ Create new network
 
     *Network version 2 only*
 
+.. option:: --tag <tag>
+
+    Tag to be added to the network (repeat option to set multiple tags)
+
+    *Network version 2 only*
+
+.. option:: --no-tag
+
+    No tags associated with the network
+
+    *Network version 2 only*
+
 .. _network_create-name:
 .. describe:: <name>
 
@@ -206,6 +219,8 @@ List networks
         [--provider-physical-network <provider-physical-network>]
         [--provider-segment <provider-segment>]
         [--agent <agent-id>]
+        [--tags <tag>[,<tag>,...]] [--any-tags <tag>[,<tag>,...]]
+        [--not-tags <tag>[,<tag>,...]] [--not-any-tags <tag>[,<tag>,...]]
 
 .. option:: --external
 
@@ -297,6 +312,32 @@ List networks
 
     List networks hosted by agent (ID only)
 
+    *Network version 2 only*
+
+.. option:: --tags <tag>[,<tag>,...]
+
+    List networks which have all given tag(s)
+
+    *Network version 2 only*
+
+.. option:: --any-tags <tag>[,<tag>,...]
+
+    List networks which have any given tag(s)
+
+    *Network version 2 only*
+
+.. option:: --not-tags <tag>[,<tag>,...]
+
+    Exclude networks which have all given tag(s)
+
+    *Network version 2 only*
+
+.. option:: --not-any-tags <tag>[,<tag>,...]
+
+    Exclude networks which have any given tag(s)
+
+    *Network version 2 only*
+
 network set
 -----------
 
@@ -318,6 +359,7 @@ Set network properties
         [--provider-physical-network <provider-physical-network>]
         [--provider-segment <provider-segment>]
         [--qos-policy <qos-policy> | --no-qos-policy]
+        [--tag <tag>] [--no-tag]
         <network>
 
 .. option:: --name <name>
@@ -392,6 +434,15 @@ Set network properties
 
     Remove the QoS policy attached to this network
 
+.. option:: --tag <tag>
+
+    Tag to be added to the network (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    Clear tags associated with the network. Specify both --tag
+    and --no-tag to overwrite current tags
+
 .. _network_set-network:
 .. describe:: <network>
 
@@ -412,3 +463,31 @@ Display network details
 .. describe:: <network>
 
     Network to display (name or ID)
+
+network unset
+-------------
+
+Unset network properties
+
+*Network version 2 only*
+
+.. program:: network unset
+.. code:: bash
+
+    openstack network unset
+        [--tag <tag> | --all-tag]
+        <network>
+
+.. option:: --tag <tag>
+
+    Tag to be removed from the network
+    (repeat option to remove multiple tags)
+
+.. option:: --all-tag
+
+    Clear all tags associated with the network
+
+.. _network_unset-network:
+.. describe:: <network>
+
+    Network to modify (name or ID)
diff --git a/doc/source/cli/command-objects/port.rst b/doc/source/cli/command-objects/port.rst
index 37814a9595..c2da09b321 100644
--- a/doc/source/cli/command-objects/port.rst
+++ b/doc/source/cli/command-objects/port.rst
@@ -33,6 +33,7 @@ Create new port
         [--qos-policy <qos-policy>]
         [--project <project> [--project-domain <project-domain>]]
         [--enable-port-security | --disable-port-security]
+        [--tag <tag> | --no-tag]
         <name>
 
 .. option:: --network <network>
@@ -126,6 +127,14 @@ Create new port
 
     Disable port security for this port
 
+.. option:: --tag <tag>
+
+    Tag to be added to the port (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    No tags associated with the port
+
 .. _port_create-name:
 .. describe:: <name>
 
@@ -163,6 +172,8 @@ List ports
         [--fixed-ip subnet=<subnet>,ip-address=<ip-address>]
         [--long]
         [--project <project> [--project-domain <project-domain>]]
+        [--tags <tag>[,<tag>,...]] [--any-tags <tag>[,<tag>,...]]
+        [--not-tags <tag>[,<tag>,...]] [--not-any-tags <tag>[,<tag>,...]]
 
 .. option:: --device-owner <device-owner>
 
@@ -204,6 +215,22 @@ List ports
     Domain the project belongs to (name or ID).
     This can be used in case collisions between project names exist.
 
+.. option:: --tags <tag>[,<tag>,...]
+
+    List ports which have all given tag(s)
+
+.. option:: --any-tags <tag>[,<tag>,...]
+
+    List ports which have any given tag(s)
+
+.. option:: --not-tags <tag>[,<tag>,...]
+
+    Exclude ports which have all given tag(s)
+
+.. option:: --not-any-tags <tag>[,<tag>,...]
+
+    Exclude ports which have any given tag(s)
+
 port set
 --------
 
@@ -233,6 +260,7 @@ Set port properties
         [--allowed-address ip-address=<ip-address>[,mac-address=<mac-address>]]
         [--no-allowed-address]
         [--data-plane-status <status>]
+        [--tag <tag>] [--no-tag]
         <port>
 
 .. option:: --description <description>
@@ -342,6 +370,15 @@ Set port properties
     Unset it to None with the 'port unset' command
     (requires data plane status extension)
 
+.. option:: --tag <tag>
+
+    Tag to be added to the port (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    Clear tags associated with the port. Specify both --tag
+    and --no-tag to overwrite current tags
+
 .. _port_set-port:
 .. describe:: <port>
 
@@ -378,6 +415,7 @@ Unset port properties
         [--allowed-address ip-address=<ip-address>[,mac-address=<mac-address>] [...]]
         [--qos-policy]
         [--data-plane-status]
+        [--tag <tag> | --all-tag]
         <port>
 
 .. option:: --fixed-ip subnet=<subnet>,ip-address=<ip-address>
@@ -410,6 +448,15 @@ Unset port properties
 
     Clear existing information of data plane status
 
+.. option:: --tag <tag>
+
+    Tag to be removed from the port
+    (repeat option to remove multiple tags)
+
+.. option:: --all-tag
+
+    Clear all tags associated with the port
+
 .. _port_unset-port:
 .. describe:: <port>
 
diff --git a/doc/source/cli/command-objects/router.rst b/doc/source/cli/command-objects/router.rst
index 8bdf81dbf7..9c9364bc75 100644
--- a/doc/source/cli/command-objects/router.rst
+++ b/doc/source/cli/command-objects/router.rst
@@ -67,6 +67,7 @@ Create new router
         [--ha | --no-ha]
         [--description <description>]
         [--availability-zone-hint <availability-zone>]
+        [--tag <tag> | --no-tag]
         <name>
 
 .. option:: --project <project>
@@ -121,6 +122,14 @@ Create new router
     (Router Availability Zone extension required,
     repeat option to set multiple availability zones)
 
+.. option:: --tag <tag>
+
+    Tag to be added to the router (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    No tags associated with the router
+
 .. _router_create-name:
 .. describe:: <name>
 
@@ -156,6 +165,8 @@ List routers
         [--long]
         [--project <project> [--project-domain <project-domain>]]
         [--agent <agent-id>]
+        [--tags <tag>[,<tag>,...]] [--any-tags <tag>[,<tag>,...]]
+        [--not-tags <tag>[,<tag>,...]] [--not-any-tags <tag>[,<tag>,...]]
 
 .. option:: --agent <agent-id>
 
@@ -186,6 +197,22 @@ List routers
     Domain the project belongs to (name or ID).
     This can be used in case collisions between project names exist.
 
+.. option:: --tags <tag>[,<tag>,...]
+
+    List routers which have all given tag(s)
+
+.. option:: --any-tags <tag>[,<tag>,...]
+
+    List routers which have any given tag(s)
+
+.. option:: --not-tags <tag>[,<tag>,...]
+
+    Exclude routers which have all given tag(s)
+
+.. option:: --not-any-tags <tag>[,<tag>,...]
+
+    Exclude routers which have any given tag(s)
+
 router remove port
 ------------------
 
@@ -246,6 +273,7 @@ Set router properties
         [--route destination=<subnet>,gateway=<ip-address> | --no-route]
         [--ha | --no-ha]
         [--external-gateway <network> [--enable-snat|--disable-snat] [--fixed-ip subnet=<subnet>,ip-address=<ip-address>]]
+        [--tag <tag>] [--no-tag]
         <router>
 
 .. option:: --name <name>
@@ -311,6 +339,15 @@ Set router properties
     subnet=<subnet>,ip-address=<ip-address>
     (repeat option to set multiple fixed IP addresses)
 
+.. option:: --tag <tag>
+
+    Tag to be added to the router (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    Clear tags associated with the router. Specify both --tag
+    and --no-tag to overwrite current tags
+
 .. _router_set-router:
 .. describe:: <router>
 
@@ -343,6 +380,7 @@ Unset router properties
     openstack router unset
         [--route destination=<subnet>,gateway=<ip-address>]
         [--external-gateway]
+        [--tag <tag> | --all-tag]
         <router>
 
 .. option:: --route destination=<subnet>,gateway=<ip-address>
@@ -356,6 +394,15 @@ Unset router properties
 
     Remove external gateway information from the router
 
+.. option:: --tag <tag>
+
+    Tag to be removed from the router
+    (repeat option to remove multiple tags)
+
+.. option:: --all-tag
+
+    Clear all tags associated with the router
+
 .. _router_unset-router:
 .. describe:: <router>
 
diff --git a/doc/source/cli/command-objects/subnet-pool.rst b/doc/source/cli/command-objects/subnet-pool.rst
index 3a60974a97..0cff4d7f56 100644
--- a/doc/source/cli/command-objects/subnet-pool.rst
+++ b/doc/source/cli/command-objects/subnet-pool.rst
@@ -25,6 +25,7 @@ Create subnet pool
         [--default | --no-default]
         [--share | --no-share]
         [--default-quota <num-ip-addresses>]
+        [--tag <tag> | --no-tag]
         --pool-prefix <pool-prefix> [...]
         <name>
 
@@ -79,6 +80,14 @@ Create subnet pool
     Set default quota for subnet pool as the number of
     IP addresses allowed in a subnet
 
+.. option:: --tag <tag>
+
+    Tag to be added to the subnet pool (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    No tags associated with the subnet pool
+
 .. option:: --pool-prefix <pool-prefix>
 
     Set subnet pool prefixes (in CIDR notation)
@@ -120,6 +129,8 @@ List subnet pools
         [--project <project> [--project-domain <project-domain>]]
         [--name <name>]
         [--address-scope <address-scope>]
+        [--tags <tag>[,<tag>,...]] [--any-tags <tag>[,<tag>,...]]
+        [--not-tags <tag>[,<tag>,...]] [--not-any-tags <tag>[,<tag>,...]]
 
 .. option:: --long
 
@@ -158,6 +169,22 @@ List subnet pools
 
     List only subnet pools of given address scope in output (name or ID)
 
+.. option:: --tags <tag>[,<tag>,...]
+
+    List subnet pools which have all given tag(s)
+
+.. option:: --any-tags <tag>[,<tag>,...]
+
+    List subnet pools which have any given tag(s)
+
+.. option:: --not-tags <tag>[,<tag>,...]
+
+    Exclude subnet pools which have all given tag(s)
+
+.. option:: --not-any-tags <tag>[,<tag>,...]
+
+    Exclude subnet pools which have any given tag(s)
+
 subnet pool set
 ---------------
 
@@ -176,6 +203,7 @@ Set subnet pool properties
         [--default | --no-default]
         [--description <description>]
         [--default-quota <num-ip-addresses>]
+        [--tag <tag>] [--no-tag]
         <subnet-pool>
 
 .. option:: --name <name>
@@ -225,6 +253,15 @@ Set subnet pool properties
     Set default quota for subnet pool as the number of
     IP addresses allowed in a subnet
 
+.. option:: --tag <tag>
+
+    Tag to be added to the subnet pool (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    Clear tags associated with the subnet pool. Specify both --tag
+    and --no-tag to overwrite current tags
+
 .. _subnet_pool_set-subnet-pool:
 .. describe:: <subnet-pool>
 
@@ -256,6 +293,7 @@ Unset subnet pool properties
 
     openstack subnet pool unset
         [--pool-prefix <pool-prefix> [...]]
+        [--tag <tag> | --all-tag]
         <subnet-pool>
 
 .. option:: --pool-prefix <pool-prefix>
@@ -263,6 +301,15 @@ Unset subnet pool properties
     Remove subnet pool prefixes (in CIDR notation).
     (repeat option to unset multiple prefixes).
 
+.. option:: --tag <tag>
+
+    Tag to be removed from the subnet pool
+    (repeat option to remove multiple tags)
+
+.. option:: --all-tag
+
+    Clear all tags associated with the subnet pool
+
 .. _subnet_pool_unset-subnet-pool:
 .. describe:: <subnet-pool>
 
diff --git a/doc/source/cli/command-objects/subnet.rst b/doc/source/cli/command-objects/subnet.rst
index 4e60936120..c228dc207d 100644
--- a/doc/source/cli/command-objects/subnet.rst
+++ b/doc/source/cli/command-objects/subnet.rst
@@ -31,6 +31,7 @@ Create new subnet
         [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}]
         [--network-segment <network-segment>]
         [--service-type <service-type>]
+        [--tag <tag> | --no-tag]
         --network <network>
         <name>
 
@@ -125,6 +126,14 @@ Create new subnet
      Must be a valid device owner value for a network port
      (repeat option to set multiple service types)
 
+.. option:: --tag <tag>
+
+    Tag to be added to the subnet (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    No tags associated with the subnet
+
 .. option:: --network <network>
 
      Network this subnet belongs to (name or ID)
@@ -167,6 +176,8 @@ List subnets
         [--gateway <gateway>]
         [--name <name>]
         [--subnet-range <subnet-range>]
+        [--tags <tag>[,<tag>,...]] [--any-tags <tag>[,<tag>,...]]
+        [--not-tags <tag>[,<tag>,...]] [--not-any-tags <tag>[,<tag>,...]]
 
 .. option:: --long
 
@@ -218,6 +229,22 @@ List subnets
     List only subnets of given subnet range (in CIDR notation) in output
     e.g.: ``--subnet-range 10.10.0.0/16``
 
+.. option:: --tags <tag>[,<tag>,...]
+
+    List subnets which have all given tag(s)
+
+.. option:: --any-tags <tag>[,<tag>,...]
+
+    List subnets which have any given tag(s)
+
+.. option:: --not-tags <tag>[,<tag>,...]
+
+    Exclude subnets which have all given tag(s)
+
+.. option:: --not-any-tags <tag>[,<tag>,...]
+
+    Exclude subnets which have any given tag(s)
+
 subnet set
 ----------
 
@@ -238,6 +265,7 @@ Set subnet properties
         [--service-type <service-type>]
         [--name <new-name>]
         [--description <description>]
+        [--tag <tag>] [--no-tag]
         <subnet>
 
 .. option:: --allocation-pool start=<ip-address>,end=<ip-address>
@@ -305,6 +333,15 @@ Set subnet properties
 
      Updated name of the subnet
 
+.. option:: --tag <tag>
+
+    Tag to be added to the subnet (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    Clear tags associated with the subnet. Specify both --tag
+    and --no-tag to overwrite current tags
+
 .. _subnet_set-subnet:
 .. describe:: <subnet>
 
@@ -340,6 +377,7 @@ Unset subnet properties
         [--dns-nameserver <dns-nameserver> [...]]
         [--host-route destination=<subnet>,gateway=<ip-address> [...]]
         [--service-type <service-type>]
+        [--tag <tag> | --all-tag]
         <subnet>
 
 .. option:: --dns-nameserver <dns-nameserver>
@@ -368,6 +406,15 @@ Unset subnet properties
      Must be a valid device owner value for a network port
      (repeat option to unset multiple service types)
 
+.. option:: --tag <tag>
+
+    Tag to be removed from the subnet
+    (repeat option to remove multiple tags)
+
+.. option:: --all-tag
+
+    Clear all tags associated with the subnet
+
 .. _subnet_unset-subnet:
 .. describe:: <subnet>
 
diff --git a/openstackclient/network/v2/_tag.py b/openstackclient/network/v2/_tag.py
new file mode 100644
index 0000000000..d1e59937fa
--- /dev/null
+++ b/openstackclient/network/v2/_tag.py
@@ -0,0 +1,134 @@
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+import argparse
+
+from openstackclient.i18n import _
+
+
+class _CommaListAction(argparse.Action):
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        setattr(namespace, self.dest, values.split(','))
+
+
+def add_tag_filtering_option_to_parser(parser, collection_name):
+    parser.add_argument(
+        '--tags',
+        metavar='<tag>[,<tag>,...]',
+        action=_CommaListAction,
+        help=_('List %s which have all given tag(s) '
+               '(Comma-separated list of tags)') % collection_name
+    )
+    parser.add_argument(
+        '--any-tags',
+        metavar='<tag>[,<tag>,...]',
+        action=_CommaListAction,
+        help=_('List %s which have any given tag(s) '
+               '(Comma-separated list of tags)') % collection_name
+    )
+    parser.add_argument(
+        '--not-tags',
+        metavar='<tag>[,<tag>,...]',
+        action=_CommaListAction,
+        help=_('Exclude %s which have all given tag(s) '
+               '(Comma-separated list of tags)') % collection_name
+    )
+    parser.add_argument(
+        '--not-any-tags',
+        metavar='<tag>[,<tag>,...]',
+        action=_CommaListAction,
+        help=_('Exclude %s which have any given tag(s) '
+               '(Comma-separated list of tags)') % collection_name
+    )
+
+
+def get_tag_filtering_args(parsed_args, args):
+    if parsed_args.tags:
+        args['tags'] = ','.join(parsed_args.tags)
+    if parsed_args.any_tags:
+        args['any_tags'] = ','.join(parsed_args.any_tags)
+    if parsed_args.not_tags:
+        args['not_tags'] = ','.join(parsed_args.not_tags)
+    if parsed_args.not_any_tags:
+        args['not_any_tags'] = ','.join(parsed_args.not_any_tags)
+
+
+def add_tag_option_to_parser_for_create(parser, resource_name):
+    tag_group = parser.add_mutually_exclusive_group()
+    tag_group.add_argument(
+        '--tag',
+        action='append',
+        dest='tags',
+        metavar='<tag>',
+        help=_("Tag to be added to the %s "
+               "(repeat option to set multiple tags)") % resource_name
+    )
+    tag_group.add_argument(
+        '--no-tag',
+        action='store_true',
+        help=_("No tags associated with the %s") % resource_name
+    )
+
+
+def add_tag_option_to_parser_for_set(parser, resource_name):
+    parser.add_argument(
+        '--tag',
+        action='append',
+        dest='tags',
+        metavar='<tag>',
+        help=_("Tag to be added to the %s "
+               "(repeat option to set multiple tags)") % resource_name
+    )
+    parser.add_argument(
+        '--no-tag',
+        action='store_true',
+        help=_("Clear tags associated with the %s. Specify both "
+               "--tag and --no-tag to overwrite current tags") % resource_name
+    )
+
+
+def update_tags_for_set(client, obj, parsed_args):
+    if parsed_args.no_tag:
+        tags = set()
+    else:
+        tags = set(obj.tags)
+    if parsed_args.tags:
+        tags |= set(parsed_args.tags)
+    if set(obj.tags) != tags:
+        client.set_tags(obj, list(tags))
+
+
+def add_tag_option_to_parser_for_unset(parser, resource_name):
+    tag_group = parser.add_mutually_exclusive_group()
+    tag_group.add_argument(
+        '--tag',
+        action='append',
+        dest='tags',
+        metavar='<tag>',
+        help=_("Tag to be removed from the %s "
+               "(repeat option to remove multiple tags)") % resource_name)
+    tag_group.add_argument(
+        '--all-tag',
+        action='store_true',
+        help=_("Clear all tags associated with the %s") % resource_name)
+
+
+def update_tags_for_unset(client, obj, parsed_args):
+    tags = set(obj.tags)
+    if parsed_args.all_tag:
+        tags = set()
+    if parsed_args.tags:
+        tags -= set(parsed_args.tags)
+    if set(obj.tags) != tags:
+        client.set_tags(obj, list(tags))
diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py
index 33decd8213..4c1725c5f6 100644
--- a/openstackclient/network/v2/network.py
+++ b/openstackclient/network/v2/network.py
@@ -20,6 +20,7 @@ from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 from openstackclient.network import common
 from openstackclient.network import sdk_utils
+from openstackclient.network.v2 import _tag
 
 
 def _format_admin_state(item):
@@ -280,6 +281,7 @@ class CreateNetwork(common.NetworkAndComputeShowOne):
             help=_("Do not make the network VLAN transparent"))
 
         _add_additional_network_options(parser)
+        _tag.add_tag_option_to_parser_for_create(parser, _('network'))
         return parser
 
     def update_parser_compute(self, parser):
@@ -299,6 +301,8 @@ class CreateNetwork(common.NetworkAndComputeShowOne):
             attrs['vlan_transparent'] = False
 
         obj = client.create_network(**attrs)
+        # tags cannot be set when created, so tags need to be set later.
+        _tag.update_tags_for_set(client, obj, parsed_args)
         display_columns, columns = _get_columns_network(obj)
         data = utils.get_item_properties(obj, columns, formatters=_formatters)
         return (display_columns, data)
@@ -424,7 +428,9 @@ class ListNetwork(common.NetworkAndComputeLister):
             '--agent',
             metavar='<agent-id>',
             dest='agent_id',
-            help=_('List networks hosted by agent (ID only)'))
+            help=_('List networks hosted by agent (ID only)')
+        )
+        _tag.add_tag_filtering_option_to_parser(parser, _('networks'))
         return parser
 
     def take_action_network(self, client, parsed_args):
@@ -441,6 +447,7 @@ class ListNetwork(common.NetworkAndComputeLister):
                 'provider_network_type',
                 'is_router_external',
                 'availability_zones',
+                'tags',
             )
             column_headers = (
                 'ID',
@@ -453,6 +460,7 @@ class ListNetwork(common.NetworkAndComputeLister):
                 'Network Type',
                 'Router Type',
                 'Availability Zones',
+                'Tags',
             )
         elif parsed_args.agent_id:
             columns = (
@@ -534,6 +542,8 @@ class ListNetwork(common.NetworkAndComputeLister):
             args['provider:segmentation_id'] = parsed_args.segmentation_id
             args['provider_segmentation_id'] = parsed_args.segmentation_id
 
+        _tag.get_tag_filtering_args(parsed_args, args)
+
         data = client.networks(**args)
 
         return (column_headers,
@@ -656,6 +666,7 @@ class SetNetwork(command.Command):
             action='store_true',
             help=_("Remove the QoS policy attached to this network")
         )
+        _tag.add_tag_option_to_parser_for_set(parser, _('network'))
         _add_additional_network_options(parser)
         return parser
 
@@ -664,7 +675,11 @@ class SetNetwork(command.Command):
         obj = client.find_network(parsed_args.network, ignore_missing=False)
 
         attrs = _get_attrs_network(self.app.client_manager, parsed_args)
-        client.update_network(obj, **attrs)
+        if attrs:
+            client.update_network(obj, **attrs)
+
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_set(client, obj, parsed_args)
 
 
 class ShowNetwork(common.NetworkAndComputeShowOne):
@@ -689,3 +704,27 @@ class ShowNetwork(common.NetworkAndComputeShowOne):
         display_columns, columns = _get_columns_compute(obj)
         data = utils.get_dict_properties(obj, columns)
         return (display_columns, data)
+
+
+class UnsetNetwork(command.Command):
+    _description = _("Unset network properties")
+
+    def get_parser(self, prog_name):
+        parser = super(UnsetNetwork, self).get_parser(prog_name)
+        parser.add_argument(
+            'network',
+            metavar="<network>",
+            help=_("Network to modify (name or ID)")
+        )
+        _tag.add_tag_option_to_parser_for_unset(parser, _('network'))
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.network
+        obj = client.find_network(parsed_args.network, ignore_missing=False)
+
+        # NOTE: As of now, UnsetNetwork has no attributes which need
+        # to be updated by update_network().
+
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_unset(client, obj, parsed_args)
diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py
index d7f197e016..9536fe8687 100644
--- a/openstackclient/network/v2/port.py
+++ b/openstackclient/network/v2/port.py
@@ -26,6 +26,7 @@ from osc_lib import utils
 from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 from openstackclient.network import sdk_utils
+from openstackclient.network.v2 import _tag
 
 
 LOG = logging.getLogger(__name__)
@@ -47,6 +48,7 @@ _formatters = {
     'extra_dhcp_opts': utils.format_list_of_dicts,
     'fixed_ips': utils.format_list_of_dicts,
     'security_group_ids': utils.format_list,
+    'tags': utils.format_list,
 }
 
 
@@ -384,6 +386,7 @@ class CreatePort(command.ShowOne):
                    "ip-address=<ip-address>[,mac-address=<mac-address>] "
                    "(repeat option to set multiple allowed-address pairs)")
         )
+        _tag.add_tag_option_to_parser_for_create(parser, _('port'))
         return parser
 
     def take_action(self, parsed_args):
@@ -416,6 +419,8 @@ class CreatePort(command.ShowOne):
             attrs['qos_policy_id'] = client.find_qos_policy(
                 parsed_args.qos_policy, ignore_missing=False).id
         obj = client.create_port(**attrs)
+        # tags cannot be set when created, so tags need to be set later.
+        _tag.update_tags_for_set(client, obj, parsed_args)
         display_columns, columns = _get_columns(obj)
         data = utils.get_item_properties(obj, columns, formatters=_formatters)
 
@@ -512,6 +517,7 @@ class ListPort(command.Lister):
                    "(name or ID): subnet=<subnet>,ip-address=<ip-address> "
                    "(repeat option to set multiple fixed IP addresses)"),
         )
+        _tag.add_tag_filtering_option_to_parser(parser, _('ports'))
         return parser
 
     def take_action(self, parsed_args):
@@ -535,8 +541,8 @@ class ListPort(command.Lister):
 
         filters = {}
         if parsed_args.long:
-            columns += ('security_group_ids', 'device_owner',)
-            column_headers += ('Security Groups', 'Device Owner',)
+            columns += ('security_group_ids', 'device_owner', 'tags')
+            column_headers += ('Security Groups', 'Device Owner', 'Tags')
         if parsed_args.device_owner is not None:
             filters['device_owner'] = parsed_args.device_owner
         if parsed_args.router:
@@ -566,6 +572,8 @@ class ListPort(command.Lister):
             filters['fixed_ips'] = _prepare_filter_fixed_ips(
                 self.app.client_manager, parsed_args)
 
+        _tag.get_tag_filtering_args(parsed_args, filters)
+
         data = network_client.ports(**filters)
 
         return (column_headers,
@@ -694,6 +702,7 @@ class SetPort(command.Command):
                    "Unset it to None with the 'port unset' command "
                    "(requires data plane status extension)")
         )
+        _tag.add_tag_option_to_parser_for_set(parser, _('port'))
 
         return parser
 
@@ -750,7 +759,11 @@ class SetPort(command.Command):
         if parsed_args.data_plane_status:
             attrs['data_plane_status'] = parsed_args.data_plane_status
 
-        client.update_port(obj, **attrs)
+        if attrs:
+            client.update_port(obj, **attrs)
+
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_set(client, obj, parsed_args)
 
 
 class ShowPort(command.ShowOne):
@@ -834,6 +847,8 @@ class UnsetPort(command.Command):
             help=_("Clear existing information of data plane status")
         )
 
+        _tag.add_tag_option_to_parser_for_unset(parser, _('port'))
+
         return parser
 
     def take_action(self, parsed_args):
@@ -889,3 +904,6 @@ class UnsetPort(command.Command):
 
         if attrs:
             client.update_port(obj, **attrs)
+
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_unset(client, obj, parsed_args)
diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py
index 8db0c4393b..4f9085373c 100644
--- a/openstackclient/network/v2/router.py
+++ b/openstackclient/network/v2/router.py
@@ -26,6 +26,7 @@ from osc_lib import utils
 from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 from openstackclient.network import sdk_utils
+from openstackclient.network.v2 import _tag
 
 
 LOG = logging.getLogger(__name__)
@@ -57,6 +58,7 @@ _formatters = {
     'availability_zones': utils.format_list,
     'availability_zone_hints': utils.format_list,
     'routes': _format_routes,
+    'tags': utils.format_list,
 }
 
 
@@ -217,6 +219,7 @@ class CreateRouter(command.ShowOne):
                    "(Router Availability Zone extension required, "
                    "repeat option to set multiple availability zones)")
         )
+        _tag.add_tag_option_to_parser_for_create(parser, _('router'))
 
         return parser
 
@@ -229,6 +232,8 @@ class CreateRouter(command.ShowOne):
         if parsed_args.no_ha:
             attrs['ha'] = False
         obj = client.create_router(**attrs)
+        # tags cannot be set when created, so tags need to be set later.
+        _tag.update_tags_for_set(client, obj, parsed_args)
 
         display_columns, columns = _get_columns(obj)
         data = utils.get_item_properties(obj, columns, formatters=_formatters)
@@ -310,6 +315,7 @@ class ListRouter(command.Lister):
             metavar='<agent-id>',
             help=_("List routers hosted by an agent (ID only)")
         )
+        _tag.add_tag_filtering_option_to_parser(parser, _('routers'))
 
         return parser
 
@@ -357,6 +363,8 @@ class ListRouter(command.Lister):
             args['tenant_id'] = project_id
             args['project_id'] = project_id
 
+        _tag.get_tag_filtering_args(parsed_args, args)
+
         if parsed_args.agent is not None:
             agent = client.get_agent(parsed_args.agent)
             data = client.agent_hosted_routers(agent)
@@ -384,6 +392,8 @@ class ListRouter(command.Lister):
                 column_headers = column_headers + (
                     'Availability zones',
                 )
+            columns = columns + ('tags',)
+            column_headers = column_headers + ('Tags',)
 
         return (column_headers,
                 (utils.get_item_properties(
@@ -567,6 +577,7 @@ class SetRouter(command.Command):
             action='store_true',
             help=_("Disable Source NAT on external gateway")
         )
+        _tag.add_tag_option_to_parser_for_set(parser, _('router'))
         return parser
 
     def take_action(self, parsed_args):
@@ -625,7 +636,10 @@ class SetRouter(command.Command):
                     ips.append(ip_spec)
                 gateway_info['external_fixed_ips'] = ips
             attrs['external_gateway_info'] = gateway_info
-        client.update_router(obj, **attrs)
+        if attrs:
+            client.update_router(obj, **attrs)
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_set(client, obj, parsed_args)
 
 
 class ShowRouter(command.ShowOne):
@@ -675,6 +689,7 @@ class UnsetRouter(command.Command):
             metavar="<router>",
             help=_("Router to modify (name or ID)")
         )
+        _tag.add_tag_option_to_parser_for_unset(parser, _('router'))
         return parser
 
     def take_action(self, parsed_args):
@@ -695,3 +710,5 @@ class UnsetRouter(command.Command):
             attrs['external_gateway_info'] = {}
         if attrs:
             client.update_router(obj, **attrs)
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_unset(client, obj, parsed_args)
diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py
index 2fdd11f07f..b96dff7f94 100644
--- a/openstackclient/network/v2/subnet.py
+++ b/openstackclient/network/v2/subnet.py
@@ -24,6 +24,7 @@ from osc_lib import utils
 from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 from openstackclient.network import sdk_utils
+from openstackclient.network.v2 import _tag
 
 
 LOG = logging.getLogger(__name__)
@@ -55,6 +56,7 @@ _formatters = {
     'dns_nameservers': utils.format_list,
     'host_routes': _format_host_routes,
     'service_types': utils.format_list,
+    'tags': utils.format_list,
 }
 
 
@@ -336,12 +338,15 @@ class CreateSubnet(command.ShowOne):
             help=_("Set subnet description")
         )
         _get_common_parse_arguments(parser)
+        _tag.add_tag_option_to_parser_for_create(parser, _('subnet'))
         return parser
 
     def take_action(self, parsed_args):
         client = self.app.client_manager.network
         attrs = _get_attrs(self.app.client_manager, parsed_args)
         obj = client.create_subnet(**attrs)
+        # tags cannot be set when created, so tags need to be set later.
+        _tag.update_tags_for_set(client, obj, parsed_args)
         display_columns, columns = _get_columns(obj)
         data = utils.get_item_properties(obj, columns, formatters=_formatters)
         return (display_columns, data)
@@ -454,6 +459,7 @@ class ListSubnet(command.Lister):
                    "(in CIDR notation) in output "
                    "e.g.: --subnet-range 10.10.0.0/16")
         )
+        _tag.add_tag_filtering_option_to_parser(parser, _('subnets'))
         return parser
 
     def take_action(self, parsed_args):
@@ -488,6 +494,7 @@ class ListSubnet(command.Lister):
             filters['name'] = parsed_args.name
         if parsed_args.subnet_range:
             filters['cidr'] = parsed_args.subnet_range
+        _tag.get_tag_filtering_args(parsed_args, filters)
         data = network_client.subnets(**filters)
 
         headers = ('ID', 'Name', 'Network', 'Subnet')
@@ -495,10 +502,10 @@ class ListSubnet(command.Lister):
         if parsed_args.long:
             headers += ('Project', 'DHCP', 'Name Servers',
                         'Allocation Pools', 'Host Routes', 'IP Version',
-                        'Gateway', 'Service Types')
+                        'Gateway', 'Service Types', 'Tags')
             columns += ('project_id', 'is_dhcp_enabled', 'dns_nameservers',
                         'allocation_pools', 'host_routes', 'ip_version',
-                        'gateway_ip', 'service_types')
+                        'gateway_ip', 'service_types', 'tags')
 
         return (headers,
                 (utils.get_item_properties(
@@ -549,6 +556,7 @@ class SetSubnet(command.Command):
             metavar='<description>',
             help=_("Set subnet description")
         )
+        _tag.add_tag_option_to_parser_for_set(parser, _('subnet'))
         _get_common_parse_arguments(parser, is_create=False)
         return parser
 
@@ -574,7 +582,10 @@ class SetSubnet(command.Command):
             attrs['allocation_pools'] = []
         if 'service_types' in attrs:
             attrs['service_types'] += obj.service_types
-        client.update_subnet(obj, **attrs)
+        if attrs:
+            client.update_subnet(obj, **attrs)
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_set(client, obj, parsed_args)
         return
 
 
@@ -643,6 +654,7 @@ class UnsetSubnet(command.Command):
                    'Must be a valid device owner value for a network port '
                    '(repeat option to unset multiple service types)')
         )
+        _tag.add_tag_option_to_parser_for_unset(parser, _('subnet'))
         parser.add_argument(
             'subnet',
             metavar="<subnet>",
@@ -678,3 +690,6 @@ class UnsetSubnet(command.Command):
             attrs['service_types'] = tmp_obj.service_types
         if attrs:
             client.update_subnet(obj, **attrs)
+
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_unset(client, obj, parsed_args)
diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py
index b72a74fc19..a583986856 100644
--- a/openstackclient/network/v2/subnet_pool.py
+++ b/openstackclient/network/v2/subnet_pool.py
@@ -24,6 +24,7 @@ from osc_lib import utils
 from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 from openstackclient.network import sdk_utils
+from openstackclient.network.v2 import _tag
 
 
 LOG = logging.getLogger(__name__)
@@ -42,6 +43,7 @@ def _get_columns(item):
 
 _formatters = {
     'prefixes': utils.format_list,
+    'tags': utils.format_list,
 }
 
 
@@ -191,6 +193,7 @@ class CreateSubnetPool(command.ShowOne):
             metavar='<num-ip-addresses>',
             help=_("Set default quota for subnet pool as the number of"
                    "IP addresses allowed in a subnet")),
+        _tag.add_tag_option_to_parser_for_create(parser, _('subnet pool'))
         return parser
 
     def take_action(self, parsed_args):
@@ -200,6 +203,8 @@ class CreateSubnetPool(command.ShowOne):
         if "prefixes" not in attrs:
             attrs['prefixes'] = []
         obj = client.create_subnet_pool(**attrs)
+        # tags cannot be set when created, so tags need to be set later.
+        _tag.update_tags_for_set(client, obj, parsed_args)
         display_columns, columns = _get_columns(obj)
         data = utils.get_item_properties(obj, columns, formatters=_formatters)
         return (display_columns, data)
@@ -293,6 +298,7 @@ class ListSubnetPool(command.Lister):
             help=_("List only subnet pools of given address scope "
                    "in output (name or ID)")
         )
+        _tag.add_tag_filtering_option_to_parser(parser, _('subnet pools'))
         return parser
 
     def take_action(self, parsed_args):
@@ -324,15 +330,16 @@ class ListSubnetPool(command.Lister):
                 parsed_args.address_scope,
                 ignore_missing=False)
             filters['address_scope_id'] = address_scope.id
+        _tag.get_tag_filtering_args(parsed_args, filters)
         data = network_client.subnet_pools(**filters)
 
         headers = ('ID', 'Name', 'Prefixes')
         columns = ('id', 'name', 'prefixes')
         if parsed_args.long:
             headers += ('Default Prefix Length', 'Address Scope',
-                        'Default Subnet Pool', 'Shared')
+                        'Default Subnet Pool', 'Shared', 'Tags')
             columns += ('default_prefix_length', 'address_scope_id',
-                        'is_default', 'is_shared')
+                        'is_default', 'is_shared', 'tags')
 
         return (headers,
                 (utils.get_item_properties(
@@ -384,6 +391,8 @@ class SetSubnetPool(command.Command):
             metavar='<num-ip-addresses>',
             help=_("Set default quota for subnet pool as the number of"
                    "IP addresses allowed in a subnet")),
+        _tag.add_tag_option_to_parser_for_set(parser, _('subnet pool'))
+
         return parser
 
     def take_action(self, parsed_args):
@@ -397,7 +406,10 @@ class SetSubnetPool(command.Command):
         if 'prefixes' in attrs:
             attrs['prefixes'].extend(obj.prefixes)
 
-        client.update_subnet_pool(obj, **attrs)
+        if attrs:
+            client.update_subnet_pool(obj, **attrs)
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_set(client, obj, parsed_args)
 
 
 class ShowSubnetPool(command.ShowOne):
@@ -441,6 +453,7 @@ class UnsetSubnetPool(command.Command):
             metavar="<subnet-pool>",
             help=_("Subnet pool to modify (name or ID)")
         )
+        _tag.add_tag_option_to_parser_for_unset(parser, _('subnet pool'))
         return parser
 
     def take_action(self, parsed_args):
@@ -461,3 +474,5 @@ class UnsetSubnetPool(command.Command):
             attrs['prefixes'] = tmp_prefixes
         if attrs:
             client.update_subnet_pool(obj, **attrs)
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_unset(client, obj, parsed_args)
diff --git a/openstackclient/tests/functional/network/v2/common.py b/openstackclient/tests/functional/network/v2/common.py
index e3835abf59..a18bc48faf 100644
--- a/openstackclient/tests/functional/network/v2/common.py
+++ b/openstackclient/tests/functional/network/v2/common.py
@@ -10,6 +10,9 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import json
+import uuid
+
 from openstackclient.tests.functional import base
 
 
@@ -20,3 +23,81 @@ class NetworkTests(base.TestCase):
     def setUpClass(cls):
         super(NetworkTests, cls).setUpClass()
         cls.haz_network = base.is_service_enabled('network')
+
+
+class NetworkTagTests(NetworkTests):
+    """Functional tests with tag operation"""
+
+    base_command = None
+
+    def test_tag_operation(self):
+        # Get project IDs
+        cmd_output = json.loads(self.openstack('token issue -f json '))
+        auth_project_id = cmd_output['project_id']
+
+        # Network create with no options
+        name1 = self._create_resource_and_tag_check('', [])
+        # Network create with tags
+        name2 = self._create_resource_and_tag_check('--tag red --tag blue',
+                                                    ['red', 'blue'])
+        # Network create with no tag explicitly
+        name3 = self._create_resource_and_tag_check('--no-tag', [])
+
+        self._set_resource_and_tag_check('set', name1, '--tag red --tag green',
+                                         ['red', 'green'])
+
+        list_expected = ((name1, ['red', 'green']),
+                         (name2, ['red', 'blue']),
+                         (name3, []))
+        self._list_tag_check(auth_project_id, list_expected)
+
+        self._set_resource_and_tag_check('set', name1, '--tag blue',
+                                         ['red', 'green', 'blue'])
+        self._set_resource_and_tag_check(
+            'set', name1,
+            '--no-tag --tag yellow --tag orange --tag purple',
+            ['yellow', 'orange', 'purple'])
+        self._set_resource_and_tag_check('unset', name1, '--tag yellow',
+                                         ['orange', 'purple'])
+        self._set_resource_and_tag_check('unset', name1, '--all-tag', [])
+        self._set_resource_and_tag_check('set', name2, '--no-tag', [])
+
+    def _assertTagsEqual(self, expected, actual):
+        # TODO(amotoki): Should migrate to cliff format columns.
+        # At now, unit test assert method needs to be replaced
+        # to handle format columns, so format_list() is used.
+        # NOTE: The order of tag is undeterminestic.
+        actual_tags = filter(bool, actual.split(', '))
+        self.assertEqual(set(expected), set(actual_tags))
+
+    def _list_tag_check(self, project_id, expected):
+        cmd_output = json.loads(self.openstack(
+            '{} list --long --project {} -f json'.format(self.base_command,
+                                                         project_id)))
+        for name, tags in expected:
+            net = [n for n in cmd_output if n['Name'] == name][0]
+            self._assertTagsEqual(tags, net['Tags'])
+
+    def _create_resource_for_tag_test(self, name, args):
+        return json.loads(self.openstack(
+            '{} create -f json {} {}'.format(self.base_command, args, name)
+        ))
+
+    def _create_resource_and_tag_check(self, args, expected):
+        name = uuid.uuid4().hex
+        cmd_output = self._create_resource_for_tag_test(name, args)
+        self.addCleanup(
+            self.openstack, '{} delete {}'.format(self.base_command, name))
+        self.assertIsNotNone(cmd_output["id"])
+        self._assertTagsEqual(expected, cmd_output['tags'])
+        return name
+
+    def _set_resource_and_tag_check(self, command, name, args, expected):
+        cmd_output = self.openstack(
+            '{} {} {} {}'.format(self.base_command, command, args, name)
+        )
+        self.assertFalse(cmd_output)
+        cmd_output = json.loads(self.openstack(
+            '{} show -f json {}'.format(self.base_command, name)
+        ))
+        self._assertTagsEqual(expected, cmd_output['tags'])
diff --git a/openstackclient/tests/functional/network/v2/test_network.py b/openstackclient/tests/functional/network/v2/test_network.py
index 91939703d1..40fb382a9e 100644
--- a/openstackclient/tests/functional/network/v2/test_network.py
+++ b/openstackclient/tests/functional/network/v2/test_network.py
@@ -16,9 +16,11 @@ import uuid
 from openstackclient.tests.functional.network.v2 import common
 
 
-class NetworkTests(common.NetworkTests):
+class NetworkTests(common.NetworkTagTests):
     """Functional tests for network"""
 
+    base_command = 'network'
+
     def setUp(self):
         super(NetworkTests, self).setUp()
         # Nothing in this class works with Nova Network
diff --git a/openstackclient/tests/functional/network/v2/test_port.py b/openstackclient/tests/functional/network/v2/test_port.py
index 09ac3566e4..a705979028 100644
--- a/openstackclient/tests/functional/network/v2/test_port.py
+++ b/openstackclient/tests/functional/network/v2/test_port.py
@@ -16,9 +16,14 @@ import uuid
 from openstackclient.tests.functional.network.v2 import common
 
 
-class PortTests(common.NetworkTests):
+class PortTests(common.NetworkTagTests):
     """Functional tests for port"""
 
+    base_command = 'port'
+
+    NAME = uuid.uuid4().hex
+    NETWORK_NAME = uuid.uuid4().hex
+
     @classmethod
     def setUpClass(cls):
         common.NetworkTests.setUpClass()
@@ -250,3 +255,9 @@ class PortTests(common.NetworkTests):
             sg_id2,
             json_output.get('security_group_ids'),
         )
+
+    def _create_resource_for_tag_test(self, name, args):
+        return json.loads(self.openstack(
+            '{} create -f json --network {} {} {}'
+            .format(self.base_command, self.NETWORK_NAME, args, name)
+        ))
diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py
index 2e5cb5ef55..95c5a96f8b 100644
--- a/openstackclient/tests/functional/network/v2/test_router.py
+++ b/openstackclient/tests/functional/network/v2/test_router.py
@@ -16,9 +16,11 @@ import uuid
 from openstackclient.tests.functional.network.v2 import common
 
 
-class RouterTests(common.NetworkTests):
+class RouterTests(common.NetworkTagTests):
     """Functional tests for router"""
 
+    base_command = 'router'
+
     def setUp(self):
         super(RouterTests, self).setUp()
         # Nothing in this class works with Nova Network
diff --git a/openstackclient/tests/functional/network/v2/test_subnet.py b/openstackclient/tests/functional/network/v2/test_subnet.py
index 040b645b1b..d5309ee674 100644
--- a/openstackclient/tests/functional/network/v2/test_subnet.py
+++ b/openstackclient/tests/functional/network/v2/test_subnet.py
@@ -17,9 +17,11 @@ import uuid
 from openstackclient.tests.functional.network.v2 import common
 
 
-class SubnetTests(common.NetworkTests):
+class SubnetTests(common.NetworkTagTests):
     """Functional tests for subnet"""
 
+    base_command = 'subnet'
+
     @classmethod
     def setUpClass(cls):
         common.NetworkTests.setUpClass()
@@ -285,3 +287,9 @@ class SubnetTests(common.NetworkTests):
                 # break and no longer retry if create successfully
                 break
         return cmd_output
+
+    def _create_resource_for_tag_test(self, name, args):
+        cmd = ('subnet create -f json --network ' +
+               self.NETWORK_NAME + ' ' + args +
+               ' --subnet-range')
+        return self._subnet_create(cmd, name)
diff --git a/openstackclient/tests/functional/network/v2/test_subnet_pool.py b/openstackclient/tests/functional/network/v2/test_subnet_pool.py
index a4b823f100..46aa6f1433 100644
--- a/openstackclient/tests/functional/network/v2/test_subnet_pool.py
+++ b/openstackclient/tests/functional/network/v2/test_subnet_pool.py
@@ -17,9 +17,11 @@ import uuid
 from openstackclient.tests.functional.network.v2 import common
 
 
-class SubnetPoolTests(common.NetworkTests):
+class SubnetPoolTests(common.NetworkTagTests):
     """Functional tests for subnet pool"""
 
+    base_command = 'subnet pool'
+
     def setUp(self):
         super(SubnetPoolTests, self).setUp()
         # Nothing in this class works with Nova Network
@@ -321,3 +323,7 @@ class SubnetPoolTests(common.NetworkTests):
                 break
 
         return cmd_output, pool_prefix
+
+    def _create_resource_for_tag_test(self, name, args):
+        cmd_output, _pool_prefix = self._subnet_pool_create(args, name)
+        return cmd_output
diff --git a/openstackclient/tests/unit/network/v2/_test_tag.py b/openstackclient/tests/unit/network/v2/_test_tag.py
new file mode 100644
index 0000000000..bd46153782
--- /dev/null
+++ b/openstackclient/tests/unit/network/v2/_test_tag.py
@@ -0,0 +1,190 @@
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+from openstackclient.tests.unit import utils as tests_utils
+
+
+class TestCreateTagMixin(object):
+    """Test case mixin to test network tag operation for resource creation.
+
+    * Each test class must create a mock for self.network.set_tags
+    * If you test tag operation combined with other options,
+      you need to write test(s) directly in individual test cases.
+    * The following instance attributes must be defined:
+
+      * _tag_test_resource: Test resource returned by mocked create_<resource>.
+      * _tag_create_resource_mock: Mocked create_<resource> method of SDK.
+      * _tag_create_required_arglist: List of required arguments when creating
+          a resource with default options.
+      * _tag_create_required_verifylist: List of expected parsed_args params
+          when creating a resource with default options.
+      * _tag_create_required_attrs: Expected attributes passed to a mocked
+          create_resource method when creating a resource with default options.
+    """
+
+    def _test_create_with_tag(self, add_tags=True):
+        arglist = self._tag_create_required_arglist[:]
+        if add_tags:
+            arglist += ['--tag', 'red', '--tag', 'blue']
+        else:
+            arglist += ['--no-tag']
+        verifylist = self._tag_create_required_verifylist[:]
+        if add_tags:
+            verifylist.append(('tags', ['red', 'blue']))
+        else:
+            verifylist.append(('no_tag', True))
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        columns, data = (self.cmd.take_action(parsed_args))
+
+        self._tag_create_resource_mock.assert_called_once_with(
+            **self._tag_create_required_attrs)
+        if add_tags:
+            self.network.set_tags.assert_called_once_with(
+                self._tag_test_resource,
+                tests_utils.CompareBySet(['red', 'blue']))
+        else:
+            self.assertFalse(self.network.set_tags.called)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
+
+    def test_create_with_tags(self):
+        self._test_create_with_tag(add_tags=True)
+
+    def test_create_with_no_tag(self):
+        self._test_create_with_tag(add_tags=False)
+
+
+class TestListTagMixin(object):
+    """Test case mixin to test network tag operation for resource listing.
+
+    * A test resource returned by find_<resource> must contains
+      "red" and "green" tags.
+    * Each test class must create a mock for self.network.set_tags
+    * If you test tag operation combined with other options,
+      you need to write test(s) directly in individual test cases.
+    * The following instance attributes must be defined:
+
+      * _tag_create_resource_mock: Mocked list_<resource> method of SDK.
+    """
+
+    def test_list_with_tag_options(self):
+        arglist = [
+            '--tags', 'red,blue',
+            '--any-tags', 'red,green',
+            '--not-tags', 'orange,yellow',
+            '--not-any-tags', 'black,white',
+        ]
+        verifylist = [
+            ('tags', ['red', 'blue']),
+            ('any_tags', ['red', 'green']),
+            ('not_tags', ['orange', 'yellow']),
+            ('not_any_tags', ['black', 'white']),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self._tag_list_resource_mock.assert_called_once_with(
+            **{'tags': 'red,blue',
+               'any_tags': 'red,green',
+               'not_tags': 'orange,yellow',
+               'not_any_tags': 'black,white'}
+        )
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, list(data))
+
+
+class TestSetTagMixin(object):
+    """Test case mixin to test network tag operation for resource update.
+
+    * A test resource returned by find_<resource> must contains
+      "red" and "green" tags.
+    * Each test class must create a mock for self.network.set_tags
+    * If you test tag operation combined with other options,
+      you need to write test(s) directly in individual test cases.
+    * The following instance attributes must be defined:
+
+      * _tag_resource_name: positional arg name of a resource to be updated.
+      * _tag_test_resource: Test resource returned by mocked update_<resource>.
+      * _tag_update_resource_mock: Mocked update_<resource> method of SDK.
+    """
+
+    def _test_set_tags(self, with_tags=True):
+        if with_tags:
+            arglist = ['--tag', 'red', '--tag', 'blue']
+            verifylist = [('tags', ['red', 'blue'])]
+            expected_args = ['red', 'blue', 'green']
+        else:
+            arglist = ['--no-tag']
+            verifylist = [('no_tag', True)]
+            expected_args = []
+        arglist.append(self._tag_test_resource.name)
+        verifylist.append(
+            (self._tag_resource_name, self._tag_test_resource.name))
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        result = self.cmd.take_action(parsed_args)
+
+        self.assertFalse(self._tag_update_resource_mock.called)
+        self.network.set_tags.assert_called_once_with(
+            self._tag_test_resource,
+            tests_utils.CompareBySet(expected_args))
+        self.assertIsNone(result)
+
+    def test_set_with_tags(self):
+        self._test_set_tags(with_tags=True)
+
+    def test_set_with_no_tag(self):
+        self._test_set_tags(with_tags=False)
+
+
+class TestUnsetTagMixin(object):
+    """Test case mixin to test network tag operation for resource update.
+
+    * Each test class must create a mock for self.network.set_tags
+    * If you test tag operation combined with other options,
+      you need to write test(s) directly in individual test cases.
+    * The following instance attributes must be defined:
+
+      * _tag_resource_name: positional arg name of a resource to be updated.
+      * _tag_test_resource: Test resource returned by mocked update_<resource>.
+      * _tag_update_resource_mock: Mocked update_<resource> method of SDK.
+    """
+
+    def _test_unset_tags(self, with_tags=True):
+        if with_tags:
+            arglist = ['--tag', 'red', '--tag', 'blue']
+            verifylist = [('tags', ['red', 'blue'])]
+            expected_args = ['green']
+        else:
+            arglist = ['--all-tag']
+            verifylist = [('all_tag', True)]
+            expected_args = []
+        arglist.append(self._tag_test_resource.name)
+        verifylist.append(
+            (self._tag_resource_name, self._tag_test_resource.name))
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        result = self.cmd.take_action(parsed_args)
+
+        self.assertFalse(self._tag_update_resource_mock.called)
+        self.network.set_tags.assert_called_once_with(
+            self._tag_test_resource,
+            tests_utils.CompareBySet(expected_args))
+        self.assertIsNone(result)
+
+    def test_unset_with_tags(self):
+        self._test_unset_tags(with_tags=True)
+
+    def test_unset_with_all_tag(self):
+        self._test_unset_tags(with_tags=False)
diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py
index 98bda1649f..eadab58461 100644
--- a/openstackclient/tests/unit/network/v2/fakes.py
+++ b/openstackclient/tests/unit/network/v2/fakes.py
@@ -350,6 +350,7 @@ class FakeNetwork(object):
             'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex,
             'ipv4_address_scope': 'ipv4' + uuid.uuid4().hex,
             'ipv6_address_scope': 'ipv6' + uuid.uuid4().hex,
+            'tags': [],
         }
 
         # Overwrite default attributes.
@@ -576,6 +577,7 @@ class FakePort(object):
             'status': 'ACTIVE',
             'tenant_id': 'project-id-' + uuid.uuid4().hex,
             'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex,
+            'tags': [],
         }
 
         # Overwrite default attributes.
@@ -1053,6 +1055,7 @@ class FakeRouter(object):
             'external_gateway_info': {},
             'availability_zone_hints': [],
             'availability_zones': [],
+            'tags': [],
         }
 
         # Overwrite default attributes.
@@ -1294,6 +1297,7 @@ class FakeSubnet(object):
             'service_types': [],
             'subnetpool_id': None,
             'description': 'subnet-description-' + uuid.uuid4().hex,
+            'tags': [],
         }
 
         # Overwrite default attributes.
@@ -1544,6 +1548,7 @@ class FakeSubnetPool(object):
             'default_quota': None,
             'ip_version': '4',
             'description': 'subnet-pool-description-' + uuid.uuid4().hex,
+            'tags': [],
         }
 
         # Overwrite default attributes.
diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py
index bc8c402487..e620cd9c1d 100644
--- a/openstackclient/tests/unit/network/v2/test_network.py
+++ b/openstackclient/tests/unit/network/v2/test_network.py
@@ -22,6 +22,7 @@ from openstackclient.network.v2 import network
 from openstackclient.tests.unit import fakes
 from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes_v2
 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3
+from openstackclient.tests.unit.network.v2 import _test_tag
 from openstackclient.tests.unit.network.v2 import fakes as network_fakes
 from openstackclient.tests.unit import utils as tests_utils
 
@@ -41,7 +42,7 @@ class TestNetwork(network_fakes.TestNetworkV2):
         self.domains_mock = self.app.client_manager.identity.domains
 
 
-class TestCreateNetworkIdentityV3(TestNetwork):
+class TestCreateNetworkIdentityV3(TestNetwork, _test_tag.TestCreateTagMixin):
 
     project = identity_fakes_v3.FakeProject.create_one_project()
     domain = identity_fakes_v3.FakeDomain.create_one_domain()
@@ -105,6 +106,7 @@ class TestCreateNetworkIdentityV3(TestNetwork):
         super(TestCreateNetworkIdentityV3, self).setUp()
 
         self.network.create_network = mock.Mock(return_value=self._network)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         # Get the command object to test
         self.cmd = network.CreateNetwork(self.app, self.namespace)
@@ -113,6 +115,22 @@ class TestCreateNetworkIdentityV3(TestNetwork):
         self.domains_mock.get.return_value = self.domain
         self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy)
 
+        # TestCreateTagMixin
+        self._tag_test_resource = self._network
+        self._tag_create_resource_mock = self.network.create_network
+        self._tag_create_required_arglist = [self._network.name]
+        self._tag_create_required_verifylist = [
+            ('name', self._network.name),
+            ('enable', True),
+            ('share', None),
+            ('project', None),
+            ('external', False),
+        ]
+        self._tag_create_required_attrs = {
+            'admin_state_up': True,
+            'name': self._network.name,
+        }
+
     def test_create_no_options(self):
         arglist = []
         verifylist = []
@@ -139,6 +157,7 @@ class TestCreateNetworkIdentityV3(TestNetwork):
             'admin_state_up': True,
             'name': self._network.name,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
@@ -287,6 +306,7 @@ class TestCreateNetworkIdentityV2(TestNetwork):
         super(TestCreateNetworkIdentityV2, self).setUp()
 
         self.network.create_network = mock.Mock(return_value=self._network)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         # Get the command object to test
         self.cmd = network.CreateNetwork(self.app, self.namespace)
@@ -328,6 +348,7 @@ class TestCreateNetworkIdentityV2(TestNetwork):
             'tenant_id': self.project.id,
             'project_id': self.project.id,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
@@ -440,7 +461,7 @@ class TestDeleteNetwork(TestNetwork):
         self.network.delete_network.assert_has_calls(calls)
 
 
-class TestListNetwork(TestNetwork):
+class TestListNetwork(TestNetwork, _test_tag.TestListTagMixin):
 
     # The networks going to be listed up.
     _network = network_fakes.FakeNetwork.create_networks(count=3)
@@ -461,6 +482,7 @@ class TestListNetwork(TestNetwork):
         'Network Type',
         'Router Type',
         'Availability Zones',
+        'Tags',
     )
 
     data = []
@@ -484,6 +506,7 @@ class TestListNetwork(TestNetwork):
             net.provider_network_type,
             network._format_router_external(net.is_router_external),
             utils.format_list(net.availability_zones),
+            utils.format_list(net.tags),
         ))
 
     def setUp(self):
@@ -501,6 +524,9 @@ class TestListNetwork(TestNetwork):
         self.network.dhcp_agent_hosting_networks = mock.Mock(
             return_value=self._network)
 
+        # TestListTagMixin
+        self._tag_list_resource_mock = self.network.networks
+
     def test_network_list_no_options(self):
         arglist = []
         verifylist = [
@@ -795,10 +821,11 @@ class TestListNetwork(TestNetwork):
         self.assertEqual(list(data), list(self.data))
 
 
-class TestSetNetwork(TestNetwork):
+class TestSetNetwork(TestNetwork, _test_tag.TestSetTagMixin):
 
     # The network to set.
-    _network = network_fakes.FakeNetwork.create_one_network()
+    _network = network_fakes.FakeNetwork.create_one_network(
+        {'tags': ['green', 'red']})
     qos_policy = (network_fakes.FakeNetworkQosPolicy.
                   create_one_qos_policy(attrs={'id': _network.qos_policy_id}))
 
@@ -806,6 +833,7 @@ class TestSetNetwork(TestNetwork):
         super(TestSetNetwork, self).setUp()
 
         self.network.update_network = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         self.network.find_network = mock.Mock(return_value=self._network)
         self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy)
@@ -813,6 +841,11 @@ class TestSetNetwork(TestNetwork):
         # Get the command object to test
         self.cmd = network.SetNetwork(self.app, self.namespace)
 
+        # TestSetTagMixin
+        self._tag_resource_name = 'network'
+        self._tag_test_resource = self._network
+        self._tag_update_resource_mock = self.network.update_network
+
     def test_set_this(self):
         arglist = [
             self._network.name,
@@ -902,9 +935,8 @@ class TestSetNetwork(TestNetwork):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         result = self.cmd.take_action(parsed_args)
 
-        attrs = {}
-        self.network.update_network.assert_called_once_with(
-            self._network, **attrs)
+        self.assertFalse(self.network.update_network.called)
+        self.assertFalse(self.network.set_tags.called)
         self.assertIsNone(result)
 
 
@@ -990,3 +1022,40 @@ class TestShowNetwork(TestNetwork):
 
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
+
+
+class TestUnsetNetwork(TestNetwork, _test_tag.TestUnsetTagMixin):
+
+    # The network to set.
+    _network = network_fakes.FakeNetwork.create_one_network(
+        {'tags': ['green', 'red']})
+    qos_policy = (network_fakes.FakeNetworkQosPolicy.
+                  create_one_qos_policy(attrs={'id': _network.qos_policy_id}))
+
+    def setUp(self):
+        super(TestUnsetNetwork, self).setUp()
+
+        self.network.update_network = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
+
+        self.network.find_network = mock.Mock(return_value=self._network)
+        self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy)
+
+        # Get the command object to test
+        self.cmd = network.UnsetNetwork(self.app, self.namespace)
+
+        # TestUnsetNetwork
+        self._tag_resource_name = 'network'
+        self._tag_test_resource = self._network
+        self._tag_update_resource_mock = self.network.update_network
+
+    def test_unset_nothing(self):
+        arglist = [self._network.name, ]
+        verifylist = [('network', self._network.name), ]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        result = self.cmd.take_action(parsed_args)
+
+        self.assertFalse(self.network.update_network.called)
+        self.assertFalse(self.network.set_tags.called)
+        self.assertIsNone(result)
diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py
index a8a6dba9be..deda6b4128 100644
--- a/openstackclient/tests/unit/network/v2/test_port.py
+++ b/openstackclient/tests/unit/network/v2/test_port.py
@@ -21,6 +21,7 @@ from osc_lib import utils
 from openstackclient.network.v2 import port
 from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
+from openstackclient.tests.unit.network.v2 import _test_tag
 from openstackclient.tests.unit.network.v2 import fakes as network_fakes
 from openstackclient.tests.unit import utils as tests_utils
 
@@ -35,7 +36,8 @@ class TestPort(network_fakes.TestNetworkV2):
         # Get a shortcut to the ProjectManager Mock
         self.projects_mock = self.app.client_manager.identity.projects
 
-    def _get_common_cols_data(self, fake_port):
+    @staticmethod
+    def _get_common_cols_data(fake_port):
         columns = (
             'admin_state_up',
             'allowed_address_pairs',
@@ -61,6 +63,7 @@ class TestPort(network_fakes.TestNetworkV2):
             'qos_policy_id',
             'security_group_ids',
             'status',
+            'tags',
         )
 
         data = (
@@ -88,19 +91,22 @@ class TestPort(network_fakes.TestNetworkV2):
             fake_port.qos_policy_id,
             utils.format_list(fake_port.security_group_ids),
             fake_port.status,
+            utils.format_list(fake_port.tags),
         )
 
         return columns, data
 
 
-class TestCreatePort(TestPort):
+class TestCreatePort(TestPort, _test_tag.TestCreateTagMixin):
 
     _port = network_fakes.FakePort.create_one_port()
+    columns, data = TestPort._get_common_cols_data(_port)
 
     def setUp(self):
         super(TestCreatePort, self).setUp()
 
         self.network.create_port = mock.Mock(return_value=self._port)
+        self.network.set_tags = mock.Mock(return_value=None)
         fake_net = network_fakes.FakeNetwork.create_one_network({
             'id': self._port.network_id,
         })
@@ -110,6 +116,24 @@ class TestCreatePort(TestPort):
         # Get the command object to test
         self.cmd = port.CreatePort(self.app, self.namespace)
 
+        # TestUnsetTagMixin
+        self._tag_test_resource = self._port
+        self._tag_create_resource_mock = self.network.create_port
+        self._tag_create_required_arglist = [
+            '--network', self._port.network_id,
+            'test-port',
+        ]
+        self._tag_create_required_verifylist = [
+            ('network', self._port.network_id,),
+            ('enable', True),
+            ('name', 'test-port'),
+        ]
+        self._tag_create_required_attrs = {
+            'admin_state_up': True,
+            'network_id': self._port.network_id,
+            'name': 'test-port',
+        }
+
     def test_create_default_options(self):
         arglist = [
             '--network', self._port.network_id,
@@ -129,10 +153,10 @@ class TestCreatePort(TestPort):
             'network_id': self._port.network_id,
             'name': 'test-port',
         })
+        self.assertFalse(self.network.set_tags.called)
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_full_options(self):
         arglist = [
@@ -166,7 +190,6 @@ class TestCreatePort(TestPort):
             ('network', self._port.network_id),
             ('dns_name', '8.8.8.8'),
             ('name', 'test-port'),
-
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
@@ -187,9 +210,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_invalid_json_binding_profile(self):
         arglist = [
@@ -239,9 +261,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_with_security_group(self):
         secgroup = network_fakes.FakeSecurityGroup.create_one_security_group()
@@ -269,9 +290,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_port_with_dns_name(self):
         arglist = [
@@ -296,9 +316,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_with_security_groups(self):
         sg_1 = network_fakes.FakeSecurityGroup.create_one_security_group()
@@ -327,9 +346,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_with_no_security_groups(self):
         arglist = [
@@ -354,9 +372,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_port_with_allowed_address_pair_ipaddr(self):
         pairs = [{'ip_address': '192.168.1.123'},
@@ -385,9 +402,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_port_with_allowed_address_pair(self):
         pairs = [{'ip_address': '192.168.1.123',
@@ -422,9 +438,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_port_with_qos(self):
         qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy()
@@ -451,9 +466,8 @@ class TestCreatePort(TestPort):
             'name': 'test-port',
         })
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
     def test_create_port_security_enabled(self):
         arglist = [
@@ -583,7 +597,7 @@ class TestDeletePort(TestPort):
         )
 
 
-class TestListPort(TestPort):
+class TestListPort(TestPort, _test_tag.TestListTagMixin):
 
     _ports = network_fakes.FakePort.create_ports(count=3)
 
@@ -603,6 +617,7 @@ class TestListPort(TestPort):
         'Status',
         'Security Groups',
         'Device Owner',
+        'Tags',
     )
 
     data = []
@@ -625,6 +640,7 @@ class TestListPort(TestPort):
             prt.status,
             utils.format_list(prt.security_group_ids),
             prt.device_owner,
+            utils.format_list(prt.tags),
         ))
 
     def setUp(self):
@@ -642,6 +658,8 @@ class TestListPort(TestPort):
         self.network.find_router = mock.Mock(return_value=fake_router)
         self.network.find_network = mock.Mock(return_value=fake_network)
         self.app.client_manager.compute = mock.Mock()
+        # TestUnsetTagMixin
+        self._tag_list_resource_mock = self.network.ports
 
     def test_port_list_no_options(self):
         arglist = []
@@ -902,9 +920,9 @@ class TestListPort(TestPort):
         self.assertEqual(self.data, list(data))
 
 
-class TestSetPort(TestPort):
+class TestSetPort(TestPort, _test_tag.TestSetTagMixin):
 
-    _port = network_fakes.FakePort.create_one_port()
+    _port = network_fakes.FakePort.create_one_port({'tags': ['green', 'red']})
 
     def setUp(self):
         super(TestSetPort, self).setUp()
@@ -912,9 +930,14 @@ class TestSetPort(TestPort):
         self.network.find_subnet = mock.Mock(return_value=self.fake_subnet)
         self.network.find_port = mock.Mock(return_value=self._port)
         self.network.update_port = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         # Get the command object to test
         self.cmd = port.SetPort(self.app, self.namespace)
+        # TestSetTagMixin
+        self._tag_resource_name = 'port'
+        self._tag_test_resource = self._port
+        self._tag_update_resource_mock = self.network.update_port
 
     def test_set_port_defaults(self):
         arglist = [
@@ -926,8 +949,8 @@ class TestSetPort(TestPort):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
         result = self.cmd.take_action(parsed_args)
-        attrs = {}
-        self.network.update_port.assert_called_once_with(self._port, **attrs)
+        self.assertFalse(self.network.update_port.called)
+        self.assertFalse(self.network.set_tags.called)
         self.assertIsNone(result)
 
     def test_set_port_fixed_ip(self):
@@ -1412,6 +1435,7 @@ class TestShowPort(TestPort):
 
     # The port to show.
     _port = network_fakes.FakePort.create_one_port()
+    columns, data = TestPort._get_common_cols_data(_port)
 
     def setUp(self):
         super(TestShowPort, self).setUp()
@@ -1442,12 +1466,11 @@ class TestShowPort(TestPort):
         self.network.find_port.assert_called_once_with(
             self._port.name, ignore_missing=False)
 
-        ref_columns, ref_data = self._get_common_cols_data(self._port)
-        self.assertEqual(ref_columns, columns)
-        self.assertEqual(ref_data, data)
+        self.assertEqual(self.columns, columns)
+        self.assertEqual(self.data, data)
 
 
-class TestUnsetPort(TestPort):
+class TestUnsetPort(TestPort, _test_tag.TestUnsetTagMixin):
 
     def setUp(self):
         super(TestUnsetPort, self).setUp()
@@ -1456,14 +1479,20 @@ class TestUnsetPort(TestPort):
                             'ip_address': '0.0.0.1'},
                            {'subnet_id': '042eb10a-3a18-4658-ab-cf47c8d03152',
                             'ip_address': '1.0.0.0'}],
-             'binding:profile': {'batman': 'Joker', 'Superman': 'LexLuthor'}})
+             'binding:profile': {'batman': 'Joker', 'Superman': 'LexLuthor'},
+             'tags': ['green', 'red'], })
         self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet(
             {'id': '042eb10a-3a18-4658-ab-cf47c8d03152'})
         self.network.find_subnet = mock.Mock(return_value=self.fake_subnet)
         self.network.find_port = mock.Mock(return_value=self._testport)
         self.network.update_port = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
         # Get the command object to test
         self.cmd = port.UnsetPort(self.app, self.namespace)
+        # TestUnsetTagMixin
+        self._tag_resource_name = 'port'
+        self._tag_test_resource = self._testport
+        self._tag_update_resource_mock = self.network.update_port
 
     def test_unset_port_parameters(self):
         arglist = [
diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py
index c153fe4a08..d65c9aa9f7 100644
--- a/openstackclient/tests/unit/network/v2/test_router.py
+++ b/openstackclient/tests/unit/network/v2/test_router.py
@@ -19,6 +19,7 @@ from osc_lib import utils as osc_utils
 
 from openstackclient.network.v2 import router
 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3
+from openstackclient.tests.unit.network.v2 import _test_tag
 from openstackclient.tests.unit.network.v2 import fakes as network_fakes
 from openstackclient.tests.unit import utils as tests_utils
 
@@ -111,7 +112,7 @@ class TestAddSubnetToRouter(TestRouter):
         self.assertIsNone(result)
 
 
-class TestCreateRouter(TestRouter):
+class TestCreateRouter(TestRouter, _test_tag.TestCreateTagMixin):
 
     # The new router created.
     new_router = network_fakes.FakeRouter.create_one_router()
@@ -129,6 +130,7 @@ class TestCreateRouter(TestRouter):
         'project_id',
         'routes',
         'status',
+        'tags',
     )
     data = (
         router._format_admin_state(new_router.admin_state_up),
@@ -143,22 +145,42 @@ class TestCreateRouter(TestRouter):
         new_router.tenant_id,
         router._format_routes(new_router.routes),
         new_router.status,
+        osc_utils.format_list(new_router.tags),
     )
 
     def setUp(self):
         super(TestCreateRouter, self).setUp()
 
         self.network.create_router = mock.Mock(return_value=self.new_router)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         # Get the command object to test
         self.cmd = router.CreateRouter(self.app, self.namespace)
 
+        # TestCreateTagMixin
+        self._tag_test_resource = self.new_router
+        self._tag_create_resource_mock = self.network.create_router
+        self._tag_create_required_arglist = [
+            self.new_router.name,
+        ]
+        self._tag_create_required_verifylist = [
+            ('name', self.new_router.name),
+            ('enable', True),
+            ('distributed', False),
+            ('ha', False),
+        ]
+        self._tag_create_required_attrs = {
+            'admin_state_up': True,
+            'name': self.new_router.name,
+        }
+
     def test_create_no_options(self):
         arglist = []
         verifylist = []
 
         self.assertRaises(tests_utils.ParserException, self.check_parser,
                           self.cmd, arglist, verifylist)
+        self.assertFalse(self.network.set_tags.called)
 
     def test_create_default_options(self):
         arglist = [
@@ -178,6 +200,7 @@ class TestCreateRouter(TestRouter):
             'admin_state_up': True,
             'name': self.new_router.name,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
@@ -345,7 +368,7 @@ class TestDeleteRouter(TestRouter):
         )
 
 
-class TestListRouter(TestRouter):
+class TestListRouter(TestRouter, _test_tag.TestListTagMixin):
 
     # The routers going to be listed up.
     routers = network_fakes.FakeRouter.create_routers(count=3)
@@ -363,11 +386,13 @@ class TestListRouter(TestRouter):
     columns_long = columns + (
         'Routes',
         'External gateway info',
-        'Availability zones'
+        'Availability zones',
+        'Tags',
     )
     columns_long_no_az = columns + (
         'Routes',
         'External gateway info',
+        'Tags',
     )
 
     data = []
@@ -404,6 +429,7 @@ class TestListRouter(TestRouter):
                 router._format_routes(r.routes),
                 router._format_external_gateway_info(r.external_gateway_info),
                 osc_utils.format_list(r.availability_zones),
+                osc_utils.format_list(r.tags),
             )
         )
     data_long_no_az = []
@@ -413,6 +439,7 @@ class TestListRouter(TestRouter):
             data[i] + (
                 router._format_routes(r.routes),
                 router._format_external_gateway_info(r.external_gateway_info),
+                osc_utils.format_list(r.tags),
             )
         )
 
@@ -432,6 +459,9 @@ class TestListRouter(TestRouter):
         self.network.get_agent = mock.Mock(return_value=self._testagent)
         self.network.get_router = mock.Mock(return_value=self.routers[0])
 
+        # TestListTagMixin
+        self._tag_list_resource_mock = self.network.routers
+
     def test_router_list_no_options(self):
         arglist = []
         verifylist = [
@@ -684,26 +714,33 @@ class TestRemoveSubnetFromRouter(TestRouter):
         self.assertIsNone(result)
 
 
-class TestSetRouter(TestRouter):
+class TestSetRouter(TestRouter, _test_tag.TestSetTagMixin):
 
     # The router to set.
     _default_route = {'destination': '10.20.20.0/24', 'nexthop': '10.20.30.1'}
     _network = network_fakes.FakeNetwork.create_one_network()
     _subnet = network_fakes.FakeSubnet.create_one_subnet()
     _router = network_fakes.FakeRouter.create_one_router(
-        attrs={'routes': [_default_route]}
+        attrs={'routes': [_default_route],
+               'tags': ['green', 'red']}
     )
 
     def setUp(self):
         super(TestSetRouter, self).setUp()
         self.network.router_add_gateway = mock.Mock()
         self.network.update_router = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
         self.network.find_router = mock.Mock(return_value=self._router)
         self.network.find_network = mock.Mock(return_value=self._network)
         self.network.find_subnet = mock.Mock(return_value=self._subnet)
         # Get the command object to test
         self.cmd = router.SetRouter(self.app, self.namespace)
 
+        # TestSetTagMixin
+        self._tag_resource_name = 'router'
+        self._tag_test_resource = self._router
+        self._tag_update_resource_mock = self.network.update_router
+
     def test_set_this(self):
         arglist = [
             self._router.name,
@@ -902,9 +939,8 @@ class TestSetRouter(TestRouter):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         result = self.cmd.take_action(parsed_args)
 
-        attrs = {}
-        self.network.update_router.assert_called_once_with(
-            self._router, **attrs)
+        self.assertFalse(self.network.update_router.called)
+        self.assertFalse(self.network.set_tags.called)
         self.assertIsNone(result)
 
     def test_wrong_gateway_params(self):
@@ -1030,6 +1066,7 @@ class TestShowRouter(TestRouter):
         'project_id',
         'routes',
         'status',
+        'tags',
     )
     data = (
         router._format_admin_state(_router.admin_state_up),
@@ -1044,6 +1081,7 @@ class TestShowRouter(TestRouter):
         _router.tenant_id,
         router._format_routes(_router.routes),
         _router.status,
+        osc_utils.format_list(_router.tags),
     )
 
     def setUp(self):
@@ -1086,12 +1124,18 @@ class TestUnsetRouter(TestRouter):
             {'routes': [{"destination": "192.168.101.1/24",
                          "nexthop": "172.24.4.3"},
                         {"destination": "192.168.101.2/24",
-                         "nexthop": "172.24.4.3"}], })
+                         "nexthop": "172.24.4.3"}],
+             'tags': ['green', 'red'], })
         self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet()
         self.network.find_router = mock.Mock(return_value=self._testrouter)
         self.network.update_router = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
         # Get the command object to test
         self.cmd = router.UnsetRouter(self.app, self.namespace)
+        # TestUnsetTagMixin
+        self._tag_resource_name = 'router'
+        self._tag_test_resource = self._testrouter
+        self._tag_update_resource_mock = self.network.update_router
 
     def test_unset_router_params(self):
         arglist = [
diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py
index 47de5616df..509fbe6b12 100644
--- a/openstackclient/tests/unit/network/v2/test_subnet.py
+++ b/openstackclient/tests/unit/network/v2/test_subnet.py
@@ -19,6 +19,7 @@ from osc_lib import utils
 
 from openstackclient.network.v2 import subnet as subnet_v2
 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3
+from openstackclient.tests.unit.network.v2 import _test_tag
 from openstackclient.tests.unit.network.v2 import fakes as network_fakes
 from openstackclient.tests.unit import utils as tests_utils
 
@@ -36,7 +37,7 @@ class TestSubnet(network_fakes.TestNetworkV2):
         self.domains_mock = self.app.client_manager.identity.domains
 
 
-class TestCreateSubnet(TestSubnet):
+class TestCreateSubnet(TestSubnet, _test_tag.TestCreateTagMixin):
 
     project = identity_fakes_v3.FakeProject.create_one_project()
     domain = identity_fakes_v3.FakeDomain.create_one_domain()
@@ -125,6 +126,7 @@ class TestCreateSubnet(TestSubnet):
         'segment_id',
         'service_types',
         'subnetpool_id',
+        'tags',
     )
 
     data = (
@@ -145,6 +147,7 @@ class TestCreateSubnet(TestSubnet):
         _subnet.segment_id,
         utils.format_list(_subnet.service_types),
         _subnet.subnetpool_id,
+        utils.format_list(_subnet.tags),
     )
 
     data_subnet_pool = (
@@ -165,6 +168,7 @@ class TestCreateSubnet(TestSubnet):
         _subnet_from_pool.segment_id,
         utils.format_list(_subnet_from_pool.service_types),
         _subnet_from_pool.subnetpool_id,
+        utils.format_list(_subnet.tags),
     )
 
     data_ipv6 = (
@@ -185,6 +189,7 @@ class TestCreateSubnet(TestSubnet):
         _subnet_ipv6.segment_id,
         utils.format_list(_subnet_ipv6.service_types),
         _subnet_ipv6.subnetpool_id,
+        utils.format_list(_subnet.tags),
     )
 
     def setUp(self):
@@ -197,6 +202,8 @@ class TestCreateSubnet(TestSubnet):
         self.domains_mock.get.return_value = self.domain
 
         # Mock SDK calls for all tests.
+        self.network.create_subnet = mock.Mock(return_value=self._subnet)
+        self.network.set_tags = mock.Mock(return_value=None)
         self.network.find_network = mock.Mock(return_value=self._network)
         self.network.find_segment = mock.Mock(
             return_value=self._network_segment
@@ -205,6 +212,28 @@ class TestCreateSubnet(TestSubnet):
             return_value=self._subnet_pool
         )
 
+        # TestUnsetTagMixin
+        self._tag_test_resource = self._subnet
+        self._tag_create_resource_mock = self.network.create_subnet
+        self._tag_create_required_arglist = [
+            "--subnet-range", self._subnet.cidr,
+            "--network", self._subnet.network_id,
+            self._subnet.name,
+        ]
+        self._tag_create_required_verifylist = [
+            ('name', self._subnet.name),
+            ('subnet_range', self._subnet.cidr),
+            ('network', self._subnet.network_id),
+            ('ip_version', self._subnet.ip_version),
+            ('gateway', 'auto'),
+        ]
+        self._tag_create_required_attrs = {
+            'cidr': self._subnet.cidr,
+            'ip_version': self._subnet.ip_version,
+            'name': self._subnet.name,
+            'network_id': self._subnet.network_id,
+        }
+
     def test_create_no_options(self):
         arglist = []
         verifylist = []
@@ -213,10 +242,11 @@ class TestCreateSubnet(TestSubnet):
         # throw a "ParserExecption"
         self.assertRaises(tests_utils.ParserException,
                           self.check_parser, self.cmd, arglist, verifylist)
+        self.assertFalse(self.network.create_subnet.called)
+        self.assertFalse(self.network.set_tags.called)
 
     def test_create_default_options(self):
         # Mock SDK calls for this test.
-        self.network.create_subnet = mock.Mock(return_value=self._subnet)
         self._network.id = self._subnet.network_id
 
         arglist = [
@@ -230,7 +260,6 @@ class TestCreateSubnet(TestSubnet):
             ('network', self._subnet.network_id),
             ('ip_version', self._subnet.ip_version),
             ('gateway', 'auto'),
-
         ]
 
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -242,13 +271,14 @@ class TestCreateSubnet(TestSubnet):
             'name': self._subnet.name,
             'network_id': self._subnet.network_id,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
     def test_create_from_subnet_pool_options(self):
         # Mock SDK calls for this test.
-        self.network.create_subnet = \
-            mock.Mock(return_value=self._subnet_from_pool)
+        self.network.create_subnet.return_value = self._subnet_from_pool
+        self.network.set_tags = mock.Mock(return_value=None)
         self._network.id = self._subnet_from_pool.network_id
 
         arglist = [
@@ -309,7 +339,7 @@ class TestCreateSubnet(TestSubnet):
 
     def test_create_options_subnet_range_ipv6(self):
         # Mock SDK calls for this test.
-        self.network.create_subnet = mock.Mock(return_value=self._subnet_ipv6)
+        self.network.create_subnet.return_value = self._subnet_ipv6
         self._network.id = self._subnet_ipv6.network_id
 
         arglist = [
@@ -376,12 +406,12 @@ class TestCreateSubnet(TestSubnet):
             'allocation_pools': self._subnet_ipv6.allocation_pools,
             'service_types': self._subnet_ipv6.service_types,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data_ipv6, data)
 
     def test_create_with_network_segment(self):
         # Mock SDK calls for this test.
-        self.network.create_subnet = mock.Mock(return_value=self._subnet)
         self._network.id = self._subnet.network_id
 
         arglist = [
@@ -410,12 +440,12 @@ class TestCreateSubnet(TestSubnet):
             'network_id': self._subnet.network_id,
             'segment_id': self._network_segment.id,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
     def test_create_with_description(self):
         # Mock SDK calls for this test.
-        self.network.create_subnet = mock.Mock(return_value=self._subnet)
         self._network.id = self._subnet.network_id
 
         arglist = [
@@ -444,6 +474,7 @@ class TestCreateSubnet(TestSubnet):
             'network_id': self._subnet.network_id,
             'description': self._subnet.description,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
@@ -527,7 +558,7 @@ class TestDeleteSubnet(TestSubnet):
         )
 
 
-class TestListSubnet(TestSubnet):
+class TestListSubnet(TestSubnet, _test_tag.TestListTagMixin):
     # The subnets going to be listed up.
     _subnet = network_fakes.FakeSubnet.create_subnets(count=3)
 
@@ -546,6 +577,7 @@ class TestListSubnet(TestSubnet):
         'IP Version',
         'Gateway',
         'Service Types',
+        'Tags',
     )
 
     data = []
@@ -572,6 +604,7 @@ class TestListSubnet(TestSubnet):
             subnet.ip_version,
             subnet.gateway_ip,
             utils.format_list(subnet.service_types),
+            utils.format_list(subnet.tags),
         ))
 
     def setUp(self):
@@ -582,6 +615,9 @@ class TestListSubnet(TestSubnet):
 
         self.network.subnets = mock.Mock(return_value=self._subnet)
 
+        # TestUnsetTagMixin
+        self._tag_list_resource_mock = self.network.subnets
+
     def test_subnet_list_no_options(self):
         arglist = []
         verifylist = [
@@ -802,15 +838,21 @@ class TestListSubnet(TestSubnet):
         self.assertEqual(self.data, list(data))
 
 
-class TestSetSubnet(TestSubnet):
+class TestSetSubnet(TestSubnet, _test_tag.TestSetTagMixin):
 
-    _subnet = network_fakes.FakeSubnet.create_one_subnet()
+    _subnet = network_fakes.FakeSubnet.create_one_subnet(
+        {'tags': ['green', 'red']})
 
     def setUp(self):
         super(TestSetSubnet, self).setUp()
         self.network.update_subnet = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
         self.network.find_subnet = mock.Mock(return_value=self._subnet)
         self.cmd = subnet_v2.SetSubnet(self.app, self.namespace)
+        # TestSetTagMixin
+        self._tag_resource_name = 'subnet'
+        self._tag_test_resource = self._subnet
+        self._tag_update_resource_mock = self.network.update_subnet
 
     def test_set_this(self):
         arglist = [
@@ -867,8 +909,8 @@ class TestSetSubnet(TestSubnet):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         result = self.cmd.take_action(parsed_args)
 
-        attrs = {}
-        self.network.update_subnet.assert_called_with(self._subnet, **attrs)
+        self.assertFalse(self.network.update_subnet.called)
+        self.assertFalse(self.network.set_tags.called)
         self.assertIsNone(result)
 
     def test_append_options(self):
@@ -982,6 +1024,7 @@ class TestShowSubnet(TestSubnet):
         'segment_id',
         'service_types',
         'subnetpool_id',
+        'tags',
     )
 
     data = (
@@ -1002,6 +1045,7 @@ class TestShowSubnet(TestSubnet):
         _subnet.segment_id,
         utils.format_list(_subnet.service_types),
         _subnet.subnetpool_id,
+        utils.format_list(_subnet.tags),
     )
 
     def setUp(self):
@@ -1039,7 +1083,7 @@ class TestShowSubnet(TestSubnet):
         self.assertEqual(self.data, data)
 
 
-class TestUnsetSubnet(TestSubnet):
+class TestUnsetSubnet(TestSubnet, _test_tag.TestUnsetTagMixin):
 
     def setUp(self):
         super(TestUnsetSubnet, self).setUp()
@@ -1055,11 +1099,17 @@ class TestUnsetSubnet(TestSubnet):
                                   {'start': '8.8.8.160',
                                    'end': '8.8.8.170'}],
              'service_types': ['network:router_gateway',
-                               'network:floatingip_agent_gateway'], })
+                               'network:floatingip_agent_gateway'],
+             'tags': ['green', 'red'], })
         self.network.find_subnet = mock.Mock(return_value=self._testsubnet)
         self.network.update_subnet = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
         # Get the command object to test
         self.cmd = subnet_v2.UnsetSubnet(self.app, self.namespace)
+        # TestUnsetTagMixin
+        self._tag_resource_name = 'subnet'
+        self._tag_test_resource = self._testsubnet
+        self._tag_update_resource_mock = self.network.update_subnet
 
     def test_unset_subnet_params(self):
         arglist = [
diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py
index 80a57bbb30..af49385608 100644
--- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py
+++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py
@@ -20,6 +20,7 @@ from osc_lib import utils
 
 from openstackclient.network.v2 import subnet_pool
 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3
+from openstackclient.tests.unit.network.v2 import _test_tag
 from openstackclient.tests.unit.network.v2 import fakes as network_fakes
 from openstackclient.tests.unit import utils as tests_utils
 
@@ -37,7 +38,7 @@ class TestSubnetPool(network_fakes.TestNetworkV2):
         self.domains_mock = self.app.client_manager.identity.domains
 
 
-class TestCreateSubnetPool(TestSubnetPool):
+class TestCreateSubnetPool(TestSubnetPool, _test_tag.TestCreateTagMixin):
 
     project = identity_fakes_v3.FakeProject.create_one_project()
     domain = identity_fakes_v3.FakeDomain.create_one_domain()
@@ -60,6 +61,7 @@ class TestCreateSubnetPool(TestSubnetPool):
         'prefixes',
         'project_id',
         'shared',
+        'tags',
     )
     data = (
         _subnet_pool.address_scope_id,
@@ -75,6 +77,7 @@ class TestCreateSubnetPool(TestSubnetPool):
         utils.format_list(_subnet_pool.prefixes),
         _subnet_pool.project_id,
         _subnet_pool.shared,
+        utils.format_list(_subnet_pool.tags),
     )
 
     def setUp(self):
@@ -82,6 +85,7 @@ class TestCreateSubnetPool(TestSubnetPool):
 
         self.network.create_subnet_pool = mock.Mock(
             return_value=self._subnet_pool)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         # Get the command object to test
         self.cmd = subnet_pool.CreateSubnetPool(self.app, self.namespace)
@@ -92,12 +96,29 @@ class TestCreateSubnetPool(TestSubnetPool):
         self.projects_mock.get.return_value = self.project
         self.domains_mock.get.return_value = self.domain
 
+        # TestUnsetTagMixin
+        self._tag_test_resource = self._subnet_pool
+        self._tag_create_resource_mock = self.network.create_subnet_pool
+        self._tag_create_required_arglist = [
+            '--pool-prefix', '10.0.10.0/24',
+            self._subnet_pool.name,
+        ]
+        self._tag_create_required_verifylist = [
+            ('prefixes', ['10.0.10.0/24']),
+            ('name', self._subnet_pool.name),
+        ]
+        self._tag_create_required_attrs = {
+            'prefixes': ['10.0.10.0/24'],
+            'name': self._subnet_pool.name,
+        }
+
     def test_create_no_options(self):
         arglist = []
         verifylist = []
 
         self.assertRaises(tests_utils.ParserException, self.check_parser,
                           self.cmd, arglist, verifylist)
+        self.assertFalse(self.network.set_tags.called)
 
     def test_create_no_pool_prefix(self):
         """Make sure --pool-prefix is a required argument"""
@@ -127,6 +148,7 @@ class TestCreateSubnetPool(TestSubnetPool):
             'prefixes': ['10.0.10.0/24'],
             'name': self._subnet_pool.name,
         })
+        self.assertFalse(self.network.set_tags.called)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
@@ -374,7 +396,7 @@ class TestDeleteSubnetPool(TestSubnetPool):
         )
 
 
-class TestListSubnetPool(TestSubnetPool):
+class TestListSubnetPool(TestSubnetPool, _test_tag.TestListTagMixin):
     # The subnet pools going to be listed up.
     _subnet_pools = network_fakes.FakeSubnetPool.create_subnet_pools(count=3)
 
@@ -388,6 +410,7 @@ class TestListSubnetPool(TestSubnetPool):
         'Address Scope',
         'Default Subnet Pool',
         'Shared',
+        'Tags',
     )
 
     data = []
@@ -408,6 +431,7 @@ class TestListSubnetPool(TestSubnetPool):
             pool.address_scope_id,
             pool.is_default,
             pool.shared,
+            utils.format_list(pool.tags),
         ))
 
     def setUp(self):
@@ -418,6 +442,9 @@ class TestListSubnetPool(TestSubnetPool):
 
         self.network.subnet_pools = mock.Mock(return_value=self._subnet_pools)
 
+        # TestUnsetTagMixin
+        self._tag_list_resource_mock = self.network.subnet_pools
+
     def test_subnet_pool_list_no_option(self):
         arglist = []
         verifylist = [
@@ -585,11 +612,12 @@ class TestListSubnetPool(TestSubnetPool):
         self.assertEqual(self.data, list(data))
 
 
-class TestSetSubnetPool(TestSubnetPool):
+class TestSetSubnetPool(TestSubnetPool, _test_tag.TestSetTagMixin):
 
     # The subnet_pool to set.
     _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool(
-        {'default_quota': 10},
+        {'default_quota': 10,
+         'tags': ['green', 'red']}
     )
 
     _address_scope = network_fakes.FakeAddressScope.create_one_address_scope()
@@ -598,6 +626,7 @@ class TestSetSubnetPool(TestSubnetPool):
         super(TestSetSubnetPool, self).setUp()
 
         self.network.update_subnet_pool = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         self.network.find_subnet_pool = mock.Mock(
             return_value=self._subnet_pool)
@@ -608,6 +637,11 @@ class TestSetSubnetPool(TestSubnetPool):
         # Get the command object to test
         self.cmd = subnet_pool.SetSubnetPool(self.app, self.namespace)
 
+        # TestUnsetTagMixin
+        self._tag_resource_name = 'subnet_pool'
+        self._tag_test_resource = self._subnet_pool
+        self._tag_update_resource_mock = self.network.update_subnet_pool
+
     def test_set_this(self):
         arglist = [
             '--name', 'noob',
@@ -667,9 +701,8 @@ class TestSetSubnetPool(TestSubnetPool):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         result = self.cmd.take_action(parsed_args)
 
-        attrs = {}
-        self.network.update_subnet_pool.assert_called_once_with(
-            self._subnet_pool, **attrs)
+        self.assertFalse(self.network.update_subnet_pool.called)
+        self.assertFalse(self.network.set_tags.called)
         self.assertIsNone(result)
 
     def test_set_len_negative(self):
@@ -854,6 +887,7 @@ class TestShowSubnetPool(TestSubnetPool):
         'prefixes',
         'project_id',
         'shared',
+        'tags',
     )
 
     data = (
@@ -870,6 +904,7 @@ class TestShowSubnetPool(TestSubnetPool):
         utils.format_list(_subnet_pool.prefixes),
         _subnet_pool.tenant_id,
         _subnet_pool.shared,
+        utils.format_list(_subnet_pool.tags),
     )
 
     def setUp(self):
@@ -908,26 +943,36 @@ class TestShowSubnetPool(TestSubnetPool):
         self.assertEqual(self.data, data)
 
 
-class TestUnsetSubnetPool(TestSubnetPool):
+class TestUnsetSubnetPool(TestSubnetPool, _test_tag.TestUnsetTagMixin):
 
     def setUp(self):
         super(TestUnsetSubnetPool, self).setUp()
         self._subnetpool = network_fakes.FakeSubnetPool.create_one_subnet_pool(
             {'prefixes': ['10.0.10.0/24', '10.1.10.0/24',
-                          '10.2.10.0/24'], })
+                          '10.2.10.0/24'],
+             'tags': ['green', 'red']})
         self.network.find_subnet_pool = mock.Mock(
             return_value=self._subnetpool)
         self.network.update_subnet_pool = mock.Mock(return_value=None)
+        self.network.set_tags = mock.Mock(return_value=None)
         # Get the command object to test
         self.cmd = subnet_pool.UnsetSubnetPool(self.app, self.namespace)
 
+        # TestUnsetTagMixin
+        self._tag_resource_name = 'subnet_pool'
+        self._tag_test_resource = self._subnetpool
+        self._tag_update_resource_mock = self.network.update_subnet_pool
+
     def test_unset_subnet_pool(self):
         arglist = [
             '--pool-prefix', '10.0.10.0/24',
             '--pool-prefix', '10.1.10.0/24',
             self._subnetpool.name,
         ]
-        verifylist = [('prefixes', ['10.0.10.0/24', '10.1.10.0/24'])]
+        verifylist = [
+            ('prefixes', ['10.0.10.0/24', '10.1.10.0/24']),
+            ('subnet_pool', self._subnetpool.name),
+        ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         result = self.cmd.take_action(parsed_args)
         attrs = {'prefixes': ['10.2.10.0/24']}
@@ -940,7 +985,10 @@ class TestUnsetSubnetPool(TestSubnetPool):
             '--pool-prefix', '10.100.1.1/25',
             self._subnetpool.name,
         ]
-        verifylist = [('prefixes', ['10.100.1.1/25'])]
+        verifylist = [
+            ('prefixes', ['10.100.1.1/25']),
+            ('subnet_pool', self._subnetpool.name),
+        ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         self.assertRaises(exceptions.CommandError,
                           self.cmd.take_action,
diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py
index 3c5c8683f1..8f9cc7b17c 100644
--- a/openstackclient/tests/unit/utils.py
+++ b/openstackclient/tests/unit/utils.py
@@ -25,6 +25,12 @@ class ParserException(Exception):
     pass
 
 
+class CompareBySet(list):
+    """Class to compare value using set."""
+    def __eq__(self, other):
+        return set(self) == set(other)
+
+
 class TestCase(testtools.TestCase):
 
     def setUp(self):
diff --git a/releasenotes/notes/bp-neutron-client-tag-ff24d13e5c70e052.yaml b/releasenotes/notes/bp-neutron-client-tag-ff24d13e5c70e052.yaml
new file mode 100644
index 0000000000..4addd07b7c
--- /dev/null
+++ b/releasenotes/notes/bp-neutron-client-tag-ff24d13e5c70e052.yaml
@@ -0,0 +1,13 @@
+---
+features:
+  - |
+    Added support for ``tags`` to the following resources:
+    ``network``, ``subnet``, ``port``, ``router`` and ``subnet pool``.
+    [Blueprint :oscbp:`neutron-client-tag`]
+
+    - Add ``--tag`` and ``--no-tag`` options to corresponding "create" commands.
+    - Add ``--tag`` and ``--no-tag`` options to corresponding "set" commands.
+    - Add ``--tag`` and ``--all-tag`` options to corresponding "unset" commands.
+      (``network unset`` command is introduced to support the tag operation)
+    - Add ``--tags``, ``--any-tags``, ``--not-tags`` and ``--not-any-tags``
+      options to corresponding "list" commands.
diff --git a/setup.cfg b/setup.cfg
index 16917e5099..ec91988f10 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -395,6 +395,7 @@ openstack.network.v2 =
     network_list = openstackclient.network.v2.network:ListNetwork
     network_set = openstackclient.network.v2.network:SetNetwork
     network_show = openstackclient.network.v2.network:ShowNetwork
+    network_unset = openstackclient.network.v2.network:UnsetNetwork
 
     network_meter_create = openstackclient.network.v2.network_meter:CreateMeter
     network_meter_delete = openstackclient.network.v2.network_meter:DeleteMeter