Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for HP iLO + bugfix for GetFromRalph method. #15

Merged
merged 2 commits into from Jun 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.

25 changes: 17 additions & 8 deletions bundled_scripts/idrac.py
@@ -1,10 +1,9 @@
#!/usr/bin/env python

import argparse
import json
import os
import pprint
import re
import sys
import uuid
from xml.etree import ElementTree as ET

Expand Down Expand Up @@ -342,14 +341,24 @@ def idrac_device_info(idrac_manager):
return device_info


def scan(host):
idrac_manager = IDRAC(host, USER, PASS)
def scan(host, user, password):
if host == "":
raise IdracError("No IP address to scan has been provided.")
if user == "":
raise IdracError("No management username has been provided.")
if host == "":
raise IdracError("No management password has been provided.")
idrac_manager = IDRAC(host, user, password)
device_info = idrac_device_info(idrac_manager)
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()
scan(args.host[0])
host = os.environ.get('IP_TO_SCAN', "")
user = os.environ.get('MANAGEMENT_USER_NAME', "")
password = os.environ.get('MANAGEMENT_USER_PASSWORD', "")
try:
scan(host, user, password)
except IdracError as e:
print(e.args[0])
sys.exit(1)
243 changes: 243 additions & 0 deletions bundled_scripts/ilo.py
@@ -0,0 +1,243 @@
#!/usr/bin/env python
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we plan to test these scripts (on some sample output) from management? We have some tests for it in Ralph2: https://github.com/allegro/ralph/tree/develop/src/ralph/scan/tests/plugins (other thing is how to run these tests here).

Btw we could do some end-to-end testing for calling ralph-cli, mocks response from the management (or just mock script output), and see how data is changed in ralph web (it should be easy to setup on travis using docker). Of course not in this PR, I'm just thinking about it :)

Copy link
Contributor Author

@xor-xor xor-xor Jun 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about testing these scripts too, but as you've noticed, running Python's tests along with Golang's may be tricky/complicated (if not impossible) on e.g. Travis.

And speaking of e2e tests - yes, that may be a sensible way to approach above problem - I'll create a ticket for that.


import json
import os
import sys
from copy import deepcopy

import hpilo


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

DEVICE_INFO_TEMPLATE = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's template for ralph-cli, and since most of our script will be written in python, maybe we could make a simple, tiny python package with base class for the plugin and some utilities like this? it could be installed by default in venv for every python plugin. @xor-xor what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, yes, I think it's a good idea. But IMO it's too soon for such optimizations - there are only two simple scripts here, and not enough common stuff between them to justify the existence of a separate package.

"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, user, password):
ilo = hpilo.Ilo(hostname=host, login=user, password=password)
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, user, password):
if host == "":
raise IloError("No IP address to scan has been provided.")
if user == "":
raise IloError("No management username has been provided.")
if host == "":
raise IloError("No management password has been provided.")
ilo_manager = get_ilo_instance(host, user, password)
ilo_version = get_ilo_version(ilo_manager)
device_info = ilo_device_info(ilo_manager, ilo_version)
print(json.dumps(device_info))


if __name__ == '__main__':
host = os.environ.get('IP_TO_SCAN', "")
user = os.environ.get('MANAGEMENT_USER_NAME', "")
password = os.environ.get('MANAGEMENT_USER_PASSWORD', "")
try:
scan(host, user, password)
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)
}
}
}
}