Skip to content

Commit

Permalink
Add global-status command
Browse files Browse the repository at this point in the history
  • Loading branch information
bmatcuk committed Sep 18, 2018
1 parent 1e98500 commit d2e19d2
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ Response:
* **Error** - Set if an error occurred.


### GlobalStatus
Get the status of all vagrant machines.

```go
func (*VagrantClient) GlobalStatus() *GlobalStatusCommand
```

Options:
* **Prune** (default: `false`) - Remove invalid entries

Response:
* **Error** - Set if an error occurred.
* **Status** - A map of vagrant machine IDs (ex: 1a2b3c4d) to GlobalStatus
objects which describe the name, state, and directory in which the machine
was created.


### Halt
Stops the vagrant machine.

Expand Down
50 changes: 50 additions & 0 deletions command_global_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package vagrant

// GlobalStatusCommand specifies options and output from vagrant global-status
type GlobalStatusCommand struct {
BaseCommand
GlobalStatusResponse

// Prune will remove invalid entries.
Prune bool
}

// GlobalStatus will return the status of all vagrant machines regardless of
// directory. After setting options as appropriate, you must call Run() or
// Start() followed by Wait() to execute. Output will be in Status and any
// error will be in Error.
func (client *VagrantClient) GlobalStatus() *GlobalStatusCommand {
return &GlobalStatusCommand{
BaseCommand: newBaseCommand(client),
GlobalStatusResponse: newGlobalStatusResponse(),
}
}

func (cmd *GlobalStatusCommand) buildArguments() []string {
args := []string{}
if cmd.Prune {
args = append(args, "--prune")
}
return args
}

func (cmd *GlobalStatusCommand) init() error {
args := cmd.buildArguments()
return cmd.BaseCommand.init(&cmd.GlobalStatusResponse, "global-status", args...)
}

// Run the command
func (cmd *GlobalStatusCommand) Run() error {
if err := cmd.Start(); err != nil {
return err
}
return cmd.Wait()
}

// Start the command. You must call Wait() to complete execution.
func (cmd *GlobalStatusCommand) Start() error {
if err := cmd.init(); err != nil {
return err
}
return cmd.BaseCommand.Start()
}
113 changes: 113 additions & 0 deletions command_global_status_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package vagrant

import (
"strings"
)

// GlobalStatus has status information about a single Vagrant VM
type GlobalStatus struct {
// Id of the vagrant vm
Id string

// Name of the vagrant vm
Name string

// Provider of the vagrant vm
Provider string

// The State of the vagrant vm
State string

// Directory where the vagrant vm was created
Directory string

// AdditionalInfo has data which may be provided by vagrant in the future as
// a map of string keys and values.
AdditionalInfo map[string]string
}

func (gs *GlobalStatus) moveInfoToProperties() {
if id, ok := gs.AdditionalInfo["id"]; ok {
delete(gs.AdditionalInfo, "id")
gs.Id = id
}
if name, ok := gs.AdditionalInfo["name"]; ok {
delete(gs.AdditionalInfo, "name")
gs.Name = name
}
if provider, ok := gs.AdditionalInfo["provider"]; ok {
delete(gs.AdditionalInfo, "provider")
gs.Provider = provider
}
if state, ok := gs.AdditionalInfo["state"]; ok {
delete(gs.AdditionalInfo, "state")
gs.State = state
}
if directory, ok := gs.AdditionalInfo["directory"]; ok {
delete(gs.AdditionalInfo, "directory")
gs.Directory = directory
}
}

// GlobalStatusResponse has the output from vagrant global-status
type GlobalStatusResponse struct {
ErrorResponse

// Status per Vagrant VM. Keys are vagrant vm ID's (ex: 1a2b3c4d) and values
// are GlobalStatus objects.
Status map[string]*GlobalStatus

hasKeys bool
keys []string
currentKey int
currentStatus *GlobalStatus
}

func newGlobalStatusResponse() GlobalStatusResponse {
return GlobalStatusResponse{
Status: make(map[string]*GlobalStatus),
}
}

func (resp *GlobalStatusResponse) handleOutput(target, key string, message []string) {
// Only interested in the following output:
// * target: _, key: ui, message: [info, X]
if key == "ui" && len(message) > 1 && message[0] == "info" {
if len(message[1]) > 0 && (strings.Contains(message[1], "\n") || strings.Trim(message[1], "-") == "") {
// There's a line of all -'s that separates the keys from the statuses,
// and the last line contains a paragraph of text with newlines.
// Ignore both.
return
}

if resp.hasKeys {
if message[1] == "" {
// we're done loading the status for a machine
resp.currentStatus.moveInfoToProperties()
resp.Status[resp.currentStatus.Id] = resp.currentStatus
resp.currentStatus = nil
resp.currentKey = 0
return
}
if resp.currentKey >= len(resp.keys) {
// more data than there are keys?
return
}

if resp.currentStatus == nil {
resp.currentStatus = &GlobalStatus{AdditionalInfo: make(map[string]string)}
}
resp.currentStatus.AdditionalInfo[resp.keys[resp.currentKey]] = message[1]
resp.currentKey++
} else {
if message[1] == "" {
// we're done loading all the keys
resp.hasKeys = true
return
}
resp.keys = append(resp.keys, message[1])
}
} else {
resp.ErrorResponse.handleOutput(target, key, message)
}
}
46 changes: 46 additions & 0 deletions command_global_status_response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package vagrant

import (
"reflect"
"testing"
)

func TestGlobalStatusResponse_handleOutput(t *testing.T) {
parser := MockOutputParser{}
data := newGlobalStatusResponse()
parser.Run(successfulOutput["global-status"], &data)

if data.Error != nil {
t.Errorf("Successful vagrant global-status should not have set an error: %v", data.Error)
}

if len(data.Status) != 3 {
t.Fatalf("There should have been 3 statuses; instead there were %v", len(data.Status))
}

status, ok := data.Status["dc1c471"]
if !ok {
t.Fatalf("There should have been a status for 'dc1c471'; instead, keys were %v",
reflect.ValueOf(data.Status).MapKeys())
}
if status.Id != "dc1c471" {
t.Errorf("Expected Id of 'dc1c471'; got %v", status.Id)
}
if status.Name != "box-2" {
t.Errorf("Expected Name of 'box-2'; got %v", status.Name)
}
if status.Provider != "virtualbox" {
t.Errorf("Expected Provider of 'virtualbox'; got %v", status.Provider)
}
if status.State != "running" {
t.Errorf("Expected State of 'running'; got %v", status.State)
}
if status.Directory != "/path/to/box/2" {
t.Errorf("Expected Directory of '/path/to/box/2'; got %v", status.Directory)
}
if len(status.AdditionalInfo) != 0 {
t.Errorf("Expected len(AdditionalInfo) to be 0; got %v with keys %v",
len(status.AdditionalInfo),
reflect.ValueOf(status.AdditionalInfo).MapKeys())
}
}
80 changes: 80 additions & 0 deletions command_global_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package vagrant

import (
"testing"
)

func init() {
successfulOutput["global-status"] = `
1537232052,,ui,info,id
1537232052,,ui,info,name
1537232052,,ui,info,provider
1537232052,,ui,info,state
1537232052,,ui,info,directory
1537232052,,ui,info,
1537232052,,ui,info,-------------------------------------------------------------------------
1537232052,,ui,info,18a7399
1537232052,,ui,info,box-0
1537232052,,ui,info,virtualbox
1537232052,,ui,info,running
1537232052,,ui,info,/path/to/box/0
1537232052,,ui,info,
1537232052,,ui,info,3bf0e6a
1537232052,,ui,info,box-1
1537232052,,ui,info,virtualbox
1537232052,,ui,info,running
1537232052,,ui,info,/path/to/box/1
1537232052,,ui,info,
1537232052,,ui,info,dc1c471
1537232052,,ui,info,box-2
1537232052,,ui,info,virtualbox
1537232052,,ui,info,running
1537232052,,ui,info,/path/to/box/2
1537232052,,ui,info,
1537232052,,ui,info, \nThe above shows information about all known Vagrant environments\non this machine. This data is cached and may not be completely\nup-to-date (use "vagrant global-status --prune" to prune invalid\nentries). To interact with any of the machines%!(VAGRANT_COMMA) you can go to that\ndirectory and run Vagrant%!(VAGRANT_COMMA) or you can use the ID directly with\nVagrant commands from any directory. For example:\n"vagrant destroy 1a2b3c4d"
`
}

func TestGlobalStatusCommand_buildArguments(t *testing.T) {
client := newMockVagrantClient()

t.Run("prune", func(t *testing.T) {
cmd := client.GlobalStatus()
cmd.Prune = true
args := cmd.buildArguments()
assertArguments(t, args, "--prune")
})
}

func TestGlobalStatusCommand_Run(t *testing.T) {
client := newMockVagrantClient()
cmd := client.GlobalStatus()
if err := cmd.Run(); err != nil {
t.Fatalf("Command failed to run: %v", err)
}
if cmd.Error != nil {
t.Fatalf("Command returned error: %v", cmd.Error)
}

if len(cmd.Status) != 3 {
t.Fatalf("Expected 3 statuses; got %v", len(cmd.Status))
}

status, ok := cmd.Status["3bf0e6a"]
if !ok {
t.Fatalf("Expected status for '3bf0e6a' VM")
}

if status.Name != "box-1" {
t.Errorf("Expected name to be 'box-1'; got %v", status.Name)
}
if status.Provider != "virtualbox" {
t.Errorf("Expected provider to be 'virtualbox'; got %v", status.Provider)
}
if status.State != "running" {
t.Errorf("Expected state to be 'running'; got %v", status.State)
}
if status.Directory != "/path/to/box/1" {
t.Errorf("Expected directory to be '/path/to/box/1'; got %v", status.Directory)
}
}

0 comments on commit d2e19d2

Please sign in to comment.