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,
BootstrapSwitch, CandidateNotFound, Dropdown)
from cfme.base.credential import Credential
from cfme.base.ui import ConfigurationView
from cfme.exceptions import RBACOperationBlocked
from cfme.modeling.base import BaseCollection, BaseEntity
from cfme.utils.appliance.implementations.ui import navigator, CFMENavigateStep, navigate_to
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)
####################################################################################################
# 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 DetailsUserView(ConfigurationView):
""" User Details view."""
toolbar = View.nested(AccessControlToolbar)
@property
def is_displayed(self):
return (
self.title.text == 'EVM User "{}"'.format(self.context['object'].name) and
self.accordions.accesscontrol.is_opened and
# tree.currently_selected returns a list of strings with each item being the text of
# each level of the accordion. Last element should be the User's name
self.accordions.accesscontrol.tree.currently_selected[-1] == self.context['object'].name
)
[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):
""" Class represents an user in CFME UI
Args:
name: Name of the user
credential: User's credentials
email: User's email
group: User's group for assigment
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)
group = 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
[docs] def remove_tag(self, tag, value):
""" Remove tag from existing user
Args:
tag: Tag category
value: Tag name
"""
view = navigate_to(self, 'EditTags')
row = view.tag_table.row(category=tag, assigned_value=value)
row[0].click()
view.save_button.click()
view = self.create_view(DetailsUserView)
view.flash.assert_success_message('Tag edits were successfully saved')
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
@attr.s
[docs]class UserCollection(BaseCollection):
ENTITY = User
[docs] def simple_user(self, userid, password):
creds = Credential(principal=userid, secret=password)
return self.instantiate(name=userid, credential=creds)
[docs] def create(self, name=None, credential=None, email=None, group=None, cost_center=None,
value_assign=None, cancel=False):
""" User creation method
Args:
name: Name of the user
credential: User's credentials
email: User's email
group: User's group for assigment
cost_center: User's cost center
value_assign: user's value to assign
appliance: appliance under test
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))
user = self.instantiate(
name=name, credential=credential, email=email, group=group, cost_center=cost_center,
value_assign=value_assign
)
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': getattr(user.group, 'description', None)
})
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
prerequisite = NavigateToSibling('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):
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')
@navigator.register(User, 'EditTags')
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 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 DetailsGroupView(ConfigurationView):
""" Details Group View in CFME UI """
toolbar = View.nested(AccessControlToolbar)
@property
def is_displayed(self):
return (
self.accordions.accesscontrol.is_opened and
self.title.text == 'EVM Group "{}"'.format(self.context['object'].description) and
# tree.currently_selected returns a list of strings with each item being the text of
# each level of the accordion. Last element should be the Group's name
(self.accordions.accesscontrol.tree.currently_selected[-1] ==
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):
"""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'), True)
changed_host_cluster = self._set_group_restriction(
view.hosts_and_clusters, updates.get('host_cluster'), True)
changed_vm_template = self._set_group_restriction(
view.vms_and_templates, updates.get('vm_template'), True)
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 remove_tag(self, tag, value):
""" Delete tag for existing group
Args:
tag: Tag category
value: Tag name
"""
view = navigate_to(self, 'EditTags')
row = view.tag_table.row(category=tag, assigned_value=value)
row[0].click()
view.save_button.click()
view = self.create_view(DetailsGroupView)
view.flash.assert_success_message('Tag edits were successfully saved')
assert view.is_displayed
[docs] def set_group_order(self, updated_order):
""" Sets group order for group lookup
Args:
updated_order: group order list
"""
name_column = "Name"
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=False):
""" 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:
if tab_view.tree.node_checked(*item):
tab_view.tree.uncheck_node(*item)
else:
tab_view.tree.check_node(*item)
updated_result = True
else:
tab_view.tree.fill(item)
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')
@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')
@navigator.register(Group, 'EditTags')
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 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) and
# tree.currently_selected returns a list of strings with each item being the text of
# each level of the accordion. Last element should be the Role's name
self.accordions.accesscontrol.tree.currently_selected[-1] == 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)
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
"""
feature_update = False
if product_features is not None and isinstance(product_features, (list, tuple, set)):
for path, option in product_features:
if option:
view.product_features_tree.check_node(*path)
else:
view.product_features_tree.uncheck_node(*path)
feature_update = True
return feature_update
@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 DetailsTenantView(ConfigurationView):
""" Details Tenant View """
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):
""" 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)
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')})
view.save_button.click()
view = self.create_view(DetailsTenantView)
view.flash.assert_success_message('Quotas for {} "{}" were saved'.format(
self.obj_type, self.name))
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
####################################################################################################