Пример #1
0
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
            })
Пример #2
0
    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 = {}
Пример #4
0
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)
Пример #5
0
    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)
Пример #6
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)
Пример #7
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
Пример #8
0
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
Пример #9
0
    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") + "...")
Пример #10
0
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
Пример #12
0
 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)
Пример #13
0
 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
             })
Пример #14
0
 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
Пример #16
0
    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
Пример #18
0
    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
Пример #19
0
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."
           ))
Пример #21
0
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)
Пример #22
0
 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))
Пример #23
0
 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)
Пример #24
0
def warning(msg):
    print(_("apt-listchanges warning: %(msg)s") % {'msg': msg},
          file=sys.stderr)
Пример #25
0
 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()
Пример #26
0
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