diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 322943d76..c6bde8828 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -128,12 +128,23 @@ Set flavor properties os flavor set [--property [...] ] + [--project ] + [--project-domain ] .. option:: --property Property to add or modify for this flavor (repeat option to set multiple properties) +.. option:: --project + + Set flavor access to project (name or ID) (admin only) + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + .. describe:: Flavor to modify (name or ID) diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 37ff831d5..48d0e27e1 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -22,6 +22,7 @@ from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common def _find_flavor(compute_client, flavor): @@ -245,6 +246,11 @@ class SetFlavor(command.Command): def get_parser(self, prog_name): parser = super(SetFlavor, self).get_parser(prog_name) + parser.add_argument( + "flavor", + metavar="", + help=_("Flavor to modify (name or ID)") + ) parser.add_argument( "--property", metavar="", @@ -253,16 +259,54 @@ class SetFlavor(command.Command): "(repeat option to set multiple properties)") ) parser.add_argument( - "flavor", - metavar="", - help=_("Flavor to modify (name or ID)") + '--project', + metavar='', + help=_('Set flavor access to project (name or ID) ' + '(admin only)'), ) + identity_common.add_project_domain_option_to_parser(parser) + return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity + flavor = _find_flavor(compute_client, parsed_args.flavor) - flavor.set_keys(parsed_args.property) + + if not parsed_args.property and not parsed_args.project: + raise exceptions.CommandError(_("Nothing specified to be set.")) + + result = 0 + if parsed_args.property: + try: + flavor.set_keys(parsed_args.property) + except Exception as e: + self.app.log.error( + _("Failed to set flavor property: %s") % str(e)) + result += 1 + + if parsed_args.project: + try: + if flavor.is_public: + msg = _("Cannot set access for a public flavor") + raise exceptions.CommandError(msg) + else: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + compute_client.flavor_access.add_tenant_access( + flavor.id, project_id) + except Exception as e: + self.app.log.error(_("Failed to set flavor access to" + " project: %s") % str(e)) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("Command Failed: One or more of" + " the operations failed")) class ShowFlavor(command.ShowOne): diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index 62a46b1d5..505469ad1 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -132,6 +132,9 @@ class FakeComputev2Client(object): self.flavors = mock.Mock() self.flavors.resource_class = fakes.FakeResource(None, {}) + self.flavor_access = mock.Mock() + self.flavor_access.resource_class = fakes.FakeResource(None, {}) + self.quotas = mock.Mock() self.quotas.resource_class = fakes.FakeResource(None, {}) diff --git a/openstackclient/tests/compute/v2/test_flavor.py b/openstackclient/tests/compute/v2/test_flavor.py index 6f507b169..e5bdffe4e 100644 --- a/openstackclient/tests/compute/v2/test_flavor.py +++ b/openstackclient/tests/compute/v2/test_flavor.py @@ -13,10 +13,14 @@ # under the License. # +import copy + from openstackclient.common import exceptions from openstackclient.common import utils from openstackclient.compute.v2 import flavor from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests import utils as tests_utils @@ -29,6 +33,13 @@ class TestFlavor(compute_fakes.TestComputev2): self.flavors_mock = self.app.client_manager.compute.flavors self.flavors_mock.reset_mock() + # Get a shortcut to the FlavorAccessManager Mock + self.flavor_access_mock = self.app.client_manager.compute.flavor_access + self.flavor_access_mock.reset_mock() + + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + class TestFlavorCreate(TestFlavor): @@ -427,16 +438,23 @@ class TestFlavorList(TestFlavor): class TestFlavorSet(TestFlavor): # Return value of self.flavors_mock.find(). - flavor = compute_fakes.FakeFlavor.create_one_flavor() + flavor = compute_fakes.FakeFlavor.create_one_flavor( + attrs={'os-flavor-access:is_public': False}) def setUp(self): super(TestFlavorSet, self).setUp() self.flavors_mock.find.return_value = self.flavor self.flavors_mock.get.side_effect = exceptions.NotFound(None) + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) self.cmd = flavor.SetFlavor(self.app, None) - def test_flavor_set(self): + def test_flavor_set_property(self): arglist = [ '--property', 'FOO="B A R"', 'baremetal' @@ -452,6 +470,83 @@ class TestFlavorSet(TestFlavor): is_public=None) self.assertIsNone(result) + def test_flavor_set_project(self): + arglist = [ + '--project', identity_fakes.project_id, + self.flavor.id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ('flavor', self.flavor.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.flavor_access_mock.add_tenant_access.assert_called_with( + self.flavor.id, + identity_fakes.project_id, + ) + + def test_flavor_set_no_project(self): + arglist = [ + '--project', '', + self.flavor.id, + ] + verifylist = [ + ('project', ''), + ('flavor', self.flavor.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_flavor_set_no_flavor(self): + arglist = [ + '--project', identity_fakes.project_id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] + + self.assertRaises(tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + def test_flavor_set_with_unexist_flavor(self): + self.flavors_mock.get.side_effect = exceptions.NotFound(None) + self.flavors_mock.find.side_effect = exceptions.NotFound(None) + + arglist = [ + '--project', identity_fakes.project_id, + 'unexist_flavor', + ] + verifylist = [ + ('project', identity_fakes.project_id), + ('flavor', 'unexist_flavor'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_flavor_set_nothing(self): + arglist = [ + self.flavor.id, + ] + verifylist = [ + ('flavor', self.flavor.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestFlavorShow(TestFlavor): diff --git a/releasenotes/notes/bug-1575461-4d7d90e792132064.yaml b/releasenotes/notes/bug-1575461-4d7d90e792132064.yaml new file mode 100644 index 000000000..6bc9f73a6 --- /dev/null +++ b/releasenotes/notes/bug-1575461-4d7d90e792132064.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support for setting flavor access to project by using below command + ``flavor set --project `` + [Bug `1575461 `_]