tools: nfdc route add command

This commit also simplifies 'nfdc face destroy' and its test case.

'nfdc register' is deprecated in favor of 'nfdc route add'.

refs #3866

Change-Id: I1de5cc0bc956d57b0793da920c1e87b3580a3297
diff --git a/docs/manpages/nfdc-route.rst b/docs/manpages/nfdc-route.rst
index a7c7f2a..7b01c44 100644
--- a/docs/manpages/nfdc-route.rst
+++ b/docs/manpages/nfdc-route.rst
@@ -5,7 +5,8 @@
 --------
 | nfdc route [list]
 | nfdc fib [list]
-| nfdc register [-I] [-C] [-c <COST>] [-e <EXPIRATION>] [-o <ORIGIN>] <PREFIX> <FACEID|FACEURI>
+| nfdc route add [prefix] <PREFIX> [nexthop] <FACEID|FACEURI> [origin <ORIGIN>] [cost <COST>]
+|   [no-inherit] [capture] [expires <EXPIRATION-MILLIS>]
 | nfdc unregister [-o <ORIGIN>] <PREFIX> <FACEID>
 
 DESCRIPTION
@@ -21,43 +22,59 @@
 The **nfdc fib list** command shows the forwarding information base (FIB),
 which is calculated from RIB routes and used directly by NFD forwarding.
 
-The **nfdc register** command adds a new route.
+The **nfdc route add** command requests to add a route.
 If a route with the same prefix, nexthop, and origin already exists,
-it is updated with the specified cost, expiration, and route inheritance flags.
+it is updated with the specified cost, route inheritance flags, and expiration period.
+This command returns when the request has been accepted, but does not wait for RIB update completion.
 
 The **nfdc unregister** command removes a route with matching prefix, nexthop, and origin.
 
 OPTIONS
 -------
--I
-    Unset CHILD_INHERIT flag in the route.
+<PREFIX>
+    Name prefix of the route.
 
--C
-    Set CAPTURE flag in the route.
+<FACEID>
+    Numerical identifier of the face.
+    It is displayed in the output of **nfdc face list** and **nfdc face create** commands.
 
--c <COST>
+<FACEURI>
+    An URI representing the remote endpoint of a face.
+    It must uniquely match an existing face.
+
+<ORIGIN>
+    Origin of the route, i.e. who is announcing the route.
+    The default is 255, indicating a static route.
+
+<COST>
     The administrative cost of the route.
     The default is 0.
 
--e <EXPIRATION>
+no-inherit
+    Unset CHILD_INHERIT flag in the route.
+
+capture
+    Set CAPTURE flag in the route.
+
+<EXPIRATION-MILLIS>
     Expiration time of the route, in milliseconds.
     When the route expires, NFD removes it from the RIB.
     The default is infinite, which keeps the route active until the nexthop face is destroyed.
 
--o <ORIGIN>
-    Origin of the route, i.e. who is announcing the route.
-    The default is 255, indicating a static route.
+EXIT CODES
+----------
 
-<PREFIX>
-    Name prefix of the route.
+0: Success
 
-<FACEURI>
-    An URI representing the remote endpoint of a face.
-    It can be used in **nfdc register** to create a new UDP or TCP face.
+1: An unspecified error occurred
 
-<FACEID>
-    A numerical identifier of the face.
-    It is displayed in the output of **nfdc face list** and **nfdc face create** commands.
+2: Malformed command line
+
+3: Face not found (**nfdc route add** only)
+
+4: FaceUri canonization failed (**nfdc route add** only)
+
+5: Ambiguous: multiple matching faces are found (**nfdc route add** only)
 
 SEE ALSO
 --------
diff --git a/tests/tools/nfdc/face-module.t.cpp b/tests/tools/nfdc/face-module.t.cpp
index 24a4325..8d33ee8 100644
--- a/tests/tools/nfdc/face-module.t.cpp
+++ b/tests/tools/nfdc/face-module.t.cpp
@@ -24,7 +24,6 @@
  */
 
 #include "nfdc/face-module.hpp"
-#include <ndn-cxx/mgmt/nfd/face-query-filter.hpp>
 
 #include "execute-command-fixture.hpp"
 #include "status-fixture.hpp"
@@ -371,17 +370,7 @@
 BOOST_AUTO_TEST_CASE(NormalByFaceId)
 {
   this->processInterest = [this] (const Interest& interest) {
-    if (Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
-      BOOST_CHECK_EQUAL(interest.getName().size(), 5);
-      FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
-      BOOST_CHECK_EQUAL(filter, FaceQueryFilter().setFaceId(10156));
-
-      FaceStatus faceStatus;
-      faceStatus.setFaceId(10156)
-                .setLocalUri("tcp4://151.26.163.27:22967")
-                .setRemoteUri("tcp4://198.57.27.40:6363")
-                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
-      this->sendDataset(interest.getName(), faceStatus);
+    if (this->respondFaceQuery(interest)) {
       return;
     }
 
@@ -404,17 +393,7 @@
 BOOST_AUTO_TEST_CASE(NormalByFaceUri)
 {
   this->processInterest = [this] (const Interest& interest) {
-    if (Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
-      BOOST_CHECK_EQUAL(interest.getName().size(), 5);
-      FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
-      BOOST_CHECK_EQUAL(filter, FaceQueryFilter().setRemoteUri("tcp4://32.121.182.82:6363"));
-
-      FaceStatus faceStatus;
-      faceStatus.setFaceId(2249)
-                .setLocalUri("tcp4://30.99.87.98:31414")
-                .setRemoteUri("tcp4://32.121.182.82:6363")
-                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
-      this->sendDataset(interest.getName(), faceStatus);
+    if (this->respondFaceQuery(interest)) {
       return;
     }
 
@@ -437,8 +416,7 @@
 BOOST_AUTO_TEST_CASE(FaceNotExist)
 {
   this->processInterest = [this] (const Interest& interest) {
-    BOOST_CHECK(Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName()));
-    this->sendEmptyDataset(interest.getName());
+    BOOST_CHECK(this->respondFaceQuery(interest));
   };
 
   this->execute("face destroy 23728");
@@ -450,18 +428,7 @@
 BOOST_AUTO_TEST_CASE(Ambiguous)
 {
   this->processInterest = [this] (const Interest& interest) {
-    BOOST_CHECK(Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName()));
-
-    FaceStatus faceStatus1, faceStatus2;
-    faceStatus1.setFaceId(6720)
-               .setLocalUri("udp4://202.83.168.28:56363")
-               .setRemoteUri("udp4://225.131.75.231:56363")
-               .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERMANENT);
-    faceStatus2.setFaceId(31066)
-               .setLocalUri("udp4://25.90.26.32:56363")
-               .setRemoteUri("udp4://225.131.75.231:56363")
-               .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERMANENT);
-    this->sendDataset(interest.getName(), faceStatus1, faceStatus2);
+    BOOST_CHECK(this->respondFaceQuery(interest));
   };
 
   this->execute("face destroy udp4://225.131.75.231:56363");
@@ -495,13 +462,7 @@
 BOOST_AUTO_TEST_CASE(ErrorCommand)
 {
   this->processInterest = [this] (const Interest& interest) {
-    if (Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
-      FaceStatus faceStatus;
-      faceStatus.setFaceId(17757)
-                .setLocalUri("tcp4://27.65.24.30:19187")
-                .setRemoteUri("tcp4://70.47.27.77:6363")
-                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
-      this->sendDataset(interest.getName(), faceStatus);
+    if (this->respondFaceQuery(interest)) {
       return;
     }
 
@@ -509,7 +470,7 @@
     // no response to command
   };
 
-  this->execute("face destroy 17757");
+  this->execute("face destroy 10156");
   BOOST_CHECK_EQUAL(exitCode, 1);
   BOOST_CHECK(out.is_empty());
   BOOST_CHECK(err.is_equal("Error 10060 when destroying face: request timed out\n"));
diff --git a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
index 485b030..d1b8274 100644
--- a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
+++ b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
@@ -26,6 +26,7 @@
 #ifndef NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
 #define NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
 
+#include <ndn-cxx/mgmt/nfd/face-query-filter.hpp>
 #include <ndn-cxx/util/dummy-client-face.hpp>
 
 #include "tests/test-common.hpp"
@@ -146,6 +147,65 @@
     this->sendDatasetReply(prefix, buffer.buf(), buffer.size());
   }
 
+  /** \brief respond to specific FaceQuery requests
+   *  \retval true the Interest matches one of the defined patterns and is responded
+   *  \retval false the Interest is not responded
+   */
+  bool
+  respondFaceQuery(const Interest& interest)
+  {
+    using ndn::nfd::FacePersistency;
+    using ndn::nfd::FaceQueryFilter;
+    using ndn::nfd::FaceStatus;
+
+    if (!Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
+      return false;
+    }
+    BOOST_CHECK_EQUAL(interest.getName().size(), 5);
+    FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
+
+    if (filter == FaceQueryFilter().setFaceId(10156)) {
+      FaceStatus faceStatus;
+      faceStatus.setFaceId(10156)
+                .setLocalUri("tcp4://151.26.163.27:22967")
+                .setRemoteUri("tcp4://198.57.27.40:6363")
+                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+      this->sendDataset(interest.getName(), faceStatus);
+      return true;
+    }
+
+    if (filter == FaceQueryFilter().setRemoteUri("tcp4://32.121.182.82:6363")) {
+      FaceStatus faceStatus;
+      faceStatus.setFaceId(2249)
+                .setLocalUri("tcp4://30.99.87.98:31414")
+                .setRemoteUri("tcp4://32.121.182.82:6363")
+                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+      this->sendDataset(interest.getName(), faceStatus);
+      return true;
+    }
+
+    if (filter == FaceQueryFilter().setFaceId(23728)) {
+      this->sendEmptyDataset(interest.getName());
+      return true;
+    }
+
+    if (filter == FaceQueryFilter().setRemoteUri("udp4://225.131.75.231:56363")) {
+      FaceStatus faceStatus1, faceStatus2;
+      faceStatus1.setFaceId(6720)
+                 .setLocalUri("udp4://202.83.168.28:56363")
+                 .setRemoteUri("udp4://225.131.75.231:56363")
+                 .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERMANENT);
+      faceStatus2.setFaceId(31066)
+                 .setLocalUri("udp4://25.90.26.32:56363")
+                 .setRemoteUri("udp4://225.131.75.231:56363")
+                 .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERMANENT);
+      this->sendDataset(interest.getName(), faceStatus1, faceStatus2);
+      return true;
+    }
+
+    return false;
+  }
+
 private:
   virtual void
   processEventsOverride(time::milliseconds timeout)
diff --git a/tests/tools/nfdc/rib-module.t.cpp b/tests/tools/nfdc/rib-module.t.cpp
index 9550c5e..04c2b5f 100644
--- a/tests/tools/nfdc/rib-module.t.cpp
+++ b/tests/tools/nfdc/rib-module.t.cpp
@@ -25,6 +25,7 @@
 
 #include "nfdc/rib-module.hpp"
 
+#include "execute-command-fixture.hpp"
 #include "status-fixture.hpp"
 
 namespace nfd {
@@ -35,6 +36,134 @@
 BOOST_AUTO_TEST_SUITE(Nfdc)
 BOOST_FIXTURE_TEST_SUITE(TestRibModule, StatusFixture<RibModule>)
 
+BOOST_FIXTURE_TEST_SUITE(AddCommand, ExecuteCommandFixture)
+
+BOOST_AUTO_TEST_CASE(NormalByFaceId)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    if (this->respondFaceQuery(interest)) {
+      return;
+    }
+
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS("/localhost/nfd/rib/register");
+    ndn::nfd::RibRegisterCommand cmd;
+    cmd.validateRequest(req);
+    cmd.applyDefaultsToRequest(req);
+    BOOST_CHECK_EQUAL(req.getName(), "/vxXoEaWeDB");
+    BOOST_CHECK_EQUAL(req.getFaceId(), 10156);
+    BOOST_CHECK_EQUAL(req.getOrigin(), ndn::nfd::ROUTE_ORIGIN_STATIC);
+    BOOST_CHECK_EQUAL(req.getCost(), 0);
+    BOOST_CHECK_EQUAL(req.getFlags(), ndn::nfd::ROUTE_FLAGS_NONE);
+    BOOST_CHECK_EQUAL(req.hasExpirationPeriod(), false);
+
+    this->succeedCommand(req);
+  };
+
+  this->execute("route add /vxXoEaWeDB 10156 no-inherit");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("route-add-accepted prefix=/vxXoEaWeDB nexthop=10156 origin=255 "
+                           "cost=0 flags=none expires=never\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(NormalByFaceUri)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    if (this->respondFaceQuery(interest)) {
+      return;
+    }
+
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS("/localhost/nfd/rib/register");
+    ndn::nfd::RibRegisterCommand cmd;
+    cmd.validateRequest(req);
+    cmd.applyDefaultsToRequest(req);
+    BOOST_CHECK_EQUAL(req.getName(), "/FLQAsaYnYf");
+    BOOST_CHECK_EQUAL(req.getFaceId(), 2249);
+    BOOST_CHECK_EQUAL(req.getOrigin(), 17591);
+    BOOST_CHECK_EQUAL(req.getCost(), 702);
+    BOOST_CHECK_EQUAL(req.getFlags(), ndn::nfd::ROUTE_FLAG_CHILD_INHERIT |
+                                      ndn::nfd::ROUTE_FLAG_CAPTURE);
+    BOOST_REQUIRE_EQUAL(req.hasExpirationPeriod(), true);
+    BOOST_REQUIRE_EQUAL(req.getExpirationPeriod(), time::milliseconds(727411987));
+
+    ControlParameters resp = req;
+    resp.setExpirationPeriod(time::milliseconds(727411154)); // server side may change expiration
+    this->succeedCommand(resp);
+  };
+
+  this->execute("route add /FLQAsaYnYf tcp4://32.121.182.82:6363 "
+                "origin 17591 cost 702 capture expires 727411987");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("route-add-accepted prefix=/FLQAsaYnYf nexthop=2249 origin=17591 "
+                           "cost=702 flags=child-inherit|capture expires=727411154ms\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(FaceNotExist)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    BOOST_CHECK(this->respondFaceQuery(interest));
+  };
+
+  this->execute("route add /GJiKDus5i 23728");
+  BOOST_CHECK_EQUAL(exitCode, 3);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Face not found\n"));
+}
+
+BOOST_AUTO_TEST_CASE(Ambiguous)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    BOOST_CHECK(this->respondFaceQuery(interest));
+  };
+
+  this->execute("route add /BQqjjnVsz udp4://225.131.75.231:56363");
+  BOOST_CHECK_EQUAL(exitCode, 5);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Multiple faces match specified remote FaceUri. "
+                           "Re-run the command with a FaceId: "
+                           "6720 (local=udp4://202.83.168.28:56363), "
+                           "31066 (local=udp4://25.90.26.32:56363)\n"));
+}
+
+BOOST_AUTO_TEST_CASE(ErrorCanonization)
+{
+  this->execute("route add /bxJfGsVtDt udp6://32.38.164.64:10445");
+  BOOST_CHECK_EQUAL(exitCode, 4);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error during remote FaceUri canonization: "
+                           "No endpoints match the specified address selector\n"));
+}
+
+BOOST_AUTO_TEST_CASE(ErrorDataset)
+{
+  this->processInterest = nullptr; // no response to dataset or command
+
+  this->execute("route add /q1Qf7go7 udp://159.242.33.78");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when querying face: Timeout\n"));
+}
+
+BOOST_AUTO_TEST_CASE(ErrorCommand)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    if (this->respondFaceQuery(interest)) {
+      return;
+    }
+
+    MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS("/localhost/nfd/rib/register");
+    // no response to command
+  };
+
+  this->execute("route add /bYiMbEuE 10156");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when adding route: request timed out\n"));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // AddCommand
+
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <rib>
     <ribEntry>
diff --git a/tools/nfdc/available-commands.cpp b/tools/nfdc/available-commands.cpp
index 586da48..d6782ac 100644
--- a/tools/nfdc/available-commands.cpp
+++ b/tools/nfdc/available-commands.cpp
@@ -24,11 +24,12 @@
  */
 
 #include "available-commands.hpp"
-#include "help.hpp"
-#include "status.hpp"
 #include "face-module.hpp"
-#include "legacy-status.hpp"
+#include "help.hpp"
 #include "legacy-nfdc.hpp"
+#include "legacy-status.hpp"
+#include "rib-module.hpp"
+#include "status.hpp"
 
 namespace nfd {
 namespace tools {
@@ -40,6 +41,7 @@
   registerHelpCommand(parser);
   registerStatusCommands(parser);
   FaceModule::registerCommands(parser);
+  RibModule::registerCommands(parser);
 
   registerLegacyStatusCommand(parser);
 
@@ -50,7 +52,7 @@
     std::string replacementCommand; ///< replacement for deprecated legacy subcommand
   };
   const std::vector<LegacyNfdcCommandDefinition> legacyNfdcSubcommands{
-    {"register", "register a prefix", ""},
+    {"register", "register a prefix", "route add"},
     {"unregister", "unregister a prefix", ""},
     {"create", "create a face", "face create"},
     {"destroy", "destroy a face", "face destroy"},
diff --git a/tools/nfdc/face-module.cpp b/tools/nfdc/face-module.cpp
index 216ded6..f0c9dfc 100644
--- a/tools/nfdc/face-module.cpp
+++ b/tools/nfdc/face-module.cpp
@@ -209,17 +209,10 @@
 void
 FaceModule::destroy(ExecuteContext& ctx)
 {
-  const boost::any faceIdOrUri = ctx.args.at("face");
+  const boost::any& faceIdOrUri = ctx.args.at("face");
 
   FindFace findFace(ctx);
-  FindFace::Code res = FindFace::Code::ERROR;
-  const uint64_t* faceId = boost::any_cast<uint64_t>(&faceIdOrUri);
-  if (faceId != nullptr) {
-    res = findFace.execute(*faceId);
-  }
-  else {
-    res = findFace.execute(boost::any_cast<FaceUri>(faceIdOrUri));
-  }
+  FindFace::Code res = findFace.execute(faceIdOrUri);
 
   ctx.exitCode = static_cast<int>(res);
   switch (res) {
diff --git a/tools/nfdc/find-face.cpp b/tools/nfdc/find-face.cpp
index 520ea92..d877e81 100644
--- a/tools/nfdc/find-face.cpp
+++ b/tools/nfdc/find-face.cpp
@@ -55,6 +55,18 @@
 }
 
 FindFace::Code
+FindFace::execute(const boost::any& faceIdOrUri)
+{
+  const uint64_t* faceId = boost::any_cast<uint64_t>(&faceIdOrUri);
+  if (faceId != nullptr) {
+    return this->execute(*faceId);
+  }
+  else {
+    return this->execute(boost::any_cast<FaceUri>(faceIdOrUri));
+  }
+}
+
+FindFace::Code
 FindFace::execute(const FaceQueryFilter& filter, bool allowMulti)
 {
   BOOST_ASSERT(m_res == Code::NOT_STARTED);
diff --git a/tools/nfdc/find-face.hpp b/tools/nfdc/find-face.hpp
index 57eb0f0..45c3f1e 100644
--- a/tools/nfdc/find-face.hpp
+++ b/tools/nfdc/find-face.hpp
@@ -70,6 +70,14 @@
   Code
   execute(uint64_t faceId);
 
+  /** \brief find face by FaceId or FaceUri
+   *  \param faceIdOrUri a boost::any that contains uint64_t or FaceUri
+   *  \note allowMulti will be set to false
+   *  \throw boost::bad_any_cast faceIdOrUri is neither uint64_t nor FaceUri
+   */
+  Code
+  execute(const boost::any& faceIdOrUri);
+
   /** \brief find face by FaceQueryFilter
    *  \pre execute has not been invoked
    */
diff --git a/tools/nfdc/rib-module.cpp b/tools/nfdc/rib-module.cpp
index 454dc1e..b85410c 100644
--- a/tools/nfdc/rib-module.cpp
+++ b/tools/nfdc/rib-module.cpp
@@ -24,6 +24,7 @@
  */
 
 #include "rib-module.hpp"
+#include "find-face.hpp"
 #include "format-helpers.hpp"
 
 namespace nfd {
@@ -31,6 +32,90 @@
 namespace nfdc {
 
 void
+RibModule::registerCommands(CommandParser& parser)
+{
+  CommandDefinition defRouteAdd("route", "add");
+  defRouteAdd
+    .setTitle("add a route")
+    .addArg("prefix", ArgValueType::NAME, Required::YES, Positional::YES)
+    .addArg("nexthop", ArgValueType::FACE_ID_OR_URI, Required::YES, Positional::YES)
+    .addArg("origin", ArgValueType::UNSIGNED, Required::NO, Positional::NO)
+    .addArg("cost", ArgValueType::UNSIGNED, Required::NO, Positional::NO)
+    .addArg("no-inherit", ArgValueType::NONE, Required::NO, Positional::NO)
+    .addArg("capture", ArgValueType::NONE, Required::NO, Positional::NO)
+    .addArg("expires", ArgValueType::UNSIGNED, Required::NO, Positional::NO);
+  parser.addCommand(defRouteAdd, &RibModule::add);
+}
+
+void
+RibModule::add(ExecuteContext& ctx)
+{
+  auto prefix = ctx.args.get<Name>("prefix");
+  const boost::any& nexthop = ctx.args.at("nexthop");
+  auto origin = ctx.args.get<uint64_t>("origin", ndn::nfd::ROUTE_ORIGIN_STATIC);
+  auto cost = ctx.args.get<uint64_t>("cost", 0);
+  bool wantChildInherit = !ctx.args.get<bool>("no-inherit", false);
+  bool wantCapture = ctx.args.get<bool>("capture", false);
+  auto expiresMillis = ctx.args.getOptional<uint64_t>("expires");
+
+  FindFace findFace(ctx);
+  FindFace::Code res = findFace.execute(nexthop);
+
+  ctx.exitCode = static_cast<int>(res);
+  switch (res) {
+    case FindFace::Code::OK:
+      break;
+    case FindFace::Code::ERROR:
+    case FindFace::Code::CANONIZE_ERROR:
+    case FindFace::Code::NOT_FOUND:
+      ctx.err << findFace.getErrorReason() << '\n';
+      return;
+    case FindFace::Code::AMBIGUOUS:
+      ctx.err << "Multiple faces match specified remote FaceUri. Re-run the command with a FaceId:";
+      findFace.printDisambiguation(ctx.err, FindFace::DisambiguationStyle::LOCAL_URI);
+      ctx.err << '\n';
+      return;
+    default:
+      BOOST_ASSERT_MSG(false, "unexpected FindFace result");
+      return;
+  }
+
+  ControlParameters registerParams;
+  registerParams
+    .setName(prefix)
+    .setFaceId(findFace.getFaceId())
+    .setOrigin(origin)
+    .setCost(cost)
+    .setFlags((wantChildInherit ? ndn::nfd::ROUTE_FLAG_CHILD_INHERIT : 0) |
+              (wantCapture ? ndn::nfd::ROUTE_FLAG_CAPTURE : 0));
+  if (expiresMillis) {
+    registerParams.setExpirationPeriod(time::milliseconds(*expiresMillis));
+  }
+
+  ctx.controller.start<ndn::nfd::RibRegisterCommand>(
+    registerParams,
+    [&] (const ControlParameters& resp) {
+      ctx.out << "route-add-accepted ";
+      text::ItemAttributes ia;
+      ctx.out << ia("prefix") << resp.getName()
+              << ia("nexthop") << resp.getFaceId()
+              << ia("origin") << resp.getOrigin()
+              << ia("cost") << resp.getCost()
+              << ia("flags") << static_cast<ndn::nfd::RouteFlags>(resp.getFlags());
+      if (resp.hasExpirationPeriod()) {
+        ctx.out << ia("expires") << resp.getExpirationPeriod().count() << "ms\n";
+      }
+      else {
+        ctx.out<< ia("expires") << "never\n";
+      }
+    },
+    ctx.makeCommandFailureHandler("adding route"),
+    ctx.makeCommandOptions());
+
+  ctx.face.processEvents();
+}
+
+void
 RibModule::fetchStatus(Controller& controller,
                        const function<void()>& onSuccess,
                        const Controller::DatasetFailCallback& onFailure,
diff --git a/tools/nfdc/rib-module.hpp b/tools/nfdc/rib-module.hpp
index 85230aa..cca1ef4 100644
--- a/tools/nfdc/rib-module.hpp
+++ b/tools/nfdc/rib-module.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,6 +27,7 @@
 #define NFD_TOOLS_NFDC_RIB_MODULE_HPP
 
 #include "module.hpp"
+#include "command-parser.hpp"
 
 namespace nfd {
 namespace tools {
@@ -41,13 +42,23 @@
 class RibModule : public Module, noncopyable
 {
 public:
-  virtual void
+  /** \brief register 'route list', 'route show', 'route add', 'route remove' commands
+   */
+  static void
+  registerCommands(CommandParser& parser);
+
+  /** \brief the 'route add' command
+   */
+  static void
+  add(ExecuteContext& ctx);
+
+  void
   fetchStatus(Controller& controller,
               const function<void()>& onSuccess,
               const Controller::DatasetFailCallback& onFailure,
               const CommandOptions& options) override;
 
-  virtual void
+  void
   formatStatusXml(std::ostream& os) const override;
 
   /** \brief format a single status item as XML
@@ -57,7 +68,7 @@
   void
   formatItemXml(std::ostream& os, const RibEntry& item) const;
 
-  virtual void
+  void
   formatStatusText(std::ostream& os) const override;
 
   /** \brief format a single status item as text