diff --git a/.dockerignore b/.dockerignore index e7bf1da..cd39ab4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ Dockerfile +modd.* tmp/**/* dist/* go.work @@ -6,3 +7,4 @@ go.work.sum modd.*conf test-initrd* test-initrd/**/* +target diff --git a/.gitignore b/.gitignore index 008db5b..636c13c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /tmp /go.work /go.work.sum +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8ef6ee4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,623 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "init" +version = "2.1.0" +dependencies = [ + "env_logger", + "eyre", + "itertools", + "log", + "nix", + "regex", + "serde", + "serde_json", + "serde_yaml", + "shell-escape", + "tokio", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..84ccc49 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "init" +version = "2.1.0" +edition = "2024" + +[profile.release] +strip = true +panic = "abort" +opt-level = "z" +lto = true + +[dependencies] +env_logger = "0.11.3" +eyre = "0.6.12" +itertools = "0.14.0" +log = "0.4.21" +nix = { version = "0.30.1", features = ["feature", "reboot"] } +regex = "1.11.1" +serde = { version = "1.0.198", features = ["derive"] } +serde_json = "1.0.116" +serde_yaml = "0.9.34" +shell-escape = "0.1.5" +tokio = { version = "1.38.0", features = ["rt", "signal", "process", "net", "macros"] } diff --git a/Dockerfile b/Dockerfile index 0881909..27d9103 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,39 +1,34 @@ -from golang:1.23.2-alpine3.20 as build -run apk add --no-cache gcc musl-dev linux-headers eudev-dev upx +from rust:1.87.0-alpine as rust + +run apk add --no-cache git musl-dev libudev-zero-dev # pkgconfig cryptsetup-dev lvm2-dev clang-dev clang-static workdir /src - copy . . - -env CGO_ENABLED=1 -run \ - --mount=type=cache,id=gomod,target=/go/pkg/mod \ - --mount=type=cache,id=gobuild,target=/root/.cache/go-build \ - go test ./... \ -&& go build -ldflags "-s -w" -o /go/bin/init -trimpath . - -run upx /go/bin/init +run --mount=type=cache,id=novit-rs,target=/usr/local/cargo/registry \ + --mount=type=cache,id=novit-rs-target,sharing=private,target=/src/target \ + cargo build --release && cp target/release/init / # ------------------------------------------------------------------------ -from alpine:3.20.3 as initrd - -run apk add --no-cache xz +from alpine:3.22.0 as initrd workdir /layer + run . /etc/os-release \ && wget -O- https://dl-cdn.alpinelinux.org/alpine/v${VERSION_ID%.*}/releases/x86_64/alpine-minirootfs-${VERSION_ID}-x86_64.tar.gz |tar zxv -run apk add --no-cache -p . musl lvm2 lvm2-extra lvm2-dmeventd udev cryptsetup e2fsprogs lsblk -run rm -rf usr/share/apk var/cache/apk +run apk add --no-cache --update -p . musl coreutils lvm2 lvm2-extra lvm2-dmeventd udev cryptsetup \ + e2fsprogs btrfs-progs lsblk openssh-server openssh-client \ + && rm -rf usr/share/apk var/cache/apk -copy --from=build /go/bin/init . +copy --from=rust /init init +run cd bin && for cmd in init-version connect-boot bootstrap; do ln -s ../init $cmd; done # check viability -run chroot /layer /init hello +run chroot . init-version run find |cpio -H newc -o >/initrd # ------------------------------------------------------------------------ -from alpine:3.20.3 +from alpine:3.22.0 copy --from=initrd /initrd / entrypoint ["base64","/initrd"] diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..057ea2e --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,5 @@ +from rust:1.77.1-alpine as rust + +run apk add --no-cache musl-dev +run apk add --no-cache musl lvm2 lvm2-extra lvm2-dmeventd udev cryptsetup e2fsprogs btrfs-progs lsblk + diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..431e758 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,2 @@ +from alpine:3.19.0 +run apk add --no-cache musl lvm2 lvm2-extra lvm2-dmeventd udev cryptsetup e2fsprogs btrfs-progs lsblk diff --git a/build-init b/build-init new file mode 100755 index 0000000..ac56ce6 --- /dev/null +++ b/build-init @@ -0,0 +1,27 @@ +set -ex + +which podman &>/dev/null && docker=podman || docker=docker + +mkdir -p empty +$docker build -t nv-rs-build --network=host -f Dockerfile.build empty + +case $1 in + release) + opts=--release + bindir=target/release + ;; + "") + bindir=target/debug + ;; + *) + echo >&2 "invalid arg: $1" + exit 1 + ;; +esac + +$docker run --rm -i --net=host --user=$UID \ + nv-rs-build \ + cargo build $opts + +mkdir -p dist +cp $bindir/init dist/ diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2928225 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +use std::process::Command; +fn main() { + let output = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .output() + .unwrap(); + let git_commit = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_COMMIT={}", git_commit); +} diff --git a/debug/bootstrap b/debug/bootstrap new file mode 120000 index 0000000..2f5573a --- /dev/null +++ b/debug/bootstrap @@ -0,0 +1 @@ +../target/debug/init \ No newline at end of file diff --git a/debug/connect-boot b/debug/connect-boot new file mode 120000 index 0000000..2f5573a --- /dev/null +++ b/debug/connect-boot @@ -0,0 +1 @@ +../target/debug/init \ No newline at end of file diff --git a/debug/init b/debug/init new file mode 120000 index 0000000..2f5573a --- /dev/null +++ b/debug/init @@ -0,0 +1 @@ +../target/debug/init \ No newline at end of file diff --git a/debug/init-version b/debug/init-version new file mode 120000 index 0000000..2f5573a --- /dev/null +++ b/debug/init-version @@ -0,0 +1 @@ +../target/debug/init \ No newline at end of file diff --git a/init-rs b/init-rs new file mode 120000 index 0000000..a0ecdcc --- /dev/null +++ b/init-rs @@ -0,0 +1 @@ +target/debug/init \ No newline at end of file diff --git a/modd.conf b/modd.conf index b0e96bb..0dbc428 100644 --- a/modd.conf +++ b/modd.conf @@ -1,14 +1,14 @@ modd.conf {} -go.??? **/*.go { - prep: go test ./... - prep: mkdir -p dist - prep: go build -o dist/init . - prep: go build -o dist/ ./tools/... +**/*.rs Cargo.* { + prep: cargo test + prep: cargo build + prep: debug/init-version } -dist/init Dockerfile { - prep: docker build -t novit-initrd-gen . - prep: docker run novit-initrd-gen |base64 -d >dist/initrd.new +target/debug/init Dockerfile { + prep: docker build --network host -t novit-initrd-gen . + prep: docker run --net=host --rm novit-initrd-gen |base64 -d >dist/initrd.new prep: mv dist/initrd.new dist/initrd + prep: ls -sh dist/initrd } diff --git a/run-docker b/run-docker new file mode 100755 index 0000000..fd93882 --- /dev/null +++ b/run-docker @@ -0,0 +1,2 @@ +docker build -t nv-initrd-test -f Dockerfile.test empty +docker run -d --name nv-initrd-test -it --privileged -v $PWD:/src --workdir /src/test-initrd nv-initrd-test diff --git a/run-test.sh b/run-test.sh index 4bc3626..6153735 100755 --- a/run-test.sh +++ b/run-test.sh @@ -10,7 +10,8 @@ if ! [ -e $disk2 ]; then qemu-img create -f qcow2 $disk2 10G fi -exec qemu-system-x86_64 -pidfile qemu.pid -kernel test-kernel -initrd test-initrd.cpio \ +exec qemu-system-x86_64 -pidfile qemu.pid \ + -kernel test-kernel -initrd test-initrd.cpio \ -smp 2 -m 2048 \ -netdev bridge,br=novit,id=eth0 -device virtio-net-pci,netdev=eth0 \ -drive file=$disk1,if=virtio \ diff --git a/src/bootstrap.rs b/src/bootstrap.rs new file mode 100644 index 0000000..ef68c36 --- /dev/null +++ b/src/bootstrap.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/src/bootstrap/config.rs b/src/bootstrap/config.rs new file mode 100644 index 0000000..bb5362a --- /dev/null +++ b/src/bootstrap/config.rs @@ -0,0 +1,158 @@ +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Config { + pub anti_phishing_code: String, + + pub keymap: Option, + pub modules: Option, + + pub auths: Vec, + + pub networks: Vec, + + #[serde(default)] + pub ssh: SSHServer, + + #[serde(default)] + pub pre_lvm_crypt: Vec, + #[serde(default)] + pub lvm: Vec, + #[serde(default)] + pub crypt: Vec, + + pub bootstrap: Bootstrap, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Auth { + pub name: String, + #[serde(rename = "sshKey")] + pub ssh_key: String, + pub password: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Network { + pub name: String, + pub interfaces: Vec, + pub script: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct NetworkInterface { + pub var: String, + pub n: usize, + pub regexps: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SSHServer { + pub listen: String, + pub keys: SSHKeys, +} +impl Default for SSHServer { + fn default() -> Self { + Self { + listen: "[::]:22".to_string(), + keys: SSHKeys::default(), + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SSHKeys { + dsa: Option, + rsa: Option, + ecdsa: Option, + ed25519: Option, +} +impl SSHKeys { + pub fn iter(&self) -> impl Iterator { + [ + self.dsa.iter(), + self.rsa.iter(), + self.ecdsa.iter(), + self.ed25519.iter(), + ] + .into_iter() + .flatten() + .map(String::as_str) + } +} +impl Default for SSHKeys { + fn default() -> Self { + Self { + dsa: Some("id_dsa".to_string()), + rsa: Some("id_rsa".to_string()), + ecdsa: Some("id_ecdsa".to_string()), + ed25519: Some("id_ed25519".to_string()), + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct LvmVG { + vg: String, + pvs: LvmPV, + + #[serde(default)] + defaults: LvmLVDefaults, + + lvs: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct LvmLVDefaults { + #[serde(default = "default_fs")] + fs: String, + raid: Option, +} +impl Default for LvmLVDefaults { + fn default() -> Self { + Self { + fs: default_fs(), + raid: None, + } + } +} + +fn default_fs() -> String { + "ext4".to_string() +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct LvmLV { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + fs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + raid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + extents: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct LvmPV { + n: i16, + regexps: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct CryptDev { + name: String, + dev: Option, + prefix: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Raid { + mirrors: Option, + stripes: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Bootstrap { + dev: String, + seed: String, +} diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 0000000..a0cba49 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,4 @@ +pub mod bootstrap; +pub mod connect_boot; +pub mod init; +pub mod version; diff --git a/src/cmd/bootstrap.rs b/src/cmd/bootstrap.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/cmd/connect_boot.rs b/src/cmd/connect_boot.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/cmd/init.rs b/src/cmd/init.rs new file mode 100644 index 0000000..61f5328 --- /dev/null +++ b/src/cmd/init.rs @@ -0,0 +1,213 @@ +use eyre::{format_err, Result}; +use log::{error, info, warn}; +use std::os::unix::fs::symlink; +use std::{fs, io, process::Command}; + +use crate::{bootstrap::config::Config, cmd::version::version_string}; + +mod networks; +mod sshd; + +pub fn run() -> Result<()> { + if std::process::id() != 1 { + return Err(format_err!( + "init must run as PID 1, not {}", + std::process::id() + )); + } + + info!("Welcome to {}", version_string()); + + let uname = nix::sys::utsname::uname()?; + let kernel_version = uname.release().to_string_lossy(); + info!("Linux version {kernel_version}"); + + let cfg: Config = retry(|| { + let cfg = fs::read("config.yaml").map_err(|e| format_err!("failed to read config: {e}"))?; + serde_yaml::from_slice(cfg.as_slice()) + .map_err(|e| format_err!("failed to parse config: {e}")) + }); + + info!("config loaded"); + info!("anti-phishing-code: {}", cfg.anti_phishing_code); + + // mount basic filesystems + mount("none", "/proc", "proc", None); + mount("none", "/sys", "sysfs", None); + mount("none", "/dev", "devtmpfs", None); + mount("none", "/dev/pts", "devpts", Some("gid=5,mode=620")); + + // mount modules + if let Some(ref modules) = cfg.modules { + mount(modules, "/modules", "squashfs", None); + + fs::create_dir_all("/lib/modules")?; + let modules_path = &format!("/modules/lib/modules/{kernel_version}"); + + if !fs::exists(modules_path)? { + let e = format_err!("invalid modules package: {modules_path} should exist"); + error!("{e}"); + return Err(e); + } + + symlink(modules_path, format!("/lib/modules/{kernel_version}"))?; + } else { + warn!("modules NOT mounted (not configured)"); + } + + // init devices + info!("initializing devices"); + start_daemon("udevd", &[]); + + exec("udevadm", &["trigger", "-c", "add", "-t", "devices"]); + exec("udevadm", &["trigger", "-c", "add", "-t", "subsystems"]); + exec("udevadm", &["settle"]); + + // networks + networks::setup(&cfg)?; + + // Wireguard VPN + // TODO startVPN() + + // SSH service + sshd::start(&cfg); + + // dmcrypt blockdevs + // TODO setupCrypt(cfg.PreLVMCrypt, map[string]string{}); + + // LVM + // TODO setupLVM(cfg); + + // bootstrap the system + // TODO bootstrap(cfg); + + // finalize + // TODO finalizeBoot(); + + exec_shell(); + Ok(()) +} + +fn mount(src: &str, dst: &str, fstype: &str, opts: Option<&str>) { + if let Err(e) = fs::create_dir_all(dst) { + error!("failed to create dir {dst}: {e}"); + } + + let mut args = vec![src, dst, "-t", fstype]; + if let Some(opts) = opts { + args.extend(["-o", opts]); + } + exec("mount", &args); +} + +fn start_daemon(prog: &str, args: &[&str]) { + let cmd_str = sh_str(prog, args); + retry_or_ignore(|| { + info!("starting as daemon: {cmd_str}"); + let mut cmd = Command::new(prog); + cmd.args(args); + cmd.spawn()?; + Ok(()) + }); +} + +fn exec(prog: &str, args: &[&str]) { + let cmd_str = sh_str(prog, args); + retry_or_ignore(|| { + info!("# {cmd_str}"); + let mut cmd = Command::new(prog); + cmd.args(args); + let s = cmd.status()?; + if s.success() { + Ok(()) + } else { + Err(format_err!("command failed: {s}")) + } + }); +} + +fn retry_or_ignore(mut action: impl FnMut() -> Result<()>) { + loop { + match action() { + Ok(_) => return, + Err(e) => { + error!("{e}"); + + loop { + eprint!("[r]etry, [i]gnore, or [s]hell? "); + + let mut line = String::new(); + io::stdin().read_line(&mut line).unwrap(); + + match line.trim() { + "r" => break, + "i" => return, + "s" => { + exec_shell(); + break; + } + v => { + eprintln!("invalid choice: {v:?}"); + } + }; + } + } + } + } +} + +fn retry(mut action: impl FnMut() -> Result) -> T { + loop { + match action() { + Ok(v) => return v, + Err(e) => { + error!("{e}"); + + loop { + eprint!("[r]etry, or [s]hell? "); + + let mut line = String::new(); + io::stdin().read_line(&mut line).unwrap(); + + match line.trim() { + "r" => break, + "s" => { + exec_shell(); + break; + } + v => { + eprintln!("invalid choice: {v:?}"); + } + }; + } + } + } + } +} + +fn exec_shell() { + let mut child = match Command::new("ash").spawn() { + Ok(c) => c, + Err(e) => { + error!("failed to exec shell: {e}"); + return; + } + }; + + let _ = child.wait(); +} + +fn sh_str(prog: &str, args: &[&str]) -> String { + use std::borrow::Cow; + + let mut buf = String::new(); + + buf.push_str(&shell_escape::escape(Cow::Borrowed(prog))); + + for &arg in args { + buf.push(' '); + buf.push_str(&shell_escape::escape(Cow::Borrowed(arg))); + } + + buf +} diff --git a/src/cmd/init/networks.rs b/src/cmd/init/networks.rs new file mode 100644 index 0000000..3f4b886 --- /dev/null +++ b/src/cmd/init/networks.rs @@ -0,0 +1,93 @@ +use itertools::Itertools; +use log::{info, warn}; +use std::collections::BTreeSet as Set; +use std::process::Command; + +use super::{format_err, retry_or_ignore, Config, Result}; +use crate::{ + udev, + utils::{select_n_by_regex, NameAliases}, +}; + +pub fn setup(cfg: &Config) -> Result<()> { + if cfg.networks.is_empty() { + warn!("no networks configured"); + return Ok(()); + } + + let mut assigned: Set = Set::new(); + + for net in &cfg.networks { + retry_or_ignore(|| { + info!("setting up network {}", net.name); + + let netdevs = get_interfaces()? + .filter(|dev| !assigned.contains(dev.name())) + .collect::>(); + + for dev in &netdevs { + info!( + "- available network device: {}, aliases [{}]", + dev.name(), + dev.aliases().join(", ") + ); + } + + let mut cmd = Command::new("ash"); + cmd.arg("-c"); + cmd.arg(&net.script); + + let mut selected = Vec::new(); + + for iface in &net.interfaces { + let var = &iface.var; + + let netdevs = netdevs.iter().filter(|na| !assigned.contains(na.name())); + let if_names = select_n_by_regex(iface.n, &iface.regexps, netdevs); + + if if_names.is_empty() { + return Err(format_err!("- no interface match for {var:?}")); + } + + let value = if_names.join(" "); + info!("- {var}={value}"); + cmd.env(var, value); + + selected.extend(if_names); + } + + info!("- running script"); + let status = cmd.status()?; + if !status.success() { + return Err(format_err!("setup script failed: {status}")); + } + + assigned.extend(selected); + Ok(()) + }); + } + + Ok(()) +} + +fn get_interfaces() -> Result> { + Ok(udev::get_devices("net")?.into_iter().map(|dev| { + let mut na = NameAliases::new(dev.sysname().to_string()); + + for (property, value) in dev.properties() { + if [ + "INTERFACE", + "ID_NET_NAME", + "ID_NET_NAME_PATH", + "ID_NET_NAME_MAC", + "ID_NET_NAME_SLOT", + ] + .contains(&property) + { + na.push(value.to_string()); + } + } + + na + })) +} diff --git a/src/cmd/init/sshd.rs b/src/cmd/init/sshd.rs new file mode 100644 index 0000000..280072b --- /dev/null +++ b/src/cmd/init/sshd.rs @@ -0,0 +1,81 @@ +use log::{info, warn}; +use std::fs; +use std::io::Write; +use std::net; +use std::os::unix::fs::PermissionsExt; +use std::process::{Command, Stdio}; +use std::thread; + +use super::{retry_or_ignore, Config}; + +pub fn start(cfg: &Config) { + retry_or_ignore(|| { + info!("ssh: writing authorized keys"); + + let ssh_dir = "/root/.ssh"; + let authorized_keys = format!("{ssh_dir}/authorized_keys"); + + fs::create_dir_all(ssh_dir)?; + fs::set_permissions(ssh_dir, fs::Permissions::from_mode(0o700))?; + + let mut ak = Vec::new(); + + for auth in &cfg.auths { + writeln!(ak, "{} {}", auth.ssh_key, auth.name)?; + } + + fs::write(authorized_keys, ak)?; + Ok(()) + }); + + retry_or_ignore(|| { + let mut sshd_args = Vec::new(); + + sshd_args.extend(["-i", "-E", "/var/log/sshd.log"]); + + for key_path in cfg.ssh.keys.iter() { + if !fs::exists(key_path).is_ok_and(|b| b) { + info!("ssh: host key not found (ignored): {key_path}"); + continue; + } + sshd_args.extend(["-h", key_path]); + } + + let sshd_args = sshd_args.into_iter().map(String::from).collect(); + + // don't pre-start ssd as it should rarely be useful at this stage, use inetd-style. + let listen_addr = cfg.ssh.listen.clone(); + info!("ssh: starting listener on {listen_addr}"); + + let listener = net::TcpListener::bind(listen_addr)?; + + thread::spawn(move || handle_ssh_connections(listener, sshd_args)); + + Ok(()) + }); +} + +fn handle_ssh_connections(listener: net::TcpListener, sshd_args: Vec) { + loop { + let Ok((stream, remote)) = listener.accept() else { + warn!("ssh: listener stopped"); + break; + }; + + info!("ssh: new connection from {remote}"); + + use std::os::unix::io::{AsRawFd, FromRawFd}; + let fd = stream.as_raw_fd(); + + let mut cmd = Command::new("/usr/sbin/sshd"); + cmd.args(&sshd_args); + + cmd.stdin(unsafe { Stdio::from_raw_fd(fd) }); + cmd.stdout(unsafe { Stdio::from_raw_fd(fd) }); + cmd.stderr(Stdio::null()); + + if let Err(e) = cmd.spawn() { + warn!("ssh: failed to start server: {e}"); + }; + } +} diff --git a/src/cmd/version.rs b/src/cmd/version.rs new file mode 100644 index 0000000..9ec38d8 --- /dev/null +++ b/src/cmd/version.rs @@ -0,0 +1,12 @@ +pub fn run() { + println!("{}", version_string()); +} + +pub fn version_string() -> String { + format!( + "Direktil {} v{} (git commit {})", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION"), + env!("GIT_COMMIT") + ) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..95f04ba --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod bootstrap; +pub mod cmd; +pub mod lsblk; +pub mod lvm; +pub mod udev; +pub mod utils; diff --git a/src/lsblk.rs b/src/lsblk.rs new file mode 100644 index 0000000..15049e2 --- /dev/null +++ b/src/lsblk.rs @@ -0,0 +1,27 @@ +use std::io; +use std::process::Command; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Report { + pub blockdevices: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct BlockDev { + pub name: String, + #[serde(rename = "maj:min")] + pub maj_min: String, + pub rm: bool, + pub size: String, + pub ro: bool, + #[serde(rename = "type")] + pub dev_type: String, + pub mountpoints: Vec>, + #[serde(default)] + pub children: Vec, +} + +pub fn report() -> io::Result { + let output = Command::new("lsblk").arg("--json").output()?; + Ok(serde_json::from_slice(output.stdout.as_slice()).unwrap()) +} diff --git a/src/lvm.rs b/src/lvm.rs new file mode 100644 index 0000000..565f0cd --- /dev/null +++ b/src/lvm.rs @@ -0,0 +1,84 @@ +use std::io; +use std::process::Command; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Report { + report: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +enum ReportObj { + PV { pv: Vec }, + VG { vg: Vec }, + LV { lv: Vec }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct PV { + pub pv_name: String, + pub vg_name: String, + pub pv_fmt: String, + pub pv_attr: String, + pub pv_size: String, + pub pv_free: String, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct VG { + pub vg_name: String, + pub pv_count: String, + pub lv_count: String, + pub snap_count: String, + pub vg_attr: String, + pub vg_size: String, + pub vg_free: String, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct LV { + lv_name: String, + vg_name: String, + lv_attr: String, + lv_size: String, + pool_lv: String, + origin: String, + data_percent: String, + metadata_percent: String, + move_pv: String, + mirror_log: String, + copy_percent: String, + convert_lv: String, +} + +pub fn pvs() -> io::Result> { + report_cmd("pvs", |o| match o { + ReportObj::PV { pv } => Some(pv), + _ => None, + }) +} + +pub fn vgs() -> io::Result> { + report_cmd("vgs", |o| match o { + ReportObj::VG { vg } => Some(vg), + _ => None, + }) +} + +pub fn lvs() -> io::Result> { + report_cmd("lvs", |o| match o { + ReportObj::LV { lv } => Some(lv), + _ => None, + }) +} + +fn report_cmd(cmd: &str, find: fn(ReportObj) -> Option>) -> io::Result> { + let output = Command::new(cmd).arg("--reportformat=json").output()?; + let report: Report = serde_json::from_slice(output.stdout.as_slice()).unwrap(); + Ok(report + .report + .into_iter() + .filter_map(find) + .flatten() + .collect()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b1d6e6e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +use eyre::Result; +use log::error; +use std::env; +use std::process::exit; + +use init::cmd; + +fn main() -> Result<()> { + env_logger::builder() + .format_timestamp_millis() + .format_target(false) + .filter_level(log::LevelFilter::Info) + .write_style(env_logger::WriteStyle::Always) + .init(); + + unsafe { + env::set_var("PATH", "/bin:/sbin:/usr/bin:/usr/sbin"); + } + + let call_name = env::args().next().unwrap_or("init".into()); + let call_name = (call_name.rsplit_once('/').map(|(_, n)| n)).unwrap_or(call_name.as_str()); + + match call_name { + "init" => cmd::init::run()?, + "init-version" => cmd::version::run(), + _ => { + error!("invalid call name: {call_name:?}"); + exit(1); + } + }; + + Ok(()) +} diff --git a/src/udev.rs b/src/udev.rs new file mode 100644 index 0000000..67c5f72 --- /dev/null +++ b/src/udev.rs @@ -0,0 +1,65 @@ +use eyre::Result; +use log::error; + +pub struct Device { + sysname: String, + output: String, +} + +impl Device { + pub fn sysname(&self) -> &str { + self.sysname.as_str() + } + + pub fn properties(&self) -> impl Iterator { + self.output + .lines() + .filter_map(|line| line.strip_prefix("E: ")?.split_once('=')) + } +} + +pub fn get_devices(class: &str) -> Result> { + let mut devices = Vec::new(); + + // none of libudev and udev crates were able to list network devices. + // falling back to manual sysfs scanning :( + // + // Even when given a syspath, + // - udev crate failed to see all properties; + // - libudev crate segfaulted on the second property (SYSNUM ok, then segfault). + // falling back to parsing udevadm output :( + // + // The best fix would be to check what's wrong with udev crate. + + let entries = std::fs::read_dir(format!("/sys/class/{class}"))?; + for entry in entries { + let Ok(entry) = entry else { + continue; + }; + + let path = entry.path(); + let path = path.to_string_lossy(); + + let output = std::process::Command::new("udevadm") + .args(&["info", &format!("--path={path}")]) + .stderr(std::process::Stdio::piped()) + .output()?; + + if !output.status.success() { + error!("udevadm fail for {path}"); + continue; + } + + let output = String::from_utf8_lossy(&output.stdout); + + let name = entry.file_name(); + let dev = Device { + sysname: name.to_string_lossy().to_string(), + output: output.into_owned(), + }; + + devices.push(dev); + } + + Ok(devices) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e8d760a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,61 @@ +use log::error; +use std::collections::BTreeSet as Set; + +pub struct NameAliases { + name: String, + aliases: Set, +} + +impl NameAliases { + pub fn new(name: String) -> Self { + Self { + name, + aliases: Set::new(), + } + } + + pub fn name(&self) -> &str { + self.name.as_str() + } + pub fn aliases(&self) -> impl Iterator { + self.aliases.iter().map(|s| s.as_str()) + } + + pub fn iter(&self) -> impl Iterator { + std::iter::once(self.name()).chain(self.aliases()) + } + + pub fn push(&mut self, alias: String) { + if self.name == alias { + return; + } + self.aliases.insert(alias); + } +} + +pub fn select_n_by_regex<'t>( + n: usize, + regexs: &Vec, + nas: impl Iterator, +) -> Vec { + // compile regexs + let regexs: Vec<_> = (regexs.iter()) + .filter_map(|re| { + regex::Regex::new(re) + .inspect_err(|e| error!("invalid regex ignored: {re:?}: {e}")) + .ok() + }) + .collect(); + + let matching = |name| regexs.iter().any(|re| re.is_match(name)); + + let nas = nas + .filter(|na| na.iter().any(matching)) + .map(|na| na.name().to_string()); + + if n <= 0 { + nas.collect() + } else { + nas.take(n).collect() + } +} diff --git a/test-initrd/modules.sqfs b/test-initrd/modules.sqfs index 46a0317..c30f395 100644 Binary files a/test-initrd/modules.sqfs and b/test-initrd/modules.sqfs differ diff --git a/test-kernel b/test-kernel index fb09b86..8c090b4 100644 Binary files a/test-kernel and b/test-kernel differ