Source code for cfme.configure.access_control

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)


[docs]class AccessControlToolbar(View): """ Toolbar on the Access Control page """ configuration = Dropdown('Configuration') policy = Dropdown('Policy')
#################################################################################################### # RBAC USER METHODS # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[docs]class UserForm(ConfigurationView): """ User Form View.""" name_txt = Input(name='name') userid_txt = Input(name='userid') password_txt = Input(id='password') password_verify_txt = Input(id='verify') email_txt = Input(name='email') user_group_select = BootstrapSelect(id='chosen_group') cancel_button = Button('Cancel')
[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 )
[docs]class EditTagsUserView(ConfigurationView): """ Tags edit for Users view.""" tag_table = Table("//div[@id='assignments_div']//table") select_tag = BootstrapSelect(id='tag_cat') select_value = BootstrapSelect(id='tag_add') save_button = Button('Save') cancel_button = Button('Cancel') reset_button = Button('Reset') @property def is_displayed(self): return ( self.accordions.accesscontrol.is_opened and self.title.text == 'Editing My Company Tags for "EVM Users"' )
@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 edit_tags(self, tag, value): """ Edits tag for existing user Args: tag: Tag category value: Tag name """ view = navigate_to(self, 'EditTags') view.fill({'select_tag': tag, 'select_value': value}) view.save_button.click() view = self.create_view(DetailsUserView) view.flash.assert_success_message('Tag edits were successfully saved') 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
[docs] def get_tags(self): tags = [] view = navigate_to(self, 'EditTags') for row in view.tag_table: tags.append((row.category.text, row.assigned_value.text)) view.cancel_button.click() return tags
# 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')
[docs]class UserTagsEdit(CFMENavigateStep): VIEW = EditTagsUserView prerequisite = NavigateToSibling('Details')
[docs] def step(self): self.prerequisite_view.toolbar.policy.item_select( "Edit 'My Company' Tags for this User")
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # RBAC USER METHODS #################################################################################################### #################################################################################################### # RBAC GROUP METHODS # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[docs]class GroupForm(ConfigurationView): """ Group Form in CFME UI.""" ldap_groups_for_user = BootstrapSelect(id='ldap_groups_user') description_txt = Input(name='description') lookup_ldap_groups_chk = Checkbox(name='lookup') role_select = BootstrapSelect(id='group_role') group_tenant = BootstrapSelect(id='group_tenant') user_to_look_up = Input(name='user') username = Input(name='user_id') password = Input(name='password') tag = SummaryFormItem('Smart Management', 'My Company Tags') cancel_button = Button('Cancel') retrieve_button = Button('Retrieve') @View.nested class my_company_tags(Tab): # noqa """ Represents 'My company tags' tab in Group Form """ TAB_NAME = "My Company Tags" tree_locator = 'tags_treebox' tree = CheckableBootstrapTreeview(tree_locator) @View.nested class hosts_and_clusters(Tab): # noqa """ Represents 'Hosts and Clusters' tab in Group Form """ TAB_NAME = "Hosts & Clusters" tree = CheckableBootstrapTreeview('hac_treebox') @View.nested class vms_and_templates(Tab): # noqa """ Represents 'VM's and Templates' tab in Group Form """ TAB_NAME = "VMs & Templates" tree = CheckableBootstrapTreeview('vat_treebox')
[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" )
[docs]class GroupEditTagsView(ConfigurationView): """ Edit Groups Tags View in CFME UI """ tag_table = Table("//div[@id='assignments_div']//table") select_tag = BootstrapSelect(id='tag_cat') select_value = BootstrapSelect(id='tag_add') save_button = Button('Save') cancel_button = Button('Cancel') reset_button = Button('Reset') @property def is_displayed(self): return ( self.accordions.accesscontrol.is_opened and self.title.text == 'Editing My Company Tags for "EVM 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 edit_tags(self, tag, value): """ Edits tag for existing group Args: tag: Tag category value: Tag name """ view = navigate_to(self, 'EditTags') view.fill({'select_tag': tag, 'select_value': value}) 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 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 get_tags(self): tags = [] view = navigate_to(self, 'EditTags') for row in view.tag_table: tags.append((row.category.text, row.assigned_value.text)) view.cancel_button.click() return tags
[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')
[docs]class GroupTagsEdit(CFMENavigateStep): VIEW = GroupEditTagsView prerequisite = NavigateToSibling('Details')
[docs] def step(self): self.prerequisite_view.toolbar.policy.item_select( "Edit 'My Company' Tags for this Group")
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # END RBAC GROUP METHODS #################################################################################################### #################################################################################################### # RBAC ROLE METHODS ####################################################################################################
[docs]class RoleForm(ConfigurationView): """ Role Form for CFME UI """ name_txt = Input(name='name') vm_restriction_select = BootstrapSelect(id='vm_restriction') product_features_tree = CheckableBootstrapTreeview("features_treebox") cancel_button = Button('Cancel')
[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 TenantForm(ConfigurationView): """ Tenant Form """ name = Input(name='name') description = Input(name='description') add_button = Button('Add') cancel_button = Button('Cancel')
[docs]class ListEntity(BaseListEntity): pass
[docs]class TenantQuotaForm(View): cpu_cb = BootstrapSwitch(id='cpu_allocated') memory_cb = BootstrapSwitch(id='mem_allocated') storage_cb = BootstrapSwitch(id='storage_allocated') vm_cb = BootstrapSwitch(id='vms_allocated') template_cb = BootstrapSwitch(id='templates_allocated') cpu_txt = Input(id='id_cpu_allocated') memory_txt = Input(id='id_mem_allocated') storage_txt = Input(id='id_storage_allocated') vm_txt = Input(id='id_vms_allocated') template_txt = Input(id='id_templates_allocated')
[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 ####################################################################################################