Commit e36355d2 authored by jan.koester's avatar jan.koester
Browse files

test

parent 00ec663b
Loading
Loading
Loading
Loading
+1 −1
Changes for modules/common/storaged.cpp: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -44,7 +44,7 @@ namespace {
const std::set<std::string> Flags = {"bitmap", "in_use", "partition", "active", "open", "plain"};
const std::set<std::string> Numbers = {"raid_disks", "degraded", "size", "component_size", "chunk", "sync_speed",
                                       "slot", "present", "partitions", "extent_size", "free", "used",
                                       "seconds", "finished"};
                                       "seconds", "finished", "mismatches", "errors", "current"};

std::string camel(const std::string &k) {
    std::string out;
+28 −5
Changes for modules/raid/raid.cpp: 28 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -33,13 +33,26 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *   create   {level, devices[], spares?[], chunk?, confirm}
 *                                               -> {array}   chunk in KiB
 *   add      {array, device, confirm}           as a spare; md rebuilds onto
 *                                               it when a member is missing
 *                                               it when a member is missing.
 *                                               A former member goes back
 *                                               into its slot instead
 *                                               -> {re_added: "yes"|"no"}
 *   remove   {array, device}                    fails an active member first;
 *                                               its superblock is wiped
 *   check    {array, stop?}                     scrub (sync_action check/idle)
 *   readd    {array, device}                    a failed or left-behind member
 *                                               back into its slot (mdadm
 *                                               --re-add; with the bitmap only
 *                                               what changed is resynced)
 *   replace  {array, device, with?, confirm?}   copies device onto a spare
 *                                               (with: a free device added as
 *                                               it) while it keeps working
 *   check    {array, repair?, stop?}            scrub (sync_action check,
 *                                               repair, idle)
 *   stop     {array}                            members keep their superblocks
 *   delete   {array, confirm}                   stop + wipe all superblocks
 *   assemble {uuid}                             a set found on free devices
 *   assemble {uuid, force?}                     a set found on free devices;
 *                                               force: out-of-date members
 *                                               are taken as current
 *
 * "devices" are only block devices nothing uses; whatever is on a device
 * that goes into an array is lost. What writes to devices runs as a job
@@ -94,11 +107,21 @@ json_object *call(const webctl_request *req) {
    if (action == "create") return create(a);
    if (action == "add") return storage::change(m::AddSpare, {{"array", array}, {"dev", device}, {"confirm", confirm}});
    if (action == "remove") return storage::change(m::RemoveMember, {{"array", array}, {"dev", device}});
    if (action == "readd") return storage::change(m::ReAddMember, {{"array", array}, {"dev", device}});
    if (action == "replace") {
        storage::Fields f = {{"array", array}, {"dev", device}, {"confirm", confirm}};
        std::string with = webctl::str(a, "with");
        if (!with.empty()) f.emplace_back("with", with);
        return storage::change(m::ReplaceMember, f);
    }
    if (action == "check")
        return storage::change(m::CheckArray, {{"array", array}, {"stop", yesNo(webctl::boolean(a, "stop"))}});
        return storage::change(m::CheckArray, {{"array", array},
                                               {"repair", yesNo(webctl::boolean(a, "repair"))},
                                               {"stop", yesNo(webctl::boolean(a, "stop"))}});
    if (action == "stop") return storage::change(m::StopArray, {{"array", array}});
    if (action == "delete") return storage::change(m::DeleteArray, {{"array", array}, {"confirm", confirm}});
    if (action == "assemble") return storage::change(m::AssembleArray, {{"uuid", webctl::str(a, "uuid")}});
    if (action == "assemble")
        return storage::change(m::AssembleArray, {{"uuid", webctl::str(a, "uuid")}, {"force", yesNo(webctl::boolean(a, "force"))}});
    return error(tr("unknown action %1", "unbekannte Aktion %1", {action}), 404);
}

+144 −15
Changes for www/modules/raid.js: 144 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -67,6 +67,32 @@
    'No free device.': 'Kein freies Gerät.',
    'Members of an array that isn\'t running. Assembling starts it with these devices.':
      'Mitglieder eines Arrays, das nicht läuft. Zusammensetzen startet es mit diesen Geräten.',
    '{0} mismatches': '{0} Abweichungen', '{0} read errors corrected': '{0} Lesefehler korrigiert',
    'being replaced': 'wird ersetzt', 'replacement': 'Ersatz',
    'Repair': 'Reparieren', 'Stop repair': 'Reparatur abbrechen',
    'Repair started on {0}.': 'Reparatur von {0} gestartet.', 'Repair on {0} stopped.': 'Reparatur von {0} abgebrochen.',
    'Re-add device': 'Gerät wieder aufnehmen', 'Re-add': 'Wieder aufnehmen',
    'Take a device back into {0}': 'Gerät wieder in {0} aufnehmen',
    'A member that failed or was left behind goes back into its place. Thanks to the write-intent bitmap only what changed since is synchronized; nothing on the device is deleted.':
      'Ein ausgefallenes oder zurückgelassenes Mitglied kommt wieder an seinen Platz. Dank der Write-Intent-Bitmap wird nur synchronisiert, was sich seither geändert hat; auf dem Gerät wird nichts gelöscht.',
    'If it is too far behind, add it as a device instead: it is then rebuilt in full.':
      'Ist es zu weit zurück, stattdessen als Gerät hinzufügen: es wird dann vollständig neu aufgebaut.',
    '{0} is back in {1}.': '{0} ist wieder in {1}.', '{0} went back into {1} as a former member.': '{0} wurde als früheres Mitglied wieder in {1} aufgenommen.',
    'left behind': 'zurückgelassen',
    'Replace device': 'Gerät ersetzen', 'Replace': 'Ersetzen', 'Replace a device of {0}': 'Gerät von {0} ersetzen',
    'Replace with': 'Ersetzen durch', 'a spare of the array': 'ein Reservegerät des Arrays',
    'The device is copied onto the replacement while it keeps working, so {0} never loses redundancy. Afterwards it is marked failed and can be removed.':
      'Das Gerät wird auf den Ersatz kopiert, während es weiterarbeitet, so verliert {0} nie seine Redundanz. Danach ist es als ausgefallen markiert und kann entfernt werden.',
    'Replacement of {0} started.': 'Ersetzen von {0} gestartet.',
    'No device to replace with: add a spare or free a device.': 'Kein Gerät als Ersatz: ein Reservegerät hinzufügen oder ein Gerät freimachen.',
    'Up to date': 'Aktuell', 'Force start': 'Erzwungen starten', 'not enough members': 'zu wenige Mitglieder',
    'Start {0} forced?': '{0} erzwungen starten?',
    'Too few members are up to date, or the array is degraded and wasn\'t shut down cleanly. Forced, the freshest out-of-date members are taken as current (like mdadm --assemble --force).':
      'Zu wenige Mitglieder sind aktuell, oder das Array ist beeinträchtigt und wurde nicht sauber beendet. Erzwungen werden die frischesten veralteten Mitglieder als aktuell übernommen (wie mdadm --assemble --force).',
    'What was written after they dropped out isn\'t on them: check the file system and run a repair afterwards.':
      'Was nach ihrem Ausfall geschrieben wurde, fehlt auf ihnen: danach das Dateisystem prüfen und eine Reparatur laufen lassen.',
    'I know that data may be inconsistent.': 'Mir ist klar, dass Daten inkonsistent sein können.',
    'Please confirm.': 'Bitte bestätigen.',
  }});

  let root;
@@ -144,16 +170,21 @@
    } else if (!a.degraded) {
      out.push(h('span', {class: 'badge ok'}, t('clean')));
    }
    if (a.mismatches > 0) out.push(' ', h('span', {class: 'badge warn'}, t('{0} mismatches', a.mismatches)));
    if (a.inUse) out.push(' ', h('span', {class: 'badge'}, t('in use')));
    return out;
  }

  function memberChips(a) {
    return h('span', {class: 'chips'}, a.members.map(m => {
      let note = '';
      if (m.state.includes('faulty')) note = ' (' + t('failed') + ')';
      else if (m.slot < 0) note = ' (' + t('spare') + ')';
      return h('span', {class: m.state.includes('faulty') ? 'chip-bad' : null}, m.device + note);
      const notes = [];
      if (m.state.includes('faulty')) notes.push(t('failed'));
      else if (m.state.includes('want_replacement')) notes.push(t('being replaced'));
      else if (m.state.includes('replacement')) notes.push(t('replacement'));
      else if (m.slot < 0) notes.push(t('spare'));
      if (m.errors > 0) notes.push(t('{0} read errors corrected', m.errors));
      const bad = m.state.includes('faulty') || m.errors > 0;
      return h('span', {class: bad ? 'chip-bad' : null}, m.device + (notes.length ? ' (' + notes.join(', ') + ')' : ''));
    }));
  }

@@ -270,7 +301,8 @@
      onSubmit: async d => {
        if (!d.confirm) throw new Error(t('Please confirm that the data will be deleted.'));
        const r = await call('raid', 'add', {array: a.name, device: d.device, confirm: true});
        if (!jobs.started(r)) toast(t('{0} added to {1}.', d.device, a.name));
        if (!jobs.started(r)) toast(r.re_added === 'yes' ? t('{0} went back into {1} as a former member.', d.device, a.name)
                                                         : t('{0} added to {1}.', d.device, a.name));
      },
    }).then(ok => { if (ok) load(); });
  }
@@ -320,20 +352,99 @@
    }).then(ok => { if (ok) load(); });
  }

  async function checkArray(a, stop) {
    const ok = await run(() => call('raid', 'check', {array: a.name, stop}),
                         stop ? t('Check on {0} stopped.', a.name) : t('Check started on {0}.', a.name));
  // failed members and members left behind at assembly: what readd takes
  function readdCandidates(a) {
    const failed = a.members.filter(m => m.state.includes('faulty')).map(m => ({device: m.device, note: t('failed')}));
    const left = data.found.filter(f => f.activeArray === a.name)
      .flatMap(f => f.devices.map(n => ({device: n, note: t('left behind')})));
    return failed.concat(left);
  }

  function readdDialog(a, device) {
    const cands = readdCandidates(a);
    dialog({
      title: t('Take a device back into {0}', a.name), submit: t('Re-add'),
      fields: () => [
        h('label', {}, t('Device'), h('select', {name: 'device'}, cands.map(c =>
          h('option', {value: c.device, selected: c.device === device}, c.device + ' (' + c.note + ')')))),
        h('p', {}, t('A member that failed or was left behind goes back into its place. Thanks to the write-intent bitmap only what changed since is synchronized; nothing on the device is deleted.')),
        h('p', {class: 'hint'}, t('If it is too far behind, add it as a device instead: it is then rebuilt in full.')),
      ],
      onSubmit: async d => {
        const r = await call('raid', 'readd', {array: a.name, device: d.device});
        if (!jobs.started(r)) toast(t('{0} is back in {1}.', d.device, a.name));
      },
    }).then(ok => { if (ok) load(); });
  }

  function replaceDialog(a) {
    const members = a.members.filter(m => m.slot >= 0 && !m.state.includes('faulty') && !m.state.includes('replacement'));
    const spares = a.members.filter(m => m.slot < 0 && !m.state.includes('faulty'));
    const free = data.devices.filter(d => !a.componentSize || d.size >= a.componentSize);
    dialog({
      title: t('Replace a device of {0}', a.name), submit: t('Replace'), danger: true,
      fields: form => {
        const withSel = h('select', {name: 'with'},
          (spares.length ? [h('option', {value: ''}, t('a spare of the array') + ' (' + spares.map(m => m.device).join(', ') + ')')] : [])
            .concat(free.map(d => h('option', {value: d.name}, deviceLabel(d)))));
        const warn = h('div', {});
        const confirm = confirmBox(t('I know that all data on the selected device will be deleted.'));
        const update = () => {
          const fresh = withSel.value !== '';
          warn.replaceChildren(...(fresh ? [dataWarning(t('Warning: all data on {0} will be deleted', '/dev/' + withSel.value))] : []));
          confirm.hidden = !fresh;
        };
        withSel.addEventListener('change', update);
        setTimeout(update);
        return [
          h('label', {}, t('Device'), h('select', {name: 'device'}, members.map(m => h('option', {value: m.device}, m.device)))),
          h('label', {}, t('Replace with'), withSel),
          h('p', {}, t('The device is copied onto the replacement while it keeps working, so {0} never loses redundancy. Afterwards it is marked failed and can be removed.', a.name)),
          warn, confirm,
        ];
      },
      onSubmit: async d => {
        if (d.with && !d.confirm) throw new Error(t('Please confirm that the data will be deleted.'));
        const args = {array: a.name, device: d.device};
        if (d.with) Object.assign(args, {with: d.with, confirm: true});
        const r = await call('raid', 'replace', args);
        if (!jobs.started(r)) toast(t('Replacement of {0} started.', d.device));
      },
    }).then(ok => { if (ok) load(); });
  }

  async function checkArray(a, stop, repair) {
    const msg = repair ? (stop ? t('Repair on {0} stopped.', a.name) : t('Repair started on {0}.', a.name))
                       : (stop ? t('Check on {0} stopped.', a.name) : t('Check started on {0}.', a.name));
    const ok = await run(() => call('raid', 'check', {array: a.name, stop, repair}), msg);
    if (ok) load();
  }

  async function assemble(f) {
    const r = await run(() => call('raid', 'assemble', {uuid: f.uuid}));
  async function assemble(f, force) {
    const r = await run(() => call('raid', 'assemble', {uuid: f.uuid, force: !!force}));
    if (r) {
      if (!jobs.started(r)) toast(t('{0} assembled.', r.array));
      load();
    }
  }

  function forceDialog(f) {
    dialog({
      title: t('Start {0} forced?', f.label), submit: t('Force start'), danger: true,
      fields: () => [
        h('div', {class: 'callout danger'},
          h('p', {}, t('Too few members are up to date, or the array is degraded and wasn\'t shut down cleanly. Forced, the freshest out-of-date members are taken as current (like mdadm --assemble --force).')),
          h('p', {}, t('What was written after they dropped out isn\'t on them: check the file system and run a repair afterwards.'))),
        confirmBox(t('I know that data may be inconsistent.')),
      ],
      onSubmit: async d => {
        if (!d.confirm) throw new Error(t('Please confirm.'));
        const r = await call('raid', 'assemble', {uuid: f.uuid, force: true});
        if (!jobs.started(r)) toast(t('{0} assembled.', r.array));
      },
    }).then(ok => { if (ok) load(); });
  }

  // ---- page ----

  function arrayActions(a) {
@@ -343,9 +454,18 @@
      out.push(h('button', {class: 'small', disabled: !data.devices.length, title: data.devices.length ? null : t('No free device.'),
                            onclick: () => addDialog(a)}, t('Add device')));
    if (running && a.members.length) out.push(h('button', {class: 'small', onclick: () => removeDialog(a)}, t('Remove device')));
    if (running && readdCandidates(a).length)
      out.push(h('button', {class: 'small', onclick: () => readdDialog(a)}, t('Re-add device')));
    if (running && a.level !== 'raid0' && a.level !== 'linear') {
      if (a.syncAction === 'check') out.push(h('button', {class: 'small', onclick: () => checkArray(a, true)}, t('Stop check')));
      else out.push(h('button', {class: 'small', disabled: a.syncAction !== 'idle', onclick: () => checkArray(a, false)}, t('Check')));
      const spare = a.members.some(m => m.slot < 0 && !m.state.includes('faulty'));
      const can = spare || data.devices.length;
      out.push(h('button', {class: 'small', disabled: a.syncAction !== 'idle' || !can,
                            title: can ? null : t('No device to replace with: add a spare or free a device.'),
                            onclick: () => replaceDialog(a)}, t('Replace device')));
      if (a.syncAction === 'check') out.push(h('button', {class: 'small', onclick: () => checkArray(a, true, false)}, t('Stop check')));
      else if (a.syncAction === 'repair') out.push(h('button', {class: 'small', onclick: () => checkArray(a, true, true)}, t('Stop repair')));
      else out.push(h('button', {class: 'small', disabled: a.syncAction !== 'idle', onclick: () => checkArray(a, false, false)}, t('Check')),
                    h('button', {class: 'small', disabled: a.syncAction !== 'idle', onclick: () => checkArray(a, false, true)}, t('Repair')));
    }
    out.push(h('button', {class: 'small', disabled: a.inUse, onclick: () => stopDialog(a)}, t('Stop')));
    if (running) out.push(h('button', {class: 'small danger', disabled: a.inUse, onclick: () => deleteDialog(a)}, t('Delete')));
@@ -388,10 +508,19 @@
          {title: t('Level'), render: f => levelText(f.level)},
          {title: t('Present'), render: f => h('span', {class: 'badge ' + (f.present >= f.raidDisks ? 'ok' : 'warn')},
            t('{0} of {1}', f.present, f.raidDisks))},
          {title: t('Up to date'), render: f => f.activeArray ? '–' : h('span', {class: 'badge ' + (f.current >= f.raidDisks ? 'ok' : 'warn')},
            t('{0} of {1}', f.current, f.raidDisks))},
          {title: t('Devices'), render: f => h('span', {class: 'chips'}, f.devices.map(n => h('span', {}, n)))},
          {title: '', class: 'actions', render: f => f.activeArray
            ? h('span', {class: 'muted'}, t('part of the running {0}', f.activeArray))
            : h('button', {class: 'small', onclick: () => assemble(f)}, t('Assemble'))},
          {title: '', class: 'actions', render: f => {
            if (f.activeArray) {
              const a = data.arrays.find(x => x.name === f.activeArray);
              return [h('span', {class: 'muted'}, t('part of the running {0}', f.activeArray)),
                a ? h('button', {class: 'small', onclick: () => readdDialog(a, f.devices[0])}, t('Re-add')) : null];
            }
            if (f.startable === 'force') return h('button', {class: 'small danger', onclick: () => forceDialog(f)}, t('Force start'));
            if (f.startable === 'no') return h('span', {class: 'muted'}, t('not enough members'));
            return h('button', {class: 'small', onclick: () => assemble(f)}, t('Assemble'));
          }},
        ], data.found)));
    }
    const defects = jobs.defects(data.defective, size);