allow device matching by udev properties
This commit is contained in:
620
Cargo.lock
generated
620
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -30,3 +30,4 @@ sys-info = "0.9.1"
|
|||||||
dkl = { git = "https://novit.tech/direktil/dkl", version = "1.0.0" }
|
dkl = { git = "https://novit.tech/direktil/dkl", version = "1.0.0" }
|
||||||
openssl = "0.10.73"
|
openssl = "0.10.73"
|
||||||
reqwest = { version = "0.12.22", features = ["native-tls"] }
|
reqwest = { version = "0.12.22", features = ["native-tls"] }
|
||||||
|
glob = "0.3.3"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from rust:1.88.0-alpine as rust
|
from rust:1.91.0-alpine as rust
|
||||||
|
|
||||||
run apk add --no-cache git musl-dev libudev-zero-dev openssl-dev cryptsetup-dev lvm2-dev clang-libs clang-dev
|
run apk add --no-cache git musl-dev libudev-zero-dev openssl-dev cryptsetup-dev lvm2-dev clang-libs clang-dev
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ run --mount=type=cache,id=novit-rs,target=/usr/local/cargo/registry \
|
|||||||
RUSTFLAGS="-C target-feature=-crt-static" cargo install --path . --root /dist
|
RUSTFLAGS="-C target-feature=-crt-static" cargo install --path . --root /dist
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from alpine:3.22.0 as initrd
|
from alpine:3.22.2 as initrd
|
||||||
run apk add zstd lz4
|
run apk add zstd lz4
|
||||||
|
|
||||||
workdir /system
|
workdir /system
|
||||||
@ -34,6 +34,6 @@ run chroot . init-version
|
|||||||
run find * |cpio -H newc -oF /initrd
|
run find * |cpio -H newc -oF /initrd
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from alpine:3.22.0
|
from alpine:3.22.2
|
||||||
copy --from=initrd /initrd /
|
copy --from=initrd /initrd /
|
||||||
entrypoint ["base64","/initrd"]
|
entrypoint ["base64","/initrd"]
|
||||||
|
|||||||
@ -4,6 +4,7 @@ modd.conf {}
|
|||||||
prep: cargo test
|
prep: cargo test
|
||||||
prep: cargo build
|
prep: cargo build
|
||||||
prep: debug/init-version
|
prep: debug/init-version
|
||||||
|
#prep: cargo run --bin test
|
||||||
}
|
}
|
||||||
|
|
||||||
target/debug/init Dockerfile {
|
target/debug/init Dockerfile {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use eyre::{Result, format_err};
|
use eyre::{format_err, Result};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use std::collections::BTreeSet as Set;
|
use std::collections::BTreeSet as Set;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
@ -6,7 +6,7 @@ use tokio::io::AsyncWriteExt;
|
|||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use super::{USED_DEVS, retry_or_ignore};
|
use super::{retry_or_ignore, USED_DEVS};
|
||||||
use crate::blockdev::{is_uninitialized, uninitialize};
|
use crate::blockdev::{is_uninitialized, uninitialize};
|
||||||
use crate::fs::walk_dir;
|
use crate::fs::walk_dir;
|
||||||
use crate::input;
|
use crate::input;
|
||||||
@ -29,7 +29,7 @@ pub async fn setup(devs: &[CryptDev]) {
|
|||||||
let all_devs = walk_dir("/dev").await;
|
let all_devs = walk_dir("/dev").await;
|
||||||
|
|
||||||
for dev in devs {
|
for dev in devs {
|
||||||
let mut mappings = find_dev(dev, &all_devs);
|
let mut mappings = find_dev(dev, &all_devs).await?;
|
||||||
mappings.retain(|(_, dev_path)| !used_devs.contains(dev_path));
|
mappings.retain(|(_, dev_path)| !used_devs.contains(dev_path));
|
||||||
|
|
||||||
if mappings.is_empty() && !dev.optional() && !done.contains(&dev.name) {
|
if mappings.is_empty() && !dev.optional() && !done.contains(&dev.name) {
|
||||||
@ -134,18 +134,39 @@ async fn cryptsetup<const N: usize>(pw: &str, args: [&str; N]) -> Result<bool> {
|
|||||||
Ok(child.wait().await?.success())
|
Ok(child.wait().await?.success())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_dev(dev: &CryptDev, all_devs: &[String]) -> Vec<(String, String)> {
|
async fn find_dev(dev: &CryptDev, all_devs: &[String]) -> Result<Vec<(String, String)>> {
|
||||||
let dev_name = &dev.name;
|
let dev_name = &dev.name;
|
||||||
match dev.filter {
|
Ok(match dev.filter() {
|
||||||
DevFilter::Dev(ref path) => (all_devs.iter())
|
DevFilter::None => vec![],
|
||||||
|
DevFilter::Dev(path) => (all_devs.iter())
|
||||||
.filter(|dev_path| dev_path == &path)
|
.filter(|dev_path| dev_path == &path)
|
||||||
.map(|dev_path| (dev.name.clone(), dev_path.clone()))
|
.map(|dev_path| (dev.name.clone(), dev_path.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
DevFilter::Prefix(ref prefix) => (all_devs.iter())
|
DevFilter::Prefix(prefix) => (all_devs.iter())
|
||||||
.filter_map(|path| {
|
.filter_map(|path| {
|
||||||
let suffix = path.strip_prefix(prefix)?;
|
let suffix = path.strip_prefix(prefix)?;
|
||||||
Some((format!("{dev_name}{suffix}"), path.clone()))
|
Some((format!("{dev_name}{suffix}"), path.clone()))
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
DevFilter::Udev(filter) => {
|
||||||
|
use crate::udev;
|
||||||
|
let devs = udev::all().await?;
|
||||||
|
|
||||||
|
let filter: udev::Filter = filter.clone().into();
|
||||||
|
|
||||||
|
(devs.iter())
|
||||||
|
.filter(|dev| dev.subsystem() == Some("block") && filter.matches(dev))
|
||||||
|
.filter_map(|dev| {
|
||||||
|
let path = dev.property("DEVNAME")?.to_string();
|
||||||
|
|
||||||
|
let mut name = dev_name.replace("${name}", dev.name()?);
|
||||||
|
for (p, v) in dev.properties() {
|
||||||
|
name = name.replace(&format!("${{{p}}}"), v);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((name, path))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,10 @@ use log::{info, warn};
|
|||||||
use std::collections::BTreeSet as Set;
|
use std::collections::BTreeSet as Set;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use super::{Result, format_err, retry_or_ignore};
|
use super::{format_err, retry_or_ignore, Result};
|
||||||
use crate::{
|
use crate::{
|
||||||
udev,
|
udev,
|
||||||
utils::{NameAliases, select_n_by_regex},
|
utils::{select_n_by_regex, select_n_by_udev, NameAliases},
|
||||||
};
|
};
|
||||||
use dkl::bootstrap::{Config, Network};
|
use dkl::bootstrap::{Config, Network};
|
||||||
|
|
||||||
@ -26,16 +26,13 @@ pub async fn setup(cfg: &Config) {
|
|||||||
async fn setup_network(net: &Network, assigned: &mut Set<String>) -> Result<()> {
|
async fn setup_network(net: &Network, assigned: &mut Set<String>) -> Result<()> {
|
||||||
info!("setting up network {}", net.name);
|
info!("setting up network {}", net.name);
|
||||||
|
|
||||||
let netdevs = get_interfaces()?
|
let netdevs = (get_interfaces().await?)
|
||||||
.filter(|dev| !assigned.contains(dev.name()))
|
.filter(|dev| !assigned.contains(dev.name()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for dev in &netdevs {
|
for dev in &netdevs {
|
||||||
info!(
|
let names = [dev.name()].into_iter().chain(dev.aliases()).join(", ");
|
||||||
"- available network device: {}, aliases [{}]",
|
info!("- available network device: {}", names);
|
||||||
dev.name(),
|
|
||||||
dev.aliases().join(", ")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cmd = Command::new("ash");
|
let mut cmd = Command::new("ash");
|
||||||
@ -47,8 +44,19 @@ async fn setup_network(net: &Network, assigned: &mut Set<String>) -> Result<()>
|
|||||||
for iface in &net.interfaces {
|
for iface in &net.interfaces {
|
||||||
let var = &iface.var;
|
let var = &iface.var;
|
||||||
|
|
||||||
let netdevs = netdevs.iter().filter(|na| !assigned.contains(na.name()));
|
let if_names = if let Some(ref udev_filter) = iface.udev {
|
||||||
let if_names = select_n_by_regex(iface.n, &iface.regexps, netdevs);
|
select_n_by_udev(
|
||||||
|
iface.n,
|
||||||
|
"net",
|
||||||
|
"INTERFACE",
|
||||||
|
&udev_filter.clone().into(),
|
||||||
|
&assigned,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
let netdevs = netdevs.iter().filter(|na| !assigned.contains(na.name()));
|
||||||
|
select_n_by_regex(iface.n, &iface.regexps, netdevs)
|
||||||
|
};
|
||||||
|
|
||||||
if if_names.is_empty() {
|
if if_names.is_empty() {
|
||||||
return Err(format_err!("- no interface match for {var:?}"));
|
return Err(format_err!("- no interface match for {var:?}"));
|
||||||
@ -71,24 +79,20 @@ async fn setup_network(net: &Network, assigned: &mut Set<String>) -> Result<()>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_interfaces() -> Result<impl Iterator<Item = NameAliases>> {
|
async fn get_interfaces() -> Result<impl Iterator<Item = NameAliases>> {
|
||||||
Ok(udev::get_devices("net")?.into_iter().map(|dev| {
|
let nas: Vec<_> = (udev::all().await?.of_subsystem("net"))
|
||||||
let mut na = NameAliases::new(dev.sysname().to_string());
|
.filter_map(|dev| {
|
||||||
|
let name = dev.property("INTERFACE")?;
|
||||||
|
let mut na = NameAliases::new(name.to_string());
|
||||||
|
|
||||||
for (property, value) in dev.properties() {
|
for (p, v) in dev.properties() {
|
||||||
if [
|
if p.starts_with("ID_NET_NAME") {
|
||||||
"INTERFACE",
|
na.push(v.to_string());
|
||||||
"ID_NET_NAME",
|
}
|
||||||
"ID_NET_NAME_PATH",
|
|
||||||
"ID_NET_NAME_MAC",
|
|
||||||
"ID_NET_NAME_SLOT",
|
|
||||||
]
|
|
||||||
.contains(&property)
|
|
||||||
{
|
|
||||||
na.push(value.to_string());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
na
|
Some(na)
|
||||||
}))
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(nas.into_iter())
|
||||||
}
|
}
|
||||||
|
|||||||
145
src/udev.rs
145
src/udev.rs
@ -63,3 +63,148 @@ pub fn get_devices(class: &str) -> Result<Vec<Device>> {
|
|||||||
|
|
||||||
Ok(devices)
|
Ok(devices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn all() -> Result<Devs> {
|
||||||
|
let output = tokio::process::Command::new("udevadm")
|
||||||
|
.args(["info", "-e"])
|
||||||
|
.stderr(std::process::Stdio::inherit())
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(eyre::format_err!("udevadm failed: {}", output.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Devs {
|
||||||
|
data: unsafe { String::from_utf8_unchecked(output.stdout) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn by_path(path: &str) -> Result<Devs> {
|
||||||
|
let output = tokio::process::Command::new("udevadm")
|
||||||
|
.args(["info", path])
|
||||||
|
.stderr(std::process::Stdio::inherit())
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(eyre::format_err!("udevadm failed: {}", output.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Devs {
|
||||||
|
data: unsafe { String::from_utf8_unchecked(output.stdout) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Devs {
|
||||||
|
data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> Devs {
|
||||||
|
pub fn iter(&'t self) -> impl Iterator<Item = Dev<'t>> {
|
||||||
|
self.data
|
||||||
|
.split("\n\n")
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| Dev(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn of_subsystem(&'t self, subsystem: &str) -> impl Iterator<Item = Dev<'t>> {
|
||||||
|
self.iter().filter(|dev| dev.subsystem() == Some(subsystem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Dev<'t>(&'t str);
|
||||||
|
|
||||||
|
impl<'t> Dev<'t> {
|
||||||
|
pub fn raw(&self) -> &str {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// alpine's udev prefixes we've seen:
|
||||||
|
// - P: Device path in /sys/
|
||||||
|
// - N: Kernel device node name
|
||||||
|
// - S: Device node symlink
|
||||||
|
// - L: Device node symlink priority [ignored]
|
||||||
|
// - E: Device property
|
||||||
|
|
||||||
|
fn by_prefix(&self, prefix: &'static str) -> impl Iterator<Item = &str> {
|
||||||
|
self.0.lines().filter_map(move |l| l.strip_prefix(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device path in /sys/
|
||||||
|
pub fn path(&self) -> Option<&str> {
|
||||||
|
self.by_prefix("P: ").next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kernel device node name
|
||||||
|
pub fn name(&self) -> Option<&str> {
|
||||||
|
self.by_prefix("N: ").next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device node symlinks
|
||||||
|
pub fn symlinks(&self) -> Vec<&str> {
|
||||||
|
self.by_prefix("S: ").collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device properties
|
||||||
|
pub fn properties(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||||
|
self.by_prefix("E: ").filter_map(|s| s.split_once("="))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device property
|
||||||
|
pub fn property(&self, name: &str) -> Option<&str> {
|
||||||
|
self.properties()
|
||||||
|
.filter_map(|(n, v)| (n == name).then_some(v))
|
||||||
|
.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device subsystem
|
||||||
|
pub fn subsystem(&self) -> Option<&str> {
|
||||||
|
self.property("SUBSYSTEM")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Filter {
|
||||||
|
Has(String),
|
||||||
|
Eq(String, String),
|
||||||
|
Glob(String, glob::Pattern),
|
||||||
|
And(Vec<Filter>),
|
||||||
|
Or(Vec<Filter>),
|
||||||
|
Not(Box<Filter>),
|
||||||
|
False,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> Filter {
|
||||||
|
pub fn matches(&self, dev: &Dev) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::False => false,
|
||||||
|
Self::Has(k) => dev.property(k).is_some(),
|
||||||
|
Self::Eq(k, v) => dev.properties().any(|kv| kv == (k, v)),
|
||||||
|
Self::Glob(k, pattern) => dev
|
||||||
|
.properties()
|
||||||
|
.any(|(pk, pv)| pk == k && pattern.matches(pv)),
|
||||||
|
Self::And(ops) => ops.iter().all(|op| op.matches(dev)),
|
||||||
|
Self::Or(ops) => ops.iter().any(|op| op.matches(dev)),
|
||||||
|
Self::Not(op) => !op.matches(dev),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> Into<Filter> for dkl::bootstrap::UdevFilter {
|
||||||
|
fn into(self) -> Filter {
|
||||||
|
match self {
|
||||||
|
Self::Has(p) => Filter::Has(p),
|
||||||
|
Self::Eq(p, v) => Filter::Eq(p, v),
|
||||||
|
Self::Glob(p, pattern) => match glob::Pattern::new(&pattern) {
|
||||||
|
Ok(pattern) => Filter::Glob(p, pattern),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("pattern {pattern:?} will never match: {e}");
|
||||||
|
Filter::False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::And(ops) => Filter::And(ops.into_iter().map(Self::into).collect()),
|
||||||
|
Self::Or(ops) => Filter::Or(ops.into_iter().map(Self::into).collect()),
|
||||||
|
Self::Not(op) => Filter::Not(Box::new((*op).into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
23
src/utils.rs
23
src/utils.rs
@ -2,6 +2,8 @@ use log::error;
|
|||||||
use std::collections::BTreeSet as Set;
|
use std::collections::BTreeSet as Set;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use crate::udev;
|
||||||
|
|
||||||
static CMDLINE: LazyLock<String> = LazyLock::new(|| {
|
static CMDLINE: LazyLock<String> = LazyLock::new(|| {
|
||||||
std::fs::read("/proc/cmdline")
|
std::fs::read("/proc/cmdline")
|
||||||
.inspect_err(|e| error!("failed to read kernel cmdline: {e}"))
|
.inspect_err(|e| error!("failed to read kernel cmdline: {e}"))
|
||||||
@ -88,3 +90,24 @@ pub fn select_n_by_regex<'t>(
|
|||||||
nas.take(n as usize).collect()
|
nas.take(n as usize).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn select_n_by_udev<'t>(
|
||||||
|
n: i16,
|
||||||
|
subsystem: &str,
|
||||||
|
result_property: &str,
|
||||||
|
filter: &udev::Filter,
|
||||||
|
in_use: &Set<String>,
|
||||||
|
) -> eyre::Result<Vec<String>> {
|
||||||
|
let devs = udev::all().await?;
|
||||||
|
let nas = devs
|
||||||
|
.of_subsystem(subsystem)
|
||||||
|
.filter(|dev| filter.matches(dev))
|
||||||
|
.filter_map(|dev| Some(dev.property(result_property)?.to_string()))
|
||||||
|
.filter(|name| !in_use.contains(name));
|
||||||
|
|
||||||
|
Ok(if n == -1 {
|
||||||
|
nas.collect()
|
||||||
|
} else {
|
||||||
|
nas.take(n as usize).collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ ssh:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
- name: loopback
|
- name: loopback
|
||||||
interfaces: [ { var: iface, n: 1, regexps: [ "^lo$" ] } ]
|
interfaces: [ { var: iface, n: 1, udev: !eq [INTERFACE, lo] } ]
|
||||||
script: |
|
script: |
|
||||||
ip a add 127.0.0.1/8 dev lo
|
ip a add 127.0.0.1/8 dev lo
|
||||||
ip a add ::1/128 dev lo
|
ip a add ::1/128 dev lo
|
||||||
@ -38,28 +38,22 @@ networks:
|
|||||||
interfaces:
|
interfaces:
|
||||||
- var: iface
|
- var: iface
|
||||||
n: 1
|
n: 1
|
||||||
regexps:
|
udev: !has ID_NET_NAME_MAC
|
||||||
- eth.*
|
|
||||||
- veth.*
|
|
||||||
- eno.*
|
|
||||||
- enp.*
|
|
||||||
script: |
|
script: |
|
||||||
ip li set $iface up
|
ip li set $iface up
|
||||||
udhcpc -i $iface -b -t1 -T1 -A5 ||
|
udhcpc -i $iface -b -t1 -T1 -A5 ||
|
||||||
ip a add 2001:41d0:306:168f::1337:2eed/64 dev $iface
|
ip a add 2001:41d0:306:168f::1337:2eed/64 dev $iface
|
||||||
|
|
||||||
pre_lvm_crypt:
|
pre_lvm_crypt:
|
||||||
- dev: /dev/vda
|
- name: sys-${name}
|
||||||
name: sys0
|
udev: !glob [ DEVNAME, /dev/vd* ]
|
||||||
- dev: /dev/vdb
|
|
||||||
name: sys1
|
|
||||||
|
|
||||||
lvm:
|
lvm:
|
||||||
- vg: storage
|
- vg: storage
|
||||||
pvs:
|
pvs:
|
||||||
n: 2
|
n: 2
|
||||||
regexps:
|
regexps:
|
||||||
- /dev/mapper/sys[01]
|
- ^/dev/mapper/sys-
|
||||||
# to match full disks
|
# to match full disks
|
||||||
#- /dev/nvme[0-9]+n[0-9]+
|
#- /dev/nvme[0-9]+n[0-9]+
|
||||||
#- /dev/vd[a-z]+
|
#- /dev/vd[a-z]+
|
||||||
|
|||||||
Reference in New Issue
Block a user