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

feat: support configuring serial ports at the minimega host level #130

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
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 docker/Dockerfile.minimega
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ ARG PHENIX_REVISION=local-dev
LABEL gov.sandia.phenix.revision="${PHENIX_REVISION}"

# iptables needed in minimega container for scorch and tap apps
RUN apt update && apt install -y iptables \
# socat needed in minimega container for serial app
RUN apt update && apt install -y iptables socat \
&& apt autoremove -y \
&& apt clean -y \
&& rm -rf /var/lib/apt/lists/* \
Expand Down
25 changes: 25 additions & 0 deletions src/go/api/experiment/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ func init() {
return fmt.Errorf("initializing experiment: %w", err)
}

if common.BridgeMode == common.BRIDGE_MODE_AUTO {
if len(c.Metadata.Name) > 15 {
return fmt.Errorf("experiment name must be 15 characters or less when using auto bridge mode")
}

exp.Spec.SetDefaultBridge(c.Metadata.Name)
}

if len(exp.Spec.DefaultBridge()) > 15 {
return fmt.Errorf("default bridge name must be 15 characters or less")
}

exp.Spec.SetUseGREMesh(exp.Spec.UseGREMesh() || common.UseGREMesh)

existing, _ := types.Experiments(false)
Expand Down Expand Up @@ -100,6 +112,19 @@ func init() {
return fmt.Errorf("re-initializing experiment (after update): %w", err)
}

// Just in case the updated experiment reset the default bridge.
if common.BridgeMode == common.BRIDGE_MODE_AUTO {
if len(c.Metadata.Name) > 15 {
return fmt.Errorf("experiment name must be 15 characters or less when using auto bridge mode")
}

exp.Spec.SetDefaultBridge(c.Metadata.Name)
}

if len(exp.Spec.DefaultBridge()) > 15 {
return fmt.Errorf("default bridge name must be 15 characters or less")
}

exp.Spec.SetUseGREMesh(exp.Spec.UseGREMesh() || common.UseGREMesh)

existing, _ := types.Experiments(false)
Expand Down
35 changes: 34 additions & 1 deletion src/go/api/soh/soh.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"phenix/api/experiment"
"phenix/api/vm"
"phenix/app"

"github.com/mitchellh/mapstructure"
)
Expand All @@ -15,7 +16,10 @@ var vlanAliasRegex = regexp.MustCompile(`(.*) \(\d*\)`)

func Get(expName, statusFilter string) (*Network, error) {
// Create an empty network
network := new(Network)
network := &Network{
Nodes: []Node{},
Edges: []Edge{},
}

// Create structure to format nodes' font
font := Font{
Expand Down Expand Up @@ -69,13 +73,16 @@ func Get(expName, statusFilter string) (*Network, error) {
// Internally use to track connections, VM's state, and whether or not the
// VM is in minimega
var (
vmIDs = make(map[string]int)
interfaces = make(map[string]int)
ifaceCount = len(vms) + 1
edgeCount int
)

// Traverse the experiment VMs and create topology
for _, vm := range vms {
vmIDs[vm.Name] = vm.ID

var vmState string

/*
Expand Down Expand Up @@ -164,6 +171,32 @@ func Get(expName, statusFilter string) (*Network, error) {
}
}

// Check to see if a scenario exists for this experiment and if it contains a
// "serial" app. If so, add edges for all the serial connections.
for _, a := range exp.Apps() {
if a.Name() == "serial" {
var config app.SerialConfig

if err := a.ParseMetadata(&config); err != nil {
continue // TODO: handle this better? Like warn the user perhaps?
}

for _, conn := range config.Connections {
// create edge for serial connection
edge := Edge{
ID: edgeCount,
Source: vmIDs[conn.Src],
Target: vmIDs[conn.Dst],
Length: 150,
Type: "serial",
}

network.Edges = append(network.Edges, edge)
edgeCount++
}
}
}

return network, err
}

Expand Down
9 changes: 5 additions & 4 deletions src/go/api/soh/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ type Node struct {
}

type Edge struct {
ID int `json:"id"`
Source int `json:"source"`
Target int `json:"target"`
Length int `json:"length"`
ID int `json:"id"`
Type string `json:"type"`
Source int `json:"source"`
Target int `json:"target"`
Length int `json:"length"`
}

type Network struct {
Expand Down
124 changes: 124 additions & 0 deletions src/go/app/serial.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,27 @@ import (
"phenix/tmpl"
"phenix/types"
ifaces "phenix/types/interfaces"
"phenix/util/mm"
)

var (
idFormat = "%s_serial_%s_%d"
lfFormat = "/tmp/%s_serial_%s_%s_%d.log"
optFormat = "-chardev socket,id=%[1]s,path=/tmp/%[1]s,server,nowait -device pci-serial,chardev=%[1]s"

defaultStartPort = 40500
)

type SerialConfig struct {
Connections []SerialConnectionConfig `mapstructure:"connections"`
}

type SerialConnectionConfig struct {
Src string `mapstructure:"src"`
Dst string `mapstructure:"dst"`
Port int `mapstructure:"port"`
}

type Serial struct{}

func (Serial) Init(...Option) error {
Expand Down Expand Up @@ -121,10 +140,91 @@ func (Serial) PreStart(ctx context.Context, exp *types.Experiment) error {
}
}

// Check to see if a scenario exists for this experiment and if it contains a
// "serial" app. If so, configure serial ports according to the app config.
for _, app := range exp.Apps() {
if app.Name() == "serial" {
var config SerialConfig

if err := app.ParseMetadata(&config); err != nil {
continue // TODO: handle this better? Like warn the user perhaps?
}

for i, conn := range config.Connections {
src := exp.Spec.Topology().FindNodeByName(conn.Src)

if src == nil {
continue // TODO: handle this better? Like warn the user perhaps?
}

appendQEMUFlags(exp.Metadata.Name, src, i)

dst := exp.Spec.Topology().FindNodeByName(conn.Dst)

if src == nil {
continue // TODO: handle this better? Like warn the user perhaps?
}

appendQEMUFlags(exp.Metadata.Name, dst, i)
}
}
}

return nil
}

func (Serial) PostStart(ctx context.Context, exp *types.Experiment) error {
// Check to see if a scenario exists for this experiment and if it contains a
// "serial" app. If so, configure serial ports according to the app config.
for _, app := range exp.Apps() {
if app.Name() == "serial" {
var (
schedule = exp.Status.Schedules()
config SerialConfig
)

if err := app.ParseMetadata(&config); err != nil {
continue // TODO: handle this better? Like warn the user perhaps?
}

for i, conn := range config.Connections {
var (
logFile = fmt.Sprintf(lfFormat, exp.Metadata.Name, conn.Src, conn.Dst, i)
srcID = fmt.Sprintf(idFormat, exp.Metadata.Name, conn.Src, i)
dstID = fmt.Sprintf(idFormat, exp.Metadata.Name, conn.Dst, i)
srcHost = schedule[conn.Src]
dstHost = schedule[conn.Dst]
)

if srcHost == dstHost { // single socat process on host connecting unix sockets
socat := fmt.Sprintf("socat -lf%s -d -d -d -d UNIX-CONNECT:/tmp/%s UNIX-CONNECT:/tmp/%s", logFile, srcID, dstID)

if err := mm.MeshBackground(srcHost, socat); err != nil {
return fmt.Errorf("starting socat on %s: %w", srcHost, err)
}
} else { // single socat process on each host connected via TCP
port := conn.Port

if port == 0 {
port = defaultStartPort + i
}

srcSocat := fmt.Sprintf("socat -lf%s -d -d -d -d UNIX-CONNECT:/tmp/%s TCP-LISTEN:%d", logFile, srcID, port)

if err := mm.MeshBackground(srcHost, srcSocat); err != nil {
return fmt.Errorf("starting socat on %s: %w", srcHost, err)
}

dstSocat := fmt.Sprintf("socat -lf%s -d -d -d -d UNIX-CONNECT:/tmp/%s TCP-CONNECT:%s:%d", logFile, dstID, srcHost, port)

if err := mm.MeshBackground(dstHost, dstSocat); err != nil {
return fmt.Errorf("starting socat on %s: %w", dstHost, err)
}
}
}
}
}

return nil
}

Expand All @@ -135,3 +235,27 @@ func (Serial) Running(ctx context.Context, exp *types.Experiment) error {
func (Serial) Cleanup(ctx context.Context, exp *types.Experiment) error {
return nil
}

func appendQEMUFlags(exp string, node ifaces.NodeSpec, idx int) error {
var (
id = fmt.Sprintf(idFormat, exp, node.General().Hostname(), idx)
options = fmt.Sprintf(optFormat, id)
)

var qemuAppend []string

if advanced := node.Advanced(); advanced != nil {
if v, ok := advanced["qemu-append"]; ok {
if strings.Contains(v, options) {
return nil
}

qemuAppend = []string{v}
}
}

qemuAppend = append(qemuAppend, options)
node.AddAdvanced("qemu-append", strings.Join(qemuAppend, " "))

return nil
}
4 changes: 2 additions & 2 deletions src/go/app/tap.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ func (this *Tap) PostStart(ctx context.Context, exp *types.Experiment) error {
return fmt.Errorf("decoding %s app metadata: %w", this.Name(), err)
}

hosts, err := mm.GetClusterHosts(true)
hosts, err := mm.GetNamespaceHosts(exp.Metadata.Name)
if err != nil {
return fmt.Errorf("getting list of cluster hosts: %w", err)
return fmt.Errorf("getting list of experiment hosts: %w", err)
}

rand.Seed(time.Now().UnixNano())
Expand Down
8 changes: 7 additions & 1 deletion src/go/app/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,14 @@ func (this UserApp) shellOut(ctx context.Context, action Action, exp *types.Expe
}

switch action {
case ACTIONCONFIG, ACTIONPRESTART:
case ACTIONCONFIG:
exp.SetSpec(result.Spec)
case ACTIONPRESTART:
exp.SetSpec(result.Spec)

if metadata, ok := result.Status.AppStatus()[this.options.Name]; ok {
exp.Status.SetAppStatus(this.options.Name, metadata)
}
case ACTIONPOSTSTART, ACTIONRUNNING:
if metadata, ok := result.Status.AppStatus()[this.options.Name]; ok {
exp.Status.SetAppStatus(this.options.Name, metadata)
Expand Down
25 changes: 21 additions & 4 deletions src/go/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,15 @@ var rootCmd = &cobra.Command{
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
common.UnixSocket = viper.GetString("unix-socket")

// Initialize use GRE mesh with option set locally by user. Later it will be
// forcefully enabled if it's enabled at the server. This must be done
// before getting options from the server (unlike deploy mode option).
// Initialize bridge mode and use GRE mesh options with values set locally
// by user. Later they will be forcefully enabled if they're enabled at the
// server. This must be done before getting options from the server (unlike
// deploy mode option).

if err := common.SetBridgeMode(viper.GetString("bridge-mode")); err != nil {
return fmt.Errorf("setting user-specified bridge mode: %w", err)
}

common.UseGREMesh = viper.GetBool("use-gre-mesh")

// check for global options set by UI server
Expand All @@ -61,7 +67,17 @@ var rootCmd = &cobra.Command{
var options map[string]any
json.Unmarshal(body, &options)

mode, _ := options["deploy-mode"].(string)
mode, _ := options["bridge-mode"].(string)

// Only override value locally set by user (above) if auto mode is set
// on the server.
if mode == string(common.BRIDGE_MODE_AUTO) {
if err := common.SetBridgeMode(mode); err != nil {
return fmt.Errorf("setting server-specified bridge mode: %w", err)
}
}

mode, _ = options["deploy-mode"].(string)
if err := common.SetDeployMode(mode); err != nil {
return fmt.Errorf("setting server-specified deploy mode: %w", err)
}
Expand Down Expand Up @@ -178,6 +194,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&hostnameSuffixes, "hostname-suffixes", "-minimega,-phenix", "hostname suffixes to strip")
rootCmd.PersistentFlags().Bool("log.error-stderr", true, "log fatal errors to STDERR")
rootCmd.PersistentFlags().String("log.level", "info", "level to log messages at")
rootCmd.PersistentFlags().String("bridge-mode", "", "bridge naming mode for experiments ('auto' uses experiment name for bridge; 'manual' uses user-specified bridge name, or 'phenix' if not specified) (options: manual | auto)")
rootCmd.PersistentFlags().String("deploy-mode", "", "deploy mode for minimega VMs (options: all | no-headnode | only-headnode)")
rootCmd.PersistentFlags().Bool("use-gre-mesh", false, "use GRE tunnels between mesh nodes for VLAN trunking")
rootCmd.PersistentFlags().String("unix-socket", "/tmp/phenix.sock", "phēnix unix socket to listen on (ui subcommand) or connect to")
Expand Down
Loading