def test_version_check_triggered(self): """Test that under the following circumstances, a cluster version & sfctl version compatibility check is triggered and verify that the last check time was left in a good state after the call: - The last check time (in state) doesn't exist yet - An error has occurred during function call - On connection to a new cluster even if time since last check is less than SF_CLI_VERSION_CHECK_INTERVAL - The last check time (in state) was greater than config.py's SF_CLI_VERSION_CHECK_INTERVAL NOTE: this is a unit test only, which relies on the custom_cluster.py - check_cluster_version function being called with the correct parameters, and being called at all. """ # Start session state with condition last check time does not exist: state_file_path = sfctl_state.get_state_path() # If anything other than one line with our state exists in the file # (2 lines total - one to specify the section) # then throw an error. This may happen if sfctl uses the state file for something else. # If the state file ends up being used for anything else # other than last checked API version time, then modify this test then to remove # only that one line. with open(state_file_path) as state_file: content = state_file.readlines() content_trimmed = [] for line in content: if line.strip(): content_trimmed.append(line) self.assertLess( len(content_trimmed), 3, 'sfctl state file should not have more than 2 lines. ' 'Content: ' + str(content_trimmed)) # empty the file open(state_file_path, 'w').close() # Create cluster version object. cluster_version = 'invalid_version' current_utc_time = datetime.utcnow() # Check cluster version. This should update the last updated time (in state) checks_passed_or_not_done = check_cluster_version( False, dummy_cluster_version=cluster_version) self.assertFalse( checks_passed_or_not_done, 'check_cluster_version should return False ' 'because checks were performed and the ' 'versions do not match') self.assertGreater( sfctl_state.get_cluster_version_check_time(), current_utc_time, 'check_cluster_version command should have modified the ' 'last checked time such that the time in state is greater than our ' 'time.') # Set the last checked time in state to something recent, and set calling on failure # to True sfctl_state.set_cluster_version_check_time(current_utc_time) checks_passed_or_not_done = check_cluster_version( on_failure_or_connection=True, dummy_cluster_version=cluster_version) self.assertFalse( checks_passed_or_not_done, 'check_cluster_version should return False ' 'because checks were performed and the ' 'versions do not match') self.assertGreater( sfctl_state.get_cluster_version_check_time(), current_utc_time, 'check_cluster_version command should have modified the ' 'last checked time such that the time in state is greater than our ' 'time.') # Last check time is in the past (well past SF_CLI_VERSION_CHECK_INTERVAL), # so should trigger an update and a check utc_time_past = datetime(year=current_utc_time.year - 1, month=12, day=20, hour=0, minute=0, second=0) sfctl_state.set_cluster_version_check_time(utc_time_past) checks_passed_or_not_done = check_cluster_version( on_failure_or_connection=False, dummy_cluster_version=cluster_version) self.assertFalse( checks_passed_or_not_done, 'check_cluster_version should return False ' 'because checks were performed and the ' 'versions do not match') self.assertGreater( sfctl_state.get_cluster_version_check_time(), utc_time_past, 'check_cluster_version command should have modified the ' 'last checked time such that the time in state is greater than our ' 'time.')
def test_version_check_not_triggered(self): # pylint: disable=invalid-name """Test that under the following circumstances, a cluster version & sfctl version compatibility check is NOT triggered and if the last check time was left in a good state after the call: - The last check time was less than config.py - SF_CLI_VERSION_CHECK_INTERVAL NOTE: this is a unit test only, which relies on the custom_cluster.py - check_cluster_version function being called with the correct parameters, and being called at all. This test assumes SF_CLI_VERSION_CHECK_INTERVAL = 24 hours """ current_utc_time = datetime.utcnow() adjusted_hour = current_utc_time.hour adjusted_minute = current_utc_time.minute adjusted_day = current_utc_time.day if adjusted_minute >= 5: adjusted_minute = adjusted_minute - 5 elif adjusted_hour >= 1: adjusted_hour = adjusted_hour - 1 else: adjusted_day = adjusted_day - 1 adjusted_hour = 23 utc_time_past = datetime(year=current_utc_time.year, month=current_utc_time.month, day=adjusted_day, hour=adjusted_hour, minute=adjusted_minute) cluster_version = 'invalid_version' # Configure last checked time to current time minus some amount of time less than 24 hours # Run check_cluster_version # Check that the values in the state file of the last checked time is correct # Test may fail if SF_CLI_VERSION_CHECK_INTERVAL value is too low. sfctl_state.set_cluster_version_check_time(utc_time_past) checks_passed_or_not_done = check_cluster_version( on_failure_or_connection=False, dummy_cluster_version=cluster_version) self.assertTrue( checks_passed_or_not_done, 'check_cluster_version should return True ' 'because no checks were performed') self.assertEqual( utc_time_past, sfctl_state.get_cluster_version_check_time(), 'check_cluster_version command should not have modified the ' 'last checked time values since it should have returned True, having ' 'done no work.') # Configure last checked time to current time # Run check_cluster_version # Check that the values in the state file of the last checked time is correct current_utc_time = datetime.utcnow() sfctl_state.set_cluster_version_check_time(current_utc_time) checks_passed_or_not_done = check_cluster_version( on_failure_or_connection=False, dummy_cluster_version=cluster_version) self.assertTrue( checks_passed_or_not_done, 'check_cluster_version should return True ' 'because no checks were performed') self.assertEqual( current_utc_time, sfctl_state.get_cluster_version_check_time(), 'check_cluster_version command should not have modified the ' 'last checked time values since it should have returned True, having ' 'done no work.')
def check_cluster_version(on_failure_or_connection, dummy_cluster_version=None): """ Check that the cluster version of sfctl is compatible with that of the cluster. Failures in making the API call (to check the cluster version) will be ignored and the time tracker will be reset to the current time. This is because we have no way of knowing if the API call failed because it doesn't exist on the cluster, or because of some other reason. We set the time tracker to the current time to avoid calling the API continuously for clusters without this API. Rather than each individual component deciding when to call this function, this should be called any time this might need to be triggered, and logic within this function will judge if a call to the cluster is required. :param on_failure_or_connection: True if this function is called due to an API call failure, or because it was called on connection to a new cluster endpoint. False otherwise. :type on_failure_or_connection: bool :param dummy_cluster_version: Used for testing purposes only. This is passed in to replace a call to the service fabric cluster to get the cluster version, in order to keep tests local. By default this value is None. If you would like to simulate the cluster call returning None, then enter 'NoResult' as a string :type dummy_cluster_version: str :returns: True if versions match, or if the check is not performed. False otherwise. """ from sfctl.state import get_cluster_version_check_time, set_cluster_version_check_time from warnings import warn # Before doing anything, see if a check needs to be triggered. # Always trigger version check if on failure or connection if not on_failure_or_connection: # Check if sufficient time has passed since last check last_check_time = get_cluster_version_check_time() if last_check_time is not None: # If we've already checked the cluster version before, see how long ago it has been time_since_last_check = datetime.utcnow() - last_check_time allowable_time = timedelta(hours=SF_CLI_VERSION_CHECK_INTERVAL) if allowable_time > time_since_last_check: # Don't perform any checks return True else: # If last_check_time is None, this means that we've not yet set a time, so it's never # been checked. Set the initial value. set_cluster_version_check_time() cluster_auth = get_cluster_auth() auth = _get_client_cert_auth(cluster_auth['pem'], cluster_auth['cert'], cluster_auth['key'], cluster_auth['ca'], cluster_auth['no_verify']) client = ServiceFabricClientAPIs(auth, base_url=client_endpoint()) sfctl_version = get_sfctl_version() # Update the timestamp of the last cluster version check set_cluster_version_check_time() if dummy_cluster_version is None: # This command may fail for various reasons. Most common reason as of writing this comment # is that the corresponding get_cluster_version API on the cluster doesn't exist. try: logger.info('Performing cluster version check') cluster_version = client.get_cluster_version().version except: # pylint: disable=bare-except ex = exc_info()[0] logger.info('Check cluster version failed due to error: %s', str(ex)) return True else: if dummy_cluster_version == 'NoResult': cluster_version = None else: cluster_version = dummy_cluster_version if cluster_version is None: # Do no checks if the get cluster version API fails, since most likely it failed # because the API doesn't exist. return True if not sfctl_cluster_version_matches(cluster_version, sfctl_version): warn( str.format( 'sfctl has version "{0}" which does not match the cluster version "{1}". ' 'See https://docs.microsoft.com/azure/service-fabric/service-fabric-cli#service-fabric-target-runtime ' # pylint: disable=line-too-long 'for version compatibility. Upgrade to a compatible version for the best experience.', sfctl_version, cluster_version)) return False return True