Skip to content

Commit 6b96c5f

Browse files
committed
Some doc and refactoring
1 parent 4a44373 commit 6b96c5f

15 files changed

Lines changed: 283 additions & 126 deletions

File tree

.github/workflows/rust.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939

4040
- name: Install apt dependencies
4141
run: |
42+
sudo apt-get update
4243
sudo apt-get install -y dnsmasq qemu-system-x86 ovmf
4344
sudo mkdir -p /etc/qemu
4445
echo allow br-pixie | sudo tee /etc/qemu/bridge.conf

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ build/*
44
buildroot/pixie.tar.gz
55
pixie-web/dist
66
.*.swp
7+
pixie-shared/Cargo.lock

pixie-server/src/dnsmasq.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Starts and configures dnsmasq.
2+
13
use crate::{find_network, state::State};
24
use anyhow::{Context, Result};
35
use macaddr::MacAddr6;
@@ -35,14 +37,15 @@ impl Drop for DnsmasqHandle {
3537
async fn write_config(state: &State) -> Result<()> {
3638
let (name, _) = find_network(state.config.hosts.listen_on)?;
3739

38-
let mut dnsmasq_conf = File::create(state.storage_dir.join("dnsmasq.conf"))?;
40+
let mut dnsmasq_conf = File::create(state.run_dir.join("dnsmasq.conf"))?;
3941

4042
let dhcp_dynamic_conf = match state.config.hosts.dhcp {
4143
DhcpMode::Static(low, high) => format!("dhcp-range=tag:netboot,{low},{high}"),
4244
DhcpMode::Proxy(ip) => format!("dhcp-range=tag:netboot,{},proxy", ip),
4345
};
4446

4547
let storage_str = state.storage_dir.to_str().unwrap();
48+
let run_str = state.run_dir.to_str().unwrap();
4649

4750
write!(
4851
dnsmasq_conf,
@@ -52,7 +55,7 @@ async fn write_config(state: &State) -> Result<()> {
5255
## net0
5356
{dhcp_dynamic_conf}
5457
dhcp-range=tag:!netboot,10.0.0.0,static,255.0.0.0
55-
dhcp-hostsfile={storage_str}/hosts
58+
dhcp-hostsfile={run_str}/hosts
5659
dhcp-boot=pixie-uefi.efi
5760
interface={name}
5861
except-interface=lo
@@ -81,7 +84,7 @@ dhcp-vendorclass=set:netboot,pixie
8184
}
8285

8386
async fn write_hosts(state: &State, hosts: &[(MacAddr6, Ipv4Addr, Option<String>)]) -> Result<()> {
84-
let file = File::create(state.storage_dir.join("hosts"))?;
87+
let file = File::create(state.run_dir.join("hosts"))?;
8588
let mut file = BufWriter::new(file);
8689

8790
for (mac, ip, hostname) in hosts {
@@ -123,8 +126,8 @@ pub async fn main(state: Arc<State>) -> Result<()> {
123126
let dnsmasq = DnsmasqHandle {
124127
child: Command::new("dnsmasq")
125128
.arg(format!(
126-
"--conf-file={storage_str}/dnsmasq.conf",
127-
storage_str = state.storage_dir.to_str().unwrap()
129+
"--conf-file={run_str}/dnsmasq.conf",
130+
run_str = state.run_dir.to_str().unwrap()
128131
))
129132
.arg("--log-dhcp")
130133
.arg("--no-daemon")

pixie-server/src/http.rs

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! HTTP server for the admin web interface.
2+
13
use crate::state::{State, UnitSelector};
24
use anyhow::Result;
35
use axum::{
@@ -17,11 +19,16 @@ use tower_http::{
1719
services::ServeDir, trace::TraceLayer, validate_request::ValidateRequestHeaderLayer,
1820
};
1921

22+
/// `GET /admin/action/{unit_selector}/{action}`
23+
///
24+
/// Sets the next [`Action`] for all [`Unit`]s accepted by the [`UnitSelector`].
25+
///
26+
/// [`Unit`]: pixie_shared::config::Unit
2027
async fn action(
21-
Path((unit_filter, action)): Path<(String, Action)>,
28+
Path((unit_selector, action)): Path<(String, Action)>,
2229
extract::State(state): extract::State<Arc<State>>,
2330
) -> impl IntoResponse {
24-
let Some(unit_selector) = UnitSelector::parse(&state, unit_filter) else {
31+
let Some(unit_selector) = UnitSelector::parse(&state, unit_selector) else {
2532
return (
2633
StatusCode::BAD_REQUEST,
2734
"Invalid unit selector\n".to_owned(),
@@ -36,11 +43,16 @@ async fn action(
3643
}
3744
}
3845

46+
/// `GET /admin/curr_action/{unit_selector}/{action}`
47+
///
48+
/// Sets the current [`Action`] for all [`Unit`]s accepted by the [`UnitSelector`].
49+
///
50+
/// [`Unit`]: pixie_shared::config::Unit
3951
async fn curr_action(
40-
Path((unit_filter, action)): Path<(String, Action)>,
52+
Path((unit_selector, action)): Path<(String, Action)>,
4153
extract::State(state): extract::State<Arc<State>>,
4254
) -> impl IntoResponse {
43-
let Some(unit_selector) = UnitSelector::parse(&state, unit_filter) else {
55+
let Some(unit_selector) = UnitSelector::parse(&state, unit_selector) else {
4456
return (
4557
StatusCode::BAD_REQUEST,
4658
"Invalid unit selector\n".to_owned(),
@@ -55,8 +67,14 @@ async fn curr_action(
5567
}
5668
}
5769

70+
/// `GET /admin/image/{unit_selector}/{image}`
71+
///
72+
/// Sets the [`Image`] for all [`Unit`]s accepted by the [`UnitSelector`].
73+
///
74+
/// [`Unit`]: pixie_shared::config::Unit
75+
/// [`Image`]: pixie_shared::Image
5876
async fn image(
59-
Path((unit_filter, image)): Path<(String, String)>,
77+
Path((unit_selector, image)): Path<(String, String)>,
6078
extract::State(state): extract::State<Arc<State>>,
6179
) -> impl IntoResponse {
6280
if !state.config.images.contains(&image) {
@@ -66,7 +84,7 @@ async fn image(
6684
);
6785
}
6886

69-
let Some(unit_selector) = UnitSelector::parse(&state, unit_filter) else {
87+
let Some(unit_selector) = UnitSelector::parse(&state, unit_selector) else {
7088
return (
7189
StatusCode::BAD_REQUEST,
7290
"Invalid unit selector\n".to_owned(),
@@ -80,11 +98,16 @@ async fn image(
8098
}
8199
}
82100

101+
/// `GET /admin/forget/{unit_selector}`
102+
///
103+
/// Forgets all [`Unit`]s selected by the [`UnitSelector`].
104+
///
105+
/// [`Unit`]: pixie_shared::config::Unit
83106
async fn forget(
84-
Path(unit_filter): Path<String>,
107+
Path(unit_selector): Path<String>,
85108
extract::State(state): extract::State<Arc<State>>,
86109
) -> impl IntoResponse {
87-
let Some(unit_selector) = UnitSelector::parse(&state, unit_filter) else {
110+
let Some(unit_selector) = UnitSelector::parse(&state, unit_selector) else {
88111
return (
89112
StatusCode::BAD_REQUEST,
90113
"Invalid unit selector\n".to_owned(),
@@ -119,13 +142,19 @@ async fn delete_image(
119142
}
120143
}
121144

145+
/// `GET /admin/gc`
146+
///
147+
/// Removes all chunks not used by any image.
122148
async fn gc(extract::State(state): extract::State<Arc<State>>) -> impl IntoResponse {
123149
match state.gc_chunks() {
124150
Ok(()) => (StatusCode::NO_CONTENT, String::new()),
125151
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}\n")),
126152
}
127153
}
128154

155+
/// `GET /admin/status`
156+
///
157+
/// Stream of json-formatted events on changes to the database.
129158
async fn status(extract::State(state): extract::State<Arc<State>>) -> impl IntoResponse {
130159
let initial_messages = [StatusUpdate::Config(state.config.clone())];
131160

@@ -161,10 +190,13 @@ pub async fn main(state: Arc<State>) -> Result<()> {
161190
let mut router = Router::new()
162191
.route("/admin/status", get(status))
163192
.route("/admin/gc", get(gc))
164-
.route("/admin/action/:unit/:action", get(action))
165-
.route("/admin/curr_action/:unit/:action", get(curr_action))
166-
.route("/admin/image/:unit/:image", get(image))
167-
.route("/admin/forget/:unit", get(forget))
193+
.route("/admin/action/:unit_selector/:action", get(action))
194+
.route(
195+
"/admin/curr_action/:unit_selector/:action",
196+
get(curr_action),
197+
)
198+
.route("/admin/image/:unit_selector/:image", get(image))
199+
.route("/admin/forget/:unit_selector", get(forget))
168200
.route("/admin/rollback/:image", get(rollback))
169201
.route("/admin/delete/:image", get(delete_image))
170202
.nest_service(

pixie-server/src/main.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ use std::{
2121
};
2222
use tokio::task::JoinHandle;
2323

24+
/// Finds the mac address for the given ip.
25+
///
26+
/// This function searches the address in the arp cache, if it is not available it tries to
27+
/// populate it by pinging the peer.
2428
fn find_mac(ip: Ipv4Addr) -> Result<MacAddr6> {
2529
struct Zombie {
2630
inner: Child,
@@ -93,10 +97,11 @@ fn find_network(ip: Ipv4Addr) -> Result<(String, Ipv4Net)> {
9397
bail!("Could not find the network for {}", ip);
9498
}
9599

100+
/// Command line arguments for pixie-server.
96101
#[derive(Parser, Debug)]
97102
struct PixieOptions {
98103
/// Directory in which files will be stored.
99-
/// Must already contain files: tftpboot/pixie-uefi.efi, config.yaml
104+
/// Must already contain files: `tftpboot/pixie-uefi.efi` and `config.yaml`.
100105
#[clap(short, long, default_value = "./storage")]
101106
storage_dir: PathBuf,
102107
}

pixie-server/src/ping.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Handles pings from clients.
2+
13
use crate::{
24
find_mac,
35
state::{State, UnitSelector},

pixie-server/src/state/images.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ use pixie_shared::{ChunkHash, ChunkStats, ChunksStats, Image, ImagesStats};
44
use tokio::sync::watch;
55

66
impl State {
7+
/// Checks whether the database contains the given chunk.
78
pub fn has_chunk(&self, hash: ChunkHash) -> bool {
89
self.chunks_stats
910
.lock()
1011
.expect("chunks_stats lock is poisoned")
1112
.contains_key(&hash)
1213
}
1314

15+
/// Get the chunk compressed data.
1416
pub fn get_chunk_cdata(&self, hash: ChunkHash) -> Result<Option<Vec<u8>>> {
1517
let path = self.storage_dir.join(CHUNKS_DIR).join(hex::encode(hash));
1618
let chunks_stats = self
@@ -24,6 +26,7 @@ impl State {
2426
Ok(cdata)
2527
}
2628

29+
/// Store the given chunk to the database.
2730
pub fn add_chunk(&self, data: &[u8]) -> Result<()> {
2831
ensure!(
2932
data.len() <= pixie_shared::MAX_CHUNK_SIZE,
@@ -57,6 +60,7 @@ impl State {
5760
res
5861
}
5962

63+
/// Finds and deletes all chunks which are not part of any image.
6064
pub fn gc_chunks(&self) -> Result<()> {
6165
let mut res = Ok(());
6266
self.images_stats.send_modify(|images_stats| {

pixie-server/src/state/mod.rs

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
//! The database for pixie-server.
2+
//!
3+
//! Structure of the storage directory:
4+
//! - `config.yaml`: configuration file for pixie-server
5+
//! - `registered.json`: json file containing all information about registered units.
6+
//! - `admin/`: directory containing the static files for the admin web interface.
7+
//! - `chunks/`: directory containing the image's chunks.
8+
//! - `images/`: directory containing the image's info.
9+
//! - `tftpboot/`: directory containing the necessary files for network boot.
10+
111
#![warn(clippy::unwrap_used)]
212

313
mod images;
414
mod units;
515

616
use anyhow::{anyhow, ensure, Context, Result};
7-
use pixie_shared::{ChunkHash, ChunkStats, ChunksStats, Config, Image, ImagesStats, Station, Unit};
17+
use pixie_shared::{
18+
ChunkHash, ChunkStats, ChunksStats, Config, Image, ImagesStats, RegistrationInfo, Unit,
19+
};
820
use std::{
921
collections::HashMap,
1022
fs::File,
@@ -25,6 +37,10 @@ const REGISTERED_JSON: &str = "registered.json";
2537
const CHUNKS_DIR: &str = "chunks";
2638
const IMAGES_DIR: &str = "images";
2739

40+
/// Atomically write `data` at the specified `path`.
41+
///
42+
/// On crash `path` is guaranteed to be in a consistent state, but a temporary file might be left
43+
/// behind.
2844
fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {
2945
static CNT: AtomicU64 = AtomicU64::new(0);
3046

@@ -44,14 +60,18 @@ fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {
4460
Ok(())
4561
}
4662

63+
/// Builds a map from ip address to hostname parsing the hostfile at `path`.
4764
fn build_hostmap(path: Option<&Path>) -> Result<HashMap<Ipv4Addr, String>> {
4865
let mut hostmap = HashMap::new();
4966
if let Some(path) = path {
5067
let hosts =
5168
hostfile::parse_file(path).map_err(|e| anyhow!("Error parsing host file: {e}"))?;
5269
for mut host in hosts {
5370
if let IpAddr::V4(ip) = host.ip {
54-
hostmap.insert(ip, std::mem::take(&mut host.names[0]));
71+
let old = hostmap.insert(ip, std::mem::take(&mut host.names[0]));
72+
if old.is_some() {
73+
log::warn!("Duplicated hostname for {ip}");
74+
}
5575
} else {
5676
log::warn!(
5777
"ignoring non-IPv4 address {} for host {}",
@@ -64,19 +84,25 @@ fn build_hostmap(path: Option<&Path>) -> Result<HashMap<Ipv4Addr, String>> {
6484
Ok(hostmap)
6585
}
6686

87+
/// See [the module-level documentation][self].
6788
pub struct State {
89+
/// The storage_dir received by command line arguments.
6890
pub storage_dir: PathBuf,
91+
/// A directory stored in ram for dynamically generated files, will be deleted on Drop.
92+
pub run_dir: PathBuf,
93+
/// The config parsed from the config file.
6994
pub config: Config,
95+
/// The hostmap built from the hostmap file.
7096
hostmap: watch::Sender<HashMap<Ipv4Addr, String>>,
7197

7298
units: watch::Sender<Vec<Unit>>,
73-
// TODO: use an Option
74-
last: Mutex<Station>,
99+
registration_hint: Mutex<Option<RegistrationInfo>>,
75100
images_stats: watch::Sender<ImagesStats>,
76101
chunks_stats: Mutex<ChunksStats>,
77102
}
78103

79104
impl State {
105+
/// Loads the [`State`] from the given path.
80106
pub fn load(storage_dir: PathBuf) -> Result<Self> {
81107
let config: Config = {
82108
let path = storage_dir.join(CONFIG_YAML);
@@ -121,22 +147,6 @@ impl State {
121147
}
122148
});
123149

124-
let last = Station {
125-
group: config
126-
.groups
127-
.iter()
128-
.next()
129-
.context("there should be at least one group")?
130-
.0
131-
.clone(),
132-
image: config
133-
.images
134-
.first()
135-
.context("there should be at least one image")?
136-
.clone(),
137-
..Default::default()
138-
};
139-
140150
let chunks_dir = storage_dir.join(CHUNKS_DIR);
141151
let mut chunks_stats: ChunksStats = std::fs::read_dir(&chunks_dir)
142152
.with_context(|| format!("open chunks dir: {}", chunks_dir.display()))?
@@ -193,12 +203,16 @@ impl State {
193203
images,
194204
};
195205

206+
let run_dir = PathBuf::from(format!("/run/pixie-{}", std::process::id()));
207+
std::fs::create_dir(&run_dir)?;
208+
196209
Ok(Self {
197210
storage_dir,
211+
run_dir,
198212
config,
199213
hostmap: watch::Sender::new(hostmap),
200214
units,
201-
last: Mutex::new(last),
215+
registration_hint: Mutex::new(None),
202216
images_stats: watch::Sender::new(images_stats),
203217
chunks_stats: Mutex::new(chunks_stats),
204218
})
@@ -214,3 +228,11 @@ impl State {
214228
self.hostmap.subscribe()
215229
}
216230
}
231+
232+
impl Drop for State {
233+
fn drop(&mut self) {
234+
// If we fail to remove the directory it's not an issue as it will be deleted on shutdown
235+
// and doesn't take much space.
236+
let _ = std::fs::remove_dir_all(&self.run_dir);
237+
}
238+
}

0 commit comments

Comments
 (0)