The uplink seat (ADR 0117), and the mesh's names written into the hosts file, not over it (hq 128) #79
@@ -923,12 +923,26 @@ func planCommand(ctx context.Context, args []string) error {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("\n--- %v %v ---\n%s", r["id"], r["path"], content)
|
||||
fmt.Printf("\n--- %s ---\n%s", shownAs(r), content)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shownAs is the heading `plan --show` puts over a resource's content.
|
||||
//
|
||||
// **A file written into says so.** Its content is the mesh's part of a file that is otherwise the
|
||||
// machine's — the keys of a JSON document (novox/hq ADR 0102), the region of a hosts file (issue
|
||||
// 128). Shown under a bare path it reads as the whole file, and a person checking what a take
|
||||
// replaces would see a hosts file of a dozen lines where the machine keeps thirty.
|
||||
func shownAs(r map[string]any) string {
|
||||
heading := fmt.Sprintf("%v %v", r["id"], r["path"])
|
||||
if into, ok := r["into"].(string); ok && into != "" {
|
||||
heading += fmt.Sprintf(" (written into, %s)", into)
|
||||
}
|
||||
return heading
|
||||
}
|
||||
|
||||
// licencesFor is what this node can be answered with by record, and what it was put on.
|
||||
//
|
||||
// A mesh with no licences at all is the ordinary case and must not be an error: every existing
|
||||
|
||||
@@ -35,3 +35,17 @@ func TestListensLinesAreEmptyForAModuleWithNothingToListenOn(t *testing.T) {
|
||||
t.Errorf("a module with no listens should print nothing, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// `plan --show` says when a file is written into rather than over (novox/hq issue 128), or the
|
||||
// mesh's region of a hosts file reads as the whole file.
|
||||
func TestAFileWrittenIntoIsShownAsSuch(t *testing.T) {
|
||||
region := shownAs(map[string]any{
|
||||
"id": "mesh-wireguard.fact-node-names", "path": "/etc/hosts", "into": "block"})
|
||||
if region != "mesh-wireguard.fact-node-names /etc/hosts (written into, block)" {
|
||||
t.Errorf("the region is shown as %q", region)
|
||||
}
|
||||
whole := shownAs(map[string]any{"id": "dnsmasq.fact-node-zones", "path": "/etc/mesh-resolver/nodes.conf"})
|
||||
if strings.Contains(whole, "written into") {
|
||||
t.Errorf("a whole file is shown as written into: %q", whole)
|
||||
}
|
||||
}
|
||||
|
||||
+74
-26
@@ -18,8 +18,10 @@ import (
|
||||
// module at all (novox/hq ADR 0040).
|
||||
|
||||
const (
|
||||
// FactNodeNames is every machine's name and address, as a hosts file.
|
||||
// FactNodeNames is every machine's name and address, as lines of a hosts file.
|
||||
//
|
||||
// Written *into* the machine's hosts file as a region of its own, never as the file: the rest
|
||||
// of that file is the distribution's, the operator's and other tools' (novox/hq issue 128).
|
||||
// Exact names only: `homer` and `homer.internal` resolve to homer. Anything *under* a machine
|
||||
// is a wildcard, which a hosts file cannot express — that is FactNodeZones.
|
||||
FactNodeNames = "node-names"
|
||||
@@ -31,20 +33,44 @@ const (
|
||||
FactNodeZones = "node-zones"
|
||||
)
|
||||
|
||||
// facts is every fact the mesh computes, and what writes it.
|
||||
// fact is one thing the mesh computes, and how it is written.
|
||||
type fact struct {
|
||||
// render is the fact's content. A fact is written from the names it is about. `every` is every
|
||||
// name the mesh serves — machines and the names it was told to route; `machines` is only the
|
||||
// machines. A fact takes the set it is true of, and the two must not be confused
|
||||
// (novox/hq 04-ISSUES/111).
|
||||
render func(r Resolution, every, machines map[string]string, suffix string) string
|
||||
// shared is whether the file the fact goes to belongs to the machine rather than to the mesh.
|
||||
//
|
||||
// **A property of the fact, not of the path a module asked for.** A hosts file is the
|
||||
// machine's wherever it lives: the distribution put `localhost` in it, the operator added
|
||||
// their own lines, and a local development tool keeps marked blocks of its own there. The
|
||||
// mesh writing it whole replaced all of that the moment the private network was taken, and
|
||||
// every later write by the other tool was lost at the next machine joining — silently, with
|
||||
// both sides believing they owned the file (novox/hq issue 128). That is ADR 0102's failure in
|
||||
// a file 0102 did not name, because its merge is structured and a hosts file is not; the
|
||||
// answer is the same idea for text — a marked region the host owns, everything outside it
|
||||
// kept byte for byte. A resolver's zones file is the other way about: the mesh owns it, and
|
||||
// nothing else writes there.
|
||||
shared bool
|
||||
}
|
||||
|
||||
// facts is every fact the mesh computes, and how each is written.
|
||||
//
|
||||
// **A closed list.** A module asking for a fact the mesh does not have is asking for a file nobody
|
||||
// will write, and finding that out on a machine — as a daemon that starts, reads nothing, and
|
||||
// answers no queries — is worse than being told where the manifest is.
|
||||
// A fact is written from the names it is about. `every` is every name the mesh serves — machines
|
||||
// and the names it was told to route; `machines` is only the machines. A fact takes the set it is
|
||||
// true of, and the two must not be confused (novox/hq 04-ISSUES/111).
|
||||
var facts = map[string]func(r Resolution, every, machines map[string]string, suffix string) string{
|
||||
FactNodeNames: func(r Resolution, every, _ map[string]string, suffix string) string {
|
||||
return nodeNames(r, every, suffix)
|
||||
var facts = map[string]fact{
|
||||
FactNodeNames: {
|
||||
render: func(r Resolution, every, _ map[string]string, suffix string) string {
|
||||
return nodeNames(r, every, suffix)
|
||||
},
|
||||
shared: true,
|
||||
},
|
||||
FactNodeZones: func(r Resolution, _, machines map[string]string, suffix string) string {
|
||||
return nodeZones(r, machines, suffix)
|
||||
FactNodeZones: {
|
||||
render: func(r Resolution, _, machines map[string]string, suffix string) string {
|
||||
return nodeZones(r, machines, suffix)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,7 +90,7 @@ func FactsInto(m Manifest, r Resolution, addresses, machines map[string]string,
|
||||
|
||||
out := make([]map[string]any, 0, len(names))
|
||||
for _, name := range names {
|
||||
write, known := facts[name]
|
||||
f, known := facts[name]
|
||||
if !known {
|
||||
return nil, fmt.Errorf(
|
||||
"%s asks the mesh for %q, which it does not compute. It has %s",
|
||||
@@ -75,10 +101,27 @@ func FactsInto(m Manifest, r Resolution, addresses, machines map[string]string,
|
||||
return nil, fmt.Errorf(
|
||||
"%s asks for %q at %q, which is not an absolute path", m.Module, name, path)
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
file := map[string]any{
|
||||
"id": "fact-" + name, "type": "file", "path": path, "mode": "0644",
|
||||
"content": write(r, addresses, machines, suffix),
|
||||
})
|
||||
"content": f.render(r, addresses, machines, suffix),
|
||||
}
|
||||
if f.shared {
|
||||
// The host owns only the lines between `# BEGIN mesh <id>` and `# END mesh <id>` and
|
||||
// keeps the rest of the file byte for byte; undeclared, the region goes and nothing
|
||||
// else does. Replacing nothing, it is written on an adopted node without being held,
|
||||
// so a machine is named on the private network before its module is taken.
|
||||
//
|
||||
// **Hosts first, then this — an order to roll out in, not a note.** A host older than
|
||||
// the block mode refuses the whole declaration on an `into` it does not know: not just
|
||||
// this file, everything the node was sent, so it stops converging on anything at all.
|
||||
// Every node on the private network receives this fact, so every node's host —
|
||||
// the controller's own machine included, which would otherwise stop taking the
|
||||
// declaration that runs the controller — must run a block-aware host before a
|
||||
// controller emitting it is rolled out. The same order ADR 0102 set for `into: json`
|
||||
// (novox/hq issue 128).
|
||||
file["into"] = "block"
|
||||
}
|
||||
out = append(out, file)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -93,7 +136,18 @@ func spokenFacts() string {
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// nodeNames is every machine's name and address, as a hosts file.
|
||||
// nodeNames is every machine's name and address, as the mesh's region of a hosts file.
|
||||
//
|
||||
// **Only the mesh's names.** No header claiming the file, no `localhost`, no `127.0.1.1` line for
|
||||
// the machine itself. Those used to be written here as the floor every Linux expects, because the
|
||||
// mesh wrote the whole file and removing them would have broken things with nothing to do with
|
||||
// the mesh. They were never the mesh's: the distribution wrote them before the mesh arrived and
|
||||
// will want them after it leaves, and a machine's own name belongs to whoever named the machine.
|
||||
// Now the host writes this into a marked region (novox/hq issue 128) and leaves the rest of the
|
||||
// file as it found it, so the floor stays where it always was — the machine's — and the mesh
|
||||
// writing a second `localhost` beside it would be one more line nobody could say the owner of.
|
||||
// The one comment line is for a person reading the file: which lines are the mesh's, and that
|
||||
// editing them is pointless.
|
||||
//
|
||||
// **A machine with no address is left out.** The mesh has a record for it — somebody added it —
|
||||
// and does not yet know where it is, which is the ordinary state between adding a machine and it
|
||||
@@ -101,21 +155,15 @@ func spokenFacts() string {
|
||||
// that hangs; leaving it out fails at once and says the name is unknown.
|
||||
func nodeNames(r Resolution, addresses map[string]string, suffix string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n")
|
||||
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n\n")
|
||||
// The floor every Linux expects, and which removing would break things that have nothing to do
|
||||
// with the mesh.
|
||||
b.WriteString("127.0.0.1\tlocalhost\n")
|
||||
b.WriteString("::1\t\tlocalhost ip6-localhost ip6-loopback\n")
|
||||
if r.Node != "" {
|
||||
fmt.Fprintf(&b, "127.0.1.1\t%s\n", r.Node)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString("# The mesh's names. This region is replaced whenever a machine joins or leaves.\n")
|
||||
for _, name := range sortedNames(addresses) {
|
||||
at := addresses[name]
|
||||
internal, bare := meshName(name, suffix)
|
||||
// Its mesh name resolves to its address on the private network rather than to loopback,
|
||||
// so a service binding the name it was given stays reachable from everywhere else.
|
||||
// so a service binding the name it was given stays reachable from everywhere else. The
|
||||
// bare name may be answered first by a line of the machine's own — `127.0.1.1 homer`,
|
||||
// above the region — and that is the machine's choice to have made; the mesh name is the
|
||||
// one nothing else in the file writes.
|
||||
fmt.Fprintf(&b, "%s\t%s\t%s", at, internal, bare)
|
||||
if bare == r.Node {
|
||||
b.WriteString("\t# this machine")
|
||||
|
||||
@@ -54,9 +54,60 @@ func TestAMachinesOwnNameIsItsMeshAddress(t *testing.T) {
|
||||
if !strings.HasPrefix(line, "10.42.0.1") {
|
||||
t.Fatalf("a machine's own mesh name is not its mesh address: %q", line)
|
||||
}
|
||||
// And the loopback floor is still there, or things with nothing to do with the mesh break.
|
||||
if !strings.Contains(out, "127.0.0.1\tlocalhost") {
|
||||
t.Fatalf("the loopback floor was removed:\n%s", out)
|
||||
}
|
||||
|
||||
// novox/hq issue 128: the names are the mesh's region of the machine's hosts file, and only that.
|
||||
//
|
||||
// **The loopback floor is the machine's now, not the mesh's.** It was written here while the mesh
|
||||
// wrote the whole file. Written into a region, a `localhost` or a `127.0.1.1 homer` of the mesh's
|
||||
// would stand beside the distribution's own — a second answer nobody could say the owner of, and
|
||||
// one that goes when the mesh leaves. A header claiming the file would be a lie about the rest of
|
||||
// it. And no line may look like the host's own markers, or the host would refuse the region.
|
||||
func TestTheNamesAreOnlyTheMeshsRegionOfTheFile(t *testing.T) {
|
||||
out := nodeNames(Resolution{Node: "homer"}, threeMachines, "")
|
||||
want := "# The mesh's names. This region is replaced whenever a machine joins or leaves.\n" +
|
||||
"10.42.0.1\thomer.internal\thomer\t# this machine\n" +
|
||||
"10.42.0.2\tmarge.internal\tmarge\n"
|
||||
if out != want {
|
||||
t.Fatalf("the region is not exactly the mesh's names:\n%s\n--- want ---\n%s", out, want)
|
||||
}
|
||||
for _, floor := range []string{"localhost", "127.0.", "::1", "Generated by the mesh", "# BEGIN mesh ", "# END mesh "} {
|
||||
if strings.Contains(out, floor) {
|
||||
t.Fatalf("the region carries %q, which is not the mesh's to write:\n%s", floor, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hosts file is written into; the resolver's zones are written whole. A property of each fact,
|
||||
// so a module asking for node-names at another path still does not get a file of the mesh's.
|
||||
func TestOnlyTheNamesAreWrittenIntoASharedFile(t *testing.T) {
|
||||
m := Manifest{Module: "resolver", Facts: map[string]string{
|
||||
FactNodeZones: "/etc/mesh-resolver/nodes.conf", FactNodeNames: "/etc/hosts",
|
||||
}}
|
||||
given, err := FactsInto(m, Resolution{Node: "homer"}, threeMachines, threeMachines, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(given) != 2 {
|
||||
t.Fatalf("expected two files, got %v", given)
|
||||
}
|
||||
for _, f := range given {
|
||||
switch f["path"] {
|
||||
case "/etc/hosts":
|
||||
if f["into"] != "block" {
|
||||
t.Errorf("the hosts file is written over rather than into: %v", f)
|
||||
}
|
||||
case "/etc/mesh-resolver/nodes.conf":
|
||||
// The mesh owns the resolver's zones; nothing else writes there.
|
||||
if into, set := f["into"]; set {
|
||||
t.Errorf("the zones file is written into (%v), and it is the mesh's whole", into)
|
||||
}
|
||||
if !strings.HasPrefix(f["content"].(string), "# Generated by the mesh.") {
|
||||
t.Errorf("the zones file lost its header: %v", f["content"])
|
||||
}
|
||||
default:
|
||||
t.Errorf("a file nobody asked for: %v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package catalogue_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/overlay"
|
||||
)
|
||||
|
||||
// novox/hq issue 128, held where the host will see it: the shipped networking module's names,
|
||||
// through the whole composition, and not only through FactsInto.
|
||||
//
|
||||
// Composition prefixes a fact's id with the module that asked for it and passes everything else
|
||||
// through; a step that dropped `into` on the way would send the region as a whole file, and the
|
||||
// host would write the machine's hosts file over again with every unit test above still green.
|
||||
|
||||
// onTheNetwork stands in for the overlay's generator: the node is part of the private network,
|
||||
// and what the generator writes is not what is under test here.
|
||||
type onTheNetwork struct{}
|
||||
|
||||
func (onTheNetwork) Resources(string) ([]map[string]any, bool, error) {
|
||||
return []map[string]any{{"id": "overlay-config", "type": "file",
|
||||
"path": "/etc/wireguard/mesh0.conf", "mode": "0600", "content": "[Interface]\n"}}, true, nil
|
||||
}
|
||||
|
||||
func TestTheHostsRegionArrivesAsTheHostWillReadIt(t *testing.T) {
|
||||
shelf := provided(t)
|
||||
// A resolver restarting on the names another module put on the machine, and one resource it
|
||||
// only runs at start — neither of which composition has any business changing.
|
||||
resolver, err := catalogue.ParseManifest([]byte(`{
|
||||
"module": "resolver", "version": "1", "requires": ["mesh-addressing"],
|
||||
"resources": [
|
||||
{"id": "seed", "type": "file", "path": "/etc/resolver/seed", "mode": "0644",
|
||||
"content": "seed\n", "at": "start"},
|
||||
{"id": "daemon", "type": "service", "unit": "resolver.service", "state": "running",
|
||||
"restart-on": ["seed", "mesh-wireguard.fact-node-names"]}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shelf[resolver.Module] = resolver
|
||||
|
||||
got, err := catalogue.Resolve(shelf, []string{overlay.Domain, "resolver"},
|
||||
catalogue.Node{Name: "homer", At: "homer.internal"}, catalogue.World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names := map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2"}
|
||||
out, err := got.Declaration(catalogue.Rendering{
|
||||
Names: names, Machines: names, Suffix: "internal",
|
||||
Generators: map[string]catalogue.Generator{overlay.Name: onTheNetwork{}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := map[string]map[string]any{}
|
||||
for _, r := range out {
|
||||
ids[r["id"].(string)] = r
|
||||
}
|
||||
|
||||
hosts := ids[overlay.Name+".fact-node-names"]
|
||||
if hosts == nil {
|
||||
t.Fatalf("no names reached the machine; the declaration has %v", keys(ids))
|
||||
}
|
||||
if hosts["path"] != "/etc/hosts" || hosts["into"] != "block" {
|
||||
t.Fatalf("the hosts file is not written into as a region: %v", hosts)
|
||||
}
|
||||
content := hosts["content"].(string)
|
||||
if !strings.Contains(content, "10.42.0.1\thomer.internal\thomer\t# this machine\n") {
|
||||
t.Errorf("the region does not name the machine:\n%s", content)
|
||||
}
|
||||
for _, floor := range []string{"Generated by the mesh", "localhost", "127.0.1.1"} {
|
||||
if strings.Contains(content, floor) {
|
||||
t.Errorf("the region carries %q, which is the machine's:\n%s", floor, content)
|
||||
}
|
||||
}
|
||||
|
||||
// The resolver's reference to it still names a resource the host will be sent.
|
||||
daemon := ids["resolver.daemon"]
|
||||
if daemon == nil {
|
||||
t.Fatalf("the resolver's service was not composed: %v", keys(ids))
|
||||
}
|
||||
for _, named := range daemon["restart-on"].([]any) {
|
||||
if ids[named.(string)] == nil {
|
||||
t.Errorf("the resolver restarts on %v, which is nothing the host is sent", named)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(daemon["restart-on"], []any{"resolver.seed", overlay.Name + ".fact-node-names"}) {
|
||||
t.Errorf("restart-on is %v", daemon["restart-on"])
|
||||
}
|
||||
|
||||
// And a resource's own `at` passes through as the manifest wrote it.
|
||||
if seed := ids["resolver.seed"]; seed == nil || seed["at"] != "start" {
|
||||
t.Errorf("a resource's at did not survive composition: %v", seed)
|
||||
}
|
||||
}
|
||||
|
||||
func keys(m map[string]map[string]any) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -47,6 +47,13 @@ var seats = []Seat{
|
||||
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||
{Name: "the-resolver-configuration", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||
{Name: "the-showcase", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||
// The program that manages the machine's own network. It delivers nothing: its holder only
|
||||
// keeps the manager and the mesh from contradicting each other — the resolver file left to the
|
||||
// mesh, the private network's interface left alone — and never declares a link, an address or
|
||||
// a wireless network, because the link is the only channel a fix could arrive on. A seat
|
||||
// rather than a condition in the resolver's module, so a machine running two managers is
|
||||
// refused at assignment instead of found by the resolver being rewritten (novox/hq ADR 0117).
|
||||
{Name: "the-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117"},
|
||||
}
|
||||
|
||||
// Seats is every seat the mesh defines, in reading order.
|
||||
|
||||
@@ -44,12 +44,32 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
delivered[s.Delivers] = s.Name
|
||||
}
|
||||
}
|
||||
if len(Seats()) != 14 {
|
||||
t.Errorf("the mesh defines %d seats rather than 14; the set is closed, so a change here is "+
|
||||
if len(Seats()) != 15 {
|
||||
t.Errorf("the mesh defines %d seats rather than 15; the set is closed, so a change here is "+
|
||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0117: a machine's uplink is a seat, held per machine, and delivers nothing.
|
||||
//
|
||||
// **Nothing, because nothing may be required of it.** A holder only keeps its network manager from
|
||||
// contradicting the mesh; a requirement resolving to it would make the manager the mesh's answer
|
||||
// for something, and the manager's link is the one thing the mesh must never be able to break.
|
||||
func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
|
||||
seat, known := SeatNamed("the-uplink")
|
||||
if !known {
|
||||
t.Fatalf("the uplink is not a seat; the seats are: %s", seatNames())
|
||||
}
|
||||
if seat.Scope != ScopeNode || seat.Delivers != "" || seat.Decision != "novox/hq ADR 0117" {
|
||||
t.Fatalf("the uplink is %+v, not a node seat delivering nothing by ADR 0117", seat)
|
||||
}
|
||||
// And a manager's module can hold it without providing anything.
|
||||
raw := []byte(`{"module":"networkmanager","version":"1","claims":[{"name":"the-uplink","scope":"node"}]}`)
|
||||
if _, err := ParseManifest(raw); err != nil {
|
||||
t.Fatalf("a network manager's module could not hold the uplink: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func claimed(claims string) []byte {
|
||||
return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh"}],"claims":` + claims + `}`)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ const DefaultSuffix = "internal"
|
||||
//
|
||||
// This is not the `/etc/hosts` floor the design removes. That floor existed because a node had to
|
||||
// reach the mesh's database before its own DNS worked — a fallback for a circularity, and the
|
||||
// circularity is gone. This is the mechanism itself: the complete set of names in this mesh,
|
||||
// generated whole and owned by the mesh (novox/hq ADR 0011), rather than a patch written
|
||||
// underneath something else.
|
||||
// circularity is gone. This is the mechanism itself: the complete set of names in this mesh
|
||||
// (novox/hq ADR 0011). Written as a marked region *into* the file rather than as the file: the
|
||||
// rest of it — `localhost`, the machine's own name, other tools' blocks — is the machine's, and
|
||||
// writing it whole replaced all of that (novox/hq issue 128).
|
||||
//
|
||||
// A file rather than a resolver daemon, deliberately, for now: it works on every Linux, needs no
|
||||
// package, and has no failure mode of its own. A daemon becomes necessary when names are wanted
|
||||
|
||||
Reference in New Issue
Block a user