master
Adam Veldhousen 5 years ago
commit fe4555465a

3
.gitignore vendored

@ -0,0 +1,3 @@
.vscode
credentials.json
dracli

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Adam Veldhousen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,45 @@
# DRACLI
A quick and dirty CLI/client library for the Integrated Dell Remote Access Controller v6.
## CLI Usage
I recommend that you setup a new user in the iDRAC admin specifically for this CLI tool. It will prevent you from getting locked out of the main account if you log in too many times.
If you do happen to lock yourself out, you can SSH into the iDRAC with your root credentials and run `racadm racreset`
```sh
# start by using login - stores at ~/.dracli/credentials.json
dracli login -u root -p calvin -h 10.0.0.5
# manage power state
dracli power [on|off|nmi|graceful_shutdown|cold_reboot|warm_reboot]
# query info about your server
dracli query pwState sysDesc fans
# query info about your server continuously
dracli query -watch 1s pwState sysDesc fans
# list help
dracli help
# log out removes credentials
dracli logout
```
## Future Features
- xml output
- manage/query multiple servers simultaneously
- remote console
- run virtual console locally (requires java)
- manage user accounts
## LICENSE
MIT

@ -0,0 +1,192 @@
package main
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
xj "github.com/basgys/goxml2json"
)
var (
PowerStatus = Attribute("pwState")
SystemDescription = Attribute("sysDesc")
SystemRevision = Attribute("sysRev")
HostName = Attribute("hostName")
OSName = Attribute("osName")
OSVersion = Attribute("osVersion")
ServiceTag = Attribute("svcTag")
ExpServiceCode = Attribute("expSvcCode")
BiosVersion = Attribute("biosVer")
FirmwareVersion = Attribute("fwVersion")
LCCFirmwareVersion = Attribute("LCCfwVersion")
IPV4Enabled = Attribute("v4Enabled")
IPV4Address = Attribute("v4IPAddr")
IPV6Enabled = Attribute("v6Enabled")
IPV6LinkLocal = Attribute("v6LinkLocal")
IPV6Address = Attribute("v6Addr")
IPV6SiteLocal = Attribute("v6SiteLocal")
MacAddress = Attribute("macAddr")
Batteries = Attribute("batteries")
FanRedundancy = Attribute("fansRedundancy")
Fans = Attribute("fans")
Intrusion = Attribute("intrusion")
PowerSupplyRedundancy = Attribute("psRedundancy")
PowerSupplies = Attribute("powerSupplies")
RMVRedundancy = Attribute("rmvsRedundancy")
RemovableStorage = Attribute("removableStorage")
Temperatures = Attribute("temperatures")
Voltages = Attribute("voltages")
KVMEnabled = Attribute("kvmEnabled")
PowerBudgetData = Attribute("budgetpowerdata")
EventLog = Attribute("eventLogEntries")
BootOnce = Attribute("vmBootOnce")
FirstBootDevice = Attribute("firstBootDevice")
VFKLicense = Attribute("vfkLicense")
User = Attribute("user")
IDRACLog = Attribute("racLogEntries")
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)
)
type Attribute string
type PowerState uint8
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")
}

@ -0,0 +1,44 @@
package main
import (
"encoding/json"
"fmt"
"os"
)
type Credential struct {
Host string
AuthToken string
}
func LoadCredentials(path string) (*Credential, error) {
fp := fmt.Sprintf("%s/credentials.json", path)
f, err := os.Open(fp)
if err != nil {
return nil, err
}
defer f.Close()
credential := &Credential{}
dec := json.NewDecoder(f)
if err := dec.Decode(&credential); err != nil {
return nil, err
}
return credential, nil
}
func SaveCredentials(path string, c Credential) error {
f, err := os.Create(fmt.Sprintf("%s/credentials.json", path))
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", "\t")
if err := enc.Encode(c); err != nil {
return err
}
return nil
}

@ -0,0 +1,7 @@
module github.com/adamveld12/dracli
require (
github.com/basgys/goxml2json v1.1.0
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect
golang.org/x/text v0.3.0 // indirect
)

@ -0,0 +1,6 @@
github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw=
github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

@ -0,0 +1,276 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/signal"
"time"
)
var (
commands = map[string]func(map[string][]string) error{
"login": loginAction,
"logout": logoutAction,
"power": powerStateAction,
"query": queryAction,
"help": helpAction,
}
queryHelp = []Attribute{
PowerStatus,
SystemDescription,
SystemRevision,
HostName,
OSName,
OSVersion,
ServiceTag,
ExpServiceCode,
BiosVersion,
FirmwareVersion,
LCCFirmwareVersion,
IPV4Enabled,
IPV4Address,
IPV6Enabled,
IPV6LinkLocal,
IPV6Address,
IPV6SiteLocal,
MacAddress,
Batteries,
FanRedundancy,
Fans,
Intrusion,
PowerSupplyRedundancy,
PowerSupplies,
RMVRedundancy,
RemovableStorage,
Temperatures,
Voltages,
KVMEnabled,
PowerBudgetData,
EventLog,
BootOnce,
FirstBootDevice,
VFKLicense,
User,
IDRACLog,
}
)
func main() {
c, err := ToCommand(os.Args[1:]...)
if err != nil {
log.Println(err)
os.Exit(-1)
}
action, ok := commands[c.Name]
if !ok {
fmt.Printf("The command \"%s\" was not found.\n", c.Name)
os.Exit(-1)
}
if err := action(c.Arguments); err != nil {
fmt.Printf("The command exited with an error:\n%v\n", err)
os.Exit(-1)
}
}
func powerStateAction(args map[string][]string) error {
c, err := NewFromCredentials(".")
if err != nil {
return err
}
rawPs, ok := args[""]
if !ok || len(rawPs) == 0 {
return errors.New("specify a power state (on|off|cold_reboot|warm_reboot)")
}
var ps PowerState
switch rawPs[0] {
case "on":
ps = PowerOn
case "off":
ps = PowerOff
case "cold_reboot":
ps = ColdReboot
case "warm_reboot":
ps = WarmReboot
case "nmi":
ps = NonMaskingInterrupt
case "graceful_shutdown":
ps = GracefulShutdown
default:
return errors.New("specify a power state (on|off|cold_reboot|warm_reboot)")
}
res, err := c.SetPowerState(ps)
if err != nil {
return err
}
fmt.Printf("%s\n", res)
return nil
}
func queryAction(args map[string][]string) error {
c, err := NewFromCredentials(".")
if err != nil {
return err
}
queryParams, ok := args[""]
if !ok {
return errors.New("you should pass query parameters")
}
qps := []Attribute{}
for _, qp := range queryParams {
qps = append(qps, Attribute(qp))
}
output, err := c.Query(qps...)
if err != nil {
return err
}
fmt.Printf("%s", output)
watch, watchOk := args["watch"]
if watchOk && len(watch) > 0 {
duration, err := time.ParseDuration(watch[0])
if err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-time.After(duration):
output, err := c.Query(qps...)
if err != nil {
return
}
fmt.Printf("%s", output)
case <-ctx.Done():
return
}
}
}()
notifChan := make(chan os.Signal)
signal.Notify(notifChan, os.Interrupt)
<-notifChan
cancel()
}
return nil
}
func loginAction(args map[string][]string) error {
u, ok1 := args["u"]
p, ok2 := args["p"]
h, ok3 := args["h"]
if !ok1 || !ok2 || !ok3 {
return errors.New("username (-u), password (-p), and a host (-h) must be defined")
}
username, password, host := u[0], p[0], h[0]
credential, err := LoadCredentials(".")
if (err == nil || os.IsExist(err)) && credential.Host == host {
return errors.New("you are already logged in")
}
fmt.Printf("logging in to %s:%s@%s\n", username, password, host)
client, err := NewClient(host, true)
if err != nil {
return err
}
authToken, err := client.Login(username, password)
if err != nil {
return err
}
if err := SaveCredentials(".", Credential{
Host: host,
AuthToken: authToken,
}); err != nil {
return err
}
return nil
}
func logoutAction(args map[string][]string) error {
_, err := LoadCredentials(".")
if err == nil && os.IsNotExist(err) {
return nil
}
if err := os.Remove("./credentials.json"); err != nil {
return err
}
return nil
}
func helpAction(args map[string][]string) error {
fmt.Println("login -u [username] -p [password] -h [host]: logs you in")
fmt.Println("logout: logs you out")
fmt.Println("power [on|off|nmi|graceful_shutdown|cold_reboot|warm_reboot]: manage power state of the server")
fmt.Printf("query [-watch 1[s|m|h]] <attribute>,<attribute2>...: gets info about the server's various sensors and attributes\nPossible attributes:")
for idx, q := range queryHelp {
if idx%10 == 0 {
fmt.Println()
}
fmt.Printf("%s ", q)
}
fmt.Println()
return nil
}
type Command struct {
Name string
Arguments map[string][]string
}
func ToCommand(args ...string) (Command, error) {
if len(args) < 1 {
return Command{}, nil
}
c := Command{
Name: args[0],
Arguments: map[string][]string{},
}
for i := 1; i < len(args); i++ {
arg := args[i]
if arg[0] == '-' {
argumentV := "true"
if len(args) > i+1 && arg[1:] != "once" {
argumentV = args[i+1]
i++
}
c.Arguments[arg[1:]] = []string{argumentV}
} else {
v, ok := c.Arguments[""]
if ok {
v = append(v, arg)
c.Arguments[""] = v
} else {
c.Arguments[""] = []string{arg}
}
}
}
return c, nil
}

@ -0,0 +1,66 @@
package main
import (
"reflect"
"strings"
"testing"
)
func Test_ToCommand(t *testing.T) {
tests := []struct {
name string
args string
want Command
wantErr bool
}{
{
name: "login works",
args: "login -u root -p calvin 10.0.0.5",
want: Command{
Name: "login",
Arguments: map[string][]string{
"u": []string{"root"},
"p": []string{"calvin"},
"": []string{"10.0.0.5"},
},
},
},
{
name: "boot settings",
args: "boot_settings -once local_cd",
want: Command{
Name: "boot_settings",
Arguments: map[string][]string{
"once": []string{"true"},
"": []string{"local_cd"},
},
},
},
{
name: "query multi value",
args: "query pwState kvmEnabled voltages temperatures",
want: Command{
Name: "query",
Arguments: map[string][]string{
"": []string{"pwState", "kvmEnabled", "voltages", "temperatures"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := strings.Split(tt.args, " ")
got, err := ToCommand(args...)
if (err != nil) != 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)
}
})
}
}

@ -0,0 +1,16 @@
.PHONY: clean debug dev test
clean:
@rm -rf ./dracli
dev: clean dracli
debug:
dlv debug --headless --api-version=2 -l 127.0.0.1:2456 -- query pwState
dracli:
@go build -o dracli
test:
@go test -v -cover
Loading…
Cancel
Save