def entitlements(self): """Return a dictionary of entitlements keyed by entitlement name. Return an empty dict if no entitlements are present. """ if self._entitlements: return self._entitlements machine_token = self.machine_token if not machine_token: return {} self._entitlements = {} contractInfo = machine_token['machineTokenInfo']['contractInfo'] ent_by_name = dict( (e['type'], e) for e in contractInfo['resourceEntitlements']) for entitlement_name, ent_value in ent_by_name.items(): entitlement_cfg = {} if ent_value.get('entitled'): entitlement_cfg = self.read_cache('machine-access-%s' % entitlement_name, silent=True) if not entitlement_cfg: # Fallback to machine-token info on unentitled entitlement_cfg = {'entitlement': ent_value} util.apply_series_overrides(entitlement_cfg) self._entitlements[entitlement_name] = entitlement_cfg return self._entitlements
def entitlements(self): """Return a dictionary of entitlements keyed by entitlement name. Return an empty dict if no entitlements are present. """ if self._entitlements: return self._entitlements machine_token = self.machine_token if not machine_token: return {} self._entitlements = {} contractInfo = machine_token["machineTokenInfo"]["contractInfo"] tokens_by_name = dict((e["type"], e["token"]) for e in machine_token.get("resourceTokens", [])) ent_by_name = dict( (e["type"], e) for e in contractInfo["resourceEntitlements"]) for entitlement_name, ent_value in ent_by_name.items(): entitlement_cfg = {"entitlement": ent_value} if entitlement_name in tokens_by_name: entitlement_cfg["resourceToken"] = tokens_by_name[ entitlement_name] util.apply_series_overrides(entitlement_cfg) self._entitlements[entitlement_name] = entitlement_cfg return self._entitlements
def test_error_on_non_entitlement_dict(self): """Raise a runtime error when seeing invalid dict type.""" with pytest.raises(RuntimeError) as exc: util.apply_series_overrides({"some": "dict"}) error = ('Expected entitlement access dict. Missing "entitlement" key:' " {'some': 'dict'}") assert error == str(exc.value)
def entitlements(self): """Return a dictionary of entitlements keyed by entitlement name. Return an empty dict if no entitlements are present. """ if self._entitlements: return self._entitlements machine_token = self.machine_token if not machine_token: return {} self._entitlements = {} contractInfo = machine_token["machineTokenInfo"]["contractInfo"] ent_by_name = dict( (e["type"], e) for e in contractInfo["resourceEntitlements"] ) for entitlement_name, ent_value in ent_by_name.items(): entitlement_cfg = {} if ent_value.get("entitled"): entitlement_cfg = self.read_cache( "machine-access-{}".format(entitlement_name), silent=True ) if not entitlement_cfg: # Fallback to machine-token info on unentitled entitlement_cfg = {"entitlement": ent_value} util.apply_series_overrides(entitlement_cfg) self._entitlements[entitlement_name] = entitlement_cfg return self._entitlements
def process_entitlement_delta(orig_access, new_access, allow_enable=False): """Process a entitlement access dictionary deltas if they exist. :param orig_access: Dict with original entitlement access details before contract refresh deltas :param orig_access: Dict with updated entitlement access details after contract refresh :param allow_enable: Boolean set True to perform enable operation on enableByDefault delta. When False log message about ignored default. """ from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME util.apply_series_overrides(new_access) deltas = util.get_dict_deltas(orig_access, new_access) if deltas: name = orig_access.get('entitlement', {}).get('type') if not name: name = deltas.get('entitlement', {}).get('type') if not name: raise RuntimeError( 'Could not determine contract delta service type %s %s' % (orig_access, new_access)) try: ent_cls = ENTITLEMENT_CLASS_BY_NAME[name] except KeyError: logging.debug( 'Skipping entitlement deltas for "%s". No such class', name) return deltas entitlement = ent_cls() entitlement.process_contract_deltas(orig_access, deltas, allow_enable=allow_enable) return deltas
def test_missing_keys_are_handled(self, _): orig_access = { "entitlement": { "series": { "xenial": { "directives": { "suites": ["xenial"] } } } } } expected = {"entitlement": {"directives": {"suites": ["xenial"]}}} util.apply_series_overrides(orig_access) assert expected == orig_access
def process_entitlement_delta( orig_access: "Dict[str, Any]", new_access: "Dict[str, Any]", allow_enable: bool = False, series_overrides: bool = True, ) -> "Dict": """Process a entitlement access dictionary deltas if they exist. :param orig_access: Dict with original entitlement access details before contract refresh deltas :param new_access: Dict with updated entitlement access details after contract refresh :param allow_enable: Boolean set True if allowed to perform the enable operation. When False, a message will be logged to inform the user about the recommended enabled service. :param series_overrides: Boolean set True if series overrides should be applied to the new_access dict. :raise UserFacingError: on failure to process deltas. :return: Dict of processed deltas """ from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME if series_overrides: util.apply_series_overrides(new_access) deltas = util.get_dict_deltas(orig_access, new_access) if deltas: name = orig_access.get("entitlement", {}).get("type") if not name: name = deltas.get("entitlement", {}).get("type") if not name: raise RuntimeError( "Could not determine contract delta service type {} {}".format( orig_access, new_access)) try: ent_cls = ENTITLEMENT_CLASS_BY_NAME[name] except KeyError: logging.debug( 'Skipping entitlement deltas for "%s". No such class', name) return deltas entitlement = ent_cls(assume_yes=allow_enable) entitlement.process_contract_deltas(orig_access, deltas, allow_enable=allow_enable) return deltas
def test_mutates_orig_access_dict(self, _): """Mutate orig_access dict when called.""" orig_access = { "entitlement": { "a": { "a1": "av1", "a2": { "aa2": "aav2" } }, "b": "b1", "c": "c1", "series": { "trusty": { "a": "t1" }, "xenial": { "a": { "a2": { "aa2": "xxv2" } }, "b": "bx1" }, }, } } expected = { "entitlement": { "a": { "a1": "av1", "a2": { "aa2": "xxv2" } }, "b": "bx1", "c": "c1", } } util.apply_series_overrides(orig_access) assert orig_access == expected
def process_contract_deltas( self, orig_access: "Dict[str, Any]", deltas: "Dict[str, Any]", allow_enable: bool = False, ) -> bool: """Process any contract access deltas for this entitlement. :param orig_access: Dictionary containing the original resourceEntitlement access details. :param deltas: Dictionary which contains only the changed access keys and values. :param allow_enable: Boolean set True if allowed to perform the enable operation. When False, a message will be logged to inform the user about the recommended enabled service. :return: True when delta operations are processed; False when noop. :raise: UserFacingError when auto-enable fails unexpectedly. """ if not deltas: return True # We processed all deltas that needed processing delta_entitlement = deltas.get("entitlement", {}) delta_directives = delta_entitlement.get("directives", {}) status_cache = self.cfg.read_cache("status-cache") transition_to_unentitled = bool(delta_entitlement == util.DROPPED_KEY) if not transition_to_unentitled: if delta_entitlement: util.apply_series_overrides(deltas) delta_entitlement = deltas["entitlement"] if orig_access and "entitled" in delta_entitlement: transition_to_unentitled = delta_entitlement["entitled"] in ( False, util.DROPPED_KEY, ) if transition_to_unentitled: if delta_directives and status_cache: application_status = self._check_application_status_on_cache() else: application_status, _ = self.application_status() if application_status != status.ApplicationStatus.DISABLED: if self.can_disable(silent=True): self.disable() logging.info( "Due to contract refresh, '%s' is now disabled.", self.name, ) else: logging.warning( "Unable to disable '%s' as recommended during contract" " refresh. Service is still active. See" " `ua status`", self.name, ) return True resourceToken = orig_access.get("resourceToken") if not resourceToken: resourceToken = deltas.get("resourceToken") delta_obligations = delta_entitlement.get("obligations", {}) can_enable = self.can_enable(silent=True) enableByDefault = bool( delta_obligations.get("enableByDefault") and resourceToken ) if can_enable and enableByDefault: if allow_enable: msg = status.MESSAGE_ENABLE_BY_DEFAULT_TMPL.format( name=self.name ) logging.info(msg) self.enable() else: msg = status.MESSAGE_ENABLE_BY_DEFAULT_MANUAL_TMPL.format( name=self.name ) logging.info(msg) return True return False
def process_contract_deltas(self, orig_access: 'Dict[str, Any]', deltas: 'Dict[str, Any]', allow_enable: bool = False) -> bool: """Process any contract access deltas for this entitlement. :param orig_access: Dictionary containing the original resourceEntitlement access details. :param deltas: Dictionary which contains only the changed access keys and values. :param allow_enable: Boolean set True if allowed to perform the enable operation. When False, a message will be logged to inform the user about the recommended enabled service. :return: True when delta operations are processed; False when noop. """ if not deltas: return True # We processed all deltas that needed processing delta_entitlement = deltas.get('entitlement', {}) transition_to_unentitled = bool(delta_entitlement == util.DROPPED_KEY) if not transition_to_unentitled: if delta_entitlement: util.apply_series_overrides(deltas) delta_entitlement = deltas['entitlement'] if orig_access and 'entitled' in delta_entitlement: transition_to_unentitled = (delta_entitlement['entitled'] in (False, util.DROPPED_KEY)) if transition_to_unentitled: application_status, _ = self.application_status() if application_status != status.ApplicationStatus.DISABLED: if self.can_disable(silent=True): self.disable() logging.info( "Due to contract refresh, '%s' is now disabled.", self.name) else: logging.warning( "Unable to disable '%s' as recommended during contract" " refresh. Service is still active. See `ua status`" % self.name) # Clean up former entitled machine-access-<name> response cache # file because uaclient doesn't access machine-access-* routes or # responses on unentitled services. self.cfg.delete_cache_key('machine-access-%s' % self.name) return True resourceToken = orig_access.get('resourceToken') if not resourceToken: resourceToken = deltas.get('resourceToken') delta_obligations = delta_entitlement.get('obligations', {}) can_enable = self.can_enable(silent=True) enableByDefault = bool( delta_obligations.get('enableByDefault') and resourceToken) if can_enable and enableByDefault: if allow_enable: msg = status.MESSAGE_ENABLE_BY_DEFAULT_TMPL.format( name=self.name) logging.info(msg) self.enable() else: msg = status.MESSAGE_ENABLE_BY_DEFAULT_MANUAL_TMPL.format( name=self.name) logging.info(msg) return True return False