From 1c8087eb4e7f0a80016401bfcb4e8432b52452ef Mon Sep 17 00:00:00 2001 From: Tasos Papaioannou Date: Wed, 16 Oct 2019 11:18:26 -0400 Subject: [PATCH] Automate multiple vm retirement, fix ec2 instance-backed retirement --- cfme/cloud/instance/__init__.py | 2 + cfme/common/vm.py | 171 +++++++---- cfme/fixtures/templates.py | 10 + cfme/infrastructure/virtual_machines.py | 4 +- .../cloud_infra_common/test_retirement.py | 283 ++++++++++++++---- .../test_retirement_manual.py | 103 ------- requirements/frozen.txt | 2 +- 7 files changed, 359 insertions(+), 216 deletions(-) delete mode 100644 cfme/tests/cloud_infra_common/test_retirement_manual.py diff --git a/cfme/cloud/instance/__init__.py b/cfme/cloud/instance/__init__.py index a9809b0676..d2d1713990 100644 --- a/cfme/cloud/instance/__init__.py +++ b/cfme/cloud/instance/__init__.py @@ -33,6 +33,7 @@ from widgetastic_manageiq import Accordion from widgetastic_manageiq import CompareToolBarActionsView from widgetastic_manageiq import ManageIQTree +from widgetastic_manageiq import PaginationPane from widgetastic_manageiq import Search from widgetastic_manageiq import SummaryTable @@ -114,6 +115,7 @@ def is_displayed(self): sidebar = View.nested(InstanceAccordion) search = View.nested(Search) including_entities = View.include(VMEntities, use_parent=True) + paginator = PaginationPane() class InstanceProviderAllView(CloudInstanceView): diff --git a/cfme/common/vm.py b/cfme/common/vm.py index 2429693bd6..58e0256b95 100644 --- a/cfme/common/vm.py +++ b/cfme/common/vm.py @@ -2,7 +2,6 @@ import json from datetime import date from datetime import datetime -from datetime import timedelta import attr from cached_property import cached_property @@ -18,6 +17,7 @@ from cfme.common.vm_console import ConsoleMixin from cfme.common.vm_views import DriftAnalysis from cfme.common.vm_views import DriftHistory +from cfme.common.vm_views import RetirementViewWithOffset from cfme.common.vm_views import RightSizeView from cfme.common.vm_views import VMPropertyDetailView from cfme.exceptions import CFMEException @@ -713,9 +713,14 @@ def exists_on_provider(self): return self.provider.mgmt.does_vm_exist(self.name) def retire(self): + """Navigate to VM's | Instance's Details page and retire it by selecting + Lifecycle > Retire this VM | Instance + """ view = navigate_to(self, 'Details', use_resetter=False) view.toolbar.reload.click() view.toolbar.lifecycle.item_select(self.TO_RETIRE, handle_alert=True) + + view = self.create_view(RequestsView) view.flash.assert_no_error() def power_control_from_cfme(self, option, cancel=True, from_details=False): @@ -869,8 +874,6 @@ def set_retirement_date(self, when=None, offset=None, warn=None): offset: :py:class:`dict` with months, weeks, days, hours keys. other keys ignored warn: When to warn, fills the select in the form in case the ``when`` is specified. - Note: this should be moved up to the common VM class when infra+cloud+common are all WT - If when and offset are both None, this removes retirement date Examples: @@ -889,61 +892,35 @@ def set_retirement_date(self, when=None, offset=None, warn=None): An enhancement to cfme.utils.timeutil extending timedelta would be great for making this a bit cleaner """ - view = navigate_to(self, 'SetRetirement') + changed = False fill_date = None fill_offset = None - # explicit is/not None use here because of empty strings and dicts - - if when is not None and offset is not None: - raise ValueError('set_retirement_date takes when or offset, but not both') - if when is not None and not isinstance(when, (datetime, date)): - raise ValueError('when argument must be a datetime object') - - # due to major differences between the forms and their interaction, I'm splitting this - # method into two major blocks, one for each version. As a result some patterns will be - # repeated in both blocks - # This will allow for making changes to one version or the other without strange - # interaction in the logic - - # format the date - # needs 4 digit year for fill - # displayed 2 digit year for flash message - # 59z/G-release retirement - changed = False # just in case it isn't set in logic - if when is not None and offset is None: - # Specific datetime retire, H+M are 00:00 by default if just date passed - fill_date = when.strftime('%m/%d/%Y %H:%M') # 4 digit year - msg_date = when.strftime('%m/%d/%y %H:%M UTC') # two digit year and timestamp - msg = 'Retirement date set to {}'.format(msg_date) - elif when is None and offset is None: - # clearing retirement date with space in textinput, - # using space here as with empty string calendar input is not cleared correctly - fill_date = ' ' - msg = 'Retirement date removed' - elif offset is not None: - # retirement by offset - fill_date = None + if when and offset: + raise ValueError("set_retirement_date takes 'when' or 'offset', but not both") + if when and not isinstance(when, (datetime, date)): + raise ValueError("'when' argument must be a datetime object") + + if offset: fill_offset = {k: v for k, v in offset.items() if k in ['months', 'weeks', 'days', 'hours']} - # hack together an offset - # timedelta can take weeks, but not months - # copy and pop, only used to generate message, not used for form fill - offset_copy = fill_offset.copy() - if 'months' in offset_copy: - new_weeks = offset_copy.get('weeks', 0) + int(offset_copy.pop('months', 0)) * 4 - offset_copy.update({'weeks': new_weeks}) - - msg_date = datetime.utcnow() + timedelta(**offset_copy) - msg = 'Retirement date set to {}'.format(msg_date.strftime('%m/%d/%y %H:%M UTC')) - # TODO move into before_fill when no need to click away from datetime picker + elif when: + # format using 4-digit year for form fill + fill_date = when.strftime('%m/%d/%Y %H:%M') + else: + # Clear retirement date with a space in TextInput widget. + # Using space here because empty string does not clear date correctly + fill_date = ' ' + + # navigate to VM's Details page and set retirement date + view = navigate_to(self, 'SetRetirement') view.form.fill({ - 'retirement_mode': - 'Time Delay from Now' if fill_offset else 'Specific Date and Time'}) + 'retirement_mode': 'Time Delay from Now' if fill_offset else 'Specific Date and Time'}) view.flush_widget_cache() # since retirement_date is conditional widget - if fill_date is not None: # specific check because of empty string - # two part fill, widget seems to block warn selection when open + + if fill_date: + # two-part fill. widget seems to block warn selection when open. changed_date = view.form.fill({ 'retirement_date': {'datetime_select': fill_date}}) view.title.click() # close datetime widget @@ -953,16 +930,16 @@ def set_retirement_date(self, when=None, offset=None, warn=None): changed = view.form.fill({ 'retirement_date': fill_offset, 'retirement_warning': warn}) - # Form save and flash messages are the same between versions if changed: view.form.save.click() else: - logger.info('No form changes for setting retirement, clicking cancel') + logger.info("No form changes for setting retirement, clicking cancel") view.form.cancel.click() - msg = 'Set/remove retirement date was cancelled by the user' - if self.DETAILS_VIEW_CLASS is not None: + + if self.DETAILS_VIEW_CLASS: view = self.create_view(self.DETAILS_VIEW_CLASS, wait='5s') - view.flash.assert_success_message(msg) + + view.flash.assert_no_error() def equal_drift_results(self, drift_section, section, *indexes): """Compares drift analysis results of a row specified by it's title text. @@ -1011,6 +988,90 @@ def _select_rows(indexes): class VMCollection(BaseVMCollection): ENTITY = VM + def retire(self, entities): + """Select and then retire one or more VMs|Instances from the All VMs|Instances page. + + Args: + entities (list): List of one or more InfraVM | Instance objects. This method + is intended to be called from either an InfraVMCollection instance (with a list + of InfraVM objects, or an InstanceCollection instance (with a list of Instance + objects). + """ + view = navigate_to(self, 'All') + if view.paginator.is_displayed: + view.paginator.set_items_per_page(1000) + for e in entities: + view.entities.get_entity(surf_pages=True, name=e.name).ensure_checked() + view.toolbar.lifecycle.item_select('Retire selected items', handle_alert=True) + + view = self.create_view(RequestsView) + view.flash.assert_no_error() + + def set_retirement_date(self, entities, when=None, offset=None, warn=None): + """Select and then set the retirement date for one or more VMs|Instances from + the All VMs|Instances page. + + Args: + entities: :py:class:`list` of one or more InfraVM | Instance objects. This method + is intended to be called from either an InfraVMCollection instance (with a list + of InfraVM objects, or an InstanceCollection instance (with a list of Instance + objects). + The other arguments are the same as in the :py:class:`VM` method. + """ + changed = False + fill_date = None + fill_offset = None + + if when and offset: + raise ValueError("set_retirement_date takes 'when' or 'offset', but not both") + if when and not isinstance(when, (datetime, date)): + raise ValueError("'when' argument must be a datetime object") + + if offset: + fill_offset = {k: v + for k, v in offset.items() + if k in ['months', 'weeks', 'days', 'hours']} + elif when: + # format using 4-digit year for form fill + fill_date = when.strftime('%m/%d/%Y %H:%M') + else: + # Clear retirement date with a space in TextInput widget. + # Using space here because empty string does not clear date correctly + fill_date = ' ' + + # set retirement date from All VMs page + view = navigate_to(self, 'All') + if view.paginator.is_displayed: + view.paginator.set_items_per_page(1000) + for e in entities: + view.entities.get_entity(surf_pages=True, name=e.name).ensure_checked() + view.toolbar.lifecycle.item_select('Set Retirement Dates') + + view = self.create_view(RetirementViewWithOffset) + view.form.fill({ + 'retirement_mode': 'Time Delay from Now' if fill_offset else 'Specific Date and Time'}) + view.flush_widget_cache() # since retirement_date is conditional widget + + if fill_date: + # two-part fill. widget seems to block warn selection when open. + changed_date = view.form.fill({ + 'retirement_date': {'datetime_select': fill_date}}) + view.title.click() # close datetime widget + changed_warn = view.form.fill({'retirement_warning': warn}) + changed = changed_date or changed_warn + elif fill_offset: + changed = view.form.fill({ + 'retirement_date': fill_offset, 'retirement_warning': warn}) + + if changed: + view.form.save.click() + else: + logger.info("No form changes for setting retirement, clicking cancel") + view.form.cancel.click() + + view = self.create_view(navigator.get_class(self, 'All').VIEW, wait='5s') + view.flash.assert_no_error() + @attr.s class Template(BaseVM, _TemplateMixin): diff --git a/cfme/fixtures/templates.py b/cfme/fixtures/templates.py index a46be94f06..52c8848a4f 100644 --- a/cfme/fixtures/templates.py +++ b/cfme/fixtures/templates.py @@ -235,3 +235,13 @@ def rhel7_minimal(provider): @pytest.fixture(scope="module") def rhel7_minimal_modscope(provider): return _get_template(provider, 'rhel7_minimal') + + +@pytest.fixture(scope="function") +def s3_template(provider): + return _get_template(provider, 's3_template') + + +@pytest.fixture(scope="module") +def s3_template_modscope(provider): + return _get_template(provider, 's3_template') diff --git a/cfme/infrastructure/virtual_machines.py b/cfme/infrastructure/virtual_machines.py index 0d8bcd3753..08430a0923 100644 --- a/cfme/infrastructure/virtual_machines.py +++ b/cfme/infrastructure/virtual_machines.py @@ -165,7 +165,7 @@ class VmsTemplatesAllView(InfraVmView): toolbar = View.nested(VMToolbar) sidebar = View.nested(VmsTemplatesAccordion) including_entities = View.include(VMEntities, use_parent=True) - pagination = PaginationPane + paginator = PaginationPane() @property def is_displayed(self): @@ -1452,8 +1452,6 @@ def step(self, *args, **kwargs): self.view.sidebar.vmstemplates.tree.click_path('All VMs & Templates') def resetter(self, *args, **kwargs): - if self.view.pagination.is_displayed: - self.view.pagination.set_items_per_page(1000) self.view.reset_page() diff --git a/cfme/tests/cloud_infra_common/test_retirement.py b/cfme/tests/cloud_infra_common/test_retirement.py index e6fa20ccd6..050e5aa189 100644 --- a/cfme/tests/cloud_infra_common/test_retirement.py +++ b/cfme/tests/cloud_infra_common/test_retirement.py @@ -1,3 +1,5 @@ +import math +import re from collections import namedtuple from datetime import date from datetime import datetime @@ -10,6 +12,7 @@ from cfme.cloud.provider.ec2 import EC2Provider from cfme.infrastructure.provider import InfraProvider from cfme.markers.env_markers.provider import providers +from cfme.services.requests import RequestsView from cfme.utils.appliance.implementations.ui import navigate_to from cfme.utils.appliance.implementations.ui import navigator from cfme.utils.generators import random_vm_name @@ -18,6 +21,7 @@ from cfme.utils.timeutil import parsetime from cfme.utils.wait import wait_for + pytestmark = [ pytest.mark.usefixtures('setup_provider'), pytest.mark.tier(1), @@ -37,6 +41,32 @@ RetirementWarning('30_day_warning', '30 Days before retirement')] +def msg_date_range(expected_dates): + """Given the expected_dates dictionary, return a string of the form 'T_1|T_2|...|T_N', where + T_1 through T_N are datetime strings formatted with '%m/%d/%y %H:%M UTC', one for each unique + time between the start and end dates. + """ + dates = [] + num_min = int(math.ceil((expected_dates['end'] - expected_dates['start']).seconds / 60.0)) + for i in range(num_min): + next_date = expected_dates['start'] + timedelta(minutes=i) + next_date = next_date.strftime('%m/%d/%y %H:%M UTC') + dates.append(next_date) + return "|".join(dates) + + +def create_vms(template_name, provider, num_vms=1, **deploy_args): + collection = provider.appliance.provider_based_collection(provider) + vms = [] + for num in range(num_vms): + vm = collection.instantiate(random_vm_name('retire'), + provider, + template_name=template_name) + vm.create_on_provider(find_in_cfme=True, allow_skip="default", timeout=1200, **deploy_args) + vms.append(vm) + return vms + + @pytest.fixture(scope="function") def retire_vm(small_template, provider): """Fixture for creating a generic vm/instance @@ -45,32 +75,38 @@ def retire_vm(small_template, provider): small_template: small template fixture, template on provider provider: provider crud object from fixture """ - collection = provider.appliance.provider_based_collection(provider) - vm = collection.instantiate(random_vm_name('retire'), - provider, - template_name=small_template.name) - vm.create_on_provider(find_in_cfme=True, allow_skip="default", timeout=1200) + vm = create_vms(small_template.name, provider)[0] yield vm vm.cleanup_on_provider() @pytest.fixture(scope="function") -def retire_ec2_s3_vm(provider): +def retire_vm_pair(small_template, provider): + """Fixture for creating a pair of generic vms/instances + + Args: + small_template: small template fixture, template on provider + provider: provider crud object from fixture + """ + vms = create_vms(small_template.name, provider, 2) + yield vms + for num in range(2): + vms[num].cleanup_on_provider() + + +@pytest.fixture(scope="function") +def retire_ec2_s3_vm(s3_template, provider): """Fixture for creating an S3 backed paravirtual instance, template is a public ec2 AMI Args: provider: provider crud object from fixture """ - collection = provider.appliance.provider_based_collection(provider) - vm = collection.instantiate(random_vm_name('retire'), - provider, - template_name='amzn-ami-pv-2015.03.rc-1.x86_64-s3') - vm.create_on_provider(find_in_cfme=True, allow_skip="default", timeout=1200) + vm = create_vms(s3_template.name, provider, instance_type='m1.small')[0] yield vm vm.cleanup_on_provider() -def verify_retirement_state(retire_vm): +def verify_retirement_state(retire_vm, *args): """Verify the vm/instance is in the 'retired' state in the UI and assert its power state Args: @@ -84,10 +120,9 @@ def verify_retirement_state(retire_vm): fail_func=view.toolbar.reload.click, message=f"Wait for VM '{retire_vm.name}' to enter retired state" ) - - retirement_states = ['retired'] view = retire_vm.load_details() - assert view.entities.summary('Power Management').get_text_of('Power State') in retirement_states + power_states = list(args) if args else ['retired'] + assert view.entities.summary('Power Management').get_text_of('Power State') in power_states def verify_retirement_date(retire_vm, expected_date='Never'): @@ -105,7 +140,7 @@ def verify_retirement_date(retire_vm, expected_date='Never'): else: convert_func = parsetime.from_american_date_only expected_date.update({'retire': convert_func(retire_vm.retirement_date)}) - logger.info('Asserting retire date "%s" is between "%s" and "%s"', # noqa + logger.info(f'Asserting retirement date "%s" is between "%s" and "%s"', # noqa expected_date['retire'], expected_date['start'], expected_date['end']) @@ -122,10 +157,10 @@ def generate_retirement_date(delta=None): """Generate a retirement date that can be used by the VM.retire() method, adding delta Args: - delta: a :py:class: `int` that will be added to today's date + delta: a :py:class: `int` that specifies the number of days to be added to today's date Returns: a :py:class: `datetime.date` object including delta as an offset from today """ - gen_date = date.today() + gen_date = datetime.now().replace(second=0) if delta: gen_date += timedelta(days=delta) return gen_date @@ -156,12 +191,51 @@ def test_retirement_now(retire_vm): # retirement. Too finicky to get it down to minute precision, nor is it really needed here. retire_times = dict() retire_times['start'] = generate_retirement_date_now() + timedelta(minutes=-5) + retire_vm.retire() + + # Verify flash message + view = retire_vm.create_view(RequestsView) + assert view.is_displayed + view.flash.assert_success_message( + "Retirement initiated for 1 VM and Instance from the CFME Database") + verify_retirement_state(retire_vm) retire_times['end'] = generate_retirement_date_now() + timedelta(minutes=5) verify_retirement_date(retire_vm, expected_date=retire_times) +@pytest.mark.rhv1 +def test_retirement_now_multiple(retire_vm_pair, provider): + """Tests on-demand retirement of two instances/vms from All VMs page + + Polarion: + assignee: tpapaioa + casecomponent: Provisioning + initialEstimate: 1/6h + """ + retire_times = {} + retire_times['start'] = generate_retirement_date_now() + timedelta(minutes=-5) + + # Retire from All VMs/Instances page + collection = retire_vm_pair[0].parent + collection.retire(retire_vm_pair) + + # Verify flash message + view = collection.create_view(RequestsView) + assert view.is_displayed + view.flash.assert_success_message( + "Retirement initiated for 2 VMs and Instances from the CFME Database") + + verify_retirement_state(retire_vm_pair[0]) + verify_retirement_state(retire_vm_pair[1]) + + retire_times['end'] = generate_retirement_date_now() + timedelta(minutes=5) + + verify_retirement_date(retire_vm_pair[0], expected_date=retire_times) + verify_retirement_date(retire_vm_pair[1], expected_date=retire_times) + + @pytest.mark.provider(gen_func=providers, filters=[ProviderFilter(classes=[EC2Provider], required_flags=['provision', 'retire'])]) @@ -178,27 +252,27 @@ def test_retirement_now_ec2_instance_backed(retire_ec2_s3_vm, tagged, appliance) """ # Tag the VM with lifecycle for full retirement based on parameter if tagged: - retire_ec2_s3_vm.add_tag(appliance.collections.categories.instantiate( - display_name='Fully retire VM and remove from Provider').collections.tags.instantiate( - display_name='LifeCycle')) - expected_power_state = ['terminated'] + category = appliance.collections.categories.instantiate(display_name='LifeCycle') + tag = category.collections.tags.instantiate( + display_name='Fully retire VM and remove from Provider') + retire_ec2_s3_vm.add_tag(tag) + power_states = ['archived'] else: - # no tagging - expected_power_state = ['on'] + power_states = ['retired'] - # For 5.7+ capture two times to assert the retire time is within a window. + # Capture two times to assert the retire time is within a window. # Too finicky to get it down to minute precision, nor is it really needed here retire_times = dict() retire_times['start'] = generate_retirement_date_now() + timedelta(minutes=-5) retire_ec2_s3_vm.retire() - view_cls = navigator.get_class(retire_ec2_s3_vm, 'Details').VIEW - reload = retire_ec2_s3_vm.appliance.browser.create_view(view_cls).toolbar.reload - assert wait_for(lambda: retire_ec2_s3_vm.is_retired, - delay=5, num_sec=10 * 60, fail_func=reload.click, - message="Wait for VM '{}' to enter retired state" - .format(retire_ec2_s3_vm.name)) - view = retire_ec2_s3_vm.load_details() - assert view.entities.power_management.get_text_of('Power State') in expected_power_state + + # Verify flash message + view = retire_ec2_s3_vm.create_view(RequestsView) + assert view.is_displayed + view.flash.assert_success_message( + "Retirement initiated for 1 VM and Instance from the CFME Database") + + verify_retirement_state(retire_ec2_s3_vm, *power_states) retire_times['end'] = generate_retirement_date_now() + timedelta(minutes=5) verify_retirement_date(retire_ec2_s3_vm, expected_date=retire_times) @@ -208,20 +282,52 @@ def test_retirement_now_ec2_instance_backed(retire_ec2_s3_vm, tagged, appliance) def test_set_retirement_date(retire_vm, warn): """Tests setting retirement date and verifies configured date is reflected in UI - Note we cannot control the retirement time, just day, so we cannot wait for the VM to retire - Polarion: assignee: tpapaioa casecomponent: Provisioning initialEstimate: 1/6h """ - # TODO retirement supports datetime (no tz) in gaprindashvili/59z, update accordingly - num_days = 2 + num_days = 60 retire_date = generate_retirement_date(delta=num_days) - retire_vm.set_retirement_date(retire_date, warn=warn.string) + retire_vm.set_retirement_date(when=retire_date, warn=warn.string) + + # Verify flash message + view = retire_vm.create_view(retire_vm.DETAILS_VIEW_CLASS, wait='5s') + assert view.is_displayed + msg_date = retire_date.strftime('%m/%d/%y %H:%M UTC') + view.flash.assert_success_message(f"Retirement date set to {msg_date}") + verify_retirement_date(retire_vm, expected_date=retire_date) +@pytest.mark.rhv3 +@pytest.mark.parametrize('warn', warnings, ids=[warning.id for warning in warnings]) +def test_set_retirement_date_multiple(retire_vm_pair, provider, warn): + """Tests setting retirement date of multiple VMs, verifies configured date is reflected in + individual VM Details pages. + + Polarion: + assignee: tpapaioa + casecomponent: Provisioning + initialEstimate: 1/6h + """ + num_days = 60 + retire_date = generate_retirement_date(delta=num_days) + + # Set retirement date from All VMs/Instances page + collection = retire_vm_pair[0].parent + collection.set_retirement_date(retire_vm_pair, when=retire_date, warn=warn.string) + + # Verify flash message + view = collection.create_view(navigator.get_class(collection, 'All').VIEW, wait='5s') + assert view.is_displayed + msg_date = retire_date.strftime('%m/%d/%y %H:%M UTC') + view.flash.assert_success_message(f"Retirement dates set to {msg_date}") + + verify_retirement_date(retire_vm_pair[0], expected_date=retire_date) + verify_retirement_date(retire_vm_pair[1], expected_date=retire_date) + + @pytest.mark.tier(2) @pytest.mark.parametrize('warn', warnings, ids=[warning.id for warning in warnings]) def test_set_retirement_offset(retire_vm, warn): @@ -234,21 +340,61 @@ def test_set_retirement_offset(retire_vm, warn): casecomponent: Provisioning initialEstimate: 1/15h """ - num_hours = 3 - num_days = 1 - num_weeks = 2 - num_months = 0 # leave at zero for now, TODO implement months->weeks calc for expected_dates - retire_offset = {'months': num_months, 'weeks': num_weeks, 'days': num_days, 'hours': num_hours} + retire_offset = {'months': 0, 'weeks': 2, 'days': 1, 'hours': 3} timedelta_offset = retire_offset.copy() - timedelta_offset.pop('months') # for timedelta use - # pad pre-retire timestamp by 30s - expected_dates = {'start': datetime.utcnow() + timedelta(seconds=-30, **timedelta_offset)} + timedelta_offset.pop('months') # months not supported in timedelta + + # pad pre-retire timestamp by 60s + expected_dates = {'start': datetime.utcnow() + timedelta(seconds=-60, **timedelta_offset)} + retire_vm.set_retirement_date(offset=retire_offset, warn=warn.string) - # pad post-retire timestamp by 30s - expected_dates['end'] = datetime.utcnow() + timedelta(seconds=30, **timedelta_offset) - verify_retirement_date(retire_vm, - expected_date=expected_dates) + # pad post-retire timestamp by 60s + expected_dates['end'] = datetime.utcnow() + timedelta(seconds=60, **timedelta_offset) + + # Verify flash message + view = retire_vm.create_view(retire_vm.DETAILS_VIEW_CLASS, wait='5s') + assert view.is_displayed + msg_dates = msg_date_range(expected_dates) + flash_regex = re.compile(f"^Retirement date set to ({msg_dates})$") + view.flash.assert_success_message(flash_regex) + + verify_retirement_date(retire_vm, expected_date=expected_dates) + + +@pytest.mark.rhv3 +@pytest.mark.parametrize('warn', warnings, ids=[warning.id for warning in warnings]) +def test_set_retirement_offset_multiple(retire_vm_pair, provider, warn): + """Tests setting retirement date of multiple VMs by offset. + Verifies configured date is reflected in individual VM Details pages. + + Polarion: + assignee: tpapaioa + casecomponent: Provisioning + initialEstimate: 1/6h + """ + retire_offset = {'months': 0, 'weeks': 2, 'days': 1, 'hours': 3} + timedelta_offset = retire_offset.copy() + timedelta_offset.pop('months') # months not supported in timedelta + + # pad pre-retire timestamp by 60s + expected_dates = {'start': datetime.utcnow() + timedelta(seconds=-60, **timedelta_offset)} + + collection = retire_vm_pair[0].parent + collection.set_retirement_date(retire_vm_pair, offset=retire_offset, warn=warn.string) + + # pad post-retire timestamp by 60s + expected_dates['end'] = datetime.utcnow() + timedelta(seconds=60, **timedelta_offset) + + # Verify flash message + view = collection.create_view(navigator.get_class(collection, 'All').VIEW, wait='5s') + assert view.is_displayed + msg_dates = msg_date_range(expected_dates) + flash_regex = re.compile(f"^Retirement dates set to ({msg_dates})$") + view.flash.assert_success_message(flash_regex) + + verify_retirement_date(retire_vm_pair[0], expected_date=expected_dates) + verify_retirement_date(retire_vm_pair[1], expected_date=expected_dates) @pytest.mark.rhv3 @@ -262,10 +408,23 @@ def test_unset_retirement_date(retire_vm): """ num_days = 3 retire_date = generate_retirement_date(delta=num_days) - retire_vm.set_retirement_date(retire_date) + retire_vm.set_retirement_date(when=retire_date) + + # Verify flash message + view = retire_vm.create_view(retire_vm.DETAILS_VIEW_CLASS, wait='5s') + assert view.is_displayed + msg_date = retire_date.strftime('%m/%d/%y %H:%M UTC') + view.flash.assert_success_message(f"Retirement date set to {msg_date}") + verify_retirement_date(retire_vm, expected_date=retire_date) - retire_vm.set_retirement_date(None) + retire_vm.set_retirement_date(when=None) + + # Verify flash message + view = retire_vm.create_view(retire_vm.DETAILS_VIEW_CLASS, wait='5s') + assert view.is_displayed + view.flash.assert_success_message("Retirement date removed") + verify_retirement_date(retire_vm, expected_date='Never') @@ -288,13 +447,29 @@ def test_resume_retired_instance(retire_vm, provider, remove_date): num_days = 5 retire_vm.retire() + + # Verify flash message + view = retire_vm.create_view(RequestsView) + assert view.is_displayed + view.flash.assert_success_message( + "Retirement initiated for 1 VM and Instance from the CFME Database") + verify_retirement_state(retire_vm) retire_date = None if remove_date else generate_retirement_date(delta=num_days) - retire_vm.set_retirement_date(retire_date) + retire_vm.set_retirement_date(when=retire_date) + + # Verify flash message + view = retire_vm.create_view(retire_vm.DETAILS_VIEW_CLASS, wait='5s') + assert view.is_displayed + if retire_date: + msg_date = retire_date.strftime('%m/%d/%y %H:%M UTC') + view.flash.assert_success_message(f"Retirement date set to {msg_date}") + else: + view.flash.assert_success_message("Retirement date removed") verify_retirement_date(retire_vm, expected_date=retire_date if retire_date else 'Never') - assert retire_vm.is_retired is False + assert not retire_vm.is_retired @pytest.mark.tier(2) diff --git a/cfme/tests/cloud_infra_common/test_retirement_manual.py b/cfme/tests/cloud_infra_common/test_retirement_manual.py deleted file mode 100644 index 616618adc7..0000000000 --- a/cfme/tests/cloud_infra_common/test_retirement_manual.py +++ /dev/null @@ -1,103 +0,0 @@ -# pylint: skip-file -"""Manual tests""" -import pytest - -from cfme import test_requirements - -pytestmark = [ - pytest.mark.ignore_stream('upstream'), - pytest.mark.manual, - test_requirements.retirement -] - - -@pytest.mark.tier(2) -def test_retire_infra_vms_folder(): - """ - test the retire funtion of vm on infra providers, at least two vm, - retire now button vms page - - Polarion: - assignee: tpapaioa - casecomponent: Provisioning - caseimportance: medium - initialEstimate: 1/2h - """ - pass - - -@pytest.mark.tier(2) -def test_retire_cloud_vms_date_folder(): - """ - test the retire funtion of vm on cloud providers, at leat two vm, set - retirement date button from vms page(without notification) - - Polarion: - assignee: tpapaioa - casecomponent: Provisioning - caseimportance: medium - initialEstimate: 1/2h - """ - pass - - -@pytest.mark.tier(2) -def test_retire_infra_vms_notification_folder(): - """ - test the retire funtion of vm on infra providers, select at least two - vms and press retirement date button from vms main page and specify - retirement warning period (1week, 2weeks, 1 months). - - Polarion: - assignee: tpapaioa - casecomponent: Provisioning - caseimportance: medium - initialEstimate: 1/2h - """ - pass - - -@pytest.mark.tier(2) -def test_retire_infra_vms_date_folder(): - """ - test the retire funtion of vm on infra providers, at least two vm, set - retirement date button from vms page(without notification) - - Polarion: - assignee: tpapaioa - casecomponent: Provisioning - caseimportance: medium - initialEstimate: 1/2h - """ - pass - - -@pytest.mark.tier(2) -def test_retire_cloud_vms_folder(): - """ - test the retire funtion of vm on cloud providers, at leat two vm, - retire now button vms page - - Polarion: - assignee: tpapaioa - casecomponent: Provisioning - caseimportance: medium - initialEstimate: 1/2h - """ - pass - - -@pytest.mark.tier(2) -def test_retire_cloud_vms_notification_folder(): - """ - test the retire funtion of vm on cloud providers, one vm, set - retirement date button from vm summary page with notification for two - vms for one of the period (1week, 2weeks, 1 months) - - Polarion: - assignee: tpapaioa - casecomponent: Provisioning - caseimportance: medium - initialEstimate: 1/2h - """ - pass diff --git a/requirements/frozen.txt b/requirements/frozen.txt index 94258016b2..a56a005605 100644 --- a/requirements/frozen.txt +++ b/requirements/frozen.txt @@ -221,7 +221,7 @@ paramiko==2.6.0 paramiko-expect==0.2.8 parsedatetime==2.4 parso==0.4.0 -pbr==5.4.1 +pbr==5.4.4 pdfminer.six==20181108 pexpect==4.7.0 pickleshare==0.7.5