From b1fd227c1bd3c85f99646595611cefdb50e15aac Mon Sep 17 00:00:00 2001 From: Chris Prather Date: Wed, 18 Nov 2020 21:09:51 -0500 Subject: [PATCH] restore admin command tree This restores the admin command tree. Once again you're able to: * list all users * view a specific user * add/remove users * update a specific user's details * list a specific user's tokens * remove a specific user's tokens --- cli/admin.go | 157 +++++++++++++++++++++++++++++++++++++++ cli/builds.go | 64 +++++++--------- cli/cli.go | 27 ++++--- cli/config.go | 19 +---- cli/datacenters.go | 49 ++++++------ cli/device_reports.go | 4 +- cli/devices.go | 13 ++-- cli/hardware.go | 25 +++---- cli/organizations.go | 6 +- cli/racks.go | 94 +++++++++++------------ cli/relays.go | 4 +- cli/roles.go | 51 ++++++------- cli/rooms.go | 65 ++++++++-------- cli/users.go | 20 +++-- cli/validations.go | 6 +- conch/racks.go | 2 +- conch/types/responses.go | 22 +++--- conch/types/templates.go | 34 +++++++++ conch/users.go | 14 +++- template/template.go | 10 ++- 20 files changed, 425 insertions(+), 261 deletions(-) create mode 100644 cli/admin.go diff --git a/cli/admin.go b/cli/admin.go new file mode 100644 index 0000000..e9720ca --- /dev/null +++ b/cli/admin.go @@ -0,0 +1,157 @@ +package cli + +import ( + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/conch/types" +) + +func adminCmd(cmd *cli.Cmd) { + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + } + cmd.Command("users", "List all Users", adminUsersCmd) + cmd.Command("user u", "Administrate a single User", adminUserCmd) +} + +func adminUsersCmd(cmd *cli.Cmd) { + var conch *conch.Client + var display Renderer + + cmd.Before = func() { + conch = config.ConchClient() + display = config.Renderer() + } + + cmd.Action = func() { display(conch.GetAllUsers()) } + + cmd.Command("get ls", "display all users", func(cmd *cli.Cmd) { + cmd.Action = func() { display(conch.GetAllUsers()) } + }) + + cmd.Command("create new add", "Add a new user to the system", func(cmd *cli.Cmd) { + email := cmd.StringArg("EMAIL", "", "A user's email") + name := cmd.StringArg("NAME", "", "A user's name") + password := cmd.StringArg("PASS", "", "A user's initial password") + admin := cmd.BoolOpt("admin", false, "make user a system admin") + notify := cmd.BoolOpt("send-email", false, "notify the user via email") + + cmd.Spec = "EMAIL NAME PASS" + + cmd.Action = func() { + display(conch.CreateUser(types.NewUser{ + Email: types.EmailAddress(*email), + IsAdmin: *admin, + Name: types.NonEmptyString(*name), + Password: types.NonEmptyString(*password), + }, *notify)) + } + }) + + cmd.Command("import", "Import a new user from the JSON output", func(cmd *cli.Cmd) { + notify := cmd.BoolOpt("send-email", false, "notify the user via email") + filePathArg := cmd.StringArg("FILE", "-", "Path to a JSON file that defines the user. '-' indicates STDIN") + + cmd.Action = func() { + input, e := getInputReader(*filePathArg) + fatalIf(e) + + u := conch.ReadUser(input) + display( + conch.CreateUser(types.NewUser{ + Email: u.Email, + IsAdmin: u.IsAdmin, + Name: u.Name, + }, *notify), + ) + } + }) +} + +func adminUserCmd(cmd *cli.Cmd) { + var conch *conch.Client + var display Renderer + var user types.UserDetailed + + email := cmd.StringArg("EMAIL", "", "A user's email") + // cmd.Spec = "EMAIL" + + cmd.Before = func() { + conch = config.ConchClient() + display = config.Renderer() + + var e error + user, e = conch.GetUserByEmail(*email) + fatalIf(e) + // TODO check to see if user is empty + } + + cmd.Action = func() { display(user, nil) } + + cmd.Command("get", "display all users", func(cmd *cli.Cmd) { + cmd.Action = func() { display(user, nil) } + }) + + cmd.Command("update", "update the information for a user", func(cmd *cli.Cmd) { + email := cmd.StringArg("EMAIL", "", "A user's email") + name := cmd.StringArg("NAME", "", "A user's name") + admin := cmd.BoolOpt("admin", false, "make user a system admin") + notify := cmd.BoolOpt("send-email", false, "notify the user via email") + + cmd.Spec = "EMAIL NAME" + cmd.Action = func() { + update := types.UpdateUser{} + if *email != "" && types.EmailAddress(*email) != user.Email { + update.Email = types.EmailAddress(*email) + } + if *name != "" && types.NonEmptyString(*name) != user.Name { + update.Name = types.NonEmptyString(*name) + } + if *admin != user.IsAdmin { + user.IsAdmin = *admin + } + conch.UpdateUser(string(user.Email), update, *notify) + } + }) + + cmd.Command("delete rm", "remove the specified user", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteUser(string(user.Email)) + display(conch.GetAllUsers()) + } + }) + + cmd.Command("tokens", "operate on the user's tokens", func(cmd *cli.Cmd) { + cmd.Action = func() { display(conch.GetUserTokens(string(user.Email))) } + + cmd.Command("get ls", "list the tokens for the current user", func(cmd *cli.Cmd) { + cmd.Action = func() { display(conch.GetUserTokens(string(user.Email))) } + }) + }) + + cmd.Command("token", "operate on a user's tokens", func(cmd *cli.Cmd) { + var token types.UserToken + + name := cmd.StringArg("NAME", "", "The string name of a setting") + cmd.Spec = "NAME" + cmd.Before = func() { + var e error + token, e = conch.GetUserTokenByName(string(user.Email), *name) + fatalIf(e) + } + + cmd.Action = func() { display(token, nil) } + + cmd.Command("get", "information about a single token for the given user", func(cmd *cli.Cmd) { + cmd.Action = func() { display(token, nil) } + }) + + cmd.Command("delete rm", "remove a token for the given user", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteUserToken(string(user.Email), token.Name) + display(conch.GetUserTokens(string(user.Email))) + } + }) + }) +} diff --git a/cli/builds.go b/cli/builds.go index 5284670..a72ee62 100644 --- a/cli/builds.go +++ b/cli/builds.go @@ -29,13 +29,11 @@ func buildsCmd(cmd *cli.Cmd) { var conch *conch.Client var display func(interface{}, error) - cmd.Before = config.Before( - requireAuth, - func(config Config) { - conch = config.ConchClient() - display = config.Renderer() - }, - ) + cmd.Before = func() { + config.requireAuth() + conch = config.ConchClient() + display = config.Renderer() + } var startedSetByUser bool var completedSetByUser bool @@ -112,19 +110,16 @@ func buildCmd(cmd *cli.Cmd) { buildNameArg := cmd.StringArg("NAME", "", "Name or ID of the build") cmd.Spec = "NAME" - cmd.Before = config.Before( - requireAuth, - func(config Config) { - conch = config.ConchClient() - display = config.Renderer() + cmd.Before = func() { + config.requireAuth() - var e error - build, e = conch.GetBuildByName(*buildNameArg) - if e != nil { - fatal(e) - } - }, - ) + conch = config.ConchClient() + display = config.Renderer() + + var e error + build, e = conch.GetBuildByName(*buildNameArg) + fatalIf(e) + } cmd.Action = func() { display(build, nil) } @@ -134,11 +129,10 @@ func buildCmd(cmd *cli.Cmd) { cmd.Command("start", "Mark the build as started", func(cmd *cli.Cmd) { cmd.Action = func() { - if e := conch.UpdateBuildByID(build.ID, types.BuildUpdate{ + e := conch.UpdateBuildByID(build.ID, types.BuildUpdate{ Started: time.Now(), - }); e != nil { - fatal(e) - } + }) + fatalIf(e) display(conch.GetBuildByID(build.ID)) } }) @@ -147,9 +141,9 @@ func buildCmd(cmd *cli.Cmd) { cmd.Action = func() { update := types.BuildUpdate{Completed: time.Now()} - if e := conch.UpdateBuildByID(build.ID, update); e != nil { - fatal(e) - } + e := conch.UpdateBuildByID(build.ID, update) + fatalIf(e) + display(conch.GetBuildByID(build.ID)) } }) @@ -186,7 +180,7 @@ func buildCmd(cmd *cli.Cmd) { cmd.Spec = "EMAIL [OPTIONS]" cmd.Action = func() { if !okBuildRole(*roleOpt) { - fatal(fmt.Errorf( + fatalIf(fmt.Errorf( "'role' value must be one of: %s", prettyBuildRoleList(), )) @@ -252,15 +246,13 @@ func buildCmd(cmd *cli.Cmd) { cmd.Spec = "NAME [OPTIONS]" cmd.Action = func() { if !okBuildRole(*roleOpt) { - fatal(fmt.Errorf( + fatalIf(fmt.Errorf( "'role' value must be one of: %s", prettyBuildRoleList(), )) } org, e := conch.GetOrganizationByName(*orgNameArg) - if e != nil { - fatal(e) - } + fatalIf(e) conch.AddBuildOrganization(*buildNameArg, types.BuildAddOrganization{ org.ID, @@ -327,13 +319,11 @@ func buildCmd(cmd *cli.Cmd) { cmd.Spec = "ID [OPTIONS]" cmd.Action = func() { b, e := conch.GetBuildByName(*buildNameArg) - if e != nil { - fatal(e) - } + fatalIf(e) + d, e := conch.GetDeviceBySerial(*deviceIDArg) - if e != nil { - fatal(e) - } + fatalIf(e) + conch.DeleteBuildDeviceByID(b.ID, d.ID) display(conch.GetAllBuildDevices(*buildNameArg)) } diff --git a/cli/cli.go b/cli/cli.go index c01213c..a4cfbc7 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -14,9 +14,11 @@ const ( stagingURL = "https://staging.conch.joyent.us" ) -func fatal(e error) { - fmt.Println(e) - cli.Exit(1) +func fatalIf(e error) { + if e != nil { + fmt.Println(e) + cli.Exit(1) + } } func getInputReader(filePathArg string) (io.Reader, error) { @@ -26,24 +28,26 @@ func getInputReader(filePathArg string) (io.Reader, error) { return os.Open(filePathArg) } -func requireAuth(c Config) { +var config Config + +func (c Config) requireAuth() { if c.ConchToken == "" { fmt.Println("Need to provide --token or set KOSH_TOKEN") cli.Exit(1) } } -func requireSysAdmin(c Config) { +func (c Config) requireSysAdmin() { if !c.ConchClient().IsSysAdmin() { fmt.Println("This action requires Conch systems administrator privileges") cli.Exit(1) } } -var config Config - // NewApp creates a new kosh app, takes a cli.Config and returns an instance of cli.Cli -func NewApp(config Config) *cli.Cli { +func NewApp(c Config) *cli.Cli { + config = c + app := cli.App("kosh", "Command line interface for Conch") app.Spec = "[-dejutvV]" @@ -80,17 +84,18 @@ func NewApp(config Config) *cli.Cli { app.BoolPtr(&config.Logger.LevelDebug, cli.BoolOpt{ Name: "d debug", Value: false, - Desc: "Enable Debugging output (for debugging purposes *very* noisy). ", + Desc: "Enable Debugging output (*very* noisy). ", EnvVar: "KOSH_DEBUG_MODE KOSH_DEBUG", // TODO in 4.0 remove KOSH_DEBUG_MODE }) app.BoolPtr(&config.Logger.LevelInfo, cli.BoolOpt{ Name: "v verbose", Value: false, - Desc: "Enable Verbose Output", + Desc: "Enable Verbose output", EnvVar: "KOSH_VERBOSE_MODE KOSH_VERBOSE", // TODO in 4.0 remove KOSH_VERBOSE_MODE }) + app.Command("admin", "System Administration Commands", adminCmd) app.Command("build b", "Work with a specific build", buildCmd) app.Command("builds bs", "Work with builds", buildsCmd) app.Command("datacenter dc", "Deal with a single datacenter", datacenterCmd) @@ -135,7 +140,7 @@ func NewApp(config Config) *cli.Cli { case "staging": config.ConchURL = stagingURL default: - fatal(errors.New("environment not one of production, staging, edge: perhaps you want --url?")) + fatalIf(errors.New("environment not one of production, staging, edge: perhaps you want --url?")) } } diff --git a/cli/config.go b/cli/config.go index a6dce4c..584dfae 100644 --- a/cli/config.go +++ b/cli/config.go @@ -91,21 +91,9 @@ func (c Config) Renderer() Renderer { return c.RenderTo(os.Stdout) } -// Before takes a list of checks and initializers and returns a function -// suitable for running in a commmand's before block -func (c Config) Before(checks ...func(c Config)) func() { - return func() { - for _, check := range checks { - check(config) - } - } -} - func renderJSON(i interface{}) string { b, e := json.Marshal(i) - if e != nil { - fatal(e) - } + fatalIf(e) return string(b) } @@ -124,9 +112,8 @@ func (c Config) RenderTo(w io.Writer) func(interface{}, error) { switch t := i.(type) { case template.Templated: s, e := template.Render(t) - if e != nil { - fatal(e) - } + fatalIf(e) + fmt.Fprintln(w, s) case tables.Tabulable: fmt.Fprintln(w, tables.Render(t)) diff --git a/cli/datacenters.go b/cli/datacenters.go index cfa59a9..e4729d5 100644 --- a/cli/datacenters.go +++ b/cli/datacenters.go @@ -13,14 +13,13 @@ func datacentersCmd(cmd *cli.Cmd) { var conch *conch.Client var display func(interface{}, error) - cmd.Before = config.Before( - requireAuth, - requireSysAdmin, - func(config Config) { - conch = config.ConchClient() - display = config.Renderer() - }, - ) + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + + conch = config.ConchClient() + display = config.Renderer() + } cmd.Command("get", "Get a list of all datacenters", func(cmd *cli.Cmd) { cmd.Action = func() { @@ -76,23 +75,21 @@ func datacenterCmd(cmd *cli.Cmd) { ) cmd.Spec = "UUID" - cmd.Before = config.Before( - requireAuth, - requireSysAdmin, - func(config Config) { - conch = config.ConchClient() - display = config.Renderer() - - var e error - dc, e = conch.GetDatacenterByName(*idArg) - if e != nil { - fatal(e) - } - if (dc == types.Datacenter{}) { - fatal(errors.New("couldn't find datacenter")) - } - }, - ) + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + + conch = config.ConchClient() + display = config.Renderer() + + var e error + dc, e = conch.GetDatacenterByName(*idArg) + fatalIf(e) + + if (dc == types.Datacenter{}) { + fatalIf(errors.New("couldn't find datacenter")) + } + } cmd.Command("get", "Information about a single datacenter", func(cmd *cli.Cmd) { cmd.Action = func() { display(dc, nil) } @@ -143,7 +140,7 @@ func datacenterCmd(cmd *cli.Cmd) { } if count == 0 { - fatal(errors.New("one option must be provided")) + fatalIf(errors.New("one option must be provided")) } conch.UpdateDatacenter(dc.ID, types.DatacenterUpdate{ Location: types.NonEmptyString(*locationOpt), diff --git a/cli/device_reports.go b/cli/device_reports.go index e0353c0..9014962 100644 --- a/cli/device_reports.go +++ b/cli/device_reports.go @@ -12,9 +12,7 @@ func deviceReportCmd(cmd *cli.Cmd) { filePathArg := cmd.StringArg("FILE", "-", "Path to a JSON file that defines the layout. '-' indicates STDIN") input, err := getInputReader(*filePathArg) - if err != nil { - fatal(err) - } + fatalIf(err) cmd.Before = func() { conch = config.ConchClient() } cmd.Action = func() { conch.SendDeviceReport(input) } diff --git a/cli/devices.go b/cli/devices.go index 6aaff98..ba70db7 100644 --- a/cli/devices.go +++ b/cli/devices.go @@ -11,12 +11,12 @@ import ( ) func devicesCmd(cmd *cli.Cmd) { - cmd.Before = config.Before(requireAuth) + cmd.Before = config.requireAuth cmd.Command("search s", "Search for devices", deviceSearchCmd) } func deviceSearchCmd(cmd *cli.Cmd) { - cmd.Before = config.Before(requireAuth) + cmd.Before = config.requireAuth cmd.Command("setting", "Search for devices by exact setting value", searchBySettingCmd) cmd.Command("tag", "Search for devices by exact tag value", searchByTagCmd) cmd.Command("hostname", "Search for devices by exact hostname", searchByHostnameCmd) @@ -228,9 +228,8 @@ func devicePreflightCmd(id *string) func(cmd *cli.Cmd) { conch = config.ConchClient() display = config.Renderer() phase, e := conch.GetDevicePhase(*id) - if e != nil { - fatal(e) - } + fatalIf(e) + if phase != "integration" { os.Stderr.WriteString("Warning: This device is no longer in the 'integration' phase. This data is likely to be inaccurate\n") } @@ -243,9 +242,7 @@ func devicePreflightCmd(id *string) func(cmd *cli.Cmd) { cmd.Command("ipmi", "IPMI address for a device in preflight", func(cmd *cli.Cmd) { cmd.Action = func() { iface, e := conch.GetDeviceInterfaceByName(*id, "ipmi1") - if e != nil { - fatal(e) - } + fatalIf(e) fmt.Println(iface.Ipaddr) } }) diff --git a/cli/hardware.go b/cli/hardware.go index 97ae3f1..fa1005d 100644 --- a/cli/hardware.go +++ b/cli/hardware.go @@ -28,14 +28,11 @@ func cmdCreateProduct(cmd *cli.Cmd) { display := config.Renderer() validationPlan, e := conch.GetValidationPlanByName(*validationPlanOpt) - if e != nil { - fatal(e) - } + fatalIf(e) vendor, e := conch.GetHardwareVendorByName(*vendor) - if e != nil { - fatal(e) - } + fatalIf(e) + create := types.HardwareProductCreate{ Name: types.MojoStandardPlaceholder(*name), Alias: types.MojoStandardPlaceholder(*alias), @@ -68,9 +65,7 @@ func cmdImportProduct(cmd *cli.Cmd) { display := config.Renderer() in, err := getInputReader(*filePathArg) - if err != nil { - fatal(err) - } + fatalIf(err) p := conch.ReadHardwareProduct(in) conch.CreateHardwareProduct(p) @@ -99,11 +94,10 @@ func hardwareCmd(cmd *cli.Cmd) { cmd.Before = func() { var e error hp, e = conch.GetHardwareProductByID(*idArg) - if e != nil { - fatal(e) - } + fatalIf(e) + if (hp == types.HardwareProduct{}) { - fatal(errors.New("Hardware Product not found for " + *idArg)) + fatalIf(errors.New("Hardware Product not found for " + *idArg)) } } cmd.Action = func() { fmt.Println(hp) } @@ -141,9 +135,8 @@ func hardwareCmd(cmd *cli.Cmd) { cmd.Before = func() { var e error hv, e = conch.GetHardwareVendorByName(*idArg) - if e != nil { - fatal(e) - } + fatalIf(e) + if (hv == types.HardwareVendor{}) { fmt.Println("Hardware Vendor not found for " + *idArg) cli.Exit(1) diff --git a/cli/organizations.go b/cli/organizations.go index 48568db..890e61c 100644 --- a/cli/organizations.go +++ b/cli/organizations.go @@ -63,9 +63,7 @@ func organizationCmd(cmd *cli.Cmd) { var e error o, e = conch.GetOrganizationByName(*organizationNameArg) - if e != nil { - fatal(e) - } + fatalIf(e) } cmd.Command("get", "Get information about a single organization by its name", func(cmd *cli.Cmd) { @@ -109,7 +107,7 @@ func organizationCmd(cmd *cli.Cmd) { cmd.Spec = "EMAIL [OPTIONS]" cmd.Action = func() { if !okBuildRole(*roleOpt) { - fatal(fmt.Errorf( + fatalIf(fmt.Errorf( "'role' value must be one of: %s", prettyBuildRoleList(), )) diff --git a/cli/racks.go b/cli/racks.go index 67db459..3f8d3ae 100644 --- a/cli/racks.go +++ b/cli/racks.go @@ -12,13 +12,11 @@ import ( func racksCmd(cmd *cli.Cmd) { var conch *conch.Client - cmd.Before = config.Before( - requireAuth, - requireSysAdmin, - func(c Config) { - conch = config.ConchClient() - }, - ) + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + conch = config.ConchClient() + } cmd.Command("create", "Create a new rack", func(cmd *cli.Cmd) { var ( @@ -41,41 +39,40 @@ func racksCmd(cmd *cli.Cmd) { // `--name ""` which will pass the cli lib's requirement // check but is still crap if *nameOpt == "" { - fatal(errors.New("--name is required")) + fatalIf(errors.New("--name is required")) } if *roomAliasOpt == "" { - fatal(errors.New("--room is required")) + fatalIf(errors.New("--room is required")) } else { room, e := conch.GetRoomByAlias(*roomAliasOpt) - if e != nil { - fatal(e) - } + fatalIf(e) + if (room == types.DatacenterRoomDetailed{}) { - fatal(errors.New("could not find room")) + fatalIf(errors.New("could not find room")) } roomID = room.ID } if *roleNameOpt == "" { - fatal(errors.New("--role is required")) + fatalIf(errors.New("--role is required")) } else { role, e := conch.GetRackRoleByName(*roleNameOpt) if e != nil { - fatal(e) + fatalIf(e) } if (role == types.RackRole{}) { - fatal(errors.New("could not find rack role")) + fatalIf(errors.New("could not find rack role")) } roleID = role.ID } if *buildNameOpt == "" { - fatal(errors.New("--build is required")) + fatalIf(errors.New("--build is required")) } else { build, e := conch.GetBuildByName(*buildNameOpt) if e != nil { - fatal(e) + fatalIf(e) } buildID = build.ID } @@ -104,21 +101,20 @@ func rackCmd(cmd *cli.Cmd) { cmd.Spec = "UUID" - cmd.Before = config.Before( - requireAuth, - func(config Config) { - conch = config.ConchClient() - display = config.Renderer() + cmd.Before = func() { + config.requireAuth() + conch = config.ConchClient() + display = config.Renderer() - var e error - rack, e = conch.GetRackByName(*idArg) - if e != nil { - fatal(e) - } - if (rack == types.Rack{}) { - fatal(errors.New("could not find the rack")) - } - }) + var e error + rack, e = conch.GetRackByName(*idArg) + if e != nil { + fatalIf(e) + } + if (rack == types.Rack{}) { + fatalIf(errors.New("could not find the rack")) + } + } cmd.Command("get", "Get a single rack", func(cmd *cli.Cmd) { cmd.Action = func() { display(rack, nil) } @@ -149,20 +145,20 @@ func rackCmd(cmd *cli.Cmd) { if *roomAliasOpt != "" { room, e := conch.GetRoomByAlias(*roomAliasOpt) if e != nil { - fatal(e) + fatalIf(e) } if (room == types.DatacenterRoomDetailed{}) { - fatal(errors.New("could not find room")) + fatalIf(errors.New("could not find room")) } roomID = room.ID } if *roleNameOpt != "" { role, e := conch.GetRackRoleByName(*roomAliasOpt) if e != nil { - fatal(e) + fatalIf(e) } if (role == types.RackRole{}) { - fatal(errors.New("could not find rack role")) + fatalIf(errors.New("could not find rack role")) } roleID = role.ID } @@ -197,10 +193,11 @@ func rackCmd(cmd *cli.Cmd) { }) cmd.Command("delete rm", "Delete a rack", func(cmd *cli.Cmd) { - cmd.Before = config.Before( - requireAuth, - requireSysAdmin, - ) + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + } + cmd.Action = func() { conch.DeleteRack(rack.ID) fmt.Println("OK") @@ -218,31 +215,30 @@ func rackCmd(cmd *cli.Cmd) { cmd.Action = func() { l, e := conch.GetRackLayout(rack.ID) if e != nil { - fatal(e) + fatalIf(e) } fmt.Println(renderJSON(l)) } }) cmd.Command("import", "Import the layout of this rack (using the same format as 'export')", func(cmd *cli.Cmd) { - var ( - filePathArg = cmd.StringArg("FILE", "-", "Path to a JSON file that defines the layout. '-' indicates STDIN") - overwriteOpt = cmd.BoolOpt("overwrite", false, "If the rack has an existing layout, *overwrite* it. This is a destructive action") - ) + filePathArg := cmd.StringArg("FILE", "-", "Path to a JSON file that defines the layout. '-' indicates STDIN") + overwriteOpt := cmd.BoolOpt("overwrite", false, "If the rack has an existing layout, *overwrite* it. This is a destructive action") + cmd.Action = func() { layout, e := conch.GetRackLayout(rack.ID) if e != nil { - fatal(e) + fatalIf(e) } if len(layout) > 0 { if !*overwriteOpt { - fatal(errors.New("rack already has a layout. use --overwrite to force")) + fatalIf(errors.New("rack already has a layout. use --overwrite to force")) } } input, e := getInputReader(*filePathArg) if e != nil { - fatal(e) + fatalIf(e) } update := conch.ReadRackLayoutUpdate(input) @@ -257,7 +253,7 @@ func rackCmd(cmd *cli.Cmd) { cmd.Action = func() { input, err := getInputReader(*filePathArg) if err != nil { - fatal(err) + fatalIf(err) } update := conch.ReadRackAssignmentUpdate(input) conch.UpdateRackAssignments(rack.ID, update) diff --git a/cli/relays.go b/cli/relays.go index 0338504..c0055bb 100644 --- a/cli/relays.go +++ b/cli/relays.go @@ -44,10 +44,10 @@ func relayCmd(cmd *cli.Cmd) { var e error relay, e = conch.GetRelayBySerial(*relayArg) if e != nil { - fatal(e) + fatalIf(e) } if (relay == types.Relay{}) { - fatal(errors.New("relay not found")) + fatalIf(errors.New("relay not found")) } } // default action is to display the relay diff --git a/cli/roles.go b/cli/roles.go index acc349a..a846e65 100644 --- a/cli/roles.go +++ b/cli/roles.go @@ -10,15 +10,13 @@ import ( func rolesCmd(cmd *cli.Cmd) { var conch *conch.Client - var display func(interface{}, error) + var display Renderer - cmd.Before = config.Before( - requireSysAdmin, - func(config Config) { - conch = config.ConchClient() - display = config.Renderer() - }, - ) + cmd.Before = func() { + config.requireSysAdmin() + conch = config.ConchClient() + display = config.Renderer() + } cmd.Command("get", "Get a list of all rack roles", func(cmd *cli.Cmd) { cmd.Action = func() { display(conch.GetAllRackRoles()) } @@ -33,11 +31,11 @@ func rolesCmd(cmd *cli.Cmd) { cmd.Spec = "--name --rack-size" cmd.Action = func() { if *nameOpt == "" { - fatal(errors.New("--name is required")) + fatalIf(errors.New("--name is required")) } if *rackSizeOpt == 0 { - fatal(errors.New("--rack-size is required and cannot be 0")) + fatalIf(errors.New("--rack-size is required and cannot be 0")) } conch.CreateRackRole(types.RackRoleCreate{ Name: types.MojoStandardPlaceholder(*nameOpt), @@ -60,23 +58,22 @@ func roleCmd(cmd *cli.Cmd) { cmd.Spec = "NAME" - cmd.Before = config.Before( - requireAuth, - requireSysAdmin, - func(config Config) { - conch := config.ConchClient() - display = config.Renderer() - - var e error - role, e = conch.GetRackRoleByName(*nameArg) - if e != nil { - fatal(e) - } - if (role == types.RackRole{}) { - fatal(errors.New("couldn't find the role")) - } - }, - ) + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + + conch := config.ConchClient() + display = config.Renderer() + + var e error + role, e = conch.GetRackRoleByName(*nameArg) + if e != nil { + fatalIf(e) + } + if (role == types.RackRole{}) { + fatalIf(errors.New("couldn't find the role")) + } + } cmd.Command("get", "Get information about a single rack role", func(cmd *cli.Cmd) { cmd.Action = func() { display(role, nil) } diff --git a/cli/rooms.go b/cli/rooms.go index ba02f88..38c94d0 100644 --- a/cli/rooms.go +++ b/cli/rooms.go @@ -12,14 +12,13 @@ func roomsCmd(cmd *cli.Cmd) { var conch *conch.Client var display func(interface{}, error) - cmd.Before = config.Before( - requireAuth, - requireSysAdmin, - func(c Config) { - conch = config.ConchClient() - display = config.Renderer() - }, - ) + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + + conch = config.ConchClient() + display = config.Renderer() + } cmd.Action = func() { display(conch.GetAllRooms()) } @@ -41,21 +40,20 @@ func roomsCmd(cmd *cli.Cmd) { // '--alias ""' which will pass the cli lib's requirement // check but is still crap if *aliasOpt == "" { - fatal(errors.New("--alias is required")) + fatalIf(errors.New("--alias is required")) } if *azOpt == "" { - fatal(errors.New("--az is required")) + fatalIf(errors.New("--az is required")) } if *datacenterIDOpt == "" { - fatal(errors.New("--datacenter-id is required")) + fatalIf(errors.New("--datacenter-id is required")) } datacenter, e := conch.GetDatacenterByName(*datacenterIDOpt) - if e != nil { - fatal(e) - } + fatalIf(e) + if (datacenter == types.Datacenter{}) { - fatal(errors.New("could not find the datacenter")) + fatalIf(errors.New("could not find the datacenter")) } conch.CreateRoom(types.DatacenterRoomCreate{ @@ -81,23 +79,20 @@ func roomCmd(cmd *cli.Cmd) { cmd.Spec = "ALIAS" - cmd.Before = config.Before( - requireAuth, - requireSysAdmin, - func(config Config) { - conch = config.ConchClient() - display = config.Renderer() - - var e error - room, e = conch.GetRoomByAlias(*aliasArg) - if e != nil { - fatal(e) - } - if (room == types.DatacenterRoomDetailed{}) { - fatal(errors.New("could not find the room")) - } - }, - ) + cmd.Before = func() { + config.requireAuth() + config.requireSysAdmin() + + conch = config.ConchClient() + display = config.Renderer() + + var e error + room, e = conch.GetRoomByAlias(*aliasArg) + fatalIf(e) + if (room == types.DatacenterRoomDetailed{}) { + fatalIf(errors.New("could not find the room")) + } + } cmd.Command("get", "Information about a single room", func(cmd *cli.Cmd) { cmd.Action = func() { display(room, nil) } @@ -113,11 +108,9 @@ func roomCmd(cmd *cli.Cmd) { cmd.Action = func() { dc, e := conch.GetDatacenterByName(*datacenterIDOpt) - if e != nil { - fatal(e) - } + fatalIf(e) if (dc == types.Datacenter{}) { - fatal(errors.New("could not find the datacenter")) + fatalIf(errors.New("could not find the datacenter")) } conch.UpdateRoom(room.ID, types.DatacenterRoomUpdate{ diff --git a/cli/users.go b/cli/users.go index c8badc5..9fbf1c5 100644 --- a/cli/users.go +++ b/cli/users.go @@ -9,9 +9,13 @@ import ( "github.com/joyent/kosh/conch/types" ) -func whoamiCmd(cmd *cli.Cmd) { profileCmd(cmd) } +func whoamiCmd(cmd *cli.Cmd) { + cmd.Before = config.requireAuth + profileCmd(cmd) +} func userCmd(cmd *cli.Cmd) { + cmd.Before = config.requireAuth cmd.Command("profile", "View your Conch profile", profileCmd) cmd.Command("settings", "Get the settings for the current user", settingsCmd) cmd.Command("setting", "Commands for dealing with a single setting for the current user", userSetting) @@ -32,7 +36,7 @@ func tokensCmd(cmd *cli.Cmd) { cmd.Command("get ls", "list the tokens for the current user", func(cmd *cli.Cmd) { cmd.Action = func() { display(conch.GetCurrentUserTokens()) } }) - cmd.Command("create new", "Get the settings for the current user", func(cmd *cli.Cmd) { + cmd.Command("create new add", "Get the settings for the current user", func(cmd *cli.Cmd) { name := cmd.StringArg("NAME", "", "The string name of a setting") user := cmd.StringOpt("user u", "", "User name to use for authentication") pass := cmd.StringOpt("pass p", "", "Password to use for authentication") @@ -40,7 +44,7 @@ func tokensCmd(cmd *cli.Cmd) { if *user != "" && *pass != "" { loginToken, e := conch.Login(*user, *pass) if e != nil { - fatal(e) + fatalIf(e) } config.Debug(fmt.Sprintf("%+v", loginToken)) conch = conch.Authorization("Bearer " + loginToken.JwtToken) @@ -64,11 +68,11 @@ func tokenCmd(cmd *cli.Cmd) { var e error if name == nil { - fatal(errors.New("must provide a valid token name")) + fatalIf(errors.New("must provide a valid token name")) } token, e = conch.GetCurrentUserTokenByName(*name) if e != nil { - fatal(e) + fatalIf(e) } } @@ -108,7 +112,7 @@ func userSetting(cmd *cli.Cmd) { cmd.Spec = "NAME" cmd.Command("get", "Get a setting for the current user", userSettingGet(name)) cmd.Command("set", "Set a setting for the current user", userSettingSet(name)) - cmd.Command("delete", "Delete a setting for the current user", userSettingDelete(name)) + cmd.Command("delete rm", "Delete a setting for the current user", userSettingDelete(name)) } func userSettingGet(setting string) func(cmd *cli.Cmd) { @@ -131,7 +135,7 @@ func userSettingSet(setting string) func(cmd *cli.Cmd) { conch := config.ConchClient() display := config.Renderer() if e := conch.SetCurrentUserSettingByName(setting, types.UserSetting(value)); e != nil { - fatal(e) + fatalIf(e) } display(conch.GetCurrentUserSettingByName(setting)) } @@ -143,7 +147,7 @@ func userSettingDelete(setting string) func(cmd *cli.Cmd) { cmd.Action = func() { conch := config.ConchClient() if e := conch.DeleteCurrentUserSetting(setting); e != nil { - fatal(e) + fatalIf(e) } } } diff --git a/cli/validations.go b/cli/validations.go index b9c87bc..882c02f 100644 --- a/cli/validations.go +++ b/cli/validations.go @@ -32,12 +32,10 @@ func validationCmd(cmd *cli.Cmd) { cmd.Before = func() { var e error plan, e = conch.GetValidationPlanByName(*idArg) - if e != nil { - fatal(e) - } + fatalIf(e) if (plan == types.ValidationPlan{}) { - fatal(errors.New("could not find the validation plan")) + fatalIf(errors.New("could not find the validation plan")) } } diff --git a/conch/racks.go b/conch/racks.go index a15f98a..f445028 100644 --- a/conch/racks.go +++ b/conch/racks.go @@ -53,7 +53,7 @@ func (c *Client) UpdateRackLayout(id types.UUID, layout types.RackLayoutUpdate) return e } -// ReadRackLayoutUpdate takes an io reader and returns a RackLayoutUpdate +// ReadRackLayoutUpdate takes an io.Reader and returns a RackLayoutUpdate // struct suitable for UpdateRackLayout func (c *Client) ReadRackLayoutUpdate(r io.Reader) (update types.RackLayoutUpdate) { json.NewDecoder(r).Decode(&update) diff --git a/conch/types/responses.go b/conch/types/responses.go index 33ad302..045ab4a 100644 --- a/conch/types/responses.go +++ b/conch/types/responses.go @@ -451,17 +451,17 @@ type Role string // UserDetailed is a struct type UserDetailed struct { - Builds Builds `json:"builds"` - Created time.Time `json:"created"` - Email EmailAddress `json:"email"` - ForcePasswordChange bool `json:"force_password_change"` - ID UUID `json:"id"` - IsAdmin bool `json:"is_admin"` - LastLogin time.Time `json:"last_login"` - LastSeen time.Time `json:"last_seen"` - Name string `json:"name"` - Organizations Organizations `json:"organizations"` - RefuseSessionAuth bool `json:"refuse_session_auth"` + Builds Builds `json:"builds"` + Created time.Time `json:"created"` + Email EmailAddress `json:"email"` + ForcePasswordChange bool `json:"force_password_change"` + ID UUID `json:"id"` + IsAdmin bool `json:"is_admin"` + LastLogin time.Time `json:"last_login"` + LastSeen time.Time `json:"last_seen"` + Name NonEmptyString `json:"name"` + Organizations Organizations `json:"organizations"` + RefuseSessionAuth bool `json:"refuse_session_auth"` } // Users is a slice of structs diff --git a/conch/types/templates.go b/conch/types/templates.go index 962c617..2aff80e 100644 --- a/conch/types/templates.go +++ b/conch/types/templates.go @@ -871,6 +871,40 @@ func (ul UsersTerse) ForEach(do func([]string)) { } } +func (ul Users) Len() int { return len(ul) } +func (ul Users) Swap(i, j int) { ul[i], ul[j] = ul[j], ul[i] } +func (ul Users) Less(i, j int) bool { return ul[i].Name < ul[j].Name } + +// Headers returns the list of headers for the table view +func (ul Users) Headers() []string { + return []string{ + "ID", + "Name", + "Email", + "Admin", + "Created", + "Last Seen", + "Last Login", + "PW", + } +} + +// ForEach iterates over each item in the list and applies a function to it +func (ul Users) ForEach(do func([]string)) { + for _, u := range ul { + do([]string{ + template.CutUUID(u.ID.String()), + string(u.Name), + string(u.Email), + template.YesOrNo(u.IsAdmin), + template.TimeStr(u.Created), + template.TimeStr(u.LastSeen), + template.TimeStr(u.LastLogin), + template.YesOrNo(u.ForcePasswordChange), + }) + } +} + const userTokenTemplate = ` Token {{ .Name }} <<<<<<< HEAD diff --git a/conch/users.go b/conch/users.go index a7ee0fa..5769e9e 100644 --- a/conch/users.go +++ b/conch/users.go @@ -1,6 +1,11 @@ package conch -import "github.com/joyent/kosh/conch/types" +import ( + "encoding/json" + "io" + + "github.com/joyent/kosh/conch/types" +) // GetCurrentUser (GET /user/me) retrieves the user associated with the current // authentication @@ -88,6 +93,7 @@ func (c *Client) DeleteCurrentUserToken(name string) error { // the given email func (c *Client) GetUserByEmail(email string) (user types.UserDetailed, e error) { _, e = c.User(email).Receive(&user) + c.Logger.Debug(user) return } @@ -98,6 +104,12 @@ func (c *Client) GetUserByID(id types.UUID) (user types.UserDetailed, e error) { return } +// ReadUser takes an io.Reader and returns a UserDetailed object +func (c *Client) ReadUser(r io.Reader) (user types.UserDetailed) { + json.NewDecoder(r).Decode(&user) + return +} + // UpdateUser (POST /user/:target_user_id_or_email?send_mail=<1|0>) will update the // user with the given email. Optionally notify the user via email. // BUG(perigrin): sendEmail is currently not implemented diff --git a/template/template.go b/template/template.go index c0efe18..0820d0d 100644 --- a/template/template.go +++ b/template/template.go @@ -15,6 +15,14 @@ import ( const dateFormat = "2006-01-02 15:04:05 -0700 MST" +// YesOrNo transforms a bool into "yes" or "no" depending on it's truth +func YesOrNo(p bool) string { + if p { + return "Yes" + } + return "No" +} + // CutUUID - trims a UUID down to a short readable version func CutUUID(id string) string { re := regexp.MustCompile("^(.+?)-") @@ -41,7 +49,7 @@ func Table(t tables.Tabulable) string { // NewTemplate returns a new template instance func NewTemplate() *template.Template { return template.New("wat").Funcs(template.FuncMap{ - "CutUUID": func(id string) string { return CutUUID(id) }, + "CutUUID": CutUUID, // func(id string) string { return CutUUID(id) }, "TimeStr": func(t time.Time) string { return TimeStr(t) }, "Table": Table, })