class Server(BaseEntity, sentaku.modeling.ElementMixin): _param_name = ParamClassName('name') name = attr.ib() sid = attr.ib(default=1) address = sentaku.ContextualMethod() login = sentaku.ContextualMethod() login_admin = sentaku.ContextualMethod() logout = sentaku.ContextualMethod() update_password = sentaku.ContextualMethod() logged_in = sentaku.ContextualMethod() current_full_name = sentaku.ContextualMethod() current_username = sentaku.ContextualMethod() current_group_name = sentaku.ContextualMethod() # zone = sentaku.ContextualProperty() # slave_servers = sentaku.ContextualProperty() @property def settings(self): from cfme.configure.configuration.server_settings import ServerInformation setting = ServerInformation(appliance=self.appliance) return setting @property def authentication(self): from cfme.configure.configuration.server_settings import AuthenticationSetting auth_settings = AuthenticationSetting(self.appliance) return auth_settings @property def collect_logs(self): from cfme.configure.configuration.diagnostics_settings import ServerCollectLog return ServerCollectLog(self.appliance) @property def zone(self): server_res = self.appliance.rest_api.collections.servers.find_by( id=self.sid) server = server_res[0] server.reload(attributes=['zone']) zone = server.zone zone_obj = self.appliance.collections.zones.instantiate( name=zone['name'], description=zone['description'], id=zone['id']) return zone_obj @property def slave_servers(self): return self.zone.collections.servers.filter({'slave': True}).all()
class BaseCondition(BaseEntity, Updateable, Pretty): TREE_NODE = None PRETTY = None FIELD_VALUE = None _param_name = ParamClassName('description') description = attr.ib() expression = attr.ib(default=None) scope = attr.ib(default=None) notes = attr.ib(default=None) def update(self, updates): """Update this Condition in UI. Args: updates: Provided by update() context manager. """ view = navigate_to(self, "Edit") view.fill(updates) view.save_button.click() view = self.create_view(ConditionDetailsView, override=updates, wait="10s") view.flash.assert_success_message( 'Condition "{}" was saved'.format(updates.get("description", self.description)) ) def delete(self, cancel=False): """Delete this Condition in UI. Args: cancel: Whether to cancel the deletion (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Delete this {} Condition".format(self.FIELD_VALUE), handle_alert=not cancel) if cancel: assert view.is_displayed view.flash.assert_no_error() else: view = self.create_view(ConditionClassAllView, wait="20s") view.flash.assert_success_message('Condition "{}": Delete successful'.format( self.description)) def read_expression(self): view = navigate_to(self, "Details") assert view.is_displayed return view.expression.text def read_scope(self): view = navigate_to(self, "Details") assert view.is_displayed return view.scope.text @property def exists(self): """Check existence of this Condition. Returns: :py:class:`bool` signalizing the presence of the Condition in the database. """ try: self.appliance.rest_api.collections.conditions.get(description=self.description) return True except ValueError: return False
class BaseVM( BaseEntity, Pretty, Updateable, PolicyProfileAssignable, Taggable, ConsoleMixin, CustomButtonEventsMixin, ): """Base VM and Template class that holds the largest common functionality between VMs, instances, templates and images. In order to inherit these, you have to implement the ``on_details`` method. """ pretty_attrs = ['name', 'provider', 'template_name'] ### # To be set or implemented # ALL_LIST_LOCATION = None TO_OPEN_EDIT = None # Name of the item in Configuration that puts you in the form QUADICON_TYPE = "vm" # Titles of the delete buttons in configuration REMOVE_SELECTED = 'Remove selected items from Inventory' REMOVE_SINGLE = 'Remove Virtual Machine from Inventory' RETIRE_DATE_FMT = parsetime.saved_report_title_format _param_name = ParamClassName('name') DETAILS_VIEW_CLASS = None ### # Shared behaviour # PROVISION_CANCEL = 'Add of new VM Provision Request was cancelled by the user' PROVISION_START = ( 'VM Provision Request was Submitted, you will be notified when your VMs ' 'are ready') name = attr.ib() provider = attr.ib() def __new__(cls, *args, **kwargs): if cls in [BaseVM, VM, Template]: raise NotImplementedError('This class cannot be instantiated.') else: # magic {waves hands} return object.__new__(cls) ### # Properties # @property def is_vm(self): return not isinstance(self, _TemplateMixin) @property def quadicon_type(self): return self.QUADICON_TYPE ### # Methods # def check_compliance(self, timeout=240): """Initiates compliance check and waits for it to finish.""" view = navigate_to(self, "Details") original_state = self.compliance_status view.toolbar.policy.item_select( "Check Compliance of Last Known Configuration", handle_alert=True) view.flash.assert_no_error() wait_for(lambda: self.compliance_status != original_state, num_sec=timeout, delay=5, message="compliance of {} checked".format(self.name)) @property def compliance_status(self): """Returns the title of the compliance SummaryTable. The title contains datetime so it can be compared. Returns: :py:class:`NoneType` if no title is present (no compliance checks before), otherwise str """ view = navigate_to(self, "Details") view.toolbar.reload.click() return view.entities.summary("Compliance").get_text_of("Status") @property def compliant(self): """Check if the VM is compliant. Returns: :py:class:`bool` """ text = self.compliance_status.strip().lower() if text.startswith("non-compliant"): return False elif text.startswith("compliant"): return True else: raise ValueError( "{} is not a known state for compliance".format(text)) def delete(self, cancel=False, from_details=False): """Deletes the VM/Instance from the VMDB. Args: cancel: Whether to cancel the action in the alert. from_details: Whether to use the details view or list view. """ if from_details: view = navigate_to(self, 'Details') view.toolbar.configuration.item_select(self.REMOVE_SINGLE, handle_alert=not cancel) else: view = navigate_to(self.parent, 'All') self.find_quadicon().check() view.toolbar.configuration.item_select(self.REMOVE_SELECTED, handle_alert=not cancel) @property def ip_address(self): """Fetches IP Address of VM""" return self.mgmt.ip @property def mac_address(self): """Fetches MAC Address of VM""" # TODO: We should update this with wrapanapi method when it becomes available. view = navigate_to(self, "Details", use_resetter=False) try: return view.entities.summary('Properties').get_text_of( "MAC Address") except NameError: # since some providers have plural 'Addresses'. return view.entities.summary('Properties').get_text_of( "MAC Addresses") @property def is_retired(self): """Check retirement status of vm""" view = navigate_to(self, "Details", use_resetter=False) if view.entities.summary('Lifecycle').get_text_of( 'Retirement Date').lower() != 'never': try: retirement_state = VersionPicker({ LOWEST: 'Retirement state', '5.10': 'Retirement State' }) status = view.entities.summary('Lifecycle').get_text_of( retirement_state).lower() return status == 'retired' except NameError: return False else: return False def find_quadicon(self, from_any_provider=False, use_search=True): """Find and return a quadicon belonging to a specific vm Args: from_any_provider: Whether to look for it anywhere (root of the tree). Useful when looking up archived or orphaned VMs Returns: entity of appropriate type Raises: VmOrInstanceNotFound """ # todo :refactor this method replace it with vm methods like get_state if from_any_provider: view = navigate_to(self.parent, 'All') else: view = navigate_to(self, 'AllForProvider', use_resetter=False) view.toolbar.view_selector.select('Grid View') try: return view.entities.get_entity(name=self.name, surf_pages=True, use_search=use_search) except ItemNotFound: raise VmOrInstanceNotFound("VM '{}' not found in UI!".format( self.name)) def open_console(self, console='VM Console', invokes_alert=None): """ Initiates the opening of one of the console types supported by the Access button. Presently we only support VM Console, which is the HTML5 Console. In case of VMware provider it could be VMRC, VNC/HTML5, WebMKS, but we only support VNC/HTML5. Possible values for 'console' could be 'VM Console' and 'Web Console', but Web Console is not supported as well. Args: console: one of the supported console types given by the Access button. invokes_alert: If the particular console will invoke a CFME popup/alert setting this to true will handle this. """ # TODO: implement vmrc vm console if console not in ['VM Console']: raise NotImplementedError( 'Not supported console type: {}'.format(console)) view = navigate_to(self, 'Details') # Click console button given by type view.toolbar.access.item_select(console, handle_alert=invokes_alert) self.vm_console def open_details(self, properties=None): """Clicks on details infoblock""" view = navigate_to(self, 'Details') view.entities.summary(properties[0]).click_at(properties[1]) return self.create_view(VMPropertyDetailView) @property def last_analysed(self): """Returns the contents of the ``Last Analysed`` field in summary""" view = navigate_to(self, "Details") view.toolbar.reload.click() return view.entities.summary("Lifecycle").get_text_of( "Last Analyzed").strip() def load_details(self, refresh=False, from_any_provider=False): """Navigates to an VM's details page. Args: refresh: Refreshes the VM page if already there from_any_provider: Archived/Orphaned VMs need this """ if from_any_provider: view = navigate_to(self, 'AnyProviderDetails', use_resetter=False) else: view = navigate_to(self, 'Details', use_resetter=False) if refresh: view.toolbar.reload.click() view.wait_displayed() return view def open_edit(self): """Loads up the edit page of the object.""" return navigate_to(self, 'Edit') def open_timelines(self): """Navigates to an VM's timeline page. Returns: :py:class:`TimelinesView` object """ return navigate_to(self, 'Timelines') def rediscover(self): """Deletes the VM from the provider and lets it discover again""" self.delete(from_details=True) self.wait_for_delete() self.provider.refresh_provider_relationships() self.wait_to_appear() def rediscover_if_analysis_data_present(self): """Rediscovers the object if it has some analysis data present. Returns: Boolean if the rediscovery happened. """ if self.last_analysed.lower() != 'never': self.rediscover() return True return False def refresh_relationships(self, from_details=False, cancel=False, from_any_provider=False): """Executes a refresh of relationships. Args: from_details: Whether or not to perform action from instance details page cancel: Whether or not to cancel the refresh relationships action """ if from_details: view = navigate_to(self, 'Details', use_resetter=False) else: view = navigate_to(self.parent, 'All') self.find_quadicon(from_any_provider=from_any_provider).check() view.toolbar.configuration.item_select( "Refresh Relationships and Power States", handle_alert=not cancel) @property def retirement_date(self): """Returns the retirement date of the selected machine, or 'Never' Returns: :py:class:`str` object """ view = navigate_to(self, "Details") return view.entities.summary("Lifecycle").get_text_of( "Retirement Date").strip() def smartstate_scan(self, cancel=False, from_details=False, wait_for_task_result=False): """Initiates fleecing from the UI. Args: cancel: Whether or not to cancel the refresh relationships action from_details: Whether or not to perform action from instance details page """ if from_details: view = navigate_to(self, 'Details', use_resetter=False) else: view = navigate_to(self.parent, 'All') self.find_quadicon().check() view.toolbar.configuration.item_select('Perform SmartState Analysis', handle_alert=not cancel) if wait_for_task_result: task = self.appliance.collections.tasks.instantiate( name='Scan from Vm {}'.format(self.name), tab='AllTasks') task.wait_for_finished() return task def wait_to_disappear(self, timeout=600): """Wait for a VM to disappear within CFME Args: timeout: time (in seconds) to wait for it to appear """ wait_for(lambda: self.exists, num_sec=timeout, delay=5, fail_func=self.browser.refresh, fail_condition=True, message="wait for vm to not exist") wait_for_delete = wait_to_disappear # An alias for more fitting verbosity def wait_to_appear(self, timeout=600, load_details=True): """Wait for a VM to appear within CFME Args: timeout: time (in seconds) to wait for it to appear load_details: when found, should it load the vm details """ def _refresh(): self.provider.refresh_provider_relationships() self.appliance.browser.widgetastic.browser.refresh( ) # strange because ViaUI wait_for(lambda: self.exists, num_sec=timeout, delay=5, fail_func=_refresh, message="wait for vm to appear") if load_details: navigate_to(self, "Details", use_resetter=False) def set_ownership(self, user=None, group=None, click_cancel=False, click_reset=False): """Set instance ownership Args: user (User): user object for ownership group (Group): group object for ownership click_cancel (bool): Whether to cancel form submission click_reset (bool): Whether to reset form after filling """ view = navigate_to(self, 'SetOwnership', wait_for_view=0) fill_result = view.form.fill({ 'user_name': user.name if user else None, 'group_name': group.description if group else group }) if not fill_result: view.form.cancel_button.click() view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message( 'Set Ownership was cancelled by the user') return # Only if the form changed if click_reset: view.form.reset_button.click() view.flash.assert_message('All changes have been reset', 'warning') # Cancel after reset assert view.form.is_displayed view.form.cancel_button.click() elif click_cancel: view.form.cancel_button.click() view.flash.assert_success_message( 'Set Ownership was cancelled by the user') else: # save the form view.form.save_button.click() view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message( 'Ownership saved for selected {}'.format(self.VM_TYPE)) def unset_ownership(self): """Remove user ownership and return group to EvmGroup-Administrator""" view = navigate_to(self, 'SetOwnership', wait_for_view=0) fill_result = view.form.fill({ 'user_name': '<No Owner>', 'group_name': 'EvmGroup-administrator' }) if fill_result: view.form.save_button.click() msg = 'Ownership saved for selected {}'.format(self.VM_TYPE) else: view.form.cancel_button.click() logger.warning('No change during unset_ownership') msg = 'Set Ownership was cancelled by the user' view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message(msg) def rename(self, new_vm_name, cancel=False, reset=False): """Rename the VM Args: new_vm_name: object for renaming vm cancel (bool): Whether to cancel form submission reset (bool): Whether to reset form after filling """ view = navigate_to(self, 'Rename') changed = view.vm_name.fill(new_vm_name) if changed: if reset: view.reset_button.click() view.flash.assert_no_error() view.cancel_button.click() else: # save the form view.save_button.click() view.flash.assert_no_error() self.name = new_vm_name return self if cancel: view.cancel_button.click() view.flash.assert_no_error()
class BaseProvider(WidgetasticTaggable, Updateable, SummaryMixin, Navigatable): # List of constants that every non-abstract subclass must have defined _param_name = ParamClassName('name') STATS_TO_MATCH = [] db_types = ["Providers"] def __hash__(self): return hash(self.key) ^ hash(type(self)) def __eq__(self, other): return type(self) is type(other) and self.key == other.key @property def data(self): return self.get_yaml_data() @property def mgmt(self): return self.get_mgmt_system() @property def type(self): return self.type_name @property def id(self): """" Return the ID associated with the specified provider name """ return self.appliance.rest_api.collections.providers.find_by( name=self.name)[0].id @property def version(self): return self.data['version'] def deployment_helper(self, deploy_args): """ Used in utils.virtual_machines and usually overidden""" return {} @property def default_endpoint(self): return self.endpoints.get('default') if hasattr(self, 'endpoints') else None def get_yaml_data(self): """ Returns yaml data for this provider. """ if hasattr(self, 'provider_data') and self.provider_data is not None: return self.provider_data elif self.key is not None: return conf.cfme_data['management_systems'][self.key] else: raise ProviderHasNoKey( 'Provider {} has no key, so cannot get yaml data'.format( self.name)) def get_mgmt_system(self): """ Returns the mgmt_system using the :py:func:`utils.providers.get_mgmt` method. """ # gotta stash this in here to prevent circular imports from cfme.utils.providers import get_mgmt if self.key: return get_mgmt(self.key) elif getattr(self, 'provider_data', None): return get_mgmt(self.provider_data) else: raise ProviderHasNoKey( 'Provider {} has no key, so cannot get mgmt system'.format( self.name)) def create(self, cancel=False, validate_credentials=True, check_existing=False, validate_inventory=False): """ Creates a provider in the UI Args: cancel (boolean): Whether to cancel out of the creation. The cancel is done after all the information present in the Provider has been filled in the UI. validate_credentials (boolean): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. check_existing (boolean): Check if this provider already exists, skip if it does validate_inventory (boolean): Whether or not to block until the provider stats in CFME match the stats gleaned from the backend management system (default: ``True``) Returns: True if it was created, False if it already existed """ if check_existing and self.exists: created = False else: created = True logger.info('Setting up Infra Provider: %s', self.key) add_view = navigate_to(self, 'Add') if not cancel or (cancel and any(self.view_value_mapping.values())): # filling main part of dialog add_view.fill(self.view_value_mapping) if not cancel or (cancel and self.endpoints): # filling endpoints for endpoint_name, endpoint in self.endpoints.items(): try: # every endpoint class has name like 'default', 'events', etc. # endpoints view can have multiple tabs, the code below tries # to find right tab by passing endpoint name to endpoints view endp_view = getattr( self.endpoints_form(parent=add_view), endpoint_name) except AttributeError: # tabs are absent in UI when there is only single (default) endpoint endp_view = self.endpoints_form(parent=add_view) endp_view.fill(endpoint.view_value_mapping) # filling credentials if hasattr(endpoint, 'credentials'): endp_view.fill(endpoint.credentials.view_value_mapping) # sometimes we have cases that we need to validate even though # there is no credentials, such as Hawkular endpoint if (validate_credentials and hasattr(endp_view, 'validate') and endp_view.validate.is_displayed): # there are some endpoints which don't demand validation like # RSA key pair endp_view.validate.click() # Flash message widget is in add_view, not in endpoints tab logger.info( 'Validating credentials flash message for endpoint %s', endpoint_name) add_view.flash.assert_no_error() add_view.flash.assert_success_message( 'Credential validation was successful') main_view = self.create_view(navigator.get_class(self, 'All').VIEW) if cancel: created = False add_view.cancel.click() cancel_text = ('Add of {} Provider was ' 'cancelled by the user'.format( self.string_name)) main_view.entities.flash.assert_message(cancel_text) main_view.entities.flash.assert_no_error() else: add_view.add.click() if main_view.is_displayed: success_text = '{} Providers "{}" was saved'.format( self.string_name, self.name) main_view.entities.flash.assert_message(success_text) else: add_view.flash.assert_no_error() raise AssertionError( "Provider wasn't added. It seems form isn't accurately" " filled") if validate_inventory: self.validate() return created def create_rest(self): logger.info('Setting up provider: %s via rest', self.key) try: self.appliance.rest_api.collections.providers.action.create( hostname=self.hostname, ipaddress=self.ip_address, name=self.name, type="ManageIQ::Providers::{}".format(self.db_types[0]), credentials={ 'userid': self.endpoints['default'].credentials.principal, 'password': self.endpoints['default'].credentials.secret }) return self.appliance.rest_api.response.status_code == 200 except APIException: return None def update(self, updates, cancel=False, validate_credentials=True): """ Updates a provider in the UI. Better to use utils.update.update context manager than call this directly. Args: updates (dict): fields that are changing. cancel (boolean): whether to cancel out of the update. validate_credentials (boolean): whether credentials have to be validated """ edit_view = navigate_to(self, 'Edit') # todo: to replace/merge this code with create # update values: # filling main part of dialog endpoints = updates.pop('endpoints', None) if updates: edit_view.fill(updates) # filling endpoints if endpoints: endpoints = self._prepare_endpoints(endpoints) for endpoint in endpoints.values(): # every endpoint class has name like 'default', 'events', etc. # endpoints view can have multiple tabs, the code below tries # to find right tab by passing endpoint name to endpoints view try: endp_view = getattr(self.endpoints_form(parent=edit_view), endpoint.name) except AttributeError: # tabs are absent in UI when there is only single (default) endpoint endp_view = self.endpoints_form(parent=edit_view) endp_view.fill(endpoint.view_value_mapping) # filling credentials # the code below looks for existing endpoint equal to passed one and # compares their credentials. it fills passed credentials # if credentials are different cur_endpoint = self.endpoints[endpoint.name] if hasattr(endpoint, 'credentials'): if not hasattr(cur_endpoint, 'credentials') or \ endpoint.credentials != cur_endpoint.credentials: if hasattr(endp_view, 'change_password'): endp_view.change_password.click() elif hasattr(endp_view, 'change_key'): endp_view.change_key.click() else: NotImplementedError( "Such endpoint doesn't have change password/key button" ) endp_view.fill(endpoint.credentials.view_value_mapping) # sometimes we have cases that we need to validate even though # there is no credentials, such as Hawkular endpoint if (validate_credentials and hasattr(endp_view, 'validate') and endp_view.validate.is_displayed): endp_view.validate.click() # cloud rhos provider always requires validation of all endpoints # there should be a bz about that from cfme.cloud.provider.openstack import OpenStackProvider if self.one_of(OpenStackProvider): for endp in self.endpoints.values(): endp_view = getattr(self.endpoints_form(parent=edit_view), endp.name) if hasattr(endp_view, 'validate') and endp_view.validate.is_displayed: endp_view.validate.click() details_view = self.create_view( navigator.get_class(self, 'Details').VIEW) main_view = self.create_view(navigator.get_class(self, 'All').VIEW) if cancel: edit_view.cancel.click() cancel_text = 'Edit of {type} Provider "{name}" ' \ 'was cancelled by the user'.format(type=self.string_name, name=self.name) main_view.entities.flash.assert_message(cancel_text) main_view.entities.flash.assert_no_error() else: edit_view.save.click() if endpoints: for endp_name, endp in endpoints.items(): self.endpoints[endp_name] = endp if updates: self.name = updates.get('name', self.name) success_text = '{} Provider "{}" was saved'.format( self.string_name, self.name) if main_view.is_displayed: # since 5.8.1 main view is displayed when edit starts from main view main_view.flash.assert_message(success_text) elif details_view.is_displayed: # details view is always displayed up to 5.8.1 details_view.flash.assert_message(success_text) else: edit_view.flash.assert_no_error() raise AssertionError( "Provider wasn't updated. It seems form isn't accurately" " filled") def delete(self, cancel=True): """ Deletes a provider from CFME Args: cancel: Whether to cancel the deletion, defaults to True """ view = navigate_to(self, 'Details') item_title = version.pick({ '5.9': 'Remove this {} Provider from Inventory', version.LOWEST: 'Remove this {} Provider' }) view.toolbar.configuration.item_select(item_title.format( self.string_name), handle_alert=not cancel) if not cancel: msg = ('Delete initiated for 1 {} Provider from ' 'the {} Database'.format(self.string_name, self.appliance.product_name)) view.flash.assert_success_message(msg) def setup(self, rest=False): """ Sets up the provider robustly """ return self.create(cancel=False, validate_credentials=True, check_existing=True, validate_inventory=True) def delete_if_exists(self, *args, **kwargs): """Combines ``.exists`` and ``.delete()`` as a shortcut for ``request.addfinalizer`` Returns: True if provider existed and delete was initiated, False otherwise """ if self.exists: self.delete(*args, **kwargs) return True return False @variable(alias='rest') def is_refreshed(self, refresh_timer=None, refresh_delta=600): if refresh_timer: if refresh_timer.is_it_time(): logger.info(' Time for a refresh!') self.refresh_provider_relationships() refresh_timer.reset() rdate = self.last_refresh_date() if not rdate: return False td = self.appliance.utc_time() - rdate if td > datetime.timedelta(0, refresh_delta): self.refresh_provider_relationships() return False else: return True def validate(self): refresh_timer = RefreshTimer(time_for_refresh=300) try: wait_for(self.is_refreshed, [refresh_timer], message="is_refreshed", num_sec=1000, delay=60, handle_exception=True) except Exception: # To see the possible error. self.load_details(refresh=True) raise def validate_stats(self, ui=False): """ Validates that the detail page matches the Providers information. This method logs into the provider using the mgmt_system interface and collects a set of statistics to be matched against the UI. The details page is then refreshed continuously until the matching of all items is complete. A error will be raised if the match is not complete within a certain defined time period. """ # If we're not using db, make sure we are on the provider detail page if ui: self.load_details() # Initial bullet check if self._do_stats_match(self.mgmt, self.STATS_TO_MATCH, ui=ui): self.mgmt.disconnect() return else: # Set off a Refresh Relationships method = 'ui' if ui else None self.refresh_provider_relationships(method=method) refresh_timer = RefreshTimer(time_for_refresh=300) wait_for(self._do_stats_match, [self.mgmt, self.STATS_TO_MATCH, refresh_timer], {'ui': ui}, message="do_stats_match_db", num_sec=1000, delay=60) self.mgmt.disconnect() @variable(alias='rest') def refresh_provider_relationships(self, from_list_view=False): # from_list_view is ignored as it is included here for sake of compatibility with UI call. logger.debug('Refreshing provider relationships') col = self.appliance.rest_api.collections.providers.find_by( name=self.name) try: col[0].action.refresh() except IndexError: raise Exception("Provider collection empty") @refresh_provider_relationships.variant('ui') def refresh_provider_relationships_ui(self, from_list_view=False): """Clicks on Refresh relationships button in provider""" if from_list_view: view = navigate_to(self, 'All') entity = view.entities.get_entity(name=self.name, surf_pages=True) entity.check() else: view = navigate_to(self, 'Details') view.toolbar.configuration.item_select(self.refresh_text, handle_alert=True) @variable(alias='rest') def last_refresh_date(self): try: col = self.appliance.rest_api.collections.providers.find_by( name=self.name)[0] return col.last_refresh_date except AttributeError: return None def _num_db_generic(self, table_str): """ Fetch number of rows related to this provider in a given table Args: table_str: Name of the table; e.g. 'vms' or 'hosts' """ res = self.appliance.db.client.engine.execute( "SELECT count(*) " "FROM ext_management_systems, {0} " "WHERE {0}.ems_id=ext_management_systems.id " "AND ext_management_systems.name='{1}'".format( table_str, self.name)) return int(res.first()[0]) def _do_stats_match(self, client, stats_to_match=None, refresh_timer=None, ui=False): """ A private function to match a set of statistics, with a Provider. This function checks if the list of stats match, if not, the page is refreshed. Note: Provider mgmt_system uses the same key names as this Provider class to avoid having to map keyname/attributes e.g. ``num_template``, ``num_vm``. Args: client: A provider mgmt_system instance. stats_to_match: A list of key/attribute names to match. Raises: KeyError: If the host stats does not contain the specified key. ProviderHasNoProperty: If the provider does not have the property defined. """ host_stats = client.stats(*stats_to_match) method = None if ui: self.browser.selenium.refresh() method = 'ui' if refresh_timer: if refresh_timer.is_it_time(): logger.info(' Time for a refresh!') self.refresh_provider_relationships() refresh_timer.reset() for stat in stats_to_match: try: cfme_stat = getattr(self, stat)(method=method) success, value = tol_check(host_stats[stat], cfme_stat, min_error=0.05, low_val_correction=2) logger.info( ' Matching stat [%s], Host(%s), CFME(%s), ' 'with tolerance %s is %s', stat, host_stats[stat], cfme_stat, value, success) if not success: return False except KeyError: raise HostStatsNotContains( "Host stats information does not contain '{}'".format( stat)) except AttributeError: raise ProviderHasNoProperty( "Provider does not know how to get '{}'".format(stat)) else: return True @property def exists(self): """ Returns ``True`` if a provider of the same name exists on the appliance """ if self.name in self.appliance.managed_provider_names: return True return False def wait_for_delete(self): view = navigate_to(self, 'All') def is_entity_present(): try: view.entities.get_entity(name=self.name, surf_pages=True) return True except ItemNotFound: return False logger.info('Waiting for a provider to delete...') wait_for(is_entity_present, fail_condition=True, message="Wait provider to disappear", num_sec=1000, fail_func=self.browser.selenium.refresh) def load_details(self, refresh=False): """To be compatible with the Taggable and PolicyProfileAssignable mixins. Returns: ProviderDetails view """ view = navigate_to(self, 'Details') if refresh: view.toolbar.reload.click() return view def get_detail(self, *ident): """ Gets details from the details infoblock The function first ensures that we are on the detail page for the specific provider. Args: *ident: An SummaryTable title, followed by the Key name, e.g. "Relationships", "Images" Returns: A string representing the contents of passed field value. """ view = self.load_details() block, field = ident return getattr(view.entities, block.lower()).get_text_of(field) @classmethod def get_credentials(cls, credential_dict, cred_type=None): """Processes a credential dictionary into a credential object. Args: credential_dict: A credential dictionary. cred_type: Type of credential (None, token, ssh, amqp, ...) Returns: A :py:class:`cfme.base.credential.Credential` instance. """ domain = credential_dict.get('domain') token = credential_dict.get('token') if not cred_type: return Credential(principal=credential_dict['username'], secret=credential_dict['password'], domain=domain) elif cred_type == 'amqp': return EventsCredential(principal=credential_dict['username'], secret=credential_dict['password']) elif cred_type == 'ssh': return SSHCredential(principal=credential_dict['username'], secret=credential_dict['password']) elif cred_type == 'candu': return CANDUCredential(principal=credential_dict['username'], secret=credential_dict['password']) elif cred_type == 'token': return TokenCredential(token=token) @classmethod def get_credentials_from_config(cls, credential_config_name, cred_type=None): """Retrieves the credential by its name from the credentials yaml. Args: credential_config_name: The name of the credential in the credentials yaml. cred_type: Type of credential (None, token, ssh, amqp, ...) Returns: A :py:class:`cfme.base.credential.Credential` instance. """ creds = conf.credentials[credential_config_name] return cls.get_credentials(creds, cred_type=cred_type) @classmethod def process_credential_yaml_key(cls, cred_yaml_key, cred_type=None): """Function that detects if it needs to look up credentials in the credential yaml and acts as expected. If you pass a dictionary, it assumes it does not need to look up in the credentials yaml file. If anything else is passed, it continues with looking up the credentials in the yaml file. Args: cred_yaml_key: Either a string pointing to the credentials.yaml or a dictionary which is considered as the credentials. Returns: :py:class:`cfme.base.credential.Credential` instance """ if isinstance(cred_yaml_key, dict): return cls.get_credentials(cred_yaml_key, cred_type=cred_type) else: return cls.get_credentials_from_config(cred_yaml_key, cred_type=cred_type) # Move to collection @classmethod def clear_providers(cls): """ Clear all providers of given class on the appliance """ from cfme.utils.appliance import current_appliance as app app.rest_api.collections.providers.reload() # cfme 5.9 doesn't allow to remove provider thru api bz_blocked = BZ(1501941, forced_streams=['5.9']).blocks if app.version < '5.9' or (app.version >= '5.9' and not bz_blocked): for prov in app.rest_api.collections.providers.all: try: if any(db_type in prov.type for db_type in cls.db_types): logger.info('Deleting provider: %s', prov.name) prov.action.delete() prov.wait_not_exists() except APIException as ex: # Provider is already gone (usually caused by NetworkManager objs) if 'RecordNotFound' not in str(ex): raise ex else: # Delete all matching for prov in app.managed_known_providers: if prov.one_of(cls): logger.info('Deleting provider: %s', prov.name) prov.delete(cancel=False) # Wait for all matching to be deleted for prov in app.managed_known_providers: if prov.one_of(cls): prov.wait_for_delete() app.rest_api.collections.providers.reload() def one_of(self, *classes): """ Returns true if provider is an instance of any of the classes or sublasses there of""" return isinstance(self, classes) @staticmethod def _prepare_endpoints(endpoints): if not endpoints: return {} elif isinstance(endpoints, dict): return endpoints elif isinstance(endpoints, Iterable): return {(e.name, e) for e in endpoints} elif isinstance(endpoints, DefaultEndpoint): return {endpoints.name: endpoints} else: raise ValueError( "Endpoints should be either dict or endpoint class") # These methods need to be overridden in the provider specific classes def get_console_connection_status(self): raise NotImplementedError( "This method is not implemented for given provider") def get_remote_console_canvas(self): raise NotImplementedError( "This method is not implemented for given provider") def get_console_ctrl_alt_del_btn(self): raise NotImplementedError( "This method is not implemented for given provider") def get_console_fullscreen_btn(self): raise NotImplementedError( "This method is not implemented for given provider") def get_all_provider_ids(self): """ Returns an integer list of provider ID's via the REST API """ # TODO: Move to ProviderCollection logger.debug('Retrieving the list of provider ids') provider_ids = [] try: for prov in self.appliance.rest_api.collections.providers.all: provider_ids.append(prov.id) except APIException: return None return provider_ids def get_all_vm_ids(self): """ Returns an integer list of vm ID's via the REST API """ # TODO: Move to VMCollection or BaseVMCollection logger.debug('Retrieving the list of vm ids') vm_ids = [] try: for vm in self.appliance.rest_api.collections.vms.all: vm_ids.append(vm.id) except APIException: return None return vm_ids def get_all_host_ids(self): """ Returns an integer list of host ID's via the Rest API """ # TODO: Move to HostCollection logger.debug('Retrieving the list of host ids') host_ids = [] try: for host in self.appliance.rest_api.collections.hosts.all: host_ids.append(host.id) except APIException: return None return host_ids def get_all_template_ids(self): """Returns an integer list of template ID's via the Rest API""" # TODO: Move to TemplateCollection logger.debug('Retrieving the list of template ids') template_ids = [] try: for template in self.appliance.rest_api.collections.templates.all: template_ids.append(template.id) except APIException: return None return template_ids def get_provider_details(self, provider_id): """Returns the name, and type associated with the provider_id""" # TODO: Move to ProviderCollection.find logger.debug( 'Retrieving the provider details for ID: {}'.format(provider_id)) details = {} try: prov = self.appliance.rest_api.collections.providers.get( id=provider_id) except APIException: return None details['id'] = prov.id details['name'] = prov.name details['type'] = prov.type return details def get_vm_details(self, vm_id): """ Returns the name, type, vendor, host_id, and power_state associated with the vm_id. """ # TODO: Move to VMCollection.find logger.debug('Retrieving the VM details for ID: {}'.format(vm_id)) details = {} try: vm = self.appliance.rest_api.collections.vms.get(id=vm_id) except APIException: return None details['id'] = vm.id details['ems_id'] = vm.ems_id details['name'] = vm.name details['type'] = vm.type details['vendor'] = vm.vendore details['host_id'] = vm.host_id details['power_state'] = vm.power_state return details def get_template_details(self, template_id): """ Returns the name, type, and guid associated with the template_id """ # TODO: Move to TemplateCollection.find logger.debug( 'Retrieving the template details for ID: {}'.format(template_id)) template_details = {} try: template = self.appliance.rest_api.collections.templates.get( id=template_id) except APIException: return None template_details['name'] = template.name template_details['type'] = template.type template_details['guid'] = template.guid return template_details def get_all_template_details(self): """ Returns a dictionary mapping template ids to their name, type, and guid """ # TODO: Move to TemplateCollection.all all_details = {} for id in self.get_all_template_ids(): all_details[id] = self.get_template_details(id) return all_details def get_vm_id(self, vm_name): """ Return the ID associated with the specified VM name """ # TODO: Get Provider object from VMCollection.find, then use VM.id to get the id logger.debug('Retrieving the ID for VM: {}'.format(vm_name)) for vm_id in self.get_all_vm_ids(): details = self.get_vm_details(vm_id) if details['name'] == vm_name: return vm_id def get_vm_ids(self, vm_names): """ Returns a dictionary mapping each VM name to it's id """ # TODO: Move to VMCollection.find or VMCollection.all name_list = vm_names[:] logger.debug('Retrieving the IDs for {} VM(s)'.format(len(name_list))) id_map = {} for vm_id in self.get_all_vm_ids(): if not name_list: break vm_name = self.get_vm_details(vm_id)['name'] if vm_name in name_list: id_map[vm_name] = vm_id name_list.remove(vm_name) return id_map def get_template_guids(self, template_dict): """ Returns a list of tuples. The inner tuples are formated so that each guid is in index 0, and its provider's name is in index 1. Expects a dictionary mapping a provider to its templates """ # TODO: Move to TemplateCollection result_list = [] all_template_details = self.get_all_template_details() for provider, templates in template_dict.iteritems(): for template_name in templates: inner_tuple = () for id in all_template_details: if ((all_template_details[id]['name'] == template_name) and (self.db_types[0] in all_template_details[id]['type'])): inner_tuple += (all_template_details[id]['guid'], ) inner_tuple += (provider, ) result_list.append(inner_tuple) return result_list
class PXEServer(Updateable, Pretty, Navigatable): """Model of a PXE Server object in CFME Args: name: Name of PXE server. depot_type: Depot type, either Samba or Network File System. uri: The Depot URI. userid: The Samba username. password: The Samba password. access_url: HTTP access path for PXE server. pxe_dir: The PXE dir for accessing configuration. windows_dir: Windows source directory. customize_dir: Customization directory for templates. menu_filename: Menu filename for iPXE/syslinux menu. """ pretty_attrs = ['name', 'uri', 'access_url'] _param_name = ParamClassName('name') def __init__(self, name=None, depot_type=None, uri=None, userid=None, password=None, access_url=None, pxe_dir=None, windows_dir=None, customize_dir=None, menu_filename=None, appliance=None): Navigatable.__init__(self, appliance=appliance) self.name = name self.depot_type = depot_type self.uri = uri self.userid = userid # todo: turn into Credentials class self.password = password self.access_url = access_url self.pxe_dir = pxe_dir self.windows_dir = windows_dir self.customize_dir = customize_dir self.menu_filename = menu_filename def create(self, cancel=False, refresh=True, refresh_timeout=120): """ Creates a PXE server object Args: cancel (boolean): Whether to cancel out of the creation. The cancel is done after all the information present in the PXE Server has been filled in the UI. refresh (boolean): Whether to run the refresh operation on the PXE server after the add has been completed. """ view = navigate_to(self, 'Add') view.fill({'name': self.name, 'depot_type': self.depot_type, 'access_url': self.access_url, 'pxe_dir': self.pxe_dir, 'windows_images_dir': self.windows_dir, 'customization_dir': self.customize_dir, 'filename': self.menu_filename, 'uri': self.uri, # Samba only 'username': self.userid, 'password': self.password, 'confirm_password': self.password}) if self.depot_type == 'Samba' and self.userid and self.password: view.validate.click() main_view = self.create_view(PXEServersView) if cancel: view.cancel.click() main_view.flash.assert_success_message('Add of new PXE Server ' 'was cancelled by the user') else: view.add.click() main_view.flash.assert_no_error() if refresh: self.refresh(timeout=refresh_timeout) @variable(alias="db") def exists(self): """ Checks if the PXE server already exists """ dbs = self.appliance.db.client candidates = list(dbs.session.query(dbs["pxe_servers"])) return self.name in [s.name for s in candidates] @exists.variant('ui') def exists_ui(self): """ Checks if the PXE server already exists """ try: navigate_to(self, 'Details') return True except NoSuchElementException: return False def update(self, updates, cancel=False): """ Updates a PXE server in the UI. Better to use utils.update.update context manager than call this directly. Args: updates (dict): fields that are changing. cancel (boolean): whether to cancel out of the update. """ view = navigate_to(self, 'Edit') view.fill(updates) if updates.get('userid') or updates.get('password'): view.validate.click() name = updates.get('name') or self.name main_view = self.create_view(PXEServersView, override=updates) if cancel: view.cancel.click() main_view.flash.assert_success_message('Edit of PXE Server "{}" was ' 'cancelled by the user'.format(name)) else: view.save.click() main_view.flash.assert_no_error() def delete(self, cancel=True): """ Deletes a PXE server from CFME Args: cancel: Whether to cancel the deletion, defaults to True """ view = navigate_to(self, 'Details') view.toolbar.configuration.item_select('Remove this PXE Server from Inventory', handle_alert=not cancel) if not cancel: main_view = self.create_view(PXEServersView) main_view.flash.assert_no_error() else: navigate_to(self, 'Details') def refresh(self, wait=True, timeout=120): """ Refreshes the PXE relationships and waits for it to be updated """ view = navigate_to(self, 'Details') last_time = view.entities.basic_information.get_text_of('Last Refreshed On') view.toolbar.configuration.item_select('Refresh Relationships', handle_alert=True) view.flash.assert_success_message('PXE Server "{}": Refresh Relationships ' 'successfully initiated'.format(self.name)) if wait: basic_info = view.entities.basic_information wait_for(lambda lt: lt != basic_info.get_text_of('Last Refreshed On'), func_args=[last_time], fail_func=view.toolbar.reload.click, num_sec=timeout, message="pxe refresh") @variable(alias='db') def get_pxe_image_type(self, image_name): pxe_i = self.appliance.db.client["pxe_images"] pxe_s = self.appliance.db.client["pxe_servers"] pxe_t = self.appliance.db.client["pxe_image_types"] hosts = list(self.appliance.db.client.session.query(pxe_t.name) .join(pxe_i, pxe_i.pxe_image_type_id == pxe_t.id) .join(pxe_s, pxe_i.pxe_server_id == pxe_s.id) .filter(pxe_s.name == self.name) .filter(pxe_i.name == image_name)) if hosts: return hosts[0][0] else: return None @get_pxe_image_type.variant('ui') def get_pxe_image_type_ui(self, image_name): view = navigate_to(self, 'Details') view.sidebar.servers.tree.click_path('All PXE Servers', self.name, 'PXE Images', image_name) details_view = self.create_view(PXESystemImageTypeDetailsView) return details_view.entities.basic_information.get_text_of('Type') def set_pxe_image_type(self, image_name, image_type): """ Function to set the image type of a PXE image """ # todo: maybe create appropriate navmazing destinations instead ? if self.get_pxe_image_type(image_name) != image_type: view = navigate_to(self, 'Details') view.sidebar.servers.tree.click_path('All PXE Servers', self.name, 'PXE Images', image_name) details_view = self.create_view(PXESystemImageTypeDetailsView) details_view.toolbar.configuration.item_select('Edit this PXE Image') edit_view = self.create_view(PXEImageEditView) edit_view.fill({'type': image_type}) edit_view.save.click()
class Report(BaseEntity, Updateable): _param_name = ParamClassName('title') menu_name = attr.ib(default=None) title = attr.ib(default=None) company_name = attr.ib() type = attr.ib(default=None) subtype = attr.ib(default=None) base_report_on = attr.ib(default=None) report_fields = attr.ib(default=None) cancel_after = attr.ib(default=None) consolidation = attr.ib(default=None) formatting = attr.ib(default=None) styling = attr.ib(default=None) filter = attr.ib(default=None) filter_show_costs = attr.ib(default=None) filter_owner = attr.ib(default=None) filter_tag_cat = attr.ib(default=None) filter_tag_value = attr.ib(default=None) interval = attr.ib(default=None) interval_size = attr.ib(default=None) interval_end = attr.ib(default=None) sort = attr.ib(default=None) chart_type = attr.ib(default=None) top_values = attr.ib(default=None) sum_other = attr.ib(default=None) base_timeline_on = attr.ib(default=None) band_units = attr.ib(default=None) event_position = attr.ib(default=None) show_event_unit = attr.ib(default=None) show_event_count = attr.ib(default=None) summary = attr.ib(default=None) charts = attr.ib(default=None) timeline = attr.ib(default=None) is_candu = attr.ib(default=False) def __attrs_post_init__(self): self._collections = {'saved_reports': SavedReportsCollection} @company_name.default def company_name_default(self): return "My Company (All Groups)" def update(self, updates): view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() view = self.create_view(ReportDetailsView, override=updates, wait='10s') view.flash.assert_no_error() if changed: view.flash.assert_message(f'Report "{self.menu_name}" was saved') else: view.flash.assert_message( f'Edit of Report "{self.menu_name}" was cancelled by the user') def copy(self): """ Copy a report via UI and return a copy of a Report object""" menu_name = f"Copy of {self.menu_name}" view = navigate_to(self, "Copy") view.add_button.click() self.create_view(AllReportsView, wait="5s") return self.appliance.collections.reports.instantiate( type=self.company_name, subtype="Custom", menu_name=menu_name, title=self.title, ) def delete(self, cancel=False): view = navigate_to(self, "Details") node = view.reports.tree.expand_path("All Reports", self.company_name, "Custom") custom_reports_number = len(view.reports.tree.child_items(node)) view.configuration.item_select("Delete this Report from the Database", handle_alert=not cancel) if cancel: view.wait_displayed() view.flash.assert_no_error() else: # This check is needed because after deleting the last custom report, # the whole "My Company (All EVM Groups)" branch in the tree will be removed. if custom_reports_number > 1: view = self.create_view(AllCustomReportsView, wait='5s') view.flash.assert_no_error() view.flash.assert_message( f'Report "{self.menu_name}": Delete successful') @cached_property def saved_reports(self): return self.collections.saved_reports def create_schedule( self, name=None, description=None, active=True, timer=None, email=None, email_options=None, cancel=False, ): view = navigate_to(self, "ScheduleReport") if email: email["emails_send"] = True schedule = self.appliance.collections.schedules.instantiate( name=name or self.menu_name, description=description or self.menu_name, active=active, report_filter={ "filter_type": self.company_name, "subfilter_type": self.subtype, "report_type": self.menu_name, }, timer=timer, email=email, email_options=email_options) view.fill(schedule.fill_dict) if cancel: view.cancel_button.click() else: view.add_button.click() view.flash.assert_no_error() assert schedule.exists return schedule def queue(self, wait_for_finish=False): view = navigate_to(self, "Details") view.report_info.queue_button.click() view.flash.assert_no_error() if wait_for_finish: # Get the queued_at value to always target the correct row if view.saved_reports.paginator.sorted_by['sortDir'] != "DESC": view.saved_reports.paginator.sort(sort_by="Queued At", ascending=False) queued_at = view.saved_reports.table[0]["Queued At"].text def _get_state(): row = view.saved_reports.table.row(queued_at=queued_at) status = row.status.text.strip().lower() assert status != "error" return status == "complete" wait_for( _get_state, delay=1, message="wait for report generation finished", fail_func=view.reload_button.click, num_sec=300, ) view.reload_button.click() first_row = view.saved_reports.table[0] saved_report = self.saved_reports.instantiate(first_row.run_at.text, first_row.queued_at.text, self.is_candu) return saved_report @property def tree_path(self): return [ "All Reports", self.type or self.company_name, self.subtype or "Custom", self.menu_name, ] @property def rest_api_entity(self): try: return self.appliance.rest_api.collections.reports.get( name=self.menu_name) except ValueError: raise RestLookupError( f"No report rest entity found matching name {self.menu_name}")
class Report(BaseEntity, Updateable): _param_name = ParamClassName('title') menu_name = attr.ib(default=None) title = attr.ib(default=None) company_name = attr.ib() type = attr.ib(default=None) subtype = attr.ib(default=None) base_report_on = attr.ib(default=None) report_fields = attr.ib(default=None) cancel_after = attr.ib(default=None) consolidation = attr.ib(default=None) formatting = attr.ib(default=None) styling = attr.ib(default=None) filter = attr.ib(default=None) filter_show_costs = attr.ib(default=None) filter_owner = attr.ib(default=None) filter_tag_cat = attr.ib(default=None) filter_tag_value = attr.ib(default=None) interval = attr.ib(default=None) interval_size = attr.ib(default=None) interval_end = attr.ib(default=None) sort = attr.ib(default=None) chart_type = attr.ib(default=None) top_values = attr.ib(default=None) sum_other = attr.ib(default=None) base_timeline_on = attr.ib(default=None) band_units = attr.ib(default=None) event_position = attr.ib(default=None) show_event_unit = attr.ib(default=None) show_event_count = attr.ib(default=None) summary = attr.ib(default=None) charts = attr.ib(default=None) timeline = attr.ib(default=None) is_candu = attr.ib(default=False) def __attrs_post_init__(self): self._collections = {'saved_reports': SavedReportsCollection} @company_name.default def company_name_default(self): return "My Company (All Groups)" def update(self, updates): view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() view = self.create_view(ReportDetailsView, override=updates, wait='10s') view.flash.assert_no_error() if changed: view.flash.assert_message('Report "{}" was saved'.format( self.menu_name)) else: view.flash.assert_message( 'Edit of Report "{}" was cancelled by the user'.format( self.menu_name)) def copy(self): """ Copy a report via UI and return a copy of a Report object""" menu_name = "Copy of {}".format(self.menu_name) view = navigate_to(self, "Copy") view.add_button.click() self.create_view(AllReportsView, wait="5s") return self.appliance.collections.reports.instantiate( type=self.company_name, subtype="Custom", menu_name=menu_name, title=self.title, ) def delete(self, cancel=False): view = navigate_to(self, "Details") node = view.reports.tree.expand_path("All Reports", self.company_name, "Custom") custom_reports_number = len(view.reports.tree.child_items(node)) view.configuration.item_select("Delete this Report from the Database", handle_alert=not cancel) if cancel: view.wait_displayed() view.flash.assert_no_error() else: # This check is needed because after deleting the last custom report, # the whole "My Company (All EVM Groups)" branch in the tree will be removed. if custom_reports_number > 1: view = self.create_view(AllCustomReportsView, wait='5s') view.flash.assert_no_error() if not BZ(1561779, forced_streams=['5.9', '5.8']).blocks: view.flash.assert_message( 'Report "{}": Delete successful'.format(self.menu_name)) @cached_property def saved_reports(self): return self.collections.saved_reports def create_schedule(self, name=None, description=None, active=None, timer=None, from_email=None, emails=None, email_options=None): view = navigate_to(self, "ScheduleReport") view.fill({ "name": name, "description": description, "active": active, "run": timer.get("run"), "time_zone": timer.get("time_zone"), "starting_date": timer.get("starting_date"), "hour": timer.get("hour"), "minute": timer.get("minute"), "emails_send": bool(emails), "from_email": from_email, "emails": emails, "send_if_empty": email_options.get("send_if_empty"), "send_txt": email_options.get("send_txt"), "send_csv": email_options.get("send_csv"), "send_pdf": email_options.get("send_pdf") }) view.add_button.click() view.flash.assert_no_error() schedule = self.appliance.collections.schedules.instantiate( name=name or self.menu_name, description=description, filter=(self.company_name, self.subtype, self.menu_name), active=active, emails=emails, email_options=email_options, ) assert schedule.exists return schedule def queue(self, wait_for_finish=False): view = navigate_to(self, "Details") view.report_info.queue_button.click() view.flash.assert_no_error() if wait_for_finish: # Get the queued_at value to always target the correct row if view.saved_reports.paginator.sorted_by['sortDir'] != "DESC": view.saved_reports.paginator.sort(sort_by="Queued At", ascending=False) queued_at = view.saved_reports.table[0]["Queued At"].text def _get_state(): row = view.saved_reports.table.row(queued_at=queued_at) status = row.status.text.strip().lower() assert status != "error" return status == "complete" wait_for( _get_state, delay=1, message="wait for report generation finished", fail_func=view.reload_button.click, num_sec=300, ) view.reload_button.click() first_row = view.saved_reports.table[0] saved_report = self.saved_reports.instantiate(first_row.run_at.text, first_row.queued_at.text, self.is_candu) return saved_report
class BaseVM(Pretty, Updateable, PolicyProfileAssignable, WidgetasticTaggable, Navigatable): """Base VM and Template class that holds the largest common functionality between VMs, instances, templates and images. In order to inherit these, you have to implement the ``on_details`` method. """ pretty_attrs = ['name', 'provider', 'template_name'] ### # Factory class methods # @classmethod def factory(cls, vm_name, provider, template_name=None, template=False): """Factory class method that determines the correct subclass for given provider. For reference how does that work, refer to the entrypoints in the setup.py Args: vm_name: Name of the VM/Instance as it appears in the UI provider: The provider object (not the string!) template_name: Source template name. Useful when the VM/Instance does not exist and you want to create it. template: Whether the generated object class should be VM/Instance or a template class. """ try: return all_types(template)[provider.type](vm_name, provider, template_name) except KeyError: # Matching via provider type failed. Maybe we have some generic classes for infra/cloud? try: return all_types(template)[provider.category](vm_name, provider, template_name) except KeyError: raise UnknownProviderType( 'Unknown type of provider CRUD object: {}'.format( provider.__class__.__name__)) ### # To be set or implemented # ALL_LIST_LOCATION = None TO_OPEN_EDIT = None # Name of the item in Configuration that puts you in the form QUADICON_TYPE = "vm" # Titles of the delete buttons in configuration REMOVE_SELECTED = VersionPick({ '5.8': 'Remove selected items', '5.9': 'Remove selected items from Inventory' }) REMOVE_SINGLE = VersionPick({ '5.8': 'Remove Virtual Machine', '5.9': 'Remove Virtual Machine from Inventory' }) RETIRE_DATE_FMT = VersionPick({ '5.8': parsetime.american_minutes_with_utc, '5.9': parsetime.saved_report_title_format }) _param_name = ParamClassName('name') DETAILS_VIEW_CLASS = None ### # Shared behaviour # def __init__(self, name, provider, template_name=None, appliance=None): super(BaseVM, self).__init__() Navigatable.__init__(self, appliance=appliance) if type(self) in {BaseVM, VM, Template}: raise NotImplementedError('This class cannot be instantiated.') self.name = name self.provider = provider self.template_name = template_name ### # Properties # @property def is_vm(self): return not isinstance(self, _TemplateMixin) @property def quadicon_type(self): return self.QUADICON_TYPE ### # Methods # def check_compliance(self, timeout=240): """Initiates compliance check and waits for it to finish.""" view = navigate_to(self, "Details") original_state = self.compliance_status view.toolbar.policy.item_select( "Check Compliance of Last Known Configuration", handle_alert=True) view.flash.assert_no_error() wait_for(lambda: self.compliance_status != original_state, num_sec=timeout, delay=5, message="compliance of {} checked".format(self.name)) @property def compliance_status(self): """Returns the title of the compliance SummaryTable. The title contains datetime so it can be compared. Returns: :py:class:`NoneType` if no title is present (no compliance checks before), otherwise str """ view = navigate_to(self, "Details") view.browser.refresh() return self.get_detail(properties=("Compliance", "Status")) @property def compliant(self): """Check if the VM is compliant. Returns: :py:class:`bool` """ text = self.compliance_status.strip().lower() if text.startswith("non-compliant"): return False elif text.startswith("compliant"): return True else: raise ValueError( "{} is not a known state for compliance".format(text)) @property def console_handle(self): ''' The basic algorithm for getting the consoles window handle is to get the appliances window handle and then iterate through the window_handles till we find one that is not the appliances window handle. Once we find this check that it has a canvas widget with a specific ID ''' browser = self.appliance.browser.widgetastic appliance_handle = browser.window_handle cur_handles = browser.selenium.window_handles logger.info("Current Window Handles: {}".format(cur_handles)) for handle in cur_handles: if handle != appliance_handle: # FIXME: Add code to verify the tab has the correct widget # for a console tab. return handle @property def vm_console(self): """Get the consoles window handle, and then create a VMConsole object, and store the VMConsole object aside. """ console_handle = self.console_handle if console_handle is None: raise TypeError("Console handle should not be None") appliance_handle = self.appliance.browser.widgetastic.window_handle logger.info("Creating VMConsole:") logger.info(" appliance_handle: {}".format(appliance_handle)) logger.info(" console_handle: {}".format(console_handle)) logger.info(" name: {}".format(self.name)) return VMConsole(appliance_handle=appliance_handle, console_handle=console_handle, vm=self) def delete(self, cancel=False, from_details=False): """Deletes the VM/Instance from the VMDB. Args: cancel: Whether to cancel the action in the alert. from_details: Whether to use the details view or list view. """ if from_details: view = navigate_to(self, 'Details') view.toolbar.configuration.item_select(self.REMOVE_SINGLE, handle_alert=not cancel) else: view = navigate_to(self, 'All') self.find_quadicon().check() view.toolbar.configuration.item_select(self.REMOVE_SELECTED, handle_alert=not cancel) @property def exists(self): """Checks presence of the quadicon in the CFME.""" try: self.find_quadicon() return True except VmOrInstanceNotFound: return False @property def ip_address(self): """Fetches IP Address of VM""" return self.provider.mgmt.get_ip_address(self.name) @property def is_retired(self): """"Check retirement status of vm""" view = self.load_details(refresh=True) if view.entities.lifecycle.get_text_of( 'Retirement Date').lower() != 'never': try: return view.entities.lifecycle.get_text_of( 'Retirement state').lower() == 'retired' except NameError: return False else: return False def find_quadicon(self, from_any_provider=False, use_search=True): """Find and return a quadicon belonging to a specific vm Args: from_any_provider: Whether to look for it anywhere (root of the tree). Useful when looking up archived or orphaned VMs Returns: entity of appropriate type Raises: VmOrInstanceNotFound """ # todo :refactor this method replace it with vm methods like get_state if from_any_provider: view = navigate_to(self, 'All') else: view = navigate_to(self, 'AllForProvider', use_resetter=False) if 'Grid View' != view.toolbar.view_selector.selected: view.toolbar.view_selector.select('Grid View') try: return view.entities.get_entity(name=self.name, surf_pages=True, use_search=use_search) except ItemNotFound: raise VmOrInstanceNotFound("VM '{}' not found in UI!".format( self.name)) def get_detail(self, properties=None): """Gets details from the details infoblock The function first ensures that we are on the detail page for the specific VM/Instance. Args: properties: An InfoBlock title, followed by the Key name, e.g. "Relationships", "Images" Returns: A string representing the contents of the InfoBlock's value. """ view = navigate_to(self, 'Details') return getattr(view.entities, properties[0].lower().replace( ' ', '_')).get_text_of(properties[1]) def open_console(self, console='VM Console', invokes_alert=None): """ Initiates the opening of one of the console types supported by the Access button. Presently we only support VM Console, which is the HTML5 Console. In case of VMware provider it could be VMRC, VNC/HTML5, WebMKS, but we only support VNC/HTML5. Possible values for 'console' could be 'VM Console' and 'Web Console', but Web Console is not supported as well. Args: console: one of the supported console types given by the Access button. invokes_alert: If the particular console will invoke a CFME popup/alert setting this to true will handle this. """ # TODO: implement vmrc vm console if console not in ['VM Console']: raise NotImplementedError( 'Not supported console type: {}'.format(console)) view = navigate_to(self, 'Details') # Click console button given by type view.toolbar.access.item_select(console, handle_alert=invokes_alert) self.vm_console def open_details(self, properties=None): """Clicks on details infoblock""" view = navigate_to(self, 'Details') getattr(view.entities, properties[0].lower().replace(' ', '_')).click_at(properties[1]) return self.create_view(VMPropertyDetailView) @classmethod def get_first_vm(cls, provider): """Get first VM/Instance.""" # todo: move this to base provider ? view = navigate_to(cls, 'AllForProvider', provider=provider) return view.entities.get_first_entity() @property def last_analysed(self): """Returns the contents of the ``Last Analysed`` field in summary""" return self.get_detail(properties=('Lifecycle', 'Last Analyzed')).strip() def load_details(self, refresh=False, from_any_provider=False): """Navigates to an VM's details page. Args: refresh: Refreshes the VM page if already there from_any_provider: Archived/Orphaned VMs need this """ if from_any_provider: view = navigate_to(self, 'AnyProviderDetails', use_resetter=False) else: view = navigate_to(self, 'Details', use_resetter=False) if refresh: view.toolbar.reload.click() view.wait_displayed() return view def open_edit(self): """Loads up the edit page of the object.""" return navigate_to(self, 'Edit') def open_timelines(self): """Navigates to an VM's timeline page. Returns: :py:class:`TimelinesView` object """ return navigate_to(self, 'Timelines') def rediscover(self): """Deletes the VM from the provider and lets it discover again""" self.delete(from_details=True) self.wait_for_delete() self.provider.refresh_provider_relationships() self.wait_to_appear() def rediscover_if_analysis_data_present(self): """Rediscovers the object if it has some analysis data present. Returns: Boolean if the rediscovery happened. """ if self.last_analysed.lower() != 'never': self.rediscover() return True return False def refresh_relationships(self, from_details=False, cancel=False, from_any_provider=False): """Executes a refresh of relationships. Args: from_details: Whether or not to perform action from instance details page cancel: Whether or not to cancel the refresh relationships action """ if from_details: view = self.load_details() else: view = navigate_to(self, 'All') self.find_quadicon(from_any_provider=from_any_provider).check() view.toolbar.configuration.item_select( "Refresh Relationships and Power States", handle_alert=not cancel) @property def retirement_date(self): """Returns the retirement date of the selected machine, or 'Never' Returns: :py:class:`str` object """ return self.get_detail(properties=("Lifecycle", "Retirement Date")).strip() def smartstate_scan(self, cancel=False, from_details=False, wait_for_task_result=False): """Initiates fleecing from the UI. Args: cancel: Whether or not to cancel the refresh relationships action from_details: Whether or not to perform action from instance details page """ if from_details: view = self.load_details() else: view = navigate_to(self, 'All') self.find_quadicon().check() view.toolbar.configuration.item_select('Perform SmartState Analysis', handle_alert=not cancel) if wait_for_task_result: view = self.appliance.browser.create_view(TasksView) wait_for(lambda: is_vm_analysis_finished(self.name), delay=15, timeout="10m", fail_func=view.reload.click) def wait_to_disappear(self, timeout=600): """Wait for a VM to disappear within CFME Args: timeout: time (in seconds) to wait for it to appear """ wait_for(lambda: self.exists, num_sec=timeout, delay=30, fail_func=self.browser.refresh, fail_condition=True, message="wait for vm to not exist") wait_for_delete = wait_to_disappear # An alias for more fitting verbosity def wait_to_appear(self, timeout=600, load_details=True): """Wait for a VM to appear within CFME Args: timeout: time (in seconds) to wait for it to appear load_details: when found, should it load the vm details """ def _refresh(): self.provider.refresh_provider_relationships() self.appliance.browser.widgetastic.browser.refresh( ) # strange because ViaUI wait_for(lambda: self.exists, num_sec=timeout, delay=5, fail_func=_refresh, message="wait for vm to appear") if load_details: self.load_details() def set_ownership(self, user=None, group=None, click_cancel=False, click_reset=False): """Set instance ownership Args: user (str): username for ownership group (str): groupname for ownership click_cancel (bool): Whether to cancel form submission click_reset (bool): Whether to reset form after filling """ view = navigate_to(self, 'SetOwnership') fill_result = view.form.fill({'user_name': user, 'group_name': group}) if not fill_result: view.form.cancel_button.click() view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message( 'Set Ownership was cancelled by the user') return # Only if the form changed if click_reset: view.form.reset_button.click() view.flash.assert_message('All changes have been reset', 'warning') # Cancel after reset assert view.form.is_displayed view.form.cancel_button.click() elif click_cancel: view.form.cancel_button.click() view.flash.assert_success_message( 'Set Ownership was cancelled by the user') else: # save the form view.form.save_button.click() view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message( 'Ownership saved for selected {}'.format(self.VM_TYPE)) def unset_ownership(self): """Remove user ownership and return group to EvmGroup-Administrator""" view = navigate_to(self, 'SetOwnership') fill_result = view.form.fill({ 'user_name': '<No Owner>', 'group_name': 'EvmGroup-administrator' }) if fill_result: view.form.save_button.click() msg = 'Ownership saved for selected {}'.format(self.VM_TYPE) else: view.form.cancel_button.click() logger.warning('No change during unset_ownership') msg = 'Set Ownership was cancelled by the user' view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message(msg)
class BaseAlertProfile(Updateable, Navigatable, Pretty): TYPE = None _param_name = ParamClassName('description') pretty_attrs = ["description", "alerts"] def __init__(self, description, alerts=None, notes=None, appliance=None): Navigatable.__init__(self, appliance=appliance) self.description = description self.notes = notes self.alerts = alerts def create(self): view = navigate_to(self, "Add") view.fill({ "description": self.description, "notes": self.notes, "alerts": [str(alert) for alert in self.alerts] }) view.add_button.click() view = self.create_view(AlertProfileDetailsView) assert view.is_displayed view.flash.assert_no_error() view.flash.assert_message('Alert Profile "{}" was added'.format( self.description)) def update(self, updates): """Update this Alert Profile in UI. Args: updates: Provided by update() context manager. cancel: Whether to cancel the update (default False). """ view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() for attr, value in updates.items(): setattr(self, attr, value) view = self.create_view(AlertProfileDetailsView) assert view.is_displayed view.flash.assert_no_error() if changed: view.flash.assert_message('Alert Profile "{}" was saved'.format( updates.get("description", self.description))) else: view.flash.assert_message( 'Edit of Alert Profile "{}" was cancelled by the user'.format( self.description)) def delete(self, cancel=False): """Delete this Alert Profile in UI. Args: cancel: Whether to cancel the deletion (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Delete this Alert Profile", handle_alert=not cancel) if cancel: assert view.is_displayed view.flash.assert_no_error() else: view = self.create_view(AlertProfilesAllView) assert view.is_displayed view.flash.assert_no_error() view.flash.assert_message( 'Alert Profile "{}": Delete successful'.format( self.description)) @property def exists(self): """Check existence of this Alert Profile. Returns: :py:class:`bool` signalizing the presence of the Alert Profile in database. """ miq_sets = self.appliance.db.client["miq_sets"] return self.appliance.db.client.session\ .query(miq_sets.description)\ .filter( miq_sets.description == self.description and miq_sets.set_type == "MiqAlertSet")\ .count() > 0 def assign_to(self, assign, selections=None, tag_category=None): """Assigns this Alert Profile to specified objects. Args: assign: Where to assign (The Enterprise, ...). selections: What items to check in the tree. N/A for The Enteprise. tag_category: Only for choices starting with Tagged. N/A for The Enterprise. """ view = navigate_to(self, "Edit assignments") changed = view.fill({ "assign_to": assign, "tag_category": tag_category, "selections": selections }) if changed: view.save_button.click() else: view.cancel_button.click() view = self.create_view(AlertProfileDetailsView) assert view.is_displayed view.flash.assert_no_error() if changed: view.flash.assert_message( 'Alert Profile "{}" assignments {} saved'.format( self.description, version.pick({ version.LOWEST: "succesfully", "5.8": "successfully", }))) else: view.flash.assert_message( 'Edit of Alert Profile "{}" was cancelled by the user'.format( self.description))
class Server(BaseEntity, sentaku.modeling.ElementMixin): _param_name = ParamClassName('name') sid = attr.ib(default=1) address = sentaku.ContextualMethod() login = sentaku.ContextualMethod() login_admin = sentaku.ContextualMethod() logout = sentaku.ContextualMethod() update_password = sentaku.ContextualMethod() logged_in = sentaku.ContextualMethod() current_full_name = sentaku.ContextualMethod() current_username = sentaku.ContextualMethod() current_group_name = sentaku.ContextualMethod() group_names = sentaku.ContextualMethod() intel_name = VersionPicker({ "5.11": "Overview", Version.lowest(): "Cloud Intel" }) # zone = sentaku.ContextualProperty() # slave_servers = sentaku.ContextualProperty() @property def name(self): """Fetch the name from the master server api entity Returns: string if entity has the name attribute None if its missing """ # empty string default for string building w/o None return getattr(self.appliance._rest_api_server(), 'name', '') @property def settings(self): from cfme.configure.configuration.server_settings import ServerInformation setting = ServerInformation(appliance=self.appliance) return setting @property def authentication(self): from cfme.configure.configuration.server_settings import AuthenticationSetting auth_settings = AuthenticationSetting(self.appliance) return auth_settings @property def collect_logs(self): from cfme.configure.configuration.diagnostics_settings import ServerCollectLog return ServerCollectLog(self.appliance) @property def zone(self): server_res = self.appliance.rest_api.collections.servers.find_by( id=self.sid) server, = server_res server.reload(attributes=['zone']) zone = server.zone zone_obj = self.appliance.collections.zones.instantiate( name=zone['name'], description=zone['description'], id=zone['id']) return zone_obj @property def slave_servers(self): return self.zone.collections.servers.filter({'slave': True}).all() @property def _api_settings_url(self): return '/'.join([ self.appliance.rest_api.collections.servers._href, str(self.sid), 'settings' ]) @property def advanced_settings(self): """GET servers/:id/settings api endpoint to query server configuration""" return self.appliance.rest_api.get(self._api_settings_url) def update_advanced_settings(self, settings_dict): """PATCH settings from the server's api/server/:id/settings endpoint Args: settings_dict: dictionary of the changes to be made to the yaml configuration JSON dumps settings_dict to pass as raw hash data to rest_api session Raises: AssertionError: On an http result >=400 (RequestsResponse.ok) """ # Calling the _session patch method because the core patch will wrap settings_dict in a list # Failing with some settings_dict, like 'authentication' # https://bugzilla.redhat.com/show_bug.cgi?id=1553394 result = self.appliance.rest_api._session.patch( url=self._api_settings_url, data=json.dumps(settings_dict)) assert result.ok def upload_custom_logo(self, file_type, file_data=None, enable=True): """ This function can be used to upload custom logo or text and use them. Args: file_type (str) : Can be either of [logo, login_logo, brand, favicon, logintext] file_data (str) : Text data if file_type is logintext else image path to be uploaded enable (bool) : True to use the custom logo/text else False """ view = navigate_to(self, "CustomLogos") try: logo_view = getattr(view.customlogos, file_type) except AttributeError: raise AttributeError( "File type not in ('logo', 'login_logo', 'brand', 'favicon', 'logintext)." ) if file_data: if file_type == "logintext": logo_view.fill({"login_text": file_data}) else: logo_view.fill({"image": file_data}) logo_view.upload_button.click() view.flash.assert_no_error() logo_view.enable.fill(enable) view.customlogos.save_button.click() view.flash.assert_no_error()
class ComputeRate(Updateable, Pretty, Navigatable): """This class represents a Compute Chargeback rate. Example: .. code-block:: python >>> import cfme.intelligence.chargeback.rates as rates >>> rate = rates.ComputeRate(description=desc, fields={'Used CPU': {'per_time': 'Hourly', 'variable_rate': '3'}, 'Used Disk I/O': {'per_time': 'Hourly', 'variable_rate': '2'}, 'Used Memory': {'per_time': 'Hourly', 'variable_rate': '2'}}) >>> rate.create() >>> rate.delete() Args: description: Rate description currency: Rate currency fields : Rate fields """ pretty_attrs = ['description'] _param_name = ParamClassName('description') def __init__( self, description=None, currency=None, fields=None, appliance=None, ): Navigatable.__init__(self, appliance=appliance) self.description = description self.currency = currency self.fields = fields def __getitem__(self, name): return self.fields.get(name) def create(self): # Create a rate in UI view = navigate_to(self, 'New') view.fill_with( { 'description': self.description, 'currency': self.currency, 'fields': self.fields }, on_change=view.add_button, no_change=view.cancel_button) view.flash.assert_success_message( 'Chargeback Rate "{}" was added'.format(self.description)) def copy(self, *args, **kwargs): new_rate = ComputeRate(*args, **kwargs) add_view = navigate_to(self, 'Copy') add_view.fill_with( { 'description': new_rate.description, 'currency': new_rate.currency, 'fields': new_rate.fields }, on_change=add_view.add_button, no_change=add_view.cancel_button) return new_rate def update(self, updates): # Update a rate in UI view = navigate_to(self, 'Edit') view.fill_with(updates, on_change=view.save_button, no_change=view.cancel_button) view.flash.assert_success_message( 'Chargeback Rate "{}" was saved'.format( updates.get('description'))) def delete(self): # Delete a rate in UI view = navigate_to(self, 'Details') view.configuration.item_select('Remove from the VMDB', handle_alert=True) view.flash.assert_success_message( 'Chargeback Rate "{}": Delete successful'.format(self.description))
class BaseVM( BaseEntity, Pretty, Updateable, PolicyProfileAssignable, Taggable, ConsoleMixin, CustomButtonEventsMixin, ): """Base VM and Template class that holds the largest common functionality between VMs, instances, templates and images. In order to inherit these, you have to implement the ``on_details`` method. """ pretty_attrs = ['name', 'provider', 'template_name'] ### # To be set or implemented # ALL_LIST_LOCATION = None TO_OPEN_EDIT = None # Name of the item in Configuration that puts you in the form QUADICON_TYPE = "vm" # Titles of the delete buttons in configuration REMOVE_SELECTED = 'Remove selected items from Inventory' REMOVE_SINGLE = 'Remove Virtual Machine from Inventory' RETIRE_DATE_FMT = '%a, %d %b %Y %H:%M:%S +0000' RETIRE_DATE_MSG_FMT = '%m/%d/%y %H:%M UTC' _param_name = ParamClassName('name') DETAILS_VIEW_CLASS = None ### # Shared behaviour # PROVISION_CANCEL = 'Add of new VM Provision Request was cancelled by the user' PROVISION_START = ( 'VM Provision Request was Submitted, you will be notified when your VMs ' 'are ready') name = attr.ib() provider = attr.ib() def __new__(cls, *args, **kwargs): if cls in [BaseVM, VM, Template]: raise NotImplementedError('This class cannot be instantiated.') else: # magic {waves hands} return object.__new__(cls) ### # Properties # @property def is_vm(self): return not isinstance(self, _TemplateMixin) @property def quadicon_type(self): return self.QUADICON_TYPE ### # Methods # def check_compliance(self, timeout=240): """Initiates compliance check and waits for it to finish.""" view = navigate_to(self, "Details") original_state = self.compliance_status view.toolbar.policy.item_select( "Check Compliance of Last Known Configuration", handle_alert=True) view.flash.assert_no_error() wait_for(lambda: self.compliance_status != original_state, num_sec=timeout, delay=5, message=f"compliance of {self.name} checked") @property def compliance_status(self): """Returns the title of the compliance SummaryTable. The title contains datetime so it can be compared. Returns: :py:class:`NoneType` if no title is present (no compliance checks before), otherwise str """ view = navigate_to(self, "Details") view.toolbar.reload.click() return view.entities.summary("Compliance").get_text_of("Status") @property def compliant(self): """Check if the VM is compliant. Returns: :py:class:`bool` """ text = self.compliance_status.strip().lower() if text.startswith("non-compliant"): return False elif text.startswith("compliant"): return True else: raise ValueError(f"{text} is not a known state for compliance") def delete(self, cancel=False, from_details=False): """Deletes the VM/Instance from the VMDB. Args: cancel: Whether to cancel the action in the alert. from_details: Whether to use the details view or list view. """ if from_details: view = navigate_to(self, 'Details') view.toolbar.configuration.item_select(self.REMOVE_SINGLE, handle_alert=not cancel) else: view = navigate_to(self.parent, 'All') self.find_quadicon().ensure_checked() view.toolbar.configuration.item_select(self.REMOVE_SELECTED, handle_alert=not cancel) def _fill_clone_form(self, view, email=None, first_name=None, last_name=None, new_name=None, provision_type=None): first_name = first_name or fauxfactory.gen_alphanumeric() last_name = last_name or fauxfactory.gen_alphanumeric() email = email or f"{first_name}@{last_name}.test" try: prov_data = cfme_data["management_systems"][ self.provider.key]["provisioning"] except (KeyError, IndexError): raise ValueError( "You have to specify the correct options in cfme_data.yaml") provisioning_data = { 'catalog': { 'vm_name': new_name, 'provision_type': provision_type }, 'request': { 'email': email, 'first_name': first_name, 'last_name': last_name }, 'environment': { "host_name": { 'name': prov_data.get("host") }, "datastore_name": { "name": prov_data.get("datastore") } }, 'network': { 'vlan': partial_match(prov_data.get("vlan")) }, } view.form.fill_with(provisioning_data, on_change=view.form.submit_button) @property def ip_address(self): """Fetches IP Address of VM First looks to see if any of the mgmt ips returned by 'all_ips' are pingable Then defaults to whatever mgmt.ip returns """ return find_pingable(self.mgmt) @property def all_ip_addresses(self): """Fetches all IP Addresses of a VM, pingable or otherwise.""" # TODO: Implement sentaku for this property with ViaMGMT impl view = navigate_to(self, "Details", use_resetter=False) try: return view.entities.summary('Properties').get_text_of( "IP Address") except NameError: # since some providers have plural 'Addresses'. return view.entities.summary('Properties').get_text_of( "IP Addresses").split(", ") @property def mac_address(self): """Fetches MAC Address of VM""" # TODO: We should update this with wrapanapi method when it becomes available. view = navigate_to(self, "Details", use_resetter=False) try: return view.entities.summary('Properties').get_text_of( "MAC Address") except NameError: # since some providers have plural 'Addresses'. return view.entities.summary('Properties').get_text_of( "MAC Addresses") @property def is_retired(self): """Check retirement status of vm""" view = navigate_to(self, "Details", use_resetter=False) if view.entities.summary('Lifecycle').get_text_of( 'Retirement Date') != 'Never': try: status = view.entities.summary('Lifecycle').get_text_of( 'Retirement State') return status == 'Retired' except NameError: return False else: return False def find_quadicon(self, from_any_provider=False, from_archived_all=False, from_orphaned_all=False, use_search=True): """Find and return a quadicon belonging to a specific vm Args: from_any_provider: Whether to look for it anywhere (root of the tree). Useful when looking up archived or orphaned VMs Returns: entity of appropriate type Raises: ItemNotFound """ # TODO(all): Refactor this method replace it with vm methods like get_state if from_any_provider: view = navigate_to(self.parent, 'All') elif from_archived_all: view = navigate_to( self.appliance.provider_based_collection(self.provider), 'ArchivedAll') elif from_orphaned_all: view = navigate_to( self.appliance.provider_based_collection(self.provider), 'OrphanedAll') else: view = navigate_to(self, 'AllForProvider', use_resetter=False) view.toolbar.view_selector.select('Grid View') try: return view.entities.get_entity(name=self.name, surf_pages=True, use_search=use_search) except ItemNotFound: raise ItemNotFound(f"VM '{self.name}' not found in UI!") def open_console(self, console='VM Console', invokes_alert=None): """ Initiates the opening of one of the console types supported by the Access button. Presently we only support VM Console, which is the HTML5 Console. In case of VMware provider it could be VMRC, VNC/HTML5, WebMKS, but we only support VNC/HTML5. Possible values for 'console' could be 'VM Console' and 'Web Console', but Web Console is not supported as well. Args: console: one of the supported console types given by the Access button. invokes_alert: If the particular console will invoke a CFME popup/alert setting this to true will handle this. """ if console not in ['VM Console', 'VMRC Console']: raise NotImplementedError(f'Not supported console type: {console}') view = navigate_to(self, 'Details') # dismiss_any_alerts() call closed subsequent alerts needed for vmrc # below code is needed to fix such issue try: view.browser.IGNORE_SUBSEQUENT_ALERTS = True # Click console button given by type view.toolbar.access.item_select(console, handle_alert=invokes_alert) finally: view.browser.IGNORE_SUBSEQUENT_ALERTS = False self.vm_console def open_details(self, properties=None): """Clicks on details infoblock""" view = navigate_to(self, 'Details') view.entities.summary(properties[0]).click_at(properties[1]) return self.create_view(VMPropertyDetailView) @property def last_analysed(self): """Returns the contents of the ``Last Analysed`` field in summary""" view = navigate_to(self, "Details") view.toolbar.reload.click() return view.entities.summary("Lifecycle").get_text_of( "Last Analyzed").strip() def load_details(self, refresh=False, from_any_provider=False): """Navigates to an VM's details page. Args: refresh: Refreshes the VM page if already there from_any_provider: Archived/Orphaned VMs need this """ if from_any_provider: view = navigate_to(self, 'AnyProviderDetails', use_resetter=False) else: view = navigate_to(self, 'Details', use_resetter=False) if refresh: view.toolbar.reload.click() view.wait_displayed() return view def open_edit(self): """Loads up the edit page of the object.""" return navigate_to(self, 'Edit') def open_timelines(self): """Navigates to an VM's timeline page. Returns: :py:class:`TimelinesView` object """ return navigate_to(self, 'Timelines') def rediscover(self): """Deletes the VM from the provider and lets it discover again""" self.delete(from_details=True) self.wait_for_delete() self.provider.refresh_provider_relationships() self.wait_to_appear() def rediscover_if_analysis_data_present(self): """Rediscovers the object if it has some analysis data present. Returns: Boolean if the rediscovery happened. """ if self.last_analysed.lower() != 'never': self.rediscover() return True return False def refresh_relationships(self, from_details=False, cancel=False, from_any_provider=False): """Executes a refresh of relationships. Args: from_details: Whether or not to perform action from instance details page cancel: Whether or not to cancel the refresh relationships action """ if from_details: view = navigate_to(self, 'Details', use_resetter=False) else: view = navigate_to(self.parent, 'All') self.find_quadicon( from_any_provider=from_any_provider).ensure_checked() view.toolbar.configuration.item_select( "Refresh Relationships and Power States", handle_alert=not cancel) @property def retirement_date(self): """Returns the retirement date of the selected machine, or 'Never' Returns: :py:class:`str` object """ view = navigate_to(self, "Details") return view.entities.summary("Lifecycle").get_text_of( "Retirement Date").strip() def smartstate_scan(self, cancel=False, from_details=False, wait_for_task_result=False): """Initiates fleecing from the UI. Args: cancel: Whether or not to cancel the refresh relationships action from_details: Whether or not to perform action from instance details page """ if from_details: view = navigate_to(self, 'Details', use_resetter=False) else: view = navigate_to(self.parent, 'All') self.find_quadicon().ensure_checked() view.toolbar.configuration.item_select('Perform SmartState Analysis', handle_alert=not cancel) if wait_for_task_result: task = self.appliance.collections.tasks.instantiate( name=f'Scan from Vm {self.name}', tab='AllTasks') task.wait_for_finished() return task def wait_to_disappear(self, timeout=600): """Wait for a VM to disappear within CFME Args: timeout: time (in seconds) to wait for it to appear """ wait_for(lambda: self.exists, num_sec=timeout, delay=5, fail_func=self.browser.refresh, fail_condition=True, message="wait for vm to not exist") wait_for_delete = wait_to_disappear # An alias for more fitting verbosity def wait_to_appear(self, timeout=600, load_details=True): """Wait for a VM to appear within CFME Args: timeout: time (in seconds) to wait for it to appear load_details: when found, should it load the vm details """ def _refresh(): self.appliance.browser.widgetastic.browser.refresh( ) # strange because ViaUI self.provider.refresh_provider_relationships(wait=600) wait_for(lambda: self.exists, num_sec=timeout, delay=5, fail_func=_refresh, message="wait for vm to appear") if load_details: navigate_to(self, "Details", use_resetter=False) def set_ownership(self, user=None, group=None, cancel=False, reset=False): """Set instance ownership Args: user (User): user object for ownership group (Group): group object for ownership cancel (bool): Whether to cancel form submission reset (bool): Whether to reset form after filling """ view = navigate_to(self, 'SetOwnership', wait_for_view=0) fill_result = view.form.fill({ 'user_name': user.name if user else None, 'group_name': group.description if group else group }) if not fill_result: view.form.cancel_button.click() view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message( 'Set Ownership was cancelled by the user') return # Only if the form changed if reset: view.form.reset_button.click() view.flash.assert_message('All changes have been reset', 'warning') # Cancel after reset assert view.form.is_displayed view.form.cancel_button.click() elif cancel: view.form.cancel_button.click() view.flash.assert_success_message( 'Set Ownership was cancelled by the user') else: # save the form view.form.save_button.click() view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message( 'Ownership saved for selected {}'.format(self.VM_TYPE)) def unset_ownership(self): """Remove user ownership and return group to EvmGroup-Administrator""" view = navigate_to(self, 'SetOwnership', wait_for_view=0) fill_result = view.form.fill({ 'user_name': '<No Owner>', 'group_name': 'EvmGroup-administrator' }) if fill_result: view.form.save_button.click() msg = f'Ownership saved for selected {self.VM_TYPE}' else: view.form.cancel_button.click() logger.warning('No change during unset_ownership') msg = 'Set Ownership was cancelled by the user' view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_success_message(msg) def rename(self, new_vm_name, cancel=False, reset=False): """Rename the VM Args: new_vm_name: object for renaming vm cancel (bool): Whether to cancel form submission reset (bool): Whether to reset form after filling """ view = navigate_to(self, 'Rename') changed = view.vm_name.fill(new_vm_name) if changed: if reset: view.reset_button.click() view.flash.assert_no_error() view.cancel_button.click() else: # save the form view.save_button.click() view.flash.assert_no_error() self.name = new_vm_name return self if cancel: view.cancel_button.click() view.flash.assert_no_error() @property def rest_api_entity(self): # This will not work if the VM has retired since we filter using provider collection = "instances" if self.VM_TYPE == "Instance" else "vms" try: return (getattr( self.appliance.rest_api.collections, collection).filter( Q("name", "=", self.name) & Q("ems_id", "=", self.provider.rest_api_entity.id)). resources[0]) except IndexError: raise RestLookupError( f"No {self.VM_TYPE} rest entity found matching name {self.name}" ) def wait_for_power_state_change_rest(self, desired_state, timeout=1200, delay=45): """Wait for a VM/Instance power state to change to a desired state. Args: desired_state: A string indicating the desired state timeout: Specify amount of time (in seconds) to wait until TimedOutError is raised TODO: Change this to use Sentaku """ return wait_for( lambda: self.rest_api_entity.power_state == desired_state, fail_func=self.rest_api_entity.reload, num_sec=timeout, delay=delay, handle_exception=True, message= f"Waiting for VM/Instance power state to change to {desired_state}" ).out
class BasePolicy(Updateable, Navigatable, Pretty): """This class represents a Policy. Example: .. code-block:: python >>> from cfme.control.explorer.policy import VMCompliancePolicy >>> policy = VMCompliancePolicy("policy_description") >>> policy.create() >>> policy.delete() Args: description: Policy name. active: Whether the policy active or not. scope: Policy scope. notes: Policy notes. """ TYPE = None TREE_NODE = None PRETTY = None _param_name = ParamClassName('description') def __init__(self, description, active=True, scope=None, notes=None, appliance=None): Navigatable.__init__(self, appliance=appliance) self.description = description self.active = active self.scope = scope self.notes = notes def __str__(self): return self.description def create(self): "Create this Policy in UI." view = navigate_to(self, "Add") view.fill({ "description": self.description, "active": self.active, "scope": self.scope, "notes": self.notes }) view.add_button.click() view = self.create_view(PolicyDetailsView) assert view.is_displayed view.flash.assert_no_error() view.flash.assert_message('Policy "{}" was added'.format( self.description)) def update(self, updates): """Update this Policy in UI. Args: updates: Provided by update() context manager. cancel: Whether to cancel the update (default False). """ view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() for attr, value in updates.items(): setattr(self, attr, value) view = self.create_view(PolicyDetailsView) assert view.is_displayed view.flash.assert_no_error() if changed: view.flash.assert_message('Policy "{}" was saved'.format( updates.get("description", self.description))) else: view.flash.assert_message( 'Edit of Policy "{}" was cancelled by the user'.format( self.description)) def delete(self, cancel=False): """Delete this Policy in UI. Args: cancel: Whether to cancel the deletion (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Delete this {} Policy".format( self.PRETTY), handle_alert=not cancel) if cancel: assert view.is_displayed view.flash.assert_no_error() else: view = self.create_view(PoliciesAllView) assert view.is_displayed view.flash.assert_no_error() def copy(self, cancel=False): """Copy this Policy in UI. Args: cancel: Whether to cancel the copying (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Copy this {} Policy".format( self.PRETTY), handle_alert=not cancel) view = self.create_view(PolicyDetailsView) assert view.is_displayed view.flash.assert_no_error() view.flash.assert_message('Policy "Copy of {}" was added'.format( self.description)) return type(self)("Copy of {}".format(self.description)) def assign_events(self, *events, **kwargs): """Assign events to this Policy. Args: events: Events which need to be assigned. extend: Do not uncheck existing events. """ events = list(events) extend = kwargs.pop("extend", False) if extend: events += self.assigned_events view = navigate_to(self, "Details") view.configuration.item_select("Edit this Policy's Event assignments") view = self.create_view(EditPolicyEventAssignments) assert view.is_displayed changed = view.fill({"events": events}) if changed: view.save_button.click() else: view.cancel_button.click() view.flash.assert_no_error() view.flash.assert_message('Policy "{}" was saved'.format( self.description)) def is_event_assigned(self, event): return event in self.assigned_events def assign_conditions(self, *conditions): """Assign conditions to this Policy. Args: conditions: Conditions which need to be assigned. """ view = navigate_to(self, "Details") view.configuration.item_select( "Edit this Policy's Condition assignments") view = self.create_view(EditPolicyConditionAssignments) assert view.is_displayed changed = view.fill({ "conditions": [condition.description for condition in conditions] }) if changed: view.save_button.click() else: view.cancel_button.click() view.flash.assert_no_error() view.flash.assert_message('Policy "{}" was saved'.format( self.description)) def is_condition_assigned(self, condition): self.testing_condition = condition view = navigate_to(self, "Condition Details") return view.is_displayed def assign_actions_to_event(self, event, actions): """ This method takes a list or dict of actions, goes into the policy event and assigns them. Actions can be passed both as the objects, but they can be passed also as a string. Actions, passed as an object but not created yet, will be created. If the specified event is not assigned to the policy, it will be assigned. Args: event: Name of the event under which the actions will be assigned. actions: If :py:class:`list` (or similar), all of these actions will be set under TRUE section. If :py:class:`dict`, the action is key and value specifies its placement. If it's True, then it will be put in the TRUE section and so on. """ true_actions, false_actions = [], [] if isinstance(actions, Action): true_actions.append(actions) elif isinstance(actions, list) or isinstance( actions, tuple) or isinstance(actions, set): true_actions.extend(actions) elif isinstance(actions, dict): for action, is_true in actions.iteritems(): if is_true: true_actions.append(action) else: false_actions.append(action) else: raise TypeError( "assign_actions_to_event expects, list, tuple, set or dict!") # Check whether actions exist for action in true_actions + false_actions: if isinstance(action, Action): if not action.exists: action.create() assert action.exists, "Could not create action {}!".format( action.description) else: # string if not Action(action, "Tag").exists: raise NameError( "Action with name {} does not exist!".format(action)) # Check whether we have all necessary events assigned if not self.is_event_assigned(event): self.assign_events(event, extend=True) assert self.is_event_assigned( event), "Could not assign event {}!".format(event) # And now we can assign actions self.testing_event = event view = navigate_to(self, "Event Details") assert view.is_displayed view.toolbar.configuration.item_select( "Edit Actions for this Policy Event") view = self.create_view(EditEventView) assert view.is_displayed changed = view.fill({ "true_actions": [str(action) for action in true_actions], "false_actions": [str(action) for action in false_actions] }) if changed: view.save_button.click() else: view.cancel_button.click() view.flash.assert_no_error() view.flash.assert_message( 'Actions for Policy Event "{}" were saved'.format(event)) @property def exists(self): policies = self.appliance.db.client["miq_policies"] return self.appliance.db.client.session\ .query(policies.description)\ .filter(policies.description == self.description)\ .count() > 0 @property def assigned_events(self): policies = self.appliance.db.client["miq_policies"] events = self.appliance.db.client["miq_event_definitions"] policy_contents = self.appliance.db.client["miq_policy_contents"] session = self.appliance.db.client.session policy_id = session.query( policies.id).filter(policies.description == self.description) assigned_events = session.query( policy_contents.miq_event_definition_id).filter( policy_contents.miq_policy_id == policy_id) return [ event_name[0] for event_name in session.query(events.description).filter( events.id.in_(assigned_events)) ]
class BaseCondition(BaseEntity, Updateable, Pretty): TREE_NODE = None PRETTY = None FIELD_VALUE = None _param_name = ParamClassName('description') def __init__(self, collection, description, expression=None, scope=None, notes=None): self.collection = collection self.appliance = self.collection.appliance self.description = description self.expression = expression self.scope = scope self.notes = notes def update(self, updates): """Update this Condition in UI. Args: updates: Provided by update() context manager. cancel: Whether to cancel the update (default False). """ view = navigate_to(self, "Edit") view.fill(updates) view.save_button.click() view = self.create_view(ConditionDetailsView, override=updates) assert view.is_displayed view.flash.assert_success_message('Condition "{}" was saved'.format( updates.get("description", self.description))) def delete(self, cancel=False): """Delete this Condition in UI. Args: cancel: Whether to cancel the deletion (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Delete this {} Condition".format( self.FIELD_VALUE), handle_alert=not cancel) if cancel: assert view.is_displayed view.flash.assert_no_error() else: view = self.create_view(ConditionsAllView) assert view.is_displayed view.flash.assert_success_message( 'Condition "{}": Delete successful'.format(self.description)) def read_expression(self): view = navigate_to(self, "Details") assert view.is_displayed return view.expression.text def read_scope(self): view = navigate_to(self, "Details") assert view.is_displayed return view.scope.text @property def exists(self): """Check existence of this Condition. Returns: :py:class:`bool` signalizing the presence of the Condition in the database. """ conditions = self.appliance.db.client["conditions"] return self.appliance.db.client.session\ .query(conditions.description)\ .filter(conditions.description == self.description)\ .count() > 0
class BaseVM(Pretty, Updateable, PolicyProfileAssignable, Taggable, SummaryMixin, Navigatable): """Base VM and Template class that holds the largest common functionality between VMs, instances, templates and images. In order to inherit these, you have to implement the ``on_details`` method. """ pretty_attrs = ['name', 'provider', 'template_name'] # Forms edit_form = Form(fields=[ ('custom_ident', Input("custom_1")), ('description_tarea', "//textarea[@id='description']"), ('parent_sel', { version.LOWEST: Select("//select[@name='chosen_parent']"), "5.5": AngularSelect("chosen_parent") }), ('child_sel', Select("//select[@id='kids_chosen']", multi=True)), ('vm_sel', Select("//select[@id='choices_chosen']", multi=True)), ('add_btn', "//img[@alt='Move selected VMs to left']"), ('remove_btn', "//img[@alt='Move selected VMs to right']"), ('remove_all_btn', "//img[@alt='Move all VMs to right']"), ]) ### # Factory class methods # @classmethod def factory(cls, vm_name, provider, template_name=None, template=False): """Factory class method that determines the correct subclass for given provider. For reference how does that work, refer to the entrypoints in the setup.py Args: vm_name: Name of the VM/Instance as it appears in the UI provider: The provider object (not the string!) template_name: Source template name. Useful when the VM/Instance does not exist and you want to create it. template: Whether the generated object class should be VM/Instance or a template class. """ try: return all_types(template)[provider.type](vm_name, provider, template_name) except KeyError: # Matching via provider type failed. Maybe we have some generic classes for infra/cloud? try: return all_types(template)[provider.category](vm_name, provider, template_name) except KeyError: raise UnknownProviderType( 'Unknown type of provider CRUD object: {}'.format( provider.__class__.__name__)) ### # To be set or implemented # ALL_LIST_LOCATION = None TO_OPEN_EDIT = None # Name of the item in Configuration that puts you in the form QUADICON_TYPE = "vm" # Titles of the delete buttons in configuration REMOVE_SELECTED = { '5.6': 'Remove selected items', '5.6.2.2': 'Remove selected items from the VMDB', '5.7': 'Remove selected items' } REMOVE_SINGLE = { '5.6': 'Remove Virtual Machine', '5.6.2.2': 'Remove from the VMDB', '5.7': 'Remove Virtual Machine' } RETIRE_DATE_FMT = { version.LOWEST: parsetime.american_date_only_format, '5.7': parsetime.american_minutes_with_utc } _param_name = ParamClassName('name') ### # Shared behaviour # def __init__(self, name, provider, template_name=None, appliance=None): super(BaseVM, self).__init__() Navigatable.__init__(self, appliance=appliance) if type(self) in {BaseVM, VM, Template}: raise NotImplementedError('This class cannot be instantiated.') self.name = name self.provider = provider self.template_name = template_name ### # Properties # @property def is_vm(self): return not isinstance(self, _TemplateMixin) @property def quadicon_type(self): return self.QUADICON_TYPE @property def paged_table(self): _paged_table_template = '//div[@id="list_grid"]/div[@class="{}"]/table/tbody' return version.pick({ version.LOWEST: SplitPagedTable(header_data=(_paged_table_template.format("xhdr"), 1), body_data=(_paged_table_template.format("objbox"), 0)), "5.5": PagedTable('//table'), }) ### # Methods # def check_compliance(self, timeout=240): """Initiates compliance check and waits for it to finish. TODO This should be refactored as it's done `Host.check_compliance`. It shouldn't return anything. `compliant` property should use `compliance_status`. """ original_state = self.compliance_status cfg_btn("Refresh Relationships and Power States", invokes_alert=True) sel.handle_alert() flash.assert_no_errors() pol_btn("Check Compliance of Last Known Configuration", invokes_alert=True) sel.handle_alert() flash.assert_no_errors() wait_for(lambda: self.compliance_status != original_state, num_sec=timeout, delay=5, message="compliance of {} checked".format(self.name)) return self.compliant @property def compliance_status(self): """Returns the title of the compliance infoblock. The title contains datetime so it can be compared. Returns: :py:class:`NoneType` if no title is present (no compliance checks before), otherwise str """ self.load_details(refresh=True) return InfoBlock("Compliance", "Status").title @property def compliant(self): """Check if the VM is compliant Returns: :py:class:`NoneType` if the VM was never verified, otherwise :py:class:`bool` """ text = self.get_detail(properties=("Compliance", "Status")).strip().lower() if text == "never verified": return None elif text.startswith("non-compliant"): return False elif text.startswith("compliant"): return True else: raise ValueError( "{} is not a known state for compliance".format(text)) @property def console_handle(self): ''' The basic algorithm for getting the consoles window handle is to get the appliances window handle and then iterate through the window_handles till we find one that is not the appliances window handle. Once we find this check that it has a canvas widget with a specific ID ''' browser = self.appliance.browser.widgetastic appliance_handle = browser.window_handle cur_handles = browser.selenium.window_handles logger.info("Current Window Handles: {}".format(cur_handles)) for handle in cur_handles: if handle != appliance_handle: # FIXME: Add code to verify the tab has the correct widget # for a console tab. return handle def delete(self, cancel=False, from_details=False): """Deletes the VM/Instance from the VMDB. Args: cancel: Whether to cancel the action in the alert. from_details: Whether to use the details view or list view. """ if from_details: self.load_details(refresh=True) cfg_btn(self.REMOVE_SINGLE, invokes_alert=True) else: self.find_quadicon().check() cfg_btn(self.REMOVE_SELECTED, invokes_alert=True) sel.handle_alert(cancel=cancel) @property def exists(self): """Checks presence of the quadicon in the CFME.""" try: self.find_quadicon() return True except VmOrInstanceNotFound: return False @property def ip_address(self): """Fetches IP Address of VM""" return self.provider.mgmt.get_ip_address(self.name) @property def is_retired(self): """"Check retirement status of vm""" self.summary.reload() if self.summary.lifecycle.retirement_date.text_value.lower( ) != 'never': try: return self.summary.lifecycle.retirement_state.text_value.lower( ) == 'retired' except AttributeError: return False else: return False def find_quadicon(self, from_any_provider=False, use_search=True): """Find and return a quadicon belonging to a specific vm Args: from_any_provider: Whether to look for it anywhere (root of the tree). Useful when looking up archived or orphaned VMs Returns: entity of appropriate type Raises: VmOrInstanceNotFound """ # todo :refactor this method replace it with vm methods like get_state if from_any_provider: view = navigate_to(self, 'All') else: view = navigate_to(self, 'AllForProvider', use_resetter=False) if 'Grid View' != view.toolbar.view_selector.selected: view.toolbar.view_selector.select('Grid View') if use_search: search.normal_search(self.name) try: return view.entities.get_entity(by_name=self.name, surf_pages=True) except ItemNotFound: raise VmOrInstanceNotFound("VM '{}' not found in UI!".format( self.name)) def get_detail(self, properties=None, icon_href=False): """Gets details from the details infoblock The function first ensures that we are on the detail page for the specific VM/Instance. Args: properties: An InfoBlock title, followed by the Key name, e.g. "Relationships", "Images" Returns: A string representing the contents of the InfoBlock's value. """ self.load_details(refresh=True) if icon_href: return InfoBlock.icon_href(*properties) else: return InfoBlock.text(*properties) def open_console(self, console='VM Console', invokes_alert=False, cancel=False): """ Initiates the opening of one of the console types supported by the Access button. Presently we only support VM Console, which is the HTML5 Console. In case of VMware provider it could be VMRC, VNC/HTML5, WebMKS, but we only support VNC/HTML5. Possible values for 'console' could be 'VM Console' and 'Web Console', but Web Console is not supported as well. Args: console: one of the supported console types given by the Access button. invokes_alert: If the particular console will invoke a CFME popup/alert setting this to true will handle this. cancel: Allows one to cancel the operation if the popup/alert occurs. """ # TODO: implement vmrc vm console if console not in ['VM Console']: raise NotImplementedError( 'Not supported console type: {}'.format(console)) view = navigate_to(self, 'Details') # Click console button given by type view.toolbar.access.item_select( console, handle_alert=None if invokes_alert is False else True) # Get the consoles window handle, and then create a VMConsole object, and store # the VMConsole object aside. console_handle = self.console_handle if console_handle is None: raise TypeError("Console handle should not be None") appliance_handle = self.appliance.browser.widgetastic.window_handle logger.info("Creating VMConsole:") logger.info(" appliance_handle: {}".format(appliance_handle)) logger.info(" console_handle: {}".format(console_handle)) logger.info(" name: {}".format(self.name)) self.vm_console = VMConsole(appliance_handle=appliance_handle, console_handle=console_handle, vm=self) def open_details(self, properties=None): """Clicks on details infoblock""" self.load_details(refresh=True) sel.click(InfoBlock(*properties)) @classmethod def get_first_vm(cls, provider): """Get first VM/Instance.""" # todo: move this to base provider ? view = navigate_to(cls, 'AllForProvider', provider=provider) return view.entities.get_first_entity() @property def last_analysed(self): """Returns the contents of the ``Last Analysed`` field in summary""" return self.get_detail(properties=('Lifecycle', 'Last Analyzed')).strip() def load_details(self, refresh=False): """Navigates to an VM's details page. Args: refresh: Refreshes the VM page if already there Raises: VmOrInstanceNotFound: When unable to find the VM passed """ navigate_to(self, 'Details', use_resetter=False) if refresh: toolbar.refresh() self.browser.plugin.ensure_page_safe() def open_edit(self): """Loads up the edit page of the object.""" self.load_details(refresh=True) cfg_btn(self.TO_OPEN_EDIT) def open_timelines(self): """Navigates to an VM's timeline page. Returns: :py:class:`TimelinesView` object """ return navigate_to(self, 'Timelines') def rediscover(self): """Deletes the VM from the provider and lets it discover again""" self.delete(from_details=True) self.wait_for_delete() self.provider.refresh_provider_relationships() self.wait_to_appear() def rediscover_if_analysis_data_present(self): """Rediscovers the object if it has some analysis data present. Returns: Boolean if the rediscovery happened. """ if self.last_analysed.lower() != 'never': self.rediscover() return True return False def refresh_relationships(self, from_details=False, cancel=False, from_any_provider=False): """Executes a refresh of relationships. Args: from_details: Whether or not to perform action from instance details page cancel: Whether or not to cancel the refresh relationships action """ if from_details: self.load_details() else: self.find_quadicon(from_any_provider=from_any_provider).check() cfg_btn('Refresh Relationships and Power States', invokes_alert=True) sel.handle_alert(cancel=cancel) @property def retirement_date(self): """Returns the retirement date of the selected machine, or 'Never' Returns: :py:class:`str` object """ return self.get_detail(properties=("Lifecycle", "Retirement Date")).strip() def smartstate_scan(self, cancel=False, from_details=False): """Initiates fleecing from the UI. Args: cancel: Whether or not to cancel the refresh relationships action from_details: Whether or not to perform action from instance details page """ if from_details: self.load_details(refresh=True) else: self.find_quadicon().check() cfg_btn('Perform SmartState Analysis', invokes_alert=True) sel.handle_alert(cancel=cancel) def wait_to_disappear(self, timeout=600, load_details=True): """Wait for a VM to disappear within CFME Args: timeout: time (in seconds) to wait for it to appear """ wait_for(lambda: self.exists, num_sec=timeout, delay=30, fail_func=sel.refresh, fail_condition=True, message="wait for vm to not exist") wait_for_delete = wait_to_disappear # An alias for more fitting verbosity def wait_to_appear(self, timeout=600, load_details=True): """Wait for a VM to appear within CFME Args: timeout: time (in seconds) to wait for it to appear load_details: when found, should it load the vm details """ def _refresh(): self.provider.refresh_provider_relationships() self.appliance.browser.widgetastic.browser.refresh( ) # strange because ViaUI wait_for(lambda: self.exists, num_sec=timeout, delay=5, fail_func=_refresh, message="wait for vm to appear") if load_details: self.load_details() def set_ownership(self, user=None, group=None, click_cancel=False, click_reset=False): """Set ownership of the VM/Instance or Template/Image""" self.find_quadicon(use_search=False).click() cfg_btn('Set Ownership') if click_reset: action = form_buttons.reset msg_assert = partial(flash.assert_message_match, 'All changes have been reset') elif click_cancel: action = form_buttons.cancel msg_assert = partial(flash.assert_success_message, 'Set Ownership was cancelled by the user') else: action = form_buttons.save msg_assert = partial( flash.assert_success_message, 'Ownership saved for selected {}'.format(self.VM_TYPE)) fill(set_ownership_form, { 'user_name': user, 'group_name': group }, action=action) msg_assert() def unset_ownership(self): """Unset ownership of the VM/Instance or Template/Image""" # choose the vm code comes here self.find_quadicon(use_search=False).click() cfg_btn('Set Ownership') fill(set_ownership_form, { 'user_name': '<No Owner>', 'group_name': 'EvmGroup-administrator' }, action=form_buttons.save) flash.assert_success_message('Ownership saved for selected {}'.format( self.VM_TYPE))
class BasePolicy(BaseEntity, Updateable, Pretty): """This class represents a Policy. Example: .. code-block:: python >>> from cfme.control.explorer.policy import VMCompliancePolicy >>> policy = VMCompliancePolicy("policy_description") >>> policy.create() >>> policy.delete() Args: description: Policy name. active: Whether the policy active or not. scope: Policy scope. notes: Policy notes. """ TYPE = None TREE_NODE = None PRETTY = None _param_name = ParamClassName('description') description = attr.ib() active = attr.ib(default=None) scope = attr.ib(default=None) notes = attr.ib(default=None) def __str__(self): return self.description @property def name_for_policy_profile(self): return "{} {}: {}".format(self.PRETTY, self.TYPE, self.description) def update(self, updates): """Update this Policy in UI. Args: updates: Provided by update() context manager. cancel: Whether to cancel the update (default False). """ view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() view = self.create_view(PolicyDetailsView, override=updates) wait_for(lambda: view.is_displayed, timeout=10, message="waits until the view is displayed") view.flash.assert_no_error() if changed: view.flash.assert_message('Policy "{}" was saved'.format( updates.get("description", self.description))) else: view.flash.assert_message( 'Edit of Policy "{}" was cancelled by the user'.format( self.description)) def delete(self, cancel=False): """Delete this Policy in UI. Args: cancel: Whether to cancel the deletion (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Delete this {} Policy".format( self.PRETTY), handle_alert=not cancel) if cancel: # no redirection assert view.is_displayed view.flash.assert_no_error() def copy(self, cancel=False): """Copy this Policy in UI. Args: cancel: Whether to cancel the copying (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Copy this {} Policy".format( self.PRETTY), handle_alert=not cancel) view = self.create_view(PolicyDetailsView) assert view.is_displayed view.flash.assert_success_message( 'Policy "Copy of {}" was added'.format(self.description)) policy_copy = copy(self) policy_copy.description = "Copy of {}".format(self.description) return policy_copy def assign_events(self, *events, **kwargs): """Assign events to this Policy. Args: events: Events which need to be assigned. extend: Do not uncheck existing events. """ events = list(events) extend = kwargs.pop("extend", False) if extend: events += self.assigned_events view = navigate_to(self, "Details") view.configuration.item_select("Edit this Policy's Event assignments") view = self.create_view(EditPolicyEventAssignments) assert view.is_displayed changed = view.fill({"events": events}) if changed: view.save_button.click() else: view.cancel_button.click() view.flash.assert_success_message('Policy "{}" was saved'.format( self.description)) def is_event_assigned(self, event): return event in self.assigned_events def assign_conditions(self, *conditions): """Assign conditions to this Policy. Args: conditions: Conditions which need to be assigned. """ view = navigate_to(self, "Details") view.configuration.item_select( "Edit this Policy's Condition assignments") view = self.create_view(EditPolicyConditionAssignments) assert view.is_displayed changed = view.fill({ "conditions": [condition.description for condition in conditions] }) if changed: view.save_button.click() flash_message = 'Policy "{}" was saved'.format(self.description) else: view.cancel_button.click() flash_message = 'Edit of Policy "{}" was cancelled by the user'.format( self.description) view.flash.assert_success_message(flash_message) def is_condition_assigned(self, condition): condition.context_policy = self view = navigate_to(condition, "Details in policy") return view.is_displayed def assign_actions_to_event(self, event, actions): """ This method takes a list or dict of actions, goes into the policy event and assigns them. Actions can be passed both as the objects, but they can be passed also as a string. If the specified event is not assigned to the policy, it will be assigned. Args: event: Name of the event under which the actions will be assigned. actions: If :py:class:`list` (or similar), all of these actions will be set under TRUE section. If :py:class:`dict`, the action is key and value specifies its placement. If it's True, then it will be put in the TRUE section and so on. """ true_actions, false_actions = [], [] if isinstance(actions, Action): true_actions.append(actions) elif isinstance(actions, (list, tuple, set)): true_actions.extend(actions) elif isinstance(actions, dict): for action, is_true in actions.items(): if is_true: true_actions.append(action) else: false_actions.append(action) else: raise TypeError( "assign_actions_to_event expects, list, tuple, set or dict!") # Check whether we have all necessary events assigned if not self.is_event_assigned(event): self.assign_events(event, extend=True) assert self.is_event_assigned( event), "Could not assign event {}!".format(event) # And now we can assign actions self.context_event = event view = navigate_to(self, "Event Details") assert view.is_displayed view.toolbar.configuration.item_select( "Edit Actions for this Policy Event") view = self.create_view(EditEventView) assert view.is_displayed changed = view.fill({ "true_actions": [str(action) for action in true_actions], "false_actions": [str(action) for action in false_actions] }) if changed: view.save_button.click() else: view.cancel_button.click() view.flash.assert_success_message( 'Actions for Policy Event "{}" were saved'.format(event)) @property def exists(self): policies = self.appliance.db.client["miq_policies"] return self.appliance.db.client.session\ .query(policies.description)\ .filter(policies.description == self.description)\ .count() > 0 @property def assigned_events(self): policies = self.appliance.db.client["miq_policies"] events = self.appliance.db.client["miq_event_definitions"] policy_contents = self.appliance.db.client["miq_policy_contents"] session = self.appliance.db.client.session policy_id = session.query( policies.id).filter(policies.description == self.description) assigned_events = session.query( policy_contents.miq_event_definition_id).filter( policy_contents.miq_policy_id == policy_id) return [ event_name[0] for event_name in session.query(events.description).filter( events.id.in_(assigned_events)) ] def assigned_actions_to_event(self, event): self.context_event = event view = navigate_to(self, "Event Details") try: true_actions = [ row.description.text for row in view.true_actions.rows() ] except NoSuchElementException: true_actions = [] try: false_actions = [ row.description.text for row in view.false_actions.rows() ] except NoSuchElementException: false_actions = [] return true_actions + false_actions
class ConfigManager(Updateable, Pretty, Navigatable): """ This is base class for Configuration manager objects (Red Hat Satellite, Foreman, Ansible Tower) Args: name: Name of the config. manager url: URL, hostname or IP of the config. manager ssl: Boolean value; `True` if SSL certificate validity should be checked, `False` otherwise credentials: Credentials to access the config. manager key: Key to access the cfme_data yaml data (same as `name` if not specified) Usage: Use Satellite or AnsibleTower classes instead. """ pretty_attr = ['name', 'url'] _param_name = ParamClassName('name') type = None refresh_flash_msg = 'Refresh Provider initiated for 1 provider' def __init__(self, name=None, url=None, ssl=None, credentials=None, key=None, appliance=None): Navigatable.__init__(self, appliance=appliance) self.name = name self.url = url self.ssl = ssl self.credentials = credentials self.key = key or name class Credential(BaseCredential, Updateable): pass @property def ui_name(self): """Return the name used in the UI""" if self.type == 'Ansible Tower': return '{} Automation Manager'.format(self.name) else: return '{} Configuration Manager'.format(self.name) def create(self, cancel=False, validate_credentials=True, validate=True, force=False): """Creates the manager through UI Args: cancel (bool): Whether to cancel out of the creation. The cancel is done after all the information present in the manager has been filled in the UI. validate_credentials (bool): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. validate (bool): Whether we want to wait for the manager's data to load and show up in it's detail page. True will also wait, False will only set it up. force (bool): Whether to force the creation even if the manager already exists. True will try anyway; False will check for its existence and leave, if present. """ def config_profiles_loaded(): # Workaround - without this, validation of provider failed config_profiles_names = [prof.name for prof in self.config_profiles] logger.info( "UI: %s\nYAML: %s", set(config_profiles_names), set(self.yaml_data['config_profiles'])) # Just validate any profiles from yaml are in UI - not all are displayed return any( [cp in config_profiles_names for cp in self.yaml_data['config_profiles']]) if not force and self.exists: return form_dict = self.__dict__ form_dict.update(self.credentials.view_value_mapping) if self.appliance.version < '5.8': form_dict['provider_type'] = self.type view = navigate_to(self, 'Add') view.entities.form.fill(form_dict) if validate_credentials: view.entities.form.validate.click() view.flash.assert_success_message('Credential validation was successful') if cancel: view.entities.cancel.click() view.flash.assert_success_message('Add of Provider was cancelled by the user') else: view.entities.add.wait_displayed('2s') view.entities.add.click() success_message = '{} Provider "{}" was added'.format(self.type, self.name) view.flash.assert_success_message(success_message) view.flash.assert_success_message(self.refresh_flash_msg) if validate: try: self.yaml_data['config_profiles'] except KeyError as e: logger.exception(e) raise wait_for( config_profiles_loaded, fail_func=self.refresh_relationships, handle_exception=True, num_sec=180, delay=30) def update(self, updates, cancel=False, validate_credentials=False): """Updates the manager through UI args: updates (dict): Data to change. cancel (bool): Whether to cancel out of the update. The cancel is done after all the new information has been filled in the UI. validate_credentials (bool): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. Note: utils.update use is recommended over use of this method. """ view = navigate_to(self, 'Edit') view.entities.form.fill(updates) if validate_credentials: view.entities.form.validate.click() view.flash.assert_success_message('Credential validation was successful') if cancel: view.entities.cancel.click() view.flash.assert_success_message('Edit of Provider was cancelled by the user') else: view.entities.save.click() view.flash.assert_success_message( '{} Provider "{}" was updated'.format(self.type, updates['name'] or self.name)) self.__dict__.update(**updates) def delete(self, cancel=False, wait_deleted=True, force=False): """Deletes the manager through UI Args: cancel (bool): Whether to cancel out of the deletion, when the alert pops up. wait_deleted (bool): Whether we want to wait for the manager to disappear from the UI. True will wait; False will only delete it and move on. force (bool): Whether to try to delete the manager even though it doesn't exist. True will try to delete it anyway; False will check for its existence and leave, if not present. """ if not force and not self.exists: return view = navigate_to(self, 'All') view.toolbar.view_selector.select('List View') provider_entity = view.entities.get_entities_by_keys(provider_name=self.ui_name) provider_entity[0].check() remove_item = 'Remove selected items from Inventory' view.toolbar.configuration.item_select(remove_item, handle_alert=not cancel) if not cancel: view.flash.assert_success_message('Delete initiated for 1 Provider') if wait_deleted: wait_for( # check the provider is not listed in all providers anymore func=lambda: not view.entities.get_entities_by_keys( provider_name=self.ui_name ), delay=15, fail_func=view.toolbar.refresh.click, num_sec=5 * 60 ) # check the provider is indeed deleted assert not self.exists @property def rest_api_entity(self): """Returns the rest entity of config manager provider""" # Since config manager provider is slightly different from other normal providers, # we cannot obtain it's rest entity the normal way, instead, we use Entity class # of manageiq_client library to instantiate the rest entity. provider_id = self.appliance.rest_api.collections.providers.get( name=self.ui_name ).provider_id return RestEntity( self.appliance.rest_api.collections.providers, data={ "href": self.appliance.url_path( "/api/providers/{}?provider_class=provider".format(provider_id) ) }, ) def create_rest(self): """Create the config manager in CFME using REST""" if "ansible_tower" in self.key: config_type = "AnsibleTower" elif "satellite" in self.key: config_type = "Foreman" payload = { "type": "ManageIQ::Providers::{}::Provider".format(config_type), "url": self.url, "name": self.name, "verify_ssl": self.ssl, "credentials": { "userid": self.credentials.view_value_mapping["username"], "password": self.credentials.view_value_mapping["password"], }, } try: self.appliance.rest_api.post( api_endpoint_url=self.appliance.url_path( "/api/providers/?provider_class=provider" ), **payload ) except APIException as err: raise AssertionError("Provider wasn't added: {}".format(err)) response = self.appliance.rest_api.response if not response: raise AssertionError( "Provider wasn't added, status code {}".format(response.status_code) ) assert_response(self.appliance) return True def delete_rest(self): """Deletes the config manager from CFME using REST""" try: self.rest_api_entity.action.delete() except APIException as err: raise AssertionError("Provider wasn't deleted: {}".format(err)) response = self.appliance.rest_api.response if not response: raise AssertionError( "Provider wasn't deleted, status code {}".format(response.status_code) ) @property def exists(self): """Returns whether the manager exists in the UI or not""" try: navigate_to(self, 'Details') except NoSuchElementException: return False return True def refresh_relationships(self, cancel=False): """Refreshes relationships and power states of this manager""" view = navigate_to(self, 'All') view.toolbar.view_selector.select('List View') provider_entity = view.entities.get_entities_by_keys(provider_name=self.ui_name)[0] provider_entity.check() if view.toolbar.configuration.item_enabled('Refresh Relationships and Power states'): view.toolbar.configuration.item_select('Refresh Relationships and Power states', handle_alert=not cancel) if not cancel: view.flash.assert_success_message(self.refresh_flash_msg) @property def config_profiles(self): """Returns 'ConfigProfile' configuration profiles (hostgroups) available on this manager""" view = navigate_to(self, 'Details') # TODO - remove it later.Workaround for BZ 1452425 view.toolbar.view_selector.select('List View') view.toolbar.refresh.click() wait_for(lambda: view.entities.elements.is_displayed, fail_func=view.toolbar.refresh.click, handle_exception=True, num_sec=60, delay=5) config_profiles = [] for row in view.entities.elements: if self.type == 'Ansible Tower': name = row.name.text else: name = row.description.text if 'unassigned' in name.lower(): continue config_profiles.append(ConfigProfile(name=name, manager=self)) return config_profiles @property def systems(self): """Returns 'ConfigSystem' configured systems (hosts) available on this manager""" return sum([prof.systems for prof in self.config_profiles]) @property def yaml_data(self): """Returns yaml data for this manager""" return conf.cfme_data.configuration_managers[self.key] @classmethod def load_from_yaml(cls, key): """Returns 'ConfigManager' object loaded from yamls, based on its key""" data = conf.cfme_data.configuration_managers[key] creds = conf.credentials[data['credentials']] return cls( name=data['name'], url=data['url'], ssl=data['ssl'], credentials=cls.Credential( principal=creds['username'], secret=creds['password']), key=key) @property def quad_name(self): if self.type == 'Ansible Tower': return '{} Automation Manager'.format(self.name) else: return '{} Configuration Manager'.format(self.name)
class ConfigManager(Updateable, Pretty, Navigatable): """ This is base class for Configuration manager objects (Red Hat Satellite, Foreman, Ansible Tower) Args: name: Name of the config. manager url: URL, hostname or IP of the config. manager ssl: Boolean value; `True` if SSL certificate validity should be checked, `False` otherwise credentials: Credentials to access the config. manager key: Key to access the cfme_data yaml data (same as `name` if not specified) Usage: Use Satellite or AnsibleTower classes instead. """ pretty_attr = ['name', 'url'] _param_name = ParamClassName('name') type = None refresh_flash_msg = 'Refresh Provider initiated for 1 provider' def __init__(self, name=None, url=None, ssl=None, credentials=None, key=None, appliance=None): Navigatable.__init__(self, appliance=appliance) self.name = name self.url = url self.ssl = ssl self.credentials = credentials self.key = key or name class Credential(BaseCredential, Updateable): pass @property def ui_name(self): """Return the name used in the UI""" return '{name} Automation Manager'.format(name=self.name) def create(self, cancel=False, validate_credentials=True, validate=True, force=False): """Creates the manager through UI Args: cancel (bool): Whether to cancel out of the creation. The cancel is done after all the information present in the manager has been filled in the UI. validate_credentials (bool): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. validate (bool): Whether we want to wait for the manager's data to load and show up in it's detail page. True will also wait, False will only set it up. force (bool): Whether to force the creation even if the manager already exists. True will try anyway; False will check for its existence and leave, if present. """ def config_profiles_loaded(): # Workaround - without this, validation of provider failed config_profiles_names = [ prof.name for prof in self.config_profiles ] logger.info("UI: %s\nYAML: %s", set(config_profiles_names), set(self.yaml_data['config_profiles'])) return all([ cp in config_profiles_names for cp in self.yaml_data['config_profiles'] ]) if not force and self.exists: return form_dict = self.__dict__ form_dict.update(self.credentials.view_value_mapping) if self.appliance.version < '5.8': form_dict['provider_type'] = self.type view = navigate_to(self, 'Add') view.entities.form.fill(form_dict) if validate_credentials: view.entities.form.validate.click() view.entities.flash.assert_success_message( 'Credential validation was successful') if cancel: view.entities.cancel.click() view.entities.flash.assert_success_message( 'Add of Provider was cancelled by the user') else: view.entities.add.click() success_message = '{} Provider "{}" was added'.format( self.type, self.name) view.entities.flash.assert_success_message(success_message) view.entities.flash.assert_success_message(self.refresh_flash_msg) if validate: try: self.yaml_data['config_profiles'] except KeyError as e: logger.exception(e) raise wait_for(config_profiles_loaded, fail_func=self.refresh_relationships, handle_exception=True, num_sec=180, delay=30) def update(self, updates, cancel=False, validate_credentials=False): """Updates the manager through UI args: updates (dict): Data to change. cancel (bool): Whether to cancel out of the update. The cancel is done after all the new information has been filled in the UI. validate_credentials (bool): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. Note: utils.update use is recommended over use of this method. """ view = navigate_to(self, 'Edit') view.entities.form.fill(updates) if validate_credentials: view.entities.form.validate.click() view.entities.flash.assert_success_message( 'Credential validation was successful') if cancel: view.entities.cancel.click() view.entities.flash.assert_success_message( 'Edit of Provider was cancelled by the user') else: view.entities.save.click() view.entities.flash.assert_success_message( '{} Provider "{}" was updated'.format( self.type, updates['name'] or self.name)) self.__dict__.update(**updates) def delete(self, cancel=False, wait_deleted=True, force=False): """Deletes the manager through UI Args: cancel (bool): Whether to cancel out of the deletion, when the alert pops up. wait_deleted (bool): Whether we want to wait for the manager to disappear from the UI. True will wait; False will only delete it and move on. force (bool): Whether to try to delete the manager even though it doesn't exist. True will try to delete it anyway; False will check for its existence and leave, if not present. """ if not force and not self.exists: return view = navigate_to(self, 'All') view.toolbar.view_selector.select('List View') row = view.entities.paginator.find_row_on_pages( view.entities.elements, provider_name=self.ui_name) row[0].check() remove_item = VersionPick({ '5.8': 'Remove selected items', '5.9': 'Remove selected items from Inventory' }).pick(self.appliance.version) view.toolbar.configuration.item_select(remove_item, handle_alert=not cancel) if not cancel: view.entities.flash.assert_success_message( 'Delete initiated for 1 Provider') if wait_deleted: wait_for(func=lambda: self.exists, fail_condition=True, delay=15, num_sec=60) @property def exists(self): """Returns whether the manager exists in the UI or not""" view = navigate_to(self, 'All') view.toolbar.view_selector.select('List View') try: view.entities.paginator.find_row_on_pages( view.entities.elements, provider_name=self.ui_name) return True except NoSuchElementException: pass return False def refresh_relationships(self, cancel=False): """Refreshes relationships and power states of this manager""" view = navigate_to(self, 'All') view.toolbar.view_selector.select('List View') row = view.entities.paginator.find_row_on_pages( view.entities.elements, provider_name=self.ui_name) row[0].check() if view.toolbar.configuration.item_enabled( 'Refresh Relationships and Power states'): view.toolbar.configuration.item_select( 'Refresh Relationships and Power states', handle_alert=not cancel) if not cancel: view.entities.flash.assert_success_message(self.refresh_flash_msg) @property def config_profiles(self): """Returns 'ConfigProfile' configuration profiles (hostgroups) available on this manager""" view = navigate_to(self, 'Details') # TODO - remove it later.Workaround for BZ 1452425 view.toolbar.view_selector.select('List View') view.toolbar.refresh.click() wait_for(lambda: view.entities.elements.is_displayed, fail_func=view.toolbar.refresh.click, handle_exception=True, num_sec=60, delay=5) config_profiles = [] for row in view.entities.elements: if self.type == 'Ansible Tower': name = row.name.text else: name = row.description.text if 'unassigned' in name.lower(): continue config_profiles.append(ConfigProfile(name=name, manager=self)) return config_profiles @property def systems(self): """Returns 'ConfigSystem' configured systems (hosts) available on this manager""" return reduce(lambda x, y: x + y, [prof.systems for prof in self.config_profiles]) @property def yaml_data(self): """Returns yaml data for this manager""" return conf.cfme_data.configuration_managers[self.key] @classmethod def load_from_yaml(cls, key): """Returns 'ConfigManager' object loaded from yamls, based on its key""" data = conf.cfme_data.configuration_managers[key] creds = conf.credentials[data['credentials']] return cls(name=data['name'], url=data['url'], ssl=data['ssl'], credentials=cls.Credential(principal=creds['username'], secret=creds['password']), key=key) @property def quad_name(self): if self.type == 'Ansible Tower': return '{} Automation Manager'.format(self.name) else: return '{} Configuration Manager'.format(self.name)
class ConfigManagerProvider(BaseProvider, Updateable, Pretty): """ This is base class for Configuration manager objects (Red Hat Satellite, Foreman, Ansible Tower) Args: name: Name of the config. manager url: URL, hostname or IP of the config. manager credentials: Credentials to access the config. manager key: Key to access the cfme_data yaml data (same as `name` if not specified) Usage: Use Satellite or AnsibleTower classes instead. """ pretty_attr = ['name', 'key'] _param_name = ParamClassName('name') category = "config_manager" refresh_flash_msg = 'Refresh Provider initiated for 1 provider' name = attr.ib(default=None) url = attr.ib(default=None) credentials = attr.ib(default=None) key = attr.ib(default=None) ui_type = None _collections = {"config_profiles": ConfigProfilesCollection} class Credential(BaseCredential, Updateable): pass @property def exists(self): """ Returns ``True`` if a provider of the same name exists on the appliance This overwrite of BaseProvider exists is necessary because MIQ appends Configuration Manager to the provider name """ for name in self.appliance.managed_provider_names: if self.name in name: return True return False def create(self, cancel=False, validate_credentials=True, validate=True, force=False, **kwargs): """Creates the manager through UI Args: cancel (bool): Whether to cancel out of the creation. The cancel is done after all the information present in the manager has been filled in the UI. validate_credentials (bool): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. validate (bool): Whether we want to wait for the manager's data to load and show up in it's detail page. True will also wait, False will only set it up. force (bool): Whether to force the creation even if the manager already exists. True will try anyway; False will check for its existence and leave, if present. """ def config_profiles_loaded(): # Workaround - without this, validation of provider failed config_profiles_names = [ prof.name for prof in self.config_profiles ] logger.info("UI: %s\nYAML: %s", set(config_profiles_names), set(self.data['config_profiles'])) # Just validate any profiles from yaml are in UI - not all are displayed return any([ cp in config_profiles_names for cp in self.data['config_profiles'] ]) if not force and self.exists: return form_dict = self.__dict__ form_dict.update(self.credentials.view_value_mapping) view = navigate_to(self, 'Add') view.entities.form.fill(form_dict) if validate_credentials: view.entities.form.validate.click() view.flash.assert_success_message( 'Credential validation was successful') if cancel: view.entities.cancel.click() view.flash.assert_success_message( 'Add of Provider was cancelled by the user') else: view.entities.add.wait_displayed('2s') view.entities.add.click() success_message = f'{self.ui_type} Provider "{self.name}" was added' view.flash.assert_success_message(success_message) view.flash.assert_success_message(self.refresh_flash_msg) if validate: try: self.data['config_profiles'] except KeyError as e: logger.exception(e) raise wait_for(config_profiles_loaded, fail_func=self.refresh_relationships, handle_exception=True, num_sec=180, delay=30) def update(self, updates, cancel=False, validate_credentials=False): """Updates the manager through UI args: updates (dict): Data to change. cancel (bool): Whether to cancel out of the update. The cancel is done after all the new information has been filled in the UI. validate_credentials (bool): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. Note: utils.update use is recommended over use of this method. """ view = navigate_to(self, 'Edit') view.entities.form.fill(updates) if validate_credentials: view.entities.form.validate.click() view.flash.assert_success_message( 'Credential validation was successful') if cancel: view.entities.cancel.click() view.flash.assert_success_message( 'Edit of Provider was cancelled by the user') else: view.entities.save.click() view.flash.assert_success_message( '{} Provider "{}" was updated'.format( self.ui_type, updates['name'] or self.name)) self.__dict__.update(**updates) def delete(self, cancel=False, wait_deleted=True, force=False): """Deletes the manager through UI Args: cancel (bool): Whether to cancel out of the deletion, when the alert pops up. wait_deleted (bool): Whether we want to wait for the manager to disappear from the UI. True will wait; False will only delete it and move on. force (bool): Whether to try to delete the manager even though it doesn't exist. True will try to delete it anyway; False will check for its existence and leave, if not present. """ if not force and not self.exists: return view = navigate_to(self, 'AllOfType') provider_entity = view.entities.get_entities_by_keys( provider_name=self.ui_name) provider_entity[0].check() remove_item = 'Remove selected items from Inventory' view.toolbar.configuration.item_select(remove_item, handle_alert=not cancel) if not cancel: view.flash.assert_success_message( 'Delete initiated for 1 Provider') if wait_deleted: wait_for( # check the provider is not listed in all providers anymore func=lambda: not view.entities.get_entities_by_keys( provider_name=self.ui_name), delay=15, fail_func=view.toolbar.refresh.click, num_sec=5 * 60) # check the provider is indeed deleted assert not self.exists @property def rest_api_entity(self): """Returns the rest entity of config manager provider""" # Since config manager provider is slightly different from other normal providers, # we cannot obtain it's rest entity the normal way, instead, we use Entity class # of manageiq_client library to instantiate the rest entity. provider_id = self.appliance.rest_api.collections.providers.get( name=self.ui_name).provider_id return RestEntity( self.appliance.rest_api.collections.providers, data={ "href": self.appliance.url_path( "/api/providers/{}?provider_class=provider".format( provider_id)) }) # TODO: implement this via Sentaku def create_rest(self): """Create the config manager in CFME using REST""" include_ssl = False if self.type == "ansible_tower": config_type = "AnsibleTower" else: config_type = "Foreman" include_ssl = True payload = { "type": "ManageIQ::Providers::{}::Provider".format(config_type), "url": self.url, "name": self.name, "credentials": { "userid": self.credentials.view_value_mapping["username"], "password": self.credentials.view_value_mapping["password"], }, } if include_ssl: payload["verify_ssl"] = self.ssl try: self.appliance.rest_api.post( api_endpoint_url=self.appliance.url_path( "/api/providers/?provider_class=provider"), **payload) except APIException as err: raise AssertionError("Provider wasn't added: {}".format(err)) response = self.appliance.rest_api.response if not response: raise AssertionError( "Provider wasn't added, status code {}".format( response.status_code)) assert_response(self.appliance) return True def delete_rest(self): """Deletes the config manager from CFME using REST""" try: self.rest_api_entity.action.delete() except APIException as err: raise AssertionError("Provider wasn't deleted: {}".format(err)) response = self.appliance.rest_api.response if not response: raise AssertionError( "Provider wasn't deleted, status code {}".format( response.status_code)) def refresh_relationships(self, cancel=False): """Refreshes relationships and power states of this manager""" view = navigate_to(self, 'AllOfType') view.toolbar.view_selector.select('List View') provider_entity = view.entities.get_entities_by_keys( provider_name=self.ui_name)[0] provider_entity.check() if view.toolbar.configuration.item_enabled( 'Refresh Relationships and Power states'): view.toolbar.configuration.item_select( 'Refresh Relationships and Power states', handle_alert=not cancel) if not cancel: view.flash.assert_success_message(self.refresh_flash_msg) @property def config_profiles(self): """Returns 'ConfigProfile' configuration profiles (hostgroups) available on this manager""" return self.collections.config_profiles.all() @property def config_systems(self): """Returns 'ConfigSystem' configured systems (hosts) available on this manager""" systems_per_prof = [ prof.config_systems for prof in self.config_profiles ] return [item for sublist in systems_per_prof for item in sublist] @property def quad_name(self): return self.ui_name
class ISODatastore(Updateable, Pretty, Navigatable): """Model of a PXE Server object in CFME Args: provider: Provider name. """ _param_name = ParamClassName('ds_name') pretty_attrs = ['provider'] def __init__(self, provider=None, appliance=None): Navigatable.__init__(self, appliance=appliance) self.provider = provider def create(self, cancel=False, refresh=True, refresh_timeout=120): """ Creates an ISO datastore object Args: cancel (boolean): Whether to cancel out of the creation. The cancel is done after all the information present in the ISO datastore has been filled in the UI. refresh (boolean): Whether to run the refresh operation on the ISO datastore after the add has been completed. """ view = navigate_to(self, 'Add') view.fill({'provider': self.provider}) main_view = self.create_view(PXEDatastoresView) if cancel: view.cancel.click() msg = 'Add of new ISO Datastore was cancelled by the user' else: view.add.click() msg = 'ISO Datastore "{}" was added'.format(self.provider) main_view.flash.assert_success_message(msg) if refresh: self.refresh(timeout=refresh_timeout) @variable(alias='db') def exists(self): """ Checks if the ISO Datastore already exists via db """ iso = self.appliance.db.client['iso_datastores'] ems = self.appliance.db.client['ext_management_systems'] name = self.provider iso_ds = list(self.appliance.db.client.session.query(iso.id) .join(ems, iso.ems_id == ems.id) .filter(ems.name == name)) if iso_ds: return True else: return False @exists.variant('ui') def exists_ui(self): """ Checks if the ISO Datastore already exists via UI """ try: navigate_to(self, 'Details') return True except NoSuchElementException: return False def delete(self, cancel=True): """ Deletes an ISO Datastore from CFME Args: cancel: Whether to cancel the deletion, defaults to True """ view = navigate_to(self, 'Details') msg = 'Remove this ISO Datastore' if self.appliance.version >= '5.9': msg = 'Remove this ISO Datastore from Inventory' view.toolbar.configuration.item_select(msg, handle_alert=not cancel) if not cancel: main_view = self.create_view(PXEDatastoresView) msg = 'ISO Datastore "{}": Delete successful'.format(self.provider) main_view.flash.assert_success_message(msg) else: navigate_to(self, 'Details') def refresh(self, wait=True, timeout=120): """ Refreshes the PXE relationships and waits for it to be updated """ view = navigate_to(self, 'Details') basic_info = view.entities.basic_information last_time = basic_info.get_text_of('Last Refreshed On') view.toolbar.configuration.item_select('Refresh Relationships', handle_alert=True) view.flash.assert_success_message(('ISO Datastore "{}": Refresh Relationships successfully ' 'initiated'.format(self.provider))) if wait: wait_for(lambda lt: lt != basic_info.get_text_of('Last Refreshed On'), func_args=[last_time], fail_func=view.toolbar.reload.click, num_sec=timeout, message="iso refresh") def set_iso_image_type(self, image_name, image_type): """ Function to set the image type of a PXE image """ view = navigate_to(self, 'All') view.sidebar.datastores.tree.click_path('All ISO Datastores', self.provider, 'ISO Images', image_name) view.toolbar.configuration.item_select('Edit this ISO Image') view = view.browser.create_view(PXEImageEditView) changed = view.fill({'type': image_type}) # Click save if enabled else click Cancel if changed: view.save.click() else: view.cancel.click()
class Server(BaseEntity, sentaku.modeling.ElementMixin): _param_name = ParamClassName('name') sid = attr.ib(default=1) address = sentaku.ContextualMethod() login = sentaku.ContextualMethod() login_admin = sentaku.ContextualMethod() logout = sentaku.ContextualMethod() update_password = sentaku.ContextualMethod() logged_in = sentaku.ContextualMethod() current_full_name = sentaku.ContextualMethod() current_username = sentaku.ContextualMethod() current_group_name = sentaku.ContextualMethod() group_names = sentaku.ContextualMethod() intel_name = VersionPicker({"5.11": "Overview", Version.lowest(): "Cloud Intel"}) # zone = sentaku.ContextualProperty() # slave_servers = sentaku.ContextualProperty() @property def name(self): """Fetch the name from the master server api entity Returns: string if entity has the name attribute None if its missing """ # empty string default for string building w/o None return getattr(self.appliance._rest_api_server, 'name', '') @property def current_string(self): """Returns the string ' (current)' if the appliance serving the UI request matches this server instance. Used to generate the appropriate tree_path for navigating configuration accordion trees.""" return ' (current)' if self.sid == self.appliance.server_id() else '' @property def tree_path(self): """Generate the path list for navigation purposes list elements follow the tree path in the configuration accordion Returns: list of path elements for tree navigation """ name_string = f' {self.name} '.replace(' ', ' ') path = [ self.zone.region.settings_string, "Zones", f"Zone: {self.zone.title_string()}", f"Server:{name_string}[{self.sid}]{self.current_string}" # variables have needed spaces ] return path @property def diagnostics_tree_path(self): """Generate tree path list for the diagnostics tree in the configuration accordion""" path = self.tree_path path.remove("Zones") return path @property def settings(self): from cfme.configure.configuration.server_settings import ServerInformation setting = ServerInformation(appliance=self.appliance) return setting @property def authentication(self): from cfme.configure.configuration.server_settings import AuthenticationSetting auth_settings = AuthenticationSetting(self.appliance) return auth_settings @property def collect_logs(self): from cfme.configure.configuration.diagnostics_settings import ServerCollectLog return ServerCollectLog(self.appliance) @property def zone(self): entity = self.rest_api_entity entity.reload(attributes=['zone']) return self.appliance.collections.zones.instantiate( name=entity.zone['name'], description=entity.zone['description'], id=entity.zone['id'] ) @property def slave_servers(self): return self.zone.collections.servers.filter({'slave': True}).all() @property def is_slave(self): return self in self.slave_servers @property def secondary_servers(self): """ Find and return a list of all other servers in this server's zone. """ return [s for s in self.zone.collections.servers.all() if s.sid != self.sid] @property def _api_settings_url(self): return '/'.join( [self.appliance.rest_api.collections.servers._href, str(self.sid), 'settings'] ) @property def rest_api_entity(self): try: return self.appliance.rest_api.collections.servers.get(id=self.sid) except ValueError: raise RestLookupError(f'No server rest entity found matching ID {self.sid}') @property def advanced_settings(self): """GET servers/:id/settings api endpoint to query server configuration""" return self.appliance.rest_api.get(self._api_settings_url) def update_advanced_settings(self, settings_dict): """PATCH settings from the server's api/server/:id/settings endpoint Args: settings_dict: dictionary of the changes to be made to the yaml configuration JSON dumps settings_dict to pass as raw hash data to rest_api session Raises: AssertionError: On an http result >=400 (RequestsResponse.ok) """ # Calling the _session patch method because the core patch will wrap settings_dict in a list # Failing with some settings_dict, like 'authentication' # https://bugzilla.redhat.com/show_bug.cgi?id=1553394 result = self.appliance.rest_api._session.patch( url=self._api_settings_url, data=json.dumps(settings_dict) ) assert result.ok def upload_custom_logo(self, file_type, file_data=None, enable=True): """ This function can be used to upload custom logo or text and use them. Args: file_type (str) : Can be either of [logo, login_logo, brand, favicon, logintext] file_data (str) : Text data if file_type is logintext else image path to be uploaded enable (bool) : True to use the custom logo/text else False """ view = navigate_to(self, "CustomLogos") try: logo_view = getattr(view.customlogos, file_type) except AttributeError: raise AttributeError( "File type not in ('logo', 'login_logo', 'brand', 'favicon', 'logintext)." ) if file_data: if file_type == "logintext": logo_view.fill({"login_text": file_data}) else: logo_view.fill({"image": file_data}) logo_view.upload_button.click() view.flash.assert_no_error() logo_view.enable.fill(enable) view.customlogos.save_button.click() view.flash.assert_no_error()
class Server(BaseEntity, sentaku.modeling.ElementMixin): _param_name = ParamClassName('name') name = attr.ib() sid = attr.ib(default=1) address = sentaku.ContextualMethod() login = sentaku.ContextualMethod() login_admin = sentaku.ContextualMethod() logout = sentaku.ContextualMethod() update_password = sentaku.ContextualMethod() logged_in = sentaku.ContextualMethod() current_full_name = sentaku.ContextualMethod() current_username = sentaku.ContextualMethod() current_group_name = sentaku.ContextualMethod() group_names = sentaku.ContextualMethod() # zone = sentaku.ContextualProperty() # slave_servers = sentaku.ContextualProperty() @property def settings(self): from cfme.configure.configuration.server_settings import ServerInformation setting = ServerInformation(appliance=self.appliance) return setting @property def authentication(self): from cfme.configure.configuration.server_settings import AuthenticationSetting auth_settings = AuthenticationSetting(self.appliance) return auth_settings @property def collect_logs(self): from cfme.configure.configuration.diagnostics_settings import ServerCollectLog return ServerCollectLog(self.appliance) @property def zone(self): server_res = self.appliance.rest_api.collections.servers.find_by( id=self.sid) server = server_res[0] server.reload(attributes=['zone']) zone = server.zone zone_obj = self.appliance.collections.zones.instantiate( name=zone['name'], description=zone['description'], id=zone['id']) return zone_obj @property def slave_servers(self): return self.zone.collections.servers.filter({'slave': True}).all() @property def _api_settings_url(self): return '/'.join([ self.appliance.rest_api.collections.servers._href, str(self.sid), 'settings' ]) @property def advanced_settings(self): """GET servers/:id/settings api endpoint to query server configuration""" return self.appliance.rest_api.get(url=self._api_settings_url) def update_advanced_settings(self, settings_dict): """PATCH settings from the server's api/server/:id/settings endpoint Args: settings_dict: dictionary of the changes to be made to the yaml configuration JSON dumps settings_dict to pass as raw hash data to rest_api session Raises: AssertionError: On an http result >=400 (RequestsResponse.ok) """ # Calling the _session patch method because the core patch will wrap settings_dict in a list # Failing with some settings_dict, like 'authentication' # https://bugzilla.redhat.com/show_bug.cgi?id=1553394 result = self.appliance.rest_api._session.patch( url=self._api_settings_url, data=json.dumps(settings_dict)) assert result.ok
class Datastore(Pretty, BaseEntity, Taggable, CustomButtonEventsMixin): """Model of an infrastructure datastore in cfme Args: name: Name of the datastore. provider: provider this datastore is attached to. """ pretty_attrs = ['name', 'provider_key'] _param_name = ParamClassName('name') name = attr.ib() provider = attr.ib() type = attr.ib(default=None) def __attrs_post_init__(self): # circular imports from cfme.infrastructure.host import HostsCollection self._collections = {'hosts': HostsCollection} @property def rest_api_entity(self): return self.appliance.rest_api.collections.data_stores.get(name=self.name) def delete(self, cancel=True): """ Deletes a datastore from CFME Args: cancel: Whether to cancel the deletion, defaults to True Note: Datastore must have 0 hosts and 0 VMs for this to work. """ # BZ 1467989 - this button is never getting enabled for some resources view = navigate_to(self, 'Details') view.toolbar.configuration.item_select('Remove Datastore from Inventory' if self.appliance.version >= '5.9' else 'Remove Datastore', handle_alert=(not cancel)) view.flash.assert_success_message('Delete initiated for Datastore from the CFME Database') @property def hosts(self): return self.collections.hosts def get_hosts(self): """ Returns names of hosts (from quadicons) that use this datastore Returns: List of strings with names or `[]` if no hosts found. """ view = navigate_to(self, 'DetailsFromProvider') view.entities.relationships.click_at('Hosts') hosts_view = view.browser.create_view(RegisteredHostsView) return hosts_view.entities.get_all() def get_vms(self): """ Returns names of VMs (from quadicons) that use this datastore Returns: List of strings with names or `[]` if no vms found. """ view = navigate_to(self, 'Details') if 'VMs' in view.entities.relationships.fields: view.entities.relationships.click_at('VMs') else: view.entities.relationships.click_at('Managed VMs') # todo: to replace with correct view vms_view = view.browser.create_view(DatastoresView) return [vm.name for vm in vms_view.entities.get_all()] def delete_all_attached_vms(self): view = navigate_to(self, 'Details') view.entities.relationships.click_at('Managed VMs') # todo: to replace with correct view vms_view = view.browser.create_view(DatastoresView) for entity in vms_view.entities.get_all(): entity.check() view.toolbar.configuration.item_select('Remove selected items from Inventory' if self.appliance.version >= '5.9' else 'Remove selected items', handle_alert=True) wait_for(lambda: bool(len(vms_view.entities.get_all())), fail_condition=True, message="Wait datastore vms to disappear", num_sec=1000, fail_func=self.browser.refresh) def delete_all_attached_hosts(self): view = navigate_to(self, 'Details') view.entities.relationships.click_at('Hosts') hosts_view = view.browser.create_view(RegisteredHostsView) for entity in hosts_view.entities.get_all(): entity.check() view.toolbar.configuration.item_select('Remove items from Inventory' if self.appliance.version >= '5.9' else 'Remove items', handle_alert=True) wait_for(lambda: bool(len(hosts_view.entities.get_all())), fail_condition=True, message="Wait datastore hosts to disappear", num_sec=1000, fail_func=self.browser.refresh) @property def exists(self): try: view = navigate_to(self, 'Details') return view.is_displayed except ItemNotFound: return False @property def host_count(self): """ number of attached hosts. Returns: :py:class:`int` host count. """ view = navigate_to(self, 'Details') return int(view.entities.relationships.get_text_of('Hosts')) @property def vm_count(self): """ number of attached VMs. Returns: :py:class:`int` vm count. """ view = navigate_to(self, 'Details') return int(view.entities.relationships.get_text_of('Managed VMs')) def run_smartstate_analysis(self, wait_for_task_result=False): """ Runs smartstate analysis on this host Note: The host must have valid credentials already set up for this to work. """ view = navigate_to(self, 'DetailsFromProvider') try: wait_for(lambda: view.toolbar.configuration.item_enabled('Perform SmartState Analysis'), fail_condition=False, num_sec=10) except TimedOutError: raise MenuItemNotFound('Smart State analysis is disabled for this datastore') view.toolbar.configuration.item_select('Perform SmartState Analysis', handle_alert=True) view.flash.assert_success_message(('"{}": scan successfully ' 'initiated'.format(self.name))) if wait_for_task_result: task = self.appliance.collections.tasks.instantiate( name="SmartState Analysis for [{}]".format(self.name), tab='MyOtherTasks') task.wait_for_finished() return task def wait_candu_data_available(self, timeout=900): """Waits until C&U data are available for this Datastore Args: timeout: Timeout passed to :py:func:`utils.wait.wait_for` """ view = navigate_to(self, 'Details') wait_for( lambda: view.toolbar.monitoring.item_enabled("Utilization"), delay=10, handle_exception=True, num_sec=timeout, fail_func=view.browser.refresh )
class Report(BaseEntity, Updateable): _param_name = ParamClassName('title') menu_name = attr.ib(default=None) title = attr.ib(default=None) company_name = attr.ib() type = attr.ib(default=None) subtype = attr.ib(default=None) base_report_on = attr.ib(default=None) report_fields = attr.ib(default=None) cancel_after = attr.ib(default=None) consolidation = attr.ib(default=None) formatting = attr.ib(default=None) styling = attr.ib(default=None) filter = attr.ib(default=None) filter_show_costs = attr.ib(default=None) filter_owner = attr.ib(default=None) filter_tag_cat = attr.ib(default=None) filter_tag_value = attr.ib(default=None) interval = attr.ib(default=None) interval_size = attr.ib(default=None) interval_end = attr.ib(default=None) sort = attr.ib(default=None) chart_type = attr.ib(default=None) top_values = attr.ib(default=None) sum_other = attr.ib(default=None) base_timeline_on = attr.ib(default=None) band_units = attr.ib(default=None) event_position = attr.ib(default=None) show_event_unit = attr.ib(default=None) show_event_count = attr.ib(default=None) summary = attr.ib(default=None) charts = attr.ib(default=None) timeline = attr.ib(default=None) is_candu = attr.ib(default=False) def __attrs_post_init__(self): self._collections = {'saved_reports': SavedReportsCollection} @company_name.default def company_name_default(self): if self.appliance.version < "5.9": return "My Company (All EVM Groups)" else: return "My Company (All Groups)" def update(self, updates): view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() view = self.create_view(ReportDetailsView, override=updates) assert view.is_displayed view.flash.assert_no_error() if changed: view.flash.assert_message('Report "{}" was saved'.format( self.menu_name)) else: view.flash.assert_message( 'Edit of Report "{}" was cancelled by the user'.format( self.menu_name)) def delete(self, cancel=False): view = navigate_to(self, "Details") node = view.reports.tree.expand_path("All Reports", self.company_name, "Custom") custom_reports_number = len(view.reports.tree.child_items(node)) view.configuration.item_select("Delete this Report from the Database", handle_alert=not cancel) if cancel: assert view.is_displayed view.flash.assert_no_error() else: # This check is needed because after deleting the last custom report, # the whole "My Company (All EVM Groups)" branch in the tree will be removed. if custom_reports_number > 1: view = self.create_view(AllCustomReportsView) assert view.is_displayed view.flash.assert_no_error() if not BZ(1561779, forced_streams=['5.9', '5.8']).blocks: view.flash.assert_message( 'Report "{}": Delete successful'.format(self.menu_name)) @cached_property def saved_reports(self): return self.collections.saved_reports def queue(self, wait_for_finish=False): view = navigate_to(self, "Details") view.report_info.queue_button.click() view.flash.assert_no_error() if wait_for_finish: # Get the queued_at value to always target the correct row queued_at = view.saved_reports.table[0]["Queued At"].text def _get_state(): row = view.saved_reports.table.row(queued_at=queued_at) status = row.status.text.strip().lower() assert status != "error" return status == "complete" wait_for( _get_state, delay=1, message="wait for report generation finished", fail_func=view.reload_button.click, num_sec=300, ) view.reload_button.click() first_row = view.saved_reports.table[0] saved_report = self.saved_reports.instantiate(first_row.run_at.text, first_row.queued_at.text, self.is_candu) return saved_report @property def exists(self): try: navigate_to(self, "Details") return True except CandidateNotFound: return False
class CustomReport(Updateable, Navigatable): _param_name = ParamClassName('title') _default_dict = { "menu_name": None, "title": None, "base_report_on": None, "report_fields": None, "cancel_after": None, "consolidation": None, "formatting": None, "styling": None, "filter": None, "filter_show_costs": None, "filter_owner": None, "filter_tag_cat": None, "filter_tag_value": None, "interval": None, "interval_size": None, "interval_end": None, "sort": None, "chart_type": None, "top_values": None, "sum_other": None, "base_timeline_on": None, "band_units": None, "event_position": None, "show_event_unit": None, "show_event_count": None, "summary": None, "charts": None, "timeline": None } def __init__(self, appliance=None, **values): Navigatable.__init__(self, appliance=appliance) # We will override the original dict self.__dict__ = dict(self._default_dict) self.__dict__.update(values) # We need to pass the knowledge whether it is a candu report try: self.is_candu except AttributeError: self.is_candu = False def create(self, cancel=False): view = navigate_to(self, "Add") view.fill(self.__dict__) view.add_button.click() view = self.create_view(AllReportsView) assert view.is_displayed view.flash.assert_no_error() view.flash.assert_message('Report "{}" was added'.format( self.menu_name)) def update(self, updates): view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() for attr, value in updates.items(): setattr(self, attr, value) view = self.create_view(CustomReportDetailsView) assert view.is_displayed view.flash.assert_no_error() if changed: view.flash.assert_message('Report "{}" was saved'.format( self.menu_name)) else: view.flash.assert_message( 'Edit of Report "{}" was cancelled by the user'.format( self.menu_name)) def delete(self, cancel=False): view = navigate_to(self, "Details") node = view.reports.tree.expand_path("All Reports", view.mycompany_title, "Custom") custom_reports_number = len(view.reports.tree.child_items(node)) view.configuration.item_select("Delete this Report from the Database", handle_alert=not cancel) if cancel: assert view.is_displayed view.flash.assert_no_error() else: # This check is needed because after deleting the last custom report, # the whole "My Company (All EVM Groups)" branch in the tree will be removed. if custom_reports_number > 1: view = self.create_view(AllCustomReportsView) assert view.is_displayed view.flash.assert_no_error() if not BZ(1561779, forced_streams=['5.9']).blocks: view.flash.assert_message( 'Report "{}": Delete successful'.format(self.menu_name)) def get_saved_reports(self): view = navigate_to(self, "Details") results = [] try: for _ in view.saved_reports.paginator.pages(): for row in view.saved_reports.table.rows(): results.append( CustomSavedReport(self, row.run_at.text.encode("utf-8"), row.queued_at.text.encode("utf-8"), self.is_candu)) except NoSuchElementException: pass return results def queue(self, wait_for_finish=False): view = navigate_to(self, "Details") view.report_info.queue_button.click() view.flash.assert_no_error() if wait_for_finish: # Get the queued_at value to always target the correct row queued_at = view.saved_reports.table[0]["Queued At"].text def _get_state(): row = view.saved_reports.table.row(queued_at=queued_at) status = row.status.text.strip().lower() assert status != "error" return status == "complete" wait_for( _get_state, delay=1, message="wait for report generation finished", fail_func=view.reload_button.click, num_sec=300, ) @property def exists(self): try: navigate_to(self, "Details") return True except CandidateNotFound: return False
class Datastore(Pretty, BaseEntity, WidgetasticTaggable): """ Model of an infrastructure datastore in cfme Args: name: Name of the datastore. provider: provider this datastore is attached to. """ pretty_attrs = ['name', 'provider_key'] _param_name = ParamClassName('name') name = attr.ib() provider = attr.ib() type = attr.ib(default=None) def delete(self, cancel=True): """ Deletes a datastore from CFME Args: cancel: Whether to cancel the deletion, defaults to True Note: Datastore must have 0 hosts and 0 VMs for this to work. """ # BZ 1467989 - this button is never getting enabled for some resources view = navigate_to(self, 'Details') view.toolbar.configuration.item_select(VersionPick({ Version.lowest(): 'Remove Datastore', '5.9': 'Remove Datastore from Inventory' }), handle_alert=(not cancel)) view.flash.assert_success_message( 'Delete initiated for Datastore from the CFME Database') def get_hosts(self): """ Returns names of hosts (from quadicons) that use this datastore Returns: List of strings with names or `[]` if no hosts found. """ view = navigate_to(self, 'DetailsFromProvider') view.entities.relationships.click_at('Hosts') hosts_view = view.browser.create_view(RegisteredHostsView) return hosts_view.entities.get_all() def get_vms(self): """ Returns names of VMs (from quadicons) that use this datastore Returns: List of strings with names or `[]` if no vms found. """ view = navigate_to(self, 'Details') if 'VMs' in view.entities.relationships.fields: view.entities.relationships.click_at('VMs') else: view.entities.relationships.click_at('Managed VMs') # todo: to replace with correct view vms_view = view.browser.create_view(DatastoresView) return [vm.name for vm in vms_view.entities.get_all()] def delete_all_attached_vms(self): view = navigate_to(self, 'Details') view.entities.relationships.click_at('Managed VMs') # todo: to replace with correct view vms_view = view.browser.create_view(DatastoresView) for entity in vms_view.entities.get_all(): entity.check() view.toolbar.configuration.item_select(VersionPick({ Version.lowest(): 'Remove selected items', '5.9': 'Remove selected items from Inventory' }), handle_alert=True) wait_for(lambda: bool(len(vms_view.entities.get_all())), fail_condition=True, message="Wait datastore vms to disappear", num_sec=1000, fail_func=self.browser.refresh) def delete_all_attached_hosts(self): view = navigate_to(self, 'Details') view.entities.relationships.click_at('Hosts') hosts_view = view.browser.create_view(RegisteredHostsView) for entity in hosts_view.entities.get_all(): entity.check() view.toolbar.configuration.item_select(VersionPick({ Version.lowest(): 'Remove items', '5.9': 'Remove items from Inventory' }), handle_alert=True) wait_for(lambda: bool(len(hosts_view.entities.get_all())), fail_condition=True, message="Wait datastore hosts to disappear", num_sec=1000, fail_func=self.browser.refresh) @property def exists(self): try: view = navigate_to(self, 'Details') return view.is_displayed except ItemNotFound: return False def run_smartstate_analysis(self, wait_for_task_result=False): """ Runs smartstate analysis on this host Note: The host must have valid credentials already set up for this to work. """ view = navigate_to(self, 'DetailsFromProvider') try: wait_for(lambda: view.toolbar.configuration.item_enabled( 'Perform SmartState Analysis'), fail_condition=False, num_sec=10) except TimedOutError: raise MenuItemNotFound( 'Smart State analysis is disabled for this datastore') view.toolbar.configuration.item_select('Perform SmartState Analysis', handle_alert=True) view.flash.assert_success_message(('"{}": scan successfully ' 'initiated'.format(self.name))) if wait_for_task_result: view = self.appliance.browser.create_view(TasksView) wait_for(lambda: is_datastore_analysis_finished(self.name), delay=15, timeout="15m", fail_func=view.reload.click)
class ComputeRate(Updateable, Pretty, Navigatable): """This class represents a Compute Chargeback rate. Example: .. code-block:: python >>> import cfme.intelligence.chargeback.rates as rates >>> rate = rates.ComputeRate(description=desc, fields={'Used CPU': {'per_time': 'Hourly', 'variable_rate': '3'}, 'Used Disk I/O': {'per_time': 'Hourly', 'variable_rate': '2'}, 'Used Memory': {'per_time': 'Hourly', 'variable_rate': '2'}}) >>> rate.create() >>> rate.delete() Args: description: Rate description currency: Rate currency fields : Rate fields """ pretty_attrs = ['description'] _param_name = ParamClassName('description') RATE_TYPE = 'Compute' def __init__( self, description=None, currency=None, fields=None, appliance=None, ): Navigatable.__init__(self, appliance=appliance) self.description = description self.currency = currency self.fields = fields def __getitem__(self, name): return self.fields.get(name) @property def exists(self): try: navigate_to(self, 'Details', wait_for_view=True) except (ChargebackRateNotFound, TimedOutError): return False else: return True def create(self): # Create a rate in UI view = navigate_to(self, 'Add') view.fill_with( { 'description': self.description, 'currency': self.currency, 'fields': self.fields }, on_change=view.add_button, no_change=view.cancel_button) view = self.create_view(navigator.get_class(self, 'All').VIEW) assert view.is_displayed view.flash.assert_no_error() def copy(self, *args, **kwargs): new_rate = ComputeRate(*args, **kwargs) add_view = navigate_to(self, 'Copy') add_view.fill_with( { 'description': new_rate.description, 'currency': new_rate.currency, 'fields': new_rate.fields }, on_change=add_view.add_button, no_change=add_view.cancel_button) return new_rate def update(self, updates): # Update a rate in UI view = navigate_to(self, 'Edit') view.fill_with(updates, on_change=view.save_button, no_change=view.cancel_button) view = self.create_view(navigator.get_class(self, 'Details').VIEW) view.flash.assert_no_error() def delete(self, cancel=False): """Delete a CB rate in the UI Args: cancel: boolean, whether to cancel the action on alert """ view = navigate_to(self, 'Details', wait_for_view=True) view.toolbar.configuration.item_select('Remove from the VMDB', handle_alert=(not cancel)) view = self.create_view(navigator.get_class(self, 'All').VIEW) assert view.is_displayed view.flash.assert_no_error()
class BaseAlertProfile(BaseEntity, Updateable, Pretty): TYPE = None _param_name = ParamClassName('description') pretty_attrs = ["description", "alerts"] description = attr.ib() alerts = attr.ib(default=None) notes = attr.ib(default=None) def update(self, updates): """Update this Alert Profile in UI. Args: updates: Provided by update() context manager. cancel: Whether to cancel the update (default False). """ view = navigate_to(self, "Edit") changed = view.fill(updates) if changed: view.save_button.click() else: view.cancel_button.click() for attrib, value in updates.items(): setattr(self, attrib, value) view = self.create_view(AlertProfileDetailsView) wait_for(lambda: view.is_displayed, timeout=10, message="wait AlertProfileDetailsView is displayed") view.flash.assert_no_error() if changed: view.flash.assert_message('Alert Profile "{}" was saved'.format( updates.get("description", self.description))) else: view.flash.assert_message( 'Edit of Alert Profile "{}" was cancelled by the user'.format( self.description)) def delete(self, cancel=False): """Delete this Alert Profile in UI. Args: cancel: Whether to cancel the deletion (default False). """ view = navigate_to(self, "Details") view.configuration.item_select("Delete this Alert Profile", handle_alert=not cancel) if cancel: assert view.is_displayed view.flash.assert_no_error() @property def exists(self): """Check existence of this Alert Profile. Returns: :py:class:`bool` signalizing the presence of the Alert Profile in database. """ miq_sets = self.appliance.db.client["miq_sets"] return self.appliance.db.client.session\ .query(miq_sets.description)\ .filter( miq_sets.description == self.description and miq_sets.set_type == "MiqAlertSet")\ .count() > 0 def assign_to(self, assign, selections=None, tag_category=None): """Assigns this Alert Profile to specified objects. Args: assign: Where to assign (The Enterprise, ...). selections: What items to check in the tree. N/A for The Enteprise. tag_category: Only for choices starting with Tagged. N/A for The Enterprise. Returns: Boolean indicating if assignment was made (form fill changed) """ view = navigate_to(self, "Edit assignments") changed = [] if selections is not None: selections = view.selections.CheckNode(selections) changed = view.fill({ "assign_to": assign, "tag_category": tag_category, "selections": selections }) if changed: view.save_button.click() else: view.cancel_button.click() view = self.create_view(AlertProfileDetailsView) assert view.is_displayed view.flash.assert_no_error() return changed
class BaseProvider(Taggable, Updateable, Navigatable, BaseEntity): # List of constants that every non-abstract subclass must have defined # TODO: Navigatable is used to ensure function until the reduced get_crud is # replaced by methods on collections. This will be fixed in next conversion PR _param_name = ParamClassName('name') STATS_TO_MATCH = [] db_types = ["Providers"] ems_events = [] settings_key = None vm_class = None # Set on type specific provider classes for VM/instance class template_class = None # Set on type specific provider classes for VM template class endpoints = attr.ib(default=attr.Factory(factory=dict)) def __attrs_post_init__(self): # attr.ib(convert=prepare_endpoints) doesn't work correctly. this is workaround self.endpoints = prepare_endpoints(self.endpoints) def __hash__(self): return hash(self.key) ^ hash(type(self)) def __eq__(self, other): return type(self) is type(other) and self.key == other.key @property def data(self): """ Returns yaml data for this provider. """ if hasattr(self, 'provider_data') and self.provider_data is not None: return self.provider_data elif self.key is not None: return conf.cfme_data['management_systems'][self.key] else: raise ProviderHasNoKey( 'Provider {} has no key, so cannot get yaml data'.format( self.name)) @property def mgmt(self): """ Returns the mgmt_system using the :py:func:`utils.providers.get_mgmt` method. """ # gotta stash this in here to prevent circular imports from cfme.utils.providers import get_mgmt if self.key: return get_mgmt(self.key) elif getattr(self, 'provider_data', None): return get_mgmt(self.provider_data) else: raise ProviderHasNoKey( 'Provider {} has no key, so cannot get mgmt system'.format( self.name)) @property def type(self): return self.type_name @property def rest_api_entity(self): return self.appliance.rest_api.collections.providers.get( name=self.name) @property def id(self): """" Return the ID associated with the specified provider name """ return self.rest_api_entity.id @property def version(self): return self.data['version'] def deployment_helper(self, deploy_args): """ Used in utils.virtual_machines and usually overidden""" return {} @property def default_endpoint(self): return self.endpoints.get('default') if hasattr(self, 'endpoints') else None def create(self, cancel=False, validate_credentials=True, check_existing=False, validate_inventory=False): """ Creates a provider in the UI Args: cancel (boolean): Whether to cancel out of the creation. The cancel is done after all the information present in the Provider has been filled in the UI. validate_credentials (boolean): Whether to validate credentials - if True and the credentials are invalid, an error will be raised. check_existing (boolean): Check if this provider already exists, skip if it does validate_inventory (boolean): Whether or not to block until the provider stats in CFME match the stats gleaned from the backend management system Returns: True if it was created, False if it already existed """ if check_existing and self.exists: created = False else: created = True logger.info('Setting up Provider: %s', self.key) add_view = navigate_to(self, 'Add') if not cancel or (cancel and any(self.view_value_mapping.values())): # Workaround for BZ#1526050 from cfme.infrastructure.provider.virtualcenter import VMwareProvider if self.appliance.version == '5.8.3.0' and self.one_of( VMwareProvider): add_view.fill({'prov_type': 'Red Hat Virtualization'}) elif '5.8.3.0' < self.appliance.version < '5.9': import warnings warnings.warn('REMOVE ME: BZ#1526050') # filling main part of dialog add_view.fill(self.view_value_mapping) if not cancel or (cancel and self.endpoints): # filling endpoints for endpoint_name, endpoint in self.endpoints.items(): try: # every endpoint class has name like 'default', 'events', etc. # endpoints view can have multiple tabs, the code below tries # to find right tab by passing endpoint name to endpoints view endp_view = getattr( self.endpoints_form(parent=add_view), endpoint_name) except AttributeError: # tabs are absent in UI when there is only single (default) endpoint endp_view = self.endpoints_form(parent=add_view) endp_view.fill(endpoint.view_value_mapping) # filling credentials if hasattr(endpoint, 'credentials'): endp_view.fill(endpoint.credentials.view_value_mapping) # sometimes we have cases that we need to validate even though # there is no credentials, such as Hawkular endpoint if (validate_credentials and hasattr(endp_view, 'validate') and endp_view.validate.is_displayed): # there are some endpoints which don't demand validation like # RSA key pair endp_view.validate.click() # Flash message widget is in add_view, not in endpoints tab logger.info( 'Validating credentials flash message for endpoint %s', endpoint_name) add_view.flash.assert_no_error() add_view.flash.assert_success_message( 'Credential validation was successful') main_view = self.create_view(navigator.get_class(self, 'All').VIEW) if cancel: created = False add_view.cancel.click() cancel_text = ('Add of {} Provider was ' 'cancelled by the user'.format( self.string_name)) main_view.flash.assert_message(cancel_text) main_view.flash.assert_no_error() else: add_view.add.click() if main_view.is_displayed: success_text = '{} Providers "{}" was saved'.format( self.string_name, self.name) main_view.flash.assert_message(success_text) else: add_view.flash.assert_no_error() raise AssertionError( "Provider wasn't added. It seems form isn't accurately" " filled") if validate_inventory: self.validate() return created def _fill_provider_attributes(self, provider_attributes): """Fills provider data. Helper method for ``self.create_rest`` """ if getattr(self, "region", None): if isinstance(self.region, dict): provider_attributes["provider_region"] = VersionPicker( self.region).pick(self.appliance.version) else: provider_attributes["provider_region"] = self.region if getattr(self, "project", None): provider_attributes["project"] = self.project if self.type_name in ('openstack_infra', 'openstack'): if getattr(self, 'api_version', None): version = 'v3' if 'v3' in self.api_version else 'v2' provider_attributes['api_version'] = version if version == 'v3' and getattr(self, 'keystone_v3_domain_id', None): provider_attributes['uid_ems'] = self.keystone_v3_domain_id if self.type_name == "azure": provider_attributes["uid_ems"] = self.tenant_id provider_attributes["provider_region"] = self.region.lower( ).replace(" ", "") if getattr(self, "subscription_id", None): provider_attributes["subscription"] = self.subscription_id def _fill_default_endpoint_dicts(self, provider_attributes, connection_configs): """Fills dicts with default endpoint data. Helper method for ``self.create_rest`` """ default_connection = {"endpoint": {"role": "default"}} endpoint_default = self.endpoints["default"] if getattr(endpoint_default.credentials, "principal", None): provider_attributes["credentials"] = { "userid": endpoint_default.credentials.principal, "password": endpoint_default.credentials.secret, } elif getattr(endpoint_default.credentials, "service_account", None): default_connection["authentication"] = { "type": "AuthToken", "auth_type": "default", "auth_key": endpoint_default.credentials.service_account, } connection_configs.append(default_connection) else: raise AssertionError( "Provider wasn't added. " "No credentials info found for provider {}.".format(self.name)) cert = getattr(endpoint_default, "ca_certs", None) if cert and self.appliance.version >= "5.8": default_connection["endpoint"]["certificate_authority"] = cert connection_configs.append(default_connection) if hasattr(endpoint_default, "verify_tls"): default_connection["endpoint"][ "verify_ssl"] = 1 if endpoint_default.verify_tls else 0 connection_configs.append(default_connection) if getattr(endpoint_default, "api_port", None): default_connection["endpoint"]["port"] = endpoint_default.api_port connection_configs.append(default_connection) if getattr(endpoint_default, "security_protocol", None): security_protocol = endpoint_default.security_protocol.lower() if security_protocol in ('basic (ssl)', 'ssl without validation'): security_protocol = "ssl" elif security_protocol == "ssl": security_protocol = 'ssl-with-validation' default_connection["endpoint"][ "security_protocol"] = security_protocol connection_configs.append(default_connection) def _fill_candu_endpoint_dicts(self, provider_attributes, connection_configs): """Fills dicts with candu endpoint data. Helper method for ``self.create_rest`` """ if "candu" not in self.endpoints: return endpoint_candu = self.endpoints["candu"] if isinstance(provider_attributes["credentials"], dict): provider_attributes["credentials"] = [ provider_attributes["credentials"] ] provider_attributes["credentials"].append({ "userid": endpoint_candu.credentials.principal, "password": endpoint_candu.credentials.secret, "auth_type": "metrics", }) candu_connection = { "endpoint": { "hostname": endpoint_candu.hostname, "path": endpoint_candu.database, "role": "metrics", }, } if getattr(endpoint_candu, "api_port", None): candu_connection["endpoint"]["port"] = endpoint_candu.api_port if hasattr(endpoint_candu, "verify_tls") and not endpoint_candu.verify_tls: candu_connection["endpoint"]["verify_ssl"] = 0 connection_configs.append(candu_connection) def _fill_rsa_endpoint_dicts(self, provider_attributes, connection_configs): """Fills dicts with rsa endpoint data. Helper method for ``self.create_rest`` """ if "rsa_keypair" not in self.endpoints: return endpoint_rsa = self.endpoints["rsa_keypair"] if isinstance(provider_attributes["credentials"], dict): provider_attributes["credentials"] = [ provider_attributes["credentials"] ] provider_attributes["credentials"].append({ "userid": endpoint_rsa.credentials.principal, "auth_key": endpoint_rsa.credentials.secret, "auth_type": "ssh_keypair", }) def _compile_connection_configurations(self, provider_attributes, connection_configs): """Compiles togetger all dicts with data for ``connection_configurations``. Helper method for ``self.create_rest`` """ provider_attributes["connection_configurations"] = [] appended = [] for config in connection_configs: role = config["endpoint"]["role"] if role not in appended: provider_attributes["connection_configurations"].append(config) appended.append(role) def create_rest(self, check_existing=False, validate_inventory=False): """ Creates a provider using REST Args: check_existing (boolean): Check if this provider already exists, skip if it does validate_inventory (boolean): Whether or not to block until the provider stats in CFME match the stats gleaned from the backend management system Returns: True if it was created, False if it already existed """ if check_existing and self.exists: return False logger.info("Setting up provider via REST: %s", self.key) # provider attributes provider_attributes = { "hostname": self.hostname, "ipaddress": self.ip_address, "name": self.name, "type": "ManageIQ::Providers::{}".format(self.db_types[0]), } # data for provider_attributes['connection_configurations'] connection_configs = [] # produce final provider_attributes self._fill_provider_attributes(provider_attributes) self._fill_default_endpoint_dicts(provider_attributes, connection_configs) self._fill_candu_endpoint_dicts(provider_attributes, connection_configs) self._fill_rsa_endpoint_dicts(provider_attributes, connection_configs) self._compile_connection_configurations(provider_attributes, connection_configs) try: self.appliance.rest_api.collections.providers.action.create( **provider_attributes) except APIException as err: raise AssertionError("Provider wasn't added: {}".format(err)) response = self.appliance.rest_api.response if not response: raise AssertionError( "Provider wasn't added, status code {}".format( response.status_code)) if validate_inventory: self.validate() self.appliance.rest_api.response = response return True def update(self, updates, cancel=False, validate_credentials=True): """ Updates a provider in the UI. Better to use utils.update.update context manager than call this directly. Args: updates (dict): fields that are changing. cancel (boolean): whether to cancel out of the update. validate_credentials (boolean): whether credentials have to be validated """ edit_view = navigate_to(self, 'Edit') # todo: to replace/merge this code with create # update values: # filling main part of dialog endpoints = updates.pop('endpoints', None) if updates: edit_view.fill(updates) # filling endpoints if endpoints: endpoints = prepare_endpoints(endpoints) for endpoint in endpoints.values(): # every endpoint class has name like 'default', 'events', etc. # endpoints view can have multiple tabs, the code below tries # to find right tab by passing endpoint name to endpoints view try: endp_view = getattr(self.endpoints_form(parent=edit_view), endpoint.name) except AttributeError: # tabs are absent in UI when there is only single (default) endpoint endp_view = self.endpoints_form(parent=edit_view) endp_view.fill(endpoint.view_value_mapping) # filling credentials # the code below looks for existing endpoint equal to passed one and # compares their credentials. it fills passed credentials # if credentials are different cur_endpoint = self.endpoints[endpoint.name] if hasattr(endpoint, 'credentials'): if not hasattr(cur_endpoint, 'credentials') or \ endpoint.credentials != cur_endpoint.credentials: if hasattr(endp_view, 'change_password'): endp_view.change_password.click() elif hasattr(endp_view, 'change_key'): endp_view.change_key.click() else: NotImplementedError( "Such endpoint doesn't have change password/key button" ) endp_view.fill(endpoint.credentials.view_value_mapping) # sometimes we have cases that we need to validate even though # there is no credentials, such as Hawkular endpoint if (validate_credentials and hasattr(endp_view, 'validate') and endp_view.validate.is_displayed): endp_view.validate.click() # cloud rhos provider always requires validation of all endpoints # there should be a bz about that from cfme.cloud.provider.openstack import OpenStackProvider if self.one_of(OpenStackProvider): for endp in self.endpoints.values(): endp_view = getattr(self.endpoints_form(parent=edit_view), endp.name) if hasattr(endp_view, 'validate') and endp_view.validate.is_displayed: endp_view.validate.click() details_view = self.create_view( navigator.get_class(self, 'Details').VIEW) main_view = self.create_view(navigator.get_class(self, 'All').VIEW) if cancel: edit_view.cancel.click() cancel_text = 'Edit of {type} Provider "{name}" ' \ 'was cancelled by the user'.format(type=self.string_name, name=self.name) main_view.flash.assert_message(cancel_text) main_view.flash.assert_no_error() else: edit_view.save.click() if endpoints: for endp_name, endp in endpoints.items(): self.endpoints[endp_name] = endp if updates: self.name = updates.get('name', self.name) success_text = '{} Provider "{}" was saved'.format( self.string_name, self.name) if main_view.is_displayed: # since 5.8.1 main view is displayed when edit starts from main view main_view.flash.assert_message(success_text) elif details_view.is_displayed: # details view is always displayed up to 5.8.1 details_view.flash.assert_message(success_text) else: edit_view.flash.assert_no_error() raise AssertionError( "Provider wasn't updated. It seems form isn't accurately" " filled") def delete(self, cancel=True): """ Deletes a provider from CFME using UI Args: cancel: Whether to cancel the deletion, defaults to True """ view = navigate_to(self, 'Details') item_title = 'Remove this {} Provider from Inventory' view.toolbar.configuration.item_select(item_title.format( self.string_name), handle_alert=not cancel) if not cancel: msg = ('Delete initiated for 1 {} Provider from ' 'the {} Database'.format(self.string_name, self.appliance.product_name)) view.flash.assert_success_message(msg) def delete_rest(self): """Deletes a provider from CFME using REST""" provider_rest = self.appliance.rest_api.collections.providers.get( name=self.name) try: provider_rest.action.delete() except APIException as err: raise AssertionError("Provider wasn't deleted: {}".format(err)) response = self.appliance.rest_api.response if not response: raise AssertionError( "Provider wasn't deleted, status code {}".format( response.status_code)) def setup(self): """ Sets up the provider robustly """ # TODO: Eventually this will become Sentakuified, but only after providers is CEMv3 if self.category in ['cloud', 'infra', 'physical']: return self.create_rest(check_existing=True, validate_inventory=True) else: return self.create(cancel=False, validate_credentials=True, check_existing=True, validate_inventory=True) def delete_if_exists(self, *args, **kwargs): """Combines ``.exists`` and ``.delete()`` as a shortcut for ``request.addfinalizer`` Returns: True if provider existed and delete was initiated, False otherwise """ if self.exists: self.delete(*args, **kwargs) return True return False @variable(alias='rest') def is_refreshed(self, refresh_timer=None, refresh_delta=600): if refresh_timer: if refresh_timer.is_it_time(): logger.info(' Time for a refresh!') self.refresh_provider_relationships() refresh_timer.reset() rdate = self.last_refresh_date() if not rdate: return False td = self.appliance.utc_time() - rdate if td > datetime.timedelta(0, refresh_delta): self.refresh_provider_relationships() return False else: return True def validate(self): refresh_timer = RefreshTimer(time_for_refresh=300) try: wait_for(self.is_refreshed, [refresh_timer], message="is_refreshed", num_sec=1000, delay=60, handle_exception=True) except Exception: # To see the possible error. self.load_details(refresh=True) raise else: if self.last_refresh_error() is not None: raise AddProviderError( "Cannot validate the provider. Error occured: {}".format( self.last_refresh_error())) def validate_stats(self, ui=False): """ Validates that the detail page matches the Providers information. This method logs into the provider using the mgmt_system interface and collects a set of statistics to be matched against the UI. The details page is then refreshed continuously until the matching of all items is complete. A error will be raised if the match is not complete within a certain defined time period. """ # If we're not using db, make sure we are on the provider detail page if ui: self.load_details() # Initial bullet check if self._do_stats_match(self.mgmt, self.STATS_TO_MATCH, ui=ui): self.mgmt.disconnect() return else: # Set off a Refresh Relationships method = 'ui' if ui else None self.refresh_provider_relationships(method=method) refresh_timer = RefreshTimer(time_for_refresh=300) wait_for(self._do_stats_match, [self.mgmt, self.STATS_TO_MATCH, refresh_timer], {'ui': ui}, message="do_stats_match_db", num_sec=1000, delay=60) self.mgmt.disconnect() @variable(alias='rest') def refresh_provider_relationships(self, from_list_view=False): # from_list_view is ignored as it is included here for sake of compatibility with UI call. logger.debug('Refreshing provider relationships') col = self.appliance.rest_api.collections.providers.find_by( name=self.name) try: col[0].action.refresh() except IndexError: raise Exception("Provider collection empty") @refresh_provider_relationships.variant('ui') def refresh_provider_relationships_ui(self, from_list_view=False): """Clicks on Refresh relationships button in provider""" if from_list_view: view = navigate_to(self, 'All') entity = view.entities.get_entity(name=self.name, surf_pages=True) entity.check() else: view = navigate_to(self, 'Details') view.toolbar.configuration.item_select(self.refresh_text, handle_alert=True) @variable(alias='rest') def last_refresh_date(self): try: col = self.appliance.rest_api.collections.providers.find_by( name=self.name)[0] return col.last_refresh_date except AttributeError: return None @variable(alias='rest') def last_refresh_error(self): try: col = self.appliance.rest_api.collections.providers.find_by( name=self.name)[0] return col.last_refresh_error except AttributeError: return None def _num_db_generic(self, table_str): """ Fetch number of rows related to this provider in a given table Args: table_str: Name of the table; e.g. 'vms' or 'hosts' """ res = self.appliance.db.client.engine.execute( "SELECT count(*) " "FROM ext_management_systems, {0} " "WHERE {0}.ems_id=ext_management_systems.id " "AND ext_management_systems.name='{1}'".format( table_str, self.name)) return int(res.first()[0]) def _do_stats_match(self, client, stats_to_match=None, refresh_timer=None, ui=False): """ A private function to match a set of statistics, with a Provider. This function checks if the list of stats match, if not, the page is refreshed. Note: Provider mgmt_system uses the same key names as this Provider class to avoid having to map keyname/attributes e.g. ``num_template``, ``num_vm``. Args: client: A provider mgmt_system instance. stats_to_match: A list of key/attribute names to match. Raises: KeyError: If the host stats does not contain the specified key. ProviderHasNoProperty: If the provider does not have the property defined. """ host_stats = client.stats(*stats_to_match) method = None if ui: self.browser.selenium.refresh() method = 'ui' if refresh_timer: if refresh_timer.is_it_time(): logger.info(' Time for a refresh!') self.refresh_provider_relationships() refresh_timer.reset() for stat in stats_to_match: try: cfme_stat = getattr(self, stat)(method=method) success, value = tol_check(host_stats[stat], cfme_stat, min_error=0.05, low_val_correction=2) logger.info( ' Matching stat [%s], Host(%s), CFME(%s), ' 'with tolerance %s is %s', stat, host_stats[stat], cfme_stat, value, success) if not success: return False except KeyError: raise HostStatsNotContains( "Host stats information does not contain '{}'".format( stat)) except AttributeError: raise ProviderHasNoProperty( "Provider does not know how to get '{}'".format(stat)) else: return True @property def exists(self): """ Returns ``True`` if a provider of the same name exists on the appliance """ if self.name in self.appliance.managed_provider_names: return True return False def wait_for_delete(self): try: provider_rest = self.appliance.rest_api.collections.providers.get( name=self.name) except (ValueError, APIException ): # if the record doesn't exist, APIException from 404 return logger.info('Waiting for a provider to delete...') provider_rest.wait_not_exists(message="Wait provider to disappear", num_sec=1000) def load_details(self, refresh=False): """To be compatible with the Taggable and PolicyProfileAssignable mixins. Returns: ProviderDetails view """ view = navigate_to(self, 'Details') if refresh: view.toolbar.reload.click() return view @classmethod def get_credentials(cls, credential_dict, cred_type=None): """Processes a credential dictionary into a credential object. Args: credential_dict: A credential dictionary. cred_type: Type of credential (None, token, ssh, amqp, ...) Returns: A :py:class:`cfme.base.credential.Credential` instance. """ domain = credential_dict.get('domain') token = credential_dict.get('token') if not cred_type: return Credential(principal=credential_dict['username'], secret=credential_dict['password'], domain=domain) elif cred_type == 'amqp': return EventsCredential(principal=credential_dict['username'], secret=credential_dict['password']) elif cred_type == 'ssh': return SSHCredential(principal=credential_dict['username'], secret=credential_dict['password']) elif cred_type == 'candu': return CANDUCredential(principal=credential_dict['username'], secret=credential_dict['password']) elif cred_type == 'token': return TokenCredential(token=token) @classmethod def get_credentials_from_config(cls, credential_config_name, cred_type=None): """Retrieves the credential by its name from the credentials yaml. Args: credential_config_name: The name of the credential in the credentials yaml. cred_type: Type of credential (None, token, ssh, amqp, ...) Returns: A :py:class:`cfme.base.credential.Credential` instance. """ creds = conf.credentials[credential_config_name] return cls.get_credentials(creds, cred_type=cred_type) @classmethod def process_credential_yaml_key(cls, cred_yaml_key, cred_type=None): """Function that detects if it needs to look up credentials in the credential yaml and acts as expected. If you pass a dictionary, it assumes it does not need to look up in the credentials yaml file. If anything else is passed, it continues with looking up the credentials in the yaml file. Args: cred_yaml_key: Either a string pointing to the credentials.yaml or a dictionary which is considered as the credentials. Returns: :py:class:`cfme.base.credential.Credential` instance """ if isinstance(cred_yaml_key, dict): return cls.get_credentials(cred_yaml_key, cred_type=cred_type) else: return cls.get_credentials_from_config(cred_yaml_key, cred_type=cred_type) # Move to collection @classmethod def clear_providers(cls): """ Clear all providers of given class on the appliance """ from cfme.utils.appliance import current_appliance as app # Delete all matching for prov in app.managed_known_providers: if prov.one_of(cls): logger.info('Deleting provider: %s', prov.name) prov.delete_rest() # Wait for all matching to be deleted for prov in app.managed_known_providers: if prov.one_of(cls): prov.wait_for_delete() def one_of(self, *classes): """ Returns true if provider is an instance of any of the classes or sublasses there of""" return isinstance(self, classes) # These methods need to be overridden in the provider specific classes def get_console_connection_status(self): raise NotImplementedError( "This method is not implemented for given provider") def get_remote_console_canvas(self): raise NotImplementedError( "This method is not implemented for given provider") def get_console_ctrl_alt_del_btn(self): raise NotImplementedError( "This method is not implemented for given provider") def get_console_fullscreen_btn(self): raise NotImplementedError( "This method is not implemented for given provider") def get_all_provider_ids(self): """ Returns an integer list of provider ID's via the REST API """ # TODO: Move to ProviderCollection logger.debug('Retrieving the list of provider ids') provider_ids = [] try: for prov in self.appliance.rest_api.collections.providers.all: provider_ids.append(prov.id) except APIException: return None return provider_ids def get_all_vm_ids(self): """ Returns an integer list of vm ID's via the REST API """ # TODO: Move to VMCollection or BaseVMCollection logger.debug('Retrieving the list of vm ids') vm_ids = [] try: for vm in self.appliance.rest_api.collections.vms.all: vm_ids.append(vm.id) except APIException: return None return vm_ids def get_all_host_ids(self): """ Returns an integer list of host ID's via the Rest API """ # TODO: Move to HostCollection logger.debug('Retrieving the list of host ids') host_ids = [] try: for host in self.appliance.rest_api.collections.hosts.all: host_ids.append(host.id) except APIException: return None return host_ids def get_all_template_ids(self): """Returns an integer list of template ID's via the Rest API""" # TODO: Move to TemplateCollection logger.debug('Retrieving the list of template ids') template_ids = [] try: for template in self.appliance.rest_api.collections.templates.all: template_ids.append(template.id) except APIException: return None return template_ids def get_provider_details(self, provider_id): """Returns the name, and type associated with the provider_id""" # TODO: Move to ProviderCollection.find logger.debug( 'Retrieving the provider details for ID: {}'.format(provider_id)) details = {} try: prov = self.appliance.rest_api.collections.providers.get( id=provider_id) except APIException: return None details['id'] = prov.id details['name'] = prov.name details['type'] = prov.type return details def get_vm_details(self, vm_id): """ Returns the name, type, vendor, host_id, and power_state associated with the vm_id. """ # TODO: Move to VMCollection.find logger.debug('Retrieving the VM details for ID: {}'.format(vm_id)) details = {} try: vm = self.appliance.rest_api.collections.vms.get(id=vm_id) except APIException: return None details['id'] = vm.id details['ems_id'] = vm.ems_id details['name'] = vm.name details['type'] = vm.type details['vendor'] = vm.vendore details['host_id'] = vm.host_id details['power_state'] = vm.power_state return details def get_template_details(self, template_id): """ Returns the name, type, and guid associated with the template_id """ # TODO: Move to TemplateCollection.find logger.debug( 'Retrieving the template details for ID: {}'.format(template_id)) template_details = {} try: template = self.appliance.rest_api.collections.templates.get( id=template_id) except APIException: return None template_details['name'] = template.name template_details['type'] = template.type template_details['guid'] = template.guid return template_details def get_all_template_details(self): """ Returns a dictionary mapping template ids to their name, type, and guid """ # TODO: Move to TemplateCollection.all all_details = {} for id in self.get_all_template_ids(): all_details[id] = self.get_template_details(id) return all_details def get_vm_id(self, vm_name): """ Return the ID associated with the specified VM name """ # TODO: Get Provider object from VMCollection.find, then use VM.id to get the id logger.debug('Retrieving the ID for VM: {}'.format(vm_name)) for vm_id in self.get_all_vm_ids(): details = self.get_vm_details(vm_id) if details['name'] == vm_name: return vm_id def get_vm_ids(self, vm_names): """ Returns a dictionary mapping each VM name to it's id """ # TODO: Move to VMCollection.find or VMCollection.all name_list = vm_names[:] logger.debug('Retrieving the IDs for {} VM(s)'.format(len(name_list))) id_map = {} for vm_id in self.get_all_vm_ids(): if not name_list: break vm_name = self.get_vm_details(vm_id)['name'] if vm_name in name_list: id_map[vm_name] = vm_id name_list.remove(vm_name) return id_map def get_template_guids(self, template_dict): """ Returns a list of tuples. The inner tuples are formated so that each guid is in index 0, and its provider's name is in index 1. Expects a dictionary mapping a provider to its templates """ # TODO: Move to TemplateCollection result_list = [] all_template_details = self.get_all_template_details() for provider, templates in template_dict.items(): for template_name in templates: inner_tuple = () for id in all_template_details: if ((all_template_details[id]['name'] == template_name) and (self.db_types[0] in all_template_details[id]['type'])): inner_tuple += (all_template_details[id]['guid'], ) inner_tuple += (provider, ) result_list.append(inner_tuple) return result_list