Have you modified Ralph to suit your needs?
Share your story here, to help other Ralph users, or get support
Have you modified Ralph to suit your needs?
Share your story here, to help other Ralph users, or get support
I use/evaluate RALPH together with FAI(https://fai-project.org/). I have modified the FAI scripts so that the FAI configuration variables are determined from RALPH custom fields. Additionally I determine the hardware configuration of a system from the lshw output using python and send that back to RALPH.
If there is interest I can share this script(it is around 300 lines of python…)
External deployments integration is something we’d definitely want to see, even if it’s 300 lines of python scripts Go ahead!
OK, there you go… The script had to be usable in the network boot environment, therefore I did not want to go overboard with respect to the employed python modules. I’ve just started out with python, so if you see room for improvement just comment here!
#! /usr/bin/python3
import json
import os
import urllib.request
import urllib.parse
from subprocess import Popen, PIPE
import xml.etree.ElementTree as ET
def eraseoldcomponents(old, headers):
for comp in old:
request=urllib.request.Request(comp['url'], headers=headers, method = 'DELETE')
res = urllib.request.urlopen(request, timeout=5)
def patchcomponent(url, inp, headers):
request=urllib.request.Request(url, data = json.dumps(inp).encode('utf-8'), headers=headers, method = 'PATCH')
res = urllib.request.urlopen(request, timeout=5)
def createcomponent(url, data, headers):
request=urllib.request.Request(url, data = json.dumps(data).encode('utf-8'), headers=headers, method = 'POST')
res = urllib.request.urlopen(request, timeout=5)
def inquirecomponent(url, headers):
request=urllib.request.Request(url, headers=headers, method = 'GET')
return json.loads(urllib.request.urlopen(request).read().decode('utf-8'))
def getDMIvalue(val):
process = Popen(["cat", "/sys/devices/virtual/dmi/id/" + val], stdout=PIPE)
(retval, err) = process.communicate()
exit_code = process.wait();
return retval.decode('utf-8').strip(' \t\n\r')
hostname=os.environ['HOSTNAME']
ipaddr = os.environ['IPADDR'] # from FAI
mac = os.environ['MAC'] #from FAI
#set the default basic config string of FAI that we will print at the end.
configstring = 'FAIBASE DEBIAN NETWORK_CLIENT '
faiserver='http://ralphserver:31415/api/'
# Set the HTTP Request headers
headers={}
headers['Authorization']='Token <Authtoken>'
headers['Content-Type'] = 'application/json'
res = inquirecomponent(faiserver + 'data-center-assets/?hostname=%s'%hostname, headers)
if res['count'] == 0: # here we have to catch the case with the defaults in case this computer has not been configured in RALPH
if 'xxx1' in hostname:
print(configstring + 'ORG LDAPCLIENT DESKTOP')
quit()
if 'xxx2' in hostname:
print(configstring + 'ORG LDAPCLIENT DESKTOP')
quit()
if 'xxx3' in hostname:
print(configstring + 'LDAPCLIENT DESKTOP DEV ORG2 GERMAN')
quit()
myres=res['results'][0]
#let's obtain the department
if myres['property_of'] is not None:
dept = myres['property_of']['name']
if dept == 'ORG':
configstring += 'ORG LDAPCLIENT '
if dept == 'DEPT1' or dept == 'DEPT2' or dept == 'DEPT3' or dept == 'DEPT4' or dept == 'ORG2':
configstring += 'ORG LDAPCLIENT ' + dept + ' '
if dept == 'EXTORG':
configstring += 'EXTORG LDAPCLIENT '
# The Service Environments set the fai_variables key with the respective FAI classes that should be used.
# That means we can obtain the configuration just from this key
configstring = configstring + myres['configuration_variables']['fai_variables'] + ' '
#let's obtain additional FAI variables specified by the user for this node:
if 'fai_additional' in myres['configuration_variables']:
configstring += myres['configuration_variables']['fai_additional'] + ' '
# let's begin the hardware detection
process = Popen(["lshw", "-xml", "-disable", "isapnp", "-disable", "pcmcia", "-disable", "usb"], stdout=PIPE)
(output, err) = process.communicate()
exit_code = process.wait();
hw=ET.XML(output)
# let's begin by enumerating the ethernet devices and associate them with the respective asset.
ethers = hw.findall(".//node[@class='network']")
# Determine MAC addresses and some type description
speeddict={#This Dictionary maps strings that are returned by lshw to the integers used by RALPH. Currently this was tested with some desktop onboard Intel cards.
'10Mbit/s' : 1,
'100Mbit/s' : 2,
'1Gbit/s' : 3,
'10Gbit/s' : 4,
'40Gbit/s' : 5,
'100Gbit/s' : 6,
}
for index, el in enumerate(ethers):
if (el.get('disabled') != "true") and (el.find('logicalname') != None):
# check first wether we have such a device...
res = inquirecomponent(faiserver+'ethernets/?mac=%s'%el.find('serial').text, headers)
if res['count'] != 0:
eraseoldcomponents(res['results'], headers)
myetherdict = {'mac': el.find('serial').text, 'base_object': myres['id']}
fwversion = el.find('configuration/setting/[@id="firmware"]')
if fwversion != None:
myetherdict['firmware_version'] = fwversion.get('value')
modelname = ""
ven = el.find('vendor')
if ven != None:
modelname = modelname + ven.text
prod = el.find('product')
if prod != None:
modelname = modelname + " " + prod.text
if len(modelname) > 1:
myetherdict['model_name'] = modelname
labelname = "net_" + hostname
if len(ethers) > 1:
labelname = labelname + "(" + str(index) + ")"
myetherdict['label'] = labelname
#try to inquire the link speed:
speed = el.find('configuration/setting/[@id="speed"]')
if speed != None:
if speed.get('value') in speeddict:
myetherdict['speed'] = speeddict[speed.get('value')]
createcomponent(faiserver+'ethernets/', myetherdict, headers)
# Now let's move on to register the IP address that we use while we are installing with the MAC that we use while installing.
res = inquirecomponent(faiserver+'ipaddresses/?address=%s'%ipaddr, headers)
if res['count'] != 0:
eraseoldcomponents(res['results'], headers)
myipdict={
"address" : ipaddr, # This info comes from FAI.
# "hostname" : hostname, # ralph seems to be able to figure out the hostname by itself
"is_management" : "false",
"status" : 1,
}
# let's find the RALPH Id of the associated ethernet device
res = inquirecomponent(faiserver+'ethernets/?mac=%s'%mac, headers)
myipdict['ethernet'] = res['results'][0]['id']
createcomponent(faiserver+'ipaddresses/', myipdict, headers)
# Let's add the CPU type
# first we have to delete the old present CPUs since they might not be up-to-date
eraseoldcomponents(myres['processors'], headers)
# Add the current CPUs
numcpu = 0
cpus = hw.findall(".//node[@class='processor']")
for cpu in cpus:
if cpu.get('disabled') != "true":
# some cpus don't report the 'capacity' which seems to be the TOP Turboboost bin.
# Hence in those cases we have to use 'size' which gives the current frequency
freq = cpu.find('capacity')
if freq == None:
freq = cpu.find('size')
cores = 1
cpuconfig = cpu.find('configuration')
if cpuconfig != None:
cores = int(cpuconfig.find('setting/[@id="enabledcores"]').get('value'))
cpudict = {
"model_name" : cpu.find('vendor').text + " " + cpu.find('product').text,
"speed" : str(int(float(freq.text)/1000/1000)),
"cores" : cores,
"base_object" : myres['id']
}
numcpu = numcpu + 1
createcomponent(faiserver+'processors/', cpudict, headers)
# Now let's add the information about the various memory DIMMs
# Let's tidy up first, to erase all stale information
eraseoldcomponents(myres['memory'], headers)
# Find System Memory Node -> If Nodes enumerate Nodes, else create a dummy Node with the entire size
# inquire all memory element types
mems = hw.findall(".//node[@class='memory']")
# Find the one System Memory. Which is the node that has a child called description with value System Memory. There can be multiples of these nodes...
sysmems = []
for el in mems:
sysdesc = el.find('description')
if sysdesc != None:
desc = sysdesc.text
if desc.lower() == "system memory":
sysmems.append(el)
totalmem = 0
for sysmem in sysmems:
dimms = sysmem.findall(".//node[@class='memory']")
numdimms = 0
for dimm in dimms:
dimmsize = dimm.find('size')
if dimmsize != None: # work around BIOS that report DIMMs that are empty
modelname = dimm.find('vendor').text
prod = dimm.find('product')
if prod != None:
modelname = modelname + " " + prod.text
memdict = {
"base_object" : myres['id'],
"model_name" : modelname,
"size" : int(float(dimmsize.text)/1024/1024)
}
clock = dimm.find('clock')
if clock != None:
memdict['speed'] = int(float(dimm.find('clock').text)/1000/1000)
totalmem = totalmem + int(dimmsize.text)
numdimms = numdimms + 1
createcomponent(faiserver+'memory/', memdict, headers)
if numdimms == 0:# No dimm had suitable information. As a fallback we recover that information from the system memory component key
for sysmem in sysmems:
memsize = sysmem.find('size')
if memsize != None:
memdict = {
"base_object" : myres['id'],
"model_name" : "DIMM",
"size" : int(float(memsize.text)/1024/1024)
}
# let's move on to the disks
# first let's erase the old ones
eraseoldcomponents(myres['disk'], headers)
# start the detection
find_disks = hw.findall(".//node[@class='disk']")
numdisks = 0
for disk in find_disks:
# has to be a hard-disk
if disk.find('size') is not None:
diskdict = {
"base_object" : myres['id'],
"size" : int(float(disk.find('size').text)/1024/1024/1024)
}
numdisks = numdisks + 1
diskserial = disk.find('serial')
if diskserial != None:
diskdict['serial_number'] = diskserial.text
modelname = ""
vendor = disk.find('vendor')
if vendor != None:
modelname = modelname + vendor.text
prod = disk.find('product')
if prod != None:
modelname = modelname + " " + prod.text
logicalname = disk.find('logicalname')
if logicalname != None:
modelname = modelname + " (" + logicalname.text + ")"
diskdict['model_name'] = modelname
createcomponent(faiserver+'disks/', diskdict, headers)
if numdisks > 1:
configstring = configstring + " SYSTEMSSD"
# Now let's move on to the Basic Info:
#The logic is like this:
#If the associated model name is dummy, then we try to find a new version. Else we do not touch this field
if myres['model']['name'].lower() == "dummy":
productname = getDMIvalue("product_name")
modelres = inquirecomponent(faiserver+'assetmodels/?name=%s'%urllib.parse.quote_plus(productname), headers)
if modelres['count'] > 0: # Model exists, no need to create a new one
mydata = {'model' : modelres['results'][0]['id']}
else:
#Let's create a new model
vendor = getDMIvalue("sys_vendor")
vendorid = None
# let's check wether that vendor exists
res = inquirecomponent(faiserver+'manufacturers/?name=%s'%urllib.parse.quote_plus(vendor), headers)
if res['count'] == 0: # We have to create a manufacturer
manufacturerdict = {'name' : vendor}
createcomponent(faiserver+'manufacturers/', manufacturerdict, headers)
#Now let's retrieve the ID
res = inquirecomponent(faiserver+'manufacturers/?name=%s'%urllib.parse.quote_plus(vendor), headers)
vendorid = res['results'][0]['id']
categ = 1 # Maps to the Desktop class
if int(getDMIvalue("chassis_type")) == 23: #Rack Mount Server
categ = 2 # Maps to the Server class
modeldata={
'name' : productname,
'type' : 2,
'cores_count' : numcpu, #NOTE: In the core count field we store the number of sockets!!
'visualization_layout_front' : 8,
'visualization_layout_front' : 8,
'manufacturer' : vendorid,
'category' : categ,
'power_consumption' : 300,
'height_of_device' : 1,
}
#register the new model and retrieve the respective RALPH-Node
createcomponent(faiserver+'assetmodels/', modeldata, headers)
modelres = inquirecomponent(faiserver+'assetmodels/?name=%s'%urllib.parse.quote_plus(productname), headers)
mydata = {'model' : modelres['results'][0]['id']}
patchcomponent(myres['url'], mydata, headers)
#let's populate the BIOS Field
biosstring = getDMIvalue("bios_vendor") + " " + getDMIvalue("bios_version")
mydata = {'bios_version' : biosstring}
patchcomponent(myres['url'], mydata, headers)
# let's populate the S/N field
mydata = {'sn' : getDMIvalue("product_serial")}
patchcomponent(myres['url'], mydata, headers)
print(configstring)# Output the final configuration string for consumption by FAI
NVMe disks are not supported by lshw currently… hence I had to replace disk detection with lsblk:
process = Popen(["lsblk", "-OJbd"], stdout=PIPE)
(lsblk, err) = process.communicate()
exit_code = process.wait()
find_disks = json.loads(lsblk.decode('utf-8').strip(' \t\n\r'))['blockdevices']
numdisks = 0
for disk in find_disks:
# has to be a hard-disk
if disk['type'] == 'disk':
diskdict = {
"base_object" : myres['id'],
"size" : int(float(disk['size'])/1024/1024/1024)
}
numdisks = numdisks + 1
diskserial = disk['serial']
if diskserial != None:
diskdict['serial_number'] = disk['serial']
modelname = disk['model']
diskdict['model_name'] = modelname
createcomponent(faiserver+'disks/', diskdict, headers)