The hint said to run 'sudo visudo' as though the account running the command already had sudo. On a machine set up the way this one was - a separate administrator, then a daily-use account - the daily-use account is not in the sudo group, so that instruction could not be followed by the person who needed it. Replaced with the two steps that actually apply, in order: 1. The account running the command must be in the sudo group. Check with 'id -nG'; if it is not there, an administrator adds it with 'usermod -aG sudo <account>' and the account signs out and back in, because group membership is fixed at sign-in. This is the step that was missing, and it is the one that cannot be done by the person stuck. 2. Passwordless sudo, from an account that already has sudo: 'sudo visudo -f /etc/sudoers.d/umbra-node', add the NOPASSWD line, save. Optional - without it the command still works and simply prompts. Written after a run stopped at a sudo password prompt with nothing on screen but 'Sorry, try again.', which is sudo's own message and appears nowhere in this codebase, so the failure looked like the system refusing rather than the shell asking. Checked against 238: umbra is already in the sudo group, so only step 2 applies there.
257 lines
12 KiB
HTML
257 lines
12 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="description" content="UmbraTrans administrative console">
|
|
<title>Administration · UmbraTrans</title>
|
|
<link rel="icon" type="image/png" href="../assets/umbratrans-emblem.png">
|
|
<link rel="stylesheet" href="../assets/styles.css">
|
|
</head>
|
|
<body class="admin-page">
|
|
<header class="admin-topbar">
|
|
<div class="admin-layout">
|
|
<a class="brand-lockup" href="../index.html">
|
|
<img class="brand-image" src="../assets/umbratrans-emblem.png" alt="">
|
|
<span>UmbraTrans</span>
|
|
</a>
|
|
<div class="admin-context">
|
|
<strong id="admin-display-name">Checking session…</strong>
|
|
<small id="admin-identity">No session</small>
|
|
</div>
|
|
<a class="button button--ghost" href="../settings.html">Sign out</a>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="admin-main">
|
|
<nav class="admin-nav" aria-label="Sections">
|
|
<a class="admin-nav-link" href="./index.html#dashboard">Console</a>
|
|
<a class="admin-nav-link admin-nav-link--active" href="#nodes" data-view-link="nodes"
|
|
aria-current="page"><span aria-hidden="true">◈</span>Nodes</a>
|
|
<a class="admin-nav-link" href="./index.html">Everything else</a>
|
|
<a class="admin-nav-link" href="../settings.html">Your account</a>
|
|
</nav>
|
|
|
|
<section class="admin-view" id="view-nodes" data-view="nodes">
|
|
<div class="admin-heading">
|
|
<div>
|
|
<p class="eyebrow">Compute</p>
|
|
<h1>Nodes</h1>
|
|
<p>
|
|
Machines that can be given work. A node is <em>online</em> because its agent
|
|
heartbeated, not because it was created — so a node you have just added will
|
|
sit at <strong>provisioning</strong> until it runs the bootstrap, and a node
|
|
whose agent has stopped will read <strong>offline</strong> about 45 seconds
|
|
after its last heartbeat even though the stored status still says online.
|
|
</p>
|
|
</div>
|
|
<button class="button button--primary" type="button" id="open-add-node">
|
|
Add a node
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Shown only when the account can see no organizations at all. A node belongs to
|
|
exactly one, so there is nothing to add it to and the form below is disabled
|
|
rather than merely described. -->
|
|
<div class="admin-banners">
|
|
<p class="admin-banner admin-banner--warning" id="no-organizations" hidden>
|
|
<strong>There are no organizations yet.</strong>
|
|
<p>
|
|
A node belongs to exactly one organization, so there is nowhere to put this one
|
|
until an organization exists. An organization is the outermost boundary in the
|
|
system: everything else lives inside one.
|
|
</p>
|
|
<p>
|
|
<a class="button button--primary" href="./index.html#organizations">
|
|
Go to organizations and create one
|
|
</a>
|
|
<a class="button button--ghost" href="./index.html#organizations">
|
|
Open the organizations screen
|
|
</a>
|
|
</p>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="admin-card" id="add-node-card" hidden>
|
|
<h2>Add a node</h2>
|
|
<p class="otp-panel-target">
|
|
This produces a one-line command and a six-digit code. Run the command on the
|
|
machine; it asks for the code, pairs with the controller, installs the agent
|
|
and registers it as a service.
|
|
</p>
|
|
|
|
<form id="add-node-form" novalidate>
|
|
<div class="form-field">
|
|
<label for="node-organization">Organization</label>
|
|
<select id="node-organization" name="organization_id" required
|
|
aria-describedby="node-organization-hint">
|
|
<option value="">Choose an organization…</option>
|
|
</select>
|
|
<small id="node-organization-hint" class="field-hint">
|
|
A node belongs to exactly one organization, so the node list and every
|
|
job it takes are scoped by it. A platform administrator has no
|
|
organization of their own and must choose one — that is why this is a
|
|
field rather than something inferred.
|
|
</small>
|
|
</div>
|
|
|
|
<div class="form-field">
|
|
<label for="node-name">Node name</label>
|
|
<input id="node-name" name="node_name" type="text" autocomplete="off"
|
|
maxlength="120" required
|
|
aria-describedby="node-name-hint">
|
|
<small id="node-name-hint" class="field-hint">
|
|
The name this machine will report. The pairing code is bound to it, so a
|
|
code that ends up in the wrong hands cannot pair a different machine.
|
|
Lowercase letters, digits and dashes read best on a terminal.
|
|
</small>
|
|
</div>
|
|
|
|
<div class="form-field">
|
|
<label for="node-locality">Where it is</label>
|
|
<input id="node-locality" name="locality" type="text" autocomplete="off"
|
|
maxlength="120" placeholder="192.168.123.238 or a name you will recognise">
|
|
<small class="field-hint">For you, not for the system. Helps when six nodes
|
|
report at once.</small>
|
|
</div>
|
|
|
|
<div class="form-field">
|
|
<label for="node-trust-domain">Trust domain</label>
|
|
<select id="node-trust-domain" name="trust_domain">
|
|
<option value="local">local — on my own network</option>
|
|
<option value="remote">remote — reached across a network I do not control</option>
|
|
<option value="cloud">cloud — a hosted machine</option>
|
|
</select>
|
|
<small class="field-hint">
|
|
Used to refuse work across a boundary you have said is a boundary.
|
|
</small>
|
|
</div>
|
|
|
|
<div class="form-field">
|
|
<label for="node-controller">Controller address</label>
|
|
<input id="node-controller" name="controller_url" type="text" autocomplete="off"
|
|
value="http://192.168.123.14:8442">
|
|
<small class="field-hint">
|
|
The address the node will reach us on, from the node's side. If the node is
|
|
on another machine, use an address <em>it</em> can resolve.
|
|
</small>
|
|
</div>
|
|
|
|
<div class="form-field">
|
|
<input id="node-as-root" name="as_root" type="checkbox">
|
|
<label for="node-as-root">
|
|
Expect to run as root on an exclusive machine
|
|
</label>
|
|
<small class="field-hint" id="root-access-hint">
|
|
Ticking this changes the command below to <code>curl … | sudo sh</code> and
|
|
installs to <code>/opt/umbra</code> as a system service running as its own
|
|
<code>umbra-node</code> account. Unticked, everything stays in your home
|
|
directory with a user service and no password is needed.
|
|
</small>
|
|
<ol class="root-access-steps">
|
|
<li>
|
|
<strong>The account running the command must be in the
|
|
<code>sudo</code> group.</strong> On a machine set up with a separate
|
|
administrator and then a daily-use account, the first account created is
|
|
the one that gets <code>sudo</code>; a later account does not. Check
|
|
with <code>id -nG</code>. If <code>sudo</code> is not listed, an
|
|
administrator adds it:
|
|
<code>sudo usermod -aG sudo <var>account</var></code>
|
|
then sign out and back in — group membership is fixed at sign-in.
|
|
</li>
|
|
<li>
|
|
<strong>To avoid typing a password for every node</strong>, grant
|
|
passwordless sudo for that one account. Run this
|
|
<em>from an account that already has sudo</em>:
|
|
<code>sudo visudo -f /etc/sudoers.d/umbra-node</code>
|
|
then add this line and save:
|
|
<code>umbra ALL=(ALL) NOPASSWD: ALL</code>
|
|
Replace <code>umbra</code> with whichever account will run the command.
|
|
Without this step the command still works — it will simply ask for a
|
|
password each time.
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button class="button button--primary" type="submit" id="add-node-submit">
|
|
Create the pairing code
|
|
</button>
|
|
<button class="button button--ghost" type="button" id="add-node-cancel">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="admin-card" id="pairing-result-card" hidden>
|
|
<h2>Run this on the machine</h2>
|
|
<p class="otp-panel-target" id="pairing-node-name-note"></p>
|
|
|
|
<div class="otp-panel">
|
|
<div class="otp-panel-head">
|
|
<span class="field-label" id="pairing-command-label">Command</span>
|
|
</div>
|
|
<p class="otp-panel-target">
|
|
Paste this into a terminal on the machine you are onboarding. It downloads the
|
|
bootstrap; read it if you want to — it is a script fetched over the network.
|
|
</p>
|
|
<!-- The value and its copy button sit side by side, reusing the one-time-code
|
|
row the provisioning dialog already uses. A command a person is going to
|
|
retype should not be something they have to select by hand. -->
|
|
<div class="otp-value-row">
|
|
<code class="otp-value" id="pairing-command"></code>
|
|
<button class="button button--ghost" type="button" id="pairing-copy-command">
|
|
Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="otp-panel">
|
|
<div class="otp-panel-head">
|
|
<span class="field-label" id="pairing-code-label">Six-digit code</span>
|
|
</div>
|
|
<p class="otp-panel-target">
|
|
The bootstrap asks for this. It expires, and it is spent by the first machine
|
|
that uses it — so copy it before you log out of this page.
|
|
</p>
|
|
<div class="otp-value-row">
|
|
<code class="otp-value" id="pairing-code"></code>
|
|
<button class="button button--ghost" type="button" id="pairing-copy-code">
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<p class="otp-hint" id="pairing-code-hint" role="status" aria-live="polite"></p>
|
|
</div>
|
|
|
|
<div class="admin-banners">
|
|
<p class="admin-banner admin-banner--warning" id="pairing-warning" hidden>
|
|
<strong>Write this down now.</strong>
|
|
<span id="pairing-warning-text"></span>
|
|
If you lose it, create a new one — a lost code is not a problem, but a code
|
|
that has expired is a wasted trip.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button class="button button--ghost" type="button" id="pairing-done">
|
|
Done — clear this from the page
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-card">
|
|
<div id="node-list" class="admin-table-wrap" aria-live="polite"></div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- showToast() in api.js looks for id="toast". With a different id every message -
|
|
success and failure alike - is appended to nothing and returns silently, which
|
|
looks exactly like the button not working. -->
|
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
|
|
|
<script src="../assets/api.js" defer></script>
|
|
<script src="../assets/nodes.js" defer></script>
|
|
</body>
|
|
</html> |