diff --git a/.gitignore b/.gitignore index b41dac6..6068ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ _testmain.go *.exe *.ini + +# Binaries +bin/* +release/* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fb48fd5 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +BIN=linode-inventory +VERSION=0.0.1 +README=README.md +LICENSE=LICENSE +RELEASE_DIR=release + +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOINSTALL=$(GOCMD) install +GOTEST=$(GOCMD) test +GODEP=$(GOCMD) get -d -v ./... +GOFMT=gofmt -w + +default: build + +build: + $(GODEP) + GOARCH=amd64 GOOS=linux $(GOBUILD) -o bin/linux-amd64/$(BIN) + GOARCH=386 GOOS=linux $(GOBUILD) -o bin/linux-386/$(BIN) + GOARCH=amd64 GOOS=darwin $(GOBUILD) -o bin/darwin-amd64/$(BIN) + +package: + rm -rf $(RELEASE_DIR)/$(BIN) + mkdir $(RELEASE_DIR)/$(BIN) + cp $(README) $(RELEASE_DIR)/$(BIN) + cp $(LICENSE) $(RELEASE_DIR)/$(BIN) + + cp -f bin/linux-amd64/$(BIN) $(RELEASE_DIR)/$(BIN)/$(BIN) + tar -czf $(RELEASE_DIR)/$(BIN)-linux-amd64-v$(VERSION).tar.gz -C $(RELEASE_DIR) $(BIN) + + cp -f bin/linux-386/$(BIN) $(RELEASE_DIR)/$(BIN)/$(BIN) + tar -czf $(RELEASE_DIR)/$(BIN)-linux-386-v$(VERSION).tar.gz -C $(RELEASE_DIR) $(BIN) + + cp -f bin/darwin-amd64/$(BIN) $(RELEASE_DIR)/$(BIN)/$(BIN) + tar -czf $(RELEASE_DIR)/$(BIN)-darwin-amd64-v$(VERSION).tar.gz -C $(RELEASE_DIR) $(BIN) + + rm -rf $(RELEASE_DIR)/$(BIN) + +format: + $(GOFMT) ./**/*.go + +clean: + $(GOCLEAN) + +test: + $(GODEP) && $(GOTEST) -v ./... diff --git a/README.md b/README.md index d918b7a..0df3cb1 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,29 @@ linode-inventory ================ Ansible Inventory plugin for use with Linode + +The Ansible repository contains an [Linode inventory plugin](https://github.com/ansible/ansible/tree/devel/plugins/inventory). + +This plugin allows filtering based on the Display Group and has no external dependencies. +Also, it includes all information in the `--list` form (with only 2 api requests to Linode). +The `--host` form returns an empty hash. + +See [Developing Dynamic Inventory Sources](http://docs.ansible.com/developing_inventory.html) for more information. + +## Usage + +Download the appropriate package from releases. + +Create a `linode-inventory.ini` file with your Linode API key. See the example ini file. + +**Command Line Usage** + +Print out your available linodes + + ./linode-inventory + +**Ansible Usage** + +Ansible will execute with the `--list` or `--host` flag. The `--host` flag will always return an empty hash. + + ./linode-inventory --list \ No newline at end of file diff --git a/api/api.go b/api/api.go index 9390562..324ed5a 100644 --- a/api/api.go +++ b/api/api.go @@ -14,13 +14,6 @@ const ( API_URL = "https://api.linode.com/" ) -type ErrorJson struct { - Errors []struct { - Code int `json:"ERRORCODE"` - Message string `json:"ERRORMESSAGE"` - } `json:"ERRORARRAY,omitempty"` -} - type queryParams map[string]string type apiAction struct { @@ -93,25 +86,39 @@ func (self apiRequest) GetJson(data interface{}) error { if err != nil { return err } + + // the linode API does not use HTTP status codes to indicate errors, + // rather it embeds in the JSON document the errors. When there is an error + // the foramt of the `DATA` element changes as well, which would cause the json decode to fail. + // + // Here we first parse the json to see if it contains errors, then re-parse with the provided + // json structure + if linodeErr := checkForLinodeError(bytes.NewReader(body)); linodeErr != nil { + return linodeErr + } decoder := json.NewDecoder(bytes.NewReader(body)) err = decoder.Decode(data) if err != nil { - linodeErr := checkForLinodeError(bytes.NewReader(body)) - if linodeErr != nil { - return linodeErr - } return err } + return nil } +type errorJson struct { + Errors []struct { + Code int `json:"ERRORCODE"` + Message string `json:"ERRORMESSAGE"` + } `json:"ERRORARRAY,omitempty"` +} + func checkForLinodeError(body *bytes.Reader) error { - data := new(ErrorJson) + data := new(errorJson) decoder := json.NewDecoder(body) err := decoder.Decode(&data) if err != nil { - // this is not actually an error + // this is not actually an error, since there is not always an error present in the JSON return nil } if len(data.Errors) > 0 { diff --git a/api/linode.go b/api/linode.go index a8ebb2a..c762bdd 100644 --- a/api/linode.go +++ b/api/linode.go @@ -2,8 +2,7 @@ package api import ( "strconv" - //"fmt" - //"bytes" + "sort" ) type Linode struct { @@ -37,6 +36,23 @@ func (self Linode) PrivateIp() string { return ip } +type sortedLinodeIps []LinodeIp + +func (self sortedLinodeIps) Len() int { + return len(self) +} +func (self sortedLinodeIps) Swap(i, j int) { + self[i], self[j] = self[j], self[i] +} +// Public first +func (self sortedLinodeIps) Less(i, j int) bool { + return self[i].Public > self[j].Public +} + +func (self Linode) SortIps() { + sort.Sort(sortedLinodeIps(self.Ips)) +} + type Linodes map[int]*Linode func (self Linodes) FilterByDisplayGroup(group string) Linodes { @@ -75,7 +91,6 @@ func LinodeList(apiKey string) (Linodes, error) { linodes := make(Linodes) for _, linode := range data.Linodes { - //linode.Ips = []LinodeIp{} l := linode linodes[linode.Id] = &l } @@ -89,6 +104,8 @@ type LinodeIp struct { Public int `json:"ISPUBLIC"` } +// first fetch the list of linodes, +// then use a batch request to list all the ips associated with those linodes func LinodeListWithIps(apiKey string) (Linodes, error) { linodes, err := LinodeList(apiKey) if err != nil { @@ -116,7 +133,7 @@ func LinodeListWithIps(apiKey string) (Linodes, error) { for _, ipList := range data { for _, linodeIp := range ipList.LinodeIps { if linode, ok := linodes[linodeIp.LinodeId]; ok { - linode.Ips = append(linode.Ips, linodeIp) + linode.Ips = append(linode.Ips, linodeIp) } } } diff --git a/inventory.go b/inventory.go index a6d6a0a..eddb0c2 100644 --- a/inventory.go +++ b/inventory.go @@ -1,5 +1,7 @@ package main +// Provides output for use as an Ansible inventory plugin + import ( "encoding/json" "github.com/awilliams/linode-inventory/api" diff --git a/linode-inventory.ini.example b/linode-inventory.ini.example new file mode 100644 index 0000000..c89252d --- /dev/null +++ b/linode-inventory.ini.example @@ -0,0 +1,3 @@ +[linode] +api-key = "supersecretlinodekey" +display-group = "Staging US" diff --git a/main.go b/main.go index 45733b3..b7761b3 100644 --- a/main.go +++ b/main.go @@ -7,31 +7,33 @@ import ( "github.com/mgutz/ansi" "log" "os" + "sort" "path/filepath" ) const CONFIG_PATH = "linode-inventory.ini" type Configuration struct { - ApiKey string `gcfg:"api-key"` - DisplayGroup string `gcfg:"display-group"` + ApiKey string `gcfg:"api-key"` + DisplayGroup string `gcfg:"display-group"` } func getConfig() (*Configuration, error) { + // first check directory where the executable is located dir, err := filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { return nil, err } path := dir + "/" + CONFIG_PATH if _, err := os.Stat(path); os.IsNotExist(err) { - // this happens with using `go run main.go` + // fallback to PWD. This is usefull when using `go run` path = CONFIG_PATH } - + var config struct { Linode Configuration } - + err = gcfg.ReadFileInto(&config, path) if err != nil { return nil, err @@ -40,12 +42,25 @@ func getConfig() (*Configuration, error) { return &config.Linode, nil } +type sortedLinodes []*api.Linode + +func (self sortedLinodes) Len() int { + return len(self) +} +func (self sortedLinodes) Swap(i, j int) { + self[i], self[j] = self[j], self[i] +} +func (self sortedLinodes) Less(i, j int) bool { + return self[i].Label < self[j].Label +} + func printLinodes(linodes api.Linodes) { - grouped := make(map[string][]*api.Linode) + grouped := make(map[string]sortedLinodes) for _, linode := range linodes { grouped[linode.DisplayGroup] = append(grouped[linode.DisplayGroup], linode) } for displayGroup, linodes := range grouped { + sort.Sort(linodes) fmt.Printf("[%s]\n\n", ansi.Color(displayGroup, "green")) for _, linode := range linodes { labelColor := "magenta" @@ -53,6 +68,7 @@ func printLinodes(linodes api.Linodes) { labelColor = "blue" } fmt.Printf(" * %-25s\tRunning=%v, Ram=%d, LinodeId=%d\n", ansi.Color(linode.Label, labelColor), linode.Status == 1, linode.Ram, linode.Id) + linode.SortIps() for _, ip := range linode.Ips { var ipType string if ip.Public == 1 { @@ -99,13 +115,14 @@ func main() { // empty hash fmt.Fprint(os.Stdout, "{}") default: - fmt.Errorf("Unrecognized flag: %v\nUse --list or --host\n", os.Args[1]) + fmt.Errorf("Unrecognized flag: %v\nAvailable flags: --list or --host\n", os.Args[1]) } } else { + // non-ansible case, just print linodes + // optionally using first arg as display group filter if len(os.Args) > 1 { linodes = linodes.FilterByDisplayGroup(os.Args[1]) - } - // just use info for non-ansible case + } printLinodes(linodes) } }