diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fa8d85ac52f19959d6fc9942c265708b4b3c2b04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..12a9c27e6617bbb90faa02b15936d1af8aec1b91 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "redox_installer" +version = "0.1.0" + +[[bin]] +name = "redox_installer" +path = "src/bin/installer.rs" + +[lib] +name = "redox_installer" +path = "src/lib.rs" + +[dependencies] +liner = "0.1" +rand = "0.3" +serde = "0.8" +serde_derive = "0.8" +termion = "1.1" +toml = { version = "0.2", default-features = false, features = ["serde"] } +userutils = { git = "https://github.com/redox-os/userutils.git" } diff --git a/README.md b/README.md index a24c902e054b60cd600c91f605fc3353918716c0..33f287c4cc2d9f45d7a04c2449d2675285834785 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ -# installer +# Redox OS installer + +The Redox installer will allow you to produce a Redox OS image. You will +be able to specify: +- Output device (raw image, ISO, QEMU, VirtualBox, drive) +- Filesystem +- Included packages +- Method of installation (from source, from binary) +- User accounts + +You will be prompted to install dependencies, based on your OS and method of +installation. The easiest method is to install from binaries. + +## Usage + +It is recommended to compile with `cargo`, in release mode: +```bash +cargo build --release +``` + +By default, you will be prompted to supply configuration options. You can +use the scripted mode by supplying a configuration file: +```bash +cargo run --release -- config/example.toml +``` +An example configuration can be found in [config/example.toml](./config/example.toml). +Unsuplied configuration will use the default. You can use the `general.prompt` +setting to prompt when configuration is not set. Multiple configurations can +be specified, they will be built in order. + +## Embedding + +The installer can also be used inside of other crates, as a library: + +```toml +# Cargo.toml +[dependencies] +redox_installer = "0.1" +``` + +```rust +// src/main.rs +extern crate redox_installer; + +use std::io; + +fn main() { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + let mut config = redox_installer::Config::default(); + ... + redox_installer::install(&mut stdout, &config); +} +``` diff --git a/config/example.toml b/config/example.toml new file mode 100644 index 0000000000000000000000000000000000000000..e8282196f3c4e9c7a9c24171c161d50c87a107ba --- /dev/null +++ b/config/example.toml @@ -0,0 +1,17 @@ +# This is an example configuration file + +# General settings +[general] +# Prompt if settings are not defined +prompt = true + +# Package settings +[packages] +orbutils = {} + +# User settings +[users.root] +uid = 0 +gid = 0 + +[users.user] diff --git a/src/bin/installer.rs b/src/bin/installer.rs new file mode 100644 index 0000000000000000000000000000000000000000..10fc634626884b2d229b920f98282d377fb9395d --- /dev/null +++ b/src/bin/installer.rs @@ -0,0 +1,65 @@ +extern crate redox_installer; +extern crate serde; +extern crate toml; + +use std::{env, process}; +use std::fs::File; +use std::io::{self, Read, Write}; + +fn main() { + let stderr = io::stderr(); + let mut stderr = stderr.lock(); + + let mut configs = vec![]; + for arg in env::args().skip(1) { + match File::open(&arg) { + Ok(mut config_file) => { + let mut config_data = String::new(); + match config_file.read_to_string(&mut config_data) { + Ok(_) => { + let mut parser = toml::Parser::new(&config_data); + match parser.parse() { + Some(parsed) => { + let mut decoder = toml::Decoder::new(toml::Value::Table(parsed)); + match serde::Deserialize::deserialize(&mut decoder) { + Ok(config) => { + configs.push(config); + }, + Err(err) => { + writeln!(stderr, "installer: {}: failed to decode: {}", arg, err).unwrap(); + process::exit(1); + } + } + }, + None => { + for error in parser.errors { + writeln!(stderr, "installer: {}: failed to parse: {}", arg, error).unwrap(); + } + process::exit(1); + } + } + }, + Err(err) => { + writeln!(stderr, "installer: {}: failed to read: {}", arg, err).unwrap(); + process::exit(1); + } + } + }, + Err(err) => { + writeln!(stderr, "installer: {}: failed to open: {}", arg, err).unwrap(); + process::exit(1); + } + } + } + + if configs.is_empty() { + configs.push(redox_installer::Config::default()); + } + + for config in configs { + if let Err(err) = redox_installer::install(config) { + writeln!(stderr, "installer: failed to install: {}", err).unwrap(); + process::exit(1); + } + } +} diff --git a/src/config/general.rs b/src/config/general.rs new file mode 100644 index 0000000000000000000000000000000000000000..f247293663df80547830aa2dd7c43582c61f1aeb --- /dev/null +++ b/src/config/general.rs @@ -0,0 +1,4 @@ +#[derive(Debug, Default, Deserialize)] +pub struct GeneralConfig { + pub prompt: bool +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..d9cee066e5b5e66d177514ba72759eab3bb42d03 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,12 @@ +use std::collections::BTreeMap; + +mod general; +mod package; +mod user; + +#[derive(Debug, Default, Deserialize)] +pub struct Config { + pub general: general::GeneralConfig, + pub packages: BTreeMap<String, package::PackageConfig>, + pub users: BTreeMap<String, user::UserConfig>, +} diff --git a/src/config/package.rs b/src/config/package.rs new file mode 100644 index 0000000000000000000000000000000000000000..2a01d77cffd0489cabc6c12125b0e9060bf2d834 --- /dev/null +++ b/src/config/package.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Default, Deserialize)] +pub struct PackageConfig { + pub version: String, + pub git: String, + pub path: String, +} diff --git a/src/config/user.rs b/src/config/user.rs new file mode 100644 index 0000000000000000000000000000000000000000..96113c68faf92fcd6871116d49a89ef2a7e6e7d9 --- /dev/null +++ b/src/config/user.rs @@ -0,0 +1,9 @@ +#[derive(Debug, Default, Deserialize)] +pub struct UserConfig { + pub password: Option<String>, + pub uid: Option<u32>, + pub gid: Option<u32>, + pub name: Option<String>, + pub home: Option<String>, + pub shell: Option<String>, +} diff --git a/src/install.rs b/src/install.rs new file mode 100644 index 0000000000000000000000000000000000000000..d17fce43781b15c892e08aa2b363e0d5accb30d8 --- /dev/null +++ b/src/install.rs @@ -0,0 +1,126 @@ +extern crate liner; +extern crate rand; +extern crate termion; +extern crate userutils; + +use self::rand::Rng; +use self::termion::input::TermRead; + +use std::io::{self, Write}; +use std::str::FromStr; + +use config::Config; + +fn unwrap_or_prompt<T: FromStr>(option: Option<T>, context: &mut liner::Context, prompt: &str) -> Result<T, String> { + match option { + None => { + let line = context.read_line(prompt, &mut |_| {}).map_err(|err| format!("failed to read line: {}", err))?; + T::from_str(&line).map_err(|_| format!("failed to parse '{}'", line)) + }, + Some(t) => Ok(t) + } +} + +fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<String, String> { + let stdin = io::stdin(); + let mut stdin = stdin.lock(); + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + stdout.write(prompt.as_bytes()).map_err(|err| format!("failed to write to stdout: {}", err))?; + stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?; + if let Some(password) = stdin.read_passwd(&mut stdout).map_err(|err| format!("failed to read password: {}", err))? { + stdout.write(b"\n").map_err(|err| format!("failed to write to stdout: {}", err))?; + stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?; + + if password.is_empty() { + Ok(password) + } else { + stdout.write(confirm_prompt.as_bytes()).map_err(|err| format!("failed to write to stdout: {}", err))?; + stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?; + if let Some(confirm_password) = stdin.read_passwd(&mut stdout).map_err(|err| format!("failed to read password: {}", err))? { + stdout.write(b"\n").map_err(|err| format!("failed to write to stdout: {}", err))?; + stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?; + + if confirm_password == password { + let salt = format!("{:X}", rand::OsRng::new().unwrap().next_u64()); + Ok(userutils::Passwd::encode(&password, &salt)) + } else { + Err("passwords do not match".to_string()) + } + } else { + Err("passwords do not match".to_string()) + } + } + } else { + Ok(String::new()) + } +} + +pub fn install(config: Config) -> Result<(), String> { + println!("Install {:#?}", config); + + let mut context = liner::Context::new(); + + macro_rules! prompt { + ($dst:expr, $($arg:tt)*) => (if config.general.prompt { + unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) + } else { + Ok($dst.unwrap_or_default()) + }) + } + + macro_rules! prompt_default { + ($dst:expr, $def:expr, $($arg:tt)*) => (if config.general.prompt { + match unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) { + Ok(res) => if res.is_empty() { + Ok($def) + } else { + Ok(res) + }, + Err(err) => Err(err) + } + } else { + Ok($def) + }) + } + + let mut passwd = String::new(); + + let mut next_uid = 1000; + for (username, user) in config.users { + let password = if let Some(password) = user.password { + password + } else if config.general.prompt { + prompt_password(&format!("{}: enter password: ", username), &format!("{}: confirm password: ", username))? + } else { + String::new() + }; + + let uid = user.uid.unwrap_or(next_uid); + + if uid >= next_uid { + next_uid = uid + 1; + } + + let gid = user.gid.unwrap_or(uid); + + let name = prompt_default!(user.name, username.clone(), "{}: name: ", username)?; + let home = prompt_default!(user.home, format!("/home/{}", username), "{}: home: ", username)?; + let shell = prompt_default!(user.shell, "/bin/ion".to_string(), "{}: shell: ", username)?; + + println!("Creating user {}:", username); + println!("\tPassword: {}", password); + println!("\tUID: {}", uid); + println!("\tGID: {}", gid); + println!("\tName: {}", name); + println!("\tHome: {}", home); + println!("\tShell: {}", shell); + + passwd.push_str(&format!("{};{};{};{};{};{};{}\n", username, password, uid, gid, name, home, shell)); + } + + print!("/etc/passwd:\n{}", passwd); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..2d6a50608d0c17904d5193dbedf3112b71bc155e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +#[macro_use] +extern crate serde_derive; + +pub use config::Config; +pub use install::install; + +mod config; +mod install;