def status(cfg: UAConfig, show_beta: bool = 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]", cfg.read_cache("status-cache")) if not response: response = _unattached_status(cfg) elif not cfg.is_attached: response = _unattached_status(cfg) else: response = _attached_status(cfg) response.update(_get_config_status(cfg)) if os.getuid() == 0: cfg.write_cache("status-cache", response) # Try to remove fix reboot notices if not applicable if not util.should_reboot(): cfg.remove_notice( "", messages.ENABLE_REBOOT_REQUIRED_TMPL.format( operation="fix operation" ), ) response = _handle_beta_resources(cfg, show_beta, response) return response
def test_read_cache_returns_none_when_data_path_absent( self, tmpdir, key, path_basename ): """Return None when the specified key data_path is not cached.""" cfg = UAConfig({"data_dir": tmpdir.strpath}) assert None is cfg.read_cache(key) assert not tmpdir.join(path_basename).check()
def test_read_cache_returns_none_when_data_path_absent( self, key, path_basename): """Return None when the specified key data_path is not cached.""" tmp_dir = self.tmp_dir() cfg = UAConfig({'data_dir': tmp_dir}) assert None is cfg.read_cache(key) assert False is os.path.exists(os.path.join(tmp_dir, path_basename))
def test_read_cache_returns_content_when_data_path_present( self, tmpdir, key, path_basename): cfg = UAConfig({'data_dir': tmpdir.strpath}) data_path = tmpdir.join(path_basename) with open(data_path.strpath, 'w') as f: f.write('content%s' % key) assert 'content%s' % key == cfg.read_cache(key)
def test_read_cache_returns_stuctured_content_when_json_data_path_present( self, tmpdir, key, path_basename): cfg = UAConfig({'data_dir': tmpdir.strpath}) data_path = tmpdir.join(path_basename) expected = {key: 'content%s' % key} with open(data_path.strpath, 'w') as f: f.write(json.dumps(expected)) assert expected == cfg.read_cache(key)
def test_read_cache_returns_content_when_data_path_present( self, key, path_basename): tmp_dir = self.tmp_dir() cfg = UAConfig({'data_dir': tmp_dir}) data_path = self.tmp_path(path_basename, tmp_dir) with open(data_path, 'w') as f: f.write('content%s' % key) assert 'content%s' % key == cfg.read_cache(key)
def test_datetimes_are_unserialised(self, tmpdir): cfg = UAConfig({"data_dir": tmpdir.strpath}) os.makedirs(tmpdir.join(PRIVATE_SUBDIR).strpath) data_path = tmpdir.join(PRIVATE_SUBDIR, "dt_test") with open(data_path.strpath, "w") as f: f.write('{"dt": "2019-07-25T14:35:51"}') actual = cfg.read_cache("dt_test") assert {"dt": datetime.datetime(2019, 7, 25, 14, 35, 51)} == actual
def test_write_cache_writes_non_private_dir_when_private_is_false( self, tmpdir): """When content is not a string, write a json string.""" cfg = UAConfig({'data_dir': tmpdir.strpath}) assert None is cfg.write_cache('key', 'value', private=False) with open(tmpdir.join('key').strpath, 'r') as stream: assert 'value' == stream.read() assert 'value' == cfg.read_cache('key')
def test_read_cache_returns_content_when_data_path_present( self, tmpdir, key, path_basename): cfg = UAConfig({"data_dir": tmpdir.strpath}) os.makedirs(tmpdir.join(PRIVATE_SUBDIR).strpath) data_path = tmpdir.join(PRIVATE_SUBDIR, path_basename) with open(data_path.strpath, "w") as f: f.write("content{}".format(key)) assert "content{}".format(key) == cfg.read_cache(key)
def upgrade_packages_and_attach( cfg: UAConfig, upgrade_pkgs: List[str], pocket: str, dry_run: bool ) -> bool: """Upgrade available packages to fix a CVE. Upgrade all packages in upgrades_packages and, if necessary, prompt regarding system attach prior to upgrading UA packages. :return: True if package upgrade completed or unneeded, False otherwise. """ if not upgrade_pkgs: return True # If we are running on --dry-run mode, we don't need to be root # to understand what will happen with the system if os.getuid() != 0 and not dry_run: print(messages.SECURITY_APT_NON_ROOT) return False if pocket != UBUNTU_STANDARD_UPDATES_POCKET: # We are now using status-cache because non-root users won't # have access to the private machine_token.json file. We # can use the status-cache as a proxy for the attached # information status_cache = cfg.read_cache("status-cache") or {} if not status_cache.get("attached", False): if not _check_attached(cfg, dry_run): return False elif _check_subscription_is_expired( status_cache=status_cache, cfg=cfg, dry_run=dry_run ): return False if not _check_subscription_for_required_service(pocket, cfg, dry_run): # User subscription does not have required service enabled return False print( colorize_commands( [ ["apt", "update", "&&"] + ["apt", "install", "--only-upgrade", "-y"] + sorted(upgrade_pkgs) ] ) ) if not dry_run: apt.run_apt_update_command() apt.run_apt_command( cmd=["apt-get", "install", "--only-upgrade", "-y"] + upgrade_pkgs, error_msg=messages.APT_INSTALL_FAILED.msg, env={"DEBIAN_FRONTEND": "noninteractive"}, ) return True
def test_read_cache_returns_stuctured_content_when_json_data_path_present( self, tmpdir, key, path_basename): cfg = UAConfig({"data_dir": tmpdir.strpath}) os.makedirs(tmpdir.join(PRIVATE_SUBDIR).strpath) data_path = tmpdir.join(PRIVATE_SUBDIR, path_basename) expected = {key: "content{}".format(key)} with open(data_path.strpath, "w") as f: f.write(json.dumps(expected)) assert expected == cfg.read_cache(key)
def test_write_cache_writes_json_string_when_content_not_a_string( self, tmpdir, key, value): """When content is not a string, write a json string.""" cfg = UAConfig({"data_dir": tmpdir.strpath}) expected_json_content = json.dumps(value) assert None is cfg.write_cache(key, value) with open(tmpdir.join(PRIVATE_SUBDIR, key).strpath, "r") as stream: assert expected_json_content == stream.read() assert value == cfg.read_cache(key)
def test_read_cache_returns_stuctured_content_when_json_data_path_present( self, key, path_basename): tmp_dir = self.tmp_dir() cfg = UAConfig({'data_dir': tmp_dir}) data_path = self.tmp_path(path_basename, tmp_dir) expected = {key: 'content%s' % key} with open(data_path, 'w') as f: f.write(json.dumps(expected)) assert expected == cfg.read_cache(key)
def test_write_cache_writes_json_string_when_content_not_a_string( self, key, value): """When content is not a string, write a json string.""" tmp_dir = self.tmp_dir() cfg = UAConfig({'data_dir': tmp_dir}) expected_json_content = json.dumps(value) assert None is cfg.write_cache(key, value) with open(self.tmp_path(key, tmp_dir), 'r') as stream: assert expected_json_content == stream.read() assert value == cfg.read_cache(key)
def test_write_cache_creates_dir_when_data_dir_does_not_exist(self): """When data_dir doesn't exist, create it.""" tmp_subdir = self.tmp_path('does/not/exist') cfg = UAConfig({'data_dir': tmp_subdir}) assert False is os.path.isdir(tmp_subdir), ( 'Found unexpected directory %s' % tmp_subdir) assert None is cfg.write_cache('somekey', 'someval') assert True is os.path.isdir(tmp_subdir), ( 'Missing expected directory %s' % tmp_subdir) assert 'someval' == cfg.read_cache('somekey')
def test_write_cache_write_key_name_in_data_dir_when_data_path_absent( self, tmpdir, key, content): """When key is not in data_paths, write content to data_dir/key.""" cfg = UAConfig({"data_dir": tmpdir.strpath}) expected_path = tmpdir.join(PRIVATE_SUBDIR, key) assert not expected_path.check(), "Found unexpected file {}".format( expected_path) assert None is cfg.write_cache(key, content) assert expected_path.check(), "Missing expected file {}".format( expected_path) assert content == cfg.read_cache(key)
def test_write_cache_write_key_name_in_data_dir_when_data_path_absent( self, tmpdir, key, content): """When key is not in data_paths, write content to data_dir/key.""" cfg = UAConfig({'data_dir': tmpdir.strpath}) expected_path = tmpdir.join(key) assert not expected_path.check(), ( 'Found unexpected file %s' % expected_path) assert None is cfg.write_cache(key, content) assert expected_path.check(), ( 'Missing expected file %s' % expected_path) assert content == cfg.read_cache(key)
def test_write_cache_write_key_name_in_data_dir_when_data_path_absent( self, key, content): """When key is not in data_paths, write content to data_dir/key.""" tmp_dir = self.tmp_dir() cfg = UAConfig({'data_dir': tmp_dir}) expected_path = os.path.join(tmp_dir, key) assert False is os.path.exists(expected_path), ( 'Found unexpected file %s' % expected_path) assert None is cfg.write_cache(key, content) assert True is os.path.exists(expected_path), ( 'Missing expected file %s' % expected_path) assert content == cfg.read_cache(key)
def test_write_cache_creates_dir_when_data_dir_does_not_exist( self, tmpdir): """When data_dir doesn't exist, create it.""" tmp_subdir = tmpdir.join("does/not/exist") cfg = UAConfig({"data_dir": tmp_subdir.strpath}) assert False is os.path.isdir( tmp_subdir.strpath), "Found unexpected directory {}".format( tmp_subdir) assert None is cfg.write_cache("somekey", "someval") assert True is os.path.isdir( tmp_subdir.strpath), "Missing expected directory {}".format( tmp_subdir) assert "someval" == cfg.read_cache("somekey")
def _get_contract_token_from_cloud_identity(cfg: config.UAConfig) -> str: """Detect cloud_type and request a contract token from identity info. :param cfg: a ``config.UAConfig`` instance :raise NonAutoAttachImageError: When not on an auto-attach image type. :raise UrlError: 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. :return: contract token obtained from identity doc """ try: instance = identity.cloud_instance_factory() except exceptions.UserFacingError as e: if cfg.is_attached: # We are attached on non-Pro Image, just report already attached raise exceptions.AlreadyAttachedError(cfg) # Unattached on non-Pro return UserFacing error msg details raise e current_iid = identity.get_instance_id() if cfg.is_attached: prev_iid = cfg.read_cache("instance-id") if current_iid == prev_iid: raise exceptions.AlreadyAttachedError(cfg) print("Re-attaching Ubuntu Advantage subscription on new instance") if _detach(cfg, assume_yes=True) != 0: raise exceptions.UserFacingError( ua_status.MESSAGE_DETACH_AUTOMATION_FAILURE ) contract_client = contract.UAContractClient(cfg) try: tokenResponse = contract_client.request_auto_attach_contract_token( instance=instance ) except contract.ContractAPIError as e: if e.code and 400 <= e.code < 500: raise exceptions.NonAutoAttachImageError( ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH ) raise e if current_iid: cfg.write_cache("instance-id", current_iid) return tokenResponse["contractToken"]
def prompt_request_macaroon(cfg: UAConfig, caveat_id: str) -> dict: discharge_macaroon = cfg.read_cache('macaroon') if discharge_macaroon: # TODO(invalidate cached macaroon on root-macaroon or discharge expiry) return discharge_macaroon email = input('Email: ') password = getpass.getpass('Password: '******'email': email, 'password': password, 'caveat_id': caveat_id} sso_client = UbuntuSSOClient(cfg) content = None twofactor_retries = 0 while True: try: content = sso_client.request_discharge_macaroon(**args) except SSOAuthError as e: if API_ERROR_2FA_REQUIRED in e: args['otp'] = input('Second-factor auth: ') continue elif API_ERROR_INVALID_CREDENTIALS in e: # This is arguably bug in canonical-identity-provider code # that the error 'code' is 'invalid-credentials' when docs # clearly designates a 'twofactor-error' code that should be # emitted when the 2FA token is invalid. There are no plans for # changes to the error codes or messages as it might break # existing clients. As a result, we have to distinguish # email/password invalid-credentials errors from 2-factor # errors by searching the attached error 'message' field for # 2-factor. if '2-factor' in e[API_ERROR_INVALID_CREDENTIALS]: if twofactor_retries < TWOFACTOR_RETRIES: args['otp'] = input( 'Invalid second-factor auth, try again: ') twofactor_retries += 1 continue raise exceptions.UserFacingError(str(e)) break if not content: raise exceptions.UserFacingError('SSO server returned empty content') return content
def run_jobs(cfg: UAConfig, current_time: datetime): """Run jobs in order when next_run is before current_time. Persist jobs-status with calculated next_run values to aid in timer state introspection for jobs which have not yet run. """ jobs_status = cfg.read_cache("jobs-status") or {} for job in UACLIENT_JOBS: if job.name in jobs_status: next_run = jobs_status[job.name]["next_run"] if next_run > current_time: continue # Skip job as expected next_run hasn't yet passed if job.run(cfg): # Persist last_run and next_run UTC-based times on job success. jobs_status[job.name] = { "last_run": current_time, "next_run": current_time + timedelta(seconds=job.run_interval_seconds(cfg)), } cfg.write_cache(key="jobs-status", content=jobs_status)