def test_exception_is_config_value_true(self, config_dict, key_val, FakeConfig): path_to_value = "features.allow_beta" cfg = FakeConfig() cfg.override_features(config_dict) with pytest.raises(exceptions.UserFacingError) as excinfo: util.is_config_value_true(config=cfg.cfg, path_to_value=path_to_value) expected_msg = status.ERROR_INVALID_CONFIG_VALUE.format( path_to_value=path_to_value, expected_value="boolean string: true or false", value=key_val, ) assert expected_msg == str(excinfo.value)
def write_esm_announcement_message(cfg: config.UAConfig, series: str) -> None: """Write human-readable messages if ESM is offered on this LTS release. Do not write ESM announcements if esm-apps is enabled or beta. :param cfg: UAConfig instance for this environment. :param series: string of Ubuntu release series: 'xenial'. """ apps_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-apps") apps_inst = apps_cls(cfg) enabled_status = ApplicationStatus.ENABLED apps_not_enabled = apps_inst.application_status()[0] != enabled_status config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta") apps_not_beta = bool(config_allow_beta or not apps_cls.is_beta) msg_dir = os.path.join(cfg.data_dir, "messages") esm_news_file = os.path.join(msg_dir, ExternalMessage.ESM_ANNOUNCE.value) platform_info = util.get_platform_info() is_active_esm = util.is_active_esm(platform_info["series"]) if is_active_esm: ua_esm_url = defaults.EOL_UA_URL_TMPL.format( hyphenatedrelease=platform_info["release"].replace(".", "-")) else: ua_esm_url = defaults.BASE_ESM_URL if apps_not_beta and apps_not_enabled: util.write_file(esm_news_file, "\n" + ANNOUNCE_ESM_TMPL.format(url=ua_esm_url)) else: util.remove_file(esm_news_file)
def _allow_fips_on_cloud_instance( self, series: str, cloud_id: str ) -> bool: """Return False when FIPS is allowed on this cloud and series. On Xenial GCP there will be no cloud-optimized kernel so block default ubuntu-fips enable. This can be overridden in config with features.allow_xenial_fips_on_cloud. GCP doesn't yet have a cloud-optimized kernel or metapackage so block enable of fips if the contract does not specify ubuntu-gcp-fips. This also can be overridden in config with features.allow_default_fips_metapackage_on_gcp. :return: False when this cloud, series or config override allows FIPS. """ if cloud_id == "gce": if util.is_config_value_true( config=self.cfg.cfg, path_to_value="features.allow_default_fips_metapackage_on_gcp", ): return True # GCE only has FIPS support for bionic and focal machines if series in ("bionic", "focal"): return True return bool("ubuntu-gcp-fips" in super().packages) return True
def status(self, show_beta=False) -> "Dict[str, Any]": """Return status as a dict, using a cache for non-root users When unattached, get available resources from the contract service to report detailed availability of different resources for this machine. Write the status-cache when called by root. """ if os.getuid() != 0: response = cast("Dict[str, Any]", self.read_cache("status-cache")) if not response: response = self._unattached_status() elif not self.is_attached: response = self._unattached_status() else: response = self._attached_status() response.update(self._get_config_status()) if os.getuid() == 0: self.write_cache("status-cache", response) config_allow_beta = util.is_config_value_true( config=self.cfg, path_to_value="features.allow_beta") show_beta |= config_allow_beta if not show_beta: response = self._remove_beta_resources(response) return response
def valid_services(cfg: UAConfig, allow_beta: bool = False, all_names: bool = False) -> List[str]: """Return a list of valid (non-beta) services. :param cfg: UAConfig instance :param allow_beta: if we should allow beta services to be marked as valid :param all_names: if we should return all the names for a service instead of just the presentation_name """ allow_beta_cfg = is_config_value_true(cfg.cfg, "features.allow_beta") allow_beta |= allow_beta_cfg entitlements = ENTITLEMENT_CLASSES if not allow_beta: entitlements = [ entitlement for entitlement in entitlements if not entitlement.is_beta ] if all_names: names = [] for entitlement in entitlements: names.extend(entitlement(cfg=cfg).valid_names) return sorted(names) return sorted([ entitlement(cfg=cfg).presentation_name for entitlement in entitlements ])
def _handle_beta_resources(cfg, show_beta, response) -> Dict[str, Any]: """Remove beta services from response dict if needed""" config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta" ) show_beta |= config_allow_beta if show_beta: return response new_response = copy.deepcopy(response) released_resources = [] for resource in new_response.get("services", {}): resource_name = resource["name"] try: ent_cls = entitlement_factory(cfg=cfg, name=resource_name) except exceptions.EntitlementNotFoundError: """ Here we cannot know the status of a service, since it is not listed as a valid entitlement. Therefore, we keep this service in the list, since we cannot validate if it is a beta service or not. """ released_resources.append(resource) continue enabled_status = UserFacingStatus.ACTIVE.value if not ent_cls.is_beta or resource.get("status", "") == enabled_status: released_resources.append(resource) if released_resources: new_response["services"] = released_resources return new_response
def _perform_enable(entitlement_name: str, cfg: config.UAConfig, *, assume_yes: bool = False, silent_if_inapplicable: bool = False, allow_beta: bool = False) -> bool: """Perform the enable action on a named entitlement. (This helper excludes any messaging, so that different enablement code paths can message themselves.) :param entitlement_name: the name of the entitlement to enable :param cfg: the UAConfig to pass to the entitlement :param assume_yes: Assume a yes response for any prompts during service enable :param silent_if_inapplicable: don't output messages when determining if an entitlement can be enabled on this system :param allow_beta: Allow enabling beta services @return: True on success, False otherwise """ ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[entitlement_name] config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta") allow_beta |= config_allow_beta if not allow_beta and ent_cls.is_beta: tmpl = ua_status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL raise exceptions.BetaServiceError( tmpl.format(operation="enable", name=entitlement_name)) entitlement = ent_cls(cfg, assume_yes=assume_yes) ret = entitlement.enable(silent_if_inapplicable=silent_if_inapplicable) cfg.status() # Update the status cache return ret
def valid_service(self): """Check if the service is marked as valid (non-beta)""" if self._valid_service is None: self._valid_service = ( not self.is_beta or self.allow_beta or is_config_value_true(self.cfg.cfg, "features.allow_beta") ) return self._valid_service
def action_auto_attach(args, cfg): disable_auto_attach = util.is_config_value_true( config=cfg.cfg, path_to_value="features.disable_auto_attach") if disable_auto_attach: msg = "Skipping auto-attach. Config disable_auto_attach is set." logging.debug(msg) print(msg) return 0 token = _get_contract_token_from_cloud_identity(cfg) return _attach_with_token(cfg, token=token, allow_enable=True)
def handle_incompatible_services( self, ) -> Tuple[bool, Optional[messages.NamedMessage]]: """ Prompt user when incompatible services are found during enable. When enabling a service, we may find that there is an incompatible service already enable. In that situation, we can ask the user if the incompatible service should be disabled before proceeding. There are also different ways to configure that behavior: We can disable removing incompatible service during enable by adding the following lines into uaclient.conf: features: block_disable_on_enable: true """ cfg_block_disable_on_enable = util.is_config_value_true( config=self.cfg.cfg, path_to_value="features.block_disable_on_enable", ) for service in self.blocking_incompatible_services(): ent = service.entitlement(self.cfg, assume_yes=True) user_msg = messages.INCOMPATIBLE_SERVICE.format( service_being_enabled=self.title, incompatible_service=ent.title, ) e_msg = messages.INCOMPATIBLE_SERVICE_STOPS_ENABLE.format( service_being_enabled=self.title, incompatible_service=ent.title, ) if cfg_block_disable_on_enable: return False, e_msg if not util.prompt_for_confirmation( msg=user_msg, assume_yes=self.assume_yes ): return False, e_msg disable_msg = "Disabling incompatible service: {}".format( ent.title ) event.info(disable_msg) ret = ent.disable(silent=True) if not ret: return ret, None return True, None
def _replace_metapackage_on_cloud_instance( self, packages: List[str] ) -> List[str]: """ Identify correct metapackage to be used if in a cloud instance. Currently, the contract backend is not delivering the right metapackage on a Bionic Azure or AWS cloud instance. For those clouds, we have cloud specific fips metapackages and we should use them. We are now performing that correction here, but this is a temporary fix. """ cfg_disable_fips_metapackage_override = util.is_config_value_true( config=self.cfg.cfg, path_to_value="features.disable_fips_metapackage_override", ) if cfg_disable_fips_metapackage_override: return packages series = util.get_platform_info().get("series") if series not in ("bionic", "focal"): return packages cloud_id, _ = get_cloud_type() if cloud_id is None: cloud_id = "" cloud_match = re.match(r"^(?P<cloud>(azure|aws|gce)).*", cloud_id) cloud_id = cloud_match.group("cloud") if cloud_match else "" if cloud_id not in ("azure", "aws", "gce"): return packages cloud_id = "gcp" if cloud_id == "gce" else cloud_id cloud_metapkg = "ubuntu-{}-fips".format(cloud_id) # Replace only the ubuntu-fips meta package if exists return [ cloud_metapkg if pkg == "ubuntu-fips" else pkg for pkg in packages ]
def poll_for_pro_license(cfg: UAConfig): if util.is_config_value_true(config=cfg.cfg, path_to_value="features.disable_auto_attach"): LOG.debug("Configured to not auto attach, shutting down") return if cfg.is_attached: LOG.debug("Already attached, shutting down") return if not util.is_current_series_lts(): LOG.debug("Not on LTS, shutting down") return try: cloud = cloud_instance_factory() except exceptions.CloudFactoryError: LOG.debug("Not on cloud, shutting down") return if not isinstance(cloud, UAAutoAttachGCPInstance): LOG.debug("Not on gcp, shutting down") return if not cloud.should_poll_for_pro_license(): LOG.debug("Not on supported instance, shutting down") return try: pro_license_present = cloud.is_pro_license_present( wait_for_change=False) except exceptions.CancelProLicensePolling: LOG.debug("Cancelling polling") return except exceptions.DelayProLicensePolling: # Continue to polling loop anyway and handle error there if it occurs # again pass else: if pro_license_present: attempt_auto_attach(cfg, cloud) return if not cfg.poll_for_pro_license: LOG.debug("Configured to not poll for pro license, shutting down") return while True: try: start = time.time() pro_license_present = cloud.is_pro_license_present( wait_for_change=True) end = time.time() except exceptions.CancelProLicensePolling: LOG.debug("Cancelling polling") return except exceptions.DelayProLicensePolling: time.sleep(cfg.polling_error_retry_delay) continue else: if cfg.is_attached: # This could have changed during the long poll or sleep LOG.debug("Already attached, shutting down") return if pro_license_present: attempt_auto_attach(cfg, cloud) return if end - start < 10: LOG.debug( "wait_for_change returned quickly and no pro license" " present. Waiting {} seconds before polling again".format( cfg.polling_error_retry_delay)) time.sleep(cfg.polling_error_retry_delay) continue
def write_apt_and_motd_templates(cfg: config.UAConfig, series: str) -> None: """Write messaging templates about available esm packages. :param cfg: UAConfig instance for this environment. :param series: string of Ubuntu release series: 'xenial'. """ apps_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_NO_PKGS.value apps_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_PKGS.value infra_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_NO_PKGS.value infra_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_PKGS.value motd_apps_no_pkg_file = ExternalMessage.MOTD_APPS_NO_PKGS.value motd_apps_pkg_file = ExternalMessage.MOTD_APPS_PKGS.value motd_infra_no_pkg_file = ExternalMessage.MOTD_INFRA_NO_PKGS.value motd_infra_pkg_file = ExternalMessage.MOTD_INFRA_PKGS.value no_warranty_file = ExternalMessage.UBUNTU_NO_WARRANTY.value msg_dir = os.path.join(cfg.data_dir, "messages") apps_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-apps") apps_inst = apps_cls(cfg) config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta") apps_valid = bool(config_allow_beta or not apps_cls.is_beta) infra_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-infra") infra_inst = infra_cls(cfg) expiry_status, remaining_days = get_contract_expiry_status(cfg) enabled_status = ApplicationStatus.ENABLED msg_esm_apps = False msg_esm_infra = False if util.is_active_esm(series): no_warranty_msg = "" if expiry_status in ( ContractExpiryStatus.EXPIRED, ContractExpiryStatus.NONE, ): no_warranty_msg = UBUNTU_NO_WARRANTY if infra_inst.application_status()[0] != enabled_status: msg_esm_infra = True no_warranty_msg = UBUNTU_NO_WARRANTY elif remaining_days <= defaults.CONTRACT_EXPIRY_PENDING_DAYS: msg_esm_infra = True _write_template_or_remove(no_warranty_msg, os.path.join(msg_dir, no_warranty_file)) if not msg_esm_infra: # write_apt_and_motd_templates is only called if util.is_lts(series) msg_esm_apps = apps_valid if msg_esm_infra: _write_esm_service_msg_templates( cfg, infra_inst, expiry_status, remaining_days, infra_pkg_file, infra_no_pkg_file, motd_infra_pkg_file, motd_infra_no_pkg_file, ) else: _remove_msg_templates( msg_dir=os.path.join(cfg.data_dir, "messages"), msg_template_names=[ infra_pkg_file, infra_no_pkg_file, motd_infra_pkg_file, motd_infra_no_pkg_file, ], ) if msg_esm_apps: _write_esm_service_msg_templates( cfg, apps_inst, expiry_status, remaining_days, apps_pkg_file, apps_no_pkg_file, motd_apps_pkg_file, motd_apps_no_pkg_file, ) else: _remove_msg_templates( msg_dir=os.path.join(cfg.data_dir, "messages"), msg_template_names=[ apps_pkg_file, apps_no_pkg_file, motd_apps_pkg_file, motd_apps_no_pkg_file, ], )
def test_is_config_value_true(self, config_dict, return_val, FakeConfig): cfg = FakeConfig() cfg.override_features(config_dict) actual_val = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta") assert return_val == actual_val