Skip to content

Commit

Permalink
Added support/script for HP iLO (version 2 and 3).
Browse files Browse the repository at this point in the history
Fix for GetFromRalph method (not checking response status codes).
  • Loading branch information
Tomasz Mieszkowski committed Jun 10, 2016
1 parent fde8049 commit 7d75a37
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 20 deletions.
9 changes: 4 additions & 5 deletions actions.go
Expand Up @@ -6,15 +6,14 @@ import (
"net/http"
)

// PerformScan runs a scan of a given host using a set of scripts.
// At this moment, we assume that there will be only one script here (idrac.py),
// and that only MAC addresses will be created/updated/deleted in Ralph.
func PerformScan(addrStr string, scripts []string, dryRun bool, cfg *Config, cfgDir string) {
// PerformScan runs a scan of a given host using a script with scriptName.
// At this moment, we assume that only MAC addresses will be created/updated/deleted in Ralph.
func PerformScan(addrStr, scriptName string, dryRun bool, cfg *Config, cfgDir string) {
if dryRun {
// TODO(xor-xor): Wire up logger here.
fmt.Println("INFO: Running in dry-run mode, no changes will be saved in Ralph.")
}
script, err := NewScript(scripts[0], cfgDir)
script, err := NewScript(scriptName, cfgDir)
if err != nil {
log.Fatalln(err)
}
Expand Down
29 changes: 26 additions & 3 deletions bindata.go

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion bundled_scripts/idrac.py
Expand Up @@ -3,7 +3,6 @@
import argparse
import json
import os
import pprint
import re
import uuid
from xml.etree import ElementTree as ET
Expand Down
241 changes: 241 additions & 0 deletions bundled_scripts/ilo.py
@@ -0,0 +1,241 @@
#!/usr/bin/env python

import argparse
import json
import os
import sys
from copy import deepcopy

import hpilo


USER = os.environ['MANAGEMENT_USER_NAME']
PASS = os.environ['MANAGEMENT_USER_PASSWORD']

MAC_PREFIX_BLACKLIST = [
'505054', '33506F', '009876', '000000', '00000C', '204153', '149120',
'020054', 'FEFFFF', '1AF920', '020820', 'DEAD2C', 'FEAD4D',
]

DEVICE_INFO_TEMPLATE = {
"model_name": "",
"processors": [],
"mac_addresses": [],
"disks": [], # unused (hpilo doesn't provide such info)
"serial_number": "",
"memory": [],
}

PROCESSOR_TEMPLATE = {
"model_name": "", # unused (hpilo doesn't provide such info)
"family": "", # hpilo returns int here, but only for iLO2 (for iLO3 this field is empty) # noqa
"label": "",
"index": None, # unused, but similar info is available as "label"
"speed": None,
"cores": None,
}

MEMORY_TEMPLATE = {
"label": "",
"size": None,
"speed": None,
"index": None, # unused, but similar info is available as "label"
}


class IloError(Exception):
pass


def normalize_mac_address(mac_address):
mac_address = mac_address.upper().replace('-', ':')
return mac_address


def get_ilo_instance(host):
ilo = hpilo.Ilo(hostname=host, login=USER, password=PASS, port=443)
return ilo


def _get_macs(raw_macs, ilo_version):
# The data structure for MAC addresses returned from hpilo is pretty nasty,
# especially for iLO3 (no clear distinction between embedded NICs and
# iSCSI ports).
if ilo_version == 3:
start_idx = 0
else:
start_idx = 1
mac_addresses = []
for m in raw_macs:
fields = m.get('fields', [])
for i in range(start_idx, len(fields), 2):
if (
fields[i]['name'] == 'Port' and
fields[i]['value'] != 'iLO' # belongs to mgmt address
):
mac = normalize_mac_address(fields[i + 1]['value'])
if mac[:6] not in MAC_PREFIX_BLACKLIST:
mac_addresses.append(mac)
return mac_addresses


def _get_speed(s):
# sample return value from hpilo: "2533 MHz"
if s is not None:
s = int(s.split(" ")[0])
return s


def _get_processors(raw_procs):

def get_cores(c):
# sample return value from hpilo: "4 of 4 cores; 8 threads"
if c is not None:
c = int(c.split(" of ")[0])
return c

processors = []
for p in raw_procs:
proc = deepcopy(PROCESSOR_TEMPLATE)
proc['family'] = str(p.get('Family', ""))
proc['label'] = p.get('Label', "")
proc['speed'] = _get_speed(p.get('Speed'))
proc['cores'] = get_cores(p.get('Execution Technology'))
processors.append(proc)
return processors


def _get_memory(raw_memory):

def get_size(s):
# sample return value from hpilo: "4096 MB"
if s is not None:
s = int(s.split(" ")[0])
return s

memory = []
for m in raw_memory:
mem = deepcopy(MEMORY_TEMPLATE)
mem['label'] = m.get('Label', "")
mem['size'] = get_size(m.get('Size'))
mem['speed'] = _get_speed(m.get('Speed'))
memory.append(mem)
return memory


# The data structure returned from python-hpilo is quite inconvenient for our
# use-case, therefore we need to reshape it a little bit.
def _prepare_host_data(raw_host_data, ilo_version):
host_data = {
"sys_info": [],
"processors": [],
"memory": [],
"mac_addresses": [],
}
if ilo_version == 2:
for part in raw_host_data:
if part.get('Subject') == 'System Information':
host_data['sys_info'].append(part)
continue
if part.get('Subject') == 'Processor Information':
host_data['processors'].append(part)
continue
if (
part.get('Subject') == 'Memory Device' and
part.get('Size') != 'not installed'
):
host_data['memory'].append(part)
continue
if part.get('Subject') is None and part.get('fields') is not None:
fields = part.get('fields')
for field in fields:
if (
isinstance(field, dict) and
field['value'] == 'Embedded NIC MAC Assignment'
):
host_data['mac_addresses'].append(part)
break
continue
elif ilo_version == 3:
for part in raw_host_data:
if part.get('Product Name') is not None:
host_data['sys_info'].append(part)
continue
if part.get('Execution Technology') is not None:
host_data['processors'].append(part)
continue
if (
part.get('Label') is not None and
part.get('Size') is not None and
part.get('Speed') is not None
):
host_data['memory'].append(part)
continue
if part.get('fields') is not None:
for field in part.get('fields'):
# The condition here is not very reliable, but that's the
# only way to distinguish between 'fields' list containing
# embedded NICs vs. iSCSIs ports.
if (
isinstance(field, dict) and
field['name'] == 'Port' and
field['value'] == 'iLO'
):
host_data['mac_addresses'].append(part)
break
continue
else:
raise IloError("Unknown version of iLO: %d".format(ilo_version))
if len(host_data['sys_info']) > 1:
raise IloError(
"There should be only one 'System Information' dict "
"in the data returned by python-hpilo."
)
return host_data


def get_ilo_version(ilo_manager):
fw_version = ilo_manager.get_fw_version()
if fw_version.get('management_processor') == "iLO3":
ilo_version = 3
elif fw_version.get('management_processor') == "iLO2":
ilo_version = 2
else:
ilo_version = None
return ilo_version


def ilo_device_info(ilo_manager, ilo_version):
raw_host_data = ilo_manager.get_host_data()
host_data = _prepare_host_data(raw_host_data, ilo_version)
device_info = DEVICE_INFO_TEMPLATE
device_info['processors'] = _get_processors(host_data['processors'])
device_info['mac_addresses'] = (
_get_macs(host_data['mac_addresses'], ilo_version)
)
device_info['serial_number'] = (
host_data['sys_info'][0].get('Serial Number', "").strip()
)
device_info['model_name'] = (
host_data['sys_info'][0].get('Product Name', "")
)
device_info['memory'] = _get_memory(host_data['memory'])
return device_info


def scan(host):
ilo_manager = get_ilo_instance(host)
ilo_version = get_ilo_version(ilo_manager)
device_info = ilo_device_info(ilo_manager, ilo_version)
print(json.dumps(device_info))


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('host', nargs=1, help='host to scan')
args = parser.parse_args()
try:
scan(args.host[0])
except (IloError, hpilo.IloCommunicationError) as e:
print(e.args[0])
sys.exit(1)
12 changes: 9 additions & 3 deletions client.go
Expand Up @@ -78,7 +78,7 @@ func (c *Client) SendToRalph(method, endpoint string, data []byte) (statusCode i
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
if resp.StatusCode >= 400 {
body, err := readBody(resp)
if err != nil {
return 0, err
Expand All @@ -97,13 +97,19 @@ func (c *Client) GetFromRalph(endpoint string, query string) ([]byte, error) {
if err != nil {
return []byte{}, err
}

resp, err := c.client.Do(req)
defer resp.Body.Close()
if err != nil {
return []byte{}, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return []byte{}, fmt.Errorf("error while sending a GET request to Ralph: %s",
resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []byte{}, err
}
return body, nil
}

Expand Down
43 changes: 42 additions & 1 deletion client_test.go
Expand Up @@ -53,7 +53,7 @@ func TestSendToRalph(t *testing.T) {
errMsg string
want int
}{
"#0 Ralph responds with >299": {
"#0 Ralph responds with >= 400": {
"POST",
"non-existing-endpoint",
[]byte{},
Expand Down Expand Up @@ -88,3 +88,44 @@ func TestSendToRalph(t *testing.T) {
}

}

func TestGetFromRalph(t *testing.T) {
var cases = map[string]struct {
endpoint string
query string
statusCode int
errMsg string
want []byte
}{
"#0 Ralph responds with >= 400": {
"non-existing-endpoint",
"some_valid_query",
404,
"error while sending a GET request to Ralph",
[]byte{},
},
}

for tn, tc := range cases {
server, client := MockServerClient(tc.statusCode, `{}`)
defer server.Close()

got, err := client.GetFromRalph(tc.endpoint, tc.query)
switch {
case tc.errMsg != "":
if err == nil || !strings.Contains(err.Error(), tc.errMsg) {
t.Errorf("%s\ndidn't get expected string: %q in err msg: %q", tn, tc.errMsg, err)
}
if !TestEqByte(got, tc.want) {
t.Errorf("%s\n got: %v\nwant: %v", tn, got, tc.want)
}
default:
if err != nil {
t.Fatalf("err: %s", err)
}
if !TestEqByte(got, tc.want) {
t.Errorf("%s\n got: %v\nwant: %v", tn, got, tc.want)
}
}
}
}
4 changes: 2 additions & 2 deletions config.go
Expand Up @@ -33,8 +33,8 @@ var DefaultCfg = Config{
ManagementUserPassword: "change_me",
}

// List of scripts that are bundled with ralph-cli (at this moment, only idrac.py).
var bundledScripts = []string{"idrac.py"}
// List of scripts that are bundled with ralph-cli.
var bundledScripts = []string{"idrac.py", "ilo.py"}

// GetCfgDirLocation gets path to current user's home dir and appends ".ralph-cli"
// to it, if baseDir is an empty string, otherwise appends ".ralph-cli" to baseDir path
Expand Down
11 changes: 7 additions & 4 deletions main.go
Expand Up @@ -40,14 +40,17 @@ func main() {
app := cli.App("ralph-cli", "Command-line interface for Ralph")

app.Command("scan", "Perform scan of a given host/network", func(cmd *cli.Cmd) {
addr := cmd.StringArg("ADDR", "", "Address of a host to scan (IP or FQDN)")
scripts := cmd.StringsOpt("scripts", []string{"idrac.py"}, "Scripts to be executed")
addr := cmd.StringArg("IP_ADDR", "", "IP address of a host to scan")
script := cmd.StringOpt("script", "", "Script to be executed")
dryRun := cmd.BoolOpt("dry-run", false, "Don't write anything")

cmd.Spec = "ADDR [--scripts=<scripts>] [--dry-run]"
cmd.Spec = "IP_ADDR --script=<script_name> [--dry-run]"

cmd.Action = func() {
PerformScan(*addr, *scripts, *dryRun, cfg, cfgDir)
if *script == "" {
log.Fatalln("No script supplied to '--script' switch. Aborting.")
}
PerformScan(*addr, *script, *dryRun, cfg, cfgDir)
}
})

Expand Down
3 changes: 2 additions & 1 deletion ralph.go
Expand Up @@ -101,7 +101,8 @@ func (m *MACAddress) MarshalJSON() ([]byte, error) {

// UnmarshalJSON deserializes MACAddress from []byte.
func (m *MACAddress) UnmarshalJSON(data []byte) error {
if string(data) == "\"\"" {
// Most management IPs in Ralph won't be associated with any MAC address.
if string(data) == "\"\"" || string(data) == "null" {
m.HardwareAddr = []byte{}
return nil
}
Expand Down

0 comments on commit 7d75a37

Please sign in to comment.