mirror of
https://github.com/dathere/ckan-devstaller.git
synced 2025-11-09 13:39:49 +00:00
feat: add improved interactivity, config customizability, disable DRUF
This commit is contained in:
parent
572cfab8ca
commit
3ab06a07ec
4 changed files with 349 additions and 190 deletions
|
|
@ -5,12 +5,8 @@
|
||||||
- [DataStore extension](https://docs.ckan.org/en/2.11/maintaining/datastore.html)
|
- [DataStore extension](https://docs.ckan.org/en/2.11/maintaining/datastore.html)
|
||||||
- [ckanext-scheming extension](https://github.com/ckan/ckanext-scheming)
|
- [ckanext-scheming extension](https://github.com/ckan/ckanext-scheming)
|
||||||
- [DataPusher+ extension](https://github.com/dathere/datapusher-plus)
|
- [DataPusher+ extension](https://github.com/dathere/datapusher-plus)
|
||||||
- [DRUF mode](https://github.com/dathere/datapusher-plus?tab=readme-ov-file#druf-dataset-resource-upload-first-workflow)
|
|
||||||
|
|
||||||
The [`datatablesview-plus` extension](https://github.com/dathere/ckanext-datatables-plus) is planned to be included in a future release.
|
[DRUF mode](https://github.com/dathere/datapusher-plus?tab=readme-ov-file#druf-dataset-resource-upload-first-workflow) is available but disabled by default. The [`datatablesview-plus` extension](https://github.com/dathere/ckanext-datatables-plus) is planned to be included in a future release.
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> We plan on including customizability for enabling/disabling features in a future release.
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
|
|
||||||
196
src/main.rs
196
src/main.rs
|
|
@ -1,6 +1,11 @@
|
||||||
|
mod questions;
|
||||||
|
mod steps;
|
||||||
mod styles;
|
mod styles;
|
||||||
|
|
||||||
use crate::styles::{highlighted_text, important_text, step_text, success_text};
|
use crate::{
|
||||||
|
questions::{question_ckan_version, question_ssh, question_sysadmin},
|
||||||
|
styles::{highlighted_text, important_text, step_text, success_text},
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use human_panic::{metadata, setup_panic};
|
use human_panic::{metadata, setup_panic};
|
||||||
|
|
@ -19,6 +24,22 @@ struct Args {
|
||||||
default: bool,
|
default: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Sysadmin {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
ssh: bool,
|
||||||
|
ckan_version: String,
|
||||||
|
sysadmin: Sysadmin,
|
||||||
|
extension_datastore: bool,
|
||||||
|
extension_ckanext_scheming: bool,
|
||||||
|
extension_datapusher_plus: bool,
|
||||||
|
druf_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
setup_panic!(metadata!()
|
setup_panic!(metadata!()
|
||||||
.homepage("https://dathere.com")
|
.homepage("https://dathere.com")
|
||||||
|
|
@ -26,33 +47,85 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
println!("Welcome to the ckan-devstaller!");
|
// Set up default config
|
||||||
println!(
|
|
||||||
"ckan-devstaller is provided by datHere - {}\n",
|
|
||||||
highlighted_text("https://datHere.com"),
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"This installer should assist in setting up {} from a source installation along with ckan-compose (https://github.com/tino097/ckan-compose). If you have any issues, please report them at https://github.com/dathere/ckan-devstaller/issues.",
|
|
||||||
highlighted_text("CKAN 2.11.3")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
important_text(
|
|
||||||
"This installer is only intended for a brand new installation of Ubuntu 22.04."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let ans = if args.default {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
Confirm::new("Would you like to begin the installation?")
|
|
||||||
.with_default(false)
|
|
||||||
.prompt()?
|
|
||||||
};
|
|
||||||
|
|
||||||
if ans {
|
|
||||||
let sh = Shell::new()?;
|
let sh = Shell::new()?;
|
||||||
let username = cmd!(sh, "whoami").read()?;
|
let username = cmd!(sh, "whoami").read()?;
|
||||||
|
steps::step_intro();
|
||||||
|
|
||||||
|
let default_config_text = r#"
|
||||||
|
The default configuration for ckan-devstaller does the following:
|
||||||
|
- Install openssh-server to enable SSH access
|
||||||
|
- Install ckan-compose (https://github.com/tino097/ckan-compose) which sets up the CKAN backend (PostgreSQL, SOLR, Redis)
|
||||||
|
- Install CKAN v2.11.3
|
||||||
|
- Install the DataStore extension
|
||||||
|
- Install the ckanext-scheming extension
|
||||||
|
- Install the DataPusher+ extension
|
||||||
|
- Disable DRUF mode for DataPusher+
|
||||||
|
"#;
|
||||||
|
println!("{default_config_text}");
|
||||||
|
let answer_customize = if args.default {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
Confirm::new(
|
||||||
|
"Would you like to customize any of these features for your CKAN installation?",
|
||||||
|
)
|
||||||
|
.prompt()?
|
||||||
|
};
|
||||||
|
let default_sysadmin = Sysadmin {
|
||||||
|
username: username.clone(),
|
||||||
|
password: "password".to_string(),
|
||||||
|
email: format!("{username}@localhost"),
|
||||||
|
};
|
||||||
|
let config = if answer_customize {
|
||||||
|
let answer_ssh = question_ssh()?;
|
||||||
|
let answer_ckan_version = question_ckan_version()?;
|
||||||
|
let answer_sysadmin = question_sysadmin(username.clone())?;
|
||||||
|
// let answer_extension_datastore = Confirm::new("Would you like to install the DataStore extension?")
|
||||||
|
// .with_default(true)
|
||||||
|
// .prompt()?;
|
||||||
|
// let answer_extension_ckanext_scheming = Confirm::new("Would you like to install the ckanext-scheming extension?")
|
||||||
|
// .with_default(true)
|
||||||
|
// .prompt()?;
|
||||||
|
let answer_extension_datapusher_plus =
|
||||||
|
Confirm::new("Would you like to install the DataPusher+ extension?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()?;
|
||||||
|
let answer_druf_mode = if answer_extension_datapusher_plus {
|
||||||
|
Confirm::new("Would you like to enable DRUF mode for DataPusher+?")
|
||||||
|
.with_default(false)
|
||||||
|
.prompt()?
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
Config {
|
||||||
|
ssh: answer_ssh,
|
||||||
|
ckan_version: answer_ckan_version,
|
||||||
|
sysadmin: answer_sysadmin,
|
||||||
|
extension_datastore: true,
|
||||||
|
extension_ckanext_scheming: true,
|
||||||
|
extension_datapusher_plus: answer_extension_datapusher_plus,
|
||||||
|
druf_mode: answer_druf_mode,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Config {
|
||||||
|
ssh: true,
|
||||||
|
ckan_version: "2.11.3".to_string(),
|
||||||
|
sysadmin: default_sysadmin,
|
||||||
|
extension_datastore: true,
|
||||||
|
extension_ckanext_scheming: true,
|
||||||
|
extension_datapusher_plus: true,
|
||||||
|
druf_mode: false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let begin_installation = if args.default {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Confirm::new("Would you like to begin the installation?").prompt()?
|
||||||
|
};
|
||||||
|
|
||||||
|
if begin_installation {
|
||||||
|
println!("{}", important_text("Starting installation..."));
|
||||||
println!(
|
println!(
|
||||||
"\n{} Running {} and {}...",
|
"\n{} Running {} and {}...",
|
||||||
step_text("1."),
|
step_text("1."),
|
||||||
|
|
@ -71,11 +144,20 @@ fn main() -> Result<()> {
|
||||||
success_text("✅ 1. Successfully ran update and upgrade commands.")
|
success_text("✅ 1. Successfully ran update and upgrade commands.")
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("\n{} Installing curl and enabling SSH...", step_text("2."));
|
println!(
|
||||||
cmd!(sh, "sudo apt install curl openssh-server -y").run()?;
|
"\n{} Installing {}...",
|
||||||
|
step_text("2."),
|
||||||
|
highlighted_text("curl")
|
||||||
|
);
|
||||||
|
cmd!(sh, "sudo apt install curl -y").run()?;
|
||||||
|
println!("{}", success_text("✅ 2.1. Successfully installed curl."));
|
||||||
|
if config.ssh {
|
||||||
|
println!("\n{} Installing openssh-server...", step_text("2."));
|
||||||
|
cmd!(sh, "sudo apt install openssh-server -y").run()?;
|
||||||
|
}
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
success_text("✅ 2. Successfully installed curl and enabled SSH.")
|
success_text("✅ 2.2. Successfully installed openssh-server.")
|
||||||
);
|
);
|
||||||
|
|
||||||
let dpkg_l_output = cmd!(sh, "dpkg -l").read()?;
|
let dpkg_l_output = cmd!(sh, "dpkg -l").read()?;
|
||||||
|
|
@ -129,7 +211,11 @@ POSTGRES_PASSWORD=pass";
|
||||||
cmd!(sh, "sudo ../ahoy up").run()?;
|
cmd!(sh, "sudo ../ahoy up").run()?;
|
||||||
println!("{}", success_text("✅ 5. Successfully ran ckan-compose."));
|
println!("{}", success_text("✅ 5. Successfully ran ckan-compose."));
|
||||||
|
|
||||||
println!("\n{} Installing CKAN 2.11.3...", step_text("6."),);
|
println!(
|
||||||
|
"\n{} Installing CKAN {}...",
|
||||||
|
step_text("6."),
|
||||||
|
config.ckan_version
|
||||||
|
);
|
||||||
cmd!(sh, "sudo apt install python3-dev libpq-dev python3-pip python3-venv git-core redis-server -y").run()?;
|
cmd!(sh, "sudo apt install python3-dev libpq-dev python3-pip python3-venv git-core redis-server -y").run()?;
|
||||||
cmd!(sh, "sudo mkdir -p /usr/lib/ckan/default").run()?;
|
cmd!(sh, "sudo mkdir -p /usr/lib/ckan/default").run()?;
|
||||||
cmd!(sh, "sudo chown {username} /usr/lib/ckan/default").run()?;
|
cmd!(sh, "sudo chown {username} /usr/lib/ckan/default").run()?;
|
||||||
|
|
@ -137,7 +223,11 @@ POSTGRES_PASSWORD=pass";
|
||||||
let venv = VirtualEnv::with_path(&sh, &venv_path)?;
|
let venv = VirtualEnv::with_path(&sh, &venv_path)?;
|
||||||
venv.pip_upgrade("pip")?;
|
venv.pip_upgrade("pip")?;
|
||||||
venv.pip_install(
|
venv.pip_install(
|
||||||
"git+https://github.com/ckan/ckan.git@ckan-2.11.3#egg=ckan[requirements]",
|
format!(
|
||||||
|
"git+https://github.com/ckan/ckan.git@ckan-{}#egg=ckan[requirements]",
|
||||||
|
config.ckan_version
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
)?;
|
)?;
|
||||||
cmd!(sh, "sudo mkdir -p /etc/ckan/default").run()?;
|
cmd!(sh, "sudo mkdir -p /etc/ckan/default").run()?;
|
||||||
cmd!(sh, "sudo chown -R {username} /etc/ckan/").run()?;
|
cmd!(sh, "sudo chown -R {username} /etc/ckan/").run()?;
|
||||||
|
|
@ -159,19 +249,25 @@ POSTGRES_PASSWORD=pass";
|
||||||
cmd!(sh, "sudo mkdir -p ckan/default").run()?;
|
cmd!(sh, "sudo mkdir -p ckan/default").run()?;
|
||||||
cmd!(sh, "sudo chown {username}.{username} ckan/default").run()?;
|
cmd!(sh, "sudo chown {username}.{username} ckan/default").run()?;
|
||||||
cmd!(sh, "ckan -c /etc/ckan/default/ckan.ini db init").run()?;
|
cmd!(sh, "ckan -c /etc/ckan/default/ckan.ini db init").run()?;
|
||||||
cmd!(sh, "ckan -c /etc/ckan/default/ckan.ini user add {username} password=password email={username}@localhost").run()?;
|
let sysadmin_username = config.sysadmin.username;
|
||||||
|
let sysadmin_password = config.sysadmin.password;
|
||||||
|
let sysadmin_email = config.sysadmin.email;
|
||||||
|
cmd!(sh, "ckan -c /etc/ckan/default/ckan.ini user add {sysadmin_username} password={sysadmin_password} email={sysadmin_email}").run()?;
|
||||||
cmd!(
|
cmd!(
|
||||||
sh,
|
sh,
|
||||||
"ckan -c /etc/ckan/default/ckan.ini sysadmin add {username}"
|
"ckan -c /etc/ckan/default/ckan.ini sysadmin add {sysadmin_username}"
|
||||||
)
|
)
|
||||||
.run()?;
|
.run()?;
|
||||||
println!("{}", success_text("✅ 6. Installed CKAN 2.11.3."));
|
println!(
|
||||||
|
"{}",
|
||||||
|
success_text(format!("✅ 6. Installed CKAN {}.", config.ckan_version).as_str())
|
||||||
|
);
|
||||||
|
|
||||||
|
if config.extension_datapusher_plus {
|
||||||
println!(
|
println!(
|
||||||
"\n{} Enabling DataStore plugin, adding config URLs in /etc/ckan/default/ckan.ini and updating permissions...",
|
"\n{} Enabling DataStore plugin, adding config URLs in /etc/ckan/default/ckan.ini and updating permissions...",
|
||||||
step_text("7."),
|
step_text("7."),
|
||||||
);
|
);
|
||||||
// TODO: use the ckan config-tool command instead of rust-ini
|
|
||||||
let mut conf = ini::Ini::load_from_file("/etc/ckan/default/ckan.ini")?;
|
let mut conf = ini::Ini::load_from_file("/etc/ckan/default/ckan.ini")?;
|
||||||
let app_main_section = conf.section_mut(Some("app:main")).unwrap();
|
let app_main_section = conf.section_mut(Some("app:main")).unwrap();
|
||||||
let mut ckan_plugins = app_main_section.get("ckan.plugins").unwrap().to_string();
|
let mut ckan_plugins = app_main_section.get("ckan.plugins").unwrap().to_string();
|
||||||
|
|
@ -316,7 +412,7 @@ ckanext.datapusher_plus.auto_unzip_one_file = true
|
||||||
ckanext.datapusher_plus.api_token = <CKAN service account token for CKAN user with sysadmin privileges>
|
ckanext.datapusher_plus.api_token = <CKAN service account token for CKAN user with sysadmin privileges>
|
||||||
ckanext.datapusher_plus.describeGPT_api_key = <Token for OpenAI API compatible service>
|
ckanext.datapusher_plus.describeGPT_api_key = <Token for OpenAI API compatible service>
|
||||||
ckanext.datapusher_plus.file_bin = /usr/bin/file
|
ckanext.datapusher_plus.file_bin = /usr/bin/file
|
||||||
ckanext.datapusher_plus.enable_druf = true
|
ckanext.datapusher_plus.enable_druf = false
|
||||||
ckanext.datapusher_plus.enable_form_redirect = true
|
ckanext.datapusher_plus.enable_form_redirect = true
|
||||||
"#;
|
"#;
|
||||||
std::fs::write("dpp_default_config.ini", dpp_default_config)?;
|
std::fs::write("dpp_default_config.ini", dpp_default_config)?;
|
||||||
|
|
@ -325,8 +421,9 @@ ckanext.datapusher_plus.enable_form_redirect = true
|
||||||
"ckan config-tool /etc/ckan/default/ckan.ini -f dpp_default_config.ini"
|
"ckan config-tool /etc/ckan/default/ckan.ini -f dpp_default_config.ini"
|
||||||
)
|
)
|
||||||
.run()?;
|
.run()?;
|
||||||
let resource_formats_str =
|
let resource_formats_str = std::fs::read_to_string(
|
||||||
std::fs::read_to_string("/usr/lib/ckan/default/src/ckan/config/resource_formats.json")?;
|
"/usr/lib/ckan/default/src/ckan/config/resource_formats.json",
|
||||||
|
)?;
|
||||||
let mut resource_formats_val: serde_json::Value =
|
let mut resource_formats_val: serde_json::Value =
|
||||||
serde_json::from_str(&resource_formats_str)?;
|
serde_json::from_str(&resource_formats_str)?;
|
||||||
let all_resource_formats = resource_formats_val
|
let all_resource_formats = resource_formats_val
|
||||||
|
|
@ -348,7 +445,7 @@ ckanext.datapusher_plus.enable_form_redirect = true
|
||||||
cmd!(sh, "sudo update-locale").run()?;
|
cmd!(sh, "sudo update-locale").run()?;
|
||||||
let token_command_output = cmd!(
|
let token_command_output = cmd!(
|
||||||
sh,
|
sh,
|
||||||
"ckan -c /etc/ckan/default/ckan.ini user token add {username} dpplus"
|
"ckan -c /etc/ckan/default/ckan.ini user token add {sysadmin_username} dpplus"
|
||||||
)
|
)
|
||||||
.read()?;
|
.read()?;
|
||||||
let tail_output = cmd!(sh, "tail -n 1").stdin(token_command_output).read()?;
|
let tail_output = cmd!(sh, "tail -n 1").stdin(token_command_output).read()?;
|
||||||
|
|
@ -363,28 +460,11 @@ ckanext.datapusher_plus.enable_form_redirect = true
|
||||||
"{}",
|
"{}",
|
||||||
success_text("✅ 8. Installed ckanext-scheming and DataPusher+ extensions.")
|
success_text("✅ 8. Installed ckanext-scheming and DataPusher+ extensions.")
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
println!("\n{}", success_text("✅ 9. Running CKAN instance..."));
|
println!("\n{}", success_text("✅ Running CKAN instance..."));
|
||||||
cmd!(sh, "ckan -c /etc/ckan/default/ckan.ini run").run()?;
|
cmd!(sh, "ckan -c /etc/ckan/default/ckan.ini run").run()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Config {
|
|
||||||
ssh: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_config_from_prompts() -> Result<Config> {
|
|
||||||
let ssh = Confirm::new("Would you like to enable SSH? (optional)")
|
|
||||||
.with_default(false)
|
|
||||||
.with_help_message(
|
|
||||||
format!(
|
|
||||||
"This step would install {}",
|
|
||||||
highlighted_text("openssh-server")
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.prompt()?;
|
|
||||||
Ok(Config { ssh })
|
|
||||||
}
|
|
||||||
|
|
|
||||||
64
src/questions.rs
Normal file
64
src/questions.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
use crate::{Sysadmin, styles::highlighted_text};
|
||||||
|
use anyhow::Result;
|
||||||
|
use inquire::{Confirm, Select, Text};
|
||||||
|
|
||||||
|
pub fn question_ssh() -> Result<bool> {
|
||||||
|
Ok(Confirm::new("Would you like to enable SSH? (optional)")
|
||||||
|
.with_default(false)
|
||||||
|
.with_help_message(
|
||||||
|
format!(
|
||||||
|
"This step would install {}",
|
||||||
|
highlighted_text("openssh-server")
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.prompt()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn question_ckan_version() -> Result<String> {
|
||||||
|
let ckan_version_options: Vec<&str> = vec!["2.11.3", "2.10.8", "Other"];
|
||||||
|
let answer_ckan_version = Select::new(
|
||||||
|
"What CKAN version would you like to install? (optional)",
|
||||||
|
ckan_version_options,
|
||||||
|
)
|
||||||
|
.with_help_message("We recommend using the latest compatible version of CKAN. Please do not choose 'Other' option unless for testing purposes as the CKAN version may not be supported and may cause a broken installation.")
|
||||||
|
.prompt()?;
|
||||||
|
if answer_ckan_version == "Other" {
|
||||||
|
Ok(
|
||||||
|
Text::new("What CKAN version would you like to install? (optional)")
|
||||||
|
.with_default("2.11.3")
|
||||||
|
.prompt()?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(answer_ckan_version.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn question_sysadmin(username: String) -> Result<Sysadmin> {
|
||||||
|
let configure_sysadmin = Confirm::new("Would you like to configure the sysadmin account for your CKAN instance?")
|
||||||
|
.with_help_message(format!("The following values are set as defaults for the sysadmin account:\n\n- Username: {username}\n- Password: password\n- Email: {username}@localhost\n").as_str())
|
||||||
|
.prompt()?;
|
||||||
|
if configure_sysadmin {
|
||||||
|
let username = Text::new("What should your sysadmin username be set to?")
|
||||||
|
.with_default(username.clone().as_str())
|
||||||
|
.prompt()?;
|
||||||
|
let password = Text::new("What should your sysadmin password be set to?")
|
||||||
|
.with_default("password")
|
||||||
|
.with_help_message("The password must be at least 8 characters long")
|
||||||
|
.prompt()?;
|
||||||
|
let email = Text::new("What should your sysadmin email be set to?")
|
||||||
|
.with_default(format!("{username}@localhost").as_str())
|
||||||
|
.prompt()?;
|
||||||
|
Ok(Sysadmin {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Sysadmin {
|
||||||
|
username: username.clone(),
|
||||||
|
password: "password".to_string(),
|
||||||
|
email: format!("{username}@localhost"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/steps.rs
Normal file
19
src/steps.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::styles::{highlighted_text, important_text};
|
||||||
|
|
||||||
|
pub fn step_intro() {
|
||||||
|
println!("Welcome to the ckan-devstaller!");
|
||||||
|
println!(
|
||||||
|
"ckan-devstaller is provided by datHere - {}\n",
|
||||||
|
highlighted_text("https://datHere.com"),
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"This installer should assist in setting up {} from a source installation along with ckan-compose. If you have any issues, please report them at https://support.dathere.com or https://github.com/dathere/ckan-devstaller/issues.",
|
||||||
|
highlighted_text("CKAN 2.11.3")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
important_text(
|
||||||
|
"This installer is only intended for a brand new installation of Ubuntu 22.04."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue