Skip to content

Commit

Permalink
fix(contrib): add os-ansible-inventory (#6218)
Browse files Browse the repository at this point in the history
* fix(contrib): add os-ansible-inventory

* fix: add gitignore
  • Loading branch information
sguiheux authored Jun 28, 2022
1 parent 72aeedb commit 1781280
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
1 change: 1 addition & 0 deletions tools/os-ansible-inventory/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
os-ansible-inventory
40 changes: 40 additions & 0 deletions tools/os-ansible-inventory/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.PHONY: clean

VERSION := $(if ${CDS_VERSION},${CDS_VERSION},snapshot)
GITHASH := $(if ${GIT_HASH},${GIT_HASH},`git log -1 --format="%H"`)
BUILDTIME := `date "+%m/%d/%y-%H:%M:%S"`

TARGET_DIR = ./dist
TARGET_BINARY = os-ansible-inventory
TARGET_LDFLAGS = -ldflags "-X github.com/ovh/cds/sdk.VERSION=$(VERSION) -X github.com/ovh/cds/sdk.GOOS=$$GOOS -X github.com/ovh/cds/sdk.GOARCH=$$GOARCH -X github.com/ovh/cds/sdk.GITHASH=$(GITHASH) -X github.com/ovh/cds/sdk.BUILDTIME=$(BUILDTIME) -X github.com/ovh/cds/sdk.BINARY=$(TARGET_BINARY)"
TARGET_OS = linux
TARGET_ARCH = $(if ${ARCH},${ARCH},amd64 arm 386 arm64)

GO_BUILD = go build
GO_LINT = ${GOPATH}/bin/gometalinter

$(TARGET_DIR):
$(info create $(TARGET_DIR) directory)
@mkdir -p $(TARGET_DIR)

$(GO_LINT):
go get -u github.com/alecthomas/gometalinter

default: build

clean:
@rm -rf $(TARGET_DIR)

build: $(TARGET_DIR)
@for GOOS in $(TARGET_OS); do \
for GOARCH in $(TARGET_ARCH); do \
EXTENSION=""; \
if test "$$GOOS" = "windows" ; then EXTENSION=".exe"; fi; \
echo Compiling $(TARGET_DIR)/$(TARGET_BINARY)-$$GOOS-$$GOARCH$$EXTENSION $(VERSION); \
GOOS=$$GOOS GOARCH=$$GOARCH $(GO_BUILD) $(TARGET_LDFLAGS) -o $(TARGET_DIR)/$(TARGET_BINARY)-$$GOOS-$$GOARCH$$EXTENSION; \
done; \
done

lint: $(GO_LINT)
$(GO_LINT) --install --force
$(GO_LINT) --fast --tests --vendor --disable=gas --disable=gotype ./...
211 changes: 211 additions & 0 deletions tools/os-ansible-inventory/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package main

import (
"log"
"os"
"strings"
"text/template"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/urfave/cli"

"github.com/ovh/cds/sdk"
)

var (
osRegionName string
)

func init() {
osRegionName = os.Getenv("OS_REGION_NAME")
}

func main() {
app := cli.NewApp()
app.Name = "os-ansible-inventory"
app.Usage = "Openstack Ansible Inventory based on metadata"
app.Version = sdk.VERSION
app.Flags = []cli.Flag{
cli.StringSliceFlag{
Name: "filter",
},
cli.StringFlag{
Name: "group-by",
},
cli.StringFlag{
Name: "net",
},
}
app.Action = func(c *cli.Context) error {
filters := c.StringSlice("filter")

fList := []Filter{}
for _, f := range filters {
tuple := strings.Split(f, "=")
if len(tuple) == 2 {
fList = append(fList, Filter{
Key: tuple[0],
Value: tuple[1],
Operator: "=",
})
}

tuple = strings.Split(f, "~")
if len(tuple) == 2 {
fList = append(fList, Filter{
Key: tuple[0],
Value: tuple[1],
Operator: "~",
})
}
}

groupBy := c.String("group-by")
net := c.String("net")

return do(fList, groupBy, net)
}

err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}

func do(filters []Filter, groupBy, net string) error {
auth, err := openstack.AuthOptionsFromEnv()
if err != nil {
return err
}

provider, err := openstack.AuthenticatedClient(auth)
if err != nil {
return err
}

nova, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{Region: osRegionName})
if err != nil {
return err
}

all, err := servers.List(nova, nil).AllPages()
if err != nil {
return err
}
serverList, err := servers.ExtractServers(all)
if err != nil {
return err
}

list := &ServerList{
l: serverList,
}

filter(list, filters)
if list.err != nil {
return list.err
}

return render(list, groupBy, net)
}

func filter(l *ServerList, filters []Filter) {
for _, f := range filters {
if l.err != nil {
return
}
l.Filter(f)
}
}

func getIPAdress(s *servers.Server, net string, netVersion int) string {
if netVersion == 0 {
netVersion = 4
}

address := map[string]string{}

for k, v := range s.Addresses {
switch v.(type) {
case []interface{}:
for _, z := range v.([]interface{}) {
var addr string
var version int
for x, y := range z.(map[string]interface{}) {
if x == "addr" {
addr = y.(string)
}
if x == "version" {
version = int(y.(float64))
}
}
//we only support IPV4
if addr != "" && version == netVersion {
address[k] = addr
}
}
}
}

if net != "" {
return address[net]
}

for net := range address {
return address[net]
}

return ""
}

func getAnsibleMetadata(s *servers.Server) map[string]string {
data := map[string]string{}
for k, v := range s.Metadata {
if strings.HasPrefix(k, "ansible_") {
data[k] = v
}
}
return data
}

func render(l *ServerList, groupBy string, net string) error {
i := &Inventory{
Groups: map[string]Group{},
}

if groupBy == "" {
g := Group{}
for _, s := range l.l {
g.Hosts = append(g.Hosts, Host{
Address: getIPAdress(&s, net, 4),
Name: s.Name,
Extra: getAnsibleMetadata(&s),
})
}
i.Groups["all"] = g
}

t := template.New("t")
t, err := t.Parse(iniFile)
if err != nil {
return err
}

err = t.Execute(os.Stdout, i)
if err != nil {
return err
}

return nil
}

var iniFile = `
{{- range $key, $value := .Groups }}
[{{$key}}]
{{ range $value.Hosts}}
{{ .Name }} ansible_connection=ssh ansible_host={{ .Address }} {{- range $ekey, $evalue := .Extra }} {{$ekey}}={{$evalue}} {{ end -}}
{{ end }}
{{ end -}}
`
84 changes: 84 additions & 0 deletions tools/os-ansible-inventory/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"fmt"
"regexp"
"strings"

"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
)

// Host is a map of HostVar, keyed by a hostname.
type Host struct {
Name string
Address string
Extra map[string]string
}

// Group is struct that contains a list of hosts.
type Group struct {
Hosts []Host `yaml:"hosts,omitempty"`
}

// Groups is a map of groups, keyed by a group name.
type Groups map[string]Group

// Inventory represents an Ansible Inventory consists of host variables and groups.
type Inventory struct {
Groups Groups `json:"groups,omitempty"`
}

// Filter represent a filter on server matadata
type Filter struct {
Key, Operator, Value string
}

// Match checks if a key, valur pair matches with the filter
func (f Filter) Match(k, v string) (bool, error) {
switch f.Operator {
case "=":
if strings.EqualFold(k, f.Key) && strings.EqualFold(v, f.Value) {
return true, nil
}
case "~":
if strings.EqualFold(k, f.Key) {
r, err := regexp.Compile(f.Value)
if err != nil {
return false, err
}
return r.MatchString(v), nil
}
default:
return false, fmt.Errorf("unsupported operator %s", f.Operator)
}
return false, nil
}

// ServerList is a list of server
type ServerList struct {
l []servers.Server
err error
}

// Filter a list of server
func (l *ServerList) Filter(f Filter) {
if l.err != nil {
fmt.Println(l.err)
return
}

list := []servers.Server{}
for _, s := range l.l {
for k, v := range s.Metadata {
ok, err := f.Match(k, v)
if err != nil {
l.err = err
return
}
if ok {
list = append(list, s)
}
}
}
l.l = list
}

0 comments on commit 1781280

Please sign in to comment.