import attr
from navmazing import NavigateToSibling, NavigateToAttribute
from widgetastic.utils import VersionPick, Version
from widgetastic.widget import Checkbox, View, Text
from widgetastic_patternfly import (
BootstrapSelect, Button, Input, Tab, CheckableBootstrapTreeview as CbTree,
BootstrapSwitch, CandidateNotFound, Dropdown)
from cfme.base.credential import Credential
from cfme.base.ui import ConfigurationView
from cfme.common import Taggable
from cfme.exceptions import CFMEException, RBACOperationBlocked
from cfme.modeling.base import BaseCollection, BaseEntity
from cfme.utils.appliance.implementations.ui import navigator, CFMENavigateStep, navigate_to
from cfme.utils.blockers import BZ
from cfme.utils.log import logger
from cfme.utils.pretty import Pretty
from cfme.utils.update import Updateable
from cfme.utils.wait import wait_for
from widgetastic_manageiq import (
UpDownSelect, PaginationPane, SummaryFormItem, Table, BaseListEntity, SummaryForm)
EVM_DEFAULT_GROUPS = [
'evmgroup-super_administrator',
'evmgroup-administrator',
'evmgroup-approver',
'evmgroup-auditor',
'evmgroup-desktop',
'evmgroup-operator',
'evmgroup-security',
'evmgroup-support',
'evmgroup-user',
'evmgroup-vm_user'
]
####################################################################################################
# RBAC USER METHODS
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[docs]class UsersEntities(View):
table = Table("//div[@id='records_div' or @id='main_div']//table")
[docs]class AllUserView(ConfigurationView):
""" All Users View."""
toolbar = View.nested(AccessControlToolbar)
entities = View.nested(UsersEntities)
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Access Control EVM Users'
)
[docs]class AddUserView(UserForm):
""" Add User View."""
add_button = Button('Add')
@property
def is_displayed(self):
return self.accordions.accesscontrol.is_opened and self.title.text == "Adding a new User"
[docs]class DetailsUserEntities(View):
smart_management = SummaryForm('Smart Management')
[docs]class DetailsUserView(ConfigurationView):
""" User Details view."""
toolbar = View.nested(AccessControlToolbar)
entities = View.nested(DetailsUserEntities)
@property
def is_displayed(self):
return (
self.title.text == 'EVM User "{}"'.format(self.context['object'].name) and
self.accordions.accesscontrol.is_opened
)
[docs]class EditUserView(UserForm):
""" User Edit View."""
save_button = Button('Save')
reset_button = Button('Reset')
change_stored_password = Text('#change_stored_password')
cancel_password_change = Text('#cancel_password_change')
@property
def is_displayed(self):
return (
self.title.text == 'Editing User "{}"'.format(self.context['object'].name) and
self.accordions.accesscontrol.is_opened
)
@attr.s
[docs]class User(Updateable, Pretty, BaseEntity, Taggable):
""" Class represents an user in CFME UI
Args:
name: Name of the user
credential: User's credentials
email: User's email
groups: Add User to multiple groups in Versions >= 5.9.
cost_center: User's cost center
value_assign: user's value to assign
appliance: appliance under test
"""
pretty_attrs = ['name', 'group']
name = attr.ib(default=None)
credential = attr.ib(default=None)
email = attr.ib(default=None)
groups = attr.ib(default=None)
cost_center = attr.ib(default=None)
value_assign = attr.ib(default=None)
_restore_user = attr.ib(default=None, init=False)
def __enter__(self):
if self._restore_user != self.appliance.user:
logger.info('Switching to new user: %s', self.credential.principal)
self._restore_user = self.appliance.user
self.appliance.server.logout()
self.appliance.user = self
def __exit__(self, *args, **kwargs):
if self._restore_user != self.appliance.user:
logger.info('Restoring to old user: %s', self._restore_user.credential.principal)
self.appliance.server.logout()
self.appliance.user = self._restore_user
self._restore_user = None
[docs] def update(self, updates):
""" Update user method
Args:
updates: user data that should be changed
Note: In case updates is the same as original user data, update will be canceled,
as 'Save' button will not be active
"""
view = navigate_to(self, 'Edit')
self.change_stored_password()
new_updates = {}
if 'credential' in updates:
new_updates.update({
'userid_txt': updates.get('credential').principal,
'password_txt': updates.get('credential').secret,
'password_verify_txt': updates.get('credential').verify_secret
})
new_updates.update({
'name_txt': updates.get('name'),
'email_txt': updates.get('email'),
'user_group_select': getattr(
updates.get('group'),
'description', None)
})
changed = view.fill({
'name_txt': new_updates.get('name_txt'),
'userid_txt': new_updates.get('userid_txt'),
'password_txt': new_updates.get('password_txt'),
'password_verify_txt': new_updates.get('password_verify_txt'),
'email_txt': new_updates.get('email_txt'),
'user_group_select': new_updates.get('user_group_select')
})
if changed:
view.save_button.click()
flash_message = 'User "{}" was saved'.format(updates.get('name', self.name))
else:
view.cancel_button.click()
flash_message = 'Edit of User was cancelled by the user'
view = self.create_view(DetailsUserView, override=updates)
view.flash.assert_message(flash_message)
assert view.is_displayed
[docs] def copy(self):
""" Creates copy of existing user
return: User object of copied user
"""
view = navigate_to(self, 'Details')
view.toolbar.configuration.item_select('Copy this User to a new User')
view = self.create_view(AddUserView)
new_user = self.parent.instantiate(
name="{}copy".format(self.name),
credential=Credential(principal='redhat', secret='redhat')
)
view.fill({
'name_txt': new_user.name,
'userid_txt': new_user.credential.principal,
'password_txt': new_user.credential.secret,
'password_verify_txt': new_user.credential.verify_secret
})
view.add_button.click()
view = self.create_view(AllUserView)
view.flash.assert_success_message('User "{}" was saved'.format(new_user.name))
assert view.is_displayed
return new_user
[docs] def delete(self, cancel=True):
"""Delete existing user
Args:
cancel: Default value 'True', user will be deleted
'False' - deletion of user will be canceled
Throws:
RBACOperationBlocked: If operation is blocked due to current user
not having appropriate permissions OR delete is not allowed
for currently selected user
"""
flash_success_msg = 'EVM User "{}": Delete successful'.format(self.name)
flash_blocked_msg = "Default EVM User \"{}\" cannot be deleted".format(self.name)
delete_user_txt = 'Delete this User'
view = navigate_to(self, 'Details')
if not view.toolbar.configuration.item_enabled(delete_user_txt):
raise RBACOperationBlocked("Configuration action '{}' is not enabled".format(
delete_user_txt))
view.toolbar.configuration.item_select(delete_user_txt, handle_alert=cancel)
try:
view.flash.assert_message(flash_blocked_msg)
raise RBACOperationBlocked(flash_blocked_msg)
except AssertionError:
pass
view.flash.assert_message(flash_success_msg)
if cancel:
view = self.create_view(AllUserView)
view.flash.assert_success_message(flash_success_msg)
else:
view = self.create_view(DetailsUserView)
assert view.is_displayed
# TODO update elements, after 1469035 fix
[docs] def change_stored_password(self, changes=None, cancel=False):
""" Changes user password
Args:
changes: dict with fields to be changes,
if None, passwords fields only be anabled
cancel: True, if you want to disable password change
"""
view = navigate_to(self, 'Edit')
self.browser.execute_script(
self.browser.get_attribute(
'onClick', self.browser.element(view.change_stored_password)))
if changes:
view.fill(changes)
if cancel:
self.browser.execute_script(
self.browser.get_attribute(
'onClick', self.browser.element(view.cancel_password_change)))
@property
def exists(self):
try:
navigate_to(self, 'Details')
return True
except CandidateNotFound:
return False
@property
def description(self):
return self.credential.principal
@property
def my_settings(self):
from cfme.configure.settings import MySettings
my_settings = MySettings(appliance=self.appliance)
return my_settings
@attr.s
[docs]class UserCollection(BaseCollection):
ENTITY = User
[docs] def simple_user(self, userid, password, fullname=None):
"""If a fullname is not supplied, userid is used for credential principal and user name"""
creds = Credential(principal=userid, secret=password)
return self.instantiate(name=fullname or userid, credential=creds)
[docs] def create(self, name=None, credential=None, email=None, groups=None, cost_center=None,
value_assign=None, fullname=None, cancel=False):
""" User creation method
Args:
name: Name of the user
credential: User's credentials
email: User's email
groups: Add User to multiple groups in Versions >= 5.9.
cost_center: User's cost center
value_assign: user's value to assign
fullname: users full name
cancel: True - if you want to cancel user creation,
by defaul user will be created
Throws:
RBACOperationBlocked: If operation is blocked due to current user
not having appropriate permissions OR update is not allowed
for currently selected role
"""
if self.appliance.version < "5.8":
user_blocked_msg = "Userid has already been taken"
else:
user_blocked_msg = ("Userid is not unique within region {}".format(
self.appliance.server.zone.region.number))
if type(groups) is not list:
groups = [groups]
if self.appliance.version < "5.9" and len(groups) > 1:
raise CFMEException(
"Assigning a user to multiple groups is only supported in CFME versions > 5.8")
user = self.instantiate(
name=name, credential=credential, email=email, groups=groups, cost_center=cost_center,
value_assign=value_assign
)
# view.fill supports iteration over a list when selecting pulldown list items but
# will throw an exception when the item doesn't appear in the list so filter out
# null items since they "shouldn't" exist
user_group_names = [getattr(ug, 'description', None) for ug in user.groups if ug]
view = navigate_to(self, 'Add')
view.fill({
'name_txt': user.name,
'userid_txt': user.credential.principal,
'password_txt': user.credential.secret,
'password_verify_txt': user.credential.verify_secret,
'email_txt': user.email,
'user_group_select': user_group_names
})
if cancel:
view.cancel_button.click()
flash_message = 'Add of new User was cancelled by the user'
else:
view.add_button.click()
flash_message = 'User "{}" was saved'.format(user.name)
try:
view.flash.assert_message(user_blocked_msg)
raise RBACOperationBlocked(user_blocked_msg)
except AssertionError:
pass
view = self.create_view(AllUserView)
view.flash.assert_success_message(flash_message)
assert view.is_displayed
# To ensure tree update
view.browser.refresh()
return user
@navigator.register(UserCollection, 'All')
[docs]class UserAll(CFMENavigateStep):
VIEW = AllUserView
prerequisite = NavigateToAttribute('appliance.server', 'Configuration')
[docs] def step(self):
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Users')
@navigator.register(UserCollection, 'Add')
[docs]class UserAdd(CFMENavigateStep):
VIEW = AddUserView
[docs] def prerequisite(self):
navigate_to(self.obj.appliance.server, 'Configuration')
return navigate_to(self.obj, 'All')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select("Add a new User")
@navigator.register(User, 'Details')
[docs]class UserDetails(CFMENavigateStep):
VIEW = DetailsUserView
prerequisite = NavigateToAttribute('parent', 'All')
[docs] def step(self):
try:
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Users', self.obj.name)
except CandidateNotFound:
self.obj.appliance.browser.widgetastic.refresh()
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Users', self.obj.name)
@navigator.register(User, 'Edit')
[docs]class UserEdit(CFMENavigateStep):
VIEW = EditUserView
prerequisite = NavigateToSibling('Details')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select('Edit this User')
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# RBAC USER METHODS
####################################################################################################
####################################################################################################
# RBAC GROUP METHODS
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[docs]class AddGroupView(GroupForm):
""" Add Group View in CFME UI """
add_button = Button("Add")
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == "Adding a new Group"
)
[docs]class DetailsGroupEntities(View):
smart_management = SummaryForm('Smart Management')
[docs]class DetailsGroupView(ConfigurationView):
""" Details Group View in CFME UI """
toolbar = View.nested(AccessControlToolbar)
entities = View.nested(DetailsGroupEntities)
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'EVM Group "{}"'.format(self.context['object'].description)
)
[docs]class EditGroupView(GroupForm):
""" Edit Group View in CFME UI """
save_button = Button("Save")
reset_button = Button('Reset')
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Editing Group "{}"'.format(self.context['object'].description)
)
[docs]class AllGroupView(ConfigurationView):
""" All Groups View in CFME UI """
toolbar = View.nested(AccessControlToolbar)
table = Table("//div[@id='main_div']//table")
paginator = PaginationPane()
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Access Control EVM Groups'
)
[docs]class EditGroupSequenceView(ConfigurationView):
""" Edit Groups Sequence View in CFME UI """
group_order_selector = UpDownSelect(
'#seq_fields',
'//button[@title="Move selected fields up"]/i',
'//button[@title="Move selected fields down"]/i')
save_button = Button('Save')
reset_button = Button('Reset')
cancel_button = Button('Cancel')
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == "Editing Sequence of User Groups"
)
@attr.s
[docs]class Group(BaseEntity, Taggable):
"""Represents a group in CFME UI
Properties:
description: group description
role: group role
tenant: group tenant
user_to_lookup: ldap user to lookup
ldap_credentials: ldap user credentials
tag: tag for group restriction
host_cluster: host/cluster for group restriction
vm_template: vm/template for group restriction
appliance: appliance under test
"""
pretty_attrs = ['description', 'role']
description = attr.ib(default=None)
role = attr.ib(default=None)
tenant = attr.ib(default="My Company")
ldap_credentials = attr.ib(default=None)
user_to_lookup = attr.ib(default=None)
tag = attr.ib(default=None)
host_cluster = attr.ib(default=None)
vm_template = attr.ib(default=None)
def _retrieve_ldap_user_groups(self):
""" Retrive ldap user groups
return: AddGroupView
"""
view = navigate_to(self.parent, 'Add')
view.fill({'lookup_ldap_groups_chk': True,
'user_to_look_up': self.user_to_lookup,
'username': self.ldap_credentials.principal,
'password': self.ldap_credentials.secret})
view.retrieve_button.click()
return view
def _retrieve_ext_auth_user_groups(self):
""" Retrive external authorization user groups
return: AddGroupView
"""
view = navigate_to(self.parent, 'Add')
view.fill({'lookup_ldap_groups_chk': True,
'user_to_look_up': self.user_to_lookup})
view.retrieve_button.click()
return view
def _fill_ldap_group_lookup(self, view):
""" Fills ldap info for group lookup
Args: view: view for group creation(AddGroupView)
"""
view.fill({'ldap_groups_for_user': self.description,
'description_txt': self.description,
'role_select': self.role,
'group_tenant': self.tenant})
view.add_button.click()
view = self.create_view(AllGroupView)
view.flash.assert_success_message('Group "{}" was saved'.format(self.description))
assert view.is_displayed
[docs] def add_group_from_ldap_lookup(self):
"""Adds a group from ldap lookup"""
view = self._retrieve_ldap_user_groups()
self._fill_ldap_group_lookup(view)
[docs] def add_group_from_ext_auth_lookup(self):
"""Adds a group from external authorization lookup"""
view = self._retrieve_ext_auth_user_groups()
self._fill_ldap_group_lookup(view)
[docs] def update(self, updates):
""" Update group method
Args:
updates: group data that should be changed
Note: In case updates is the same as original group data, update will be canceled,
as 'Save' button will not be active
"""
edit_group_txt = 'Edit this Group'
view = navigate_to(self, 'Details')
if not view.toolbar.configuration.item_enabled(edit_group_txt):
raise RBACOperationBlocked("Configuration action '{}' is not enabled".format(
edit_group_txt))
view = navigate_to(self, 'Edit')
changed = view.fill({
'description_txt': updates.get('description'),
'role_select': updates.get('role'),
'group_tenant': updates.get('tenant')
})
changed_tag = self._set_group_restriction(view.my_company_tags, updates.get('tag'))
changed_host_cluster = self._set_group_restriction(
view.hosts_and_clusters, updates.get('host_cluster'))
changed_vm_template = self._set_group_restriction(
view.vms_and_templates, updates.get('vm_template'))
if changed or changed_tag or changed_host_cluster or changed_vm_template:
view.save_button.click()
flash_message = 'Group "{}" was saved'.format(
updates.get('description', self.description))
else:
view.cancel_button.click()
flash_message = 'Edit of Group was cancelled by the user'
view = self.create_view(DetailsGroupView, override=updates)
view.flash.assert_message(flash_message)
assert view.is_displayed
[docs] def delete(self, cancel=True):
"""
Delete existing group
Args:
cancel: Default value 'True', group will be deleted
'False' - deletion of group will be canceled
Throws:
RBACOperationBlocked: If operation is blocked due to current user
not having appropriate permissions OR delete is not allowed
for currently selected group
"""
flash_success_msg = 'EVM Group "{}": Delete successful'.format(self.description)
flash_blocked_msg_list = [
('EVM Group "{}": '
'Error during delete: A read only group cannot be deleted.'.format(self.description)),
('EVM Group "{}": Error during delete: '
'The group has users assigned that do not '
'belong to any other group'.format(self.description))]
delete_group_txt = 'Delete this Group'
view = navigate_to(self, 'Details')
if not view.toolbar.configuration.item_enabled(delete_group_txt):
raise RBACOperationBlocked("Configuration action '{}' is not enabled".format(
delete_group_txt))
view.toolbar.configuration.item_select(delete_group_txt, handle_alert=cancel)
for flash_blocked_msg in flash_blocked_msg_list:
try:
view.flash.assert_message(flash_blocked_msg)
raise RBACOperationBlocked(flash_blocked_msg)
except AssertionError:
pass
view.flash.assert_no_error()
view.flash.assert_message(flash_success_msg)
if cancel:
view = self.create_view(AllGroupView)
view.flash.assert_success_message(flash_success_msg)
else:
view = self.create_view(DetailsGroupView)
assert view.is_displayed, (
"Access Control Group {} Detail View is not displayed".format(self.description))
[docs] def set_group_order(self, updated_order):
""" Sets group order for group lookup
Args:
updated_order: group order list
"""
if self.appliance.version < "5.9.2":
name_column = "Name"
else:
name_column = "Description"
find_row_kwargs = {name_column: self.description}
view = navigate_to(self.parent, 'All')
row = view.paginator.find_row_on_pages(view.table, **find_row_kwargs)
original_sequence = row.sequence.text
original_order = self.group_order[:len(updated_order)]
view = self.create_view(EditGroupSequenceView)
assert view.is_displayed
# We pick only the same amount of items for comparing
if updated_order == original_order:
return # Ignore that, would cause error on Save click
view.group_order_selector.fill(updated_order)
view.save_button.click()
view = self.create_view(AllGroupView)
assert view.is_displayed
row = view.paginator.find_row_on_pages(view.table, **find_row_kwargs)
changed_sequence = row.sequence.text
assert original_sequence != changed_sequence, "{} Group Edit Sequence Failed".format(
self.description)
def _set_group_restriction(self, tab_view, item, update=True):
""" Sets tag/host/template restriction for the group
Args:
tab_view: tab view
item: path to check box that should be selected/deselected
update: If True - checkbox state will be updated
Returns: True - if update is successful
"""
updated_result = False
if item is not None:
if update:
path, action_type = item
if isinstance(path, list):
node = (tab_view.tree.CheckNode(path) if action_type else
tab_view.tree.UncheckNode(path))
tab_view.tree.fill(node)
updated_result = True
return updated_result
@property
def group_order(self):
view = navigate_to(self, 'EditGroupSequence')
return view.group_order_selector.items
@property
def exists(self):
try:
navigate_to(self, 'Details')
return True
except CandidateNotFound:
return False
@attr.s
[docs]class GroupCollection(BaseCollection):
""" Collection object for the :py:class: `cfme.configure.access_control.Group`. """
ENTITY = Group
[docs] def create(self, description=None, role=None, tenant="My Company", ldap_credentials=None,
user_to_lookup=None, tag=None, host_cluster=None, vm_template=None, cancel=False):
""" Create group method
Args:
description: group description
role: group role
tenant: group tenant
user_to_lookup: ldap user to lookup
ldap_credentials: ldap user credentials
tag: tag for group restriction
host_cluster: host/cluster for group restriction
vm_template: vm/template for group restriction
appliance: appliance under test
cancel: True - if you want to cancel group creation,
by default group will be created
Throws:
RBACOperationBlocked: If operation is blocked due to current user
not having appropriate permissions OR delete is not allowed
for currently selected user
"""
if self.appliance.version < "5.8":
flash_blocked_msg = ("Description has already been taken")
else:
flash_blocked_msg = "Description is not unique within region {}".format(
self.appliance.server.zone.region.number)
view = navigate_to(self, 'Add')
group = self.instantiate(
description=description, role=role, tenant=tenant, ldap_credentials=ldap_credentials,
user_to_lookup=user_to_lookup, tag=tag, host_cluster=host_cluster,
vm_template=vm_template)
view.fill({
'description_txt': group.description,
'role_select': group.role,
'group_tenant': group.tenant
})
group._set_group_restriction(view.my_company_tags, group.tag)
group._set_group_restriction(view.hosts_and_clusters, group.host_cluster)
group._set_group_restriction(view.vms_and_templates, group.vm_template)
if cancel:
view.cancel_button.click()
flash_message = 'Add of new Group was cancelled by the user'
else:
view.add_button.click()
flash_message = 'Group "{}" was saved'.format(group.description)
view = self.create_view(AllGroupView)
try:
view.flash.assert_message(flash_blocked_msg)
raise RBACOperationBlocked(flash_blocked_msg)
except AssertionError:
pass
view.flash.assert_success_message(flash_message)
assert view.is_displayed
# To ensure that the group list is updated
view.browser.refresh()
return group
@navigator.register(GroupCollection, 'All')
[docs]class GroupAll(CFMENavigateStep):
VIEW = AllGroupView
prerequisite = NavigateToAttribute('appliance.server', 'Configuration')
[docs] def step(self):
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Groups')
[docs] def resetter(self, *args, **kwargs):
self.obj.appliance.browser.widgetastic.browser.refresh()
@navigator.register(GroupCollection, 'Add')
[docs]class GroupAdd(CFMENavigateStep):
VIEW = AddGroupView
prerequisite = NavigateToSibling('All')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select("Add a new Group")
@navigator.register(Group, 'EditGroupSequence')
[docs]class EditGroupSequence(CFMENavigateStep):
VIEW = EditGroupSequenceView
prerequisite = NavigateToAttribute('parent', 'All')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select(
'Edit Sequence of User Groups for LDAP Look Up')
@navigator.register(Group, 'Details')
[docs]class GroupDetails(CFMENavigateStep):
VIEW = DetailsGroupView
prerequisite = NavigateToAttribute('parent', 'All')
[docs] def step(self):
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Groups', self.obj.description)
@navigator.register(Group, 'Edit')
[docs]class GroupEdit(CFMENavigateStep):
VIEW = EditGroupView
prerequisite = NavigateToSibling('Details')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select('Edit this Group')
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# END RBAC GROUP METHODS
####################################################################################################
####################################################################################################
# RBAC ROLE METHODS
####################################################################################################
[docs]class AddRoleView(RoleForm):
""" Add Role View """
add_button = Button('Add')
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Adding a new Role'
)
[docs]class EditRoleView(RoleForm):
""" Edit Role View """
save_button = Button('Save')
reset_button = Button('Reset')
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Editing Role "{}"'.format(self.context['object'].name)
)
[docs]class DetailsRoleView(RoleForm):
""" Details Role View """
toolbar = View.nested(AccessControlToolbar)
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Role "{}"'.format(self.context['object'].name)
)
[docs]class AllRolesView(ConfigurationView):
""" All Roles View """
toolbar = View.nested(AccessControlToolbar)
table = Table("//div[@id='main_div']//table")
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Access Control Roles'
)
@attr.s
[docs]class Role(Updateable, Pretty, BaseEntity):
""" Represents a role in CFME UI
Args:
name: role name
vm_restriction: restriction used for role
product_features: product feature to select
appliance: appliance unter test
"""
pretty_attrs = ['name', 'product_features']
name = attr.ib(default=None)
vm_restriction = attr.ib(default=None)
product_features = attr.ib(default=None)
def __attrs_post_init__(self):
if not self.product_features:
self.product_features = []
[docs] def update(self, updates):
""" Update role method
Args:
updates: role data that should be changed
Note: In case updates is the same as original role data, update will be canceled,
as 'Save' button will not be active
"""
flash_blocked_msg = "Read Only Role \"{}\" can not be edited".format(self.name)
edit_role_txt = 'Edit this Role'
view = navigate_to(self, 'Details')
if not view.toolbar.configuration.item_enabled(edit_role_txt):
raise RBACOperationBlocked("Configuration action '{}' is not enabled".format(
edit_role_txt))
view = navigate_to(self, 'Edit')
try:
view.flash.assert_message(flash_blocked_msg)
raise RBACOperationBlocked(flash_blocked_msg)
except AssertionError:
pass
changed = view.fill({
'name_txt': updates.get('name'),
'vm_restriction_select': updates.get('vm_restriction')
})
feature_changed = self.set_role_product_features(view, updates.get('product_features'))
if changed or feature_changed:
view.save_button.click()
flash_message = 'Role "{}" was saved'.format(updates.get('name', self.name))
else:
view.cancel_button.click()
flash_message = 'Edit of Role was cancelled by the user'
view = self.create_view(DetailsRoleView, override=updates)
view.flash.assert_message(flash_message)
# Typically this would be a safe check but BZ 1561698 will sometimes cause the accordion
# to fail to update the role name w/o a manual refresh causing is_displayed to fail
# Instead of inserting a blind refresh, just disable this until the bug is resolved since
# it's a good check for accordion UI failures
# See BZ https://bugzilla.redhat.com/show_bug.cgi?id=1561698
if not BZ(1561698, forced_streams=['5.9']).blocks:
assert view.is_displayed
[docs] def delete(self, cancel=True):
""" Delete existing role
Args:
cancel: Default value 'True', role will be deleted
'False' - deletion of role will be canceled
Throws:
RBACOperationBlocked: If operation is blocked due to current user
not having appropriate permissions OR delete is not allowed
for currently selected role
"""
flash_blocked_msg = ("Role \"{}\": Error during delete: Cannot delete record "
"because of dependent entitlements".format(self.name))
flash_success_msg = 'Role "{}": Delete successful'.format(self.name)
delete_role_txt = 'Delete this Role'
view = navigate_to(self, 'Details')
if not view.toolbar.configuration.item_enabled(delete_role_txt):
raise RBACOperationBlocked("Configuration action '{}' is not enabled".format(
delete_role_txt))
view.toolbar.configuration.item_select(delete_role_txt, handle_alert=cancel)
try:
view.flash.assert_message(flash_blocked_msg)
raise RBACOperationBlocked(flash_blocked_msg)
except AssertionError:
pass
view.flash.assert_message(flash_success_msg)
if cancel:
view = self.create_view(AllRolesView)
view.flash.assert_success_message(flash_success_msg)
else:
view = self.create_view(DetailsRoleView)
assert view.is_displayed
[docs] def copy(self, name=None):
""" Creates copy of existing role
Returns: Role object of copied role
"""
if name is None:
name = "{}_copy".format(self.name)
view = navigate_to(self, 'Details')
view.toolbar.configuration.item_select('Copy this Role to a new Role')
view = self.create_view(AddRoleView)
new_role = self.parent.instantiate(name=name)
view.fill({'name_txt': new_role.name})
view.add_button.click()
view = self.create_view(AllRolesView)
view.flash.assert_success_message('Role "{}" was saved'.format(new_role.name))
assert view.is_displayed
return new_role
[docs] def set_role_product_features(self, view, product_features):
""" Sets product features for role restriction
Args:
view: AddRoleView or EditRoleView
product_features: list of product features with options to select
"""
if product_features is not None and isinstance(product_features, (list, tuple, set)):
changes = [
view.fill({
'features_tree': CbTree.CheckNode(path) if option else CbTree.UncheckNode(path)
})
for path, option in product_features
]
return True in changes
else:
return False
@attr.s
[docs]class RoleCollection(BaseCollection):
ENTITY = Role
[docs] def create(self, name=None, vm_restriction=None, product_features=None, cancel=False):
""" Create role method
Args:
cancel: True - if you want to cancel role creation,
by default, role will be created
Raises:
RBACOperationBlocked: If operation is blocked due to current user
not having appropriate permissions OR update is not allowed
for currently selected role
"""
flash_blocked_msg = "Name has already been taken"
role = self.instantiate(
name=name, vm_restriction=vm_restriction, product_features=product_features
)
view = navigate_to(self, 'Add')
view.fill({'name_txt': role.name,
'vm_restriction_select': role.vm_restriction})
role.set_role_product_features(view, role.product_features)
if cancel:
view.cancel_button.click()
flash_message = 'Add of new Role was cancelled by the user'
else:
view.add_button.click()
flash_message = 'Role "{}" was saved'.format(role.name)
view = self.create_view(AllRolesView)
try:
view.flash.assert_message(flash_blocked_msg)
raise RBACOperationBlocked(flash_blocked_msg)
except AssertionError:
pass
view.flash.assert_success_message(flash_message)
assert view.is_displayed
return role
@navigator.register(RoleCollection, 'All')
[docs]class RoleAll(CFMENavigateStep):
VIEW = AllRolesView
prerequisite = NavigateToAttribute('appliance.server', 'Configuration')
[docs] def step(self):
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Roles')
@navigator.register(RoleCollection, 'Add')
[docs]class RoleAdd(CFMENavigateStep):
VIEW = AddRoleView
prerequisite = NavigateToSibling('All')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select("Add a new Role")
@navigator.register(Role, 'Details')
[docs]class RoleDetails(CFMENavigateStep):
VIEW = DetailsRoleView
prerequisite = NavigateToAttribute('parent', 'All')
[docs] def step(self):
self.prerequisite_view.browser.refresh() # workaround for 5.9 issue of role now shown
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Roles', self.obj.name)
@navigator.register(Role, 'Edit')
[docs]class RoleEdit(CFMENavigateStep):
VIEW = EditRoleView
prerequisite = NavigateToSibling('Details')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select('Edit this Role')
####################################################################################################
# RBAC TENANT METHODS
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[docs]class ListEntity(BaseListEntity):
pass
[docs]class TenantQuotaView(ConfigurationView):
""" Tenant Quota View """
form = View.nested(TenantQuotaForm)
save_button = Button('Save')
reset_button = Button('Reset')
cancel_button = Button('Cancel')
@property
def is_displayed(self):
return (
self.form.template_cb.is_displayed and
self.title.text == 'Manage quotas for {} "{}"'.format(self.context['object'].obj_type,
self.context['object'].name))
[docs]class AllTenantView(ConfigurationView):
""" All Tenants View """
toolbar = View.nested(AccessControlToolbar)
table = Table(VersionPick(
{Version.lowest(): '//*[@id="records_div"]/table',
'5.9': '//*[@id="miq-gtl-view"]/miq-data-table/div/table'}))
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'Access Control Tenants'
)
[docs]class AddTenantView(ConfigurationView):
""" Add Tenant View """
form = View.nested(TenantForm)
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.form.description.is_displayed and
self.title.text in ('Adding a new Project', 'Adding a new Tenant')
)
[docs]class DetailsTenantEntities(View):
smart_management = SummaryForm('Smart Management')
[docs]class DetailsTenantView(ConfigurationView):
""" Details Tenant View """
entities = View.nested(DetailsTenantEntities)
# Todo move to entities
toolbar = View.nested(AccessControlToolbar)
name = Text('Name')
description = Text('Description')
parent = Text('Parent')
table = Table('//*[self::fieldset or @id="fieldset"]/table')
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == '{} "{}"'.format(self.context['object'].obj_type,
self.context['object'].name)
)
[docs]class ParentDetailsTenantView(DetailsTenantView):
""" Parent Tenant Details View """
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == '{} "{}"'.format(self.context['object'].parent_tenant.obj_type,
self.context['object'].parent_tenant.name)
)
[docs]class EditTenantView(View):
""" Edit Tenant View """
form = View.nested(TenantForm)
save_button = Button('Save')
reset_button = Button('Reset')
@property
def is_displayed(self):
return (
self.form.accordions.accesscontrol.is_opened and
self.form.description.is_displayed and
self.form.title.text == 'Editing {} "{}"'.format(self.context['object'].obj_type,
self.context['object'].name)
)
@attr.s
[docs]class Tenant(Updateable, BaseEntity, Taggable):
""" Class representing CFME tenants in the UI.
* Kudos to mfalesni *
The behaviour is shared with Project, which is the same except it cannot create more nested
tenants/projects.
Args:
name: Name of the tenant
description: Description of the tenant
parent_tenant: Parent tenant, can be None, can be passed as string or object
"""
obj_type = 'Tenant'
name = attr.ib()
description = attr.ib(default="")
parent_tenant = attr.ib(default=None)
_default = attr.ib(default=False)
[docs] def update(self, updates):
""" Update tenant/project method
Args:
updates: tenant/project data that should be changed
Note: In case updates is the same as original tenant/project data, update will be canceled,
as 'Save' button will not be active
"""
view = navigate_to(self, 'Edit', wait_for_view=True)
changed = view.form.fill(updates)
if changed:
view.save_button.click()
if self.appliance.version < '5.9':
flash_message = 'Project "{}" was saved'.format(updates.get('name', self.name))
else:
flash_message = '{} "{}" has been successfully saved.'.format(
self.obj_type, updates.get('name', self.name))
else:
view.cancel_button.click()
if self.appliance.version < '5.9':
flash_message = 'Edit of Project "{}" was cancelled by the user'.format(
updates.get('name', self.name))
else:
flash_message = 'Edit of {} "{}" was canceled by the user.'.format(
self.obj_type, updates.get('name', self.name))
view = self.create_view(DetailsTenantView, override=updates)
view.flash.assert_message(flash_message)
[docs] def delete(self, cancel=True):
""" Delete existing role
Args:
cancel: Default value 'True', role will be deleted
'False' - deletion of role will be canceled
"""
view = navigate_to(self, 'Details')
view.toolbar.configuration.item_select(
'Delete this item', handle_alert=cancel)
if cancel:
view = self.create_view(ParentDetailsTenantView)
view.flash.assert_success_message(
'Tenant "{}": Delete successful'.format(self.description))
else:
view = self.create_view(DetailsRoleView)
assert view.is_displayed
[docs] def set_quota(self, **kwargs):
""" Sets tenant quotas """
view = navigate_to(self, 'ManageQuotas', wait_for_view=True)
changed = view.form.fill({'cpu_cb': kwargs.get('cpu_cb'),
'cpu_txt': kwargs.get('cpu'),
'memory_cb': kwargs.get('memory_cb'),
'memory_txt': kwargs.get('memory'),
'storage_cb': kwargs.get('storage_cb'),
'storage_txt': kwargs.get('storage'),
'vm_cb': kwargs.get('vm_cb'),
'vm_txt': kwargs.get('vm'),
'template_cb': kwargs.get('template_cb'),
'template_txt': kwargs.get('template')})
if changed:
view.save_button.click()
expected_msg = 'Quotas for {} "{}" were saved'.format(self.obj_type, self.name)
else:
view.cancel_button.click()
expected_msg = 'Manage quotas for {} "{}" was cancelled by the user'\
.format(self.obj_type, self.name)
view = self.create_view(DetailsTenantView)
view.flash.assert_success_message(expected_msg)
assert view.is_displayed
@property
def quota(self):
view = navigate_to(self, 'Details')
quotas = {
'cpu': 'Allocated Virtual CPUs',
'memory': 'Allocated Memory in GB',
'storage': 'Allocated Storage in GB',
'num_vms': 'Allocated Number of Virtual Machines',
'templates': 'Allocated Number of Templates'
}
for field in quotas:
item = view.table.row(name=quotas[field])
quotas[field] = {
'total': item.total_quota.text,
'in_use': item.in_use.text,
'allocated': item.allocated.text,
'available': item.available.text
}
return quotas
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
else:
return self.tree_path == other.tree_path
@property
def exists(self):
try:
navigate_to(self, 'Details')
return True
except CandidateNotFound:
return False
@property
def tree_path(self):
if self._default:
return [self.name]
else:
return self.parent_tenant.tree_path + [self.name]
@property
def parent_path(self):
return self.parent_tenant.tree_path
@attr.s
[docs]class TenantCollection(BaseCollection):
"""Collection class for Tenant"""
ENTITY = Tenant
[docs] def get_root_tenant(self):
return self.instantiate(str(self.appliance.rest_api.collections.tenants[0].name),
default=True)
[docs] def create(self, name, description, parent):
if self.appliance.version > '5.9':
tenant_success_flash_msg = 'Tenant "{}" has been successfully added.'
else:
tenant_success_flash_msg = 'Tenant "{}" was saved'
tenant = self.instantiate(name, description, parent)
view = navigate_to(tenant.parent_tenant, 'Details')
view.toolbar.configuration.item_select('Add child Tenant to this Tenant')
view = self.create_view(AddTenantView)
wait_for(lambda: view.is_displayed, timeout=5)
changed = view.form.fill({'name': name,
'description': description})
if changed:
view.form.add_button.click()
else:
view.form.cancel_button.click()
view = self.create_view(ParentDetailsTenantView)
view.flash.assert_success_message(tenant_success_flash_msg.format(name))
return tenant
[docs] def delete(self, *tenants):
view = navigate_to(self, 'All')
for tenant in tenants:
try:
view.table.row(name=tenant.name).check()
except Exception:
logger.exception('Failed to check element "%s"', tenant.name)
else:
view.toolbar.configuration.item_select('Delete selected items', handle_alert=True)
@navigator.register(TenantCollection, 'All')
[docs]class TenantAll(CFMENavigateStep):
VIEW = AllTenantView
prerequisite = NavigateToAttribute('appliance.server', 'Configuration')
[docs] def step(self):
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Tenants')
@navigator.register(Tenant, 'Details')
[docs]class TenantDetails(CFMENavigateStep):
VIEW = DetailsTenantView
prerequisite = NavigateToAttribute('parent', 'All')
[docs] def step(self):
self.prerequisite_view.accordions.accesscontrol.tree.click_path(
self.obj.appliance.server_region_string(), 'Tenants', *self.obj.tree_path)
@navigator.register(Tenant, 'Edit')
[docs]class TenantEdit(CFMENavigateStep):
VIEW = EditTenantView
prerequisite = NavigateToSibling('Details')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select('Edit this item')
@navigator.register(Tenant, 'ManageQuotas')
[docs]class TenantManageQuotas(CFMENavigateStep):
VIEW = TenantQuotaView
prerequisite = NavigateToSibling('Details')
[docs] def step(self):
self.prerequisite_view.toolbar.configuration.item_select('Manage Quotas')
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# END TENANT METHODS
####################################################################################################
####################################################################################################
# RBAC PROJECT METHODS
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[docs]class Project(Tenant):
""" Class representing CFME projects in the UI.
Project cannot create more child tenants/projects.
Args:
name: Name of the project
description: Description of the project
parent_tenant: Parent project, can be None, can be passed as string or object
"""
obj_type = 'Project'
[docs]class ProjectCollection(TenantCollection):
"""Collection class for Projects under Tenants"""
ENTITY = Project
[docs] def get_root_tenant(self):
# returning Tenant directly because 'My Company' needs to be treated like Tenant object,
# to be able to make child tenant/project under it
return self.appliance.collections.tenants.instantiate(
name=str(self.appliance.rest_api.collections.tenants[0].name), default=True)
[docs] def create(self, name, description, parent):
if self.appliance.version > '5.9':
project_success_flash_msg = 'Project "{}" has been successfully added.'
else:
project_success_flash_msg = 'Project "{}" was saved'
project = self.instantiate(name, description, parent)
view = navigate_to(project.parent_tenant, 'Details')
view.toolbar.configuration.item_select('Add Project to this Tenant')
view = self.create_view(AddTenantView)
wait_for(lambda: view.is_displayed, timeout=5)
changed = view.form.fill({'name': name,
'description': description})
if changed:
view.form.add_button.click()
else:
view.form.cancel_button.click()
view = self.create_view(ParentDetailsTenantView)
view.flash.assert_success_message(project_success_flash_msg.format(name))
return project
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# END PROJECT METHODS
####################################################################################################