package main import ( "context" "encoding/base64" "encoding/json" "errors" "flag" "fmt" "io" "net" "os" "path/filepath" "strings" "time" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/token" ) // what a machine is, and what it is allowed to be told. // // Split out of main.go, which had reached 2,769 lines because appending was always the // cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636: // nothing in it was wrong, and no one edit was the one that should have been a new file. func nodeCommand(ctx context.Context, args []string) error { if len(args) == 0 { return errors.New("node add , node list, node show , " + publicDomainUsage + ", or " + handOverUsage) } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory switch args[0] { case "show": if len(args) != 2 { return errors.New("node show ") } return showNode(ctx, open, args[1]) case "add": return addNode(ctx, inv, args[1:]) case "list": nodes, err := inv.Nodes(ctx) if err != nil { return err } // **The same list, for something other than a person** — the console's discovery reads it // (novox/hq ADR 0195), and a reader that parses a printed column breaks when it is reworded. if len(args) > 1 && args[1] == "--json" { type listed struct { Name string `json:"name"` Heard string `json:"heard"` Mode string `json:"mode"` ID string `json:"id"` } out := make([]listed, 0, len(nodes)) for _, n := range nodes { out = append(out, listed{Name: n.Name, Heard: heardFrom(n), Mode: modeOf(n), ID: n.ID}) } return printJSON(out) } if len(nodes) == 0 { // Said rather than printed as nothing: an empty list and a failed read must never // look the same, and this command answering "none" is only honest because getting // here means the store answered. fmt.Println("this mesh has no node records yet") return nil } for _, n := range nodes { fmt.Printf("%-20s %-14s %-9s %s\n", n.Name, heardFrom(n), modeOf(n), n.ID) } return nil case "public-domain": // The domain this node composes its routed names under (novox/hq ADR 0066). // // **The form with no argument reports; clearing is asked for by name.** It used to clear — // so `node public-domain anchor`, which reads like a question and is what anybody types to // find out what the answer is, silently took every routed name the node had. A read-shaped // invocation must never be a destructive write: there is no output that makes up for it, // because the damage is already done by the time it prints. return publicDomain(ctx, inv, args[1:]) case "networks": // Removed by novox/hq ADR 0140, which superseded the record that added it. The filter no // longer names any network: it constrains what arrives from outside the machine and says // nothing about what did not, so there is no list to keep. Answered rather than met with // "unknown command", because this was the documented way to stop a flip cutting a machine's // containers off and somebody will reasonably still type it. return errors.New("`node networks` is gone (novox/hq ADR 0140). The filter constrains what " + "arrives from outside this machine and says nothing about traffic that did not, so no " + "network is named anywhere and nothing needs to be said to keep a machine's own " + "containers reaching outward. The machine reports which of its links face outside; see " + "`node show `") case "agent-account": // The account agents run as on this machine, when it is not the operator's (novox/hq ADR 0266). Here, // at the controller's terminal, and nowhere else: no verb and no setting names it, so no agent can // name itself another account. return nodeAgentAccount(ctx, inv, args[1:]) case "account": // The operator's login on this machine (novox/hq to-be 29): what a home-scoped file is // owned by and which account `ssh ` uses. Reports with no argument; sets with one; // an optional second argument is the home when it is not /home/. return nodeAccount(ctx, inv, args[1:]) case "hand-over": // A directory the node-engine uses as found, handed to the mesh (novox/hq issue 356, issue 339). Here, at // the controller's terminal, and nowhere else: at the next apply root gives the directory to the account // the module declares, and whoever may call a verb includes agents. return nodeHandOver(ctx, open, args[1:]) case "setuid-search": // A fresh search for setuid programs on a node (novox/hq issue 361), after the operator changed by hand // what the last one found. Here, at the controller's terminal, and nowhere else: a search never makes a // machine free wrongly, but asked again and again it would keep the machine unjudged and its disks busy, // and whoever may call a verb includes agents. return nodeSetuidSearch(ctx, open, args[1:]) case "judge-directories": // Which of a node's declared directories the walk that follows no link another account could make would // refuse (novox/hq issue 358). Read-only on the node and here, so a verb may run it too. return nodeJudgeDirectories(ctx, open, args[1:]) default: return fmt.Errorf("node has no %q; it has add, list, show, public-domain, account, agent-account, hand-over, "+ "setuid-search and judge-directories", args[0]) } } const handOverUsage = "node hand-over — hand a directory the node-engine on uses as found " + "to the mesh: its next apply gives it the declared owner and mode. The directory's absolute path, as the module's " + "condition names it" // handOverLine reads a hand-over's line: the node and the directory's absolute path, exactly as the engine states // it. Judged before anything is asked, and judged again by the engine, which is the one that acts. func handOverLine(args []string) (node, path string, err error) { if len(args) != 2 { return "", "", errors.New(handOverUsage) } node, path = args[0], args[1] if node == "" || strings.HasPrefix(node, "-") { return "", "", fmt.Errorf("%q is not a node's name; %s", node, handOverUsage) } if !filepath.IsAbs(path) { return "", "", fmt.Errorf("%q is not an absolute path; %s", path, handOverUsage) } if filepath.Clean(path) != path { return "", "", fmt.Errorf("%q is not the directory's path as the engine states it (no `..`, no doubled or "+ "trailing separator); %s", path, handOverUsage) } return node, path, nil } // handOverBy is who hands the directory over, in the words a verb's caller is recorded in: the operator through // mesh-cli on the control-node, or whoever runs this controller's binary at its terminal. func handOverBy() string { if by := strings.TrimSpace(os.Getenv(link.CallerVar)); by != "" { return by } return "the controller's terminal" } // nodeHandOver asks the node's engine to take a directory it uses as found as the mesh's, and says what came of // it. The engine records the hand-over or refuses; nothing is recorded here, because the directory is the // machine's and the engine is the one that reads it. The ask is signed with the mesh's key (issue 356's review). func nodeHandOver(ctx context.Context, open *stores, args []string) error { known := func(node string) error { _, err := open.inventory.NodeByName(ctx, node) return err } ask := func(node, path, by string) (link.HandOverAnswer, error) { ident, err := open.Identity(ctx) if err != nil { return link.HandOverAnswer{}, fmt.Errorf("the mesh's signing key cannot be read, so nothing was asked of %s: %w", node, err) } address, err := broker.BusAddress() if err != nil { return link.HandOverAnswer{}, err } js, err := broker.Dial(address) if err != nil { return link.HandOverAnswer{}, fmt.Errorf("cannot reach the bus, so nothing was asked of %s: %w", node, err) } defer js.Close() return link.AskHandOver(ctx, js.Conn(), ident, node, path, by, link.HandOverWithin) } return handOverAsked(args, known, ask, os.Stdout) } // handOverAsked is the hand-over's line with its two acts given: whether the mesh knows the node, and the ask. // Nothing is asked of a line or a node that is refused, and the engine's refusal is this command's failure — // never a success with the refusal printed. func handOverAsked(args []string, known func(node string) error, ask func(node, path, by string) (link.HandOverAnswer, error), out io.Writer) error { node, path, err := handOverLine(args) if err != nil { return err } if err := known(node); err != nil { return fmt.Errorf("nothing was asked: %w", err) } answer, err := ask(node, path, handOverBy()) if err != nil { return err } if answer.Refused != "" { return fmt.Errorf("%s refused: %s", node, answer.Refused) } fmt.Fprintln(out, answer.Said) fmt.Fprintf(out, " the module's condition clears once %s applies; `nox push %s` applies it now\n", node, node) return nil } const setuidSearchUsage = "node setuid-search — throw away the node-engine's last search for setuid " + "programs on and start a full one: after a setuid-root program it found was removed by hand. Until it " + "completes, root-free says the node is not judged yet" // nodeSetuidSearch asks the node's engine for a fresh search, signed with the mesh's key as a hand-over is. func nodeSetuidSearch(ctx context.Context, open *stores, args []string) error { known := func(node string) error { _, err := open.inventory.NodeByName(ctx, node) return err } ask := func(node, by string) (link.HandOverAnswer, error) { ident, err := open.Identity(ctx) if err != nil { return link.HandOverAnswer{}, fmt.Errorf("the mesh's signing key cannot be read, so nothing was asked of %s: %w", node, err) } address, err := broker.BusAddress() if err != nil { return link.HandOverAnswer{}, err } js, err := broker.Dial(address) if err != nil { return link.HandOverAnswer{}, fmt.Errorf("cannot reach the bus, so nothing was asked of %s: %w", node, err) } defer js.Close() return link.AskSetuidSearch(ctx, js.Conn(), ident, node, by, link.HandOverWithin) } return setuidSearchAsked(args, known, ask, os.Stdout) } // setuidSearchAsked is the line with its two acts given: whether the mesh knows the node, and the ask. The // engine's refusal is this command's failure. func setuidSearchAsked(args []string, known func(node string) error, ask func(node, by string) (link.HandOverAnswer, error), out io.Writer) error { if len(args) != 1 || args[0] == "" || strings.HasPrefix(args[0], "-") { return errors.New(setuidSearchUsage) } node := args[0] if err := known(node); err != nil { return fmt.Errorf("nothing was asked: %w", err) } answer, err := ask(node, handOverBy()) if err != nil { return err } if answer.Refused != "" { return fmt.Errorf("%s refused: %s", node, answer.Refused) } fmt.Fprintln(out, answer.Said) fmt.Fprintf(out, " the controller's root-free verb shows the search's progress until it completes\n") return nil } // addNode creates a node record, adopted when the operator says so (novox/hq ADR 0100). func addNode(ctx context.Context, inv *inventory.Inventory, args []string) error { set := flag.NewFlagSet("node add", flag.ContinueOnError) adopted := set.Bool("adopted", false, "the machine is in use: keep what is found on it until each module is taken") positionals, err := parseAround(set, args) if err != nil { return err } if len(positionals) != 1 { return errors.New("node add [--adopted]") } node, err := inv.AddNodeAs(ctx, positionals[0], *adopted) if err != nil { return err } fmt.Printf("added %s (%s)", node.Name, node.ID) if node.Adopted { fmt.Print(", adopted") } fmt.Println() return nil } // modeOf is a node's mode as a word (novox/hq ADR 0100): an adopted node is said to be adopted // wherever the mesh reports a node's state. func modeOf(n inventory.Node) string { if n.Adopted { return "adopted" } return "converged" } // publicDomainUsage is the one description of the three forms, so a refusal and the help agree. // nodeAccount reports or sets a node's operator account (novox/hq to-be 29). Read-shaped with no // argument, like public-domain: `node account novox` answers, it does not change anything. func nodeAccount(ctx context.Context, inv *inventory.Inventory, positionals []string) error { if len(positionals) == 0 || len(positionals) > 3 { return errors.New("node account — what it is now; " + "node account [home] — set it (home defaults to /home/)") } node := positionals[0] if len(positionals) == 1 { who, err := inv.NodeByName(ctx, node) if err != nil { return err } if who.Account == "" { fmt.Printf("%s has no operator account known\n", node) fmt.Printf(" `node account %s ` sets it\n", node) return nil } fmt.Printf("%s logs a person in as %s (home %s)\n", node, who.Account, who.Home()) return nil } home := "" if len(positionals) == 3 { home = positionals[2] } if err := inv.SetAccount(ctx, node, positionals[1], home); err != nil { return err } fmt.Printf("%s logs a person in as %s\n", node, positionals[1]) fmt.Printf(" run `push %s` once ssh-client is assigned, to send its operator config\n", node) return nil } const agentAccountUsage = "node agent-account — what it is now; " + " [home] to name the account agents run as (home defaults to /home/); " + " --clear to have them run as the operator account again" // nodeAgentAccount reports, names or clears the account agents run as on a node (novox/hq ADR 0266). Read- // shaped with no account, like public-domain; clearing is asked for by name. func nodeAgentAccount(ctx context.Context, inv *inventory.Inventory, args []string) error { set := flag.NewFlagSet("node agent-account", flag.ContinueOnError) clear := set.Bool("clear", false, "agents run as the operator account again") positionals, err := parseAround(set, args) if err != nil { return err } if len(positionals) == 0 || len(positionals) > 3 { return errors.New(agentAccountUsage) } node := positionals[0] switch { case *clear && len(positionals) > 1: return fmt.Errorf("name an agent account for %s or --clear, not both", node) case *clear: if err := inv.SetAgentAccount(ctx, node, "", ""); err != nil { return err } fmt.Printf("agents on %s run as the operator account again\n", node) fmt.Printf(" run `push %s` to send it; the agent account itself is kept (the mesh never deletes a login)\n", node) return nil case len(positionals) >= 2: home := "" if len(positionals) == 3 { home = positionals[2] } if err := inv.SetAgentAccount(ctx, node, positionals[1], home); err != nil { return err } fmt.Printf("agents on %s run as %s\n", node, positionals[1]) fmt.Printf(" run `push %s` to send it; the self-check says once its node-engine has judged it "+ "unable to become root\n", node) return nil default: n, err := inv.NodeByName(ctx, node) if err != nil { return err } for _, line := range agentAccountLines(ctx, inv, n) { fmt.Println(strings.TrimPrefix(line, " ")) } return nil } } const publicDomainUsage = "node public-domain — what it is now; " + " to set it; --clear to take it away" // publicDomain reads, sets or clears the domain a node composes its routed names under. // // Three forms, and the destructive one is the only one that has to be asked for. Clearing is a // real thing to want — a machine that stops facing the outside composes no names, and // lab-versus-production is this one setting (novox/hq ADR 0066) — so it keeps a way to say it. // What it does not keep is being the thing that happens when nothing was said at all. func publicDomain(ctx context.Context, inv *inventory.Inventory, args []string) error { set := flag.NewFlagSet("node public-domain", flag.ContinueOnError) clear := set.Bool("clear", false, "take the domain away; it composes no routed names after") positionals, err := parseAround(set, args) if err != nil { return err } if len(positionals) == 0 || len(positionals) > 2 { return errors.New(publicDomainUsage) } node := positionals[0] switch { case *clear && len(positionals) == 2: // Both, which cannot be meant. Refused rather than one of them silently winning. return fmt.Errorf("give %s a domain or --clear, not both: %q and --clear say opposite "+ "things and the mesh will not choose between them", node, positionals[1]) case *clear: if err := inv.SetPublicDomain(ctx, node, ""); err != nil { return err } fmt.Printf("%s has no public domain, so it composes no routed names\n", node) fmt.Printf(" run `push %s` to take them off it\n", node) return nil case len(positionals) == 2: if err := inv.SetPublicDomain(ctx, node, positionals[1]); err != nil { return err } fmt.Printf("%s composes its routed names under %s\n", node, positionals[1]) fmt.Printf(" run `push %s` to send it\n", node) return nil default: // Asked, so answered. NodeByName first, so a name the mesh has never heard of is a refusal // rather than "it has no public domain", which is true of that name and says nothing. if _, err := inv.NodeByName(ctx, node); err != nil { return err } domain, err := inv.PublicDomainOf(ctx, node) if err != nil { return err } if domain == "" { fmt.Printf("%s has no public domain, so it composes no routed names\n", node) fmt.Printf(" `node public-domain %s ` gives it one\n", node) return nil } fmt.Printf("%s composes its routed names under %s\n", node, domain) return nil } } func tokenCommand(ctx context.Context, args []string) error { if len(args) == 0 || args[0] != "issue" { return errors.New("token issue --node , or token issue --new ") } set := flag.NewFlagSet("token issue", flag.ContinueOnError) existing := set.String("node", "", "issue for a node record that already exists") fresh := set.String("new", "", "create the node record, then issue for it") validFor := set.Duration("for", time.Hour, "how long the token may be used") adopted := set.Bool("adopted", false, "the machine joining is in use: it is adopted, and keeps what is found on it") tunnelKey := set.String("overlay-key", "", "the public half of the tunnel key the machine made (`nox-mesh-host key`): it joins through the tunnel") if err := set.Parse(args[1:]); err != nil { return err } // Exactly one, because the difference is what the token binds to. A command that guessed // would sometimes create a second record for a machine that already has one. if (*existing == "") == (*fresh == "") { return errors.New("give exactly one of --node or --new : the first is a " + "machine the mesh already has a record for, the second is one it has never seen") } open, err := openStores(ctx) if err != nil { return err } defer open.Close() inv := open.inventory issued, err := issueFor(ctx, inv, *existing, *fresh, *adopted, *validFor) if err != nil { return err } // Assembled from two contexts by the process that holds both grants. Neither reads the // other's store (novox/hq ADR 0008) — each is asked for its own part. ident, err := openIdentity(ctx) if err != nil { return err } defer ident.Close() key, err := ident.Establish(ctx) if err != nil { return err } // The account is created before the token is handed over, which is what removes the // chicken-and-egg entirely: the mesh runs the broker, so a joining node's credentials can // exist before it does. The one-time secret IS the password, so a node's first connection is // already authenticated and enrolment is what happens over it. made := token.Token{Node: issued.Node.Name, Signer: key.Public, Secret: issued.Secret, Adopted: issued.Node.Adopted} // Absent is a state, not a failure: a control plane can hold records and a key before it has // a broker. What it cannot do is issue a token anybody could use, and Missing() says so. known, err := broker.FromEnvironment() switch { case err == nil: made.Broker, made.Fingerprint = known.Address, known.Fingerprint case errors.Is(err, broker.ErrNotConfigured): default: return err } // **Through the tunnel** (novox/hq ADR 0169): the machine's key recorded, its address given, the // hub sent it as a peer — all before the token is shown, so the tunnel answers the first time the // machine knocks. The bus is then reached at its address on the private network. if *tunnelKey != "" { if made.Tunnel, made.Broker, err = throughTheTunnel(ctx, open, issued.Node, *tunnelKey, made.Broker); err != nil { return err } } encoded, err := made.Encode() if err != nil { return err } joins := "" if made.Adopted { joins = ", joining adopted" } fmt.Printf("token for %s%s, usable once, until %s\n\n %s\n\n", issued.Node.Name, joins, issued.Expires.Format(time.RFC3339), encoded) fmt.Println("This is the only time it is shown. What is stored is a hash of the secret.") if missing := made.Missing(); len(missing) > 0 { fmt.Printf("\nINCOMPLETE — this token cannot be used to join anything yet. Missing:\n") for _, m := range missing { fmt.Printf(" - %s\n", m) } fmt.Printf("\nSet %s and %s once the broker is raised.\n", broker.AddressVar, broker.CertificateVar) } return nil } // throughTheTunnel makes a machine a peer of the hub for its token, and says what the token carries // for it: its first tunnel, and the bus at its address on the private network (novox/hq ADR 0169). // // The hub is pushed here, before the token is shown. A token shown before the hub knew the key is a // tunnel that does not answer, and a machine that cannot tell that from a bus that is down. func throughTheTunnel(ctx context.Context, open *stores, node inventory.Node, key, busAt string) ( *token.Tunnel, string, error) { inv := open.inventory key = strings.TrimSpace(key) if raw, err := base64.StdEncoding.DecodeString(key); err != nil || len(raw) != 32 { return nil, "", fmt.Errorf("%q is not a tunnel public key: it is 32 bytes in base64, as "+ "`nox-mesh-host key` prints it", key) } // The bus on the private network is its holder's address at the bus's own port, so the port must // be known before anything is recorded. _, port, err := net.SplitHostPort(busAt) if err != nil || port == "" { return nil, "", fmt.Errorf("the bus's address %q has no port to reach it on", busAt) } places, err := inv.Overlays(ctx) if err != nil { return nil, "", err } var hub *inventory.Overlay for i := range places { if places[i].Hub { hub = &places[i] } } if hub == nil || hub.Key == "" || hub.Endpoint == "" || hub.Address == "" { return nil, "", errors.New("this mesh has no hub with a key, an address and an endpoint to " + "dial, so there is no tunnel to join through: place one (`overlay place --hub " + "--endpoint :`), or issue the token without --overlay-key") } if err := inv.RecordOverlayKey(ctx, node.ID, key); err != nil { return nil, "", err } if err := inv.BindTokenToKey(ctx, node.ID, key); err != nil { return nil, "", err } cidr, err := overlayRange(ctx, inv) if err != nil { return nil, "", err } address, err := inv.AssignAddress(ctx, node.ID, cidr) if err != nil { return nil, "", err } // The bus at its holder's address on the private network, reached through the hub like the rest // of the range; the hub's own when no machine is recorded as holding it yet. busAddress := hub.Address if holders, err := seatHolders(ctx, inv); err != nil { return nil, "", err } else if h, held := holders[catalogue.BrokerSeat]; held { for _, p := range places { if p.Name == h.Node && p.Address != "" { busAddress = p.Address } } } if err := sendTo(ctx, open, []string{hub.Name}); err != nil { return nil, "", fmt.Errorf("%s was made a peer of the hub, and the hub could not be sent "+ "it, so the tunnel would not answer — the token is not shown; issue it again once %s "+ "can be pushed: %w", node.Name, hub.Name, err) } // An address, not a name — nothing resolves before the machine has joined (novox/hq ADR 0004). return &token.Tunnel{ Key: key, Address: address + "/32", Range: cidr, HubKey: hub.Key, HubEndpoint: hub.Endpoint, }, net.JoinHostPort(busAddress, port), nil } // issueFor is the inventory's half of issuing a token: the record, made when it is new, adopted // when the operator says so, and the one-time secret for it. The node in what it returns carries // its mode, which is what the token says. func issueFor(ctx context.Context, inv *inventory.Inventory, existing, fresh string, adopted bool, validFor time.Duration) (inventory.Issued, error) { name := existing if fresh != "" { node, err := inv.AddNodeAs(ctx, fresh, adopted) if err != nil { return inventory.Issued{}, err } name = node.Name } // Not saying adopted leaves the node as it is: re-issuing a token for an adopted node does not // converge it — converging is its own act, previewed (novox/hq ADR 0100). And saying it for a // node already converged is refused rather than done quietly: returning a node to adopted is // its own act too, which unloads the mesh's filter and enables the found firewall again. if adopted && fresh == "" { node, err := inv.NodeByName(ctx, name) if err != nil { return inventory.Issued{}, err } if !node.Adopted { return inventory.Issued{}, fmt.Errorf("%s is converged, and a token does not change "+ "that: run `adopt %s` to return it to adopted, then issue the token without "+ "--adopted", name, name) } } return inv.IssueToken(ctx, name, validFor) } func identityCommand(ctx context.Context, args []string) error { if len(args) == 0 || args[0] != "show" { return errors.New("identity show") } ident, err := openIdentity(ctx) if err != nil { return err } defer ident.Close() // Establish rather than read: a control plane asked for its identity before it has one should // get one, not an error. Generating it is idempotent, so this is safe to run at any time. key, err := ident.Establish(ctx) if err != nil { return err } fmt.Printf("signing key %s\n", key.ID) fmt.Printf("fingerprint %s\n", key.Fingerprint()) fmt.Printf("created %s\n", key.Created.Format(time.RFC3339)) fmt.Printf("\nThe public half of this travels in every enrolment token. A node believes a\n" + "declaration because it carries a signature this key made (novox/hq ADR 0004).\n") return nil } func brokerCommand(ctx context.Context, args []string) error { if len(args) > 0 && args[0] == "certificate" { return busCertificate(args[1:]) } if len(args) > 0 && args[0] == "accounts" { return busAccounts(ctx, args[1:]) } if len(args) > 0 && args[0] == "consumer-reset" { return consumerReset(ctx, args[1:]) } if len(args) == 0 || args[0] != "show" { return errors.New("broker show | broker certificate [--check] --into | broker accounts --into | " + "broker consumer-reset ") } known, err := broker.FromEnvironment() if errors.Is(err, broker.ErrNotConfigured) { fmt.Printf("no broker configured. Set %s and %s.\n\n"+ "Until then tokens carry the signing key and the one-time secret, and say what they\n"+ "are missing. They cannot be used to join.\n", broker.AddressVar, broker.CertificateVar) return nil } if err != nil { return err } fmt.Printf("address %s\n", known.Address) fmt.Printf("fingerprint %s\n", known.Fingerprint) fmt.Print("\nThe fingerprint is computed from the certificate on disk, never configured. A\n" + "node checks it before sending anything (novox/hq ADR 0004).\n") return nil } // consumerReset re-makes one consumer on a stream that keeps history to start from now (novox/hq issue // 248): the way out of a consumer replaying a week of announcements, said rather than done by hand. A // person's act — what was pending is dropped — so it is a command, and nothing calls it on its own. func consumerReset(ctx context.Context, args []string) error { set := flag.NewFlagSet("broker consumer-reset", flag.ContinueOnError) // A repair by hand, which says why (novox/hq to-be 45 §7). why := addHandActFlags(set) args, err := parseAround(set, args) if err != nil { return err } if len(args) != 2 { return errors.New("broker consumer-reset --why , e.g. broker consumer-reset EVENTS controller --why ...") } if err := why.require("broker consumer-reset"); err != nil { return err } why.record(ctx, "broker consumer-reset", args) address, err := broker.BusAddress() if err != nil { return err } js, err := broker.Dial(address) if err != nil { return fmt.Errorf("cannot reach the bus: %w", err) } defer js.Close() before, after, err := js.ResetConsumer(args[0], args[1]) if err != nil { return err } fmt.Printf("consumer %s on %s re-made to deliver from now\n", args[1], args[0]) fmt.Printf(" before: delivers %s, delivered to %d, acknowledged to %d, %d pending, %d unacknowledged\n", before.DeliverPolicy, before.Delivered, before.AckFloor, before.Pending, before.AckPending) fmt.Printf(" after: delivers %s, %d pending; what was pending is dropped. A holder bound to it may need its "+ "process restarted to bind again\n", after.DeliverPolicy, after.Pending) return nil } // heardFrom says when a node was last heard from, in a form somebody can act on. // // "never" and "an hour ago" are different answers and are kept different. A node that has never // spoken did not finish joining; a node last heard from an hour ago is running an hour-old // picture of the mesh. func heardFrom(n inventory.Node) string { silent, ever := n.Silent() switch { case !ever: return "never spoken" case silent > SilentFor: return "out of touch " + roughly(silent) default: return "here" } } // roughly is a duration a person reads rather than parses. func roughly(d time.Duration) string { switch { case d < time.Hour: return fmt.Sprintf("%dm", int(d.Minutes())) case d < 48*time.Hour: return fmt.Sprintf("%dh", int(d.Hours())) default: return fmt.Sprintf("%dd", int(d.Hours()/24)) } } // showNode says what one machine reported about itself, in its own words. // // **A capability is detected and never assumed** (novox/hq ADR 0009), so the only account of what // a machine can do is the one it gave — and its detail is half of that account. The mesh was // keeping the yes and discarding the reason, which makes *this machine has no seat* an answer with // nowhere to go: a person told a machine lacks something wants to know what the detector saw. // // It is also where "what should it be configured as" is read. The same line that gates an // assignment carries `card1-DP-1`, and a person composing settings for that machine needs it. func showNode(ctx context.Context, stored *stores, name string) error { inv := stored.inventory node, err := inv.NodeByName(ctx, name) if err != nil { return err } fmt.Printf("%s\n", node.Name) fmt.Printf(" last heard from %s\n", heardFrom(node)) // Which host runs it, as it reported (novox/hq 04-ISSUES/087). Said whenever known, because a // host refuses a declaration carrying a field it does not understand and refuses it WHOLE — so // which host a machine runs is what decides whether the mesh can send it anything new, and // nothing could say it. "not reported" rather than blank: a machine that has not said is a // different thing from one running nothing. fmt.Printf(" host %s\n", orNotReported(node.HostVersion)) if err := showMode(ctx, inv, node); err != nil { return err } // What it was last sent, by whom, under which epoch, from which generation, naming which modules, and what // it answered (novox/hq issue 485): the record of sends is read through this verb, never the store's reader. // Unreadable is said, not taken for none. switch last, found, err := inv.LastSend(ctx, name); { case err != nil: fmt.Printf(" what it was last sent could NOT be read: %v\n", err) case !found: fmt.Printf(" last sent no send recorded since the mesh began keeping them\n") default: for _, line := range lastSendLines(last) { fmt.Println(line) } } // Whom agents run as here, and whether that account can become root without a person (ADR 0266). for _, line := range agentAccountLines(ctx, inv, node) { fmt.Println(line) } // The domain its routed names are composed under, when it has one (novox/hq ADR 0066). Shown // only when set: a machine that serves nothing to the outside has no domain, and saying so of // every internal node would be noise. domain, err := inv.PublicDomainOf(ctx, name) if err != nil { return err } if domain != "" { fmt.Printf(" public domain %s\n", domain) } // Every open condition about this machine (novox/hq to-be 45 §2) — a provider here failing a // consumer, or a consumer here failed, among them (ADR 0224). Before the capabilities, because it // is something not working now and they are a description. Unreadable is said, not passed over. open, err := openConditions(ctx) if err != nil { fmt.Printf("\n the open conditions could NOT be read, so whether anything here is wrong is not known: %v\n", err) } var here []conditions.Condition for _, c := range open { if concerns(c, name) { here = append(here, c) } } if len(here) > 0 { fmt.Printf("\n %d open condition(s) about this machine:\n", len(here)) for _, line := range conditionLines(here, time.Now()) { fmt.Printf(" %s\n", line) } } // What it runs for its modules, and how each is (novox/hq ADR 0240): "is it working" answered without // a terminal on the machine. if h, had, err := inv.HealthOf(ctx, name); err != nil { fmt.Printf("\n what it says of what it runs could NOT be read: %v\n", err) } else { fmt.Println() for _, line := range healthLines(h, had, time.Now()) { fmt.Println(line) } for _, line := range networkLines(h, had, time.Now()) { fmt.Println(line) } for _, line := range unitsLines(h, had, time.Now()) { fmt.Println(line) } } held, err := inv.Profile(ctx, name) if err != nil { return err } if held == nil { // Never reported is not the same as reported nothing, and the remedy differs: one is a // machine that has not run the host yet, the other is a machine that ran it and can do // nothing. fmt.Printf("\n this machine has never said what it can do, so everything requiring a\n" + " capability is refused here — run the host on it\n") return nil } if len(held) == 0 { fmt.Printf("\n it reported no capabilities at all\n") return nil } fmt.Printf("\n what it can do, as it reported:\n") for _, c := range held { mark := "no " if c.Present { mark = "yes" } fmt.Printf(" %s %-20s %s\n", mark, c.Name, c.Detail) } assigned, err := inv.Assigned(ctx, name) if err != nil { return err } if len(assigned) > 0 { fmt.Printf("\n assigned: %s\n", strings.Join(assigned, ", ")) } // And which of them a push leaves out, and why (novox/hq issue 380): judged as the push judges it, the same // reason its condition says. Not computable is said, never read as "all applied". plan, settings, err := planFor(ctx, stored, name) switch { case err != nil: fmt.Printf("\n whether a send leaves any assigned module out is NOT known: %s\n", oneLine(err.Error())) default: for _, line := range notAppliedLines(name, leftOutOf(plan, settings, node.Adopted)) { fmt.Println(line) } } return nil } // orNotReported is a fact a machine states about itself, or the fact that it has not. func orNotReported(s string) string { if strings.TrimSpace(s) == "" { return "not reported — this machine has not said since the mesh began keeping it" } return s } // printJSON prints a value as indented JSON, the shape every `--json` answers in. func printJSON(v any) error { return printJSONTo(os.Stdout, v) } // printJSONTo is printJSON to a writer of the caller's. func printJSONTo(w io.Writer, v any) error { body, err := json.MarshalIndent(v, "", " ") if err != nil { return err } fmt.Fprintln(w, string(body)) return nil } const judgeDirectoriesUsage = "node judge-directories — read-only: which of the directories is " + "declared the node-engine's walk that follows no link another account could make would refuse, one line each " + "(novox/hq issue 358). Nothing is made, chmodded or chowned" // nodeJudgeDirectories asks the node's engine to judge its declared directories, signed with the mesh's key as a // hand-over is. Read-only: the engine changes nothing and keeps nothing. func nodeJudgeDirectories(ctx context.Context, open *stores, args []string) error { known := func(node string) error { _, err := open.inventory.NodeByName(ctx, node) return err } ask := func(node, by string) (link.HandOverAnswer, error) { ident, err := open.Identity(ctx) if err != nil { return link.HandOverAnswer{}, fmt.Errorf("the mesh's signing key cannot be read, so nothing was asked of %s: %w", node, err) } address, err := broker.BusAddress() if err != nil { return link.HandOverAnswer{}, err } js, err := broker.Dial(address) if err != nil { return link.HandOverAnswer{}, fmt.Errorf("cannot reach the bus, so nothing was asked of %s: %w", node, err) } defer js.Close() return link.AskJudgeDirectories(ctx, js.Conn(), ident, node, by, link.HandOverWithin) } return judgeDirectoriesAsked(args, known, ask, os.Stdout) } // judgeDirectoriesAsked is the line with its two acts given: whether the mesh knows the node, and the ask. The // engine's refusal is this command's failure. func judgeDirectoriesAsked(args []string, known func(node string) error, ask func(node, by string) (link.HandOverAnswer, error), out io.Writer) error { if len(args) != 1 || args[0] == "" || strings.HasPrefix(args[0], "-") { return errors.New(judgeDirectoriesUsage) } node := args[0] if err := known(node); err != nil { return fmt.Errorf("nothing was asked: %w", err) } answer, err := ask(node, handOverBy()) if err != nil { return err } if answer.Refused != "" { return fmt.Errorf("%s refused: %s", node, answer.Refused) } fmt.Fprintf(out, "%s (read-only: nothing on %s was changed)\n", node, node) fmt.Fprintln(out, answer.Said) return nil }