Manage updates for bugzilla and CVE issues (fate #305503).
authorJán Kupec <jkupec@suse.cz>
Wed, 15 Jul 2009 06:30:49 +0000 (08:30 +0200)
committerJán Kupec <jkupec@suse.cz>
Wed, 15 Jul 2009 06:30:49 +0000 (08:30 +0200)
- list-updates: --bugzilla, --cve, --issues, --all options added
- patch: --bugzilla, --cve options added

src/Zypper.cc
src/update.cc
src/update.h

index 37736cd..9f2ee60 100644 (file)
@@ -1566,6 +1566,9 @@ void Zypper::processCommandOptions()
       {"no-recommends",             no_argument,       0,  0 },
       {"recommends",                no_argument,       0,  0 },
       {"dry-run",                   no_argument,       0, 'D'},
+      {"bz",                        required_argument, 0, 'b'},
+      {"bugzilla",                  required_argument, 0, 'b'},
+      {"cve",                       required_argument, 0,  0 },
       {"help", no_argument, 0, 'h'},
       {0, 0, 0, 0}
     };
@@ -1577,16 +1580,18 @@ void Zypper::processCommandOptions()
       "\n"
       "  Command options:\n"
       "\n"
-      "-r, --repo <alias|#|URI>    Load only the specified repository.\n"
       "    --skip-interactive      Skip interactive patches.\n"
       "-l, --auto-agree-with-licenses\n"
       "                            Automatically say 'yes' to third party license\n"
       "                            confirmation prompt.\n"
       "                            See man zypper for more details.\n"
+      "-b, --bugzilla #            Install patch fixing the specified bugzilla issue.\n"
+      "    --cve #                 Install patch fixing the specified CVE issue.\n"
       "    --debug-solver          Create solver test case for debugging.\n"
       "    --no-recommends         Do not install recommended packages, only required.\n"
       "    --recommends            Install also recommended packages in addition\n"
       "                            to the required.\n"
+      "-r, --repo <alias|#|URI>    Load only the specified repository.\n"
       "-D, --dry-run               Test the update, do not actually update.\n"
     );
     break;
@@ -1596,6 +1601,11 @@ void Zypper::processCommandOptions()
   {
     static struct option list_updates_options[] = {
       {"repo",        required_argument, 0, 'r'},
+      {"bz",          optional_argument, 0, 'b'},
+      {"bugzilla",    optional_argument, 0, 'b'},
+      {"cve",         optional_argument, 0,  0 },
+      {"issues",      optional_argument, 0,  0 },
+      {"all",         no_argument,       0, 'a'},
       {"help", no_argument, 0, 'h'},
       {0, 0, 0, 0}
     };
@@ -1606,7 +1616,11 @@ void Zypper::processCommandOptions()
       "List all available needed patches.\n"
       "\n"
       "  Command options:\n"
-      "-r, --repo <alias|#|URI>        List only patches from the specified repository.\n"
+      "-b, --bugzilla[=#]         List needed patches for Bugzilla issues.\n"
+      "    --cve[=#]              List needed patches for CVE issues.\n"
+      "    --issues[=string]      Look for issues matching the specified string.\n"
+      "-a, --all                  List all patches, not only the needed ones.\n"
+      "-r, --repo <alias|#|URI>   List only patches from the specified repository.\n"
     );
     break;
   }
@@ -3591,6 +3605,8 @@ void Zypper::doCommand()
     else
       kinds.insert(ResKind::package);
 
+    //! \todo drop this option - it's the default for packages now, irrelevant
+    //! for patches; just test with products and patterns
     bool best_effort = copts.count( "best-effort" );
 
     if (globalOpts().is_rug_compatible && best_effort)
@@ -3602,6 +3618,15 @@ void Zypper::doCommand()
         Out::HIGH);
     }
 
+    if ((copts.count("bugzilla") || copts.count("bz") || copts.count("cve")) &&
+        copts.count("issues"))
+    {
+      out().error(str::form(
+        _("Cannot use %s together with %s."), "--issues", "--bz, --cve"));
+      setExitCode(ZYPPER_EXIT_ERR_INVALID_ARGS);
+      return;
+    }
+
     initRepoManager();
     init_target(*this);
     init_repos(*this);
@@ -3610,7 +3635,11 @@ void Zypper::doCommand()
     load_resolvables(*this);
     resolve(*this);
 
-    list_updates(*this, kinds, best_effort);
+    if (copts.count("bugzilla") || copts.count("bz")
+        || copts.count("cve") || copts.count("issues"))
+      list_patches_by_issue(*this);
+    else
+      list_updates(*this, kinds, best_effort);
 
     break;
   }
@@ -3698,7 +3727,10 @@ void Zypper::doCommand()
     bool skip_interactive =
       copts.count("skip-interactive") || globalOpts().non_interactive;
 
-    mark_updates(*this, kinds, skip_interactive, best_effort);
+    if (copts.count("bugzilla") || copts.count("bz") || copts.count("cve"))
+      mark_updates_by_issue(*this);
+    else
+      mark_updates(*this, kinds, skip_interactive, best_effort);
 
     solve_and_commit(*this);
 
index 4b95f8c..91925d8 100755 (executable)
@@ -11,6 +11,7 @@
 
 #include "Table.h"
 #include "update.h"
+#include "main.h"
 
 using namespace std;
 using namespace zypp;
@@ -181,6 +182,8 @@ static void xml_list_updates(const ResKindSet & kinds)
 
 static bool list_patch_updates(Zypper & zypper)
 {
+  bool all = zypper.cOpts().count("all");
+
   Table tbl;
   Table pm_tbl; // only those that affect packagemanager (restartSuggested()), they have priority
   TableHeader th;
@@ -200,7 +203,7 @@ static bool list_patch_updates(Zypper & zypper)
   {
     ResObject::constPtr res = it->resolvable();
 
-    if ( it->isRelevant() && ! it->isSatisfied() )
+    if (all || it->isBroken())
     {
       Patch::constPtr patch = asKind<Patch>(res);
 
@@ -209,9 +212,9 @@ static bool list_patch_updates(Zypper & zypper)
         tr << patch->repoInfo().name();
         tr << res->name () << res->edition ().asString();
         tr << patch->category();
-        tr <<  _("Needed");
+        tr << (it->isBroken() ? _("needed") : _("not needed"));
 
-        if (patch->restartSuggested ())
+        if (!all && patch->restartSuggested ())
           pm_tbl << tr;
         else
           tbl << tr;
@@ -646,6 +649,8 @@ mark_patch_update(ui::Selectable & s,
                   bool skip_interactive, bool ignore_affects_pm)
 {
   Patch::constPtr patch = asKind<Patch>(s.candidateObj());
+  XXX << "candidate patch " << patch->name() << " " << ignore_affects_pm << ", "
+        << patch->restartSuggested() << endl;
   if (s.isBroken()) // bnc #506860
   {
     DBG << "candidate patch " << patch->name() << " " << ignore_affects_pm << ", "
@@ -825,3 +830,259 @@ void mark_updates(Zypper & zypper, const ResKindSet & kinds, bool skip_interacti
     }
   }
 }
+
+// ----------------------------------------------------------------------------
+
+void list_patches_by_issue(Zypper & zypper)
+{
+  // lu --issues               - list all issues which need to be fixed
+  // lu --issues=foo           - look for foo in issue # or description
+  // lu --bz                   - list all bugzilla issues
+  // lu --cve                  - list all CVE issues
+  // lu --bz=foo --cve=foo     - look for foo in bugzillas or CVEs
+  // --all                     - list all, not only needed patches
+
+  // --bz, --cve can't be used together with --issue; this case is ruled out
+  // in the initial arguments validation in Zypper.cc
+
+  typedef set<pair<string, string> > Issues;
+  bool only_needed = !zypper.cOpts().count("all");
+  bool specific = false; // whether specific issue numbers were given
+
+  Table t;
+  TableHeader th;
+  th << _("Issue") << _("No.") << _("Patch") << _("Category");
+  t << th;
+
+#define FILL_ISSUES(TYPE, ID) \
+  it = zypper.cOpts().find(TYPE); \
+  if (it != zypper.cOpts().end()) \
+    for_(i, it->second.begin(), it->second.end()) \
+    { \
+      issues.insert(pair<string, string>(ID, *i)); \
+      if (!i->empty()) \
+        specific = true; \
+    } \
+
+  // make a set of unique issues
+  Issues issues;
+  parsed_opts::const_iterator it;
+  FILL_ISSUES("bugzilla", "bugzilla");
+  FILL_ISSUES("bz", "bugzilla");
+  FILL_ISSUES("cve", "cve");
+  FILL_ISSUES("issues", "issues");
+
+  // remove those without arguments if there are any with arguments
+  // (will show only the specified ones)
+  if (specific)
+    for (Issues::const_iterator i = issues.begin(); i != issues.end(); )
+    {
+      if (i->second.empty())
+      {
+        zypper.out().warning(str::form(_(
+            "Ignoring %s without argument because similar option with"
+            " an argument has been specified."),
+            ("--" + i->first).c_str()));
+        issues.erase(i++);
+      }
+      else
+        ++i;
+    }
+
+  // construct a PoolQuery for each argument separately and add the results
+  // to the Table.
+  string issuesstr;
+  for_(issue, issues.begin(), issues.end())
+  {
+    PoolQuery q;
+    q.setMatchSubstring();
+    q.setCaseSensitive(false);
+    q.addKind(ResKind::patch); // is this unnecessary? only patches should have updateReference* attributes
+
+    // specific bugzilla or CVE
+    if (specific)
+    {
+      if (issue->first == "bugzilla" || issue->first == "cve")
+        q.addAttribute(sat::SolvAttr::updateReferenceId, issue->second);
+    }
+    // all bugzillas or CVEs
+    else
+    {
+      if (issue->first == "bugzilla")
+        q.addAttribute(sat::SolvAttr::updateReferenceType, "bugzilla");
+      else if (issue->first == "cve")
+        q.addAttribute(sat::SolvAttr::updateReferenceType, "cve");
+    }
+    // look for substring in description and reference ID (issue number)
+    if (issue->first == "issues")
+    {
+      // whether argument was given or not, it does not matter; without
+      // argument, all issues will be found
+      q.addAttribute(sat::SolvAttr::updateReferenceId, issue->second);
+      issuesstr = issue->second;
+    }
+
+    cout << "*****" << endl << q << endl << "*****" << endl;
+
+    for_(it, q.begin(), q.end())
+    {
+      PoolItem pi(*it);
+      if (only_needed && !pi.status().isBroken())
+        continue;
+      Patch::constPtr patch = asKind<Patch>(pi.resolvable());
+
+      // Print details about each match in that solvable:
+      for_( d, it.matchesBegin(), it.matchesEnd() )
+      {
+        string itype =
+          d->subFind(sat::SolvAttr::updateReferenceType).asString();
+        if (itype != issue->first)
+          continue;
+
+        TableRow tr;
+        tr << itype;
+        tr << d->subFind(sat::SolvAttr::updateReferenceId).asString();
+        tr << (patch->name() + "-" + patch->edition().asString());
+        tr << patch->category();
+        t << tr;
+      }
+    }
+  }
+
+  // look for matches in patch descriptions
+  Table t1;
+  TableHeader th1;
+  th1 << _("Name") << _("Version") << _("Category") << _("Summary");
+  t1 << th1;
+  if (!issuesstr.empty())
+  {
+    PoolQuery q;
+    q.setMatchSubstring();
+    q.setCaseSensitive(false);
+    q.addKind(ResKind::patch);
+    q.addAttribute(sat::SolvAttr::summary, issuesstr);
+    q.addAttribute(sat::SolvAttr::description, issuesstr);
+
+    for_(it, q.begin(), q.end())
+    {
+      PoolItem pi(*it);
+      if (only_needed && !pi.status().isBroken())
+        continue;
+      Patch::constPtr patch = asKind<Patch>(pi.resolvable());
+
+      TableRow tr;
+      tr << patch->name() << patch->edition().asString() << patch->category();
+      //! \todo could show a highlighted match with a portion of surrounding
+      //! text. Needs case-insensitive find.
+      tr << patch->summary();
+      t1 << tr;
+    }
+  }
+
+  if (!zypper.globalOpts().no_abbrev)
+    t1.allowAbbrev(3);
+  t.sort(3);
+  t1.sort(0);
+
+  if (t.empty() && t1.empty())
+    zypper.out().info(_("No matching issues found."));
+  else
+  {
+    if (!t.empty())
+    {
+      if (!issuesstr.empty())
+      {
+        cout << endl;
+        zypper.out().info(_(
+            "The following matches in issue numbers have been found:"));
+      }
+
+      cout << endl << t;
+    }
+
+    if (!t1.empty())
+    {
+      if (!t.empty())
+        cout << endl;
+      zypper.out().info(_(
+          "Matches in patch descriptions of the following patches have been"
+          " found:"));
+      cout << endl << t1;
+    }
+  }
+}
+
+// ----------------------------------------------------------------------------
+
+void mark_updates_by_issue(Zypper & zypper)
+{
+  typedef set<pair<string, string> > Issues;
+  Issues issues;
+  parsed_opts::const_iterator it = zypper.cOpts().find("bugzilla");
+  if (it != zypper.cOpts().end())
+    for_(i, it->second.begin(), it->second.end())
+      issues.insert(pair<string, string>("b", *i));
+  it = zypper.cOpts().find("bz");
+  if (it != zypper.cOpts().end())
+    for_(i, it->second.begin(), it->second.end())
+      issues.insert(pair<string, string>("b", *i));
+  it = zypper.cOpts().find("cve");
+  if (it != zypper.cOpts().end())
+    for_(i, it->second.begin(), it->second.end())
+      issues.insert(pair<string, string>("c", *i));
+
+  for_(issue, issues.begin(), issues.end())
+  {
+    PoolQuery q;
+    q.setMatchExact();
+    q.setCaseSensitive(false);
+    q.addKind(ResKind::patch); // is this unnecessary?
+    if (issue->first == "b")
+      q.addAttribute(sat::SolvAttr::updateReferenceId, issue->second);
+    else if (issue->first == "c")
+      q.addAttribute(sat::SolvAttr::updateReferenceId, issue->second);
+
+    bool found = false;
+    for_(sit, q.begin(), q.end())
+    {
+      if (!PoolItem(*sit).status().isBroken()) // not needed
+        continue;
+
+      for_( d, sit.matchesBegin(), sit.matchesEnd() )
+      {
+        if (issue->first == "b" &&
+            d->subFind(sat::SolvAttr::updateReferenceType).asString() == "bugzilla")
+        {
+          if (mark_patch_update(*God->pool().proxy().lookup(*sit),
+                zypper.cOpts().count("skip-interactive"), true))
+            found = true;
+          else
+            DBG << str::form("fix for bugzilla issue number %s was not marked.",
+                issue->second.c_str());
+        }
+        else if (issue->first == "b" &&
+            d->subFind(sat::SolvAttr::updateReferenceType).asString() == "bugzilla")
+        {
+          if (mark_patch_update(*God->pool().proxy().lookup(*sit),
+                zypper.cOpts().count("skip-interactive"), true))
+            found = true;
+          else
+            DBG << str::form("fix for CVE issue number %s was not marked.",
+                issue->second.c_str());
+        }
+      }
+    }
+    if (!found)
+    {
+      if (issue->first == "b")
+        zypper.out().info(str::form(_(
+            "Fix for bugzilla issue number %s was not found or is not needed."),
+            issue->second.c_str()));
+      else if (issue->first == "c")
+        zypper.out().info(str::form(_(
+            "Fix for CVE issue number %s was not found or is not needed."),
+            issue->second.c_str()));
+      zypper.setExitCode(ZYPPER_EXIT_INF_CAP_NOT_FOUND);
+    }
+  } // next issue from --bz --cve
+}
index a7c96c9..2f4c8b1 100755 (executable)
@@ -23,9 +23,19 @@ void list_updates(Zypper & zypper,
                   bool best_effort);
 
 /**
- * \param kind  resolvable type
- * \param skip_interactive whether to skip updates that need user interaction
+ * List available fixes to all issues or issues specified in --bugzilla
+ * or --cve options, or look for --issues[=str[ in numbers and descriptions
+ */
+void list_patches_by_issue(Zypper & zypper);
+
+/**
+ * Mark patches for installation or package for update.
+ *
+ * \param kinds  set of resolvable types
+ * \param skip_interactive
+ *               whether to skip updates that need user interaction
  * \param best_effort
+ *               only require the resolvable name, let the solver choose
  */
 void mark_updates(Zypper & zypper,
                   const ResKindSet & kinds,
@@ -33,6 +43,12 @@ void mark_updates(Zypper & zypper,
                   bool best_effort);
 
 /**
+ * Mark patches for installation according to bugzilla or CVE number specified
+ * in --cve or --bugzilla or --bz.
+ */
+void mark_updates_by_issue(Zypper & zypper);
+
+/**
  * Find best (according to edition) uninstalled item
  * with same kind/name/arch as \a item.
  *