diff --git a/airgun/entities/host_new.py b/airgun/entities/host_new.py index 09066cb48..996f912b8 100644 --- a/airgun/entities/host_new.py +++ b/airgun/entities/host_new.py @@ -1,16 +1,26 @@ +import time + from navmazing import NavigateToSibling from airgun.entities.host import HostEntity from airgun.navigation import NavigateStep from airgun.navigation import navigator from airgun.views.host_new import AllAssignedRolesView +from airgun.views.host_new import EditSystemPurposeView from airgun.views.host_new import EnableTracerView from airgun.views.host_new import InstallPackagesView +from airgun.views.host_new import ManageHostCollectionModal from airgun.views.host_new import ModuleStreamDialog from airgun.views.host_new import NewHostDetailsView +from airgun.views.host_new import ParameterDeleteDialog +from airgun.views.host_new import RemediationView from airgun.views.job_invocation import JobInvocationCreateView +global available_param_types +available_param_types = ['string', 'boolean', 'integer', 'real', 'array', 'hash', 'yaml', 'json'] + + class NewHostEntity(HostEntity): def create(self, values): """Create new host entity""" @@ -32,6 +42,187 @@ def get_details(self, entity_name, widget_names=None): self.browser.plugin.ensure_page_safe() return view.read(widget_names=widget_names) + def edit_system_purpose( + self, entity_name, role=None, sla=None, usage=None, release_ver=None, add_ons=None + ): + """ + Function that edits the system purpose of a host + + Args: + entity_name: Name of the host + role: Role to be assigned + sla: SLA to be assigned + usage: Usage to be assigned + release_ver: Release version to be assigned + add_ons: Add-ons to be assigned + + Raises: + ValueError: If no parameters are passed. + """ + + if not any([role, sla, usage, release_ver, add_ons]): + raise ValueError( + 'At least one of the role, sla, usage, release_ver, add_ons must be provided!' + ) + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.overview.system_purpose.edit_system_purpose.click() + view = EditSystemPurposeView(self.browser) + self.browser.plugin.ensure_page_safe() + view.wait_displayed() + if role: + view.role.fill(role) + if sla: + view.sla.fill(sla) + if usage: + view.usage.fill(usage) + if release_ver: + view.release_version.fill(release_ver) + if add_ons: + view.add_ons.fill(add_ons) + view.save.click() + + def add_host_to_host_collection( + self, entity_name, host_collection_name=None, add_to_all_collections=False + ): + """ + Function that adds host to host collection + + Args: + entity_name: Name of the host + host_collection_name: Name or list of the host collection we want to add the host to + add_to_all_collections: If True, host will be added to all host collections + + Raises: + ValueError: If specific hostColName is set to be removed and add all is set to True + or if no parameters are passed. + ValueError: If host is already assigned to selected host collection. + ValueError: If there is empty list provided for host_collection_name. + ValueError: If there are no host collections left for addition. + ValueError: Given host collection name is not found in host collections. + """ + + if (host_collection_name and add_to_all_collections) or ( + (host_collection_name is None) and (add_to_all_collections is False) + ): + raise ValueError( + 'Either host_collection_name or add_to_all_collections must be provided!' + ) + + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + # Handle the case where there are no host collections assigned + if view.overview.host_collections.no_host_collections.is_displayed: + view.overview.host_collections.add_to_host_collection.click() + else: + if ( + host_collection_name + in view.overview.host_collections.assigned_host_collections.read() + ): + raise ValueError(f'{host_collection_name} already assigned to host!') + view.overview.host_collections.kebab_menu.item_select('Add host to collections') + + view = ManageHostCollectionModal(self.browser) + self.browser.plugin.ensure_page_safe() + view.wait_displayed() + + if view.create_host_collection.is_displayed: + raise ValueError('No host collections found or left for addition!') + + if not add_to_all_collections: + if type(host_collection_name) is list: + if not host_collection_name: + raise ValueError('host_collection_name list is empty!') + for host_col in host_collection_name: + view.searchbar.fill("name = " + host_col, enter_timeout=2) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + if view.host_collection_table.row_count == 0: + raise ValueError(f'{host_col} not found in host collections!') + time.sleep(3) + # Select the host collection via checkbox in the table + view.host_collection_table[0][0].widget.click() + else: + view.searchbar.fill("name = " + host_collection_name, enter_timeout=2) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + if view.host_collection_table.row_count == 0: + raise ValueError(f'{host_collection_name} not found in host collections!') + time.sleep(3) + # Select the host collection via checkbox in the table + view.host_collection_table[0][0].widget.click() + else: + view.select_all.click() + view.add.click() + + def remove_host_from_host_collection( + self, entity_name, host_collection_name=None, remove_from_all_collections=False + ): + """ + Function that removes host from host collection + + Args: + entity_name: Name of the host + host_collection_name: Name or list of the host collection we want to remove host from + remove_from_all_collections: If True, host will be removed from all host collections + + Raises: + ValueError: If specific hostColName is set to be removed & remove all is set to True + or if no parameters are passed. + ValueError: If host is not assigned to any host collection. + ValueError: If there is empty list provided for host_collection_name. + ValueError: If given host col name we want to remove is not assigned to host. + """ + + if ((host_collection_name is not None) and remove_from_all_collections) or ( + (host_collection_name is None) and (remove_from_all_collections is False) + ): + raise ValueError( + 'Either host_collection_name or add_to_all_collections must be provided!' + ) + + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + # Handle the case where there are no host collections assigned + view.overview.click() + if view.overview.host_collections.no_host_collections.is_displayed: + raise ValueError('No host collections assigned to host, thus nothing to remove!') + + view.overview.host_collections.kebab_menu.item_select('Remove host from collections') + view = ManageHostCollectionModal(self.browser) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + + if not remove_from_all_collections: + if type(host_collection_name) is list: + if not host_collection_name: + raise ValueError('host_collection_name list is empty!') + for host_col in host_collection_name: + view.searchbar.fill("name = " + host_col, enter_timeout=2) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + if not view.host_collection_table.is_displayed: + raise ValueError(f"{host_col} not assigned to host, thus can't remove it!") + time.sleep(3) + # Select the host collection via checkbox in the table + view.host_collection_table[0][0].widget.click() + else: + view.searchbar.fill("name = " + host_collection_name, enter_timeout=2) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + if not view.host_collection_table.is_displayed: + raise ValueError( + f"{host_collection_name} not assigned to host, thus can't remove it!" + ) + time.sleep(3) + # Select the host collection via checkbox in the table + view.host_collection_table[0][0].widget.click() + else: + view.select_all.click() + view.remove.click() + def schedule_job(self, entity_name, values): """Schedule a remote execution on selected host""" view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) @@ -272,15 +463,154 @@ def get_parameters(self, entity_name): return view.parameters.read() def add_new_parameter(self, entity_name, parameter_name, parameter_type, parameter_value): + """ + Function that adds new parameter to the host + + Args: + entity_name: host on which we want to add parameter + parameter_name: Name of the parameter to be added + parameter_type: Type of the parameter to be added + [string, boolean, integer, real, array, hash, yaml, json] + parameter_value: Value of the parameter to be added + + Raises: + ValueError: If parameter type is not valid + ValueError: If parameter with the same name already exists + """ + + # Sanitize input + parameter_type = parameter_type.lower() + if parameter_type not in available_param_types: + raise ValueError( + f'Parameter type {parameter_type} is not valid!' + f'Available types are: {available_param_types}' + ) + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) view.wait_displayed() self.browser.plugin.ensure_page_safe() + view.parameters.click() + view.parameters.table_header.sort_by('name', 'ascending') + view.parameters.searchbar.fill(parameter_name) + view.wait_displayed() + # Check if parameter with given name does not exist. If it does, raise ValueError. + if view.parameters.parameters_table.row_count != 0: + if view.parameters.parameters_table[0][0].text == parameter_name: + raise ValueError(f'Parameter with name {parameter_name} already exists!') + view.parameters.add_parameter.click() - view.parameters.new_parameter_name.fill(parameter_name) - view.parameters.new_parameter_type.fill(parameter_type) - view.parameters.new_parameter_value.fill(parameter_value) + view.parameters.parameter_name_input.fill(parameter_name) + view.parameters.parameter_type_input.fill(parameter_type) + view.parameters.parameter_value_input.fill(parameter_value) view.parameters.confirm_addition.click() + def edit_parameter( + self, + entity_name, + parameter_to_change, + new_parameter_name=None, + new_parameter_type=None, + new_parameter_value=None, + ): + """ + Function that can edit parameter name, type or value separetely or all of them at once. + + Args: + entity_name: Name of the entity to be edited + parameter_to_change: Name of the parameter to be edited + new_parameter_name: New name of the parameter + new_parameter_type: New type of the parameter + [string, boolean, integer, real, array, hash, yaml, json] + new_parameter_value: New value of the parameter + + Raises: + ValueError: No new parameter name, type or value provided + ValueError: If parameter type is not valid + ValueError: If parameter_to_change is not found on host + ValueError: If parameter with new name already exists + """ + + if not any([new_parameter_name, new_parameter_type, new_parameter_value]): + raise ValueError( + 'At least one of the new_parameter_name, new_parameter_type, new_parameter_value ' + 'must be provided!' + ) + # Sanitize input + new_parameter_type = new_parameter_type.lower() + if new_parameter_type not in available_param_types: + raise ValueError( + f'Parameter type {new_parameter_type} is not valid!' + f'Available types are: {available_param_types}' + ) + + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + view.parameters.click() + view.parameters.table_header.sort_by('name', 'ascending') + view.parameters.searchbar.fill(parameter_to_change) + view.wait_displayed() + if (view.parameters.parameters_table.row_count == 0) or ( + view.parameters.parameters_table[0][0].text != parameter_to_change + ): + raise ValueError( + f'Parameter {parameter_to_change} not found on {entity_name}, ' + 'thus cannot be edited.' + ) + + view.parameters.searchbar.fill(new_parameter_name) + view.wait_displayed() + if view.parameters.parameters_table.row_count != 0: + if view.parameters.parameters_table[0][0].text == new_parameter_name: + raise ValueError( + f'Cannot rename {parameter_to_change} to {new_parameter_name}. ' + 'This parameter already exists.' + ) + + view.parameters.searchbar.fill(parameter_to_change) + view.wait_displayed() + # Click edit button in the row + view.parameters.parameters_table[0][4].widget.click() + view.wait_displayed() + if new_parameter_name: + view.parameters.parameter_name_input.fill(new_parameter_name) + if new_parameter_type: + view.parameters.parameter_type_input.fill(new_parameter_type) + if new_parameter_value: + view.parameters.parameter_value_input.fill(new_parameter_value) + view.parameters.confirm_addition.click() + + def delete_parameter(self, entity_name, parameter_name): + """ + Function that deletes parameter from the host + + Args: + entity_name: Name of the host to be edited + parameter_name: Name of the parameter to be deleted + + Raises: + ValueError: If given parameter is not found on host + """ + + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + view.parameters.click() + view.parameters.searchbar.fill(parameter_name) + view.wait_displayed() + # Fail if there are no parameters or if first parameter is not the one we are looking for + if (view.parameters.parameters_table.row_count == 0) or ( + view.parameters.parameters_table[0][0].text != parameter_name + ): + raise ValueError( + f'Parameter {parameter_name} not found on {entity_name}, thus cannot be deleted.' + ) + + view.parameters.parameters_table[0][5].widget.item_select('Delete') + delete_modal = ParameterDeleteDialog(self.browser) + if delete_modal.is_displayed: + delete_modal.confirm_delete.click() + def get_traces(self, entity_name): view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) view.wait_displayed() @@ -314,6 +644,80 @@ def get_insights(self, entity_name): self.browser.plugin.ensure_page_safe() return view.insights.read() + def remediate_with_insights( + self, entity_name, recommendation_to_remediate=None, remediate_all=False + ): + """ + Function that can remediate all or one recommendation with insights. + + Args: + entity_name: Name of the host on which recommendations are to be remediated + recommendation_to_remediate: Name of the recommendation to be remediated + remediate_all: If True, all recommendations will be remediated + + Raises: + ValueError: If recommendation_to_remediate is None and remediate_all is False + or if both recommendation_to_remediate and remediate_all are provided. + IndexError: If given recommendation is not found + + """ + + if ((recommendation_to_remediate is not None) and remediate_all) or ( + (recommendation_to_remediate is None) and (remediate_all is False) + ): + raise ValueError( + 'Either recommendation_to_remediate or remediate_all must be provided!' + ) + + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + if remediate_all: + view.insights.select_all_one_page.click() + view.insights.select_all_pages.click() + else: + view.insights.recommendations_table.sort_by('Recommendation', 'ascending') + + if type(recommendation_to_remediate) is list: + if not recommendation_to_remediate: + raise ValueError('List of recommendations cannot be empty!') + for recommendation in recommendation_to_remediate: + view.insights.click() + # Excape double quotes in the recommendation + recommendation = recommendation.replace('"', '\\"') + recommendation = f'title = "{recommendation}"' + view.insights.search_bar.fill(recommendation, enter_timeout=3) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + time.sleep(3) + try: + # Click the checkbox of the first recommendation + view.insights.recommendations_table[0][0].widget.click() + except IndexError: + raise IndexError( + f'Recommendation {recommendation} not found on {entity_name}, ' + 'thus cannot be remediated.' + ) + else: + # Excape double quotes in the recommendation + recommendation_to_remediate = recommendation_to_remediate.replace('"', '\\"') + recommendation_to_remediate = f'title = "{recommendation_to_remediate}"' + view.insights.search_bar.fill(recommendation_to_remediate, enter_timeout=3) + view.wait_displayed() + self.browser.plugin.ensure_page_safe() + time.sleep(3) + try: + # Click the checkbox of the first recommendation + view.insights.recommendations_table[0][0].widget.click() + except IndexError: + raise IndexError( + f'Recommendation {recommendation_to_remediate} not found ' + f'on {entity_name}, thus cannot be remediated.' + ) + view.insights.remediate.click() + view = RemediationView(self.browser) + view.remediate.click() + @navigator.register(NewHostEntity, 'NewDetails') class ShowNewHostDetails(NavigateStep): diff --git a/airgun/views/host_new.py b/airgun/views/host_new.py index a31e3a5e9..2940cb3b0 100644 --- a/airgun/views/host_new.py +++ b/airgun/views/host_new.py @@ -15,8 +15,9 @@ from widgetastic_patternfly4.ouia import BreadCrumb from widgetastic_patternfly4.ouia import Button as OUIAButton from widgetastic_patternfly4.ouia import ExpandableTable -from widgetastic_patternfly4.ouia import Modal +from widgetastic_patternfly4.ouia import FormSelect as OUIAFormSelect from widgetastic_patternfly4.ouia import PatternflyTable +from widgetastic_patternfly4.ouia import Select as OUIASelect from airgun.views.common import BaseLoggedInView from airgun.widgets import Accordion @@ -28,21 +29,21 @@ class SearchInput(TextInput): - def fill(self, value): + def fill(self, value, enter_timeout=1): changed = super().fill(value) if changed: # workaround for BZ #2140636 - time.sleep(1) + time.sleep(enter_timeout) self.browser.send_keys(Keys.ENTER, self) return changed -class RemediationView(Modal): +class RemediationView(View): """Remediation window view""" - OUIA_ID = 'OUIA-Generated-Modal-large-1' - remediate = Button('Remediate') - cancel = Button('Cancel') + ROOT = './/div[@id="remediation-modal"]' + remediate = Button("Remediate") + cancel = Button("Cancel") table = PatternflyTable( component_id='OUIA-Generated-Table-4', column_widgets={ @@ -97,6 +98,17 @@ def read(self): return items +class HostColectionsList(Widget): + """Host collections list in host details page""" + + ROOT = './/div[@class="pf-c-card__body host-collection-card-body"]' + ITEMS = './/span[contains(@class, "pf-c-expandable-section__toggle-text")]' + + def read(self): + """Return a list of assigned host collections""" + return [self.browser.text(item) for item in self.browser.elements(self.ITEMS)] + + class NewHostDetailsView(BaseLoggedInView): breadcrumb = BreadCrumb('breadcrumbs-list') @@ -176,6 +188,15 @@ class total_risks(Card): important = Text('.//*[@id="legend-labels-2"]/*') critical = Text('.//*[@id="legend-labels-3"]/*') + @View.nested + class host_collections(Card): + ROOT = './/article[.//div[text()="Host collections"]]' + kebab_menu = Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]') + no_host_collections = Text('.//h2') + add_to_host_collection = OUIAButton('add-to-a-host-collection-button') + + assigned_host_collections = HostColectionsList() + @View.nested class recent_jobs(Card): ROOT = './/article[.//div[text()="Recent jobs"]]' @@ -440,14 +461,15 @@ class parameters(Tab): searchbar = SearchInput( locator='//input[contains(@class, "pf-c-search-input__text-input")]' ) - new_parameter_name = TextInput(locator='.//td//input[contains(@aria-label, "name")]') - new_parameter_type = Select( + parameter_name_input = TextInput(locator='.//td//input[contains(@aria-label, "name")]') + parameter_type_input = Select( locator='.//td[2]//div[@data-ouia-component-type="PF4/Select"]' ) - new_parameter_value = TextInput(locator='.//td[3]//textarea') + parameter_value_input = TextInput(locator='.//td[3]//textarea') cancel_addition = Button(locator='.//td[5]//button[1]') confirm_addition = Button(locator='.//td[5]//button[2]') + table_header = PatternflyTable(locator='.//table[@data-ouia-component-type="PF4/Table"]') parameters_table = Table( locator='.//table[@aria-label="Parameters table"]', column_widgets={ @@ -461,7 +483,7 @@ class parameters(Tab): '[contains(@data-ouia-component-id, "OUIA-Generated-Button-plain-")]' ) ), - 5: Button(locator='.//td[contains(@class, "parameters-actions")]//button'), + 5: Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]'), }, ) pagination = Pagination() @@ -667,7 +689,11 @@ class insights(Tab): remediate = Button(locator='.//button[text()="Remediate"]') insights_dropdown = Dropdown(locator='.//div[contains(@class, "insights-dropdown")]') - select_all = Checkbox(locator='.//input[@name="check-all"]') + select_all_one_page = Checkbox(locator='.//input[@name="check-all"]') + select_all_pages = Button( + locator='.//button[text()="Select recommendations from all pages"]' + ) + recommendations_table = PatternflyTable( component_id='OUIA-Generated-Table-2', column_widgets={ @@ -679,7 +705,6 @@ class insights(Tab): }, ) pagination = Pagination() - remediation_window = View.nested(RemediationView) class InstallPackagesView(View): @@ -724,6 +749,56 @@ class EnableTracerView(View): confirm = Button(locator='//*[@data-ouia-component-id="enable-tracer-modal"]/footer/button[1]') +class ParameterDeleteDialog(View): + """Confirmation dialog for deleting host parameter""" + + ROOT = './/div[@data-ouia-component-id="app-confirm-modal"]' + + confirm_delete = OUIAButton('btn-modal-confirm') + cancel_delete = OUIAButton('btn-modal-cancel') + + +class ManageHostCollectionModal(View): + """Host Collection Modal""" + + ROOT = './/div[@data-ouia-component-id="host-collections-modal"]' + + create_host_collection = OUIAButton('empty-state-primary-action-button') + select_all = Checkbox(locator='.//input[contains(@aria-label, "Select all")]') + searchbar = SearchInput(locator='.//input[contains(@class, "pf-m-search")]') + + host_collection_table = Table( + locator='.//table[contains(@class, "pf-c-table")]', + column_widgets={ + 0: Checkbox(locator='.//input[@type="checkbox"]'), + 'host_collecntion': Text('.//a'), + 'capacity': Text('.//td[3]'), + 'description': Text('.//td[4]'), + }, + ) + + pagination = Pagination() + + add = OUIAButton('add-button') + remove = OUIAButton('add-button') + cancel = OUIAButton('cancel-button') + + +class EditSystemPurposeView(View): + """Edit System Purpose Modal""" + + ROOT = './/div[@data-ouia-component-id="syspurpose-edit-modal"]' + + role = OUIAFormSelect('role-select') + sla = OUIAFormSelect('service-level-select') + usage = OUIAFormSelect('usage-select') + release_version = OUIAFormSelect('release-version-select') + add_ons = OUIASelect('syspurpose-addons-select') + + save = OUIAButton('save-syspurpose') + cancel = OUIAButton('cancel-syspurpose') + + class EditAnsibleRolesView(View): """Edit Ansible Roles Modal"""