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
Exemple #6
0
    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
Exemple #7
0
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
Exemple #8
0
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
Exemple #12
0
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)
Exemple #13
0
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
Exemple #14
0
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
Exemple #16
0
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
Exemple #17
0
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
Exemple #19
0
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)
Exemple #22
0
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)
Exemple #23
0
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 {}, ''