[botanist] Expose IPv4 address to subprocess
This change computes the IPv4 address of a fuchsia node via mDNS and
includes it in the environment of `botanist run`'s subprocess.
Test: was able to successfully resolve the IP address of
my NUC locally
Change-Id: I38a7171e5618d805c17556e5a9bae0fa9691970d
diff --git a/botanist/ip.go b/botanist/ip.go
new file mode 100644
index 0000000..dbd3c4b
--- /dev/null
+++ b/botanist/ip.go
@@ -0,0 +1,75 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package botanist
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "time"
+
+ "fuchsia.googlesource.com/tools/logger"
+ "fuchsia.googlesource.com/tools/mdns"
+ "fuchsia.googlesource.com/tools/retry"
+)
+
+// Interval at which ResolveIP will wait for a response to a question packet.
+const mDNSTimeout time.Duration = 2 * time.Second
+
+func getLocalDomain(nodename string) string {
+ return nodename + ".local"
+}
+
+// ResolveIP returns the IPv4 address of a fuchsia node via mDNS.
+//
+// TODO(joshuaseaton): Refactor dev_finder to share 'resolve' logic with botanist.
+func ResolveIPv4(ctx context.Context, nodename string, timeout time.Duration) (net.IP, error) {
+ var m mdns.MDNS
+ out := make(chan net.IP)
+ domain := getLocalDomain(nodename)
+ m.AddHandler(func(iface net.Interface, addr net.Addr, packet mdns.Packet) {
+ for _, a := range packet.Answers {
+ if a.Class == mdns.IN && a.Type == mdns.A && a.Domain == domain {
+ out <- net.IP(a.Data)
+ return
+ }
+ }
+ })
+ m.AddWarningHandler(func(addr net.Addr, err error) {
+ logger.Infof(ctx, "from: %v; warn: %v", addr, err)
+ })
+ errs := make(chan error)
+ m.AddErrorHandler(func(err error) {
+ errs <- err
+ })
+
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ if err := m.Start(ctx, mdns.DefaultPort); err != nil {
+ return nil, fmt.Errorf("could not start mDNS client: %v", err)
+ }
+
+ // Send question packets to the mDNS server at intervals of mDNSTimeout for a total of
+ // |timeout|; retry, as it takes time for the netstack and server to be brought up.
+ var ip net.IP
+ var err error
+ err = retry.Retry(ctx, &retry.ZeroBackoff{}, func() error {
+ m.Send(mdns.QuestionPacket(domain))
+ ctx, cancel := context.WithTimeout(context.Background(), mDNSTimeout)
+ defer cancel()
+
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("timeout")
+ case err = <-errs:
+ return err
+ case ip = <-out:
+ return nil
+ }
+ }, nil)
+
+ return ip, err
+}
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index 81ce2aa..de8f6bd 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -23,6 +23,8 @@
"golang.org/x/crypto/ssh"
)
+const netstackTimeout time.Duration = 1 * time.Minute
+
// RunCommand is a Command implementation for booting a device and running a
// given command locally.
type RunCommand struct {
@@ -131,10 +133,16 @@
return err
}
+ ip, err := botanist.ResolveIPv4(ctx, nodename, netstackTimeout)
+ if err != nil {
+ return fmt.Errorf("could not resolve IP address: %v", err)
+ }
+
env := append(
os.Environ(),
fmt.Sprintf("FUCHSIA_NODENAME=%s", nodename),
- fmt.Sprintf("FUCHSIA_SSH_KEY=%s", string(privKeys[0])),
+ fmt.Sprintf("FUCHSIA_IPV4_ADDR=%s", ip),
+ fmt.Sprintf("FUCHSIA_SSH_KEY=%s", privKeys[0]),
)
// Run command.
diff --git a/mdns/mdns.go b/mdns/mdns.go
index 7233b9a..61baced 100644
--- a/mdns/mdns.go
+++ b/mdns/mdns.go
@@ -19,6 +19,9 @@
"golang.org/x/net/ipv4"
)
+// DefaultPort is the mDNS port required of the spec, though this library is port-agnostic.
+const DefaultPort int = 5353
+
type Header struct {
ID uint16
Flags uint16