v0.318.0: a part is offered for formatting only while nothing on its disk is mounted (agent 0.158.1 formattable)
gates / gates (push) Successful in 45s
gates / gates (push) Successful in 45s
The agent's root mkfs wrapper (security review of agent 0.158.0) refuses any mounted node and trusts no sibling claim; the wizard now matches it. A free filesystem part of an in-use disk stays offered for use. Red-proved. Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -3404,15 +3404,16 @@ the controller only asks.
|
||||
- **Second copies** (v0.317.0): Tier-2 and the one-copy notice ask the agent (`POST /disks/same-disk`); a copy counts only on a
|
||||
proven different disk. **The whole-box backup offer** (v0.318.0, R-1084) does the same: proven different → the disk-failure
|
||||
promise; unproven (a VM) → offered without the promise (`backup.target.offer_unproven`); same disk → not offered.
|
||||
- **The internal data place „Belső tárhely"** (v0.318.0, agent ≥ 0.158.0): a volume on the system disk that the installer (or
|
||||
- **The internal data place „Belső tárhely"** (v0.318.0, agent ≥ 0.158.1): a volume on the system disk that the installer (or
|
||||
the operator's signed `add_internal_place`) makes on a one-disk box. When the agent reports it (`GET /disks`, kind
|
||||
`internal`), the drive-gate tick binds it into the guest and, on the next tick, registers it as a drive of kind `internal`
|
||||
— the default drive unless the household has one. Media and data-folder apps then install with no extra step. Eject,
|
||||
retire and format are refused (`err.web.belso_tarhely_eletciklus`); moving its data to another drive is allowed.
|
||||
- **Partitions** (v0.318.0, agent ≥ 0.158.0): the format and attach wizards list each part of a free disk — „format this
|
||||
part" (empty, or with data through the typed confirmation) and „use this part" (an ext4/xfs filesystem). A disk whose other
|
||||
part is already a drive offers only its free parts; formatting that whole disk is refused on the server too. No part is
|
||||
ever created, deleted or resized from the dashboard (`16` D5).
|
||||
- **Partitions** (v0.318.0, agent ≥ 0.158.1): the format and attach wizards list each part of a free disk — „format this
|
||||
part" (empty, or with data through the typed confirmation), only while nothing on the disk is mounted; „use this part"
|
||||
(an ext4/xfs filesystem) always. A disk whose other part is already a drive offers only its free filesystem parts, for
|
||||
use; formatting that whole disk is refused on the server too. No part is ever created, deleted or resized from the
|
||||
dashboard (`16` D5).
|
||||
- **`/mnt/sys_drive` is never offered to attach** (v0.318.0, R-1082): it rides in every whole-box copy. A box that already
|
||||
registered it keeps it.
|
||||
|
||||
|
||||
@@ -500,6 +500,9 @@ type CandidatePartition struct {
|
||||
Mountable bool `json:"mountable"`
|
||||
DurableID string `json:"durable_id,omitempty"` // uuid:<fs-uuid> — the attach key
|
||||
MountedAt string `json:"mounted_at,omitempty"`
|
||||
// Formattable (agent ≥ 0.158.1): false for every part of a disk with a mounted node — the root mkfs wrapper refuses
|
||||
// those and trusts no „it is Felhom's own" claim. Only formattable parts are offered for formatting.
|
||||
Formattable bool `json:"formattable"`
|
||||
}
|
||||
|
||||
// Partition states (agent ≥ 0.158.0).
|
||||
|
||||
@@ -186,13 +186,13 @@ func dropRegisteredDrives(resp agentapi.CandidatesResult, mountsText string, reg
|
||||
if len(inUse) == 0 {
|
||||
return resp
|
||||
}
|
||||
keep := func(in []agentapi.DiskCandidate) []agentapi.DiskCandidate {
|
||||
keep := func(in []agentapi.DiskCandidate, usable func(agentapi.CandidatePartition) bool) []agentapi.DiskCandidate {
|
||||
out := make([]agentapi.DiskCandidate, 0, len(in))
|
||||
for _, c := range in {
|
||||
if candidateBacksInUse(c, inUse) {
|
||||
// `16` §6 (v0.318.0): a disk whose ONE part is a registered drive keeps its FREE parts on offer —
|
||||
// `16` §6 (v0.318.0): a disk whose ONE part is a registered drive keeps its usable FREE parts on offer —
|
||||
// the whole disk itself, never (WholeDiskInUse; handleStorageInit refuses it server-side too).
|
||||
if free := freePartitions(c.Partitions, inUse); len(free) > 0 {
|
||||
if free := freePartitions(c.Partitions, inUse, usable); len(free) > 0 {
|
||||
c.Partitions, c.WholeDiskInUse = free, true
|
||||
out = append(out, c)
|
||||
}
|
||||
@@ -202,8 +202,10 @@ func dropRegisteredDrives(resp agentapi.CandidatesResult, mountsText string, reg
|
||||
}
|
||||
return out
|
||||
}
|
||||
resp.Initialize = keep(resp.Initialize)
|
||||
resp.Attach = keep(resp.Attach)
|
||||
// Formatting a part needs a disk with nothing mounted (the agent's Formattable — its root wrapper refuses otherwise);
|
||||
// USING a part with a filesystem erases nothing and stays offered.
|
||||
resp.Initialize = keep(resp.Initialize, func(p agentapi.CandidatePartition) bool { return p.Formattable })
|
||||
resp.Attach = keep(resp.Attach, func(p agentapi.CandidatePartition) bool { return p.State == agentapi.PartFilesystem && p.Mountable })
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -223,11 +225,12 @@ func deviceBacksRegistered(device, mountsText string, registered map[string]bool
|
||||
return candidateBacksInUse(agentapi.DiskCandidate{Device: device}, inUse)
|
||||
}
|
||||
|
||||
// freePartitions keeps the parts that are not a registered drive and not already Felhom's (state in_use).
|
||||
func freePartitions(parts []agentapi.CandidatePartition, inUse map[string]bool) []agentapi.CandidatePartition {
|
||||
// freePartitions keeps the parts that are not a registered drive, not already Felhom's (state in_use), and usable for
|
||||
// the list in question.
|
||||
func freePartitions(parts []agentapi.CandidatePartition, inUse map[string]bool, usable func(agentapi.CandidatePartition) bool) []agentapi.CandidatePartition {
|
||||
var out []agentapi.CandidatePartition
|
||||
for _, p := range parts {
|
||||
if p.State == agentapi.PartInUse || inUse[p.Device] {
|
||||
if p.State == agentapi.PartInUse || inUse[p.Device] || !usable(p) {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
|
||||
@@ -28,18 +28,23 @@ func diskWithParts() agentapi.DiskCandidate {
|
||||
}}
|
||||
}
|
||||
|
||||
func TestPartitions_FreePartsOfAnInUseDiskStayOffered(t *testing.T) {
|
||||
// A disk whose one part is a registered drive: its free FILESYSTEM part stays offered for USE (attach erases nothing);
|
||||
// nothing of it is offered for formatting — the agent marks its parts not Formattable (a node is mounted) and its root
|
||||
// wrapper would refuse (agent 0.158.1, security review). Positive control: an unmounted disk's formattable part.
|
||||
func TestPartitions_InUseDiskOffersItsFreePartsForUseOnly(t *testing.T) {
|
||||
reg := map[string]bool{"/mnt/felhom-drives/hdd_1": true}
|
||||
got := dropRegisteredDrives(agentapi.CandidatesResult{Initialize: []agentapi.DiskCandidate{diskWithParts()}}, partMounts, reg)
|
||||
if len(got.Initialize) != 1 {
|
||||
t.Fatalf("the disk must stay listed for its free parts, got %+v", got.Initialize)
|
||||
d := diskWithParts()
|
||||
got := dropRegisteredDrives(agentapi.CandidatesResult{Initialize: []agentapi.DiskCandidate{d}, Attach: []agentapi.DiskCandidate{d}}, partMounts, reg)
|
||||
if len(got.Initialize) != 0 {
|
||||
t.Fatalf("nothing of a disk with a mounted part may be offered for formatting, got %+v", got.Initialize)
|
||||
}
|
||||
c := got.Initialize[0]
|
||||
if !c.WholeDiskInUse {
|
||||
t.Fatalf("the whole disk must be marked in use (no whole-disk action): %+v", c)
|
||||
if len(got.Attach) != 1 || !got.Attach[0].WholeDiskInUse || len(got.Attach[0].Partitions) != 1 || got.Attach[0].Partitions[0].Device != "/dev/sdb3" {
|
||||
t.Fatalf("the free filesystem part must stay offered for use (and only it), got %+v", got.Attach)
|
||||
}
|
||||
if len(c.Partitions) != 2 || c.Partitions[0].Device != "/dev/sdb2" || c.Partitions[1].Device != "/dev/sdb3" {
|
||||
t.Fatalf("only the free parts may be offered, got %+v", c.Partitions)
|
||||
free := agentapi.DiskCandidate{Device: "/dev/sdc", Partitions: []agentapi.CandidatePartition{{Device: "/dev/sdc1", State: agentapi.PartEmpty, Formattable: true}}}
|
||||
got = dropRegisteredDrives(agentapi.CandidatesResult{Initialize: []agentapi.DiskCandidate{free}}, partMounts, reg)
|
||||
if len(got.Initialize) != 1 || len(got.Initialize[0].Partitions) != 1 {
|
||||
t.Fatalf("positive control: an unmounted disk's formattable part stays offered, got %+v", got.Initialize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,8 @@ async function loadDisks(){
|
||||
// `16` §6: a disk whose other part is already a drive offers only its free parts, never the whole disk.
|
||||
if(!d.whole_disk_in_use) html+=card(d.device, title, parts.join(' · '), d.data_bearing);
|
||||
(d.partitions||[]).forEach(function(p){
|
||||
if(p.state!=='empty' && p.state!=='filesystem') return;
|
||||
// only a part the agent says can be formatted (nothing else on the disk is mounted — its root wrapper refuses otherwise)
|
||||
if((p.state!=='empty' && p.state!=='filesystem') || !p.formattable) return;
|
||||
var ps=[esc(p.device)]; if(p.size_bytes) ps.push(fmtSize(p.size_bytes)); if(p.fstype) ps.push('{{T "storage_init.jelenlegi"}} '+esc(p.fstype));
|
||||
html+=card(p.device, title+' — '+'{{T "storage_wizard.resz_n"}}'.replace('{n}', p.number||'?'), ps.join(' · '), p.state==='filesystem');
|
||||
});
|
||||
|
||||
@@ -272,7 +272,8 @@ async function loadDisks(){
|
||||
|
||||
if(!d.whole_disk_in_use) html+=card(d.device, title, parts.join(' · '), d.data_bearing);
|
||||
(d.partitions||[]).forEach(function(p){
|
||||
if(p.state!=='empty' && p.state!=='filesystem') return;
|
||||
|
||||
if((p.state!=='empty' && p.state!=='filesystem') || !p.formattable) return;
|
||||
var ps=[esc(p.device)]; if(p.size_bytes) ps.push(fmtSize(p.size_bytes)); if(p.fstype) ps.push('jelenlegi: '+esc(p.fstype));
|
||||
html+=card(p.device, title+' — '+'{n}. rész'.replace('{n}', p.number||'?'), ps.join(' · '), p.state==='filesystem');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user