Add unified FRU/Elabel card with Huawei iBMC OEM IPMI support
Replaces separate IPMI FRU and SAA DMI cards with a single FRU / Elabel card that reads all available sources in parallel and shows each field with a color-coded source chip (IPMI FRU / Huawei iBMC / SAA DMI). Huawei elabel fields are read/written via OEM IPMI raw commands (NetFn 0x30, cmd 0x90) with 19-byte chunking protocol, matching the FusionServer ElabelTool V511 wire format. Covers DeviceName, DeviceSerialNumber, ProductName, ProductSerialNumber, ProductAssetTag, ProductManufacturer, MainboardManufacturer, BoardProductName, ChassisPartnumber, ChassisType (read-only), IOChassisSerial, IOChassisAssetTag, and GUID (read-only via standard 0x06 0x08). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -202,83 +202,3 @@ func runIPMIFRUWriteTask(ctx context.Context, j *jobState, exportDir string, p t
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderIPMIFRUCard() string {
|
||||
return `<div class="card"><div class="card-head card-head-actions">IPMI — FRU<div class="card-head-buttons"><button class="btn btn-sm btn-secondary" onclick="fruRead()">Read</button></div></div><div class="card-body">
|
||||
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits FRU fields via ipmitool (In-Band, device 0). Works on any server with IPMI support.</p>
|
||||
<div id="fru-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||
<div id="fru-table"></div>
|
||||
</div></div>
|
||||
<script>
|
||||
var _fruActBtnStyle = 'width:22px;height:22px;padding:0;font-size:13px;line-height:1;border:1px solid var(--line);border-radius:3px;background:var(--surface);cursor:pointer;vertical-align:middle;';
|
||||
var _fruInputStyle = 'width:100%;padding:3px 6px;border:1.5px solid #888;border-radius:3px;font-size:13px;font-family:monospace;background:var(--surface);color:var(--ink);';
|
||||
function fruRead() {
|
||||
var status = document.getElementById('fru-status');
|
||||
status.textContent = 'Reading...'; status.style.color = 'var(--muted)';
|
||||
document.getElementById('fru-table').innerHTML = '';
|
||||
fetch('/api/tools/ipmi-fru', {cache:'no-store'})
|
||||
.then(function(r) { return r.json().then(function(d){if(!r.ok)throw new Error(d.error||r.statusText);return d;}); })
|
||||
.then(function(fields) {
|
||||
if (!fields || !fields.length) { status.textContent = 'No FRU fields returned.'; return; }
|
||||
status.textContent = '';
|
||||
var rows = fields.map(function(f) {
|
||||
var val = escHtml(f.value || '');
|
||||
return '<tr>'
|
||||
+ '<td style="color:var(--muted);white-space:nowrap;padding-right:16px;vertical-align:middle;font-size:13px">' + escHtml(f.name) + '</td>'
|
||||
+ '<td style="vertical-align:middle"><input class="fru-inp" style="' + _fruInputStyle + '"'
|
||||
+ ' data-area="' + escHtml(f.area||'') + '" data-index="' + (f.index||0) + '" data-name="' + escHtml(f.name) + '"'
|
||||
+ ' data-original="' + val + '" value="' + val + '" oninput="fruChanged(this)"></td>'
|
||||
+ '<td class="fru-act" style="display:none;white-space:nowrap;padding-left:6px;vertical-align:middle">'
|
||||
+ '<button style="' + _fruActBtnStyle + 'color:var(--ok-fg,green);margin-right:3px" title="Save" onclick="fruSave(this)">✓</button>'
|
||||
+ '<button style="' + _fruActBtnStyle + 'color:var(--crit-fg,#9f3a38)" title="Cancel" onclick="fruCancel(this)">✗</button>'
|
||||
+ '<span class="fru-msg" style="font-size:11px;margin-left:5px;color:var(--muted)"></span>'
|
||||
+ '</td></tr>';
|
||||
}).join('');
|
||||
document.getElementById('fru-table').innerHTML = '<table style="width:100%;border-collapse:collapse">' + rows + '</table>';
|
||||
})
|
||||
.catch(function(e) { status.textContent = 'Error: '+e.message; status.style.color='var(--crit-fg)'; });
|
||||
}
|
||||
function escHtml(s) {
|
||||
return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
function fruChanged(inp) {
|
||||
inp.closest('tr').querySelector('.fru-act').style.display = inp.value !== inp.dataset.original ? '' : 'none';
|
||||
}
|
||||
function fruCancel(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.fru-inp');
|
||||
inp.value = inp.dataset.original;
|
||||
row.querySelector('.fru-act').style.display = 'none';
|
||||
row.querySelector('.fru-msg').textContent = '';
|
||||
}
|
||||
function fruSave(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.fru-inp');
|
||||
var msg = row.querySelector('.fru-msg');
|
||||
var cancelBtn = row.querySelectorAll('.fru-act button')[1];
|
||||
btn.disabled = true; cancelBtn.disabled = true;
|
||||
msg.textContent = '…'; msg.style.color = 'var(--muted)';
|
||||
fetch('/api/tools/ipmi-fru/write', {method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({changes:[{area:inp.dataset.area, index:parseInt(inp.dataset.index,10), name:inp.dataset.name, value:inp.value}]})})
|
||||
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||r.statusText);return d;});})
|
||||
.then(function(d){
|
||||
var poll = setInterval(function(){
|
||||
fetch('/api/tasks',{cache:'no-store'}).then(function(r){return r.json();}).then(function(tasks){
|
||||
var t = Array.isArray(tasks)?tasks.find(function(x){return x.id===d.task_id;}):null;
|
||||
if(!t) return;
|
||||
if(t.status==='done'){
|
||||
clearInterval(poll);
|
||||
inp.dataset.original = inp.value;
|
||||
row.querySelector('.fru-act').style.display = 'none';
|
||||
msg.textContent = '';
|
||||
} else if(t.status==='failed'||t.status==='cancelled'){
|
||||
clearInterval(poll);
|
||||
msg.textContent = t.error||t.status; msg.style.color='var(--crit-fg)';
|
||||
btn.disabled=false; cancelBtn.disabled=false;
|
||||
}
|
||||
});
|
||||
},1500);
|
||||
})
|
||||
.catch(function(e){ msg.textContent='Error: '+e.message; msg.style.color='var(--crit-fg)'; btn.disabled=false; cancelBtn.disabled=false; });
|
||||
}
|
||||
</script>`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user