Add quick-register one-liner for target-host registration
CI / Lint + build + test (push) Failing after 5m15s
CI / Lint + build + test (push) Failing after 5m15s
Operator pastes `curl -fsSL $ORCH/register/quick.sh | sudo bash` on the target host (pre-wipe). The script probes MAC + CPU/RAM/disks/NICs/GPUs, emits an expected-spec YAML, and POSTs to a new LAN-trusted JSON endpoint /api/v1/hosts. The register page shows the command prefilled with the orchestrator URL; the manual form moves into a collapsible "Register manually" disclosure.
This commit is contained in:
@@ -102,6 +102,7 @@ func main() {
|
|||||||
EventHub: hub,
|
EventHub: hub,
|
||||||
Runner: runner,
|
Runner: runner,
|
||||||
Tiles: tiles,
|
Tiles: tiles,
|
||||||
|
PublicURL: cfg.Server.PublicURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
agentAPI := &api.Agent{
|
agentAPI := &api.Agent{
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"vetting/internal/api"
|
||||||
|
"vetting/internal/db"
|
||||||
|
"vetting/internal/model"
|
||||||
|
"vetting/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newUI(t *testing.T, publicURL string) (*api.UI, *store.Hosts) {
|
||||||
|
t.Helper()
|
||||||
|
tmp := t.TempDir()
|
||||||
|
conn, err := db.Open(filepath.Join(tmp, "vetting.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = conn.Close() })
|
||||||
|
hostStore := &store.Hosts{DB: conn}
|
||||||
|
return &api.UI{
|
||||||
|
Hosts: hostStore,
|
||||||
|
PublicURL: publicURL,
|
||||||
|
}, hostStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func validQuickRegisterBody() []byte {
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "qr-host-01",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:01",
|
||||||
|
"wol_broadcast_ip": "10.0.0.255",
|
||||||
|
"wol_port": 9,
|
||||||
|
"expected_spec_yaml": "cpu:\n model: \"Xeon\"\n logical_cores: 8\n" +
|
||||||
|
"memory:\n total_gib: 16\n",
|
||||||
|
"notes": "registered via smoke test",
|
||||||
|
})
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateHostJSON_Success(t *testing.T) {
|
||||||
|
ui, hostStore := newUI(t, "")
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/hosts",
|
||||||
|
bytes.NewReader(validQuickRegisterBody()))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.CreateHostJSON(rr, req)
|
||||||
|
|
||||||
|
if rr.Code != http.StatusCreated {
|
||||||
|
t.Fatalf("status = %d, want 201; body=%q", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
MAC string `json:"mac"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if resp.ID == 0 || resp.Name != "qr-host-01" || resp.MAC != "aa:bb:cc:dd:ee:01" {
|
||||||
|
t.Fatalf("response = %+v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := hostStore.Get(context.Background(), resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get host: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != "qr-host-01" || got.MAC != "aa:bb:cc:dd:ee:01" || got.WoLPort != 9 {
|
||||||
|
t.Fatalf("host row = %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateHostJSON_DefaultsWoLPort(t *testing.T) {
|
||||||
|
ui, hostStore := newUI(t, "")
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "qr-host-noport",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:02",
|
||||||
|
"wol_broadcast_ip": "10.0.0.255",
|
||||||
|
"expected_spec_yaml": "cpu:\n logical_cores: 4\n",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/hosts", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.CreateHostJSON(rr, req)
|
||||||
|
if rr.Code != http.StatusCreated {
|
||||||
|
t.Fatalf("status = %d, want 201; body=%q", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
hosts, err := hostStore.List(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list: %v", err)
|
||||||
|
}
|
||||||
|
if len(hosts) != 1 || hosts[0].WoLPort != 9 {
|
||||||
|
t.Fatalf("hosts = %+v", hosts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateHostJSON_BadMAC(t *testing.T) {
|
||||||
|
ui, _ := newUI(t, "")
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "bad-mac",
|
||||||
|
"mac": "not-a-mac",
|
||||||
|
"wol_broadcast_ip": "10.0.0.255",
|
||||||
|
"expected_spec_yaml": "cpu:\n logical_cores: 1\n",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/hosts", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.CreateHostJSON(rr, req)
|
||||||
|
if rr.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want 400; body=%q", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
var resp map[string]string
|
||||||
|
_ = json.NewDecoder(rr.Body).Decode(&resp)
|
||||||
|
if !strings.Contains(resp["error"], "MAC") {
|
||||||
|
t.Fatalf("error = %q, want mention of MAC", resp["error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateHostJSON_BadYAML(t *testing.T) {
|
||||||
|
ui, _ := newUI(t, "")
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "bad-yaml",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:03",
|
||||||
|
"wol_broadcast_ip": "10.0.0.255",
|
||||||
|
"expected_spec_yaml": "cpu: [unterminated",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/hosts", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.CreateHostJSON(rr, req)
|
||||||
|
if rr.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want 400; body=%q", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateHostJSON_DuplicateMAC(t *testing.T) {
|
||||||
|
ui, hostStore := newUI(t, "")
|
||||||
|
// Seed a host with the MAC we'll try to re-register.
|
||||||
|
if _, err := hostStore.Create(context.Background(), model.Host{
|
||||||
|
Name: "existing",
|
||||||
|
MAC: "aa:bb:cc:dd:ee:04",
|
||||||
|
WoLBroadcastIP: "10.0.0.255",
|
||||||
|
WoLPort: 9,
|
||||||
|
ExpectedSpecYAML: "cpu:\n logical_cores: 1\n",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("seed host: %v", err)
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "dupe",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:04",
|
||||||
|
"wol_broadcast_ip": "10.0.0.255",
|
||||||
|
"expected_spec_yaml": "cpu:\n logical_cores: 1\n",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/hosts", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.CreateHostJSON(rr, req)
|
||||||
|
if rr.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("status = %d, want 409; body=%q", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
var resp map[string]string
|
||||||
|
_ = json.NewDecoder(rr.Body).Decode(&resp)
|
||||||
|
if !strings.Contains(strings.ToLower(resp["error"]), "mac") {
|
||||||
|
t.Fatalf("error = %q, want mention of MAC", resp["error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateHostJSON_BadJSON(t *testing.T) {
|
||||||
|
ui, _ := newUI(t, "")
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/hosts",
|
||||||
|
strings.NewReader("not json"))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.CreateHostJSON(rr, req)
|
||||||
|
if rr.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want 400; body=%q", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuickRegisterScript_UsesPublicURL(t *testing.T) {
|
||||||
|
ui, _ := newUI(t, "https://vetting.example.lan")
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/register/quick.sh", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.QuickRegisterScript(rr, req)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", rr.Code)
|
||||||
|
}
|
||||||
|
if ct := rr.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/x-shellscript") {
|
||||||
|
t.Fatalf("Content-Type = %q, want text/x-shellscript...", ct)
|
||||||
|
}
|
||||||
|
body := rr.Body.String()
|
||||||
|
if !strings.Contains(body, "#!/usr/bin/env bash") {
|
||||||
|
t.Fatalf("body missing bash shebang: %q", body[:min(120, len(body))])
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "https://vetting.example.lan") {
|
||||||
|
t.Fatalf("body missing configured public URL")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "/api/v1/hosts") {
|
||||||
|
t.Fatalf("body missing POST target /api/v1/hosts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuickRegisterScript_FallsBackToRequestHost(t *testing.T) {
|
||||||
|
ui, _ := newUI(t, "")
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/register/quick.sh", nil)
|
||||||
|
req.Host = "10.0.0.5:8080"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.QuickRegisterScript(rr, req)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d", rr.Code)
|
||||||
|
}
|
||||||
|
body := rr.Body.String()
|
||||||
|
if !strings.Contains(body, "http://10.0.0.5:8080") {
|
||||||
|
t.Fatalf("body missing request-host fallback URL; body prefix=%q",
|
||||||
|
body[:min(160, len(body))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifies the unique-name path also hits friendlyDBError, not just MAC.
|
||||||
|
func TestCreateHostJSON_DuplicateName(t *testing.T) {
|
||||||
|
ui, hostStore := newUI(t, "")
|
||||||
|
if _, err := hostStore.Create(context.Background(), model.Host{
|
||||||
|
Name: "already-taken",
|
||||||
|
MAC: "aa:bb:cc:dd:ee:05",
|
||||||
|
WoLBroadcastIP: "10.0.0.255",
|
||||||
|
WoLPort: 9,
|
||||||
|
ExpectedSpecYAML: "cpu:\n logical_cores: 1\n",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("seed: %v", err)
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "already-taken",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:99",
|
||||||
|
"wol_broadcast_ip": "10.0.0.255",
|
||||||
|
"expected_spec_yaml": "cpu:\n logical_cores: 1\n",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/hosts", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ui.CreateHostJSON(rr, req)
|
||||||
|
if rr.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("status = %d", rr.Code)
|
||||||
|
}
|
||||||
|
var resp map[string]string
|
||||||
|
_ = json.NewDecoder(rr.Body).Decode(&resp)
|
||||||
|
if !strings.Contains(strings.ToLower(resp["error"]), "name") {
|
||||||
|
t.Fatalf("error = %q, want friendly name-conflict message", resp["error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+109
-1
@@ -1,12 +1,14 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -15,6 +17,7 @@ import (
|
|||||||
"vetting/internal/model"
|
"vetting/internal/model"
|
||||||
"vetting/internal/orchestrator"
|
"vetting/internal/orchestrator"
|
||||||
"vetting/internal/store"
|
"vetting/internal/store"
|
||||||
|
"vetting/internal/web"
|
||||||
"vetting/internal/web/templates"
|
"vetting/internal/web/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,10 +28,32 @@ type UI struct {
|
|||||||
EventHub *events.Hub
|
EventHub *events.Hub
|
||||||
Runner *orchestrator.Runner
|
Runner *orchestrator.Runner
|
||||||
Tiles *TileEnricher
|
Tiles *TileEnricher
|
||||||
|
PublicURL string // user-visible base URL baked into the quick-register one-liner
|
||||||
}
|
}
|
||||||
|
|
||||||
var macRe = regexp.MustCompile(`^[0-9a-f]{2}(:[0-9a-f]{2}){5}$`)
|
var macRe = regexp.MustCompile(`^[0-9a-f]{2}(:[0-9a-f]{2}){5}$`)
|
||||||
|
|
||||||
|
// quickRegisterTmpl is parsed once at startup — a malformed template
|
||||||
|
// should fail the binary at init, not on a visitor's first hit.
|
||||||
|
var quickRegisterTmpl = template.Must(
|
||||||
|
template.ParseFS(web.Register, "register/quick.sh.tmpl"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// baseURL returns the orchestrator URL to bake into generated artefacts
|
||||||
|
// (the quick-register one-liner, its rendered script). Prefers the
|
||||||
|
// operator-configured public URL; falls back to the request's own host
|
||||||
|
// so a dev run on http://127.0.0.1:8080 still produces a working command.
|
||||||
|
func (u *UI) baseURL(r *http.Request) string {
|
||||||
|
if u.PublicURL != "" {
|
||||||
|
return strings.TrimRight(u.PublicURL, "/")
|
||||||
|
}
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
return scheme + "://" + r.Host
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UI) Dashboard(w http.ResponseWriter, r *http.Request) {
|
func (u *UI) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
hosts, err := u.Hosts.List(r.Context())
|
hosts, err := u.Hosts.List(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,7 +117,22 @@ func (u *UI) StartRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UI) NewHostForm(w http.ResponseWriter, r *http.Request) {
|
func (u *UI) NewHostForm(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = templates.Registration(templates.RegistrationForm{}).Render(r.Context(), w)
|
_ = templates.Registration(templates.RegistrationForm{
|
||||||
|
QuickRegisterURL: u.baseURL(r),
|
||||||
|
}).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuickRegisterScript renders the bash one-liner an operator pastes on
|
||||||
|
// the target host: hardware autodetect + POST to /api/v1/hosts. The
|
||||||
|
// orchestrator URL is substituted in so the script is self-contained.
|
||||||
|
func (u *UI) QuickRegisterScript(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/x-shellscript; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
if err := quickRegisterTmpl.Execute(w, struct{ OrchestratorURL string }{
|
||||||
|
OrchestratorURL: u.baseURL(r),
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("quick-register script render: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UI) CreateHost(w http.ResponseWriter, r *http.Request) {
|
func (u *UI) CreateHost(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -107,6 +147,7 @@ func (u *UI) CreateHost(w http.ResponseWriter, r *http.Request) {
|
|||||||
WoLPort: r.PostForm.Get("wol_port"),
|
WoLPort: r.PostForm.Get("wol_port"),
|
||||||
ExpectedSpecYAML: r.PostForm.Get("expected_spec_yaml"),
|
ExpectedSpecYAML: r.PostForm.Get("expected_spec_yaml"),
|
||||||
Notes: strings.TrimSpace(r.PostForm.Get("notes")),
|
Notes: strings.TrimSpace(r.PostForm.Get("notes")),
|
||||||
|
QuickRegisterURL: u.baseURL(r),
|
||||||
}
|
}
|
||||||
|
|
||||||
if errMsg := validateHostForm(&form); errMsg != "" {
|
if errMsg := validateHostForm(&form); errMsg != "" {
|
||||||
@@ -138,6 +179,73 @@ func (u *UI) CreateHost(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// quickRegisterPayload is the POST body accepted by /api/v1/hosts —
|
||||||
|
// the shape the quick-register bash one-liner emits.
|
||||||
|
type quickRegisterPayload struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
MAC string `json:"mac"`
|
||||||
|
WoLBroadcastIP string `json:"wol_broadcast_ip"`
|
||||||
|
WoLPort int `json:"wol_port"`
|
||||||
|
ExpectedSpecYAML string `json:"expected_spec_yaml"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateHostJSON is the API counterpart to CreateHost. Accepts the same
|
||||||
|
// fields as the form but in JSON, so a target host can POST its own
|
||||||
|
// registration payload over curl from the quick-register one-liner.
|
||||||
|
// Same validation as the form; no auth (LAN-only).
|
||||||
|
func (u *UI) CreateHostJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var p quickRegisterPayload
|
||||||
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 256*1024)).Decode(&p); err != nil {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "bad json: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form := templates.RegistrationForm{
|
||||||
|
Name: strings.TrimSpace(p.Name),
|
||||||
|
MAC: strings.ToLower(strings.TrimSpace(p.MAC)),
|
||||||
|
WoLBroadcastIP: strings.TrimSpace(p.WoLBroadcastIP),
|
||||||
|
ExpectedSpecYAML: p.ExpectedSpecYAML,
|
||||||
|
Notes: strings.TrimSpace(p.Notes),
|
||||||
|
}
|
||||||
|
if p.WoLPort > 0 {
|
||||||
|
form.WoLPort = strconv.Itoa(p.WoLPort)
|
||||||
|
}
|
||||||
|
if errMsg := validateHostForm(&form); errMsg != "" {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, errMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wolPort := p.WoLPort
|
||||||
|
if wolPort == 0 {
|
||||||
|
wolPort = 9
|
||||||
|
}
|
||||||
|
id, err := u.Hosts.Create(r.Context(), model.Host{
|
||||||
|
Name: form.Name,
|
||||||
|
MAC: form.MAC,
|
||||||
|
WoLBroadcastIP: form.WoLBroadcastIP,
|
||||||
|
WoLPort: wolPort,
|
||||||
|
ExpectedSpecYAML: form.ExpectedSpecYAML,
|
||||||
|
Notes: form.Notes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeJSONError(w, http.StatusConflict, friendlyDBError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("api: registered host %d (%s, %s)", id, form.Name, form.MAC)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": id,
|
||||||
|
"name": form.Name,
|
||||||
|
"mac": form.MAC,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSONError(w http.ResponseWriter, status int, msg string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
||||||
|
}
|
||||||
|
|
||||||
// OverrideWipeStorage is the operator's explicit "yes, wipe the disk
|
// OverrideWipeStorage is the operator's explicit "yes, wipe the disk
|
||||||
// even though we found filesystem signatures" button. Only meaningful
|
// even though we found filesystem signatures" button. Only meaningful
|
||||||
// when the latest run is FailedHolding with failed_stage=Storage — the
|
// when the latest run is FailedHolding with failed_stage=Storage — the
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ func NewRouter(d Deps) http.Handler {
|
|||||||
r.Post("/sensor", d.Agent.Sensor)
|
r.Post("/sensor", d.Agent.Sensor)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Quick-register: the bash one-liner fetched from /register/quick.sh
|
||||||
|
// POSTs here from the target host. LAN-trusted, same threat model
|
||||||
|
// as the browser UI.
|
||||||
|
r.Post("/api/v1/hosts", d.UI.CreateHostJSON)
|
||||||
|
|
||||||
// Browser UI — no auth; bind to loopback or LAN only, or front
|
// Browser UI — no auth; bind to loopback or LAN only, or front
|
||||||
// with a reverse proxy if you need a password.
|
// with a reverse proxy if you need a password.
|
||||||
r.Get("/", d.UI.Dashboard)
|
r.Get("/", d.UI.Dashboard)
|
||||||
@@ -58,6 +63,7 @@ func NewRouter(d Deps) http.Handler {
|
|||||||
r.Post("/hosts/{id}/start", d.UI.StartRun)
|
r.Post("/hosts/{id}/start", d.UI.StartRun)
|
||||||
r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage)
|
r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage)
|
||||||
r.Get("/reports/{runID}", d.UI.Report)
|
r.Get("/reports/{runID}", d.UI.Report)
|
||||||
|
r.Get("/register/quick.sh", d.UI.QuickRegisterScript)
|
||||||
r.Get("/events", d.UI.SSE)
|
r.Get("/events", d.UI.SSE)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ import "embed"
|
|||||||
|
|
||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
var Static embed.FS
|
var Static embed.FS
|
||||||
|
|
||||||
|
//go:embed register/*.tmpl
|
||||||
|
var Register embed.FS
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Vetting quick-register.
|
||||||
|
#
|
||||||
|
# Run on the target host (any Linux with root) before the host is wiped:
|
||||||
|
# curl -fsSL {{.OrchestratorURL}}/register/quick.sh | sudo bash
|
||||||
|
#
|
||||||
|
# Detects the primary NIC's MAC, probes hardware (CPU / RAM / disks /
|
||||||
|
# NICs / GPUs) into an expected-spec YAML, and POSTs everything to
|
||||||
|
# {{.OrchestratorURL}}/api/v1/hosts. After registration, go to the
|
||||||
|
# orchestrator's dashboard and click "Start vetting" for the new host.
|
||||||
|
#
|
||||||
|
# Env overrides (all optional):
|
||||||
|
# NAME Host display name (default: `hostname -s`)
|
||||||
|
# MAC Force a specific MAC (default: autodetect)
|
||||||
|
# WOL_BROADCAST WoL broadcast IP (default: primary iface broadcast)
|
||||||
|
# WOL_PORT WoL UDP port (default: 9)
|
||||||
|
# NOTES Free-text notes
|
||||||
|
# ORCH_URL Override orchestrator base URL
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ORCH_URL="${ORCH_URL:-{{.OrchestratorURL}}}"
|
||||||
|
|
||||||
|
if [[ -z "${ORCH_URL}" ]]; then
|
||||||
|
echo "ERROR: ORCH_URL is empty; pass it via env." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
primary_iface() {
|
||||||
|
# Pick the first physical ethernet-style interface that isn't a loopback,
|
||||||
|
# bridge, veth, docker, virbr, bond, tun, or tap.
|
||||||
|
ip -o link show 2>/dev/null \
|
||||||
|
| awk '$0 ~ /link\/ether/ {
|
||||||
|
name=$2; sub(":","",name)
|
||||||
|
if (name ~ /^(lo|docker|br-|veth|virbr|bond|tun|tap|wlan|wlp)/) next
|
||||||
|
print name; exit
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
IFACE="$(primary_iface || true)"
|
||||||
|
if [[ -z "${IFACE}" ]]; then
|
||||||
|
echo "ERROR: could not pick a primary network interface." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NAME="${NAME:-$(hostname -s 2>/dev/null || hostname)}"
|
||||||
|
MAC="${MAC:-$(cat /sys/class/net/${IFACE}/address 2>/dev/null || true)}"
|
||||||
|
if [[ -z "${MAC}" ]]; then
|
||||||
|
echo "ERROR: could not read MAC for ${IFACE}." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${WOL_BROADCAST:-}" ]]; then
|
||||||
|
WOL_BROADCAST="$(ip -o -4 addr show dev "${IFACE}" 2>/dev/null \
|
||||||
|
| awk '{for(i=1;i<=NF;i++) if($i=="brd") {print $(i+1); exit}}')"
|
||||||
|
fi
|
||||||
|
WOL_BROADCAST="${WOL_BROADCAST:-255.255.255.255}"
|
||||||
|
WOL_PORT="${WOL_PORT:-9}"
|
||||||
|
|
||||||
|
# --- Hardware probes ---
|
||||||
|
cores="$(nproc 2>/dev/null || echo 0)"
|
||||||
|
cpu_model="$(awk -F: '/^model name/ {sub(/^ */, "", $2); print $2; exit}' /proc/cpuinfo 2>/dev/null || true)"
|
||||||
|
mem_gib="$(awk '/^MemTotal:/ {printf "%d", ($2/1024/1024) + 0.5; exit}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
disk_yaml=""
|
||||||
|
while read -r name size serial; do
|
||||||
|
[[ -z "${name}" ]] && continue
|
||||||
|
[[ "${name}" =~ ^(sd|nvme|vd|hd) ]] || continue
|
||||||
|
[[ -z "${serial}" ]] && continue
|
||||||
|
size_gb=$(( size / 1000 / 1000 / 1000 ))
|
||||||
|
disk_yaml+=" - serial: \"${serial}\"
|
||||||
|
size_gb: ${size_gb}
|
||||||
|
"
|
||||||
|
done < <(lsblk -dn -b -o NAME,SIZE,SERIAL 2>/dev/null || true)
|
||||||
|
|
||||||
|
nic_yaml=""
|
||||||
|
for iface_dir in /sys/class/net/*; do
|
||||||
|
iface_name="$(basename "${iface_dir}")"
|
||||||
|
[[ "${iface_name}" =~ ^(lo|docker|br-|veth|virbr|bond|tun|tap)$ ]] && continue
|
||||||
|
nic_mac="$(cat "${iface_dir}/address" 2>/dev/null || true)"
|
||||||
|
[[ -z "${nic_mac}" || "${nic_mac}" == "00:00:00:00:00:00" ]] && continue
|
||||||
|
speed_mbps="$(cat "${iface_dir}/speed" 2>/dev/null || echo 0)"
|
||||||
|
if [[ "${speed_mbps}" =~ ^[0-9]+$ ]] && (( speed_mbps > 0 )); then
|
||||||
|
speed_gbps=$(( (speed_mbps + 999) / 1000 ))
|
||||||
|
else
|
||||||
|
speed_gbps=0
|
||||||
|
fi
|
||||||
|
nic_yaml+=" - mac: \"${nic_mac}\"
|
||||||
|
speed_gbps: ${speed_gbps}
|
||||||
|
"
|
||||||
|
done
|
||||||
|
|
||||||
|
gpu_yaml=""
|
||||||
|
if command -v lspci >/dev/null 2>&1; then
|
||||||
|
while IFS= read -r gpu; do
|
||||||
|
[[ -z "${gpu}" ]] && continue
|
||||||
|
gpu_yaml+=" - model: \"${gpu}\"
|
||||||
|
"
|
||||||
|
done < <(lspci -mm 2>/dev/null | awk -F\" '/"(VGA|3D|Display)/ {print $6}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Assemble the YAML spec. Empty sections are omitted so the diff engine
|
||||||
|
# skips them rather than demanding empty arrays on the actual side.
|
||||||
|
spec="cpu:
|
||||||
|
model: \"${cpu_model}\"
|
||||||
|
logical_cores: ${cores}
|
||||||
|
memory:
|
||||||
|
total_gib: ${mem_gib}
|
||||||
|
"
|
||||||
|
[[ -n "${disk_yaml}" ]] && spec+="disks:
|
||||||
|
${disk_yaml}"
|
||||||
|
[[ -n "${nic_yaml}" ]] && spec+="nics:
|
||||||
|
${nic_yaml}"
|
||||||
|
[[ -n "${gpu_yaml}" ]] && spec+="gpus:
|
||||||
|
${gpu_yaml}"
|
||||||
|
|
||||||
|
# --- JSON escape (backslash, double-quote, newline, tab, CR) ---
|
||||||
|
json_escape() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s//\\/\\\\}"
|
||||||
|
s="${s//\"/\\\"}"
|
||||||
|
s="${s//$'\t'/\\t}"
|
||||||
|
s="${s//$'\r'/\\r}"
|
||||||
|
s="${s//$'\n'/\\n}"
|
||||||
|
printf '%s' "${s}"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"name": "$(json_escape "${NAME}")",
|
||||||
|
"mac": "$(json_escape "${MAC}")",
|
||||||
|
"wol_broadcast_ip": "$(json_escape "${WOL_BROADCAST}")",
|
||||||
|
"wol_port": ${WOL_PORT},
|
||||||
|
"expected_spec_yaml": "$(json_escape "${spec}")",
|
||||||
|
"notes": "$(json_escape "${NOTES:-registered via quick-register on $(date -Is)}")"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Registering ${NAME} (${MAC}) with ${ORCH_URL}..."
|
||||||
|
resp="$(curl -fsS -X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "${payload}" \
|
||||||
|
"${ORCH_URL}/api/v1/hosts")"
|
||||||
|
echo "OK: ${resp}"
|
||||||
|
echo
|
||||||
|
echo "Open ${ORCH_URL}/ and click 'Start vetting' on ${NAME}."
|
||||||
@@ -208,3 +208,38 @@ button.danger:hover { background: rgba(229,100,102,.1); }
|
|||||||
.login-card button:hover { background: var(--accent); border-color: var(--accent); }
|
.login-card button:hover { background: var(--accent); border-color: var(--accent); }
|
||||||
|
|
||||||
body.bare main { max-width: none; }
|
body.bare main { max-width: none; }
|
||||||
|
|
||||||
|
.quick-register {
|
||||||
|
background: var(--bg-elev);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.quick-register h2 { margin: 0 0 8px; font-size: 16px; }
|
||||||
|
.quick-register p { margin: 6px 0; font-size: 13px; color: var(--text-dim); }
|
||||||
|
.quick-register p b { color: var(--text); }
|
||||||
|
.quick-register .muted { color: var(--text-dim); font-weight: 400; }
|
||||||
|
.quick-register code { font-family: var(--mono); }
|
||||||
|
.quick-register .one-liner {
|
||||||
|
background: #0b0d12;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
user-select: all;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.quick-register .one-liner code { white-space: pre; }
|
||||||
|
|
||||||
|
.manual-register { margin-top: 16px; }
|
||||||
|
.manual-register summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
.manual-register summary:hover { color: var(--text); }
|
||||||
|
.manual-register[open] summary { margin-bottom: 12px; }
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("host-%d", t.Host.ID))
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("host-%d", t.Host.ID))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 15, Col: 40}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 15, Col: 40}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -64,7 +64,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 1, Col: 0}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 1, Col: 0}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -77,7 +77,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("tile-%d", t.Host.ID))
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("tile-%d", t.Host.ID))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 17, Col: 46}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 17, Col: 46}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -90,7 +90,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(t.Host.Name)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(t.Host.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 21, Col: 39}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 21, Col: 39}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -103,7 +103,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(tileStatus(t.Latest))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(tileStatus(t.Latest))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 22, Col: 50}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 22, Col: 50}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -116,7 +116,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(t.Host.MAC)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(t.Host.MAC)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 27, Col: 20}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 27, Col: 20}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -129,7 +129,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s:%d", t.Host.WoLBroadcastIP, t.Host.WoLPort))
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s:%d", t.Host.WoLBroadcastIP, t.Host.WoLPort))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 31, Col: 69}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 31, Col: 69}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -147,7 +147,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(t.Latest.FailedStage)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(t.Latest.FailedStage)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 36, Col: 31}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 36, Col: 31}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -166,7 +166,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var11 string
|
var templ_7745c5c3_Var11 string
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d critical", t.SpecDiffCritical))
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d critical", t.SpecDiffCritical))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 42, Col: 69}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 42, Col: 69}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -189,7 +189,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var12 string
|
var templ_7745c5c3_Var12 string
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(sshInvocation(t.HoldKeyPath, t.Latest.HoldIP))
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(sshInvocation(t.HoldKeyPath, t.Latest.HoldIP))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 49, Col: 74}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 49, Col: 74}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -208,7 +208,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("log-%d", t.Latest.ID))
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("log-%d", t.Latest.ID))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 55, Col: 43}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 55, Col: 43}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -221,7 +221,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var14 string
|
var templ_7745c5c3_Var14 string
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("log-%d", t.Latest.ID))
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("log-%d", t.Latest.ID))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 56, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 56, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -244,7 +244,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var15 templ.SafeURL
|
var templ_7745c5c3_Var15 templ.SafeURL
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/start", t.Host.ID)))
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/start", t.Host.ID)))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 62, Col: 89}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 62, Col: 89}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -268,7 +268,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var16 templ.SafeURL
|
var templ_7745c5c3_Var16 templ.SafeURL
|
||||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/override-wipe", t.Host.ID)))
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/override-wipe", t.Host.ID)))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 69, Col: 97}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 69, Col: 97}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -287,7 +287,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var17 templ.SafeURL
|
var templ_7745c5c3_Var17 templ.SafeURL
|
||||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/reports/%d", t.Latest.ID)))
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/reports/%d", t.Latest.ID)))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 74, Col: 88}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 74, Col: 88}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -305,7 +305,7 @@ func HostTile(t TileData) templ.Component {
|
|||||||
var templ_7745c5c3_Var18 templ.SafeURL
|
var templ_7745c5c3_Var18 templ.SafeURL
|
||||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/delete", t.Host.ID)))
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/hosts/%d/delete", t.Host.ID)))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/host_tile.templ`, Line: 76, Col: 89}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `host_tile.templ`, Line: 76, Col: 89}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func Layout(title string) templ.Component {
|
|||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 9, Col: 17}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layout.templ`, Line: 9, Col: 17}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -86,7 +86,7 @@ func BareLayout(title string) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 38, Col: 17}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layout.templ`, Line: 38, Col: 17}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type RegistrationForm struct {
|
|||||||
ExpectedSpecYAML string
|
ExpectedSpecYAML string
|
||||||
Notes string
|
Notes string
|
||||||
Error string
|
Error string
|
||||||
|
QuickRegisterURL string // base URL (no trailing slash) used in the one-liner
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Registration(form RegistrationForm) {
|
templ Registration(form RegistrationForm) {
|
||||||
@@ -17,38 +18,49 @@ templ Registration(form RegistrationForm) {
|
|||||||
if form.Error != "" {
|
if form.Error != "" {
|
||||||
<div class="error">{ form.Error }</div>
|
<div class="error">{ form.Error }</div>
|
||||||
}
|
}
|
||||||
<form method="post" action="/hosts" class="host-form">
|
if form.QuickRegisterURL != "" {
|
||||||
<label>
|
<div class="quick-register">
|
||||||
Name
|
<h2>Quick register <span class="muted">(recommended)</span></h2>
|
||||||
<input type="text" name="name" value={ form.Name } required pattern="[A-Za-z0-9_\-\.]+" placeholder="pve-node-03"/>
|
<p>Run this on the target host as root before wiping. It auto-detects MAC and hardware, then registers with this orchestrator:</p>
|
||||||
</label>
|
<pre class="one-liner"><code>{ "curl -fsSL " + form.QuickRegisterURL + "/register/quick.sh | sudo bash" }</code></pre>
|
||||||
<label>
|
<p class="muted">After the script prints <code>OK</code>, refresh the dashboard and click <b>Start vetting</b> on the new host.</p>
|
||||||
MAC address
|
</div>
|
||||||
<input type="text" name="mac" value={ form.MAC } required placeholder="aa:bb:cc:dd:ee:ff"/>
|
}
|
||||||
</label>
|
<details class="manual-register">
|
||||||
<div class="grid-2">
|
<summary>Register manually</summary>
|
||||||
|
<form method="post" action="/hosts" class="host-form">
|
||||||
<label>
|
<label>
|
||||||
WoL broadcast IP
|
Name
|
||||||
<input type="text" name="wol_broadcast_ip" value={ form.WoLBroadcastIP } required placeholder="10.0.0.255"/>
|
<input type="text" name="name" value={ form.Name } required pattern="[A-Za-z0-9_\-\.]+" placeholder="pve-node-03"/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
WoL port
|
MAC address
|
||||||
<input type="number" name="wol_port" value={ defaultPort(form.WoLPort) } min="1" max="65535"/>
|
<input type="text" name="mac" value={ form.MAC } required placeholder="aa:bb:cc:dd:ee:ff"/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<div class="grid-2">
|
||||||
<label>
|
<label>
|
||||||
Expected hardware spec (YAML)
|
WoL broadcast IP
|
||||||
<textarea name="expected_spec_yaml" rows="12" required placeholder="cpu: model_match: ...">{ form.ExpectedSpecYAML }</textarea>
|
<input type="text" name="wol_broadcast_ip" value={ form.WoLBroadcastIP } required placeholder="10.0.0.255"/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Notes
|
WoL port
|
||||||
<textarea name="notes" rows="3">{ form.Notes }</textarea>
|
<input type="number" name="wol_port" value={ defaultPort(form.WoLPort) } min="1" max="65535"/>
|
||||||
</label>
|
</label>
|
||||||
<div class="actions">
|
</div>
|
||||||
<button type="submit">Register</button>
|
<label>
|
||||||
<a class="button-secondary" href="/">Cancel</a>
|
Expected hardware spec (YAML)
|
||||||
</div>
|
<textarea name="expected_spec_yaml" rows="12" required placeholder="cpu: model_match: ...">{ form.ExpectedSpecYAML }</textarea>
|
||||||
</form>
|
</label>
|
||||||
|
<label>
|
||||||
|
Notes
|
||||||
|
<textarea name="notes" rows="3">{ form.Notes }</textarea>
|
||||||
|
</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
<a class="button-secondary" href="/">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type RegistrationForm struct {
|
|||||||
ExpectedSpecYAML string
|
ExpectedSpecYAML string
|
||||||
Notes string
|
Notes string
|
||||||
Error string
|
Error string
|
||||||
|
QuickRegisterURL string // base URL (no trailing slash) used in the one-liner
|
||||||
}
|
}
|
||||||
|
|
||||||
func Registration(form RegistrationForm) templ.Component {
|
func Registration(form RegistrationForm) templ.Component {
|
||||||
@@ -63,7 +64,7 @@ func Registration(form RegistrationForm) templ.Component {
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(form.Error)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(form.Error)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 18, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 19, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -74,85 +75,104 @@ func Registration(form RegistrationForm) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/hosts\" class=\"host-form\"><label>Name <input type=\"text\" name=\"name\" value=\"")
|
if form.QuickRegisterURL != "" {
|
||||||
if templ_7745c5c3_Err != nil {
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"quick-register\"><h2>Quick register <span class=\"muted\">(recommended)</span></h2><p>Run this on the target host as root before wiping. It auto-detects MAC and hardware, then registers with this orchestrator:</p><pre class=\"one-liner\"><code>")
|
||||||
return templ_7745c5c3_Err
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("curl -fsSL " + form.QuickRegisterURL + "/register/quick.sh | sudo bash")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 25, Col: 108}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</code></pre><p class=\"muted\">After the script prints <code>OK</code>, refresh the dashboard and click <b>Start vetting</b> on the new host.</p></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var4 string
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<details class=\"manual-register\"><summary>Register manually</summary><form method=\"post\" action=\"/hosts\" class=\"host-form\"><label>Name <input type=\"text\" name=\"name\" value=\"")
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 23, Col: 53}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" required pattern=\"[A-Za-z0-9_\\-\\.]+\" placeholder=\"pve-node-03\"></label> <label>MAC address <input type=\"text\" name=\"mac\" value=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(form.MAC)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 27, Col: 51}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 34, Col: 54}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" required placeholder=\"aa:bb:cc:dd:ee:ff\"></label><div class=\"grid-2\"><label>WoL broadcast IP <input type=\"text\" name=\"wol_broadcast_ip\" value=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" required pattern=\"[A-Za-z0-9_\\-\\.]+\" placeholder=\"pve-node-03\"></label> <label>MAC address <input type=\"text\" name=\"mac\" value=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(form.WoLBroadcastIP)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(form.MAC)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 32, Col: 76}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 38, Col: 52}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" required placeholder=\"10.0.0.255\"></label> <label>WoL port <input type=\"number\" name=\"wol_port\" value=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" required placeholder=\"aa:bb:cc:dd:ee:ff\"></label><div class=\"grid-2\"><label>WoL broadcast IP <input type=\"text\" name=\"wol_broadcast_ip\" value=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(defaultPort(form.WoLPort))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(form.WoLBroadcastIP)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 36, Col: 76}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 43, Col: 77}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" min=\"1\" max=\"65535\"></label></div><label>Expected hardware spec (YAML) <textarea name=\"expected_spec_yaml\" rows=\"12\" required placeholder=\"cpu: model_match: ...\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" required placeholder=\"10.0.0.255\"></label> <label>WoL port <input type=\"number\" name=\"wol_port\" value=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(form.ExpectedSpecYAML)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(defaultPort(form.WoLPort))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 41, Col: 125}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 47, Col: 77}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</textarea></label> <label>Notes <textarea name=\"notes\" rows=\"3\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" min=\"1\" max=\"65535\"></label></div><label>Expected hardware spec (YAML) <textarea name=\"expected_spec_yaml\" rows=\"12\" required placeholder=\"cpu: model_match: ...\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(form.Notes)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(form.ExpectedSpecYAML)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/registration.templ`, Line: 45, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 52, Col: 126}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</textarea></label><div class=\"actions\"><button type=\"submit\">Register</button> <a class=\"button-secondary\" href=\"/\">Cancel</a></div></form></section>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</textarea></label> <label>Notes <textarea name=\"notes\" rows=\"3\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(form.Notes)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `registration.templ`, Line: 56, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</textarea></label><div class=\"actions\"><button type=\"submit\">Register</button> <a class=\"button-secondary\" href=\"/\">Cancel</a></div></form></details></section>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user