Customization stories and tricks

Have you modified Ralph to suit your needs?

Share your story here, to help other Ralph users, or get support :slight_smile:

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…)

1 Like

External deployments integration is something we’d definitely want to see, even if it’s 300 lines of python scripts :slight_smile: 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
1 Like

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)
1 Like