pull/1/merge
Adam Veldhousen 2019-01-31 05:09:20 +00:00 committed by GitHub
commit 37dfed2471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 264 additions and 153 deletions

5
.gitignore vendored
View File

@ -1,3 +1,6 @@
.vscode
credentials.json
dracli
dracli
viewer.jnlp
.DS_Store
drac

34
.semaphore/semaphore.yml Normal file
View File

@ -0,0 +1,34 @@
version: v1.0
name: Drac Pipeline
agent:
machine:
type: e1-standard-2
os_image: ubuntu1804
blocks:
- name: "Vet"
task:
jobs:
- name: Test
commands:
- checkout
- make setup
- echo "make test"
- name: Lint code
commands:
- checkout
- make setup
- echo "make lint"
- name: "Build"
task:
env_vars:
- name: APP_ENV
value: prod
jobs:
- name: build
commands:
- checkout
- make setup
- make dev

View File

@ -1,5 +1,11 @@
# DRACLI
[![GoDoc](https://godoc.org/github.com/adamveld12/dracli?status.svg)](http://godoc.org/github.com/adamveld12/dracli)
[![Go Walker](http://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/adamveld12/dracli)
[![Gocover](http://gocover.io/_badge/github.com/adamveld12/dracli)](http://gocover.io/github.com/adamveld12/dracli)
[![Go Report Card](https://goreportcard.com/badge/github.com/adamveld12/dracli)](https://goreportcard.com/report/github.com/adamveld12/dracli)
[![Build Status](https://semaphoreci.com/api/v1/adamveld12/dracli/branches/master/badge.svg)](https://semaphoreci.com/adamveld12/dracli)
A quick and dirty CLI/client library for the Integrated Dell Remote Access Controller v6.
## CLI Usage

307
client.go
View File

@ -14,6 +14,165 @@ import (
xj "github.com/basgys/goxml2json"
)
//Client is an http client that talks to the iDRAC
type Client struct {
client *http.Client
Username string
AuthToken string
Host string
}
func (c *Client) doHTTP(path, qs string, body io.Reader) (string, []*http.Cookie, error) {
method := "GET"
if strings.ContainsAny(qs, "set") {
method = "POST"
}
uri := fmt.Sprintf("https://%s/data/%s?%s", c.Host, path, qs)
req, _ := http.NewRequest(method, uri, body)
req.AddCookie(&http.Cookie{
Name: "_appwebSessionId_",
Value: c.AuthToken,
})
res, err := c.client.Do(req)
if err != nil {
return "", nil, errors.New("could not make request")
}
defer res.Body.Close()
jsonData, err := xj.Convert(res.Body)
if err != nil {
return "", nil, err
}
if res.StatusCode >= 300 || res.StatusCode < 200 {
return jsonData.String(), res.Cookies(), fmt.Errorf("got a non 200 status: %d", res.StatusCode)
}
return jsonData.String(), res.Cookies(), nil
}
//OpenConsole downloads the jnlp that opens the console
func (c *Client) OpenConsole() error {
//https://192.168.0.228/viewer.jnlp(192.168.0.228@0@root@1548390931405)
uri := fmt.Sprintf("https://%s/viewer.jnlp(%s@0@%s@%d)", c.Host, c.Host, c.Username, time.Now().Unix())
req, _ := http.NewRequest("GET", uri, nil)
req.AddCookie(&http.Cookie{
Name: "_appwebSessionId_",
Value: c.AuthToken,
})
res, err := c.client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return errors.New("bad status")
}
f, err := os.Create("./viewer.jnlp")
if err != nil {
return errors.New("could not download")
}
defer f.Close()
if _, err := io.Copy(f, res.Body); err != nil {
return err
}
return nil
}
//SetPowerState changes the power state of the server (ie turn off or on)
func (c *Client) SetPowerState(ps PowerState) (string, error) {
res, _, err := c.doHTTP("", fmt.Sprintf("set=pwState:%d", ps), nil)
return res, err
}
//SetBootOverride changes the boot override settings
func (c *Client) SetBootOverride(bo BootDevice, bootOnce bool) (string, error) {
res, _, err := c.doHTTP("", fmt.Sprintf("set=vmBootOnce:%v,firstBootDevice:%d", bootOnce, bo), nil)
return res, err
}
//Query queries attributes and returns them to json
func (c *Client) Query(ds ...Attribute) (string, error) {
bufs := bytes.NewBufferString("")
for idx, attr := range ds {
bufs.WriteString(string(attr))
if idx < len(ds)-1 {
bufs.WriteString(",")
}
}
res, _, err := c.doHTTP("", fmt.Sprintf("get=%s", bufs.String()), nil)
return res, err
}
//NewFromCredentials creates a new client from a credentials file at the specified path
func NewFromCredentials(path string) (*Client, error) {
credential, err := LoadCredentials(path)
if err != nil {
if os.IsNotExist(err) {
return nil, errors.New("You should log in first")
}
return nil, err
}
c, err := NewClient(credential.Host, true)
if err != nil {
return nil, err
}
c.AuthToken = credential.AuthToken
c.Username = credential.Username
return c, nil
}
//NewClient creates a new Client
func NewClient(host string, skipVerify bool) (*Client, error) {
c := &http.Client{
Timeout: time.Second * 5,
Transport: &http.Transport{
IdleConnTimeout: time.Second,
TLSHandshakeTimeout: time.Second * 5,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipVerify,
},
},
}
client := &Client{client: c, Host: host}
// err := client.Login(host, username, password)
// if err != nil {
// return nil, err
// }
return client, nil
}
//Login sends a login http request to the iDRAC
func (c *Client) Login(username, password string) (string, error) {
body := bytes.NewBufferString(fmt.Sprintf("user=%s&password=%s", username, password))
_, cookies, err := c.doHTTP("login", "", body)
if err != nil {
return "", err
}
for _, cookie := range cookies {
if cookie.Name == "_appwebSessionId_" {
c.AuthToken = cookie.Value
return cookie.Value, nil
}
}
return "", errors.New("could not find auth token in cookie")
}
var (
PowerStatus = Attribute("pwState")
SystemDescription = Attribute("sysDesc")
@ -54,139 +213,25 @@ var (
PowerOff = PowerState(0)
PowerOn = PowerState(1)
NonMaskingInterrupt = PowerState(2)
GracefulShutdown = PowerState(3)
ColdReboot = PowerState(4)
WarmReboot = PowerState(5)
NoOverride = BootDevice(0)
PXE = BootDevice(1)
HardDrive = BootDevice(2)
BIOS = BootDevice(6)
VirtualCD = BootDevice(8)
LocalSD = BootDevice(16)
LocalCD = BootDevice(5)
ColdReboot = PowerState(2)
WarmReboot = PowerState(3)
NonMaskingInterrupt = PowerState(4)
GracefulShutdown = PowerState(5)
NoOverride = BootDevice(0)
PXE = BootDevice(1)
HardDrive = BootDevice(2)
BIOS = BootDevice(6)
VirtualCD = BootDevice(8)
LocalSD = BootDevice(16)
LocalCD = BootDevice(5)
)
//Attribute is an attribute that can be queried
type Attribute string
//PowerState represents a powerstate option
type PowerState uint8
//BootDevice represents a bootdevice option
type BootDevice uint8
type SensorType struct {
}
type Client struct {
client *http.Client
AuthToken string
Host string
}
func (c *Client) doHTTP(path, qs string, body io.Reader) (string, []*http.Cookie, error) {
method := "GET"
if strings.ContainsAny(qs, "set") {
method = "POST"
}
uri := fmt.Sprintf("https://%s/data/%s?%s", c.Host, path, qs)
req, _ := http.NewRequest(method, uri, body)
req.AddCookie(&http.Cookie{
Name: "_appwebSessionId_",
Value: c.AuthToken,
})
res, err := c.client.Do(req)
if err != nil {
return "", nil, errors.New("could not make request")
}
defer res.Body.Close()
jsonData, err := xj.Convert(res.Body)
if err != nil {
return "", nil, err
}
if res.StatusCode >= 300 || res.StatusCode < 200 {
return jsonData.String(), res.Cookies(), fmt.Errorf("got a non 200 status: %d", res.StatusCode)
}
return jsonData.String(), res.Cookies(), nil
}
func (c *Client) SetPowerState(ps PowerState) (string, error) {
res, _, err := c.doHTTP("", fmt.Sprintf("set=pwState:%d", ps), nil)
return res, err
}
func (c *Client) SetBootOverride(bo BootDevice, bootOnce bool) (string, error) {
res, _, err := c.doHTTP("", fmt.Sprintf("set=vmBootOnce:%v,firstBootDevice:%d", bootOnce, bo), nil)
return res, err
}
func (c *Client) Query(ds ...Attribute) (string, error) {
bufs := bytes.NewBufferString("")
for idx, attr := range ds {
bufs.WriteString(string(attr))
if idx < len(ds)-1 {
bufs.WriteString(",")
}
}
res, _, err := c.doHTTP("", fmt.Sprintf("get=%s", bufs.String()), nil)
return res, err
}
func NewFromCredentials(path string) (*Client, error) {
credential, err := LoadCredentials(path)
if err != nil {
if os.IsNotExist(err) {
return nil, errors.New("You should log in first")
}
return nil, err
}
c, err := NewClient(credential.Host, true)
if err != nil {
return nil, err
}
c.AuthToken = credential.AuthToken
return c, nil
}
func NewClient(host string, skipVerify bool) (*Client, error) {
c := &http.Client{
Timeout: time.Second * 5,
Transport: &http.Transport{
IdleConnTimeout: time.Second,
TLSHandshakeTimeout: time.Second * 5,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipVerify,
},
},
}
client := &Client{client: c, Host: host}
// err := client.Login(host, username, password)
// if err != nil {
// return nil, err
// }
return client, nil
}
func (c *Client) Login(username, password string) (string, error) {
body := bytes.NewBufferString(fmt.Sprintf("user=%s&password=%s", username, password))
_, cookies, err := c.doHTTP("login", "", body)
if err != nil {
return "", err
}
for _, cookie := range cookies {
if cookie.Name == "_appwebSessionId_" {
c.AuthToken = cookie.Value
return cookie.Value, nil
}
}
return "", errors.New("could not find auth token in cookie")
}

View File

@ -6,11 +6,14 @@ import (
"os"
)
//Credential represents credentials for the credentials json
type Credential struct {
Host string
Username string
AuthToken string
}
//LoadCredentials from a credentials json file
func LoadCredentials(path string) (*Credential, error) {
fp := fmt.Sprintf("%s/credentials.json", path)
f, err := os.Open(fp)
@ -28,6 +31,7 @@ func LoadCredentials(path string) (*Credential, error) {
return credential, nil
}
//SaveCredentials saves credentials to a json file
func SaveCredentials(path string, c Credential) error {
f, err := os.Create(fmt.Sprintf("%s/credentials.json", path))
if err != nil {

30
main.go
View File

@ -12,11 +12,12 @@ import (
var (
commands = map[string]func(map[string][]string) error{
"login": loginAction,
"logout": logoutAction,
"power": powerStateAction,
"query": queryAction,
"help": helpAction,
"login": loginAction,
"logout": logoutAction,
"console": consoleAction,
"power": powerStateAction,
"query": queryAction,
"help": helpAction,
}
queryHelp = []Attribute{
@ -60,7 +61,7 @@ var (
)
func main() {
c, err := ToCommand(os.Args[1:]...)
c, err := toCommand(os.Args[1:]...)
if err != nil {
log.Println(err)
os.Exit(-1)
@ -172,6 +173,15 @@ func queryAction(args map[string][]string) error {
return nil
}
func consoleAction(args map[string][]string) error {
c, err := NewFromCredentials(".")
if err != nil || os.IsNotExist(err) {
return errors.New("you are already logged in")
}
return c.OpenConsole()
}
func loginAction(args map[string][]string) error {
u, ok1 := args["u"]
p, ok2 := args["p"]
@ -236,17 +246,17 @@ func helpAction(args map[string][]string) error {
return nil
}
type Command struct {
type command struct {
Name string
Arguments map[string][]string
}
func ToCommand(args ...string) (Command, error) {
func toCommand(args ...string) (command, error) {
if len(args) < 1 {
return Command{}, nil
return command{}, nil
}
c := Command{
c := command{
Name: args[0],
Arguments: map[string][]string{},
}

View File

@ -6,17 +6,17 @@ import (
"testing"
)
func Test_ToCommand(t *testing.T) {
func Test_toCommand(t *testing.T) {
tests := []struct {
name string
args string
want Command
want command
wantErr bool
}{
{
name: "login works",
args: "login -u root -p calvin 10.0.0.5",
want: Command{
want: command{
Name: "login",
Arguments: map[string][]string{
"u": []string{"root"},
@ -29,7 +29,7 @@ func Test_ToCommand(t *testing.T) {
{
name: "boot settings",
args: "boot_settings -once local_cd",
want: Command{
want: command{
Name: "boot_settings",
Arguments: map[string][]string{
"once": []string{"true"},
@ -41,7 +41,7 @@ func Test_ToCommand(t *testing.T) {
{
name: "query multi value",
args: "query pwState kvmEnabled voltages temperatures",
want: Command{
want: command{
Name: "query",
Arguments: map[string][]string{
"": []string{"pwState", "kvmEnabled", "voltages", "temperatures"},
@ -53,13 +53,13 @@ func Test_ToCommand(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := strings.Split(tt.args, " ")
got, err := ToCommand(args...)
got, err := toCommand(args...)
if (err != nil) != tt.wantErr {
t.Errorf("ToCommand() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("toCommand() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToCommand() = %v, want %v", got, tt.want)
t.Errorf("toCommand() = %v, want %v", got, tt.want)
}
})
}

View File

@ -1,5 +1,6 @@
.PHONY: clean debug dev test
.PHONY: check clean debug dev test
ci: clean setup lint test
clean:
@rm -rf ./dracli
@ -10,7 +11,15 @@ debug:
dlv debug --headless --api-version=2 -l 127.0.0.1:2456 -- query pwState
dracli:
@go build -o dracli
@go build -o dracli
test:
lint:
go get golang.org/x/lint/golint
golint -min_confidence 1.5 -set_exit_status
go vet -all -v
setup:
@go get -t -d -v ./...
test:
@go test -v -cover