v0.95.0: enrollment wizards use the raw-device scan /disks/candidates (Impl-2b)
Both wizards now source candidates from the agent's Impl-2a raw-device scan (GET /disks/candidates, proxied) instead of the Observe-based /api/disks — so a brand-new non-PVE-storage drive is finally discoverable + enrollable end-to-end. agentapi.ListCandidates + a passthrough proxy (no controller-side filtering; the agent's unclaimed filter is authoritative). storage_init renders `initialize`, storage_attach renders `attach`; the enroll flow + Impl-1 guarded mkfs unchanged. Tests + go build/vet/test clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,8 @@ func (s *Server) ServeDiskAPI(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/api/disks" && r.Method == http.MethodGet:
|
||||
s.agentDisksListHandler(w, r)
|
||||
case r.URL.Path == "/api/disks/candidates" && r.Method == http.MethodGet:
|
||||
s.agentDiskCandidatesHandler(w, r)
|
||||
case r.URL.Path == "/api/disks/assign" && r.Method == http.MethodPost:
|
||||
s.agentDiskAssignHandler(w, r)
|
||||
case r.URL.Path == "/api/disks/eject" && r.Method == http.MethodPost:
|
||||
@@ -110,6 +112,25 @@ func (s *Server) agentDisksListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeDiskJSON(w, http.StatusOK, true, "", resp)
|
||||
}
|
||||
|
||||
// agentDiskCandidatesHandler proxies GET /api/disks/candidates → agent GET /disks/candidates (Impl-2b):
|
||||
// the raw-device scan (Impl-2a) that feeds the enrollment wizards. The agent's unclaimed-disk filter
|
||||
// already excludes claimed/OS/enrolled disks (fail-safe), so the controller passes the list through
|
||||
// untouched — no controller-side filtering.
|
||||
func (s *Server) agentDiskCandidatesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
client, err := s.agentClient()
|
||||
if err != nil {
|
||||
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
resp, err := client.ListCandidates(r.Context())
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] disk candidates via agent failed: %v", err)
|
||||
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
writeDiskJSON(w, http.StatusOK, true, "", resp)
|
||||
}
|
||||
|
||||
// sortDisksForView orders the agent's disk list deterministically (user-data → system → backup →
|
||||
// unrecognized; alphabetical by storage name within each tier). A stable Go-side contract beats
|
||||
// relying on map iteration order or template JS alone.
|
||||
|
||||
@@ -51,27 +51,34 @@
|
||||
<script>
|
||||
var selDevice = "", selFSType = "";
|
||||
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
|
||||
function classBadge(d){
|
||||
if(d.class==='fast') return '<span class="badge badge-ok">gyors</span>';
|
||||
if(d.class==='slow') return '<span class="badge badge-muted">lassú</span>';
|
||||
return '';
|
||||
function fmtSize(b){
|
||||
if(!b) return '';
|
||||
var g=b/1073741824;
|
||||
return g>=1 ? g.toFixed(g>=10?0:1)+' GB' : Math.round(b/1048576)+' MB';
|
||||
}
|
||||
|
||||
async function loadDisks(){
|
||||
try{
|
||||
var r = await fetch('/api/disks'); var j = await r.json();
|
||||
// Impl-2b: the raw-device scan (agent GET /disks/candidates). `attach` = unclaimed disks already
|
||||
// carrying a mountable ext4/xfs FS (mounted + bound, NO format — data preserved). The agent's filter
|
||||
// excludes OS/enrolled/claimed disks, so we trust the list as-is.
|
||||
var r = await fetch('/api/disks/candidates'); var j = await r.json();
|
||||
if(!j.ok){ throw new Error(j.error||'Hiba'); }
|
||||
var disks = (j.data&&j.data.disks)||[];
|
||||
// Attachable: a user-data drive with a backing device, an fs-UUID identity, not mounted yet.
|
||||
var attachable = disks.filter(function(d){ return d.backing_device!=="" && (d.durable_id||"").indexOf("uuid:")===0 && !d.mount_path && d.role==='user-data'; });
|
||||
if(attachable.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs csatolható (fájlrendszerrel rendelkező, még nem csatolt) felhasználói adatmeghajtó.</p>'; return; }
|
||||
var cands = (j.data&&j.data.attach)||[];
|
||||
if(cands.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs csatolható meghajtó — a csatoláshoz olyan (még nem használt) meghajtó kell, amelyen már van fájlrendszer. Új, üres meghajtóhoz használja az „Inicializálás” lehetőséget.</p>'; return; }
|
||||
var html='<div class="drive-list">';
|
||||
attachable.forEach(function(d,i){
|
||||
var sub = esc(d.type)+' · '+esc(d.backing_device);
|
||||
cands.forEach(function(d,i){
|
||||
var title = d.model ? esc(d.model) : esc(d.device);
|
||||
var parts = [esc(d.device)];
|
||||
if(d.size_bytes) parts.push(fmtSize(d.size_bytes));
|
||||
if(d.fstype) parts.push(esc(d.fstype));
|
||||
var sub = parts.join(' · ');
|
||||
// Attach the FS-bearing node (mount_source, e.g. /dev/sdd1); the agent resolves its UUID.
|
||||
var dev = d.mount_source || d.device;
|
||||
html+='<label class="drive-card role-user-data is-selectable" id="dc-'+i+'">'
|
||||
+'<div class="drive-card-top"><div class="drive-select"><input type="radio" name="disk" value="'+esc(d.backing_device)+'" data-i="'+i+'" onchange="pickDisk(this)">'
|
||||
+'<div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div></div>'
|
||||
+'<div class="drive-badges"><span class="badge badge-ok">Felhasználói adat</span>'+classBadge(d)+'</div></div></label>';
|
||||
+'<div class="drive-card-top"><div class="drive-select"><input type="radio" name="disk" value="'+esc(dev)+'" data-fs="'+esc(d.fstype)+'" data-i="'+i+'" onchange="pickDisk(this)">'
|
||||
+'<div class="drive-id"><span class="drive-name">'+title+'</span><span class="drive-sub">'+sub+'</span></div></div>'
|
||||
+'<div class="drive-badges"><span class="badge badge-ok">Fájlrendszer: '+esc(d.fstype)+'</span></div></div></label>';
|
||||
});
|
||||
html+='</div>';
|
||||
document.getElementById('disk-list').innerHTML=html;
|
||||
@@ -79,7 +86,7 @@ async function loadDisks(){
|
||||
}
|
||||
|
||||
function pickDisk(radio){
|
||||
selDevice=radio.value;
|
||||
selDevice=radio.value; selFSType=radio.getAttribute('data-fs')||"";
|
||||
document.getElementById('sel-device').textContent=selDevice;
|
||||
document.querySelectorAll('.drive-card').forEach(function(c){c.classList.remove('is-picked');});
|
||||
var card=document.getElementById('dc-'+radio.getAttribute('data-i')); if(card) card.classList.add('is-picked');
|
||||
@@ -92,7 +99,7 @@ async function submitAttach(ev){
|
||||
var btn=document.getElementById('attach-btn'); var out=document.getElementById('attach-result');
|
||||
btn.disabled=true; out.innerHTML='<p class="form-hint">Csatlakoztatás folyamatban…</p>';
|
||||
try{
|
||||
var body={device:selDevice, fstype:"", mount_name:document.getElementById('mount-name').value,
|
||||
var body={device:selDevice, fstype:selFSType, mount_name:document.getElementById('mount-name').value,
|
||||
label:document.getElementById('storage-label').value, set_default:document.getElementById('set-default').checked};
|
||||
var r=await fetch('/api/storage/attach',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)});
|
||||
var j=await r.json();
|
||||
|
||||
@@ -66,32 +66,35 @@ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){retur
|
||||
|
||||
function dataBadge(d){
|
||||
return d.data_bearing
|
||||
? '<span class="badge badge-error" title="'+esc(d.data_reason)+'">Adatot tartalmaz</span>'
|
||||
? '<span class="badge badge-error">Adatot tartalmaz — a formázás törli</span>'
|
||||
: '<span class="badge badge-ok">Üres — formázható</span>';
|
||||
}
|
||||
function classBadge(d){
|
||||
if(d.class==='fast') return '<span class="badge badge-ok">gyors</span>';
|
||||
if(d.class==='slow') return '<span class="badge badge-muted">lassú</span>';
|
||||
return '';
|
||||
function fmtSize(b){
|
||||
if(!b) return '';
|
||||
var g=b/1073741824;
|
||||
return g>=1 ? g.toFixed(g>=10?0:1)+' GB' : Math.round(b/1048576)+' MB';
|
||||
}
|
||||
|
||||
async function loadDisks(){
|
||||
try{
|
||||
var r = await fetch('/api/disks'); var j = await r.json();
|
||||
// Impl-2b: the raw-device scan (agent GET /disks/candidates via the controller proxy). The agent's
|
||||
// unclaimed-disk filter already excludes OS/enrolled/claimed disks (fail-safe), so we trust the list
|
||||
// as-is — no client-side filtering. `initialize` = every unclaimed disk (blank or data-bearing).
|
||||
var r = await fetch('/api/disks/candidates'); var j = await r.json();
|
||||
if(!j.ok){ throw new Error(j.error||'Hiba'); }
|
||||
var disks = (j.data&&j.data.disks)||[];
|
||||
// Only USER-DATA drives with a block device that are NOT already mounted are valid init (format)
|
||||
// targets — a mounted drive must be ejected (Leválasztás) first, like the attach wizard. This
|
||||
// keeps an already-managed drive (e.g. felhom-usb) from showing up as an "initialize" candidate.
|
||||
var formattable = disks.filter(function(d){ return d.backing_device!=="" && d.role==='user-data' && !d.mount_path; });
|
||||
if(formattable.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs formázható felhasználói adatmeghajtó. (A csatlakoztatott meghajtókat előbb le kell választani; a rendszer- és biztonsági-mentés meghajtók védettek.)</p>'; return; }
|
||||
var cands = (j.data&&j.data.initialize)||[];
|
||||
if(cands.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs elérhető meghajtó az inicializáláshoz. Csatlakoztasson egy új adathordozót — a rendszer-, a biztonsági mentés- és a már használatban lévő meghajtók itt nem jelennek meg.</p>'; return; }
|
||||
var html='<div class="drive-list">';
|
||||
formattable.forEach(function(d,i){
|
||||
var sub = esc(d.type)+' · '+esc(d.backing_device)+(d.mount_path?' · '+esc(d.mount_path):'');
|
||||
cands.forEach(function(d,i){
|
||||
var title = d.model ? esc(d.model) : esc(d.device);
|
||||
var parts = [esc(d.device)];
|
||||
if(d.size_bytes) parts.push(fmtSize(d.size_bytes));
|
||||
if(d.fstype) parts.push('jelenlegi: '+esc(d.fstype));
|
||||
var sub = parts.join(' · ');
|
||||
html+='<label class="drive-card role-user-data is-selectable" id="dc-'+i+'">'
|
||||
+'<div class="drive-card-top"><div class="drive-select"><input type="radio" name="disk" value="'+esc(d.backing_device)+'" data-db="'+(d.data_bearing?'1':'0')+'" data-i="'+i+'" onchange="pickDisk(this)">'
|
||||
+'<div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div></div>'
|
||||
+'<div class="drive-badges"><span class="badge badge-ok">Felhasználói adat</span>'+classBadge(d)+dataBadge(d)+'</div></div></label>';
|
||||
+'<div class="drive-card-top"><div class="drive-select"><input type="radio" name="disk" value="'+esc(d.device)+'" data-db="'+(d.data_bearing?'1':'0')+'" data-i="'+i+'" onchange="pickDisk(this)">'
|
||||
+'<div class="drive-id"><span class="drive-name">'+title+'</span><span class="drive-sub">'+sub+'</span></div></div>'
|
||||
+'<div class="drive-badges">'+dataBadge(d)+'</div></div></label>';
|
||||
});
|
||||
html+='</div>';
|
||||
document.getElementById('disk-list').innerHTML=html;
|
||||
|
||||
Reference in New Issue
Block a user