Add resource option immutable

This patch adds the --immutable and --no-immutable option to the
role, project and domain CLI.

Related-Patch: https://review.opendev.org/#/c/712182/

Change-Id: I9c3bdd741f28bf558267fb217818d947597ce13e
This commit is contained in:
Vishakha Agarwal 2020-03-26 22:23:57 +05:30
parent 05da145eae
commit 7f66273d3f
10 changed files with 523 additions and 11 deletions

View File

@ -16,6 +16,7 @@ Create new project
[--domain <domain>]
[--parent <project>]
[--description <description>]
[--immutable | --no-immutable]
[--enable | --disable]
[--property <key=value>]
[--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 <key=value>
Add a property to :ref:`\<name\> <project_create-name>`
@ -180,6 +190,7 @@ Set project properties
[--name <name>]
[--domain <domain>]
[--description <description>]
[--immutable | --no-immutable]
[--enable | --disable]
[--property <key=value>]
[--tag <tag> | --clear-tags | --remove-tags <tag>]
@ -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)

View File

@ -97,6 +97,7 @@ Create new role
openstack role create
[--or-show]
[--domain <domain>]
[--immutable | --no-immutable]
<name>
.. option:: --domain <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 <name>]
[--domain <domain>]
[--immutable | --no-immutable]
<role>
.. option:: --name <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
---------

View File

@ -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)'),
)

View File

@ -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)

View File

@ -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 <project> '
'(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)

View File

@ -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='<name>',
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):

View File

@ -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):

View File

@ -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):

View File

@ -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(

View File

@ -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.