Rename hosts to hostname, which also writes /etc/hostname (hq ADR 0223)
Two files say one fact, the machine's name, and nothing owned /etc/hostname. The name written is the operator's hostname setting, with no default: three of four machines call themselves something other than their mesh name, and renaming one is the operator's call. It takes effect at the next boot.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# hostname
|
||||
|
||||
The machine's names (novox/hq ADR 0199, ADR 0223): it holds the node seat `node-hostname` and owns
|
||||
the two files that say what a machine is called — `/etc/hostname` and the machine's own lines in
|
||||
`/etc/hosts`. It was `hosts`, holding `node-hosts-file`; the seat was renamed, and the old name
|
||||
resolves to it as an alias.
|
||||
|
||||
## What it writes
|
||||
|
||||
- **`/etc/hostname`, whole**: the module's `hostname` setting, and nothing else. There is no
|
||||
default. A machine's name is the operator's: the mesh's name for a machine and the name it calls
|
||||
itself need not be the same, and writing the mesh's name silently would rename a machine. Set it
|
||||
per machine — `settings set hostname '{"hostname": "<name>"}' --node <node>` — before the module
|
||||
is assigned there; without it the module is left out of that machine's declaration, naming the
|
||||
key. A mesh-wide `{"hostname": "${machine:name}"}` makes every machine call itself by its mesh
|
||||
name, and a machine's own setting still overrides it.
|
||||
- **The machine's own lines in `/etc/hosts`**, as the mesh's marked region at the start of the file:
|
||||
`localhost` and `127.0.1.1` with the machine's mesh name. Every other line is the operator's, kept
|
||||
byte for byte and given back when the module goes.
|
||||
|
||||
## When a new name takes effect
|
||||
|
||||
At the machine's **next boot**. The kernel's name is set from `/etc/hostname` when the machine
|
||||
starts; writing the file changes what `hostnamectl` reports as the static name and nothing that is
|
||||
running. The module declares nothing that would set it live: a graphical session's X authority is
|
||||
keyed by the name the session started under, so changing it underneath a running session refuses
|
||||
every new window until the person logs in again. Reboot when that is acceptable, or run
|
||||
`hostnamectl hostname <name>` by hand.
|
||||
|
||||
## Its verbs
|
||||
|
||||
`<node>/node-hostname.entries`, `.add` and `.remove`: the hosts file's lines, each marked whose it
|
||||
is; add one address and its names to the operator's lines; remove a name or an address from them.
|
||||
They change the machine's file and nothing else. `/etc/hostname` has no verb — it is the setting.
|
||||
+2
-2
@@ -72,7 +72,7 @@ func execRunner(ctx context.Context, name string, args ...string) (string, error
|
||||
type Line struct {
|
||||
// Text is the line exactly as it is in the file.
|
||||
Text string `json:"text"`
|
||||
// Owner is whose it is: the block's id (`mesh hosts.own`, or another tool's) or "operator".
|
||||
// Owner is whose it is: the block's id (`mesh hostname.own`, or another tool's) or "operator".
|
||||
Owner string `json:"owner"`
|
||||
// Address and Names are an entry's; absent for a comment or a blank line.
|
||||
Address string `json:"address,omitempty"`
|
||||
@@ -319,7 +319,7 @@ func (h HostsFile) write(ctx context.Context, content string) error {
|
||||
if err := os.WriteFile(staged, []byte(content), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
beside := filepath.Join(filepath.Dir(h.Path), "."+filepath.Base(h.Path)+".hosts-tools")
|
||||
beside := filepath.Join(filepath.Dir(h.Path), "."+filepath.Base(h.Path)+".hostname-tools")
|
||||
if _, err := h.Run(ctx, "install", "-m", "0644", staged, beside); err != nil {
|
||||
return err
|
||||
}
|
||||
+33
-6
@@ -17,10 +17,10 @@ import (
|
||||
const file = "# Static table lookup for hostnames.\n" +
|
||||
"127.0.0.1\tlocaldev.example.com\n" +
|
||||
"127.0.0.1 a.example.com b.example.com\n" +
|
||||
"# BEGIN mesh hosts.own\n" +
|
||||
"# BEGIN mesh hostname.own\n" +
|
||||
"127.0.0.1\tlocalhost\n" +
|
||||
"::1\tlocalhost\n" +
|
||||
"# END mesh hosts.own\n" +
|
||||
"# END mesh hostname.own\n" +
|
||||
"# BEGIN other-tool\n" +
|
||||
"192.0.2.7\tproject.test\n" +
|
||||
"# END other-tool\n"
|
||||
@@ -47,7 +47,7 @@ func TestEveryLineSaysWhoseItIs(t *testing.T) {
|
||||
if lines[0].Address != "" || lines[0].Owner != Operator {
|
||||
t.Errorf("a comment is the operator's and no entry: %+v", lines[0])
|
||||
}
|
||||
if lines[3].Owner != "mesh hosts.own" || lines[4].Owner != "mesh hosts.own" || lines[6].Owner != "mesh hosts.own" {
|
||||
if lines[3].Owner != "mesh hostname.own" || lines[4].Owner != "mesh hostname.own" || lines[6].Owner != "mesh hostname.own" {
|
||||
t.Errorf("the mesh's block, its markers included: %+v", lines[3:7])
|
||||
}
|
||||
if lines[8].Owner != "other-tool" || !reflect.DeepEqual(lines[8].Names, []string{"project.test"}) {
|
||||
@@ -146,7 +146,7 @@ func TestRemoveTakesOneNameOrOneAddressAndBlocksStayByteForByte(t *testing.T) {
|
||||
if err != nil || n != 2 {
|
||||
t.Fatalf("%d %v", n, err)
|
||||
}
|
||||
if !strings.Contains(all, "# BEGIN mesh hosts.own\n127.0.0.1\tlocalhost\n") {
|
||||
if !strings.Contains(all, "# BEGIN mesh hostname.own\n127.0.0.1\tlocalhost\n") {
|
||||
t.Errorf("the mesh's own localhost is not the operator's to remove: %q", all)
|
||||
}
|
||||
if !reflect.DeepEqual(blocks(all), blocks(file)) {
|
||||
@@ -158,7 +158,7 @@ func TestRemoveRefusesANameOnlyABlockWritesNamingWhose(t *testing.T) {
|
||||
if _, _, err := WithRemoved(file, "project.test"); err == nil || !strings.Contains(err.Error(), "written by other-tool") {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if _, _, err := WithRemoved(file, "::1"); err == nil || !strings.Contains(err.Error(), "written by mesh hosts.own") {
|
||||
if _, _, err := WithRemoved(file, "::1"); err == nil || !strings.Contains(err.Error(), "written by mesh hostname.own") {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if _, n, err := WithRemoved(file, "nowhere.test"); err != nil || n != 0 {
|
||||
@@ -200,7 +200,7 @@ func TestTheVerbsWriteTheFileWholeBesideItAndRenameItOver(t *testing.T) {
|
||||
if err != nil || !added.Added || added.Line != "192.0.2.9\tlab.test" {
|
||||
t.Fatalf("%+v %v", added, err)
|
||||
}
|
||||
beside := filepath.Join(dir, ".hosts.hosts-tools")
|
||||
beside := filepath.Join(dir, ".hosts.hostname-tools")
|
||||
if len(calls) != 2 || calls[0][0] != "install" || calls[0][len(calls[0])-1] != beside ||
|
||||
!reflect.DeepEqual(calls[1], []string{"mv", "-f", beside, path}) {
|
||||
t.Errorf("%v", calls)
|
||||
@@ -284,3 +284,30 @@ func TestNamesAreSplitOnSpacesAndCommasOrTakenAsAList(t *testing.T) {
|
||||
t.Errorf("%v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// /etc/hostname is the module's whole file (novox/hq ADR 0223), and what it says is the operator's
|
||||
// `hostname` setting — never the mesh's name for the machine written silently: on the mesh this was
|
||||
// built for, three of four machines call themselves something else, and renaming a machine is the
|
||||
// operator's to decide.
|
||||
func TestTheMachinesNameIsTheOperatorsSetting(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Resources []map[string]any `json:"resources"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if r["path"] != "/etc/hostname" {
|
||||
continue
|
||||
}
|
||||
if r["id"] != "name" || r["into"] != nil || r["content"] != "${setting:hostname}\n" {
|
||||
t.Errorf("/etc/hostname is not written whole from the hostname setting: %v", r)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Error("the module does not write /etc/hostname")
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// hosts-tools (novox/hq ADR 0199): the hosts file's tools. One binary, launched by the machine's tool
|
||||
// runtime and speaking MCP to it over stdio through the Go SDK (ADR 0193, ADR 0198): the
|
||||
// node-hosts-file seat's three verbs — the file's lines with whose each is, add an operator's line,
|
||||
// remove one. They change the machine's file and nothing else; the controller holds none of it.
|
||||
// hostname-tools (novox/hq ADR 0199, ADR 0223): the tools of the machine's names. One binary,
|
||||
// launched by the machine's tool runtime and speaking MCP to it over stdio through the Go SDK (ADR
|
||||
// 0193, ADR 0198): the node-hostname seat's three verbs — the hosts file's lines with whose each
|
||||
// is, add an operator's line, remove one. They change the machine's file and nothing else; the
|
||||
// controller holds none of it. /etc/hostname has no verb: it is the module's resource, set by the
|
||||
// module's `hostname` setting.
|
||||
//
|
||||
// stdout is the MCP channel; everything this module says, it says on stderr.
|
||||
package main
|
||||
@@ -17,11 +19,11 @@ import (
|
||||
)
|
||||
|
||||
// Seat is the role this module holds.
|
||||
const Seat = "node-hosts-file"
|
||||
const Seat = "node-hostname"
|
||||
|
||||
func main() {
|
||||
if err := stdio.Serve("", tools(HostsFile{Path: HostsPath, Run: execRunner})); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[hosts] %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "[hostname] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +63,7 @@ func namesArg(a map[string]any) []string {
|
||||
}
|
||||
|
||||
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's
|
||||
// subject, as <node>/node-hosts-file.<verb>.
|
||||
// subject, as <node>/node-hostname.<verb>.
|
||||
func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool {
|
||||
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module hosts
|
||||
module hostname
|
||||
|
||||
go 1.25.0
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"module": "hostname",
|
||||
"version": "1",
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-hostname",
|
||||
"scope": "node",
|
||||
"serves": [
|
||||
"entries",
|
||||
"add",
|
||||
"remove"
|
||||
]
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "own",
|
||||
"type": "file",
|
||||
"path": "/etc/hosts",
|
||||
"mode": "0644",
|
||||
"into": "block",
|
||||
"at": "start",
|
||||
"content": "# The machine's own names (module hostname, novox/hq ADR 0199, ADR 0223). Every line outside this\n# block is the operator's: kept across every push, changed through the node-hostname verbs add and\n# remove, and given back when this module goes. The mesh's names are not here: the mesh's resolver\n# answers them.\n127.0.0.1\tlocalhost\n::1\tlocalhost\n127.0.1.1\t${machine:name}\n"
|
||||
},
|
||||
{
|
||||
"id": "name",
|
||||
"type": "file",
|
||||
"path": "/etc/hostname",
|
||||
"mode": "0644",
|
||||
"content": "${setting:hostname}\n"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "tools",
|
||||
"kind": "bundle",
|
||||
"language": "go",
|
||||
"system": "arch",
|
||||
"from": "cmd/hostname-tools",
|
||||
"binary": "hostname-tools",
|
||||
"loads": [
|
||||
"hostname-tools"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"module": "hosts",
|
||||
"version": "1",
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-hosts-file",
|
||||
"scope": "node",
|
||||
"serves": [
|
||||
"entries",
|
||||
"add",
|
||||
"remove"
|
||||
]
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "own",
|
||||
"type": "file",
|
||||
"path": "/etc/hosts",
|
||||
"mode": "0644",
|
||||
"into": "block",
|
||||
"at": "start",
|
||||
"content": "# The machine's own names (module hosts, novox/hq ADR 0199). Every line outside this block is the\n# operator's: kept across every push, changed through the node-hosts-file verbs add and remove, and\n# given back when this module goes. The mesh's names are not here: the mesh's resolver answers them.\n127.0.0.1\tlocalhost\n::1\tlocalhost\n127.0.1.1\t${machine:name}\n"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "tools",
|
||||
"kind": "bundle",
|
||||
"language": "go",
|
||||
"system": "arch",
|
||||
"from": "cmd/hosts-tools",
|
||||
"binary": "hosts-tools",
|
||||
"loads": [
|
||||
"hosts-tools"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user