Loading modules/shares/shares.cpp +1 −119 Changes for modules/shares/shares.cpp: 1 added line, 119 removed lines. Original line number Diff line number Diff line Loading @@ -43,14 +43,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * smbPassword {name, password} set the user's password for login and * SMB; an empty password ends SMB logins * * iSCSI targets (sharedd's LIO targets): * info adds iscsi{targets[{name, iqn, comment, luns[], readOnly, open, * initiators[], chapUser, chapPasswordSet, chapUserIn, * chapPasswordInSet, sessions[], error}], devices[{device, kind, size, * inUse}]}; settings add iscsi, iscsiBaseName, iscsiPortals[] * iscsiCreate {target}, iscsiUpdate {name, target} (chapPassword / * chapPasswordIn missing or null: kept), iscsiDelete {name} * * Files and folders inside a share (sharedd's Browse & co.): * browse {share, path} -> {path, self{owner, group, mode}, entries[{name, type, size, mtime, * owner, group, mode}], accounts{users, groups}} Loading Loading @@ -155,88 +147,6 @@ std::string shareFields(json_object *s, Fields &out) { return ""; } json_object *strings(const std::vector<std::string> &v) { json_object *a = json_object_new_array(); for (auto &x : v) json_object_array_add(a, jstr(x)); return a; } json_object *iscsiTargetJson(const Fields &f) { json_object *o = json_object_new_object(); std::vector<std::string> luns, initiators, sessions; for (auto &p : f) { const std::string &k = p.first, &v = p.second; if (k == "name" || k == "iqn" || k == "comment" || k == "error") json_object_object_add(o, k.c_str(), jstr(v)); else if (k == "lun") luns.push_back(v); else if (k == "initiator") initiators.push_back(v); else if (k == "session") sessions.push_back(v); else if (k == "read_only") json_object_object_add(o, "readOnly", json_object_new_boolean(v == "yes")); else if (k == "open") json_object_object_add(o, "open", json_object_new_boolean(v == "yes")); else if (k == "chap_user") json_object_object_add(o, "chapUser", jstr(v)); else if (k == "chap_user_in") json_object_object_add(o, "chapUserIn", jstr(v)); else if (k == "chap_password_set") json_object_object_add(o, "chapPasswordSet", json_object_new_boolean(v == "yes")); else if (k == "chap_password_in_set") json_object_object_add(o, "chapPasswordInSet", json_object_new_boolean(v == "yes")); } json_object_object_add(o, "luns", strings(luns)); json_object_object_add(o, "initiators", strings(initiators)); json_object_object_add(o, "sessions", strings(sessions)); return o; } // iSCSI targets and the devices that could be LUNs; nullptr or an error json_object *iscsiInfo(json_object *out) { sysbus::Message r; if (json_object *e = callSharedd(sharedd::method::ListIscsiTargets, {}, r)) return e; json_object *iscsi = json_object_new_object(); json_object *targets = json_object_new_array(); for (auto &t : sharesOf(r)) json_object_array_add(targets, iscsiTargetJson(t)); json_object_object_add(iscsi, "targets", targets); if (json_object *e = callSharedd(sharedd::method::ListIscsiDevices, {}, r)) return json_object_put(iscsi), e; json_object *devices = json_object_new_array(), *cur = nullptr; for (auto &f : r.fields) { if (f.first == "device") { cur = json_object_new_object(); json_object_array_add(devices, cur); json_object_object_add(cur, "device", jstr(f.second)); } else if (cur && f.first == "size") { json_object_object_add(cur, "size", json_object_new_int64(std::strtoll(f.second.c_str(), nullptr, 10))); } else if (cur && f.first == "kind") { json_object_object_add(cur, "kind", jstr(f.second)); } else if (cur && f.first == "in_use") { json_object_object_add(cur, "inUse", jstr(f.second)); } } json_object_object_add(iscsi, "devices", devices); json_object_object_add(out, "iscsi", iscsi); return nullptr; } // a target from the UI as sharedd's fields; keepSecrets: missing/null // passwords stay as they are std::string iscsiFields(json_object *t, Fields &out, bool keepSecrets) { if (!t || !json_object_is_type(t, json_type_object)) return tr("no target given", "kein Target angegeben"); out.emplace_back("comment", webctl::str(t, "comment")); out.emplace_back("read_only", yesNo(webctl::boolean(t, "readOnly"))); out.emplace_back("open", yesNo(webctl::boolean(t, "open"))); for (auto &k : {std::make_pair("luns", "lun"), std::make_pair("initiators", "initiator")}) { json_object *list = nullptr; if (!json_object_object_get_ex(t, k.first, &list) || !list) continue; if (!json_object_is_type(list, json_type_array)) return tr("%1 must be a list", "%1 muss eine Liste sein", {k.first}); for (size_t i = 0; i < json_object_array_length(list); ++i) { const char *v = json_object_get_string(json_object_array_get_idx(list, i)); if (v && *v) out.emplace_back(k.second, v); } } out.emplace_back("chap_user", webctl::str(t, "chapUser")); out.emplace_back("chap_user_in", webctl::str(t, "chapUserIn")); for (auto &k : {std::make_pair("chapPassword", "chap_password"), std::make_pair("chapPasswordIn", "chap_password_in")}) { json_object *v = nullptr; bool given = json_object_object_get_ex(t, k.first, &v) && v; if (given || !keepSecrets) out.emplace_back(k.second, webctl::str(t, k.first)); } return ""; } json_object *info() { json_object *out = json_object_new_object(); Loading @@ -247,7 +157,7 @@ json_object *info() { json_object_put(e); } else { json_object *st = json_object_new_object(); for (const char *k : {"smb_state", "smb_error", "nfs_state", "nfs_error", "iscsi_state", "iscsi_error"}) for (const char *k : {"smb_state", "smb_error", "nfs_state", "nfs_error"}) json_object_object_add(st, k, jstr(r.get(k))); json_object_object_add(out, "servers", st); // not "status": the frontend takes that for the HTTP status Loading @@ -265,16 +175,12 @@ json_object *info() { json_object_object_add(set, "nfs", json_object_new_boolean(r.get("nfs") == "yes")); json_object_object_add(set, "nfsThreads", json_object_new_int(std::atoi(r.get("nfs_threads").c_str()))); json_object_object_add(set, "nfsRoot", jstr(r.get("nfs_root"))); json_object_object_add(set, "iscsi", json_object_new_boolean(r.get("iscsi") == "yes")); json_object_object_add(set, "iscsiBaseName", jstr(r.get("iscsi_base_name"))); json_object_object_add(set, "iscsiPortals", strings(r.getAll("iscsi_portal"))); json_object_object_add(out, "settings", set); if (json_object *e = callSharedd(sharedd::method::ListShares, {}, r)) return json_object_put(out), e; json_object *shares = json_object_new_array(); for (auto &s : sharesOf(r)) json_object_array_add(shares, shareJson(s)); json_object_object_add(out, "shares", shares); if (json_object *e = iscsiInfo(out)) return json_object_put(out), e; } // who can be given access, and who has an SMB password Loading Loading @@ -323,14 +229,6 @@ json_object *settings(json_object *a) { flag("nfs", "nfs"); if (webctl::has(a, "nfsThreads")) f.emplace_back("nfs_threads", std::to_string(webctl::integer(a, "nfsThreads", 8))); text("nfsRoot", "nfs_root"); flag("iscsi", "iscsi"); text("iscsiBaseName", "iscsi_base_name"); json_object *portals = nullptr; if (json_object_object_get_ex(a, "iscsiPortals", &portals) && portals && json_object_is_type(portals, json_type_array)) for (size_t i = 0; i < json_object_array_length(portals); ++i) { const char *v = json_object_get_string(json_object_array_get_idx(portals, i)); f.emplace_back("iscsi_portal", v ? v : ""); } sysbus::Message r; if (json_object *e = callSharedd(sharedd::method::SetSettings, f, r)) return e; return webctl::ok(); Loading Loading @@ -437,22 +335,6 @@ json_object *call(const webctl_request *req) { return webctl::ok(); } if (action == "settings") return settings(a); if (action == "iscsiDelete") { if (json_object *e = callSharedd(sharedd::method::DeleteIscsiTarget, {{"name", webctl::str(a, "name")}}, r)) return e; return webctl::ok(); } if (action == "iscsiCreate" || action == "iscsiUpdate") { json_object *t = nullptr; json_object_object_get_ex(a, "target", &t); bool update = action == "iscsiUpdate"; Fields f = {{"name", update ? webctl::str(a, "name") : webctl::str(t, "name")}}; std::string why = iscsiFields(t, f, update); if (!why.empty()) return error(why); if (update && webctl::str(t, "name") != f[0].second) f.emplace_back("new_name", webctl::str(t, "name")); if (json_object *e = callSharedd(update ? sharedd::method::EditIscsiTarget : sharedd::method::CreateIscsiTarget, f, r)) return e; return webctl::ok(); } if (action == "delete") { if (json_object *e = callSharedd(sharedd::method::DeleteShare, {{"name", webctl::str(a, "name")}}, r)) return e; return webctl::ok(); Loading www/modules/shares.js +2 −142 Changes for www/modules/shares.js: 2 added lines, 142 removed lines. Original line number Diff line number Diff line Loading @@ -4,30 +4,6 @@ const {h, t, table, dialog, confirmAction, call, run, toast} = webctl; webctl.translations({de: { 'iSCSI targets': 'iSCSI-Targets', 'New target': 'Neues Target', 'Target {0}': 'Target {0}', 'No iSCSI targets yet.': 'Noch keine iSCSI-Targets.', 'LUNs': 'LUNs', 'Sessions': 'Sitzungen', 'Volumes and disks': 'Volumes und Laufwerke', 'Image files (one path per line)': 'Image-Dateien (ein Pfad pro Zeile)', 'Ticked in order: LUN 0, 1, ... Mounted volumes can\'t be exported.': 'In Reihenfolge angehakt: LUN 0, 1, ... Eingehängte Volumes lassen sich nicht exportieren.', 'Any initiator (no access list; for a lab)': 'Jeder Initiator (keine Zugriffsliste; für ein Labor)', 'Initiators allowed (IQNs, one per line)': 'Erlaubte Initiatoren (IQNs, einer pro Zeile)', 'The initiator shows its IQN, e.g. iscsictl status or /etc/iscsi/initiatorname.iscsi.': 'Der Initiator zeigt seine IQN, z. B. iscsictl status oder /etc/iscsi/initiatorname.iscsi.', 'CHAP (the initiator logs in with user and secret)': 'CHAP (der Initiator meldet sich mit Benutzer und Passwort an)', 'CHAP user': 'CHAP-Benutzer', 'CHAP secret': 'CHAP-Passwort', 'unchanged': 'unverändert', 'Mutual CHAP (the target proves itself too)': 'Gegenseitiges CHAP (auch das Target weist sich aus)', 'Target user': 'Target-Benutzer', 'Target secret': 'Target-Passwort', 'any initiator': 'jeder Initiator', 'none': 'keine', 'in use': 'in Benutzung', 'Target {0} saved.': 'Target {0} gespeichert.', 'Target {0} created.': 'Target {0} angelegt.', 'Delete target {0}': 'Target {0} löschen', 'Initiators connected to it lose their disks; the volumes and files themselves stay.': 'Verbundene Initiatoren verlieren ihre Laufwerke; die Volumes und Dateien selbst bleiben.', 'Target {0} deleted.': 'Target {0} gelöscht.', 'iSCSI target (LIO)': 'iSCSI-Target (LIO)', 'Base name (targets: base name:name)': 'Basisname (Targets: Basisname:Name)', 'Portals (address:port, one per line)': 'Portale (Adresse:Port, eines pro Zeile)', 'Changing the base name renames every target: initiators have to discover them again.': 'Ein neuer Basisname benennt alle Targets um: die Initiatoren müssen sie neu suchen.', 'Name (target: {0}:name)': 'Name (Target: {0}:Name)', 'Shares': 'Freigaben', 'File servers': 'Dateiserver', 'running': 'läuft', 'failed': 'Fehler', 'disabled': 'aus', 'Settings': 'Einstellungen', 'New share': 'Neue Freigabe', 'Create': 'Anlegen', 'Name': 'Name', 'Path': 'Pfad', 'Protocols': 'Protokolle', 'Access': 'Zugriff', 'Who': 'Wer', Loading Loading @@ -284,110 +260,18 @@ h('div', {class: 'row'}, h('label', {}, t('Threads'), h('input', {name: 'nfsThreads', type: 'number', min: 1, max: 256, value: s.nfsThreads})), h('label', {}, t('NFSv4 root (clients see it as /)'), h('input', {name: 'nfsRoot', value: s.nfsRoot}))), h('label', {class: 'check'}, h('input', {type: 'checkbox', name: 'iscsi', checked: s.iscsi}), h('strong', {}, t('iSCSI target (LIO)'))), h('label', {}, t('Base name (targets: base name:name)'), h('input', {name: 'iscsiBaseName', value: s.iscsiBaseName, class: 'mono'})), h('p', {class: 'hint'}, t('Changing the base name renames every target: initiators have to discover them again.')), h('label', {}, t('Portals (address:port, one per line)'), h('textarea', {name: 'iscsiPortals', rows: 2, class: 'mono'}, (s.iscsiPortals || []).join('\n'))), ], onSubmit: async d => { await call('shares', 'settings', {smb: d.smb, workgroup: d.workgroup, netbiosName: d.netbiosName, serverString: d.serverString, minProtocol: d.minProtocol, maxProtocol: d.maxProtocol, signing: d.signing, mapToGuest: d.mapToGuest, guestAccount: d.guestAccount, nfs: d.nfs, nfsThreads: Number(d.nfsThreads), nfsRoot: d.nfsRoot.trim(), iscsi: d.iscsi, iscsiBaseName: d.iscsiBaseName.trim(), iscsiPortals: list(d.iscsiPortals)}); nfsRoot: d.nfsRoot.trim()}); toast(t('Settings saved.')); load(); }, }); } // ---- iSCSI targets ---- function iscsiDialog(tg) { const old = tg; tg = tg || {name: '', comment: '', luns: [], readOnly: false, open: false, initiators: [], chapUser: '', chapUserIn: ''}; const devices = data.iscsi.devices; const deviceOf = p => devices.find(d => d.device === p); const files = tg.luns.filter(l => !deviceOf(l)); const password = (name, isSet) => h('input', {name, type: 'password', autocomplete: 'new-password', maxlength: 255, placeholder: old && isSet ? t('unchanged') : ''}); dialog({ title: old ? t('Target {0}', old.name) : t('New target'), submit: old ? undefined : t('Create'), wide: true, fields: () => { const open = h('input', {type: 'checkbox', name: 'open', checked: tg.open}); const initiators = h('label', {}, t('Initiators allowed (IQNs, one per line)'), h('textarea', {name: 'initiators', rows: 3, class: 'mono'}, tg.initiators.join('\n'))); const chap = h('input', {type: 'checkbox', name: 'chap', checked: !!tg.chapUser}); const chapBox = h('div', {class: 'row'}, h('label', {}, t('CHAP user'), h('input', {name: 'chapUser', value: tg.chapUser, maxlength: 255})), h('label', {}, t('CHAP secret'), password('chapPassword', tg.chapPasswordSet))); const mutual = h('input', {type: 'checkbox', name: 'mutual', checked: !!tg.chapUserIn}); const mutualLabel = h('label', {class: 'check'}, mutual, t('Mutual CHAP (the target proves itself too)')); const mutualBox = h('div', {class: 'row'}, h('label', {}, t('Target user'), h('input', {name: 'chapUserIn', value: tg.chapUserIn, maxlength: 255})), h('label', {}, t('Target secret'), password('chapPasswordIn', tg.chapPasswordInSet))); const update = () => { initiators.hidden = open.checked; chapBox.hidden = mutualLabel.hidden = !chap.checked; mutualBox.hidden = !chap.checked || !mutual.checked; }; [open, chap, mutual].forEach(x => x.addEventListener('change', update)); update(); return [ h('label', {}, t('Name (target: {0}:name)', data.settings.iscsiBaseName), h('input', {name: 'name', value: tg.name, required: true, maxlength: 63, pattern: '[a-z0-9][a-z0-9.\\-]*'})), h('label', {}, t('Comment'), h('input', {name: 'comment', value: tg.comment})), h('label', {}, t('Volumes and disks')), h('div', {class: 'checklist'}, devices.map(d => { const mine = tg.luns.includes(d.device); return h('label', {class: 'check'}, h('input', {type: 'checkbox', name: 'devices', value: d.device, checked: mine, disabled: !mine && !!d.inUse}), h('span', {class: 'mono'}, d.device), ' ', h('span', {class: 'muted'}, d.kind + ', ' + bytes(d.size)), !mine && d.inUse ? [' ', h('span', {class: 'badge'}, d.inUse)] : null); })), h('label', {}, t('Image files (one path per line)'), h('textarea', {name: 'files', rows: 2, class: 'mono'}, files.join('\n'))), h('p', {class: 'hint'}, t('Ticked in order: LUN 0, 1, ... Mounted volumes can\'t be exported.')), h('label', {class: 'check'}, h('input', {type: 'checkbox', name: 'readOnly', checked: tg.readOnly}), t('Read-only')), h('label', {class: 'check'}, open, t('Any initiator (no access list; for a lab)')), initiators, h('p', {class: 'hint'}, t('The initiator shows its IQN, e.g. iscsictl status or /etc/iscsi/initiatorname.iscsi.')), h('label', {class: 'check'}, chap, t('CHAP (the initiator logs in with user and secret)')), chapBox, mutualLabel, mutualBox, ]; }, onSubmit: async d => { // keep the old LUN order for what stays, new ones after it const picked = d.devices || []; const luns = tg.luns.filter(l => picked.includes(l) || (!deviceOf(l) && d.files.split('\n').map(x => x.trim()).includes(l))); for (const l of [...picked, ...d.files.split('\n').map(x => x.trim()).filter(Boolean)]) if (!luns.includes(l)) luns.push(l); const target = {name: d.name.trim(), comment: d.comment, luns, readOnly: d.readOnly, open: d.open, initiators: d.open ? [] : d.initiators.split(/[\s,]+/).filter(Boolean), chapUser: d.chap ? d.chapUser.trim() : '', chapUserIn: d.chap && d.mutual ? d.chapUserIn.trim() : ''}; if (!d.chap) Object.assign(target, {chapPassword: '', chapPasswordIn: ''}); else { if (!old || d.chapPassword) target.chapPassword = d.chapPassword; if (!d.mutual) target.chapPasswordIn = ''; else if (!old || d.chapPasswordIn) target.chapPasswordIn = d.chapPasswordIn; } if (old) await call('shares', 'iscsiUpdate', {name: old.name, target}); else await call('shares', 'iscsiCreate', {target}); toast(old ? t('Target {0} saved.', target.name) : t('Target {0} created.', target.name)); load(); }, }); } async function deleteIscsi(tg) { if (!await confirmAction(t('Delete target {0}', tg.name), t('Initiators connected to it lose their disks; the volumes and files themselves stay.'), t('Delete'))) return; if (await run(() => call('shares', 'iscsiDelete', {name: tg.name}), t('Target {0} deleted.', tg.name)) !== undefined) load(); } function who(s) { const parts = []; if (s.smb) parts.push(s.users.length ? s.users.join(', ') : t('everyone with an SMB password')); Loading Loading @@ -543,8 +427,7 @@ h('button', {onclick: settingsDialog}, t('Settings'))), h('div', {class: 'table-wrap'}, h('table', {class: 'facts'}, h('tbody', {}, h('tr', {}, h('th', {}, 'SMB'), h('td', {}, badge(st.smb_state), st.smb_error ? h('span', {class: 'muted'}, ' ' + st.smb_error) : null)), h('tr', {}, h('th', {}, 'NFS'), h('td', {}, badge(st.nfs_state), st.nfs_error ? h('span', {class: 'muted'}, ' ' + st.nfs_error) : null)), h('tr', {}, h('th', {}, 'iSCSI'), h('td', {}, badge(st.iscsi_state), st.iscsi_error ? h('span', {class: 'muted'}, ' ' + st.iscsi_error) : null))))))); h('tr', {}, h('th', {}, 'NFS'), h('td', {}, badge(st.nfs_state), st.nfs_error ? h('span', {class: 'muted'}, ' ' + st.nfs_error) : null))))))); sections.push(h('section', {class: 'panel'}, h('div', {class: 'toolbar'}, h('h2', {}, t('Shares')), Loading @@ -563,29 +446,6 @@ h('button', {class: 'small danger', onclick: () => deleteShare(s)}, t('Delete')), ]}, ], data.shares, t('No shares yet.')))); if (data.iscsi && data.settings.iscsi) { const deviceOf = p => data.iscsi.devices.find(d => d.device === p); sections.push(h('section', {class: 'panel'}, h('div', {class: 'toolbar'}, h('h2', {}, t('iSCSI targets')), h('button', {class: 'primary', onclick: () => iscsiDialog(null)}, t('New target'))), table([ {title: t('Name'), render: x => [h('strong', {}, x.name), h('div', {class: 'muted mono'}, x.iqn), x.comment ? h('div', {class: 'muted'}, x.comment) : null]}, {title: t('LUNs'), render: x => x.luns.map((l, i) => h('div', {class: 'mono'}, i + ': ' + l, deviceOf(l) ? h('span', {class: 'muted'}, ' ' + bytes(deviceOf(l).size)) : null))}, {title: t('Access'), render: x => [ h('span', {class: 'badge ' + (x.readOnly ? '' : 'ok')}, x.readOnly ? t('read-only') : t('read-write')), x.chapUser ? [' ', h('span', {class: 'badge'}, 'CHAP' + (x.chapUserIn ? ' ⇄' : ''))] : null, h('div', {class: 'muted mono'}, x.open ? t('any initiator') : x.initiators.join(', '))]}, {title: t('Sessions'), render: x => [ x.sessions.length ? x.sessions.map(i => h('div', {class: 'mono'}, i)) : h('span', {class: 'muted'}, t('none')), x.error ? h('div', {}, h('span', {class: 'badge danger'}, x.error)) : null]}, {title: '', class: 'actions', render: x => [ h('button', {class: 'small', onclick: () => iscsiDialog(x)}, t('Edit')), h('button', {class: 'small danger', onclick: () => deleteIscsi(x)}, t('Delete'))]}, ], data.iscsi.targets, t('No iSCSI targets yet.')))); } } sections.push(h('section', {class: 'panel'}, h('div', {class: 'toolbar'}, h('h2', {}, t('SMB access'))), Loading Loading
modules/shares/shares.cpp +1 −119 Changes for modules/shares/shares.cpp: 1 added line, 119 removed lines. Original line number Diff line number Diff line Loading @@ -43,14 +43,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * smbPassword {name, password} set the user's password for login and * SMB; an empty password ends SMB logins * * iSCSI targets (sharedd's LIO targets): * info adds iscsi{targets[{name, iqn, comment, luns[], readOnly, open, * initiators[], chapUser, chapPasswordSet, chapUserIn, * chapPasswordInSet, sessions[], error}], devices[{device, kind, size, * inUse}]}; settings add iscsi, iscsiBaseName, iscsiPortals[] * iscsiCreate {target}, iscsiUpdate {name, target} (chapPassword / * chapPasswordIn missing or null: kept), iscsiDelete {name} * * Files and folders inside a share (sharedd's Browse & co.): * browse {share, path} -> {path, self{owner, group, mode}, entries[{name, type, size, mtime, * owner, group, mode}], accounts{users, groups}} Loading Loading @@ -155,88 +147,6 @@ std::string shareFields(json_object *s, Fields &out) { return ""; } json_object *strings(const std::vector<std::string> &v) { json_object *a = json_object_new_array(); for (auto &x : v) json_object_array_add(a, jstr(x)); return a; } json_object *iscsiTargetJson(const Fields &f) { json_object *o = json_object_new_object(); std::vector<std::string> luns, initiators, sessions; for (auto &p : f) { const std::string &k = p.first, &v = p.second; if (k == "name" || k == "iqn" || k == "comment" || k == "error") json_object_object_add(o, k.c_str(), jstr(v)); else if (k == "lun") luns.push_back(v); else if (k == "initiator") initiators.push_back(v); else if (k == "session") sessions.push_back(v); else if (k == "read_only") json_object_object_add(o, "readOnly", json_object_new_boolean(v == "yes")); else if (k == "open") json_object_object_add(o, "open", json_object_new_boolean(v == "yes")); else if (k == "chap_user") json_object_object_add(o, "chapUser", jstr(v)); else if (k == "chap_user_in") json_object_object_add(o, "chapUserIn", jstr(v)); else if (k == "chap_password_set") json_object_object_add(o, "chapPasswordSet", json_object_new_boolean(v == "yes")); else if (k == "chap_password_in_set") json_object_object_add(o, "chapPasswordInSet", json_object_new_boolean(v == "yes")); } json_object_object_add(o, "luns", strings(luns)); json_object_object_add(o, "initiators", strings(initiators)); json_object_object_add(o, "sessions", strings(sessions)); return o; } // iSCSI targets and the devices that could be LUNs; nullptr or an error json_object *iscsiInfo(json_object *out) { sysbus::Message r; if (json_object *e = callSharedd(sharedd::method::ListIscsiTargets, {}, r)) return e; json_object *iscsi = json_object_new_object(); json_object *targets = json_object_new_array(); for (auto &t : sharesOf(r)) json_object_array_add(targets, iscsiTargetJson(t)); json_object_object_add(iscsi, "targets", targets); if (json_object *e = callSharedd(sharedd::method::ListIscsiDevices, {}, r)) return json_object_put(iscsi), e; json_object *devices = json_object_new_array(), *cur = nullptr; for (auto &f : r.fields) { if (f.first == "device") { cur = json_object_new_object(); json_object_array_add(devices, cur); json_object_object_add(cur, "device", jstr(f.second)); } else if (cur && f.first == "size") { json_object_object_add(cur, "size", json_object_new_int64(std::strtoll(f.second.c_str(), nullptr, 10))); } else if (cur && f.first == "kind") { json_object_object_add(cur, "kind", jstr(f.second)); } else if (cur && f.first == "in_use") { json_object_object_add(cur, "inUse", jstr(f.second)); } } json_object_object_add(iscsi, "devices", devices); json_object_object_add(out, "iscsi", iscsi); return nullptr; } // a target from the UI as sharedd's fields; keepSecrets: missing/null // passwords stay as they are std::string iscsiFields(json_object *t, Fields &out, bool keepSecrets) { if (!t || !json_object_is_type(t, json_type_object)) return tr("no target given", "kein Target angegeben"); out.emplace_back("comment", webctl::str(t, "comment")); out.emplace_back("read_only", yesNo(webctl::boolean(t, "readOnly"))); out.emplace_back("open", yesNo(webctl::boolean(t, "open"))); for (auto &k : {std::make_pair("luns", "lun"), std::make_pair("initiators", "initiator")}) { json_object *list = nullptr; if (!json_object_object_get_ex(t, k.first, &list) || !list) continue; if (!json_object_is_type(list, json_type_array)) return tr("%1 must be a list", "%1 muss eine Liste sein", {k.first}); for (size_t i = 0; i < json_object_array_length(list); ++i) { const char *v = json_object_get_string(json_object_array_get_idx(list, i)); if (v && *v) out.emplace_back(k.second, v); } } out.emplace_back("chap_user", webctl::str(t, "chapUser")); out.emplace_back("chap_user_in", webctl::str(t, "chapUserIn")); for (auto &k : {std::make_pair("chapPassword", "chap_password"), std::make_pair("chapPasswordIn", "chap_password_in")}) { json_object *v = nullptr; bool given = json_object_object_get_ex(t, k.first, &v) && v; if (given || !keepSecrets) out.emplace_back(k.second, webctl::str(t, k.first)); } return ""; } json_object *info() { json_object *out = json_object_new_object(); Loading @@ -247,7 +157,7 @@ json_object *info() { json_object_put(e); } else { json_object *st = json_object_new_object(); for (const char *k : {"smb_state", "smb_error", "nfs_state", "nfs_error", "iscsi_state", "iscsi_error"}) for (const char *k : {"smb_state", "smb_error", "nfs_state", "nfs_error"}) json_object_object_add(st, k, jstr(r.get(k))); json_object_object_add(out, "servers", st); // not "status": the frontend takes that for the HTTP status Loading @@ -265,16 +175,12 @@ json_object *info() { json_object_object_add(set, "nfs", json_object_new_boolean(r.get("nfs") == "yes")); json_object_object_add(set, "nfsThreads", json_object_new_int(std::atoi(r.get("nfs_threads").c_str()))); json_object_object_add(set, "nfsRoot", jstr(r.get("nfs_root"))); json_object_object_add(set, "iscsi", json_object_new_boolean(r.get("iscsi") == "yes")); json_object_object_add(set, "iscsiBaseName", jstr(r.get("iscsi_base_name"))); json_object_object_add(set, "iscsiPortals", strings(r.getAll("iscsi_portal"))); json_object_object_add(out, "settings", set); if (json_object *e = callSharedd(sharedd::method::ListShares, {}, r)) return json_object_put(out), e; json_object *shares = json_object_new_array(); for (auto &s : sharesOf(r)) json_object_array_add(shares, shareJson(s)); json_object_object_add(out, "shares", shares); if (json_object *e = iscsiInfo(out)) return json_object_put(out), e; } // who can be given access, and who has an SMB password Loading Loading @@ -323,14 +229,6 @@ json_object *settings(json_object *a) { flag("nfs", "nfs"); if (webctl::has(a, "nfsThreads")) f.emplace_back("nfs_threads", std::to_string(webctl::integer(a, "nfsThreads", 8))); text("nfsRoot", "nfs_root"); flag("iscsi", "iscsi"); text("iscsiBaseName", "iscsi_base_name"); json_object *portals = nullptr; if (json_object_object_get_ex(a, "iscsiPortals", &portals) && portals && json_object_is_type(portals, json_type_array)) for (size_t i = 0; i < json_object_array_length(portals); ++i) { const char *v = json_object_get_string(json_object_array_get_idx(portals, i)); f.emplace_back("iscsi_portal", v ? v : ""); } sysbus::Message r; if (json_object *e = callSharedd(sharedd::method::SetSettings, f, r)) return e; return webctl::ok(); Loading Loading @@ -437,22 +335,6 @@ json_object *call(const webctl_request *req) { return webctl::ok(); } if (action == "settings") return settings(a); if (action == "iscsiDelete") { if (json_object *e = callSharedd(sharedd::method::DeleteIscsiTarget, {{"name", webctl::str(a, "name")}}, r)) return e; return webctl::ok(); } if (action == "iscsiCreate" || action == "iscsiUpdate") { json_object *t = nullptr; json_object_object_get_ex(a, "target", &t); bool update = action == "iscsiUpdate"; Fields f = {{"name", update ? webctl::str(a, "name") : webctl::str(t, "name")}}; std::string why = iscsiFields(t, f, update); if (!why.empty()) return error(why); if (update && webctl::str(t, "name") != f[0].second) f.emplace_back("new_name", webctl::str(t, "name")); if (json_object *e = callSharedd(update ? sharedd::method::EditIscsiTarget : sharedd::method::CreateIscsiTarget, f, r)) return e; return webctl::ok(); } if (action == "delete") { if (json_object *e = callSharedd(sharedd::method::DeleteShare, {{"name", webctl::str(a, "name")}}, r)) return e; return webctl::ok(); Loading
www/modules/shares.js +2 −142 Changes for www/modules/shares.js: 2 added lines, 142 removed lines. Original line number Diff line number Diff line Loading @@ -4,30 +4,6 @@ const {h, t, table, dialog, confirmAction, call, run, toast} = webctl; webctl.translations({de: { 'iSCSI targets': 'iSCSI-Targets', 'New target': 'Neues Target', 'Target {0}': 'Target {0}', 'No iSCSI targets yet.': 'Noch keine iSCSI-Targets.', 'LUNs': 'LUNs', 'Sessions': 'Sitzungen', 'Volumes and disks': 'Volumes und Laufwerke', 'Image files (one path per line)': 'Image-Dateien (ein Pfad pro Zeile)', 'Ticked in order: LUN 0, 1, ... Mounted volumes can\'t be exported.': 'In Reihenfolge angehakt: LUN 0, 1, ... Eingehängte Volumes lassen sich nicht exportieren.', 'Any initiator (no access list; for a lab)': 'Jeder Initiator (keine Zugriffsliste; für ein Labor)', 'Initiators allowed (IQNs, one per line)': 'Erlaubte Initiatoren (IQNs, einer pro Zeile)', 'The initiator shows its IQN, e.g. iscsictl status or /etc/iscsi/initiatorname.iscsi.': 'Der Initiator zeigt seine IQN, z. B. iscsictl status oder /etc/iscsi/initiatorname.iscsi.', 'CHAP (the initiator logs in with user and secret)': 'CHAP (der Initiator meldet sich mit Benutzer und Passwort an)', 'CHAP user': 'CHAP-Benutzer', 'CHAP secret': 'CHAP-Passwort', 'unchanged': 'unverändert', 'Mutual CHAP (the target proves itself too)': 'Gegenseitiges CHAP (auch das Target weist sich aus)', 'Target user': 'Target-Benutzer', 'Target secret': 'Target-Passwort', 'any initiator': 'jeder Initiator', 'none': 'keine', 'in use': 'in Benutzung', 'Target {0} saved.': 'Target {0} gespeichert.', 'Target {0} created.': 'Target {0} angelegt.', 'Delete target {0}': 'Target {0} löschen', 'Initiators connected to it lose their disks; the volumes and files themselves stay.': 'Verbundene Initiatoren verlieren ihre Laufwerke; die Volumes und Dateien selbst bleiben.', 'Target {0} deleted.': 'Target {0} gelöscht.', 'iSCSI target (LIO)': 'iSCSI-Target (LIO)', 'Base name (targets: base name:name)': 'Basisname (Targets: Basisname:Name)', 'Portals (address:port, one per line)': 'Portale (Adresse:Port, eines pro Zeile)', 'Changing the base name renames every target: initiators have to discover them again.': 'Ein neuer Basisname benennt alle Targets um: die Initiatoren müssen sie neu suchen.', 'Name (target: {0}:name)': 'Name (Target: {0}:Name)', 'Shares': 'Freigaben', 'File servers': 'Dateiserver', 'running': 'läuft', 'failed': 'Fehler', 'disabled': 'aus', 'Settings': 'Einstellungen', 'New share': 'Neue Freigabe', 'Create': 'Anlegen', 'Name': 'Name', 'Path': 'Pfad', 'Protocols': 'Protokolle', 'Access': 'Zugriff', 'Who': 'Wer', Loading Loading @@ -284,110 +260,18 @@ h('div', {class: 'row'}, h('label', {}, t('Threads'), h('input', {name: 'nfsThreads', type: 'number', min: 1, max: 256, value: s.nfsThreads})), h('label', {}, t('NFSv4 root (clients see it as /)'), h('input', {name: 'nfsRoot', value: s.nfsRoot}))), h('label', {class: 'check'}, h('input', {type: 'checkbox', name: 'iscsi', checked: s.iscsi}), h('strong', {}, t('iSCSI target (LIO)'))), h('label', {}, t('Base name (targets: base name:name)'), h('input', {name: 'iscsiBaseName', value: s.iscsiBaseName, class: 'mono'})), h('p', {class: 'hint'}, t('Changing the base name renames every target: initiators have to discover them again.')), h('label', {}, t('Portals (address:port, one per line)'), h('textarea', {name: 'iscsiPortals', rows: 2, class: 'mono'}, (s.iscsiPortals || []).join('\n'))), ], onSubmit: async d => { await call('shares', 'settings', {smb: d.smb, workgroup: d.workgroup, netbiosName: d.netbiosName, serverString: d.serverString, minProtocol: d.minProtocol, maxProtocol: d.maxProtocol, signing: d.signing, mapToGuest: d.mapToGuest, guestAccount: d.guestAccount, nfs: d.nfs, nfsThreads: Number(d.nfsThreads), nfsRoot: d.nfsRoot.trim(), iscsi: d.iscsi, iscsiBaseName: d.iscsiBaseName.trim(), iscsiPortals: list(d.iscsiPortals)}); nfsRoot: d.nfsRoot.trim()}); toast(t('Settings saved.')); load(); }, }); } // ---- iSCSI targets ---- function iscsiDialog(tg) { const old = tg; tg = tg || {name: '', comment: '', luns: [], readOnly: false, open: false, initiators: [], chapUser: '', chapUserIn: ''}; const devices = data.iscsi.devices; const deviceOf = p => devices.find(d => d.device === p); const files = tg.luns.filter(l => !deviceOf(l)); const password = (name, isSet) => h('input', {name, type: 'password', autocomplete: 'new-password', maxlength: 255, placeholder: old && isSet ? t('unchanged') : ''}); dialog({ title: old ? t('Target {0}', old.name) : t('New target'), submit: old ? undefined : t('Create'), wide: true, fields: () => { const open = h('input', {type: 'checkbox', name: 'open', checked: tg.open}); const initiators = h('label', {}, t('Initiators allowed (IQNs, one per line)'), h('textarea', {name: 'initiators', rows: 3, class: 'mono'}, tg.initiators.join('\n'))); const chap = h('input', {type: 'checkbox', name: 'chap', checked: !!tg.chapUser}); const chapBox = h('div', {class: 'row'}, h('label', {}, t('CHAP user'), h('input', {name: 'chapUser', value: tg.chapUser, maxlength: 255})), h('label', {}, t('CHAP secret'), password('chapPassword', tg.chapPasswordSet))); const mutual = h('input', {type: 'checkbox', name: 'mutual', checked: !!tg.chapUserIn}); const mutualLabel = h('label', {class: 'check'}, mutual, t('Mutual CHAP (the target proves itself too)')); const mutualBox = h('div', {class: 'row'}, h('label', {}, t('Target user'), h('input', {name: 'chapUserIn', value: tg.chapUserIn, maxlength: 255})), h('label', {}, t('Target secret'), password('chapPasswordIn', tg.chapPasswordInSet))); const update = () => { initiators.hidden = open.checked; chapBox.hidden = mutualLabel.hidden = !chap.checked; mutualBox.hidden = !chap.checked || !mutual.checked; }; [open, chap, mutual].forEach(x => x.addEventListener('change', update)); update(); return [ h('label', {}, t('Name (target: {0}:name)', data.settings.iscsiBaseName), h('input', {name: 'name', value: tg.name, required: true, maxlength: 63, pattern: '[a-z0-9][a-z0-9.\\-]*'})), h('label', {}, t('Comment'), h('input', {name: 'comment', value: tg.comment})), h('label', {}, t('Volumes and disks')), h('div', {class: 'checklist'}, devices.map(d => { const mine = tg.luns.includes(d.device); return h('label', {class: 'check'}, h('input', {type: 'checkbox', name: 'devices', value: d.device, checked: mine, disabled: !mine && !!d.inUse}), h('span', {class: 'mono'}, d.device), ' ', h('span', {class: 'muted'}, d.kind + ', ' + bytes(d.size)), !mine && d.inUse ? [' ', h('span', {class: 'badge'}, d.inUse)] : null); })), h('label', {}, t('Image files (one path per line)'), h('textarea', {name: 'files', rows: 2, class: 'mono'}, files.join('\n'))), h('p', {class: 'hint'}, t('Ticked in order: LUN 0, 1, ... Mounted volumes can\'t be exported.')), h('label', {class: 'check'}, h('input', {type: 'checkbox', name: 'readOnly', checked: tg.readOnly}), t('Read-only')), h('label', {class: 'check'}, open, t('Any initiator (no access list; for a lab)')), initiators, h('p', {class: 'hint'}, t('The initiator shows its IQN, e.g. iscsictl status or /etc/iscsi/initiatorname.iscsi.')), h('label', {class: 'check'}, chap, t('CHAP (the initiator logs in with user and secret)')), chapBox, mutualLabel, mutualBox, ]; }, onSubmit: async d => { // keep the old LUN order for what stays, new ones after it const picked = d.devices || []; const luns = tg.luns.filter(l => picked.includes(l) || (!deviceOf(l) && d.files.split('\n').map(x => x.trim()).includes(l))); for (const l of [...picked, ...d.files.split('\n').map(x => x.trim()).filter(Boolean)]) if (!luns.includes(l)) luns.push(l); const target = {name: d.name.trim(), comment: d.comment, luns, readOnly: d.readOnly, open: d.open, initiators: d.open ? [] : d.initiators.split(/[\s,]+/).filter(Boolean), chapUser: d.chap ? d.chapUser.trim() : '', chapUserIn: d.chap && d.mutual ? d.chapUserIn.trim() : ''}; if (!d.chap) Object.assign(target, {chapPassword: '', chapPasswordIn: ''}); else { if (!old || d.chapPassword) target.chapPassword = d.chapPassword; if (!d.mutual) target.chapPasswordIn = ''; else if (!old || d.chapPasswordIn) target.chapPasswordIn = d.chapPasswordIn; } if (old) await call('shares', 'iscsiUpdate', {name: old.name, target}); else await call('shares', 'iscsiCreate', {target}); toast(old ? t('Target {0} saved.', target.name) : t('Target {0} created.', target.name)); load(); }, }); } async function deleteIscsi(tg) { if (!await confirmAction(t('Delete target {0}', tg.name), t('Initiators connected to it lose their disks; the volumes and files themselves stay.'), t('Delete'))) return; if (await run(() => call('shares', 'iscsiDelete', {name: tg.name}), t('Target {0} deleted.', tg.name)) !== undefined) load(); } function who(s) { const parts = []; if (s.smb) parts.push(s.users.length ? s.users.join(', ') : t('everyone with an SMB password')); Loading Loading @@ -543,8 +427,7 @@ h('button', {onclick: settingsDialog}, t('Settings'))), h('div', {class: 'table-wrap'}, h('table', {class: 'facts'}, h('tbody', {}, h('tr', {}, h('th', {}, 'SMB'), h('td', {}, badge(st.smb_state), st.smb_error ? h('span', {class: 'muted'}, ' ' + st.smb_error) : null)), h('tr', {}, h('th', {}, 'NFS'), h('td', {}, badge(st.nfs_state), st.nfs_error ? h('span', {class: 'muted'}, ' ' + st.nfs_error) : null)), h('tr', {}, h('th', {}, 'iSCSI'), h('td', {}, badge(st.iscsi_state), st.iscsi_error ? h('span', {class: 'muted'}, ' ' + st.iscsi_error) : null))))))); h('tr', {}, h('th', {}, 'NFS'), h('td', {}, badge(st.nfs_state), st.nfs_error ? h('span', {class: 'muted'}, ' ' + st.nfs_error) : null))))))); sections.push(h('section', {class: 'panel'}, h('div', {class: 'toolbar'}, h('h2', {}, t('Shares')), Loading @@ -563,29 +446,6 @@ h('button', {class: 'small danger', onclick: () => deleteShare(s)}, t('Delete')), ]}, ], data.shares, t('No shares yet.')))); if (data.iscsi && data.settings.iscsi) { const deviceOf = p => data.iscsi.devices.find(d => d.device === p); sections.push(h('section', {class: 'panel'}, h('div', {class: 'toolbar'}, h('h2', {}, t('iSCSI targets')), h('button', {class: 'primary', onclick: () => iscsiDialog(null)}, t('New target'))), table([ {title: t('Name'), render: x => [h('strong', {}, x.name), h('div', {class: 'muted mono'}, x.iqn), x.comment ? h('div', {class: 'muted'}, x.comment) : null]}, {title: t('LUNs'), render: x => x.luns.map((l, i) => h('div', {class: 'mono'}, i + ': ' + l, deviceOf(l) ? h('span', {class: 'muted'}, ' ' + bytes(deviceOf(l).size)) : null))}, {title: t('Access'), render: x => [ h('span', {class: 'badge ' + (x.readOnly ? '' : 'ok')}, x.readOnly ? t('read-only') : t('read-write')), x.chapUser ? [' ', h('span', {class: 'badge'}, 'CHAP' + (x.chapUserIn ? ' ⇄' : ''))] : null, h('div', {class: 'muted mono'}, x.open ? t('any initiator') : x.initiators.join(', '))]}, {title: t('Sessions'), render: x => [ x.sessions.length ? x.sessions.map(i => h('div', {class: 'mono'}, i)) : h('span', {class: 'muted'}, t('none')), x.error ? h('div', {}, h('span', {class: 'badge danger'}, x.error)) : null]}, {title: '', class: 'actions', render: x => [ h('button', {class: 'small', onclick: () => iscsiDialog(x)}, t('Edit')), h('button', {class: 'small danger', onclick: () => deleteIscsi(x)}, t('Delete'))]}, ], data.iscsi.targets, t('No iSCSI targets yet.')))); } } sections.push(h('section', {class: 'panel'}, h('div', {class: 'toolbar'}, h('h2', {}, t('SMB access'))), Loading