Skip to content

Commit 93c7d00

Browse files
committed
HTTP API documentation
1 parent f3c849d commit 93c7d00

File tree

3 files changed

+113
-29
lines changed

3 files changed

+113
-29
lines changed

README.md

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,50 +36,113 @@ netbootd exposes all "mounts" via both TFTP and HTTP simultatenously.
3636
Naturally, it's not a good idea to transfer really large files over TFTP but PXE generally
3737
requires use of TFTP in most cases.
3838

39-
TFTP and HTTP content can either be static text (embedded in the manifest),
40-
generated content (using Go's `text/template` templating engine) or proxied to upstream HTTP(S).
41-
This last feature is mainly intended to proxy TFTP to HTTP(S) but very well may be used to
42-
reverse-proxy HTTP in otherwise isolated environments and can use a proxy itself
39+
TFTP and HTTP content can either be static text (embedded in the manifest), generated content (using
40+
Go's `text/template` templating engine) or proxied to upstream HTTP(S). This last feature is mainly intended to proxy
41+
TFTP to HTTP(S) but very well may be used to reverse-proxy HTTP in otherwise isolated environments and can use a proxy
42+
itself
4343
(`HTTP_PROXY` and `NO_PROXY` is honored automatically by Go).
4444

45-
netbootd cannot serve local files. An exception is a bundled version of [iPXE](https://ipxe.org/),
46-
which allows to download (typically) kernel and initrd over HTTP instead of TFTP.
45+
netbootd cannot serve local files. An exception is a bundled version of [iPXE](https://ipxe.org/), which allows
46+
downloading (typically) kernel and initrd over HTTP instead of TFTP.
4747

4848
## Manifests
4949

5050
A manifest represents a machine to be provisioned/served. The behavior of built-in
5151
DHCP, TFTP and HTTP server is specific to a manifest, meaning that it varies based
5252
on source MAC/IP. Each host may see different content at `/something` path.
5353

54-
Note that this is not a security feature and you should not host any sensitive content.
55-
MAC and IPs can be easily spoofed. In fact, netbootd includes a convenience feature to
56-
spoof source IP for troubleshooting purposes. Append `?spoof=<ip-address>` to HTTP request
57-
to see the response for a particular host. There is no TFTP counterpart of this feature.
54+
Note that this is not a security feature and you should not host any sensitive content. MAC and IPs can be easily
55+
spoofed. In fact, netbootd includes a convenience feature to spoof source IP for troubleshooting purposes.
56+
Append `?spoof=<ip-address>` to HTTP request to see the response for a particular host. There is no TFTP counterpart of
57+
this feature.
5858

5959
Example manifests are included in the `examples/` directory.
6060

6161
## HTTP API
6262

63-
TODO.
63+
In this preview/development version, this HTTP API does not support authentication.
64+
65+
<details>
66+
<summary>GET /api/manifests</summary>
67+
Returns a dictionary of all manifests keyed by their ID.
68+
69+
Supports `Accept` header (if provided) that allows selecting a json output (`Accept: application/json`).
70+
</details>
71+
72+
<details>
73+
<summary>GET /api/manifests/{id}</summary>
74+
Returns a single manifest with ID provided in the URL path.
75+
76+
Supports `Accept` header (if provided) that allows selecting a json output (`Accept: application/json`).
77+
78+
Returns:
79+
80+
* 200 for successful response
81+
* 404 if manifest with provided ID does not exist
82+
83+
</details>
84+
85+
<details>
86+
<summary>PUT /api/manifests/{id}</summary>
87+
Accepts a manifest in either JSON (`Content-type: application/json`) or YAML (default) format.
88+
89+
Returns:
90+
91+
* 201 Created on success
92+
* 400 for malformed request (invalid manifest)
93+
94+
</details>
95+
96+
<details>
97+
<summary>DELETE /api/manifests/{id}</summary>
98+
Ensures that manifest with provided ID does not exist.
99+
100+
Always returns 204, even if manifest already did not exist.
101+
</details>
102+
103+
<details>
104+
<summary>GET|POST /api/self/suspend-boot</summary>
105+
Allows a provisioned host to ask not to be booted again.
106+
This does not block DHCP, TFTP or HTTP requests, it only removes NBP information from DHCP responses.
107+
108+
This operation looks for a manifest matching the IP address of the requester. It is possible to spoof it
109+
with `?spoof=1.2.3.4` query parameter.
110+
</details>
111+
112+
<details>
113+
<summary>GET|POST /api/self/unsuspend-boot</summary>
114+
Re-enables booting for a provisioned host.
115+
116+
This operation looks for a manifest matching the IP address of the requester. It is possible to spoof it
117+
with `?spoof=1.2.3.4` query parameter.
118+
</details>
119+
120+
<details>
121+
<summary>GET /api/self/manifest</summary>
122+
Returns a manifest matching requester's IP Address.
123+
124+
Supports `Accept` header (if provided) that allows selecting a json output (`Accept: application/json`).
125+
126+
This operation looks for a manifest matching the IP address of the requester. It is possible to spoof it
127+
with `?spoof=1.2.3.4` query parameter.
128+
</details>
64129

65130
## Usage
66131

67132
```
68133
Usage:
69-
netbootd [flags]
134+
netbootd server [flags]
70135
71136
Flags:
72137
-a, --address string IP address to listen on (DHCP, TFTP, HTTP)
73138
-r, --api-port int HTTP API port to listen on (default 8081)
74-
-d, --debug enable debug logging
75-
-h, --help help for netbootd
139+
-h, --help help for server
76140
-p, --http-port int HTTP port to listen on (default 8080)
77141
-i, --interface string interface to listen on, e.g. eth0 (DHCP)
78142
-m, --manifests string load manifests from directory
79-
--trace enable trace logging
80143
```
81144

82-
Run e.g. `./netbootd --trace -m ./examples/`
145+
Run e.g. `./netbootd --trace server -m ./examples/`
83146

84147
## Roadmap / TODOs
85148

api/server.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io/ioutil"
1212
"net"
1313
"net/http"
14+
"strings"
1415
"time"
1516
)
1617

@@ -63,10 +64,15 @@ func NewServer(store *store.Store) (server *Server, err error) {
6364

6465
// GET /api/manifests
6566
r.HandleFunc("/api/manifests", func(w http.ResponseWriter, r *http.Request) {
66-
w.Header().Set("Content-Type", "text/yaml")
67+
var b []byte
68+
if strings.Contains(r.Header.Get("Accept"), "application/json") {
69+
w.Header().Set("Content-Type", "applications/json")
70+
b, _ = json.Marshal(store.GetAll())
71+
} else {
72+
w.Header().Set("Content-Type", "text/yaml")
73+
b, _ = yaml.Marshal(store.GetAll())
74+
}
6775
w.WriteHeader(http.StatusOK)
68-
69-
b, _ := yaml.Marshal(store.GetAll())
7076
w.Write(b)
7177
}).Methods("GET")
7278

@@ -78,17 +84,23 @@ func NewServer(store *store.Store) (server *Server, err error) {
7884
http.Error(w, "not found", http.StatusNotFound)
7985
return
8086
}
81-
w.Header().Set("Content-Type", "text/yaml")
87+
var b []byte
88+
if strings.Contains(r.Header.Get("Accept"), "application/json") {
89+
w.Header().Set("Content-Type", "applications/json")
90+
b, _ = json.Marshal(store.GetAll())
91+
} else {
92+
w.Header().Set("Content-Type", "text/yaml")
93+
b, _ = yaml.Marshal(store.GetAll())
94+
}
8295
w.WriteHeader(http.StatusOK)
83-
b, _ := yaml.Marshal(m)
8496
w.Write(b)
8597
}).Methods("GET")
8698

8799
// PUT /api/manifests/{id}
88100
r.HandleFunc("/api/manifests/{id}", func(w http.ResponseWriter, r *http.Request) {
89101
buf, _ := ioutil.ReadAll(r.Body)
90102
var m manifest.Manifest
91-
if r.Header.Get("Content-type") == "application/json" {
103+
if r.Header.Get("Content-Type") == "application/json" {
92104
err = json.Unmarshal(buf, &m)
93105
if err != nil {
94106
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -113,6 +125,7 @@ func NewServer(store *store.Store) (server *Server, err error) {
113125
w.WriteHeader(http.StatusNoContent)
114126
}).Methods("DELETE")
115127

128+
// GET|POST /api/self/suspend-boot
116129
r.HandleFunc("/api/self/suspend-boot", func(w http.ResponseWriter, r *http.Request) {
117130
var ip net.IP
118131
if queryFirst(r, "spoof") != "" {
@@ -130,8 +143,9 @@ func NewServer(store *store.Store) (server *Server, err error) {
130143
m.Suspended = true
131144

132145
w.WriteHeader(http.StatusOK)
133-
})
146+
}).Methods("GET", "POST")
134147

148+
// GET|POST /api/self/unsuspend-boot
135149
r.HandleFunc("/api/self/unsuspend-boot", func(w http.ResponseWriter, r *http.Request) {
136150
var ip net.IP
137151
if queryFirst(r, "spoof") != "" {
@@ -149,8 +163,9 @@ func NewServer(store *store.Store) (server *Server, err error) {
149163
m.Suspended = false
150164

151165
w.WriteHeader(http.StatusOK)
152-
})
166+
}).Methods("GET", "POST")
153167

168+
// GET /api/self/manifest
154169
r.HandleFunc("/api/self/manifest", func(w http.ResponseWriter, r *http.Request) {
155170
var ip net.IP
156171
if queryFirst(r, "spoof") != "" {
@@ -165,11 +180,17 @@ func NewServer(store *store.Store) (server *Server, err error) {
165180
http.Error(w, "not found", http.StatusNotFound)
166181
return
167182
}
168-
w.Header().Set("Content-Type", "text/yaml")
183+
var b []byte
184+
if strings.Contains(r.Header.Get("Accept"), "application/json") {
185+
w.Header().Set("Content-Type", "applications/json")
186+
b, _ = json.Marshal(store.GetAll())
187+
} else {
188+
w.Header().Set("Content-Type", "text/yaml")
189+
b, _ = yaml.Marshal(store.GetAll())
190+
}
169191
w.WriteHeader(http.StatusOK)
170-
buf, _ := yaml.Marshal(m)
171-
w.Write(buf)
172-
})
192+
w.Write(b)
193+
}).Methods("GET")
173194

174195
return server, nil
175196
}

dhcpd/handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (server *Server) HandleMsg4(buf []byte, oob *ipv4.ControlMessage, peer net.
119119
}
120120

121121
// NBP
122-
if req.IsOptionRequested(dhcpv4.OptionTFTPServerName) {
122+
if req.IsOptionRequested(dhcpv4.OptionTFTPServerName) && !manifest.Suspended {
123123
resp.Options.Update(dhcpv4.OptTFTPServerName(localIp.String()))
124124
}
125125

0 commit comments

Comments
 (0)