From aaed4b315fa1e86b1ddf73201b5350dbdb4a660e Mon Sep 17 00:00:00 2001
From: Dongcan Ye <hellochosen@gmail.com>
Date: Sat, 24 Feb 2018 09:13:48 +0000
Subject: [PATCH] Network: Add tag support for security group

Change-Id: Icccb23429913724c6a8bd15d4737672b47a5f13a
Closes-Bug: #1750983
---
 .../cli/command-objects/security-group.rst    |  74 ++++++++
 openstackclient/network/v2/security_group.py  |  43 ++++-
 .../tests/unit/network/v2/fakes.py            |   1 +
 .../network/v2/test_security_group_network.py | 174 +++++++++++++++++-
 .../notes/bug-1750983-420945d6c0afb509.yaml   |   8 +
 setup.cfg                                     |   1 +
 6 files changed, 295 insertions(+), 6 deletions(-)
 create mode 100644 releasenotes/notes/bug-1750983-420945d6c0afb509.yaml

diff --git a/doc/source/cli/command-objects/security-group.rst b/doc/source/cli/command-objects/security-group.rst
index a95a96f49f..403e5fc08f 100644
--- a/doc/source/cli/command-objects/security-group.rst
+++ b/doc/source/cli/command-objects/security-group.rst
@@ -19,6 +19,7 @@ Create a new security group
     openstack security group create
         [--description <description>]
         [--project <project> [--project-domain <project-domain>]]
+        [--tag <tag> | --no-tag]
         <name>
 
 .. option:: --description <description>
@@ -38,6 +39,18 @@ Create a new security group
 
     *Network version 2 only*
 
+.. option:: --tag <tag>
+
+    Tag to be added to the security group (repeat option to set multiple tags)
+
+    *Network version 2 only*
+
+.. option:: --no-tag
+
+    No tags associated with the security group
+
+    *Network version 2 only*
+
 .. describe:: <name>
 
     New security group name
@@ -68,6 +81,8 @@ List security groups
     openstack security group list
         [--all-projects]
         [--project <project> [--project-domain <project-domain>]]
+        [--tags <tag>[,<tag>,...]] [--any-tags <tag>[,<tag>,...]]
+        [--not-tags <tag>[,<tag>,...]] [--not-any-tags <tag>[,<tag>,...]]
 
 .. option:: --all-projects
 
@@ -89,6 +104,30 @@ List security groups
 
     *Network version 2 only*
 
+.. option:: --tags <tag>[,<tag>,...]
+
+    List security groups which have all given tag(s)
+
+    *Network version 2 only*
+
+.. option:: --any-tags <tag>[,<tag>,...]
+
+    List security groups which have any given tag(s)
+
+    *Network version 2 only*
+
+.. option:: --not-tags <tag>[,<tag>,...]
+
+    Exclude security groups which have all given tag(s)
+
+    *Network version 2 only*
+
+.. option:: --not-any-tags <tag>[,<tag>,...]
+
+    Exclude security groups which have any given tag(s)
+
+    *Network version 2 only*
+
 security group set
 ------------------
 
@@ -100,6 +139,7 @@ Set security group properties
     openstack security group set
         [--name <new-name>]
         [--description <description>]
+        [--tag <tag>] [--no-tag]
         <group>
 
 .. option:: --name <new-name>
@@ -110,6 +150,15 @@ Set security group properties
 
     New security group description
 
+.. option:: --tag <tag>
+
+    Tag to be added to the security group (repeat option to set multiple tags)
+
+.. option:: --no-tag
+
+    Clear tags associated with the security group. Specify both --tag
+    and --no-tag to overwrite current tags
+
 .. describe:: <group>
 
     Security group to modify (name or ID)
@@ -128,3 +177,28 @@ Display security group details
 .. describe:: <group>
 
     Security group to display (name or ID)
+
+security group unset
+--------------------
+
+Unset security group properties
+
+.. program:: security group unset
+.. code:: bash
+
+    openstack security group unset
+        [--tag <tag> | --all-tag]
+        <group>
+
+.. option:: --tag <tag>
+
+    Tag to be removed from the security group
+    (repeat option to remove multiple tags)
+
+.. option:: --all-tag
+
+    Clear all tags associated with the security group
+
+.. describe:: <group>
+
+    Security group to modify (name or ID)
diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py
index 75af3587e0..ed6c8d7c24 100644
--- a/openstackclient/network/v2/security_group.py
+++ b/openstackclient/network/v2/security_group.py
@@ -15,6 +15,7 @@
 
 import argparse
 
+from osc_lib.command import command
 from osc_lib import utils
 import six
 
@@ -23,6 +24,7 @@ from openstackclient.identity import common as identity_common
 from openstackclient.network import common
 from openstackclient.network import sdk_utils
 from openstackclient.network import utils as network_utils
+from openstackclient.network.v2 import _tag
 
 
 def _format_network_security_group_rules(sg_rules):
@@ -106,6 +108,7 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne):
             help=_("Owner's project (name or ID)")
         )
         identity_common.add_project_domain_option_to_parser(parser)
+        _tag.add_tag_option_to_parser_for_create(parser, _('security group'))
         return parser
 
     def _get_description(self, parsed_args):
@@ -130,6 +133,8 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne):
 
         # Create the security group and display the results.
         obj = client.create_security_group(**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, property_columns = _get_columns(obj)
         data = utils.get_item_properties(
             obj,
@@ -198,6 +203,7 @@ class ListSecurityGroup(common.NetworkAndComputeLister):
                    "(name or ID)")
         )
         identity_common.add_project_domain_option_to_parser(parser)
+        _tag.add_tag_filtering_option_to_parser(parser, _('security group'))
         return parser
 
     def update_parser_compute(self, parser):
@@ -220,19 +226,23 @@ class ListSecurityGroup(common.NetworkAndComputeLister):
             ).id
             filters['tenant_id'] = project_id
             filters['project_id'] = project_id
+
+        _tag.get_tag_filtering_args(parsed_args, filters)
         data = client.security_groups(**filters)
 
         columns = (
             "ID",
             "Name",
             "Description",
-            "Project ID"
+            "Project ID",
+            "tags"
         )
         column_headers = (
             "ID",
             "Name",
             "Description",
-            "Project"
+            "Project",
+            "Tags"
         )
         return (column_headers,
                 (utils.get_item_properties(
@@ -282,6 +292,10 @@ class SetSecurityGroup(common.NetworkAndComputeCommand):
         )
         return parser
 
+    def update_parser_network(self, parser):
+        _tag.add_tag_option_to_parser_for_set(parser, _('security group'))
+        return parser
+
     def take_action_network(self, client, parsed_args):
         obj = client.find_security_group(parsed_args.group,
                                          ignore_missing=False)
@@ -295,6 +309,9 @@ class SetSecurityGroup(common.NetworkAndComputeCommand):
         # the update.
         client.update_security_group(obj, **attrs)
 
+        # tags is a subresource and it needs to be updated separately.
+        _tag.update_tags_for_set(client, obj, parsed_args)
+
     def take_action_compute(self, client, parsed_args):
         data = client.api.security_group_find(parsed_args.group)
 
@@ -344,3 +361,25 @@ class ShowSecurityGroup(common.NetworkAndComputeShowOne):
             formatters=_formatters_compute
         )
         return (display_columns, data)
+
+
+class UnsetSecurityGroup(command.Command):
+    _description = _("Unset security group properties")
+
+    def get_parser(self, prog_name):
+        parser = super(UnsetSecurityGroup, self).get_parser(prog_name)
+        parser.add_argument(
+            'group',
+            metavar="<group>",
+            help=_("Security group to modify (name or ID)")
+        )
+        _tag.add_tag_option_to_parser_for_unset(parser, _('security group'))
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.network
+        obj = client.find_security_group(parsed_args.group,
+                                         ignore_missing=False)
+
+        # 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/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py
index a77cab8b5f..e69df88adf 100644
--- a/openstackclient/tests/unit/network/v2/fakes.py
+++ b/openstackclient/tests/unit/network/v2/fakes.py
@@ -1131,6 +1131,7 @@ class FakeSecurityGroup(object):
             'description': 'security-group-description-' + uuid.uuid4().hex,
             'project_id': 'project-id-' + uuid.uuid4().hex,
             'security_group_rules': [],
+            'tags': []
         }
 
         # Overwrite default attributes.
diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py
index 35b7e366d6..83208287cc 100644
--- a/openstackclient/tests/unit/network/v2/test_security_group_network.py
+++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py
@@ -40,8 +40,8 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
     project = identity_fakes.FakeProject.create_one_project()
     domain = identity_fakes.FakeDomain.create_one_domain()
     # The security group to be created.
-    _security_group = \
-        network_fakes.FakeSecurityGroup.create_one_security_group()
+    _security_group = (
+        network_fakes.FakeSecurityGroup.create_one_security_group())
 
     columns = (
         'description',
@@ -49,6 +49,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
         'name',
         'project_id',
         'rules',
+        'tags',
     )
 
     data = (
@@ -57,6 +58,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
         _security_group.name,
         _security_group.project_id,
         '',
+        _security_group.tags,
     )
 
     def setUp(self):
@@ -67,6 +69,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
 
         self.projects_mock.get.return_value = self.project
         self.domains_mock.get.return_value = self.domain
+        self.network.set_tags = mock.Mock(return_value=None)
 
         # Get the command object to test
         self.cmd = security_group.CreateSecurityGroup(self.app, self.namespace)
@@ -118,6 +121,43 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
 
+    def _test_create_with_tag(self, add_tags=True):
+        arglist = [self._security_group.name]
+        if add_tags:
+            arglist += ['--tag', 'red', '--tag', 'blue']
+        else:
+            arglist += ['--no-tag']
+
+        verifylist = [
+            ('name', self._security_group.name),
+        ]
+        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.network.create_security_group.assert_called_once_with(**{
+            'description': self._security_group.name,
+            'name': self._security_group.name,
+        })
+        if add_tags:
+            self.network.set_tags.assert_called_once_with(
+                self._security_group,
+                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 TestDeleteSecurityGroupNetwork(TestSecurityGroupNetwork):
 
@@ -214,6 +254,7 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork):
         'Name',
         'Description',
         'Project',
+        'Tags',
     )
 
     data = []
@@ -223,6 +264,7 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork):
             grp.name,
             grp.description,
             grp.project_id,
+            grp.tags,
         ))
 
     def setUp(self):
@@ -300,12 +342,38 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork):
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, list(data))
 
+    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.network.security_groups.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 TestSetSecurityGroupNetwork(TestSecurityGroupNetwork):
 
     # The security group to be set.
-    _security_group = \
-        network_fakes.FakeSecurityGroup.create_one_security_group()
+    _security_group = (
+        network_fakes.FakeSecurityGroup.create_one_security_group(
+            attrs={'tags': ['green', 'red']}))
 
     def setUp(self):
         super(TestSetSecurityGroupNetwork, self).setUp()
@@ -314,6 +382,7 @@ class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork):
 
         self.network.find_security_group = mock.Mock(
             return_value=self._security_group)
+        self.network.set_tags = mock.Mock(return_value=None)
 
         # Get the command object to test
         self.cmd = security_group.SetSecurityGroup(self.app, self.namespace)
@@ -366,6 +435,34 @@ class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork):
         )
         self.assertIsNone(result)
 
+    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._security_group.name)
+        verifylist.append(
+            ('group', self._security_group.name))
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        result = self.cmd.take_action(parsed_args)
+
+        self.assertTrue(self.network.update_security_group.called)
+        self.network.set_tags.assert_called_once_with(
+            self._security_group,
+            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 TestShowSecurityGroupNetwork(TestSecurityGroupNetwork):
 
@@ -385,6 +482,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork):
         'name',
         'project_id',
         'rules',
+        'tags',
     )
 
     data = (
@@ -394,6 +492,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork):
         _security_group.project_id,
         security_group._format_network_security_group_rules(
             [_security_group_rule._info]),
+        _security_group.tags,
     )
 
     def setUp(self):
@@ -424,3 +523,70 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork):
             self._security_group.id, ignore_missing=False)
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, data)
+
+
+class TestUnsetSecurityGroupNetwork(TestSecurityGroupNetwork):
+
+    # The security group to be unset.
+    _security_group = (
+        network_fakes.FakeSecurityGroup.create_one_security_group(
+            attrs={'tags': ['green', 'red']}))
+
+    def setUp(self):
+        super(TestUnsetSecurityGroupNetwork, self).setUp()
+
+        self.network.update_security_group = mock.Mock(return_value=None)
+
+        self.network.find_security_group = mock.Mock(
+            return_value=self._security_group)
+        self.network.set_tags = mock.Mock(return_value=None)
+
+        # Get the command object to test
+        self.cmd = security_group.UnsetSecurityGroup(self.app, self.namespace)
+
+    def test_set_no_options(self):
+        self.assertRaises(tests_utils.ParserException,
+                          self.check_parser, self.cmd, [], [])
+
+    def test_set_no_updates(self):
+        arglist = [
+            self._security_group.name,
+        ]
+        verifylist = [
+            ('group', self._security_group.name),
+        ]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        result = self.cmd.take_action(parsed_args)
+
+        self.assertFalse(self.network.update_security_group.called)
+        self.assertFalse(self.network.set_tags.called)
+        self.assertIsNone(result)
+
+    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._security_group.name)
+        verifylist.append(
+            ('group', self._security_group.name))
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        result = self.cmd.take_action(parsed_args)
+
+        self.assertFalse(self.network.update_security_group.called)
+        self.network.set_tags.assert_called_once_with(
+            self._security_group,
+            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/releasenotes/notes/bug-1750983-420945d6c0afb509.yaml b/releasenotes/notes/bug-1750983-420945d6c0afb509.yaml
new file mode 100644
index 0000000000..2c37d88fad
--- /dev/null
+++ b/releasenotes/notes/bug-1750983-420945d6c0afb509.yaml
@@ -0,0 +1,8 @@
+---
+fixes:
+  - |
+    Add supports tagging for Network security group.
+    Add ``tag``, ``no-tag`` to ``security group create`` and ``security group set`` commands.
+    Add ``tags``, ``any-tags``, ``not-tags``, ``not-any-tags`` to ``security group list`` command.
+    Add ``tag`` and ``all-tag`` to ``security group unset`` command.
+    [Bug `1750983 <https://bugs.launchpad.net/python-openstackclient/+bug/1750983>`_]
diff --git a/setup.cfg b/setup.cfg
index 63bfdafb13..37592f495f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -467,6 +467,7 @@ openstack.network.v2 =
     security_group_list = openstackclient.network.v2.security_group:ListSecurityGroup
     security_group_set = openstackclient.network.v2.security_group:SetSecurityGroup
     security_group_show = openstackclient.network.v2.security_group:ShowSecurityGroup
+    security_group_unset = openstackclient.network.v2.security_group:UnsetSecurityGroup
 
     security_group_rule_create = openstackclient.network.v2.security_group_rule:CreateSecurityGroupRule
     security_group_rule_delete = openstackclient.network.v2.security_group_rule:DeleteSecurityGroupRule