Bootaction client initial implementation
This is the implementation of a standalone bootaction signal API client to be used on deployed nodes for signaling details and results of bootactions. Change-Id: Icc3d39253a02457a76f79d7a7c06333ae494d735
This commit is contained in:
parent
9a52dca199
commit
ecb3e01527
11
Makefile
11
Makefile
|
@ -24,7 +24,7 @@ USE_PROXY ?= false
|
||||||
PUSH_IMAGE ?= false
|
PUSH_IMAGE ?= false
|
||||||
LABEL ?= commit-id
|
LABEL ?= commit-id
|
||||||
IMAGE ?= ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${IMAGE_NAME}:${IMAGE_TAG}
|
IMAGE ?= ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${IMAGE_NAME}:${IMAGE_TAG}
|
||||||
GO_BUILDER ?= docker.io/golang:1.10-alpine
|
GO_BUILDER ?= docker.io/golang:1.10-stretch
|
||||||
|
|
||||||
export
|
export
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ run_images: run_drydock
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
.PHONY: tests
|
.PHONY: tests
|
||||||
tests: pep8 security docs unit_tests
|
tests: pep8 security docs unit_tests test_baclient
|
||||||
|
|
||||||
# Install external (not managed by tox/pip) dependencies
|
# Install external (not managed by tox/pip) dependencies
|
||||||
external_dep: requirements-host.txt requirements-host-test.txt
|
external_dep: requirements-host.txt requirements-host-test.txt
|
||||||
|
@ -112,7 +112,12 @@ endif
|
||||||
# Make target for building bootaction signal client
|
# Make target for building bootaction signal client
|
||||||
.PHONY: build_baclient
|
.PHONY: build_baclient
|
||||||
build_baclient: external_dep
|
build_baclient: external_dep
|
||||||
docker run -tv $(shell realpath go):/work -v $(shell realpath $(BUILD_DIR)):/build -e GOPATH=/work $(GO_BUILDER) go build -o /build/baclient baclient
|
docker run -tv $(shell realpath go):/work -v $(shell realpath $(BUILD_DIR)):/build -e GOPATH=/work $(GO_BUILDER) go build -o /build/baclient baclient
|
||||||
|
|
||||||
|
# Make target for testing bootaction signal client
|
||||||
|
.PHONY: test_baclient
|
||||||
|
test_baclient: external_dep
|
||||||
|
docker run -tv $(shell realpath go):/work -e GOPATH=/work $(GO_BUILDER) go test -v baclient
|
||||||
|
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
docs: clean drydock_docs
|
docs: clean drydock_docs
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Interact with the Drydock Bootaction API
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (msg *BootactionMessage) post(url string, key string) error {
|
||||||
|
timeout, _ := time.ParseDuration("60s")
|
||||||
|
|
||||||
|
api_request, err := buildRequest(url, key, msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error build API request: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(api_request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error sending the API request: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return fmt.Errorf("Error response: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRequest(url string, key string, msg *BootactionMessage) (*http.Request, error) {
|
||||||
|
body, err := json.Marshal(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error encoding message: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyReader := bytes.NewBuffer(body)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, bodyReader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error creating API request: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("X-Bootaction-Key", key)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
|
@ -1,9 +1,131 @@
|
||||||
|
// Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// This is a CLI for interacting with the Airship-Drydock Bootaction Signal
|
||||||
|
// API
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Printf("Hello World!\n")
|
conf := parseConfig()
|
||||||
|
|
||||||
|
// Indicates the help CLI flag was given
|
||||||
|
if conf == nil {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !conf.validate() {
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if conf.wrapExecutable != "" {
|
||||||
|
err = reportExecution(conf)
|
||||||
|
} else {
|
||||||
|
err = reportMessage(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Bootaction status posted.\n")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportExecution(conf *ClientConfig) error {
|
||||||
|
url := renderURL(conf)
|
||||||
|
|
||||||
|
msg, _ := newMessage("Bootaction starting execution.", false, "")
|
||||||
|
err := msg.post(url, conf.bootactionKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error accessing API: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := executeAction(conf.wrapExecutable, conf.proxyEnvironment)
|
||||||
|
|
||||||
|
if result {
|
||||||
|
msg, _ = newMessage("Bootaction execution successful.", false, SUCCESS)
|
||||||
|
} else {
|
||||||
|
msg, _ = newMessage("Bootaction execution failed.", true, FAILURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = msg.post(url, conf.bootactionKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error accessing API: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportMessage(conf *ClientConfig) error {
|
||||||
|
url := renderURL(conf)
|
||||||
|
|
||||||
|
msg, err := newMessage(conf.message, conf.isError, conf.status)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating message: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = msg.post(url, conf.bootactionKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error accesing API: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderURL(conf *ClientConfig) (fullURL string) {
|
||||||
|
fullURL = fmt.Sprintf("%s/%s/", conf.apiURL, conf.bootactionID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMessageDetail(msg string, isError bool) (*BootactionDetail, error) {
|
||||||
|
// isError defaults to false if nil
|
||||||
|
if msg == "" {
|
||||||
|
return nil, fmt.Errorf("Error creating MessageDetail, message string undefined.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg_detail BootactionDetail
|
||||||
|
|
||||||
|
msg_detail.Message = msg
|
||||||
|
msg_detail.IsError = isError
|
||||||
|
|
||||||
|
return &msg_detail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMessage(msg string, isError bool, finalStatus string) (*BootactionMessage, error) {
|
||||||
|
msg_detail, err := newMessageDetail(msg, isError)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error creating Message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var message BootactionMessage
|
||||||
|
|
||||||
|
message.Status = finalStatus
|
||||||
|
message.Details = []BootactionDetail{*msg_detail}
|
||||||
|
|
||||||
|
return &message, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
// Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Tests for the baclient
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
mrand "math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostIncompleteMessage(t *testing.T) {
|
||||||
|
var conf ClientConfig
|
||||||
|
|
||||||
|
bootactionID := generateID()
|
||||||
|
bootactionKey := generateKey()
|
||||||
|
|
||||||
|
ts := buildTestServer(t, bootactionID, bootactionKey, "")
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
conf.apiURL = fmt.Sprintf("%s/api/v1.0/bootaction", ts.URL)
|
||||||
|
conf.bootactionID = bootactionID
|
||||||
|
conf.bootactionKey = bootactionKey
|
||||||
|
conf.message = "Testing 1 2 3"
|
||||||
|
conf.isError = false
|
||||||
|
|
||||||
|
err := reportMessage(&conf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(fmt.Sprintf("%s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSuccessMessage(t *testing.T) {
|
||||||
|
var conf ClientConfig
|
||||||
|
|
||||||
|
bootactionID := generateID()
|
||||||
|
bootactionKey := generateKey()
|
||||||
|
|
||||||
|
ts := buildTestServer(t, bootactionID, bootactionKey, SUCCESS)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
conf.apiURL = fmt.Sprintf("%s/api/v1.0/bootaction", ts.URL)
|
||||||
|
conf.bootactionID = bootactionID
|
||||||
|
conf.bootactionKey = bootactionKey
|
||||||
|
conf.message = "Testing 1 2 3"
|
||||||
|
conf.isError = false
|
||||||
|
conf.status = SUCCESS
|
||||||
|
|
||||||
|
err := reportMessage(&conf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(fmt.Sprintf("%s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostFailureMessage(t *testing.T) {
|
||||||
|
var conf ClientConfig
|
||||||
|
|
||||||
|
bootactionID := generateID()
|
||||||
|
bootactionKey := generateKey()
|
||||||
|
|
||||||
|
ts := buildTestServer(t, bootactionID, bootactionKey, FAILURE)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
conf.apiURL = fmt.Sprintf("%s/api/v1.0/bootaction", ts.URL)
|
||||||
|
conf.bootactionID = bootactionID
|
||||||
|
conf.bootactionKey = bootactionKey
|
||||||
|
conf.message = "Testing 1 2 3"
|
||||||
|
conf.isError = true
|
||||||
|
conf.status = FAILURE
|
||||||
|
|
||||||
|
err := reportMessage(&conf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(fmt.Sprintf("%s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSuccessExec(t *testing.T) {
|
||||||
|
var conf ClientConfig
|
||||||
|
|
||||||
|
bootactionID := generateID()
|
||||||
|
bootactionKey := generateKey()
|
||||||
|
|
||||||
|
ts := buildTestServer(t, bootactionID, bootactionKey, SUCCESS)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
conf.apiURL = fmt.Sprintf("%s/api/v1.0/bootaction", ts.URL)
|
||||||
|
conf.bootactionID = bootactionID
|
||||||
|
conf.bootactionKey = bootactionKey
|
||||||
|
conf.wrapExecutable = "/bin/true"
|
||||||
|
|
||||||
|
err := reportExecution(&conf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(fmt.Sprintf("%s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostFailureExec(t *testing.T) {
|
||||||
|
var conf ClientConfig
|
||||||
|
|
||||||
|
bootactionID := generateID()
|
||||||
|
bootactionKey := generateKey()
|
||||||
|
|
||||||
|
ts := buildTestServer(t, bootactionID, bootactionKey, FAILURE)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
conf.apiURL = fmt.Sprintf("%s/api/v1.0/bootaction", ts.URL)
|
||||||
|
conf.bootactionID = bootactionID
|
||||||
|
conf.bootactionKey = bootactionKey
|
||||||
|
conf.wrapExecutable = "/bin/false"
|
||||||
|
|
||||||
|
err := reportExecution(&conf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(fmt.Sprintf("%s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateID() string {
|
||||||
|
// In order to stay within the Go stdlib and because real randomness here
|
||||||
|
// isn't that valuable, just pick one of a few hardcoded ulids
|
||||||
|
var ulidPool [5]string = [5]string{
|
||||||
|
"01CP38QN33KZ5E2MZBC0S7PJHR",
|
||||||
|
"01CP393Q44NW9TFVT1W8QTY2PP",
|
||||||
|
"01CP39489G7SRNJX6G1E61P4X5",
|
||||||
|
"01CP394JQEEH6127FCQVB4TBKY",
|
||||||
|
"01CP394TFYMH38VSM4JNJZHM9Y",
|
||||||
|
}
|
||||||
|
|
||||||
|
selector := mrand.Int31n(5)
|
||||||
|
return ulidPool[selector]
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKey() string {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, _ = rand.Read(key)
|
||||||
|
|
||||||
|
keyHex := make([]byte, hex.EncodedLen(len(key)))
|
||||||
|
hex.Encode(keyHex, key)
|
||||||
|
|
||||||
|
return string(keyHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTestServer(t *testing.T, bootactionID string, bootactionKey string, expectedResult string) *httptest.Server {
|
||||||
|
hf := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
t.Logf("Request used method %s.\n", r.Method)
|
||||||
|
w.WriteHeader(405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
if contentType != "application/json" {
|
||||||
|
t.Logf("Request had content type '%s'\n", contentType)
|
||||||
|
w.WriteHeader(415)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqKey := r.Header.Get("X-Bootaction-Key")
|
||||||
|
|
||||||
|
if reqKey != bootactionKey {
|
||||||
|
t.Logf("Request contained 'X-Bootaction-Key': %s\n", reqKey)
|
||||||
|
w.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(r.URL.Path, bootactionID) {
|
||||||
|
t.Logf("Requested URL path '%s' missing bootactionID\n", r.URL.Path)
|
||||||
|
w.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := ioutil.ReadAll(r.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Error reading test request: %s\n", err)
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var message BootactionMessage
|
||||||
|
|
||||||
|
err = json.Unmarshal(reqBody, &message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Error parsing test request: %s\n", err)
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.Status != "" && message.Status != expectedResult {
|
||||||
|
t.Logf("Did not receive expected result, instead received '%s'\n", message.Status)
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Handled request: %s - %s\n", r.Method, r.URL.Path)
|
||||||
|
t.Logf("Key: %s\n", reqKey)
|
||||||
|
t.Logf("Body:\n %s\n", reqBody)
|
||||||
|
w.WriteHeader(201)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(hf))
|
||||||
|
|
||||||
|
return ts
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseConfig() *ClientConfig {
|
||||||
|
var clientConfig ClientConfig
|
||||||
|
|
||||||
|
parseFlagConfig(&clientConfig)
|
||||||
|
|
||||||
|
if clientConfig.showHelp {
|
||||||
|
flag.PrintDefaults()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parseEnvConfig(&clientConfig)
|
||||||
|
|
||||||
|
if clientConfig.bootactionKey == "" {
|
||||||
|
if clientConfig.bootactionKeyPath != "" {
|
||||||
|
clientConfig.bootactionKey, _ = readKeyFile(clientConfig.bootactionKeyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKeyFile(keyPath string) (string, error) {
|
||||||
|
keyFile, err := os.Open(keyPath)
|
||||||
|
defer keyFile.Close()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
var keyString string
|
||||||
|
|
||||||
|
bufReader := bufio.NewReader(keyFile)
|
||||||
|
|
||||||
|
keyString, err = bufReader.ReadString('\n')
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error reading key file: %s", err)
|
||||||
|
} else {
|
||||||
|
keyString = strings.Trim(keyString, "\n")
|
||||||
|
return keyString, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("Error opening key file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFlagConfig(clientConfig *ClientConfig) {
|
||||||
|
// If neither 's' or 'f' are specified, the API call will omit the 'status' field
|
||||||
|
success := flag.Bool("s", false, "Does this message indicate bootaction success.")
|
||||||
|
failure := flag.Bool("f", false, "Does this message indicate bootaction failure.")
|
||||||
|
|
||||||
|
if *failure {
|
||||||
|
clientConfig.status = FAILURE
|
||||||
|
} else if *success {
|
||||||
|
clientConfig.status = SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.BoolVar(&clientConfig.showHelp, "h", false, "Show help and exit")
|
||||||
|
flag.BoolVar(&clientConfig.isError, "e", false, "Does this message indicate error")
|
||||||
|
flag.BoolVar(&clientConfig.proxyEnvironment, "np", false, "When wrapping an executable, should proxying the environment be disabled.")
|
||||||
|
|
||||||
|
flag.StringVar(&clientConfig.apiURL, "url", "", "Drydock API URL")
|
||||||
|
flag.StringVar(&clientConfig.bootactionID, "id", "", "Bootaction ID")
|
||||||
|
flag.StringVar(&clientConfig.bootactionKey, "key", "", "Bootaction ID")
|
||||||
|
flag.StringVar(&clientConfig.bootactionKeyPath, "keyfile", "", "Absolute path to a file containing the API key")
|
||||||
|
flag.StringVar(&clientConfig.message, "msg", "", "The detail message to record for the bootaction")
|
||||||
|
flag.StringVar(&clientConfig.wrapExecutable, "exec", "", "The absolute path to an executable to run and report result.")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnvConfig(clientConfig *ClientConfig) {
|
||||||
|
// for security, support reading the bootaction key from the environment
|
||||||
|
baKey := os.Getenv("BOOTACTION_KEY")
|
||||||
|
|
||||||
|
if baKey != "" {
|
||||||
|
clientConfig.bootactionKey = baKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clientConfig *ClientConfig) validate() bool {
|
||||||
|
valid := true
|
||||||
|
|
||||||
|
if clientConfig.bootactionID == "" {
|
||||||
|
valid = false
|
||||||
|
fmt.Printf("No Bootaction ID specified.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientConfig.bootactionKey == "" && clientConfig.bootactionKeyPath == "" {
|
||||||
|
valid = false
|
||||||
|
fmt.Printf("No Bootaction Key is specified.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientConfig.message == "" {
|
||||||
|
valid = false
|
||||||
|
fmt.Printf("Status message required.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func executeAction(commandLine string, proxyEnv bool) bool {
|
||||||
|
cmdParts := strings.Split(commandLine, " ")
|
||||||
|
var cmdArgs []string
|
||||||
|
|
||||||
|
if len(cmdParts) > 1 {
|
||||||
|
cmdArgs = cmdParts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(cmdParts[0], cmdArgs...)
|
||||||
|
|
||||||
|
if !proxyEnv {
|
||||||
|
cmd.Env = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// This is a CLI for interacting with the Airship-Drydock Bootaction Signal
|
||||||
|
// API
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
const (
|
||||||
|
SUCCESS = "success"
|
||||||
|
FAILURE = "failure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
apiURL string
|
||||||
|
bootactionID string
|
||||||
|
bootactionKey string
|
||||||
|
bootactionKeyPath string
|
||||||
|
message string
|
||||||
|
isError bool
|
||||||
|
status string
|
||||||
|
wrapExecutable string
|
||||||
|
proxyEnvironment bool
|
||||||
|
showHelp bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type BootactionMessage struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Details []BootactionDetail `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BootactionDetail struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
IsError bool `json:"error"`
|
||||||
|
}
|
Loading…
Reference in New Issue