Skip to content

Commit

Permalink
feat: new deploy mode option for CLI and UI
Browse files Browse the repository at this point in the history
This commit replaces the previous `runLocal` setting for experiments
with a new `deployMode` setting that can be set via the CLI and the UI
when creating new experiments.

The value of this new setting must be one of `all`, `no-headnode`, or
`only-headnode`, with the default being `no-headnode` to preserve the
previous default value of `runLocal: false`.

The overall goal of this commit is to support the new option of `all`,
which allows experiment VMs to run on the head node as well as all the
other compute nodes. The previous `runLocal` setting only supported the
current `no-headnode` and `only-headnode` options.
  • Loading branch information
Casey Glatter authored and activeshadow committed Feb 5, 2024
1 parent a771588 commit 796e603
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 47 deletions.
3 changes: 2 additions & 1 deletion src/go/api/experiment/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,10 @@ func Create(ctx context.Context, opts ...CreateOption) error {
},
}

specMap := map[string]interface{}{
specMap := map[string]any{
"experimentName": o.name,
"baseDir": o.baseDir,
"deployMode": o.deployMode,
"topology": topo,
}

Expand Down
11 changes: 10 additions & 1 deletion src/go/api/experiment/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ type createOptions struct {
vlanAliases map[string]int
schedules map[string]string
baseDir string
deployMode common.DeploymentMode
}

func newCreateOptions(opts ...CreateOption) createOptions {
var o createOptions
o := createOptions{
deployMode: common.DeployMode,
}

for _, opt := range opts {
opt(&o)
Expand Down Expand Up @@ -94,6 +97,12 @@ func CreateWithBaseDirectory(b string) CreateOption {
}
}

func CreateWithDeployMode(m common.DeploymentMode) CreateOption {
return func(o *createOptions) {
o.deployMode = m
}
}

type SaveOption func(*saveOptions)

type saveOptions struct {
Expand Down
22 changes: 11 additions & 11 deletions src/go/cmd/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func newExperimentDeleteCmd() *cobra.Command {
desc := `Delete an experiment
Used to delete an exisitng experiment; experiment must be stopped.
Using 'all' instead of a specific experiment name will include all
Using 'all' instead of a specific experiment name will include all
stopped experiments`

cmd := &cobra.Command{
Expand Down Expand Up @@ -301,8 +301,8 @@ func newExperimentDeleteCmd() *cobra.Command {

func newExperimentScheduleCmd() *cobra.Command {
desc := `Schedule an experiment
Apply an algorithm to a given experiment. Run 'phenix experiment schedulers'
Apply an algorithm to a given experiment. Run 'phenix experiment schedulers'
to return a list of algorithms`

cmd := &cobra.Command{
Expand Down Expand Up @@ -333,10 +333,10 @@ func newExperimentScheduleCmd() *cobra.Command {
func newExperimentStartCmd() *cobra.Command {
desc := `Start an experiment
Used to start a stopped experiment, using 'all' instead of a specific
experiment name will include all stopped experiments; dry-run will do
Used to start a stopped experiment, using 'all' instead of a specific
experiment name will include all stopped experiments; dry-run will do
everything but call out to minimega.
NOTE: passing the --honor-run-periodically flag will prevent the CLI from
returning. If Ctrl+c is pressed, the experiment will continue to run but
the running stage will no longer continue to be triggered for any apps
Expand Down Expand Up @@ -398,7 +398,7 @@ func newExperimentStartCmd() *cobra.Command {

notes.PrettyPrint(ctx, false)

plog.Info("experiment started", "exp", exp.Metadata.Name, "dryrun", dryrun)
plog.Info("experiment started", "exp", exp.Metadata.Name, "dryrun", dryrun, "deploy-mode", exp.Spec.DeployMode())

if periodic {
plog.Info("honor-run-periodically flag was passed")
Expand Down Expand Up @@ -431,7 +431,7 @@ func newExperimentStartCmd() *cobra.Command {
func newExperimentStopCmd() *cobra.Command {
desc := `Stop an experiment
Used to stop a running experiment, using 'all' instead of a specific
Used to stop a running experiment, using 'all' instead of a specific
experiment name will include all running experiments.`

cmd := &cobra.Command{
Expand Down Expand Up @@ -487,8 +487,8 @@ func newExperimentStopCmd() *cobra.Command {
func newExperimentRestartCmd() *cobra.Command {
desc := `Restart an experiment
Used to restart a running experiment, using 'all' instead of a specific
experiment name will include all running experiments; dry-run will do
Used to restart a running experiment, using 'all' instead of a specific
experiment name will include all running experiments; dry-run will do
everything but call out to minimega.`

cmd := &cobra.Command{
Expand Down Expand Up @@ -539,7 +539,7 @@ func newExperimentRestartCmd() *cobra.Command {
return err.Humanized()
}

plog.Info("experiment restarted", "exp", exp.Metadata.Name)
plog.Info("experiment restarted", "exp", exp.Metadata.Name, "dryrun", dryrun, "deploy-mode", exp.Spec.DeployMode())
}

return nil
Expand Down
52 changes: 48 additions & 4 deletions src/go/cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/user"
"path/filepath"
Expand Down Expand Up @@ -32,20 +37,57 @@ var rootCmd = &cobra.Command{
Use: "phenix",
Short: "A cli application for phēnix",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
common.UnixSocket = viper.GetString("unix-socket")

// check for global options set by UI server
if common.UnixSocket != "" {
cli := http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", common.UnixSocket)
},
},
}

if resp, err := cli.Get("http://unix/api/v1/options"); err == nil {
defer resp.Body.Close()

if body, err := io.ReadAll(resp.Body); err == nil {
var options map[string]any
json.Unmarshal(body, &options)

if mode, _ := options["deploy-mode"].(string); mode != "" {
if deployMode, err := common.ParseDeployMode(mode); err == nil {
common.DeployMode = deployMode
}
}
}
}
}

plog.NewPhenixHandler()
plog.SetLevelText(viper.GetString("log.level"))

common.PhenixBase = viper.GetString("base-dir.phenix")
common.MinimegaBase = viper.GetString("base-dir.minimega")
common.HostnameSuffixes = viper.GetString("hostname-suffixes")

// if deploy mode option is set locally by user, use it instead of global from UI
if opt := viper.GetString("deploy-mode"); opt != "" {
mode, err := common.ParseDeployMode(opt)
if err != nil {
return fmt.Errorf("parsing deploy mode: %w", err)
}

common.DeployMode = mode
}

var (
endpoint = viper.GetString("store.endpoint")
errFile = viper.GetString("log.error-file")
errOut = viper.GetBool("log.error-stderr")
logLevel = viper.GetString("log.level")
)

plog.NewPhenixHandler()
plog.SetLevelText(logLevel)

common.ErrorFile = errFile
common.StoreEndpoint = endpoint

Expand Down Expand Up @@ -131,6 +173,8 @@ 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("deploy-mode", "", "deploy mode for minimega VMs (options: all | no-headnode | only-headnode)")
rootCmd.PersistentFlags().String("unix-socket", "/tmp/phenix.sock", "phēnix unix socket to listen on (ui subcommand) or connect to")

if uid == "0" {
os.MkdirAll("/etc/phenix", 0755)
Expand Down
10 changes: 8 additions & 2 deletions src/go/cmd/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"phenix/util"
"phenix/util/common"
"phenix/util/plog"
"phenix/web"

Expand All @@ -28,7 +29,6 @@ func newUICmd() *cobra.Command {

opts := []web.ServerOption{
web.ServeOnEndpoint(viper.GetString("ui.listen-endpoint")),
web.ServeOnUnixSocket(viper.GetString("ui.unix-socket-endpoint")),
web.ServeBasePath(viper.GetString("ui.base-path")),
web.ServeWithJWTKey(viper.GetString("ui.jwt-signing-key")),
web.ServeWithJWTLifetime(viper.GetDuration("ui.jwt-lifetime")),
Expand All @@ -39,6 +39,12 @@ func newUICmd() *cobra.Command {
web.ServeWithProxyAuthHeader(viper.GetString("ui.proxy-auth-header")),
}

if endpoint := viper.GetString("ui.unix-socket-endpoint"); endpoint != "" {
plog.Warn("The --ui.unix-socket-endpoint option for the ui subcommand is DEPRECATED. Use the root phenix --unix-socket option instead.")

common.UnixSocket = endpoint
}

if viper.GetString("ui.log-level") != "" {
plog.Warn("The --log-level option for the ui subcommand is DEPRECATED. Use the root phenix --log.level option instead.")
}
Expand Down Expand Up @@ -88,7 +94,7 @@ func newUICmd() *cobra.Command {
}

cmd.Flags().StringP("listen-endpoint", "e", "0.0.0.0:3000", "endpoint to listen on")
cmd.Flags().String("unix-socket-endpoint", "", "unix socket path to listen on (no auth, only exposes workflow API)")
cmd.Flags().String("unix-socket-endpoint", "", "unix socket path to listen on - DEPRECATED (use root --unix-socket option instead)")
cmd.Flags().StringP("base-path", "b", "/", "base path to use for UI (must run behind proxy if not '/')")
cmd.Flags().StringP("jwt-signing-key", "k", "", "Secret key used to sign JWT for authentication")
cmd.Flags().Duration("jwt-lifetime", 24*time.Hour, "Lifetime of JWT authentication tokens")
Expand Down
4 changes: 3 additions & 1 deletion src/go/tmpl/templates/minimega_script.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ vlans add {{ $alias }} {{ $id }}
{{- end }}
{{- end }}

{{- if .RunLocal }}
{{- if eq .DeployMode "all" }}
ns add-host localhost
{{- else if eq .DeployMode "only-headnode" }}
ns del-host all
ns add-host localhost
{{- end }}
Expand Down
3 changes: 2 additions & 1 deletion src/go/types/interfaces/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type ExperimentSpec interface {
Scenario() ScenarioSpec
VLANs() VLANSpec
Schedules() map[string]string
RunLocal() bool
DeployMode() string

SetExperimentName(string)
SetBaseDir(string)
Expand All @@ -32,6 +32,7 @@ type ExperimentSpec interface {
SetSchedule(map[string]string)
SetTopology(TopologySpec)
SetScenario(ScenarioSpec)
SetDeployMode(string)

VerifyScenario(context.Context) error
ScheduleNode(string, string) error
Expand Down
10 changes: 7 additions & 3 deletions src/go/types/version/v1/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ type ExperimentSpec struct {
ScenarioF *v2.ScenarioSpec `json:"scenario" yaml:"scenario" structs:"scenario" mapstructure:"scenario"`
VLANsF *VLANSpec `json:"vlans" yaml:"vlans" structs:"vlans" mapstructure:"vlans"`
SchedulesF map[string]string `json:"schedules" yaml:"schedules" structs:"schedules" mapstructure:"schedules"`
RunLocalF bool `json:"runLocal" yaml:"runLocal" structs:"runLocal" mapstructure:"runLocal"`
DeployModeF string `json:"deployMode" yaml:"deployMode" structs:"deployMode" mapstructure:"deployMode"`
}

func (this *ExperimentSpec) Init() error {
Expand Down Expand Up @@ -167,8 +167,12 @@ func (this ExperimentSpec) Schedules() map[string]string {
return this.SchedulesF
}

func (this ExperimentSpec) RunLocal() bool {
return this.RunLocalF
func (this ExperimentSpec) DeployMode() string {
return this.DeployModeF
}

func (this *ExperimentSpec) SetDeployMode(mode string) {
this.DeployModeF = mode
}

func (this *ExperimentSpec) SetExperimentName(name string) {
Expand Down
30 changes: 28 additions & 2 deletions src/go/util/common/common.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
package common

import (
"fmt"
"strings"
)

type DeploymentMode string

const (
DEPLOY_MODE_UNSET DeploymentMode = ""
DEPLOY_MODE_NO_HEADNODE DeploymentMode = "no-headnode"
DEPLOY_MODE_ONLY_HEADNODE DeploymentMode = "only-headnode"
DEPLOY_MODE_ALL DeploymentMode = "all"
)

var (
PhenixBase = "/phenix"
MinimegaBase = "/tmp/minimega"

LogFile = "/var/log/phenix/phenix.log"
ErrorFile = "/var/log/phenix/error.log"
DeployMode = DEPLOY_MODE_NO_HEADNODE

LogFile = "/var/log/phenix/phenix.log"
ErrorFile = "/var/log/phenix/error.log"
UnixSocket = "/tmp/phenix.sock"

StoreEndpoint string
HostnameSuffixes string
Expand All @@ -22,3 +35,16 @@ func TrimHostnameSuffixes(str string) string {

return str
}

func ParseDeployMode(mode string) (DeploymentMode, error) {
switch strings.ToLower(mode) {
case "no-headnode":
return DEPLOY_MODE_NO_HEADNODE, nil
case "only-headnode":
return DEPLOY_MODE_ONLY_HEADNODE, nil
case "all":
return DEPLOY_MODE_ALL, nil
}

return DEPLOY_MODE_UNSET, fmt.Errorf("unknown deploy mode provided: %s", mode)
}
8 changes: 8 additions & 0 deletions src/go/web/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"phenix/api/vm"
"phenix/app"
"phenix/store"
"phenix/util/common"
"phenix/util/mm"
"phenix/util/notes"
"phenix/util/plog"
Expand Down Expand Up @@ -171,13 +172,20 @@ func CreateExperiment(w http.ResponseWriter, r *http.Request) {

defer cache.UnlockExperiment(req.Name)

deployMode, err := common.ParseDeployMode(req.DeployMode)
if err != nil {
plog.Warn("error parsing experiment deploy mode ('%s') - using default of '%s'", req.DeployMode, common.DeployMode)
deployMode = common.DeployMode
}

opts := []experiment.CreateOption{
experiment.CreateWithName(req.Name),
experiment.CreateWithTopology(req.Topology),
experiment.CreateWithScenario(req.Scenario),
experiment.CreateWithVLANMin(int(req.VlanMin)),
experiment.CreateWithVLANMax(int(req.VlanMax)),
experiment.CreatedWithDisabledApplications(req.DisabledApps),
experiment.CreateWithDeployMode(deployMode),
}

if req.WorkflowBranch != "" {
Expand Down
Loading

0 comments on commit 796e603

Please sign in to comment.