Skip to content

Commit

Permalink
[6.16.z] Cherrypick of All Hosts Manage Errata (#1562)
Browse files Browse the repository at this point in the history
* Add coverage for manage errata via the nee all hosts UI

* Add support for CVE modal and PF4 version of LCE Selector (#1514)

* Support for Bulk Hostgroup actions in All Hosts (#1523)

* Slightly adjust a few locators

* Add waitfor when loading into the Job Invocation template

* Fix spacing issues

---------

Co-authored-by: Ladislav Vasina <[email protected]>
  • Loading branch information
sambible and LadislavVasina1 committed Sep 12, 2024
1 parent 0005b5f commit 044eacc
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 9 deletions.
127 changes: 127 additions & 0 deletions airgun/entities/all_hosts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from asyncio import wait_for

from widgetastic.exceptions import NoSuchElementException

from airgun.entities.base import BaseEntity
Expand All @@ -9,7 +11,9 @@
BuildManagementDialog,
BulkHostDeleteDialog,
HostDeleteDialog,
HostgroupDialog,
ManageCVEModal,
ManageErrataModal,
ManagePackagesModal,
)
from airgun.views.job_invocation import JobInvocationCreateView
Expand Down Expand Up @@ -83,6 +87,17 @@ def build_management(self, reboot=False, rebuild=False):
self.browser.wait_for_element(view.alert_message, exception=False)
return view.alert_message.read()

def change_hostgroup(self, name):
"""Change hostgroup of all hosts to chosen hostgroup"""
view = self.navigate_to(self, 'All')
self.browser.plugin.ensure_page_safe(timeout='5s')
view.wait_displayed()
view.select_all.fill(True)
view.bulk_actions.item_select('Change host group')
view = HostgroupDialog(self.browser)
view.hostgroup_dropdown.item_select(name)
view.save_button.click()

def manage_cve(self, lce=None, cv=None):
"""Bulk reassign Content View Environments through the All Hosts page
args:
Expand Down Expand Up @@ -268,6 +283,118 @@ def manage_packages(
view = JobInvocationCreateView(self.browser)
view.submit.click()

def manage_errata_helper(self, view, erratas_to_apply_by_id, individual_search_queries):
"""
Helper function to manage errata for selected hosts.
Based on the user input it finds errata by provied ids or by individual search queries.
args:
view (ManageErrataModal): ManageErrataModal view
erratas_to_apply_by_id (list): list of erratas to apply by their ids
individual_search_queries (list): list of search queries for each errata
"""

values_to_iterate = None
search_query_prefix = ''

if erratas_to_apply_by_id is not None:
values_to_iterate = erratas_to_apply_by_id
search_query_prefix = 'errata_id = '

elif individual_search_queries is not None:
values_to_iterate = individual_search_queries

for search_query in values_to_iterate:
clear_search = view.select_errata.clear_search
if clear_search.is_displayed:
clear_search.click()
view.select_errata.search_input.fill(f'{search_query_prefix}{search_query}')

self.browser.wait_for_element(view.select_errata.table[0][0].widget, exception=False)
view.select_errata.table[0][0].widget.fill(True)

def manage_errata(
self,
host_names=None,
select_all_hosts=False,
erratas_to_apply_by_id=None,
individual_search_queries=None,
manage_by_customized_rex=False,
):
"""
Navigate to Manage Errata for selected hosts and run the management action based on user input.
args:
host_names (str or list): str with one host or list of hosts to select
select_all_hosts (bool): select all hosts flag
erratas_to_apply_by_id (str or list): str with one errata or list of erratas to apply by their ids
individual_search_queries (list): list of string of search queries for each errata, use only if not using erratas_to_apply_by_id!
manage_by_customized_rex (bool): manage by customized rex flag
"""

# Check validity of user input
if select_all_hosts and host_names:
raise ValueError("Cannot select all and specify host names at the same time!")

# if both erratas_to_apply_by_id and individual_search_queries are specified, raise an error
if erratas_to_apply_by_id is not None and individual_search_queries is not None:
raise ValueError(
"Cannot specify both erratas_to_apply_by_id and individual_search_queries at the same time!"
)

# if erratas_to_apply_by_id is specified, make sure it is a list
if erratas_to_apply_by_id is not None and not isinstance(erratas_to_apply_by_id, list):
erratas_to_apply_by_id = [erratas_to_apply_by_id]

# Navigate to All Hosts
view = self.navigate_to(self, 'All')
self.browser.plugin.ensure_page_safe(timeout='5s')
view.wait_displayed()

# Select all hosts from the table
if select_all_hosts:
view.select_all.fill(True)
# Select user-specified hosts
else:
if not isinstance(host_names, list):
host_names = [host_names]
for host_name in host_names:
view.search(host_name)
view.table[0][0].widget.fill(True)

# Open Manage Erratas modal
view.bulk_actions_kebab.click()
# This is here beacuse there is nested flyout menu which needs to be hovered over first so we can use item_select in the next step
self.browser.move_to_element(view.bulk_actions_menu.item_element('Manage content'))
view.bulk_actions.item_select('Errata')

view = ManageErrataModal(self.browser)

# Select erratas to apply
self.manage_errata_helper(view, erratas_to_apply_by_id, individual_search_queries)

# In this particular case dropdown has slightly different structure that what is defined in widgetastic
view.review.manage_via_dropdown.ITEMS_LOCATOR = (
"//ul[contains(@class, 'pf-c-dropdown__menu')]/li"
)
view.review.manage_via_dropdown.ITEM_LOCATOR = (
"//*[contains(@class, 'pf-c-dropdown__menu-item') and normalize-space(.)={}]"
)
# Select how to manage errata
if not manage_by_customized_rex:
view.review.expander.click()
view.review.manage_via_dropdown.item_select('via remote execution')
view.review.finish_errata_management_btn.click()
else:
# In this case "Run job" page is opened on which user can specify job details
# Here we just select tu run with prefilled values
view.review.expander.click()
view.review.manage_via_dropdown.item_select('via customized remote execution')
view.review.finish_errata_management_btn.click()
view = JobInvocationCreateView(self.browser)
wait_for(lambda: view.submit.is_displayed, timeout=10)
view.submit.click()


@navigator.register(AllHostsEntity, 'All')
class ShowAllHostsScreen(NavigateStep):
Expand Down
140 changes: 131 additions & 9 deletions airgun/views/all_hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ class AllHostsSelect(Select):
)


class CVESelect(Select):
BUTTON_LOCATOR = ".//button[@aria-label='Options menu']"
ITEMS_LOCATOR = ".//ul[contains(@class, 'pf-c-select__menu')]/li"
ITEM_LOCATOR = (
"//*[contains(@class, 'pf-c-select__menu-item') and .//*[contains(normalize-space(.), {})]]"
)
SELECTED_ITEM_LOCATOR = ".//span[contains(@class, 'ins-c-conditional-filter')]"
TEXT_LOCATOR = ".//div[contains(@class, 'pf-c-select') and child::button]"
DEFAULT_LOCATOR = (
'.//div[contains(@class, "pf-c-select") and @data-ouia-component-id="select-content-view"]'
)


class AllHostsTableView(BaseLoggedInView, SearchableViewMixinPF4):
title = Text("//h1[normalize-space(.)='Hosts']")
select_all = Checkbox(
Expand Down Expand Up @@ -126,6 +139,24 @@ def is_displayed(self):
return self.browser.wait_for_element(self.title, exception=False) is not None


class HostgroupDialog(View):
"""Dialog for bulk changing Hosts' assigned hostgroup"""

ROOT = './/div[@id="bulk-reassign-hg-modal"]'

title = Text("//span[normalize-space(.)='Change host group']")
hostgroup_dropdown = AllHostsSelect(
locator='.//div[contains(@class, "pf-c-select") and @data-ouia-component-id="select-host-group"]'
)

save_button = Button(locator='//button[normalize-space(.)="Save"]')
cancel_button = Button(locator='//button[normalize-space(.)="Cancel"]')

@property
def is_displayed(self):
return self.browser.wait_for_element(self.title, exception=False) is not None


class AllHostsCheckboxTreeView(PF4CheckboxTreeView):
"""Small tweaks to work with All Hosts"""

Expand Down Expand Up @@ -259,7 +290,7 @@ class remove_packages(WizardStepView):

@View.nested
class review_hosts(WizardStepView):
locator_prefix = './/div[text()="Review hosts"]/descendant::'
locator_prefix = './/div[contains(.,"Review hosts")]/descendant::'

expander = Text('.//button[contains(.,"Review hosts")]')
content_text = Text('.//div[@class="pf-c-content"]')
Expand Down Expand Up @@ -294,19 +325,13 @@ class review(WizardStepView):
# it changes based on the selected action but generally it looks the same
# Returns 'All' or number of packages to manage
number_of_packages_to_manage = Text(
'''
.//span[contains(.,"Packages to")]/following-sibling::
span/span[@class="pf-c-badge pf-m-read"]
'''
'''.//span[contains(.,"Packages to")]/following-sibling::span/span[@class="pf-c-badge pf-m-read"]'''
)

edit_selected_packages = Button('.//button[@aria-label="Edit packages list"]')
# Returns number of hosts to manage
number_of_hosts_to_manage = Text(
'''
.//span[contains(.,"Hosts")]/following-sibling::
span/span[@class="pf-c-badge pf-m-read"]
'''
'''.//span[contains(.,"Hosts")]/following-sibling::span/span[@class="pf-c-badge pf-m-read"]'''
)
edit_selected_hosts = Button('.//button[@aria-label="Edit host selection"]')
manage_via_dropdown = Dropdown(
Expand All @@ -319,3 +344,100 @@ class review(WizardStepView):
@property
def is_displayed(self):
return self.browser.wait_for_element(self.title, exception=False) is not None


class ManageErrataModal(Modal):
"""
This class represents the Manage Errata modal that is used to apply errata on hosts.
It contains several nested views that represent the steps of the wizard.
"""

OUIA_ID = 'bulk-errata-wizard-modal'

title = './/h2[@data-ouia-component-type="PF4/Title"]'
close_btn = Button(locator='//button[@class="pf-c-button pf-m-plain pf-c-wizard__close"]')
cancel_btn = Button(locator='//button[normalize-space(.)="Cancel"]')
back_btn = Button(locator='//button[normalize-space(.)="Back"]')
next_btn = Button(locator='//button[normalize-space(.)="Next"]')

@View.nested
class select_errata(WizardStepView):
wizard_step_name = "Select errata"
locator_prefix = f'.//div[contains(., "{wizard_step_name}")]/descendant::'
expander = Text(f'.//button[text()="{wizard_step_name}"]')
content_text = Text('.//div[@class="pf-c-content"]')

select_all = Checkbox(locator=f'{locator_prefix}div[@id="selection-checkbox"]')
search_input = SearchInput(locator=f'{locator_prefix}input[@aria-label="Search input"]')
clear_search = Button(locator=f'{locator_prefix}button[@aria-label="Reset search"]')
search = Button(locator=f'{locator_prefix}button[@aria-label="Search"]')

table = PatternflyTable(
component_id='table',
column_widgets={
0: Checkbox(locator='.//input[@type="checkbox"]'),
'Erratum': Text('.//td[2]'),
'Title': Text('.//td[3]'),
'Type': Text('.//td[4]'),
'Severity': Text('.//td[5]'),
'Affected hosts': Text('.//td[6]'),
},
)
pagination = Pagination()

@View.nested
class review_hosts(WizardStepView):
wizard_step_name = "Review hosts"
locator_prefix = f'.//div[contains(., "{wizard_step_name}")]/descendant::'

expander = Text(f'.//button[text()="{wizard_step_name}"]')
content_text = Text('.//div[@class="pf-c-content"]')
error_message = OUIAAlert('no-hosts-alert')

select_all = Checkbox(locator=f'{locator_prefix}div[@id="selection-checkbox"]')
search_input = SearchInput(locator=f'{locator_prefix}input[@aria-label="Search input"]')
search = Button(locator=f'{locator_prefix}button[@aria-label="Search"]')

table = PatternflyTable(
component_id='table',
column_widgets={
0: Checkbox(locator='.//input[@type="checkbox"]'),
'Name': Text('.//td[2]'),
'OS': Text('.//td[3]'),
},
)
pagination = Pagination()

@View.nested
class review(WizardStepView):
wizard_step_name = "Review"
expander = Text(f'.//button[text()="{wizard_step_name}"]')

tree_expander_errata = Button(
'.//button[@class="pf-c-tree-view__node" and contains(.,"Errata to")]'
)
expanded_errata_list = ItemsList(
locator='//ul[@class="pf-c-tree-view__list" and @role="tree"][1]'
)

number_of_errata_to_manage = Text(
'''.//span[contains(.,"Errata to")]/following-sibling::span/span[@class="pf-c-badge pf-m-read"]'''
)

edit_selected_errata = Button('.//button[@aria-label="Edit errata list"]')
# Returns number of hosts to manage
number_of_hosts_to_manage = Text(
'''.//span[contains(.,"Hosts")]/following-sibling::span/span[@class="pf-c-badge pf-m-read"]'''
)
edit_selected_hosts = Button('.//button[@aria-label="Edit host selection"]')
manage_via_dropdown = Dropdown(
locator='//div[@data-ouia-component-id="bulk-errata-wizard-dropdown"]'
)

finish_errata_management_btn = Button(
locator='//*[@data-ouia-component-type="PF4/Button" and normalize-space(.)="Apply"]'
)

@property
def is_displayed(self):
return self.browser.wait_for_element(self.title, exception=False) is not None

0 comments on commit 044eacc

Please sign in to comment.