diff --git a/doc/source/cli/command-objects/project.rst b/doc/source/cli/command-objects/project.rst index ac7e8cd1ce..afa785cbbe 100644 --- a/doc/source/cli/command-objects/project.rst +++ b/doc/source/cli/command-objects/project.rst @@ -16,6 +16,7 @@ Create new project [--domain ] [--parent ] [--description ] + [--immutable | --no-immutable] [--enable | --disable] [--property ] [--or-show] @@ -46,6 +47,15 @@ Create new project Disable project +.. option:: --immutable + + Make project immutable. An immutable project may not be deleted or + modified except to remove the immutable flag + +.. option:: --no-immutable + + Make project mutable (default) + .. option:: --property Add a property to :ref:`\ ` @@ -180,6 +190,7 @@ Set project properties [--name ] [--domain ] [--description ] + [--immutable | --no-immutable] [--enable | --disable] [--property ] [--tag | --clear-tags | --remove-tags ] @@ -199,6 +210,15 @@ Set project properties Set project description +.. option:: --immutable + + Make project immutable. An immutable project may not be deleted or + modified except to remove the immutable flag + +.. option:: --no-immutable + + Make project mutable (default) + .. option:: --enable Enable project (default) diff --git a/doc/source/cli/command-objects/role.rst b/doc/source/cli/command-objects/role.rst index 752dc4e03c..f9fd28eb75 100644 --- a/doc/source/cli/command-objects/role.rst +++ b/doc/source/cli/command-objects/role.rst @@ -97,6 +97,7 @@ Create new role openstack role create [--or-show] [--domain ] + [--immutable | --no-immutable] .. option:: --domain @@ -119,6 +120,15 @@ Create new role Add description about the role +.. option:: --immutable + + Make role immutable. An immutable role may not be deleted or modified + except to remove the immutable flag + +.. option:: --no-immutable + + Make role mutable (default) + role delete ----------- @@ -253,6 +263,7 @@ Set role properties openstack role set [--name ] [--domain ] + [--immutable | --no-immutable] .. option:: --name @@ -269,6 +280,15 @@ Set role properties Role to modify (name or ID) +.. option:: --immutable + + Make role immutable. An immutable role may not be deleted or modified + except to remove the immutable flag + +.. option:: --no-immutable + + Make role mutable (default) + role show --------- diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 7be2a17b72..e70d87d21f 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -213,6 +213,15 @@ def _find_identity_resource(identity_client_manager, name_or_id, return resource_type(None, {'id': name_or_id, 'name': name_or_id}) +def get_immutable_options(parsed_args): + options = {} + if parsed_args.immutable: + options['immutable'] = True + if parsed_args.no_immutable: + options['immutable'] = False + return options + + def add_user_domain_option_to_parser(parser): parser.add_argument( '--user-domain', @@ -261,3 +270,18 @@ def add_inherited_option_to_parser(parser): help=_('Specifies if the role grant is inheritable to the sub ' 'projects'), ) + + +def add_resource_option_to_parser(parser): + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--immutable', + action='store_true', + help=_('Make resource immutable. An immutable project may not ' + 'be deleted or modified except to remove the immutable flag'), + ) + enable_group.add_argument( + '--no-immutable', + action='store_true', + help=_('Make resource mutable (default)'), + ) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index dbcc97f6d2..e33fce05c5 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -60,6 +60,7 @@ class CreateDomain(command.ShowOne): action='store_true', help=_('Return existing domain'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -69,10 +70,13 @@ class CreateDomain(command.ShowOne): if parsed_args.disable: enabled = False + options = common.get_immutable_options(parsed_args) + try: domain = identity_client.domains.create( name=parsed_args.name, description=parsed_args.description, + options=options, enabled=enabled, ) except ks_exc.Conflict: @@ -163,6 +167,7 @@ class SetDomain(command.Command): action='store_true', help=_('Disable domain'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -180,6 +185,10 @@ class SetDomain(command.Command): if parsed_args.disable: kwargs['enabled'] = False + options = common.get_immutable_options(parsed_args) + if options: + kwargs['options'] = options + identity_client.domains.update(domain.id, **kwargs) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 9ecc70ef4e..e32da165b4 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -78,6 +78,7 @@ class CreateProject(command.ShowOne): action='store_true', help=_('Return existing project'), ) + common.add_resource_option_to_parser(parser) tag.add_tag_option_to_parser_for_create(parser, _('project')) return parser @@ -99,6 +100,9 @@ class CreateProject(command.ShowOne): enabled = True if parsed_args.disable: enabled = False + + options = common.get_immutable_options(parsed_args) + kwargs = {} if parsed_args.property: kwargs = parsed_args.property.copy() @@ -111,6 +115,7 @@ class CreateProject(command.ShowOne): parent=parent, description=parsed_args.description, enabled=enabled, + options=options, **kwargs ) except ks_exc.Conflict: @@ -317,6 +322,7 @@ class SetProject(command.Command): help=_('Set a property on ' '(repeat option to set multiple properties)'), ) + common.add_resource_option_to_parser(parser) tag.add_tag_option_to_parser_for_set(parser, _('project')) return parser @@ -336,6 +342,9 @@ class SetProject(command.Command): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False + options = common.get_immutable_options(parsed_args) + if options: + kwargs['options'] = options if parsed_args.property: kwargs.update(parsed_args.property) tag.update_tags_in_args(parsed_args, project, kwargs) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 36f3f9384b..980ebf1165 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -191,6 +191,7 @@ class CreateRole(command.ShowOne): action='store_true', help=_('Return existing role'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -201,10 +202,12 @@ class CreateRole(command.ShowOne): domain_id = common.find_domain(identity_client, parsed_args.domain).id + options = common.get_immutable_options(parsed_args) + try: role = identity_client.roles.create( name=parsed_args.name, domain=domain_id, - description=parsed_args.description) + description=parsed_args.description, options=options) except ks_exc.Conflict: if parsed_args.or_show: @@ -366,6 +369,7 @@ class SetRole(command.Command): metavar='', help=_('Set role name'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -376,12 +380,14 @@ class SetRole(command.Command): domain_id = common.find_domain(identity_client, parsed_args.domain).id + options = common.get_immutable_options(parsed_args) role = utils.find_resource(identity_client.roles, parsed_args.role, domain_id=domain_id) identity_client.roles.update(role.id, name=parsed_args.name, - description=parsed_args.description) + description=parsed_args.description, + options=options) class ShowRole(command.ShowOne): diff --git a/openstackclient/tests/unit/identity/v3/test_domain.py b/openstackclient/tests/unit/identity/v3/test_domain.py index 014986e573..46f389e890 100644 --- a/openstackclient/tests/unit/identity/v3/test_domain.py +++ b/openstackclient/tests/unit/identity/v3/test_domain.py @@ -68,6 +68,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -97,6 +98,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': 'new desc', + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -126,6 +128,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -155,6 +158,7 @@ class TestDomainCreate(TestDomain): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': False, } self.domains_mock.create.assert_called_with( @@ -164,6 +168,66 @@ class TestDomainCreate(TestDomain): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_domain_create_with_immutable(self): + arglist = [ + '--immutable', + self.domain.name, + ] + verifylist = [ + ('immutable', True), + ('name', self.domain.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.domain.name, + 'description': None, + 'options': {'immutable': True}, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_domain_create_with_no_immutable(self): + arglist = [ + '--no-immutable', + self.domain.name, + ] + verifylist = [ + ('no_immutable', True), + ('name', self.domain.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.domain.name, + 'description': None, + 'options': {'immutable': False}, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + class TestDomainDelete(TestDomain): @@ -354,6 +418,52 @@ class TestDomainSet(TestDomain): ) self.assertIsNone(result) + def test_domain_set_immutable_option(self): + arglist = [ + '--immutable', + self.domain.id, + ] + verifylist = [ + ('immutable', True), + ('domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': True}, + } + self.domains_mock.update.assert_called_with( + self.domain.id, + **kwargs + ) + self.assertIsNone(result) + + def test_domain_set_no_immutable_option(self): + arglist = [ + '--no-immutable', + self.domain.id, + ] + verifylist = [ + ('no_immutable', True), + ('domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': False}, + } + self.domains_mock.update.assert_called_with( + self.domain.id, + **kwargs + ) + self.assertIsNone(result) + class TestDomainShow(TestDomain): diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 466bea1867..8852aa8ecc 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -98,7 +98,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -156,7 +157,8 @@ class TestProjectCreate(TestProject): 'description': 'new desc', 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -194,7 +196,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -232,7 +235,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } self.projects_mock.create.assert_called_with( **kwargs @@ -266,7 +270,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -302,7 +307,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': False, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, # description=, enabled=, **kwargs) @@ -339,7 +345,8 @@ class TestProjectCreate(TestProject): 'parent': None, 'fee': 'fi', 'fo': 'fum', - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -380,7 +387,8 @@ class TestProjectCreate(TestProject): 'parent': self.parent.id, 'description': None, 'enabled': True, - 'tags': [] + 'tags': [], + 'options': {}, } self.projects_mock.create.assert_called_with( @@ -465,7 +473,8 @@ class TestProjectCreate(TestProject): 'description': None, 'enabled': True, 'parent': None, - 'tags': ['foo'] + 'tags': ['foo'], + 'options': {}, } self.projects_mock.create.assert_called_with( **kwargs @@ -474,6 +483,86 @@ class TestProjectCreate(TestProject): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_project_create_with_immutable_option(self): + arglist = [ + '--immutable', + self.project.name, + ] + verifylist = [ + ('immutable', True), + ('description', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('parent', None), + ('tags', []) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'tags': [], + 'options': {'immutable': True}, + } + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_with_no_immutable_option(self): + arglist = [ + '--no-immutable', + self.project.name, + ] + verifylist = [ + ('no_immutable', True), + ('description', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('parent', None), + ('tags', []) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'tags': [], + 'options': {'immutable': False}, + } + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + class TestProjectDelete(TestProject): @@ -927,6 +1016,60 @@ class TestProjectSet(TestProject): ) self.assertIsNone(result) + def test_project_set_with_immutable_option(self): + arglist = [ + '--domain', self.project.domain_id, + '--immutable', + self.project.name, + ] + verifylist = [ + ('domain', self.project.domain_id), + ('immutable', True), + ('enable', False), + ('disable', False), + ('project', self.project.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': True}, + } + self.projects_mock.update.assert_called_with( + self.project.id, + **kwargs + ) + self.assertIsNone(result) + + def test_project_set_with_no_immutable_option(self): + arglist = [ + '--domain', self.project.domain_id, + '--no-immutable', + self.project.name, + ] + verifylist = [ + ('domain', self.project.domain_id), + ('no_immutable', True), + ('enable', False), + ('disable', False), + ('project', self.project.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': False}, + } + self.projects_mock.update.assert_called_with( + self.project.id, + **kwargs + ) + self.assertIsNone(result) + class TestProjectShow(TestProject): diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 4278ab1cc1..544da7c15d 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -333,6 +333,7 @@ class TestRoleCreate(TestRole): 'domain': None, 'name': identity_fakes.role_name, 'description': None, + 'options': {}, } # RoleManager.create(name=, domain=) @@ -377,6 +378,7 @@ class TestRoleCreate(TestRole): 'domain': identity_fakes.domain_id, 'name': identity_fakes.ROLE_2['name'], 'description': None, + 'options': {}, } # RoleManager.create(name=, domain=) @@ -420,6 +422,97 @@ class TestRoleCreate(TestRole): 'description': identity_fakes.role_description, 'name': identity_fakes.ROLE_2['name'], 'domain': None, + 'options': {}, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + + def test_role_create_with_immutable_option(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('immutable', True), + ('name', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + + 'options': {'immutable': True}, + 'description': None, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + + def test_role_create_with_no_immutable_option(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--no-immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('no_immutable', True), + ('name', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + + 'options': {'immutable': False}, + 'description': None, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, } # RoleManager.create(name=, domain=) @@ -871,6 +964,7 @@ class TestRoleSet(TestRole): kwargs = { 'name': 'over', 'description': None, + 'options': {}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( @@ -903,6 +997,7 @@ class TestRoleSet(TestRole): kwargs = { 'name': 'over', 'description': None, + 'options': {}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( @@ -935,6 +1030,73 @@ class TestRoleSet(TestRole): kwargs = { 'name': 'over', 'description': identity_fakes.role_description, + 'options': {}, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_with_immutable(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('immutable', True), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': None, + 'options': {'immutable': True}, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_with_no_immutable(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--no-immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('no_immutable', True), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': None, + 'options': {'immutable': False}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( diff --git a/releasenotes/notes/add_resource_option_immutable-efed6e1ebdc69591.yaml b/releasenotes/notes/add_resource_option_immutable-efed6e1ebdc69591.yaml new file mode 100644 index 0000000000..814823fe19 --- /dev/null +++ b/releasenotes/notes/add_resource_option_immutable-efed6e1ebdc69591.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added the below mentioned parameters to the role, project and domain commands. + + * --immutable + * --no-immutable + + This will allow user to set "immutable" resource option. \ No newline at end of file