Skip to content

Commit

Permalink
scion: one command to rule them all (#3708)
Browse files Browse the repository at this point in the history
Cobra has shown to be a very useful framework for writing CLI applications.
It makes it easy to write applications with many sub-commands and high configurability.

We currently have many binaries in the code base, that do not really warrant to be their own application. With the approach that we put most application logic in `go/pkg/...`, we can easily create standalone binaries (when needed/desired) and also bundle them in an all-in-one scion command.

This PR is a exploration how this could look like.
At the base is the `scion` command that bundles all the other commands.

For this exploration, I moved the showpaths logic to `go/pkgs/showpaths` such that it can be imported as a library. The old `showpaths` binary is not affected by the change, and if we continue to support the standalone binary, this will be easy to do.

Under `go/scion` the all-in-on binary is set up. Currently, it only has 3 commands:
- `completion` : generate auto-completion
- `version`: display the scion version
- `showpaths`: run the showpaths application

The nice thing is, that we gradually can add sub-commands that make sense to be bundled.
E.g., I envision `scion echo`, `scion traceroute`, `scion pingpong`, and the soon to be added `scion pathdbdump` (#3707) tools to be part of `scion` eventually.
  • Loading branch information
oncilla authored Apr 21, 2020
1 parent 374ed32 commit 3a1b9ed
Show file tree
Hide file tree
Showing 12 changed files with 925 additions and 18 deletions.
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pkg_tar(
"//go/dispatcher:dispatcher",
"//go/tools/logdog:logdog",
"//go/sciond:sciond",
"//go/scion:scion",
"//go/tools/scion-pki:scion-pki",
"//go/tools/scmp:scmp",
"//go/tools/showpaths:showpaths",
Expand Down
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ require (
github.com/google/gopacket v1.1.16-0.20190123011826-102d5ca2098c
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
github.com/inconshreveable/log15 v0.0.0-20161013181240-944cbfb97b44
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kormat/fmt15 v0.0.0-20181112140556-ee69fecb2656
github.com/lucas-clemente/quic-go v0.7.1-0.20190212114006-fd7246d7ed6e
github.com/marten-seemann/qtls v0.0.0-20190207043627-591c71538704 // indirect
Expand All @@ -32,8 +31,7 @@ require (
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44
github.com/smartystreets/goconvey v1.6.4
github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b
github.com/spf13/cobra v0.0.3-0.20180408161736-cd30c2a7e91a
github.com/spf13/pflag v1.0.1-0.20180403115518-1ce0cc6db402 // indirect
github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709
github.com/syndtr/gocapability v0.0.0-20160928074757-e7cb7fa329f4
github.com/uber/jaeger-client-go v2.20.1+incompatible
Expand Down
92 changes: 85 additions & 7 deletions go.sum

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions go/pkg/showpaths/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "go_default_library",
srcs = [
"config.go",
"showpaths.go",
],
importpath = "github.com/scionproto/scion/go/pkg/showpaths",
visibility = ["//visibility:public"],
deps = [
"//go/lib/addr:go_default_library",
"//go/lib/common:go_default_library",
"//go/lib/sciond:go_default_library",
"//go/lib/sciond/pathprobe:go_default_library",
"//go/lib/serrors:go_default_library",
"//go/lib/snet:go_default_library",
"//go/lib/snet/addrutil:go_default_library",
],
)
38 changes: 38 additions & 0 deletions go/pkg/showpaths/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2020 Anapaya Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package showpaths

import (
"net"
)

// DefaultMaxPaths is the maximum number of paths that are displayed by default.
const DefaultMaxPaths = 10

// Config configures the showpath run.
type Config struct {
// Local configures the local IP address to use. If this option is not provided,
// a local IP that can reach SCION hosts is selected with the help of the kernel.
Local net.IP
// SCIOND configures a specific SCION Deamon address.
SCIOND string
// MaxPaths configures the maximum number of displayed paths. If this option is
// not provided, the DefaultMaxPaths is used.
MaxPaths int
// Refresh configures whether sciond is queried with the refresh flag.
Refresh bool
// NoProbe configures whether the path status is probed or not.
NoProbe bool
}
170 changes: 170 additions & 0 deletions go/pkg/showpaths/showpaths.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright 2018 ETH Zurich, Anapaya Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package showpaths

import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"strings"
"time"

"github.com/scionproto/scion/go/lib/addr"
"github.com/scionproto/scion/go/lib/common"
"github.com/scionproto/scion/go/lib/sciond"
"github.com/scionproto/scion/go/lib/sciond/pathprobe"
"github.com/scionproto/scion/go/lib/serrors"
"github.com/scionproto/scion/go/lib/snet"
"github.com/scionproto/scion/go/lib/snet/addrutil"
)

// Result contains all the discovered paths.
type Result struct {
Destination addr.IA `json:"destination"`
Paths []Path `json:"paths"`
}

// Path holds information about the discovered path.
type Path struct {
FullPath snet.Path `json:"-"`
Fingerprint string `json:"fingerprint"`
Hops []Hop `json:"hops"`
NextHop string `json:"next_hop"`
Expiry time.Time `json:"expiry"`
MTU uint16 `json:"mtu"`
Status string `json:"status,omitempty"`
StatusInfo string `json:"status_info,omitempty"`
Local net.IP `json:"local_ip,omitempty"`
}

// Hop represents an hop on the path.
type Hop struct {
IfID common.IFIDType `json:"ifid"`
IA addr.IA `json:"isd_as"`
}

// Human writes human readable output to the writer.
func (r Result) Human(w io.Writer, showExpiration bool) {
fmt.Fprintln(w, "Available paths to", r.Destination)
for i, path := range r.Paths {
fmt.Fprintf(w, "[%2d] %s", i, fmt.Sprintf("%s", path.FullPath))
if showExpiration {
ttl := time.Until(path.Expiry).Truncate(time.Second)
fmt.Fprintf(w, " Expires: %s (%s)", path.Expiry, ttl)
}
if path.Status != "" {
fmt.Fprintf(w, " Status: %s LocalIP: %s", path.Status, path.Local)
}
fmt.Fprintln(w)
}
}

// JSON writes the showpaths result as a json object to the writer.
func (r Result) JSON(w io.Writer) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(r)
}

// Run lists the paths to the specified ISD-AS to stdout.
func Run(ctx context.Context, dst addr.IA, cfg Config) (*Result, error) {
sdConn, err := sciond.NewService(cfg.SCIOND).Connect(ctx)
if err != nil {
return nil, serrors.WrapStr("error connecting to SCIOND", err)
}
localIA, err := sdConn.LocalIA(ctx)
if err != nil {
return nil, serrors.WrapStr("error determining local ISD-AS", err)
}

// TODO(lukedirtwalker): Replace this with snet.Router once we have the
// possibility to have the same functionality, i.e. refresh, fetch all paths.
// https://github.com/scionproto/scion/issues/3348
paths, err := sdConn.Paths(ctx, dst, addr.IA{},
sciond.PathReqFlags{Refresh: cfg.Refresh, PathCount: uint16(cfg.MaxPaths)})
if err != nil {
return nil, serrors.WrapStr("failed to retrieve paths from SCIOND", err)
}

var statuses map[string]pathprobe.Status
var localIP net.IP
if !cfg.NoProbe {
// Resolve local IP in case it is not configured.
if localIP = cfg.Local; localIP == nil {
localIP, err = findDefaultLocalIP(ctx, sdConn)
if err != nil {
return nil, serrors.WrapStr("failed to determine local IP", err)
}
}
statuses, err = pathprobe.Prober{
DstIA: dst,
LocalIA: localIA,
LocalIP: localIP,
}.GetStatuses(ctx, paths)
if err != nil {
serrors.WrapStr("failed to get status", err)
}
}

res := &Result{Destination: dst}
for _, path := range paths {
rpath := Path{
FullPath: path,
Fingerprint: path.Fingerprint().String()[:16],
NextHop: path.OverlayNextHop().String(),
Expiry: path.Expiry(),
MTU: path.MTU(),
Local: localIP,
}
for _, hop := range path.Interfaces() {
rpath.Hops = append(rpath.Hops, Hop{IA: hop.IA(), IfID: hop.ID()})
}
if status, ok := statuses[pathprobe.PathKey(path)]; ok {
rpath.Status = strings.ToLower(string(status.Status))
rpath.StatusInfo = status.AdditionalInfo
}
res.Paths = append(res.Paths, rpath)
}
return res, nil
}

// TODO(matzf): this is a simple, hopefully temporary, workaround to not having
// wildcard addresses in snet.
// Here we just use a seemingly sensible default IP, but in the general case
// the local IP would depend on the next hop of selected path. This approach
// will not work in more complicated setups where e.g. different network
// interface are used to talk to different AS interfaces.
// Once a available, a wildcard address should be used and this should simply
// be removed.
//
// findDefaultLocalIP returns _a_ IP of this host in the local AS.
func findDefaultLocalIP(ctx context.Context, sciondConn sciond.Connector) (net.IP, error) {
hostInLocalAS, err := findAnyHostInLocalAS(ctx, sciondConn)
if err != nil {
return nil, err
}
return addrutil.ResolveLocal(hostInLocalAS)
}

// findAnyHostInLocalAS returns the IP address of some (infrastructure) host in the local AS.
func findAnyHostInLocalAS(ctx context.Context, sciondConn sciond.Connector) (net.IP, error) {
addr, err := sciond.TopoQuerier{Connector: sciondConn}.OverlayAnycast(ctx, addr.SvcBS)
if err != nil {
return nil, err
}
return addr.IP, nil
}
29 changes: 29 additions & 0 deletions go/scion/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//:scion.bzl", "scion_go_binary")

go_library(
name = "go_default_library",
srcs = [
"completion.go",
"scion.go",
"showpaths.go",
"version.go",
],
importpath = "github.com/scionproto/scion/go/scion",
visibility = ["//visibility:private"],
deps = [
"//go/lib/addr:go_default_library",
"//go/lib/env:go_default_library",
"//go/lib/log:go_default_library",
"//go/lib/sciond:go_default_library",
"//go/lib/serrors:go_default_library",
"//go/pkg/showpaths:go_default_library",
"@com_github_spf13_cobra//:go_default_library",
],
)

scion_go_binary(
name = "scion",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
58 changes: 58 additions & 0 deletions go/scion/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2020 Anapaya Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"os"

"github.com/spf13/cobra"

"github.com/scionproto/scion/go/lib/serrors"
)

var completionShell string

// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion",
Short: "Generates bash completion scripts",
Long: `'completion' outputs the autocomplete configuration for some shells.
For example, you can add autocompletion for your current bash session using:
. <( scion completion )
To permanently add bash autocompletion, run:
scion completion > /etc/bash_completion.d/scion
`,
RunE: func(cmd *cobra.Command, args []string) error {
switch completionShell {
case "bash":
return rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
return rootCmd.GenZshCompletion(os.Stdout)
case "fish":
return rootCmd.GenFishCompletion(os.Stdout, true)
default:
return serrors.New("unknown shell", "input", completionShell)
}
},
}

func init() {
rootCmd.AddCommand(completionCmd)
completionCmd.Flags().StringVar(&completionShell, "shell", "bash",
"Shell type (bash|zsh|fish)")
}
43 changes: 43 additions & 0 deletions go/scion/scion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2020 Anapaya Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
}

var rootCmd = &cobra.Command{
Use: "scion",
Short: "A clean-slate internet architecture",
Args: cobra.NoArgs,
// Silence the errors, since we print them in main. Otherwise, cobra
// will print any non-nil errors returned by a RunE function.
// See https://github.com/spf13/cobra/issues/340.
// Commands should turn off the usage help message, if they deem the arguments
// to be reasonable well-formed. This avoids outputing help message on errors
// that are not caused by malformed input.
// See https://github.com/spf13/cobra/issues/340#issuecomment-374617413.
SilenceErrors: true,
}
Loading

0 comments on commit 3a1b9ed

Please sign in to comment.