Add support for multiple hostnames for a given node

This will be useful both on a roboRIO and to allow encoding the team
number in the hostnames on the Pis.

Change-Id: I3975785240d7502bc3922c92e9faad4bdadda0d9
diff --git a/aos/BUILD b/aos/BUILD
index ac5c3e9..47de872 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -475,6 +475,7 @@
         "testdata/expected.json",
         "testdata/expected_multinode.json",
         "testdata/good_multinode.json",
+        "testdata/good_multinode_hostnames.json",
         "testdata/invalid_destination_node.json",
         "testdata/invalid_nodes.json",
         "testdata/invalid_source_node.json",
diff --git a/aos/configuration.cc b/aos/configuration.cc
index 2d4b867..1ca28ad 100644
--- a/aos/configuration.cc
+++ b/aos/configuration.cc
@@ -362,7 +362,7 @@
   // Check that if there is a node list, all the source nodes are filled out and
   // valid, and all the destination nodes are valid (and not the source).  This
   // is a basic consistency check.
-  if (result.message().has_nodes()) {
+  if (result.message().has_nodes() && config.message().has_channels()) {
     for (const Channel *c : *config.message().channels()) {
       CHECK(c->has_source_node()) << ": Channel " << FlatbufferToJson(c)
                                   << " is missing \"source_node\"";
@@ -597,9 +597,16 @@
 const Node *GetNodeFromHostname(const Configuration *config,
                                 std::string_view hostname) {
   for (const Node *node : *config->nodes()) {
-    if (node->hostname()->string_view() == hostname) {
+    if (node->has_hostname() && node->hostname()->string_view() == hostname) {
       return node;
     }
+    if (node->has_hostnames()) {
+      for (const auto &candidate : *node->hostnames()) {
+        if (candidate->string_view() == hostname) {
+          return node;
+        }
+      }
+    }
   }
   return nullptr;
 }
diff --git a/aos/configuration.fbs b/aos/configuration.fbs
index 317c100..576616e 100644
--- a/aos/configuration.fbs
+++ b/aos/configuration.fbs
@@ -126,6 +126,12 @@
   hostname:string;
   // Port to serve forwarded data from.
   port:ushort = 9971;
+
+  // An alternative to hostname which allows specifying multiple hostnames,
+  // any of which will match this node.
+  //
+  // Don't specify a hostname in multiple nodes in the same configuration.
+  hostnames:[string];
 }
 
 // Overall configuration datastructure for the pubsub.
diff --git a/aos/configuration_test.cc b/aos/configuration_test.cc
index 0c0874b..c36bd93 100644
--- a/aos/configuration_test.cc
+++ b/aos/configuration_test.cc
@@ -677,6 +677,46 @@
             GetNodeOrDie(&config.message(), config2.message().nodes()->Get(0)));
 }
 
+TEST_F(ConfigurationTest, GetNodeFromHostname) {
+  FlatbufferDetachedBuffer<Configuration> config =
+      ReadConfig("aos/testdata/good_multinode.json");
+  EXPECT_EQ("pi1",
+            CHECK_NOTNULL(GetNodeFromHostname(&config.message(), "raspberrypi"))
+                ->name()
+                ->string_view());
+  EXPECT_EQ("pi2", CHECK_NOTNULL(
+                       GetNodeFromHostname(&config.message(), "raspberrypi2"))
+                       ->name()
+                       ->string_view());
+  EXPECT_EQ(nullptr, GetNodeFromHostname(&config.message(), "raspberrypi3"));
+  EXPECT_EQ(nullptr, GetNodeFromHostname(&config.message(), "localhost"));
+  EXPECT_EQ(nullptr, GetNodeFromHostname(&config.message(), "3"));
+}
+
+TEST_F(ConfigurationTest, GetNodeFromHostnames) {
+  FlatbufferDetachedBuffer<Configuration> config =
+      ReadConfig("aos/testdata/good_multinode_hostnames.json");
+  EXPECT_EQ("pi1",
+            CHECK_NOTNULL(GetNodeFromHostname(&config.message(), "raspberrypi"))
+                ->name()
+                ->string_view());
+  EXPECT_EQ("pi2", CHECK_NOTNULL(
+                       GetNodeFromHostname(&config.message(), "raspberrypi2"))
+                       ->name()
+                       ->string_view());
+  EXPECT_EQ("pi2", CHECK_NOTNULL(
+                       GetNodeFromHostname(&config.message(), "raspberrypi3"))
+                       ->name()
+                       ->string_view());
+  EXPECT_EQ("pi2", CHECK_NOTNULL(
+                       GetNodeFromHostname(&config.message(), "other"))
+                       ->name()
+                       ->string_view());
+  EXPECT_EQ(nullptr, GetNodeFromHostname(&config.message(), "raspberrypi4"));
+  EXPECT_EQ(nullptr, GetNodeFromHostname(&config.message(), "localhost"));
+  EXPECT_EQ(nullptr, GetNodeFromHostname(&config.message(), "3"));
+}
+
 }  // namespace testing
 }  // namespace configuration
 }  // namespace aos
diff --git a/aos/testdata/good_multinode_hostnames.json b/aos/testdata/good_multinode_hostnames.json
new file mode 100644
index 0000000..b82f6f3
--- /dev/null
+++ b/aos/testdata/good_multinode_hostnames.json
@@ -0,0 +1,18 @@
+{
+  "nodes": [
+    {
+      "name": "pi1",
+      "hostnames": [
+        "raspberrypi"
+      ]
+    },
+    {
+      "name": "pi2",
+      "hostnames": [
+        "raspberrypi2",
+        "raspberrypi3",
+        "other"
+      ]
+    }
+  ]
+}