Enable BytesWarnings.
[portage.git] / bin / glsa-check
old mode 100644 (file)
new mode 100755 (executable)
index b7b9b47..87eafca
@@ -1,80 +1,79 @@
-#!/usr/bin/python
+#!/usr/bin/python -bb
+# Copyright 2008-2014 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
 
-# $Header: $
-# This program is licensed under the GPL, version 2
+from __future__ import print_function
 
-import os
 import sys
+import codecs
 
-try:
-       import portage
-except ImportError:
-       from os import path as osp
-       sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
-       import portage
-
-from portage.output import *
-
-from optparse import OptionGroup, OptionParser
+from os import path as osp
+pym_path = osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")
+sys.path.insert(0, pym_path)
+import portage
+portage._internal_caller = True
+from portage import os
+from portage.output import green, red, nocolor, white
+from portage.util._argparse import ArgumentParser
 
 __program__ = "glsa-check"
 __author__ = "Marius Mauch <genone@gentoo.org>"
 __version__ = "1.0"
 
-def cb_version(*args, **kwargs):
-       """Callback for --version"""
-       sys.stderr.write("\n"+ __program__ + ", version " + __version__ + "\n")
-       sys.stderr.write("Author: " + __author__ + "\n")
-       sys.stderr.write("This program is licensed under the GPL, version 2\n\n")
-       sys.exit(0)
-
 # option parsing
-parser = OptionParser(usage="%prog <option> [glsa-list]",
-               version="%prog "+ __version__)
-parser.epilog = "glsa-list can contain an arbitrary number of GLSA ids," \
+epilog = "glsa-list can contain an arbitrary number of GLSA ids," \
                " filenames containing GLSAs or the special identifiers" \
                " 'all', 'new' and 'affected'"
+parser = ArgumentParser(usage=__program__ + " <option> [glsa-list]",
+       epilog=epilog)
 
-modes = OptionGroup(parser, "Modes")
-modes.add_option("-l", "--list", action="store_const",
+modes = parser.add_argument_group("Modes")
+modes.add_argument("-l", "--list", action="store_const",
                const="list", dest="mode",
                help="List all unapplied GLSA")
-modes.add_option("-d", "--dump", action="store_const",
+modes.add_argument("-d", "--dump", action="store_const",
                const="dump", dest="mode",
                help="Show all information about the given GLSA")
-modes.add_option("", "--print", action="store_const",
+modes.add_argument("--print", action="store_const",
                const="dump", dest="mode",
                help="Alias for --dump")
-modes.add_option("-t", "--test", action="store_const",
+modes.add_argument("-t", "--test", action="store_const",
                const="test", dest="mode",
                help="Test if this system is affected by the given GLSA")
-modes.add_option("-p", "--pretend", action="store_const",
+modes.add_argument("-p", "--pretend", action="store_const",
                const="pretend", dest="mode",
                help="Show the necessary commands to apply this GLSA")
-modes.add_option("-f", "--fix", action="store_const",
+modes.add_argument("-f", "--fix", action="store_const",
                const="fix", dest="mode",
                help="Try to auto-apply this GLSA (experimental)")
-modes.add_option("-i", "--inject", action="store_const", dest="mode",
-               help="Inject the given GLSA into the checkfile")
-modes.add_option("-m", "--mail", action="store_const",
+modes.add_argument("-i", "--inject", action="store_const",
+               const="inject", dest="mode",
+               help="inject the given GLSA into the glsa_injected file")
+modes.add_argument("-m", "--mail", action="store_const",
                const="mail", dest="mode",
                help="Send a mail with the given GLSAs to the administrator")
-parser.add_option_group(modes)
 
-parser.remove_option("--version")
-parser.add_option("-V", "--version", action="callback",
-               callback=cb_version, help="Some information about this tool")
-parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+parser.add_argument("-V", "--version", action="store_true",
+               help="Some information about this tool")
+parser.add_argument("-v", "--verbose", action="store_true", dest="verbose",
                help="Print more information")
-parser.add_option("-n", "--nocolor", action="callback",
-               callback=lambda *args, **kwargs: nocolor(),
+parser.add_argument("-n", "--nocolor", action="store_true",
                help="Disable colors")
-parser.add_option("-e", "--emergelike", action="store_false", dest="least_change",
+parser.add_argument("-e", "--emergelike", action="store_false", dest="least_change",
                help="Do not use a least-change algorithm")
-parser.add_option("-c", "--cve", action="store_true", dest="list_cve",
+parser.add_argument("-c", "--cve", action="store_true", dest="list_cve",
                help="Show CAN ids in listing mode")
 
-options, params = parser.parse_args()
+options, params = parser.parse_known_args()
+
+if options.nocolor:
+       nocolor()
+
+if options.version:
+       sys.stderr.write("\n"+ __program__ + ", version " + __version__ + "\n")
+       sys.stderr.write("Author: " + __author__ + "\n")
+       sys.stderr.write("This program is licensed under the GPL, version 2\n\n")
+       sys.exit(0)
 
 mode = options.mode
 least_change = options.least_change
@@ -93,17 +92,19 @@ elif mode != "list" and not params:
        parser.print_help()
        sys.exit(1)
 elif mode in ["fix", "inject"] and os.geteuid() != 0:
-       # we need root priviledges for write access
+       # we need root privileges for write access
        sys.stderr.write("\nThis tool needs root access to "+options.mode+" this GLSA\n\n")
        sys.exit(2)
 elif mode == "list" and not params:
        params.append("new")
 
 # delay this for speed increase
-from portage.glsa import *
+from portage.glsa import (Glsa, GlsaTypeException, GlsaFormatException,
+       get_applied_glsas, get_glsa_list)
 
-vardb = portage.db[portage.settings["ROOT"]]["vartree"].dbapi
-portdb = portage.db["/"]["porttree"].dbapi
+eroot = portage.settings['EROOT']
+vardb = portage.db[eroot]["vartree"].dbapi
+portdb = portage.db[eroot]["porttree"].dbapi
 
 # build glsa lists
 completelist = get_glsa_list(portage.settings)
@@ -115,7 +116,7 @@ glsalist = []
 if "new" in params:
        glsalist = todolist
        params.remove("new")
-       
+
 if "all" in params:
        glsalist = completelist
        params.remove("all")
@@ -124,7 +125,7 @@ if "affected" in params:
        for x in todolist:
                try:
                        myglsa = Glsa(x, portage.settings, vardb, portdb)
-               except (GlsaTypeException, GlsaFormatException), e:
+               except (GlsaTypeException, GlsaFormatException) as e:
                        if verbose:
                                sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (x, e)))
                        continue
@@ -140,8 +141,17 @@ for p in params[:]:
 
 glsalist.extend([g for g in params if g not in glsalist])
 
-def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr):
-       fd2.write(white("[A]")+" means this GLSA was already applied,\n")
+def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr, encoding="utf-8"):
+       # Get to the raw streams in py3k before wrapping them with an encoded writer
+       # to avoid writing bytes to a text stream (stdout/stderr are text streams
+       # by default in py3k)
+       if hasattr(fd1, "buffer"):
+               fd1 = fd1.buffer
+       if hasattr(fd2, "buffer"):
+               fd2 = fd2.buffer
+       fd1 = codecs.getwriter(encoding)(fd1)
+       fd2 = codecs.getwriter(encoding)(fd2)
+       fd2.write(white("[A]")+" means this GLSA was marked as applied (injected),\n")
        fd2.write(green("[U]")+" means the system is not affected and\n")
        fd2.write(red("[N]")+" indicates that the system might be affected.\n\n")
 
@@ -149,11 +159,11 @@ def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr):
        for myid in myglsalist:
                try:
                        myglsa = Glsa(myid, portage.settings, vardb, portdb)
-               except (GlsaTypeException, GlsaFormatException), e:
+               except (GlsaTypeException, GlsaFormatException) as e:
                        if verbose:
                                fd2.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
                        continue
-               if myglsa.isApplied():
+               if myglsa.isInjected():
                        status = "[A]"
                        color = white
                elif myglsa.isVulnerable():
@@ -170,13 +180,13 @@ def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr):
 
                fd1.write(color(myglsa.nr) + " " + color(status) + " " + color(access) + myglsa.title + " (")
                if not verbose:
-                       for pkg in myglsa.packages.keys()[:3]:
+                       for pkg in list(myglsa.packages)[:3]:
                                fd1.write(" " + pkg + " ")
                        if len(myglsa.packages) > 3:
                                fd1.write("... ")
                else:
-                       for pkg in myglsa.packages.keys():
-                               mylist = vardb.match(portage.dep_getkey(pkg))
+                       for pkg in myglsa.packages:
+                               mylist = vardb.match(pkg)
                                if len(mylist) > 0:
                                        pkg = color(" ".join(mylist))
                                fd1.write(" " + pkg + " ")
@@ -184,7 +194,7 @@ def summarylist(myglsalist, fd1=sys.stdout, fd2=sys.stderr):
                fd1.write(")")
                if list_cve:
                        fd1.write(" "+(",".join([r[:13] for r in myglsa.references if r[:4] in ["CAN-", "CVE-"]])))
-               fd1.write("\n")         
+               fd1.write("\n")
        return 0
 
 if mode == "list":
@@ -195,46 +205,53 @@ if mode in ["dump", "fix", "inject", "pretend"]:
        for myid in glsalist:
                try:
                        myglsa = Glsa(myid, portage.settings, vardb, portdb)
-               except (GlsaTypeException, GlsaFormatException), e:
+               except (GlsaTypeException, GlsaFormatException) as e:
                        if verbose:
                                sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
                        continue
                if mode == "dump":
                        myglsa.dump()
                elif mode == "fix":
-                       sys.stdout.write("fixing "+myid+"\n")
-                       mergelist = myglsa.getMergeList(least_change=least_change)
-                       for pkg in mergelist:
-                               sys.stdout.write(">>> merging "+pkg+"\n")
-                               # using emerge for the actual merging as it contains the dependency
-                               # code and we want to be consistent in behaviour. Also this functionality
-                               # will be integrated in emerge later, so it shouldn't hurt much.
-                               emergecmd = "emerge --oneshot " + portage.settings["EMERGE_OPTS"] + " =" + pkg
-                               if verbose:
-                                       sys.stderr.write(emergecmd+"\n")
-                               exitcode = os.system(emergecmd)
-                               # system() returns the exitcode in the high byte of a 16bit integer
-                               if exitcode >= 1<<8:
-                                       exitcode >>= 8
-                               if exitcode:
-                                       sys.exit(exitcode)
-                       myglsa.inject()
+                       sys.stdout.write("Fixing GLSA "+myid+"\n")
+                       if not myglsa.isVulnerable():
+                               sys.stdout.write(">>> no vulnerable packages installed\n")
+                       else:
+                               mergelist = myglsa.getMergeList(least_change=least_change)
+                               if mergelist == []:
+                                       sys.stdout.write(">>> cannot fix GLSA, no unaffected packages available\n")
+                                       sys.exit(2)
+                               for pkg in mergelist:
+                                       sys.stdout.write(">>> merging "+pkg+"\n")
+                                       # using emerge for the actual merging as it contains the dependency
+                                       # code and we want to be consistent in behaviour. Also this functionality
+                                       # will be integrated in emerge later, so it shouldn't hurt much.
+                                       emergecmd = "emerge --oneshot " + " =" + pkg
+                                       if verbose:
+                                               sys.stderr.write(emergecmd+"\n")
+                                       exitcode = os.system(emergecmd)
+                                       # system() returns the exitcode in the high byte of a 16bit integer
+                                       if exitcode >= 1<<8:
+                                               exitcode >>= 8
+                                       if exitcode:
+                                               sys.exit(exitcode)
+                       if len(mergelist):
+                               sys.stdout.write("\n")
                elif mode == "pretend":
                        sys.stdout.write("Checking GLSA "+myid+"\n")
-                       mergelist = myglsa.getMergeList(least_change=least_change)
-                       if mergelist:
-                               sys.stdout.write("The following updates will be performed for this GLSA:\n")
-                               for pkg in mergelist:
-                                       oldver = None
-                                       for x in vardb.match(portage.dep_getkey(pkg)):
-                                               if vardb.aux_get(x, ["SLOT"]) == portdb.aux_get(pkg, ["SLOT"]):
-                                                       oldver = x
-                                       if oldver == None:
-                                               raise ValueError("could not find old version for package %s" % pkg)
-                                       oldver = oldver[len(portage.dep_getkey(oldver))+1:]
-                                       sys.stdout.write("     " + pkg + " (" + oldver + ")\n")
+                       if not myglsa.isVulnerable():
+                               sys.stdout.write(">>> no vulnerable packages installed\n")
                        else:
-                               sys.stdout.write("Nothing to do for this GLSA\n")
+                               mergedict = {}
+                               for (vuln, update) in myglsa.getAffectionTable(least_change=least_change):
+                                       mergedict.setdefault(update, []).append(vuln)
+
+                               sys.stdout.write(">>> The following updates will be performed for this GLSA:\n")
+                               for pkg in mergedict:
+                                       if pkg != "":
+                                               sys.stdout.write("     " + pkg + " (vulnerable: " + ", ".join(mergedict[pkg]) + ")\n")
+                               if "" in mergedict:
+                                       sys.stdout.write("\n>>> For the following packages, no upgrade path exists:\n")
+                                       sys.stdout.write("     " + ", ".join(mergedict[""]))
                elif mode == "inject":
                        sys.stdout.write("injecting " + myid + "\n")
                        myglsa.inject()
@@ -247,7 +264,7 @@ if mode == "test":
        for myid in glsalist:
                try:
                        myglsa = Glsa(myid, portage.settings, vardb, portdb)
-               except (GlsaTypeException, GlsaFormatException), e:
+               except (GlsaTypeException, GlsaFormatException) as e:
                        if verbose:
                                sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
                        continue
@@ -266,9 +283,9 @@ if mode == "test":
 # mail mode as requested by solar
 if mode == "mail":
        import portage.mail, socket
-       from StringIO import StringIO
+       from io import BytesIO
        from email.mime.text import MIMEText
-       
+
        # color doesn't make any sense for mail
        nocolor()
 
@@ -276,7 +293,7 @@ if mode == "mail":
                myrecipient = portage.settings["PORTAGE_ELOG_MAILURI"].split()[0]
        else:
                myrecipient = "root@localhost"
-       
+
        if "PORTAGE_ELOG_MAILFROM" in portage.settings:
                myfrom = portage.settings["PORTAGE_ELOG_MAILFROM"]
        else:
@@ -285,31 +302,34 @@ if mode == "mail":
        mysubject = "[glsa-check] Summary for %s" % socket.getfqdn()
 
        # need a file object for summarylist()
-       myfd = StringIO()
-       myfd.write("GLSA Summary report for host %s\n" % socket.getfqdn())
-       myfd.write("(Command was: %s)\n\n" % " ".join(sys.argv))
+       myfd = BytesIO()
+       line = "GLSA Summary report for host %s\n" % socket.getfqdn()
+       myfd.write(line.encode("utf-8"))
+       line = "(Command was: %s)\n\n" % " ".join(sys.argv)
+       myfd.write(line.encode("utf-8"))
        summarylist(glsalist, fd1=myfd, fd2=myfd)
-       summary = str(myfd.getvalue())
+       summary = myfd.getvalue().decode("utf-8")
        myfd.close()
 
        myattachments = []
        for myid in glsalist:
                try:
                        myglsa = Glsa(myid, portage.settings, vardb, portdb)
-               except (GlsaTypeException, GlsaFormatException), e:
+               except (GlsaTypeException, GlsaFormatException) as e:
                        if verbose:
                                sys.stderr.write(("invalid GLSA: %s (error message was: %s)\n" % (myid, e)))
                        continue
-               myfd = StringIO()
+               myfd = BytesIO()
                myglsa.dump(outstream=myfd)
-               myattachments.append(MIMEText(str(myfd.getvalue()), _charset="utf8"))
+               attachment = myfd.getvalue().decode("utf-8")
+               myattachments.append(MIMEText(attachment, _charset="utf8"))
                myfd.close()
-               
+
        mymessage = portage.mail.create_message(myfrom, myrecipient, mysubject, summary, myattachments)
        portage.mail.send_mail(portage.settings, mymessage)
-               
+
        sys.exit(0)
-       
+
 # something wrong here, all valid paths are covered with sys.exit()
 sys.stderr.write("nothing more to do\n")
 sys.exit(2)