def mail_changes(config, changes, subject): ALCLog.info( _("Mailing %(address)s: %(subject)s") % { 'address': config.email_address, 'subject': subject }) charset = email.charset.Charset('utf-8') charset.body_encoding = '8bit' charset.header_encoding = email.charset.QP message = email.message.Message() if config.email_format == 'html': changes = html(config).convert_to_html(subject, changes) message['Content-Type'] = 'text/html; charset=utf-8' message['Auto-Submitted'] = 'auto-generated' message['Subject'] = email.header.Header(subject, charset) message['To'] = config.email_address message.set_payload(changes, charset) try: subprocess.run(['/usr/sbin/sendmail', '-oi', '-t'], input=message.as_bytes(), check=True) except Exception as ex: ALCLog.warning( _("Failed to send mail to %(address)s: %(errmsg)s") % { 'address': config.email_address, 'errmsg': ex })
def _find_user_pw(self): if os.getuid() != 0: return None pwdata = None for envvar in ('APT_LISTCHANGES_USER', 'SUDO_USER', 'USERNAME'): if envvar in os.environ: try: user = os.environ[envvar] pwdata = pwd.getpwnam( user) if not user.isdigit() else pwd.getpwuid(user) break # check the first environment variable only except Exception as ex: raise RuntimeError( _("Error getting user from variable '%(envvar)s': %(errmsg)s" ) % { 'envvar': envvar, 'errmsg': str(ex) }) from ex if pwdata and pwdata.pw_uid: return pwdata ALCLog.warning(_("Cannot find suitable user to drop root privileges")) return None
def __init__(self, path, readOnly=False): super().__init__() self._extension = '.db' if path[-3:] != self._extension: raise DbError( _("Database %(db)s does not end with %(ext)s") % { 'db': path, 'ext': self._extension }) self._dbpath = path[:-3] # strip the .db suffix try: mode = 'r' if readOnly else 'c' self._seen = ndbm.open(self._dbpath, mode, 0o644) 'foo%0' in self._seen except Exception as ex: raise DbError( _("Database %(db)s failed to load: %(errmsg)s") % { 'db': path, 'errmsg': str(ex) }) from ex # Will replace seen after changes have actually been seen self._seen_new = {}
def confirm_or_exit(config, frontend): if not config.confirm: return try: if not frontend.confirm(): ALCLog.error(_('Aborting')) sys.exit(BREAK_APT_EXIT_CODE) except (KeyboardInterrupt, EOFError): sys.exit(BREAK_APT_EXIT_CODE) except Exception as ex: ALCLog.error(_("Confirmation failed: %s") % str(ex)) sys.exit(1)
def display_output(self, text): # Note: the following fork() call is needed to have temporary file deleted # after the process created by Popen finishes. if not self.wait and os.fork() != 0: # We are the parent, return. return tmp = tempfile.NamedTemporaryFile(prefix="apt-listchanges-tmp", suffix=self.suffix, dir=self.get_tmpdir()) tmp.write(self.enc.to_bytes(self._render(text))) tmp.flush() self.fchown_tmpfile(tmp.fileno()) process = subprocess.Popen(self.get_command() + [tmp.name], preexec_fn=self.get_preexec_fn(), env=self.get_environ()) status = process.wait() self._close_temp_file(tmp) if status != 0: raise OSError( _("Command %(cmd)s exited with status %(status)d") % { 'cmd': str(process.args), 'status': status }) if not self.wait: # We are a child; exit sys.exit(0)
def _find_tmpdir(self): if not self._user_pw: return None tmpdir = tempfile.gettempdir() flags = os.R_OK | os.W_OK | os.X_OK os.setreuid(self._user_pw.pw_uid, 0) try: # check the default directory from $TMPDIR variable if os.access(tmpdir, flags): return tmpdir checked_tmpdirs = [tmpdir] # replace pam_tmpdir's directory /tmp/user/0 into e.g. /tmp/user/1000 if tmpdir.endswith("/0"): tmpdir = tmpdir[0:-1] + str(self._user_pw.pw_uid) if os.access(tmpdir, flags): return tmpdir checked_tmpdirs.append(tmpdir) # finally try hard-coded location if tmpdir != "/tmp": tmpdir = "/tmp" if os.access(tmpdir, flags): return tmpdir checked_tmpdirs.append(tmpdir) raise RuntimeError( _("None of the following directories is accessible" " by user %(user)s: %(dirs)s") % { 'user': self._user_pw.pw_name, 'dirs': str(checked_tmpdirs) }) finally: os.setuid(0)
def read(self): fd = self._open_apt_fd() if self._config.debug: ALCLog.debug(_("APT pipeline messages:")) self._read_version(fd) self._read_options(fd) debs = self._read_packages(fd) if self._config.debug: ALCLog.debug(_("Packages list:")) for d in debs: ALCLog.debug("\t%s" % d) ALCLog.debug("") return debs
def can_send_emails(config, replacementFrontend=None): if not os.path.exists("/usr/sbin/sendmail"): if replacementFrontend: ALCLog.error( _("The mail frontend needs an installed 'sendmail', using %s") % replacementFrontend) return False if not config.email_address: if replacementFrontend: ALCLog.error( _("The mail frontend needs an e-mail address to be configured, using %s" ) % replacementFrontend) return False return True
def update_progress(self, diff=1): if self.config.quiet > 1: return if not hasattr(self, 'message_printed'): self.message_printed = 1 ALCLog.info(_("Reading changelogs") + "...")
def _select_frontend(config, frontends): ''' Utility function used for testing purposes ''' prompt = "\n" + _("Available apt-listchanges frontends:") + "\n" + \ "".join([" %d. %s\n"%(i+1,frontends[i]) for i in range(0, len(frontends))]) + \ _("Choose a frontend by entering its number: ") for i in (1, 2, 3): try: response = ttyconfirm(config).ttyask(prompt) if not response: break return frontends[int(response) - 1] except Exception as ex: ALCLog.error(_("Error: %s") % str(ex)) ALCLog.info(_("Using default frontend: %s") % config.frontend) return config.frontend
def readdeb(self, deb): try: command = ['dpkg-deb', '-f', deb] + ControlStanza.fields_to_read output = subprocess.check_output(command) self.stanzas.append(ControlStanza(output.decode('utf-8', 'replace'))) except Exception as ex: raise RuntimeError(_("Error processing '%(what)s': %(errmsg)s") % {'what': file, 'errmsg': str(ex)}) from ex
def _read_version(self, fd): version = fd.readline().rstrip() if version != "VERSION 2": raise AptPipelineError( _("Wrong or missing VERSION from apt pipeline\n" "(is Dpkg::Tools::Options::/usr/bin/apt-listchanges::Version set to 2?)" )) if self._config.debug: ALCLog.debug("\t%s" % version)
def __init__(self, *args): super().__init__(*args) self._user_pw = self._find_user_pw() self._tmpdir = self._find_tmpdir() if self.config.debug and self._user_pw: ALCLog.debug( _("Found user: %(user)s, temporary directory: %(dir)s") % { 'user': self._user_pw.pw_name, 'dir': self._tmpdir })
def update_progress(self, diff=1): if not diff: return if not hasattr(self, 'progress'): # First call self.progress = 0 self.line_length = 0 self.progress += diff line = _("Reading changelogs") + "... %d%%" % (self.progress * 100 / self.packages_count) self.line_length = len(line) sys.stdout.write(line + '\r') sys.stdout.flush()
def extract_changes_via_apt(self, since_version, reverse): '''Run apt-get changelog and parse the downloaded changelog. Retrieve changelog using the "apt-get changelog" command, and parse it. If since_version is specified, only return entries later than the specified version. Returns a single sequence of Changes objects or None on downloading or parsing failure.''' # Retrieve changelog file and save it in a temporary directory tempdir = tempfile.mkdtemp(prefix='apt-listchanges') changelog_file = os.path.join(tempdir, self.binary + '.changelog') changelog_fd = open(changelog_file, 'w') try: command = ['apt-get', '-qq', 'changelog', '%s=%s' % (self.binary, self.Version)] ALCLog.debug(_("Calling %(cmd)s to retrieve changelog") % {'cmd': str(command)}) subprocess.run(command, stdout=changelog_fd, stderr=subprocess.PIPE, timeout=120, check=True) except subprocess.CalledProcessError as ex: ALCLog.error(_('Unable to retrieve changelog for package %(pkg)s; ' + "'apt-get changelog' failed with: %(errmsg)s") % {'pkg': self.binary, 'errmsg': ex.stderr.decode('utf-8', 'replace') if ex.stderr else str(ex)}) except Exception as ex: ALCLog.error(_('Unable to retrieve changelog for package %(pkg)s; ' + "could not run 'apt-get changelog': %(errmsg)s") % {'pkg': self.binary, 'errmsg': str(ex)}) else: return self._read_changelog(changelog_file, since_version, reverse) finally: changelog_fd.close() shutil.rmtree(tempdir, 1) return None
def _setup_less_variable(self): prompt = "-P?e(%s)$" % _("press q to quit") less = os.environ.get('LESS', '') if not less: os.environ['LESS'] = prompt return # When the environment contains the LESS variable, try to disable # flags like -E (--QUIT-AT-EOF) and -F (--quit-if-one-screen) that # might cause to less program to quit before even user is able to # see the generated file. if 'E' in less or '--QUIT-A' in less: less += ' -+E' if 'F' in less or '--quit-i' in less: less += ' -+F' os.environ['LESS'] = less + ' ' + prompt
def _open_changelog_file(self, filename): filenames = glob.glob(filename) for filename in filenames: try: if os.path.isdir(filename): ALCLog.error(_("Ignoring `%s' (seems to be a directory!)") % filename) elif filename.endswith('.gz'): return gzip.GzipFile(filename) else: return open(filename, 'rb') break except IOError as e: if e.errno != errno.ENOENT and e.errno != errno.ELOOP: raise return None
def _open_apt_fd(self): if not 'APT_HOOK_INFO_FD' in os.environ: raise AptPipelineError( _("APT_HOOK_INFO_FD environment variable is not defined\n" "(is Dpkg::Tools::Options::/usr/bin/apt-listchanges::InfoFD set to 20?)" )) try: apt_hook_info_fd_val = int(os.environ['APT_HOOK_INFO_FD']) except Exception as ex: raise AptPipelineError( _("Invalid (non-numeric) value of APT_HOOK_INFO_FD" " environment variable")) from ex if self._config.debug: ALCLog.debug( _("Will read apt pipeline messages from file descriptor %d") % apt_hook_info_fd_val) if apt_hook_info_fd_val == 0: # TODO: remove this backward compatibility code in Debian 10 (Buster) ALCLog.warning( _("Incorrect value (0) of APT_HOOK_INFO_FD environment variable.\n" "If the warning persists after restart of the package manager (e.g. aptitude),\n" "please check if the /etc/apt/apt.conf.d/20listchanges file was properly updated." )) elif apt_hook_info_fd_val < 3: raise AptPipelineError( _("APT_HOOK_INFO_FD environment variable is incorrectly defined\n" "(Dpkg::Tools::Options::/usr/bin/apt-listchanges::InfoFD should be greater than 2)." )) try: return os.fdopen(apt_hook_info_fd_val, 'rt') except Exception as ex: raise AptPipelineError( _("Cannot read from file descriptor %(fd)d: %(errmsg)s") % { 'fd': apt_hook_info_fd_val, 'errmsg': str(ex) }) from ex
def error(msg): print(_("apt-listchanges: %(msg)s") % {'msg': msg}, file=sys.stderr)
def dump(self): raise DbError( _("Path to the seen database is unknown.\n" "Please either specify it with --save-seen option\n" "or pass --profile=apt to have it read from the configuration file." ))
def make_frontend(config, packages_count): frontends = { 'text': text_frd, 'pager': pager_frd, 'debconf': debconf_frd, 'mail': mail_frd, 'browser': browser_frd, 'xterm-pager': xterm_pager_frd, 'xterm-browser': xterm_browser_frd, 'gtk': None, # handled below 'none': None } if config.select_frontend: # For testing purposes name = _select_frontend(config, sorted(list(frontends.keys()))) else: name = config.frontend if name == 'none': return None # If user does not want any messages force either the mail frontend # or no frontend at all if mail is not usable if config.quiet >= 2: if can_send_emails(config): name = 'mail' else: return None # If apt is in quiet (loggable) mode, we should make our output # loggable too unless the mail frontend is used (see #788059) elif config.quiet == 1: if name != 'mail' or not can_send_emails(config, 'text'): name = 'text' # Non-quiet mode else: if name == "mail" and not can_send_emails(config, 'pager'): name = 'pager' if name in ('gtk', 'xterm-pager', 'xterm-browser') and "DISPLAY" not in os.environ: name = name[6:] if name.startswith('xterm-') else 'pager' ALCLog.error( _("$DISPLAY is not set, falling back to %(frontend)s") % {'frontend': name}) # TODO: it would probably be nice to have a frontends subdir and # import from that. that would mean a uniform mechanism for all # frontends (that would become small files inside if name == "gtk": try: gtk = __import__("AptListChangesGtk") frontends[name] = gtk.gtk_frd except ImportError as ex: if config.apt_mode and config.frontend_from_env: # Most probably apt-listchanges was called from tool like synaptic # Force text frontend without confirmations asthe the tool might # appear to hang otherwise. name = 'text' config.confirm = False else: name = 'pager' ALCLog.error( _("The gtk frontend needs a working python3-gi,\n" "but it cannot be loaded. Falling back to %(frontend)s.\n" "The error is: %(errmsg)s") % { 'frontend': name, 'errmsg': ex }) config.frontend = name if name not in frontends: raise EUnknownFrontend return frontends[name](config, packages_count)
def preexec(): try: os.setgid(self._user_pw.pw_gid) os.setuid(self._user_pw.pw_uid) except Exception as ex: ALCLog.error(_("Error: %s") % str(ex))
def confirm(self): response = self.ttyask('apt-listchanges: ' + _('Do you want to continue? [Y/n] ')) return response == '' or re.search(locale.nl_langinfo(locale.YESEXPR), response)
def warning(msg): print(_("apt-listchanges warning: %(msg)s") % {'msg': msg}, file=sys.stderr)
def progress_done(self): if hasattr(self, 'line_length'): sys.stdout.write(' ' * self.line_length + '\r') sys.stdout.write( _("Reading changelogs") + "... " + _("Done") + "\n") sys.stdout.flush()
def info(msg): print(_("apt-listchanges: %(msg)s") % {'msg': msg}, file=sys.stdout)
def readfile(self, file): try: self.stanzas += [ControlStanza(x) for x in open(file, 'r', encoding='utf-8', errors='replace').read().split('\n\n') if x] except Exception as ex: raise RuntimeError(_("Error processing '%(what)s': %(errmsg)s") % {'what': file, 'errmsg': str(ex)}) from ex