Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/scion: add json/yaml format to ping and traceroute #4287

Merged
merged 25 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/command/scion/scion_ping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Options
-c, --count uint16 total number of packets to send
--dispatcher string Path to the dispatcher socket (default "/run/shm/dispatcher/default.sock")
--epic Enable EPIC for path probing.
--format string Specify the output format (human|json|yaml) (default "human")
--healthy-only only use healthy paths
-h, --help help for ping
-i, --interactive interactive mode
Expand All @@ -99,7 +100,7 @@ Options
'payload_size' and 'packet_size' flags.
--no-color disable colored output
--packet-size uint number of bytes to be sent including the SCION Header and SCMP echo header,
the desired size must provide enough space for the required headers. This flag
the desired size must provide enough space for the required headers. This flag
overrides the 'payload_size' flag.
-s, --payload-size uint number of bytes to be sent in addition to the SCION Header and SCMP echo header;
the total size of the packet is still variable size due to the variable size of
Expand Down
4 changes: 1 addition & 3 deletions doc/command/scion/scion_showpaths.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ By default, the paths are probed. Paths served from the SCION Deamon's might not
forward traffic successfully (e.g. if a network link went down, or there is a black
hole on the path). To disable path probing, set the appropriate flag.

'showpaths' can be instructed to output the paths as json using the the \--json flag.

If no alive path is discovered, json output is not enabled, and probing is not
disabled, showpaths will exit with the code 1.
On other errors, showpaths will exit with code 2.
Expand Down Expand Up @@ -93,9 +91,9 @@ Options
--dispatcher string Path to the dispatcher socket (default "/run/shm/dispatcher/default.sock")
--epic Enable EPIC.
-e, --extended Show extended path meta data information
--format string Specify the output format (human|json|yaml) (default "human")
-h, --help help for showpaths
--isd-as isd-as The local ISD-AS to use. (default 0-0)
-j, --json Write the output as machine readable json
-l, --local ip Local IP address to listen on. (default zero IP)
--log.level string Console logging level verbosity (debug|info|error)
-m, --maxpaths int Maximum number of paths that are displayed (default 10)
Expand Down
1 change: 1 addition & 0 deletions doc/command/scion/scion_traceroute.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Options

--dispatcher string Path to the dispatcher socket (default "/run/shm/dispatcher/default.sock")
--epic Enable EPIC.
--format string Specify the output format (human|json|yaml) (default "human")
-h, --help help for traceroute
-i, --interactive interactive mode
--isd-as isd-as The local ISD-AS to use. (default 0-0)
Expand Down
64 changes: 39 additions & 25 deletions private/path/pathpol/sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,33 +83,14 @@ func (s *Sequence) Eval(paths []snet.Path) []snet.Path {
}
result := []snet.Path{}
for _, path := range paths {
var desc string
ifaces := path.Metadata().Interfaces
switch {
// Path should contain even number of interfaces. 1 for source AS,
// 1 for destination AS and 2 per each intermediate AS. Invalid paths should
// not occur but if they do let's ignore them.
case len(ifaces)%2 != 0:
log.Error("Invalid path with even number of hops", "path", path)
desc, err := GetSequence(path)
if desc != "" {
desc = desc + " "
}
if err != nil {
log.Error("get sequence from path", "err", err)
continue
// Empty paths are special cased.
case len(ifaces) == 0:
desc = ""
default:
// Turn the path into a string. For each AS on the path there will be
// one element in form <IA>#<inbound-interface>,<outbound-interface>,
// e.g. 64-ff00:0:112#3,5. For the source AS, the inbound interface will be
// zero. For destination AS, outbound interface will be zero.

hops := make([]string, 0, len(ifaces)/2+1)
hops = append(hops, hop(ifaces[0].IA, 0, ifaces[0].ID))
for i := 1; i < len(ifaces)-1; i += 2 {
hops = append(hops, hop(ifaces[i].IA, ifaces[i].ID, ifaces[i+1].ID))
}
hops = append(hops, hop(ifaces[len(ifaces)-1].IA, ifaces[len(ifaces)-1].ID, 0))
desc = strings.Join(hops, " ") + " "
}

// Check whether the string matches the sequence regexp.
if s.re.MatchString(desc) {
result = append(result, path)
Expand Down Expand Up @@ -315,3 +296,36 @@ func (l *sequenceListener) ExitIFace(c *sequence.IFaceContext) {
func hop(ia addr.IA, ingress, egress common.IFIDType) string {
return fmt.Sprintf("%s#%d,%d", ia, ingress, egress)
}

// GetSequence constructs the sequence string from snet path
// output format:
//
// 1-ff00:0:133#42 1-ff00:0:120#2,1 1-ff00:0:110#21
func GetSequence(path snet.Path) (string, error) {
var desc string
ifaces := path.Metadata().Interfaces
switch {
// Path should contain even number of interfaces. 1 for source AS,
// 1 for destination AS and 2 per each intermediate AS. Invalid paths should
// not occur but if they do let's ignore them.
case len(ifaces)%2 != 0:
return "", serrors.New("Invalid path with odd number of hops", "path", path)
// Empty paths are special cased.
case len(ifaces) == 0:
desc = ""
default:
// Turn the path into a string. For each AS on the path there will be
// one element in form <IA>#<inbound-interface>,<outbound-interface>,
// e.g. 64-ff00:0:112#3,5. For the source AS, the inbound interface will be
// zero. For destination AS, outbound interface will be zero.

hops := make([]string, 0, len(ifaces)/2+1)
hops = append(hops, hop(ifaces[0].IA, 0, ifaces[0].ID))
for i := 1; i < len(ifaces)-1; i += 2 {
hops = append(hops, hop(ifaces[i].IA, ifaces[i].ID, ifaces[i+1].ID))
}
hops = append(hops, hop(ifaces[len(ifaces)-1].IA, ifaces[len(ifaces)-1].ID, 0))
desc = strings.Join(hops, " ")
}
return desc, nil
}
4 changes: 4 additions & 0 deletions scion/cmd/scion/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go_library(
name = "go_default_library",
srcs = [
"address.go",
"common.go",
"gendocs.go",
"main.go",
"observability.go",
Expand All @@ -18,6 +19,7 @@ go_library(
"//pkg/addr:go_default_library",
"//pkg/daemon:go_default_library",
"//pkg/log:go_default_library",
"//pkg/private/common:go_default_library",
"//pkg/private/serrors:go_default_library",
"//pkg/snet:go_default_library",
"//pkg/snet/addrutil:go_default_library",
Expand All @@ -28,6 +30,7 @@ go_library(
"//private/app/flag:go_default_library",
"//private/app/path:go_default_library",
"//private/env:go_default_library",
"//private/path/pathpol:go_default_library",
"//private/topology:go_default_library",
"//private/tracing:go_default_library",
"//scion/ping:go_default_library",
Expand All @@ -36,6 +39,7 @@ go_library(
"@com_github_opentracing_opentracing_go//:go_default_library",
"@com_github_spf13_cobra//:go_default_library",
"@com_github_spf13_cobra//doc:go_default_library",
"@in_gopkg_yaml_v2//:go_default_library",
],
)

Expand Down
102 changes: 102 additions & 0 deletions scion/cmd/scion/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2022 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 (
"encoding/json"
"fmt"
"io"
"math"
"net"
"time"

"github.com/scionproto/scion/pkg/addr"
"github.com/scionproto/scion/pkg/private/common"
"github.com/scionproto/scion/pkg/private/serrors"
"github.com/scionproto/scion/pkg/snet"
)

// Path defines the base model for the `ping` and `traceroute` result path
type Path struct {
// Hex-string representing the paths fingerprint.
Fingerprint string `json:"fingerprint" yaml:"fingerprint"`
Hops []Hop `json:"hops" yaml:"hops"`
Sequence string `json:"sequence" yaml:"sequence"`

LocalIP net.IP `json:"local_ip,omitempty" yaml:"local_ip,omitempty"`

// The internal UDP/IP underlay address of the SCION router that forwards traffic for this path.
NextHop string `json:"next_hop" yaml:"next_hop"`
}

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

// getHops constructs a list of snet path interfaces from an snet path
func getHops(path snet.Path) []Hop {
ifaces := path.Metadata().Interfaces
var hops []Hop
if len(ifaces) == 0 {
return hops
}
for i := range ifaces {
intf := ifaces[i]
hops = append(hops, Hop{IA: intf.IA, ID: intf.ID})
}
return hops
}

// getPrintf returns a printf function for the "human" formatting flag and an empty one for machine
// readable format flags
func getPrintf(output string, writer io.Writer) (func(format string, ctx ...interface{}), error) {
switch output {
case "human":
return func(format string, ctx ...interface{}) {
fmt.Fprintf(writer, format, ctx...)
}, nil
case "yaml", "json":
return func(format string, ctx ...interface{}) {}, nil
default:
return nil, serrors.New("format not supported", "format", output)
}
}

type durationMillis time.Duration

func (d durationMillis) String() string {
return fmt.Sprintf("%.3fms", d.Millis())
}

func (d durationMillis) MarshalJSON() ([]byte, error) {
return json.Marshal(d.MillisRounded())
}

func (d durationMillis) MarshalYAML() (interface{}, error) {
return d.MillisRounded(), nil
}

// millis returns the duration as a floating point number of milliseconds
func (d durationMillis) Millis() float64 {
return float64(d) / 1e6
}

// millisRounded returns the duration as a floating point number of
// milliseconds, rounded to microseconds (3 digits precision).
func (d durationMillis) MillisRounded() float64 {
return math.Round(float64(d)/1000) / 1000
}
Loading