%PDF- %PDF-
Direktori : /lib/python3/dist-packages/uaclient/ |
Current File : //lib/python3/dist-packages/uaclient/actions.py |
import datetime import glob import json import logging import os import re import shutil from typing import List, Optional # noqa: F401 from uaclient import ( api, clouds, config, contract, entitlements, event_logger, exceptions, livepatch, ) from uaclient import log as pro_log from uaclient import messages, secret_manager from uaclient import status as ua_status from uaclient import system, timer, util from uaclient.clouds import AutoAttachCloudInstance # noqa: F401 from uaclient.defaults import ( APPARMOR_PROFILES, CLOUD_BUILD_INFO, DEFAULT_CONFIG_FILE, DEFAULT_LOG_PREFIX, ) from uaclient.files.state_files import ( AttachmentData, attachment_data_file, machine_id_file, timer_jobs_state_file, ) event = event_logger.get_event_logger() LOG = logging.getLogger(util.replace_top_level_logger_name(__name__)) UA_SERVICES = ( "apt-news.service", "esm-cache.service", "ua-timer.service", "ua-timer.timer", "ua-auto-attach.path", "ua-auto-attach.service", "ua-reboot-cmds.service", "ubuntu-advantage.service", ) USER_LOG_COLLECTED_LIMIT = 10 def _handle_partial_attach( cfg: config.UAConfig, contract_client: contract.UAContractClient, attached_at: datetime.datetime, ): from uaclient.timer.update_messaging import update_motd_messages attachment_data_file.write(AttachmentData(attached_at=attached_at)) ua_status.status(cfg=cfg) update_motd_messages(cfg) contract_client.update_activity_token() def _enable_default_services( cfg: config.UAConfig, services_to_be_enabled: List[contract.EnableByDefaultService], contract_client: contract.UAContractClient, attached_at: datetime.datetime, silent: bool = False, ): ret = True failed_services = [] unexpected_errors = [] try: for enable_by_default_service in services_to_be_enabled: ent_ret, reason = enable_entitlement_by_name( cfg=cfg, name=enable_by_default_service.name, assume_yes=True, allow_beta=True, variant=enable_by_default_service.variant, silent=silent, ) ret &= ent_ret if not ent_ret: failed_services.append(enable_by_default_service.name) else: event.service_processed(service=enable_by_default_service.name) except exceptions.ConnectivityError as exc: event.service_failed(enable_by_default_service.name) _handle_partial_attach(cfg, contract_client, attached_at) raise exc except exceptions.UbuntuProError: failed_services.append(enable_by_default_service.name) ret = False except Exception as e: ret = False failed_services.append(enable_by_default_service.name) unexpected_errors.append(e) if not ret: # Persist updated status in the event of partial attach _handle_partial_attach(cfg, contract_client, attached_at) event.services_failed(failed_services) if unexpected_errors: raise exceptions.AttachFailureUnknownError( failed_services=[ ( name, messages.UNEXPECTED_ERROR.format( error_msg=str(exception), log_path=pro_log.get_user_or_root_log_file_path(), ), ) for name, exception in zip( failed_services, unexpected_errors ) ] ) else: raise exceptions.AttachFailureDefaultServices( failed_services=[ (name, messages.E_ATTACH_FAILURE_DEFAULT_SERVICES) for name in failed_services ] ) def attach_with_token( cfg: config.UAConfig, token: str, allow_enable: bool, silent: bool = False, ) -> None: """ Common functionality to take a token and attach via contract backend :raise ConnectivityError: On unexpected connectivity issues to contract server or inability to access identity doc from metadata service. :raise ContractAPIError: On unexpected errors when talking to the contract server. """ from uaclient.entitlements import ( check_entitlement_apt_directives_are_unique, ) from uaclient.timer.update_messaging import update_motd_messages secret_manager.secrets.add_secret(token) contract_client = contract.UAContractClient(cfg) attached_at = datetime.datetime.now(tz=datetime.timezone.utc) new_machine_token = contract_client.add_contract_machine( contract_token=token, attachment_dt=attached_at ) cfg.machine_token_file.write(new_machine_token) try: check_entitlement_apt_directives_are_unique(cfg) except exceptions.EntitlementsAPTDirectivesAreNotUnique as e: cfg.machine_token_file.delete() raise e system.get_machine_id.cache_clear() machine_id = new_machine_token.get("machineTokenInfo", {}).get( "machineId", system.get_machine_id(cfg) ) machine_id_file.write(machine_id) if allow_enable: services_to_be_enabled = contract.get_enabled_by_default_services( cfg, cfg.machine_token_file.entitlements ) _enable_default_services( cfg=cfg, services_to_be_enabled=services_to_be_enabled, contract_client=contract_client, attached_at=attached_at, silent=silent, ) attachment_data_file.write(AttachmentData(attached_at=attached_at)) update_motd_messages(cfg) timer.start() def auto_attach( cfg: config.UAConfig, cloud: clouds.AutoAttachCloudInstance, allow_enable=True, ) -> None: """ :raise ConnectivityError: On unexpected connectivity issues to contract server or inability to access identity doc from metadata service. :raise ContractAPIError: On unexpected errors when talking to the contract server. :raise NonAutoAttachImageError: If this cloud type does not have auto-attach support. """ contract_client = contract.UAContractClient(cfg) tokenResponse = contract_client.get_contract_token_for_cloud_instance( instance=cloud ) token = tokenResponse["contractToken"] attach_with_token(cfg, token=token, allow_enable=allow_enable) def enable_entitlement_by_name( cfg: config.UAConfig, name: str, *, assume_yes: bool = False, allow_beta: bool = False, access_only: bool = False, variant: str = "", silent: bool = False, extra_args: Optional[List[str]] = None ): """ Constructs an entitlement based on the name provided. Passes kwargs onto the entitlement constructor. :raise EntitlementNotFoundError: If no entitlement with the given name is found, then raises this error. """ ent_cls = entitlements.entitlement_factory( cfg=cfg, name=name, variant=variant ) entitlement = ent_cls( cfg, assume_yes=assume_yes, allow_beta=allow_beta, called_name=name, access_only=access_only, extra_args=extra_args, ) if not silent: event.info(messages.ENABLING_TMPL.format(title=entitlement.title)) ent_ret, reason = entitlement.enable(api.ProgressWrapper()) if ent_ret and not silent: event.info(messages.ENABLED_TMPL.format(title=entitlement.title)) return ent_ret, reason def status( cfg: config.UAConfig, *, simulate_with_token: Optional[str] = None, show_all: bool = False ): """ Construct the current Pro status dictionary. """ if simulate_with_token: status, ret = ua_status.simulate_status( cfg=cfg, token=simulate_with_token, show_all=show_all, ) else: status = ua_status.status(cfg=cfg, show_all=show_all) ret = 0 return status, ret def _write_apparmor_logs_to_file(filename: str) -> None: """ Helper which gets ubuntu_pro apparmor logs from the kernel from the last day and writes them to the specified filename. """ # can't use journalctl's --grep, because xenial doesn't support it :/ cmd = ["journalctl", "-b", "-k", "--since=1 day ago"] # all profiles are prefixed with "ubuntu_pro_" apparmor_re = r"apparmor=\".*(profile=\"ubuntu_pro_|name=\"ubuntu_pro_)" kernel_logs = None try: kernel_logs, _ = system.subp(cmd) except exceptions.ProcessExecutionError as e: LOG.warning("Failed to collect kernel logs:\n%s", str(e)) system.write_file("{}-error".format(filename), str(e)) else: if kernel_logs: # some unit tests mock subp to return (None,None) apparmor_logs = [] # filter out only what interests us for kernel_line in kernel_logs.split("\n"): if re.search(apparmor_re, kernel_line): apparmor_logs.append(kernel_line) system.write_file(filename, "\n".join(apparmor_logs)) def _write_command_output_to_file( cmd, filename: str, return_codes: Optional[List[int]] = None ) -> None: """Helper which runs a command and writes output or error to filename.""" try: out, _ = system.subp(cmd.split(), rcs=return_codes) except exceptions.ProcessExecutionError as e: system.write_file("{}-error".format(filename), str(e)) else: system.write_file(filename, out) def _get_state_files(cfg: config.UAConfig): # include cfg log files here because they could be set to non default return [ cfg.cfg_path or DEFAULT_CONFIG_FILE, cfg.log_file, timer_jobs_state_file.ua_file.path, CLOUD_BUILD_INFO, *( entitlement(cfg).repo_file for entitlement in entitlements.ENTITLEMENT_CLASSES if issubclass(entitlement, entitlements.repo.RepoEntitlement) ), ] def collect_logs(cfg: config.UAConfig, output_dir: str): """ Write all relevant Ubuntu Pro logs to the specified directory """ _write_command_output_to_file( "cloud-id", "{}/cloud-id.txt".format(output_dir) ) _write_command_output_to_file( "{} status".format(livepatch.LIVEPATCH_CMD), "{}/livepatch-status.txt".format(output_dir), ) _write_command_output_to_file( "systemctl list-timers --all", "{}/systemd-timers.txt".format(output_dir), ) _write_command_output_to_file( ( "journalctl --boot=0 -o short-precise " "-u cloud-init-local.service " "-u cloud-init-config.service " "-u cloud-config.service" ), "{}/cloud-init-journal.txt".format(output_dir), ) _write_command_output_to_file( ("journalctl -o short-precise " "{}").format( " ".join( ["-u {}".format(s) for s in UA_SERVICES if ".service" in s] ) ), "{}/pro-journal.txt".format(output_dir), ) for service in UA_SERVICES: _write_command_output_to_file( "systemctl status {}".format(service), "{}/{}.txt".format(output_dir, service), return_codes=[0, 3], ) pro_status, _ = status(cfg=cfg, show_all=False) system.write_file( "{}/pro-status.json".format(output_dir), json.dumps(pro_status, cls=util.DatetimeAwareJSONEncoder), ) env_vars = util.get_pro_environment() system.write_file( "{}/environment_vars.json".format(output_dir), json.dumps(env_vars), ) state_files = _get_state_files(cfg) user_log_files = ( pro_log.get_all_user_log_files()[:USER_LOG_COLLECTED_LIMIT] if util.we_are_currently_root() else [pro_log.get_user_log_file()] ) # save log file in compressed file for log_file_idx, log_file in enumerate(user_log_files): try: content = util.redact_sensitive_logs(system.load_file(log_file)) system.write_file( os.path.join(output_dir, "user{}.log".format(log_file_idx)), content, ) except Exception as e: LOG.warning( "Failed to collect user log file: %s\n%s", log_file, str(e) ) # also get default logrotated log files for f in state_files + glob.glob(DEFAULT_LOG_PREFIX + "*"): if os.path.isfile(f): try: content = system.load_file(f) except Exception as e: # If we fail to load that file for any reason we will # not break the command, we will instead warn the user # about the issue and try to process the other files LOG.warning("Failed to load file: %s\n%s", f, str(e)) continue content = util.redact_sensitive_logs(content) if util.we_are_currently_root(): # if root, overwrite the original with redacted content system.write_file(f, content) system.write_file( os.path.join(output_dir, os.path.basename(f)), content ) # get apparmor logs _write_apparmor_logs_to_file("{}/apparmor_logs.txt".format(output_dir)) # include apparmor profiles for f in APPARMOR_PROFILES: if os.path.isfile(f): try: shutil.copy(f, output_dir) except Exception as e: LOG.warning("Failed to copy file: %s\n%s", f, str(e)) continue