def load_provisioning(self): parsed_ips = [] for address in self.parameters["IPs"].split(','): address = unicode(address) if "/" in address: try: parsed_ips.append( ipaddress.ip_network(address.strip(), strict=True)) # type: ignore except (ipaddress.AddressValueError, ipaddress.NetmaskValueError, ValueError) as parsing_error: LOGGER.warning("Error parsing IP range: %s", parsing_error) else: try: parsed_ips.append(ipaddress.ip_address( address.strip())) # type: ignore except (ipaddress.AddressValueError, ipaddress.NetmaskValueError, ValueError) as parsing_error: LOGGER.warning("Error parsing IP : %s", parsing_error) return parsed_ips
def is_enabled(self, context: dict = None, default_value: bool = False) -> bool: # pylint: disable=unused-argument """ Checks if feature is enabled. :param context: Context information :param default_value: Deprecated! Users should use the fallback_function on the main is_enabled() method. :return: """ flag_value = False if self.enabled: try: if self.strategies: strategy_result = any(x.execute(context) for x in self.strategies) else: # If no strategies are present, should default to true. This isn't possible via UI. strategy_result = True flag_value = strategy_result except Exception as strategy_except: LOGGER.warning("Error checking feature flag: %s", strategy_except) self.increment_stats(flag_value) LOGGER.info("Feature toggle status for feature %s: %s", self.name, flag_value) return flag_value
def _create_strategies(provisioning: dict, strategy_mapping: dict, cache: FileCache) -> list: feature_strategies = [] for strategy in provisioning["strategies"]: try: if "parameters" in strategy.keys(): strategy_provisioning = strategy['parameters'] else: strategy_provisioning = {} if "constraints" in strategy.keys(): constraint_provisioning = strategy['constraints'] else: constraint_provisioning = {} feature_strategies.append(strategy_mapping[strategy['name']]( constraints=constraint_provisioning, parameters=strategy_provisioning )) except Exception as excep: if FAILED_STRATEGIES not in cache.keys(): cache[FAILED_STRATEGIES] = [] # Initialize cache key only if failures exist. if strategy['name'] not in cache[FAILED_STRATEGIES]: LOGGER.warning("Failed to load strategy. This may be a problem with a custom strategy. Exception: %s", excep) cache[FAILED_STRATEGIES].append(strategy['name']) return feature_strategies
def is_enabled(self, context: dict = None, default_value: bool = False) -> bool: """ Checks if feature is enabled. :param context: Context information :param default_value: Optional, but allows for override. :return: """ flag_value = default_value if self.enabled: try: if self.strategies: strategy_result = any( [x.execute(context) for x in self.strategies]) else: # If no strategies are present, should default to true. This isn't possible via UI. strategy_result = True flag_value = flag_value or strategy_result except Exception as strategy_except: LOGGER.warning("Error checking feature flag: %s", strategy_except) self.increment_stats(flag_value) LOGGER.info( f"Feature toggle status for feature {self.name} with context {context} : {flag_value}" ) return flag_value
def is_enabled(self, context: dict = None, default_value: bool = False) -> bool: """ Checks if feature is enabled. :param context: Context information :param default_value: Optional, but allows for override. :return: """ flag_value = default_value if self.enabled: try: for strategy in self.strategies: flag_value = flag_value or strategy(context) except Exception as strategy_except: LOGGER.warning("Error checking feature flag: %s", strategy_except) self.increment_stats(flag_value) LOGGER.info("Feature toggle status for feature %s: %s", self.name, flag_value) return flag_value
def apply(self, context: dict = None) -> bool: """ Returns true if IP is in list of IPs :return: """ return_value = False try: context_ip = ipaddress.ip_address(context["remoteAddress"]) except (ipaddress.AddressValueError, ipaddress.NetmaskValueError, ValueError) as parsing_error: LOGGER.warning("Error parsing IP : %s", parsing_error) context_ip = None if context_ip: for addr_or_range in [value for value in self.parsed_provisioning if value.version == context_ip.version]: if isinstance(addr_or_range, (ipaddress.IPv4Address, ipaddress.IPv6Address)): if context_ip == addr_or_range: return_value = True break else: if context_ip in addr_or_range: return_value = True break return return_value
def register_client(url: str, app_name: str, instance_id: str, metrics_interval: int, custom_headers: dict, custom_options: dict, supported_strategies: dict) -> bool: """ Attempts to register client with unleash server. Notes: * If unsuccessful (i.e. not HTTP status code 202), exception will be caught and logged. This is to allow "safe" error handling if unleash server goes down. :param url: :param app_name: :param instance_id: :param metrics_interval: :param custom_headers: :param custom_options: :param supported_strategies: :return: true if registration successful, false if registration unsuccessful or exception. """ registation_request = { "appName": app_name, "instanceId": instance_id, "sdkVersion": "{}:{}".format(SDK_NAME, SDK_VERSION), "strategies": [*supported_strategies], "started": datetime.now(timezone.utc).isoformat(), "interval": metrics_interval } try: LOGGER.info("Registering unleash client with unleash @ %s", url) LOGGER.info("Registration request information: %s", registation_request) resp = requests.post(url + REGISTER_URL, data=json.dumps(registation_request), headers={ **custom_headers, **APPLICATION_HEADERS }, timeout=REQUEST_TIMEOUT, **custom_options) if resp.status_code != 202: log_resp_info(resp) LOGGER.warning( "Unleash Client registration failed due to unexpected HTTP status code." ) return False LOGGER.info("Unleash Client successfully registered!") return True except Exception: LOGGER.exception( "Unleash Client registration failed due to exception: %s", Exception) return False
def get_feature_toggles(url: str, app_name: str, instance_id: str, custom_headers: dict, custom_options: dict, project: str = None) -> dict: """ Retrieves feature flags from unleash central server. Notes: * If unsuccessful (i.e. not HTTP status code 200), exception will be caught and logged. This is to allow "safe" error handling if unleash server goes down. :param url: :param app_name: :param instance_id: :param custom_headers: :param custom_options: :param project: :return: Feature flags if successful, empty dict if not. """ try: LOGGER.info("Getting feature flag.") headers = { "UNLEASH-APPNAME": app_name, "UNLEASH-INSTANCEID": instance_id } base_url = f"{url}{FEATURES_URL}" base_params = {} if project: base_params = {'project': project} resp = requests.get(base_url, headers={ **custom_headers, **headers }, params=base_params, timeout=REQUEST_TIMEOUT, **custom_options) if resp.status_code != 200: log_resp_info(resp) LOGGER.warning( "Unleash Client feature fetch failed due to unexpected HTTP status code." ) raise Exception("Unleash Client feature fetch failed!") return resp.json() except Exception as exc: LOGGER.exception( "Unleash Client feature fetch failed due to exception: %s", exc) return {}
def load_features(cache: FileCache, feature_toggles: dict, strategy_mapping: dict) -> None: """ Caching :param cache: Should be the cache class variable from UnleashClient :param feature_toggles: Should be the features class variable from UnleashClient :param strategy_mapping: :return: """ # Pull raw provisioning from cache. try: feature_provisioning = cache[FEATURES_URL] # Parse provisioning parsed_features = {} feature_names = [d["name"] for d in feature_provisioning["features"]] for provisioning in feature_provisioning["features"]: parsed_features[provisioning["name"]] = provisioning # Delete old features/cache for feature in list(feature_toggles.keys()): if feature not in feature_names: del feature_toggles[feature] # Update existing objects for feature in feature_toggles.keys(): feature_for_update = feature_toggles[feature] strategies = parsed_features[feature]["strategies"] feature_for_update.enabled = parsed_features[feature]["enabled"] if strategies: parsed_strategies = _create_strategies(parsed_features[feature], strategy_mapping, cache) feature_for_update.strategies = parsed_strategies if 'variants' in parsed_features[feature]: feature_for_update.variants = Variants( parsed_features[feature]['variants'], parsed_features[feature]['name'] ) # Handle creation or deletions new_features = list(set(feature_names) - set(feature_toggles.keys())) for feature in new_features: feature_toggles[feature] = _create_feature(parsed_features[feature], strategy_mapping, cache) except KeyError as cache_exception: LOGGER.warning("Cache Exception: %s", cache_exception) LOGGER.warning("Unleash client does not have cached features. " "Please make sure client can communicate with Unleash server!")
def fetch_and_load_features(url, app_name, instance_id, custom_headers, custom_options, cache, features, strategy_mapping): feature_provisioning = get_feature_toggles(url, app_name, instance_id, custom_headers, custom_options) if feature_provisioning: cache[FEATURES_URL] = feature_provisioning cache.sync() else: LOGGER.warning( "Unable to get feature flag toggles, using cached provisioning.") load_features(cache, features, strategy_mapping)
def get_variant(self, feature_name: str, context: dict = {}) -> dict: """ Checks if a feature toggle is enabled. If so, return variant. Notes: * If client hasn't been initialized yet or an error occurs, flat will default to false. :param feature_name: Name of the feature :param context: Dictionary with context (e.g. IPs, email) for feature toggle. :return: Dict with variant and feature flag status. """ context.update(self.unleash_static_context) if self.is_initialized: try: return self.features[feature_name].get_variant(context) except Exception as excep: LOGGER.warning( "Returning default flag/variation for feature: %s", feature_name) LOGGER.warning("Error checking feature flag variant: %s", excep) return consts.DISABLED_VARIATION else: LOGGER.warning("Returning default flag/variation for feature: %s", feature_name) LOGGER.warning( "Attempted to get feature flag/variation %s, but client wasn't initialized!", feature_name) return consts.DISABLED_VARIATION
def fetch_and_load_features(url: str, app_name: str, instance_id: str, custom_headers: dict, cache: FileCache, features: dict, strategy_mapping: dict) -> None: feature_provisioning = get_feature_toggles(url, app_name, instance_id, custom_headers) if feature_provisioning: cache[FEATURES_URL] = feature_provisioning cache.sync() else: LOGGER.warning( "Unable to get feature flag toggles, using cached provisioning.") load_features(cache, features, strategy_mapping)
def _create_strategies(provisioning, strategy_mapping): feature_strategies = [] for strategy in provisioning["strategies"]: try: if "parameters" in strategy.keys(): feature_strategies.append(strategy_mapping[strategy["name"]](strategy["parameters"])) else: feature_strategies.append(strategy_mapping[strategy["name"]]()) # type: ignore except Exception as excep: LOGGER.warning("Failed to load strategy. This may be a problem with a custom strategy. Exception: %s", excep) return feature_strategies
def send_metrics(url: str, request_body: dict, custom_headers: dict, custom_options: dict) -> bool: """ Attempts to send metrics to Unleash server Notes: * If unsuccessful (i.e. not HTTP status code 200), message will be logged :param url: :param app_name: :param instance_id: :param metrics_interval: :param custom_headers: :param custom_options: :return: true if registration successful, false if registration unsuccessful or exception. """ try: LOGGER.info("Sending messages to with unleash @ %s", url) LOGGER.info("unleash metrics information: %s", request_body) resp = requests.post(url + METRICS_URL, data=json.dumps(request_body), headers={ **custom_headers, **APPLICATION_HEADERS }, timeout=REQUEST_TIMEOUT, **custom_options) if resp.status_code != 202: log_resp_info(resp) LOGGER.warning("Unleash CLient metrics submission failed.") return False LOGGER.info("Unleash Client metrics successfully sent!") return True except Exception: LOGGER.exception( "Unleash Client metrics submission failed dye to exception: %s", Exception) return False
def get_variant(self, context: dict = None) -> dict: """ Checks if feature is enabled and, if so, get the variant. :param context: Context information :return: """ variant = DISABLED_VARIATION is_feature_enabled = self.is_enabled(context) if is_feature_enabled and self.variants is not None: try: variant = self.variants.get_variant(context) variant['enabled'] = is_feature_enabled except Exception as variant_exception: LOGGER.warning("Error selecting variant: %s", variant_exception) return variant
def _create_strategies(provisioning: dict, strategy_mapping: dict, cache: BaseCache, global_segments: Optional[dict]) -> list: feature_strategies = [] for strategy in provisioning["strategies"]: try: if "parameters" in strategy.keys(): strategy_provisioning = strategy['parameters'] else: strategy_provisioning = {} if "constraints" in strategy.keys(): constraint_provisioning = strategy['constraints'] else: constraint_provisioning = {} if "segments" in strategy.keys(): segment_provisioning = strategy['segments'] else: segment_provisioning = [] feature_strategies.append(strategy_mapping[strategy['name']]( constraints=constraint_provisioning, parameters=strategy_provisioning, global_segments=global_segments, segment_ids=segment_provisioning)) except Exception as excep: strategies = cache.get(FAILED_STRATEGIES, []) if strategy['name'] not in strategies: LOGGER.warning( "Failed to load strategy. This may be a problem with a custom strategy. Exception: %s", excep) strategies.append(strategy['name']) cache.set(FAILED_STRATEGIES, strategies) return feature_strategies
def get_feature_toggles(url: str, app_name: str, instance_id: str, custom_headers: dict) -> dict: """ Retrieves feature flags from unleash central server. Notes: * If unsuccessful (i.e. not HTTP status code 200), exception will be caught and logged. This is to allow "safe" error handling if unleash server goes down. :param url: :param app_name: :param instance_id: :param custom_headers: :return: Feature flags if successful, empty dict if not. """ try: LOGGER.info("Getting feature flag.") headers = { "UNLEASH-APPNAME": app_name, "UNLEASH-INSTANCEID": instance_id } resp = requests.get(url + FEATURES_URL, headers={ **custom_headers, **headers }, timeout=REQUEST_TIMEOUT) if resp.status_code != 200: LOGGER.warning("unleash feature fetch failed!") raise Exception("unleash feature fetch failed!") return json.loads(resp.content) except Exception: LOGGER.exception("Unleash feature fetch failed!") return {}
def _create_strategies(provisioning: dict, strategy_mapping: dict) -> list: feature_strategies = [] for strategy in provisioning["strategies"]: try: if "parameters" in strategy.keys(): strategy_provisioning = strategy['parameters'] else: strategy_provisioning = {} if "constraints" in strategy.keys(): constraint_provisioning = strategy['constraints'] else: constraint_provisioning = {} feature_strategies.append(strategy_mapping[strategy['name']](constraints=constraint_provisioning, parameters=strategy_provisioning)) except Exception as excep: LOGGER.warning("Failed to load strategy. This may be a problem with a custom strategy. Exception: %s", excep) return feature_strategies
def fetch_and_load_features(url: str, app_name: str, instance_id: str, custom_headers: dict, custom_options: dict, cache: redis.Redis, features: dict, strategy_mapping: dict) -> None: feature_provisioning = get_feature_toggles(url, app_name, instance_id, custom_headers, custom_options) if feature_provisioning: # Sample data we're writing into cache # { # "features": [{ # "name": "haptik.development.enable_smart_skills", # "description": "Feature to enable smart skills on dev servers", # "type": "release", # "project": "default", # "enabled": true, # "stale": false, # "strategies": [ # { # "name": "EnableForPartners", # "parameters": { # "partner_names": "Platform Demo,haptik,demo,aksc" # } # } # ], # "variants": [], # "createdAt": "2021-03-08T09:14:41.828Z" # }] # } features = feature_provisioning.get('features', []) if not features: LOGGER.warning("Features are empty") cache.set(FEATURES_URL, pickle.dumps(features)) else: LOGGER.warning( "Unable to get feature flag toggles, using cached provisioning.") load_features(cache, features, strategy_mapping)
def send_metrics(url, request_body, custom_headers): """ Attempts to send metrics to Unleash server Notes: * If unsuccessful (i.e. not HTTP status code 200), message will be logged :param url: :param app_name: :param instance_id: :param metrics_interval: :param custom_headers: :return: true if registration successful, false if registration unsuccessful or exception. """ try: LOGGER.info("Sending messages to with unleash @ %s", url) LOGGER.info("unleash metrics information: %s", request_body) headers = APPLICATION_HEADERS.copy() headers.update(custom_headers) resp = requests.post(url + METRICS_URL, data=json.dumps(request_body), headers=headers, timeout=REQUEST_TIMEOUT) if resp.status_code != 202: LOGGER.warning("unleash metrics submission failed.") return False LOGGER.info("unleash metrics successfully sent!") return True except Exception: LOGGER.exception("unleash metrics failed to send.") return False
def is_enabled(self, feature_name: str, context: dict = {}, default_value: bool = False, fallback_function: Callable = None) -> bool: """ Checks if a feature toggle is enabled. Notes: * If client hasn't been initialized yet or an error occurs, flat will default to false. :param feature_name: Name of the feature :param context: Dictionary with context (e.g. IPs, email) for feature toggle. :param default_value: Allows override of default value. (DEPRECIATED, used fallback_function instead!) :param fallback_function: Allows users to provide a custom function to set default value. :return: True/False """ context.update(self.unleash_static_context) if default_value: default_value_warning() if self.is_initialized: try: return self.features[feature_name].is_enabled( context, default_value) except Exception as excep: LOGGER.warning("Returning default value for feature: %s", feature_name) LOGGER.warning("Error checking feature flag: %s", excep) return self._get_fallback_value(fallback_function, feature_name, context) else: LOGGER.warning("Returning default value for feature: %s", feature_name) LOGGER.warning( "Attempted to get feature_flag %s, but client wasn't initialized!", feature_name) return self._get_fallback_value(fallback_function, feature_name, context)
def load_features(cache: BaseCache, feature_toggles: dict, strategy_mapping: dict, global_segments: Optional[dict] = None) -> None: """ Caching :param cache: Should be the cache class variable from UnleashClient :param feature_toggles: Should be the features class variable from UnleashClient :param strategy_mapping: :return: """ # Pull raw provisioning from cache. feature_provisioning = cache.get(FEATURES_URL) if not feature_provisioning: LOGGER.warning( "Unleash client does not have cached features. " "Please make sure client can communicate with Unleash server!") return # Parse provisioning parsed_features = {} feature_names = [d["name"] for d in feature_provisioning["features"]] if "segments" in feature_provisioning.keys(): segments = feature_provisioning["segments"] global_segments = {segment["id"]: segment for segment in segments} else: global_segments = {} for provisioning in feature_provisioning["features"]: parsed_features[provisioning["name"]] = provisioning # Delete old features/cache for feature in list(feature_toggles.keys()): if feature not in feature_names: del feature_toggles[feature] # Update existing objects for feature in feature_toggles.keys(): feature_for_update = feature_toggles[feature] strategies = parsed_features[feature]["strategies"] feature_for_update.enabled = parsed_features[feature]["enabled"] if strategies: parsed_strategies = _create_strategies(parsed_features[feature], strategy_mapping, cache, global_segments) feature_for_update.strategies = parsed_strategies if 'variants' in parsed_features[feature]: feature_for_update.variants = Variants( parsed_features[feature]['variants'], parsed_features[feature]['name']) # Handle creation or deletions new_features = list(set(feature_names) - set(feature_toggles.keys())) for feature in new_features: feature_toggles[feature] = _create_feature(parsed_features[feature], strategy_mapping, cache, global_segments)
def get_feature_toggles(url: str, app_name: str, instance_id: str, custom_headers: dict, custom_options: dict, project: str = None, cached_etag: str = '') -> Tuple[dict, str]: """ Retrieves feature flags from unleash central server. Notes: * If unsuccessful (i.e. not HTTP status code 200), exception will be caught and logged. This is to allow "safe" error handling if unleash server goes down. :param url: :param app_name: :param instance_id: :param custom_headers: :param custom_options: :param project: :param cached_etag: :return: (Feature flags, etag) if successful, ({},'') if not """ try: LOGGER.info("Getting feature flag.") headers = { "UNLEASH-APPNAME": app_name, "UNLEASH-INSTANCEID": instance_id } if cached_etag: headers['If-None-Match'] = cached_etag base_url = f"{url}{FEATURES_URL}" base_params = {} if project: base_params = {'project': project} resp = requests.get(base_url, headers={ **custom_headers, **headers }, params=base_params, timeout=REQUEST_TIMEOUT, **custom_options) if resp.status_code not in [200, 304]: log_resp_info(resp) LOGGER.warning( "Unleash Client feature fetch failed due to unexpected HTTP status code." ) raise Exception("Unleash Client feature fetch failed!") etag = '' if 'etag' in resp.headers.keys(): etag = resp.headers['etag'] if resp.status_code == 304: return None, etag return resp.json(), etag except Exception as exc: LOGGER.exception( "Unleash Client feature fetch failed due to exception: %s", exc) return {}, ''