From fdf6662391f87cda803be3c1787a31bbbcbae98b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 29 Feb 2024 11:26:46 +0100 Subject: [PATCH 01/98] Web server - support SSL (HTTPS) communication - Use either the cerfificate specified via command line arguments or generate a self-signed certificate - Redirect external HTTP requests to HTTPS - Allow HTTP for internal connections (http://localhost) - Optionally listen on a secondary address (to allow listening on both HTTP/80 and HTTPS/433 ports) --- rust/Cargo.lock | 91 ++++++-- rust/agama-server/Cargo.toml | 5 + rust/agama-server/src/agama-web-server.rs | 253 ++++++++++++++++++++-- rust/agama-server/src/cert.rs | 63 ++++++ rust/agama-server/src/lib.rs | 1 + rust/package/agama.changes | 13 ++ 6 files changed, 397 insertions(+), 29 deletions(-) create mode 100644 rust/agama-server/src/cert.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8bef766f82..c6ebe6c0ff 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -55,12 +55,16 @@ dependencies = [ "cidr", "clap", "config", + "futures-util", "gettext-rs", "http-body-util", + "hyper", + "hyper-util", "jsonwebtoken", "log", "macaddr", "once_cell", + "openssl", "pam", "rand", "regex", @@ -72,6 +76,7 @@ dependencies = [ "systemd-journal-logger", "thiserror", "tokio", + "tokio-openssl", "tokio-stream", "tower", "tower-http", @@ -1131,6 +1136,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1167,9 +1187,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" @@ -1204,9 +1224,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -1215,21 +1235,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-macro", @@ -1431,9 +1451,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", @@ -1445,6 +1465,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "smallvec", "tokio", ] @@ -2013,6 +2034,32 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -2021,9 +2068,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -2765,9 +2812,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" @@ -3010,6 +3057,18 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "tokio-openssl" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffab79df67727f6acf57f1ff743091873c24c579b1e2ce4d8f53e47ded4d63d" +dependencies = [ + "futures-util", + "openssl", + "openssl-sys", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 25cc8ccba5..cf379e9952 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -48,6 +48,11 @@ chrono = { version = "0.4.34", default-features = false, features = [ ] } pam = "0.8.0" serde_with = "3.6.1" +openssl = "0.10.64" +hyper = "1.2.0" +hyper-util = "0.1.3" +tokio-openssl = "0.6.4" +futures-util = { version = "0.3.30", default-features = false, features = ["alloc"] } [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index d8a16fc565..907257a658 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -1,11 +1,22 @@ -use std::process::{ExitCode, Termination}; - +use std::{path::PathBuf, pin::Pin}; +// use agama_lib::connection; use agama_dbus_server::{ l10n::helpers, web::{self, run_monitor}, }; +use axum::{ + http::{Request, Response}, + Router, +}; use clap::{Parser, Subcommand}; +use futures_util::pin_mut; +use hyper::body::Incoming; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod}; +use std::process::{ExitCode, Termination}; use tokio::sync::broadcast::channel; +use tokio_openssl::SslStream; +use tower::Service; use tracing_subscriber::prelude::*; use utoipa::OpenApi; @@ -13,10 +24,28 @@ use utoipa::OpenApi; enum Commands { /// Start the API server. Serve { - // Address to listen on (":::3000" listens for both IPv6 and IPv4 + // Address to listen to (":::3000" listens for both IPv6 and IPv4 // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only) - #[arg(long, default_value = ":::3000")] + #[arg(long, default_value = ":::3000", help = "Primary address to listen on")] address: String, + #[arg( + long, + default_value = "", + help = "Optional secondary address to listen to" + )] + address2: String, + #[arg( + long, + default_value = "", + help = "Path to the SSL private key file in PEM format" + )] + key: String, + #[arg( + long, + default_value = "", + help = "Path to the SSL certificate file in PEM format" + )] + cert: String, }, /// Display the API documentation in OpenAPI format. Openapi, @@ -32,23 +61,216 @@ struct Cli { pub command: Commands, } +// check whether the connection uses SSL or not +async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { + // a buffer for reading the first byte from the TCP connection + let mut buf = [0u8; 1]; + + // peek() receives the data without removing it from the stream, + // the data is not consumed, it will be read from the stream again later + stream.peek(&mut buf).await.unwrap(); + + // SSL3.0/TLS1.x starts with byte 0x16 + // SSL2 starts with 0x80 (but should not be used as it is considered) + // see https://stackoverflow.com/q/3897883 + // otherwise consider the stream as a plain HTTP stream possibly starting with + // "GET ... HTTP/1.1" or "POST ... HTTP/1.1" or a similar line + buf[0] == 0x16u8 || buf[0] == 0x80u8 +} + +// build a SSL acceptor using a provided SSL certificate or generate a self-signed one +fn create_ssl_acceptor(cert_file: &String, key_file: &String) -> SslAcceptor { + let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server()).unwrap(); + + if cert_file.is_empty() && key_file.is_empty() { + let (cert, key) = agama_dbus_server::cert::create_certificate().unwrap(); + tracing::info!("Generated self signed certificate: {:#?}", cert); + tls_builder.set_private_key(key.as_ref()).unwrap(); + tls_builder.set_certificate(cert.as_ref()).unwrap(); + + // for debugging you might dump the certificate to a file: + // use std::io::Write; + // let mut cert_file = std::fs::File::create("agama_cert.pem").unwrap(); + // let mut key_file = std::fs::File::create("agama_key.pem").unwrap(); + // cert_file.write_all(cert.to_pem().unwrap().as_ref()).unwrap(); + // key_file.write_all(key.private_key_to_pem_pkcs8().unwrap().as_ref()).unwrap(); + } else { + tracing::info!("Loading PEM certificate: {}", cert_file); + tls_builder + .set_certificate_file(PathBuf::from(cert_file), SslFiletype::PEM) + .unwrap(); + + tracing::info!("Loading PEM key: {}", key_file); + tls_builder + .set_private_key_file(PathBuf::from(key_file), SslFiletype::PEM) + .unwrap(); + } + + // check that the key belongs to the certificate + tls_builder.check_private_key().unwrap(); + + tls_builder.build() +} + +// build a response for the HTTP -> HTTPS redirection +// returns 308 permanent redirect +fn redirect_https(host: &str, uri: &hyper::Uri) -> Response { + let builder = Response::builder() + // build the redirection target URL + .header("Location", format!("https://{}{}", host, uri)) + .status(hyper::StatusCode::PERMANENT_REDIRECT); + + builder.body(String::from("")).unwrap() +} + +// build an error response for the HTTP -> HTTPS redirection when we cannot build +// the redirect response from the original request +// returns error 400 +fn redirect_error() -> Response { + let builder = Response::builder().status(hyper::StatusCode::BAD_REQUEST); + + let msg = "HTTP protocol is not allowed for external requests, please use HTTPS.\n"; + builder.body(String::from(msg)).unwrap() +} + +// build a router for the HTTP -> HTTPS redirection +// if the redirection URL cannot be built from the original request it returns error 400 +// instead of the redirection +fn https_redirect() -> Router { + // see https://docs.rs/axum/latest/axum/routing/struct.Router.html#example + let redirect_service = tower::service_fn(|req: axum::extract::Request| async move { + let request_host = req.headers().get("host"); + match request_host { + // missing host in the request header, we cannot build the redirection URL, return error 400 + None => return Ok(redirect_error()), + Some(host) => { + let host_string = host.to_str(); + match host_string { + // invalid host value in the request + Err(_) => return Ok(redirect_error()), + Ok(host_str) => return Ok(redirect_https(host_str, req.uri())), + } + } + } + }); + + Router::new() + .route_service( + // the wildcard path below does not match an empty path, we need to match it explicitly + "/", + redirect_service, + ) + .route_service("/*path", redirect_service) +} + +// start the web server +async fn start_server(address: String, service: Router, ssl_acceptor: SslAcceptor) { + tracing::info!("Starting Agama web server at {}", address); + + // see https://github.com/tokio-rs/axum/blob/main/examples/low-level-openssl/src/main.rs + // how to use axum with openSSL + let listener = tokio::net::TcpListener::bind(&address) + .await + .unwrap_or_else(|_| panic!("could not listen on {}", &address)); + + pin_mut!(listener); + + let redirector = https_redirect(); + + loop { + let tower_service = service.clone(); + let redirector_service = redirector.clone(); + let tls_acceptor = ssl_acceptor.clone(); + + // Wait for a new tcp connection + let (tcp_stream, addr) = listener.accept().await.unwrap(); + + tokio::spawn(async move { + if is_ssl_stream(&tcp_stream).await { + // handle HTTPS connection + let ssl = Ssl::new(tls_acceptor.context()).unwrap(); + let mut tls_stream = SslStream::new(ssl, tcp_stream).unwrap(); + if let Err(err) = SslStream::accept(Pin::new(&mut tls_stream)).await { + tracing::error!("Error during TSL handshake from {}: {}", addr, err); + } + + let stream = TokioIo::new(tls_stream); + let hyper_service = + hyper::service::service_fn(move |request: Request| { + tower_service.clone().call(request) + }); + + let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(stream, hyper_service) + .await; + + if let Err(err) = ret { + tracing::error!("Error serving connection from {}: {}", addr, err); + } + } else { + // handle HTTP connection + let stream = TokioIo::new(tcp_stream); + let hyper_service = + hyper::service::service_fn(move |request: Request| { + // check if it is local connection or external + // the to_canonical() converts IPv4-mapped IPv6 addresses + // to plain IPv4, then is_loopback() works correctly for the IPv4 connections + if addr.ip().to_canonical().is_loopback() { + // accept plain HTTP on the local connection + tower_service.clone().call(request) + } else { + // redirect external connections to HTTPS + redirector_service.clone().call(request) + } + }); + + let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(stream, hyper_service) + .await; + + if let Err(err) = ret { + tracing::error!("Error serving connection from {}: {}", addr, err); + } + } + }); + } +} + /// Start serving the API. -async fn serve_command(address: &str) -> anyhow::Result<()> { +async fn serve_command( + address: &String, + address2: &String, + cert: &String, + key: &String, +) -> anyhow::Result<()> { let journald = tracing_journald::layer().expect("could not connect to journald"); tracing_subscriber::registry().with(journald).init(); - let listener = tokio::net::TcpListener::bind(address) - .await - .unwrap_or_else(|_| panic!("could not listen on {}", address)); - let (tx, _) = channel(16); run_monitor(tx.clone()).await?; let config = web::ServiceConfig::load().unwrap(); let service = web::service(config, tx); - axum::serve(listener, service) - .await - .expect("could not mount app on listener"); + let ssl_acceptor = create_ssl_acceptor(cert, key); + + let mut servers = vec![]; + if !address.is_empty() { + servers.push(tokio::spawn(start_server( + address.clone(), + service.clone(), + ssl_acceptor.clone(), + ))); + } + if !address2.is_empty() { + servers.push(tokio::spawn(start_server( + address2.clone(), + service.clone(), + ssl_acceptor.clone(), + ))); + } + + futures_util::future::join_all(servers).await; + Ok(()) } @@ -60,7 +282,12 @@ fn openapi_command() -> anyhow::Result<()> { async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { - Commands::Serve { address } => serve_command(&address).await, + Commands::Serve { + address, + address2, + key, + cert, + } => serve_command(&address, &address2, &cert, &key).await, Commands::Openapi => openapi_command(), } } diff --git a/rust/agama-server/src/cert.rs b/rust/agama-server/src/cert.rs new file mode 100644 index 0000000000..f7a3ba4c21 --- /dev/null +++ b/rust/agama-server/src/cert.rs @@ -0,0 +1,63 @@ +use openssl::error::ErrorStack; +use openssl::pkey::{PKey, Private}; +use openssl::rsa::Rsa; +use openssl::x509::extension::{KeyUsage, SubjectAlternativeName}; +use openssl::x509::{X509NameBuilder, X509}; + +use openssl::asn1::Asn1Time; +use openssl::bn::{BigNum, MsbOption}; +use openssl::hash::MessageDigest; +use openssl::x509::extension::{BasicConstraints, SubjectKeyIdentifier}; + +// Generate a self-signed SSL certificate +// see https://github.com/sfackler/rust-openssl/blob/master/openssl/examples/mk_certs.rs +pub fn create_certificate() -> Result<(X509, PKey), ErrorStack> { + let rsa = Rsa::generate(2048)?; + let key = PKey::from_rsa(rsa)?; + + let mut x509_name = X509NameBuilder::new()?; + x509_name.append_entry_by_text("O", "Agama")?; + x509_name.append_entry_by_text("CN", "localhost")?; + let x509_name = x509_name.build(); + + let mut builder = X509::builder()?; + builder.set_version(2)?; + let serial_number = { + let mut serial = BigNum::new()?; + serial.rand(159, MsbOption::MAYBE_ZERO, false)?; + serial.to_asn1_integer()? + }; + builder.set_serial_number(&serial_number)?; + builder.set_subject_name(&x509_name)?; + builder.set_pubkey(&key)?; + + let not_before = Asn1Time::days_from_now(0)?; + builder.set_not_before(¬_before)?; + let not_after = Asn1Time::days_from_now(365)?; + builder.set_not_after(¬_after)?; + + builder.append_extension(BasicConstraints::new().critical().ca().build()?)?; + builder.append_extension( + KeyUsage::new() + .critical() + .key_cert_sign() + .crl_sign() + .build()? + )?; + + builder.append_extension( + SubjectAlternativeName::new() + .dns("agama") + .dns("agama.local") + .build(&builder.x509v3_context(None, None))? + )?; + + let subject_key_identifier = + SubjectKeyIdentifier::new().build(&builder.x509v3_context(None, None))?; + builder.append_extension(subject_key_identifier)?; + + builder.sign(&key, MessageDigest::sha256())?; + let cert = builder.build(); + + Ok((cert, key)) +} diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index 421b9e3efd..c848c85105 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -1,4 +1,5 @@ pub mod error; +pub mod cert; pub mod l10n; pub mod network; pub mod questions; diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 656cd074eb..1ec250c748 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,16 @@ +------------------------------------------------------------------- +Thu Feb 29 09:49:18 UTC 2024 - Ladislav Slezák + +- Web server: + - Accept also IPv6 connections + - Added SSL (HTTPS) support + - Use either the cerfificate specified via command line + arguments or generate a self-signed certificate + - Redirect external HTTP requests to HTTPS + - Allow HTTP for internal connections (http://localhost) + - Optionally listen on a secondary address + (to allow listening on both HTTP/80 and HTTPS/433 ports) + ------------------------------------------------------------------- Tue Feb 27 15:55:28 UTC 2024 - Imobach Gonzalez Sosa From 10a6500da4d56565eb95e96ae423a1e4c79f87ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 29 Feb 2024 13:35:07 +0100 Subject: [PATCH 02/98] Fixes by clippy --- rust/agama-server/src/agama-web-server.rs | 12 ++++++------ rust/agama-server/tests/common/mod.rs | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 907257a658..86632f8f0b 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -142,12 +142,12 @@ fn https_redirect() -> Router { let request_host = req.headers().get("host"); match request_host { // missing host in the request header, we cannot build the redirection URL, return error 400 - None => return Ok(redirect_error()), + None => Ok(redirect_error()), Some(host) => { let host_string = host.to_str(); match host_string { // invalid host value in the request - Err(_) => return Ok(redirect_error()), + Err(_) => Ok(redirect_error()), Ok(host_str) => return Ok(redirect_https(host_str, req.uri())), } } @@ -238,8 +238,8 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto /// Start serving the API. async fn serve_command( - address: &String, - address2: &String, + address: &str, + address2: &str, cert: &String, key: &String, ) -> anyhow::Result<()> { @@ -256,14 +256,14 @@ async fn serve_command( let mut servers = vec![]; if !address.is_empty() { servers.push(tokio::spawn(start_server( - address.clone(), + address.to_owned(), service.clone(), ssl_acceptor.clone(), ))); } if !address2.is_empty() { servers.push(tokio::spawn(start_server( - address2.clone(), + address2.to_owned(), service.clone(), ssl_acceptor.clone(), ))); diff --git a/rust/agama-server/tests/common/mod.rs b/rust/agama-server/tests/common/mod.rs index cd77539774..4525c96fa4 100644 --- a/rust/agama-server/tests/common/mod.rs +++ b/rust/agama-server/tests/common/mod.rs @@ -41,6 +41,12 @@ pub trait ServerState {} impl ServerState for Started {} impl ServerState for Stopped {} +impl Default for DBusServer { + fn default() -> Self { + Self::new() + } +} + impl DBusServer { pub fn new() -> Self { let uuid = Uuid::new_v4(); From 05b1b098a8ef9fae55cf0f7ef3f79094d6366b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 29 Feb 2024 13:37:25 +0100 Subject: [PATCH 03/98] fmt fixes --- rust/agama-server/src/cert.rs | 10 +++++----- rust/agama-server/src/lib.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/agama-server/src/cert.rs b/rust/agama-server/src/cert.rs index f7a3ba4c21..d2a3eb422f 100644 --- a/rust/agama-server/src/cert.rs +++ b/rust/agama-server/src/cert.rs @@ -42,14 +42,14 @@ pub fn create_certificate() -> Result<(X509, PKey), ErrorStack> { .critical() .key_cert_sign() .crl_sign() - .build()? + .build()?, )?; builder.append_extension( - SubjectAlternativeName::new() - .dns("agama") - .dns("agama.local") - .build(&builder.x509v3_context(None, None))? + SubjectAlternativeName::new() + .dns("agama") + .dns("agama.local") + .build(&builder.x509v3_context(None, None))?, )?; let subject_key_identifier = diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index c848c85105..da17034be4 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -1,5 +1,5 @@ -pub mod error; pub mod cert; +pub mod error; pub mod l10n; pub mod network; pub mod questions; From fb7cd41ab0d5281c11103662bd9fbe3097790887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 29 Feb 2024 14:04:32 +0100 Subject: [PATCH 04/98] Cleanup --- rust/agama-server/src/agama-web-server.rs | 28 ++++++++++------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 86632f8f0b..6e8665b48e 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -1,5 +1,3 @@ -use std::{path::PathBuf, pin::Pin}; -// use agama_lib::connection; use agama_dbus_server::{ l10n::helpers, web::{self, run_monitor}, @@ -14,6 +12,7 @@ use hyper::body::Incoming; use hyper_util::rt::{TokioExecutor, TokioIo}; use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod}; use std::process::{ExitCode, Termination}; +use std::{path::PathBuf, pin::Pin}; use tokio::sync::broadcast::channel; use tokio_openssl::SslStream; use tower::Service; @@ -24,14 +23,14 @@ use utoipa::OpenApi; enum Commands { /// Start the API server. Serve { - // Address to listen to (":::3000" listens for both IPv6 and IPv4 + // Address/port to listen on (":::3000" listens for both IPv6 and IPv4 // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only) #[arg(long, default_value = ":::3000", help = "Primary address to listen on")] address: String, #[arg( long, default_value = "", - help = "Optional secondary address to listen to" + help = "Optional secondary address to listen on" )] address2: String, #[arg( @@ -155,11 +154,8 @@ fn https_redirect() -> Router { }); Router::new() - .route_service( - // the wildcard path below does not match an empty path, we need to match it explicitly - "/", - redirect_service, - ) + // the wildcard path below does not match an empty path, we need to match it explicitly + .route_service("/", redirect_service) .route_service("/*path", redirect_service) } @@ -254,13 +250,13 @@ async fn serve_command( let ssl_acceptor = create_ssl_acceptor(cert, key); let mut servers = vec![]; - if !address.is_empty() { - servers.push(tokio::spawn(start_server( - address.to_owned(), - service.clone(), - ssl_acceptor.clone(), - ))); - } + servers.push(tokio::spawn(start_server( + address.to_owned(), + service.clone(), + ssl_acceptor.clone(), + ))); + + // optionally listen on the secondary address/port if !address2.is_empty() { servers.push(tokio::spawn(start_server( address2.to_owned(), From f64696557dd3d8457cbc373cd417f9697a9dd854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 1 Mar 2024 10:48:57 +0100 Subject: [PATCH 05/98] Documentation update --- rust/WEB-SERVER.md | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md index 75534d03ee..b92c900596 100644 --- a/rust/WEB-SERVER.md +++ b/rust/WEB-SERVER.md @@ -51,12 +51,26 @@ $ sudo ./target/debug/agama-web-server serve If it fails to compile, please check whether `clang-devel` and `pam-devel` are installed. -You can add a `--listen` flag if you want to use a different port: +By default the server uses port 3000 and listens on all network interfaces. You +can use the `--address` option if you want to use a different port or a specific +network interface: ``` -$ sudo ./target/debug/agama-web-server serve --listen 0.0.0.0:5678 +$ sudo ./target/debug/agama-web-server serve --address :::5678 ``` +Some more examples: + +- Both IPv6 and IPv4, all interfaces: `--address :::5678` +- Both IPv6 and IPv4, only local loopback : `--address ::1:5678` +- IPv4 only, all interfaces: `--address 0.0.0.0:5678` +- IPv4 only, only local loopback : `--address 127.0.0.1:5678` +- IPv4, only specific interface: `--address 192.168.1.2:5678` (use the IP + address of that interface) + +The server can optionally listen on a secondary address, use the `--address2` +option for that. + ## Trying the server You can check whether the server is up and running by just performing a ping: @@ -105,3 +119,28 @@ Now, you can use the following command to connect: $ websocat ws://localhost:3000/ws -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" ``` + +## SSL/TLS (HTTPS) Support + +The web server supports encrypted communication using the HTTPS protocol. + +The SSL certificate used by the server can be specified by the `--cert` and +`--key` command line options which should point to the PEM files: + +``` +$ sudo ./target/debug/agama-web-server serve --cert certificate.pem --key key.pem +``` +The certificate is expected in the PEM format, if you have a certificate in +another format you can convert it using the openSSL tools. + +If a SSL certificate is not specified via command line then the server generates +a self-signed certificate. Currently it is only kept in memory and generated +again at each start. + +The HTTPS protocol is required for external connections, the HTTP connections +are automatically redirected to HTTPS. *But it still means that the original +HTTP communication can be intercepted by an attacker, do not rely on this +redirection!* + +For internal connections coming from the same machine (via the +`http://localhost` URL) the unencrypted HTTP communication is allowed. From 43726235aca0b153339b971d6cea6035316e5b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 14:56:20 +0000 Subject: [PATCH 06/98] service: Export device sid in the proposal actions --- service/lib/agama/dbus/storage/proposal.rb | 1 + service/test/agama/dbus/storage/proposal_test.rb | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/service/lib/agama/dbus/storage/proposal.rb b/service/lib/agama/dbus/storage/proposal.rb index 2d55331f35..591d142451 100644 --- a/service/lib/agama/dbus/storage/proposal.rb +++ b/service/lib/agama/dbus/storage/proposal.rb @@ -155,6 +155,7 @@ def dbus_settings # @return [Hash] def to_dbus_action(action) { + "Device" => action.target_device.sid, "Text" => action.sentence, "Subvol" => action.device_is?(:btrfs_subvolume), "Delete" => action.delete? diff --git a/service/test/agama/dbus/storage/proposal_test.rb b/service/test/agama/dbus/storage/proposal_test.rb index 790014eeaa..fc61302129 100644 --- a/service/test/agama/dbus/storage/proposal_test.rb +++ b/service/test/agama/dbus/storage/proposal_test.rb @@ -257,14 +257,18 @@ let(:action1) do instance_double(Y2Storage::CompoundAction, - sentence: "test1", device_is?: false, delete?: false) + sentence: "test1", target_device: device1, device_is?: false, delete?: false) end let(:action2) do instance_double(Y2Storage::CompoundAction, - sentence: "test2", device_is?: true, delete?: true) + sentence: "test2", target_device: device2, device_is?: true, delete?: true) end + let(:device1) { instance_double(Y2Storage::Device, sid: 1) } + + let(:device2) { instance_double(Y2Storage::Device, sid: 2) } + it "returns a list with a hash for each action" do expect(subject.actions.size).to eq(2) expect(subject.actions).to all(be_a(Hash)) @@ -272,12 +276,14 @@ action1, action2 = subject.actions expect(action1).to eq({ + "Device" => 1, "Text" => "test1", "Subvol" => false, "Delete" => false }) expect(action2).to eq({ + "Device" => 2, "Text" => "test2", "Subvol" => true, "Delete" => true From ca3497862e26e275581ef8d7238728afac119b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 14:58:08 +0000 Subject: [PATCH 07/98] service: Export unused slots --- .../interfaces/device/partition_table.rb | 11 +++++++ .../device/partition_table_examples.rb | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb b/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb index 546ddfd934..97dcb90e25 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb @@ -60,11 +60,22 @@ def partition_table_partitions storage_device.partition_table.partitions.map { |p| tree.path_for(p) } end + # Available slots within a partition table, that is, the spaces that can be used to + # create a new partition. + # + # @return [Array] The first block and the size of each slot. + def partition_table_unused_slots + storage_device.partition_table.unused_partition_slots.map do |slot| + [slot.region.start, slot.region.size.to_i] + end + end + def self.included(base) base.class_eval do dbus_interface PARTITION_TABLE_INTERFACE do dbus_reader :partition_table_type, "s", dbus_name: "Type" dbus_reader :partition_table_partitions, "ao", dbus_name: "Partitions" + dbus_reader :partition_table_unused_slots, "a(tt)", dbus_name: "UnusedSlots" end end end diff --git a/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb b/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb index 1af30111ab..22fb9efae1 100644 --- a/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb @@ -20,6 +20,9 @@ # find current contact information at www.suse.com. require_relative "../../../../../test_helper" +require "y2storage/disk_size" +require "y2storage/partition_tables/partition_slot" +require "y2storage/region" shared_examples "PartitionTable interface" do describe "PartitionTable D-Bus interface" do @@ -39,5 +42,34 @@ expect(subject.partition_table_partitions).to contain_exactly(tree.path_for(md0p1)) end end + + describe "#partition_table_unused_slots" do + before do + allow(device).to receive(:partition_table).and_return(partition_table) + allow(partition_table).to receive(:unused_partition_slots).and_return(unused_slots) + end + + let(:partition_table) { device.partition_table } + + let(:unused_slots) do + [ + instance_double(Y2Storage::PartitionTables::PartitionSlot, region: region1), + instance_double(Y2Storage::PartitionTables::PartitionSlot, region: region2) + ] + end + + let(:region1) do + instance_double(Y2Storage::Region, start: 234, size: Y2Storage::DiskSize.new(1024)) + end + + let(:region2) do + instance_double(Y2Storage::Region, start: 987, size: Y2Storage::DiskSize.new(2048)) + end + + it "returns the information about the unused slots" do + md0p1 = devicegraph.find_by_name("/dev/md0p1") + expect(subject.partition_table_unused_slots).to contain_exactly([234, 1024], [987, 2048]) + end + end end end From 02a93e9761e5876a77d0d3653f8527163e9e644f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 14:58:57 +0000 Subject: [PATCH 08/98] service: Export more info about block devices --- .../dbus/storage/interfaces/device/block.rb | 16 ++++++++++++++++ .../storage/interfaces/device/block_examples.rb | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/service/lib/agama/dbus/storage/interfaces/device/block.rb b/service/lib/agama/dbus/storage/interfaces/device/block.rb index b5c0e00017..fa7c3e301b 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/block.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/block.rb @@ -51,6 +51,13 @@ def block_name storage_device.name end + # Position of the first block of the region. + # + # @return [Integer] + def block_start + storage_device.start + end + # Whether the block device is currently active # # @return [Boolean] @@ -58,6 +65,13 @@ def block_active storage_device.active? end + # Whether the block device is encrypted. + # + # @return [Boolean] + def block_encrypted + storage_device.encrypted? + end + # Name of the udev by-id links # # @return [Array] @@ -100,7 +114,9 @@ def self.included(base) base.class_eval do dbus_interface BLOCK_INTERFACE do dbus_reader :block_name, "s", dbus_name: "Name" + dbus_reader :block_start, "t", dbus_name: "Start" dbus_reader :block_active, "b", dbus_name: "Active" + dbus_reader :block_encrypted, "b", dbus_name: "Encrypted" dbus_reader :block_udev_ids, "as", dbus_name: "UdevIds" dbus_reader :block_udev_paths, "as", dbus_name: "UdevPaths" dbus_reader :block_size, "t", dbus_name: "Size" diff --git a/service/test/agama/dbus/storage/interfaces/device/block_examples.rb b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb index 11590a3390..6211a2d913 100644 --- a/service/test/agama/dbus/storage/interfaces/device/block_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb @@ -33,6 +33,16 @@ end end + describe "#block_start" do + before do + allow(device).to receive(:start).and_return(345) + end + + it "returns the first block of the region" do + expect(subject.block_start).to eq(345) + end + end + describe "#block_active" do before do allow(device).to receive(:active?).and_return(true) @@ -43,6 +53,12 @@ end end + describe "#block_encrypted" do + it "returns whether the device is encrypted" do + expect(subject.block_encrypted).to eq(false) + end + end + describe "#block_udev_ids" do before do allow(device).to receive(:udev_ids).and_return(udev_ids) From 26fb99da1cf56aae7b3f44a9cf782b59f5d00884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 15:02:14 +0000 Subject: [PATCH 09/98] service: Export LVM devices --- .../lib/agama/dbus/storage/devices_tree.rb | 9 +- .../agama/dbus/storage/interfaces/device.rb | 1 + .../storage/interfaces/device/component.rb | 2 + .../dbus/storage/interfaces/device/lvm_vg.rb | 90 +++++++++++++++++++ .../test/agama/dbus/storage/device_test.rb | 19 +++- .../interfaces/device/lvm_vg_examples.rb | 64 +++++++++++++ service/test/fixtures/trivial_lvm.yml | 24 +++++ 7 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb create mode 100644 service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb create mode 100644 service/test/fixtures/trivial_lvm.yml diff --git a/service/lib/agama/dbus/storage/devices_tree.rb b/service/lib/agama/dbus/storage/devices_tree.rb index c06501bd1e..a930c7840b 100644 --- a/service/lib/agama/dbus/storage/devices_tree.rb +++ b/service/lib/agama/dbus/storage/devices_tree.rb @@ -70,13 +70,16 @@ def dbus_object?(dbus_object, device) # Right now, only the required information for calculating a proposal is exported, that is: # * Potential candidate devices (i.e., disk devices, MDs). # * Partitions of the candidate devices in order to indicate how to find free space. - # - # TODO: export LVM VGs and file systems of directly formatted devices. + # * LVM volume groups and logical volumes. # # @param devicegraph [Y2Storage::Devicegraph] # @return [Array] def devices(devicegraph) - devices = devicegraph.disk_devices + devicegraph.software_raids + devices = devicegraph.disk_devices + + devicegraph.software_raids + + devicegraph.lvm_vgs + + devicegraph.lvm_lvs + devices + partitions_from(devices) end diff --git a/service/lib/agama/dbus/storage/interfaces/device.rb b/service/lib/agama/dbus/storage/interfaces/device.rb index 98f09690c2..f721da72ef 100644 --- a/service/lib/agama/dbus/storage/interfaces/device.rb +++ b/service/lib/agama/dbus/storage/interfaces/device.rb @@ -35,6 +35,7 @@ module Device require "agama/dbus/storage/interfaces/device/component" require "agama/dbus/storage/interfaces/device/drive" require "agama/dbus/storage/interfaces/device/filesystem" +require "agama/dbus/storage/interfaces/device/lvm_vg" require "agama/dbus/storage/interfaces/device/md" require "agama/dbus/storage/interfaces/device/multipath" require "agama/dbus/storage/interfaces/device/partition_table" diff --git a/service/lib/agama/dbus/storage/interfaces/device/component.rb b/service/lib/agama/dbus/storage/interfaces/device/component.rb index c66422b2b4..a4de9ce3e3 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/component.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/component.rb @@ -84,6 +84,8 @@ def self.included(base) base.class_eval do dbus_interface COMPONENT_INTERFACE do dbus_reader :component_type, "s", dbus_name: "Type" + # The names are provided just in case the device is component of a device that + # is not exported yet (e.g., Bcache devices). dbus_reader :component_device_names, "as", dbus_name: "DeviceNames" dbus_reader :component_devices, "ao", dbus_name: "Devices" end diff --git a/service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb b/service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb new file mode 100644 index 0000000000..b69b84a5f5 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for a LVM Volume Group. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module LvmVg + # Whether this interface should be implemented for the given device. + # + # @note LVM Volume Groups implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:lvm_vg) + end + + VOLUME_GROUP_INTERFACE = "org.opensuse.Agama.Storage1.LVM.VolumeGroup" + private_constant :VOLUME_GROUP_INTERFACE + + # Name of the volume group + # + # @return [String] e.g., "/dev/mapper/vg0" + def lvm_vg_name + storage_device.name + end + + # Size of the volume group in bytes + # + # @return [Integer] + def lvm_vg_size + storage_device.size.to_i + end + + # D-Bus paths of the objects representing the physical volumes. + # + # @return [Array] + def lvm_vg_pvs + storage_device.lvm_pvs.map { |p| tree.path_for(p.plain_blk_device) } + end + + # D-Bus paths of the objects representing the logical volumes. + # + # @return [Array] + def lvm_vg_lvs + storage_device.lvm_lvs.map { |l| tree.path_for(l) } + end + + def self.included(base) + base.class_eval do + dbus_interface VOLUME_GROUP_INTERFACE do + dbus_reader :lvm_vg_name, "s", dbus_name: "Name" + dbus_reader :lvm_vg_size, "t", dbus_name: "Size" + dbus_reader :lvm_vg_pvs, "ao", dbus_name: "PhysicalVolumes" + dbus_reader :lvm_vg_lvs, "ao", dbus_name: "LogicalVolumes" + end + end + end + end + end + end + end + end +end diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index 649131a83f..457951fadb 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -19,19 +19,20 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/dbus/storage/device" -require "agama/dbus/storage/devices_tree" -require "dbus" require_relative "../../../test_helper" require_relative "../../storage/storage_helpers" require_relative "./interfaces/device/block_examples" require_relative "./interfaces/device/component_examples" require_relative "./interfaces/device/drive_examples" require_relative "./interfaces/device/filesystem_examples" +require_relative "./interfaces/device/lvm_vg_examples" require_relative "./interfaces/device/md_examples" require_relative "./interfaces/device/multipath_examples" require_relative "./interfaces/device/partition_table_examples" require_relative "./interfaces/device/raid_examples" +require "agama/dbus/storage/device" +require "agama/dbus/storage/devices_tree" +require "dbus" describe Agama::DBus::Storage::Device do include Agama::RSpec::StorageHelpers @@ -110,6 +111,16 @@ end end + context "when the given device is a LVM volume group" do + let(:scenario) { "trivial_lvm.yml" } + + let(:device) { devicegraph.find_by_name("/dev/vg0") } + + it "defines the LVM.VolumeGroup interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.LVM.VolumeGroup") + end + end + context "when the given device has a partition table" do let(:scenario) { "partitioned_md.yml" } @@ -142,6 +153,8 @@ include_examples "Block interface" + include_examples "LVM.VolumeGroup interface" + include_examples "PartitionTable interface" include_examples "Filesystem interface" diff --git a/service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb b/service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb new file mode 100644 index 0000000000..ad646ca618 --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../../../test_helper" + +shared_examples "LVM.VolumeGroup interface" do + describe "LVM.VolumeGroup D-Bus interface" do + let(:scenario) { "trivial_lvm.yml" } + + let(:device) { devicegraph.find_by_name("/dev/vg0") } + + describe "#lvm_vg_name" do + it "returns the name of the volume group" do + expect(subject.lvm_vg_name).to eq("/dev/vg0") + end + end + + describe "#lvm_vg_size" do + before do + allow(device).to receive(:size).and_return(size) + end + + let(:size) { Y2Storage::DiskSize.new(1024) } + + it "returns the size in bytes" do + expect(subject.lvm_vg_size).to eq(1024) + end + end + + describe "#lvm_vg_pvs" do + it "returns the D-Bus path of the physical volumes" do + sda1 = devicegraph.find_by_name("/dev/sda1") + + expect(subject.lvm_vg_pvs).to contain_exactly(tree.path_for(sda1)) + end + end + + describe "#lvm_vg_lvs" do + it "returns the D-Bus path of the logical volumes" do + lv1 = devicegraph.find_by_name("/dev/vg0/lv1") + + expect(subject.lvm_vg_lvs).to contain_exactly(tree.path_for(lv1)) + end + end + end +end diff --git a/service/test/fixtures/trivial_lvm.yml b/service/test/fixtures/trivial_lvm.yml new file mode 100644 index 0000000000..86f3fc904e --- /dev/null +++ b/service/test/fixtures/trivial_lvm.yml @@ -0,0 +1,24 @@ +--- +- disk: + name: /dev/sda + size: 200 GiB + partition_table: gpt + partitions: + + - partition: + size: unlimited + name: /dev/sda1 + id: lvm + +- lvm_vg: + vg_name: vg0 + lvm_pvs: + - lvm_pv: + blk_device: /dev/sda1 + + lvm_lvs: + - lvm_lv: + size: unlimited + lv_name: lv1 + file_system: btrfs + mount_point: / From 36ff75ae49f225721d45d8dce3909892d3719ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 15:06:21 +0000 Subject: [PATCH 10/98] service: Avoid updating D-Bus nodes of devices tree --- service/lib/agama/dbus/base_tree.rb | 9 ++++++-- service/lib/agama/dbus/storage/device.rb | 20 ++++++++++++++---- .../lib/agama/dbus/storage/devices_tree.rb | 21 +++++++++++++------ .../test/agama/dbus/storage/device_test.rb | 6 ------ 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/service/lib/agama/dbus/base_tree.rb b/service/lib/agama/dbus/base_tree.rb index db28aa4877..a699f03bf3 100644 --- a/service/lib/agama/dbus/base_tree.rb +++ b/service/lib/agama/dbus/base_tree.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -47,11 +47,16 @@ def initialize(service, root_path, logger: nil) # # @param objects [Array] def objects=(objects) - try_add_objects(objects) try_update_objects(objects) + try_add_objects(objects) try_delete_objects(objects) end + # Unexports the current D-Bus objects of this tree. + def clean + dbus_objects.each { |o| service.unexport(o) } + end + private # @return [::DBus::ObjectServer] diff --git a/service/lib/agama/dbus/storage/device.rb b/service/lib/agama/dbus/storage/device.rb index 4e1b07a206..17ecbe167e 100644 --- a/service/lib/agama/dbus/storage/device.rb +++ b/service/lib/agama/dbus/storage/device.rb @@ -30,8 +30,15 @@ module Storage # # The D-Bus object includes the required interfaces for the storage object that it represents. class Device < BaseObject - # @return [Y2Storage::Device] - attr_reader :storage_device + # sid of the Y2Storage device. + # + # @note A Y2Storage device is a wrapper over a libstorage-ng object. If the source + # devicegraph does not exist anymore (e.g., after reprobing), then the Y2Storage device + # object cannot be used (memory error). The device sid is stored to avoid accessing to + # the old Y2Storage device when updating the represented device, see {#storage_device=}. + # + # @return [Integer] + attr_reader :sid # Constructor # @@ -43,6 +50,7 @@ def initialize(storage_device, path, tree, logger: nil) super(path, logger: logger) @storage_device = storage_device + @sid = storage_device.sid @tree = tree add_interfaces end @@ -54,12 +62,13 @@ def initialize(storage_device, path, tree, logger: nil) # # @param value [Y2Storage::Device] def storage_device=(value) - if value.sid != storage_device.sid + if value.sid != sid raise "Cannot update the D-Bus object because the given device has a different sid: " \ - "#{value} instead of #{storage_device.sid}" + "#{value} instead of #{sid}" end @storage_device = value + @sid = value.sid interfaces_and_properties.each do |interface, properties| dbus_properties_changed(interface, properties, []) @@ -71,6 +80,9 @@ def storage_device=(value) # @return [DevicesTree] attr_reader :tree + # @return [Y2Storage::Device] + attr_reader :storage_device + # Adds the required interfaces according to the storage object. def add_interfaces interfaces = Interfaces::Device.constants diff --git a/service/lib/agama/dbus/storage/devices_tree.rb b/service/lib/agama/dbus/storage/devices_tree.rb index a930c7840b..c49a0d3d73 100644 --- a/service/lib/agama/dbus/storage/devices_tree.rb +++ b/service/lib/agama/dbus/storage/devices_tree.rb @@ -36,10 +36,19 @@ def path_for(device) ::DBus::ObjectPath.new(File.join(root_path, device.sid.to_s)) end - # Updates the D-Bus tree according to the given devicegraph + # Updates the D-Bus tree according to the given devicegraph. + # + # @note In the devices tree it is important to avoid updating D-Bus nodes. Note that an + # already exported D-Bus object could require to add or remove interfaces (e.g., an + # existing partition needs to add the Filesystem interface after formatting the + # partition). Dynamically adding or removing intefaces is not possible with ruby-dbus + # once the object is exported on D-Bus. + # + # Updating the currently exported D-Bus objects is avoided by calling to {#clean} first. # # @param devicegraph [Y2Storage::Devicegraph] def update(devicegraph) + clean self.objects = devices(devicegraph) end @@ -52,17 +61,17 @@ def create_dbus_object(device) end # @see BaseTree - # @param dbus_object [Device] - # @param device [Y2Storage::Device] - def update_dbus_object(dbus_object, device) - dbus_object.storage_device = device + # + # @note D-Bus objects representing devices cannot be safely updated, see {#update}. + def update_dbus_object(_dbus_object, _device) + nil end # @see BaseTree # @param dbus_object [Device] # @param device [Y2Storage::Device] def dbus_object?(dbus_object, device) - dbus_object.storage_device.sid == device.sid + dbus_object.sid == device.sid end # Devices to be exported. diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index 457951fadb..f67ecaaa8a 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -181,12 +181,6 @@ context "if the given device has the same sid" do let(:new_device) { devicegraph.find_by_name("/dev/sda") } - it "sets the new device" do - subject.storage_device = new_device - - expect(subject.storage_device).to equal(new_device) - end - it "emits a properties changed signal for each interface" do subject.interfaces_and_properties.each_key do |interface| expect(subject).to receive(:dbus_properties_changed).with(interface, anything, anything) From fa1ace321f1b2f14f70a119cb96fcf24da3a4d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 15:07:11 +0000 Subject: [PATCH 11/98] service: Export staging devices --- service/lib/agama/dbus/storage/manager.rb | 12 ++- .../agama/dbus/storage/devices_tree_test.rb | 82 +++++++++---------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 188062579c..3d9380569d 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -289,6 +289,7 @@ def register_proposal_callbacks proposal.on_calculate do export_proposal proposal_properties_changed + refresh_staging_devices end end @@ -332,6 +333,11 @@ def refresh_system_devices system_devices_tree.update(devicegraph) end + def refresh_staging_devices + devicegraph = Y2Storage::StorageManager.instance.staging + staging_devices_tree.update(devicegraph) + end + def refresh_iscsi_nodes nodes = backend.iscsi.nodes iscsi_nodes_tree.update(nodes) @@ -348,6 +354,10 @@ def system_devices_tree @system_devices_tree ||= DevicesTree.new(@service, tree_path("system"), logger: logger) end + def staging_devices_tree + @staging_devices_tree ||= DevicesTree.new(@service, tree_path("staging"), logger: logger) + end + def tree_path(tree_root) File.join(PATH, tree_root) end diff --git a/service/test/agama/dbus/storage/devices_tree_test.rb b/service/test/agama/dbus/storage/devices_tree_test.rb index 42e0024a33..a876c2f77d 100644 --- a/service/test/agama/dbus/storage/devices_tree_test.rb +++ b/service/test/agama/dbus/storage/devices_tree_test.rb @@ -22,8 +22,8 @@ require_relative "../../../test_helper" require_relative "../../storage/storage_helpers" require "agama/dbus/storage/devices_tree" -require "y2storage" require "dbus" +require "y2storage" describe Agama::DBus::Storage::DevicesTree do include Agama::RSpec::StorageHelpers @@ -87,7 +87,8 @@ mock_storage(devicegraph: scenario) allow(service).to receive(:get_node).with(root_path, anything).and_return(root_node) - allow(root_node).to receive(:descendant_objects).and_return(dbus_objects) + # Returning an empty list for the second call to mock the effect of calling to #clear. + allow(root_node).to receive(:descendant_objects).and_return(dbus_objects, []) allow(service).to receive(:export) allow(service).to receive(:unexport) @@ -102,57 +103,48 @@ let(:devicegraph) { Y2Storage::StorageManager.instance.probed } - context "if a device is not exported yet" do - let(:dbus_objects) { [] } + let(:dbus_objects) { [dbus_object1, dbus_object2] } + let(:dbus_object1) { Agama::DBus::Storage::Device.new(sda, subject.path_for(sda), subject) } + let(:dbus_object2) { Agama::DBus::Storage::Device.new(sdb, subject.path_for(sdb), subject) } + let(:sda) { devicegraph.find_by_name("/dev/sda") } + let(:sdb) { devicegraph.find_by_name("/dev/sdb") } - it "exports a D-Bus object" do - sda = devicegraph.find_by_name("/dev/sda") - sdb = devicegraph.find_by_name("/dev/sdb") - md0 = devicegraph.find_by_name("/dev/md0") - sda1 = devicegraph.find_by_name("/dev/sda1") - sda2 = devicegraph.find_by_name("/dev/sda2") - md0p1 = devicegraph.find_by_name("/dev/md0p1") - - expect(service).to export_object("#{root_path}/#{sda.sid}") - expect(service).to export_object("#{root_path}/#{sdb.sid}") - expect(service).to export_object("#{root_path}/#{md0.sid}") - expect(service).to export_object("#{root_path}/#{sda1.sid}") - expect(service).to export_object("#{root_path}/#{sda2.sid}") - expect(service).to export_object("#{root_path}/#{md0p1.sid}") - expect(service).to_not receive(:export) + it "unexports the current D-Bus objects" do + expect(service).to unexport_object("#{root_path}/#{sda.sid}") + expect(service).to unexport_object("#{root_path}/#{sdb.sid}") - subject.update(devicegraph) - end + subject.update(devicegraph) end - context "if a device is already exported" do - let(:dbus_objects) { [dbus_object1] } - let(:dbus_object1) { Agama::DBus::Storage::Device.new(sda, subject.path_for(sda), subject) } - let(:sda) { devicegraph.find_by_name("/dev/sda") } - - it "does not export a D-Bus object" do - expect(service).to_not export_object("#{root_path}/#{sda.sid}") - - subject.update(devicegraph) - end - - it "updates the D-Bus object" do - expect(dbus_object1.storage_device).to equal(sda) + it "exports disk devices and partitions" do + md0 = devicegraph.find_by_name("/dev/md0") + sda1 = devicegraph.find_by_name("/dev/sda1") + sda2 = devicegraph.find_by_name("/dev/sda2") + md0p1 = devicegraph.find_by_name("/dev/md0p1") + + expect(service).to export_object("#{root_path}/#{sda.sid}") + expect(service).to export_object("#{root_path}/#{sdb.sid}") + expect(service).to export_object("#{root_path}/#{md0.sid}") + expect(service).to export_object("#{root_path}/#{sda1.sid}") + expect(service).to export_object("#{root_path}/#{sda2.sid}") + expect(service).to export_object("#{root_path}/#{md0p1.sid}") + expect(service).to_not receive(:export) + + subject.update(devicegraph) + end - subject.update(devicegraph) + context "if there are LVM volume groups" do + let(:scenario) { "trivial_lvm.yml" } - expect(dbus_object1.storage_device).to_not equal(sda) - expect(dbus_object1.storage_device.sid).to equal(sda.sid) - end - end + let(:dbus_objects) { [] } - context "if an exported D-Bus object does not represent any of the current devices" do - let(:dbus_objects) { [dbus_object1] } - let(:dbus_object1) { Agama::DBus::Storage::Device.new(sdd, subject.path_for(sdd), subject) } - let(:sdd) { instance_double(Y2Storage::Disk, sid: 1, is?: false, filesystem: false) } + it "exports the LVM volume groups and the logical volumes" do + vg0 = devicegraph.find_by_name("/dev/vg0") + lv1 = devicegraph.find_by_name("/dev/vg0/lv1") - it "unexports the D-Bus object" do - expect(service).to unexport_object("#{root_path}/1") + expect(service).to receive(:export) + expect(service).to export_object("#{root_path}/#{vg0.sid}") + expect(service).to export_object("#{root_path}/#{lv1.sid}") subject.update(devicegraph) end From d43bb46715e8e033fd0f1f09e523615f14ae0b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 4 Mar 2024 16:12:04 +0000 Subject: [PATCH 12/98] service: Fix rubocop config - agama.gemspec was renamed, see https://github.com/openSUSE/agama/pull/1056. - The file name was not updated in the rubocop config. --- service/.rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/.rubocop.yml b/service/.rubocop.yml index 137ea30537..ada6a9f15e 100644 --- a/service/.rubocop.yml +++ b/service/.rubocop.yml @@ -8,7 +8,7 @@ AllCops: Exclude: - vendor/**/* - lib/agama/dbus/y2dir/**/* - - agama.gemspec + - agama-yast.gemspec - package/*.spec # a D-Bus method definition may take up more line lenght than usual From 8128fd7381ba2bd7416940319d9344e163312cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 16:11:46 +0000 Subject: [PATCH 13/98] service: Fix typo --- service/lib/agama/dbus/storage/devices_tree.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/agama/dbus/storage/devices_tree.rb b/service/lib/agama/dbus/storage/devices_tree.rb index c49a0d3d73..000bc17255 100644 --- a/service/lib/agama/dbus/storage/devices_tree.rb +++ b/service/lib/agama/dbus/storage/devices_tree.rb @@ -41,7 +41,7 @@ def path_for(device) # @note In the devices tree it is important to avoid updating D-Bus nodes. Note that an # already exported D-Bus object could require to add or remove interfaces (e.g., an # existing partition needs to add the Filesystem interface after formatting the - # partition). Dynamically adding or removing intefaces is not possible with ruby-dbus + # partition). Dynamically adding or removing interfaces is not possible with ruby-dbus # once the object is exported on D-Bus. # # Updating the currently exported D-Bus objects is avoided by calling to {#clean} first. From 1ba7c2be636cf3fde2c674caeb5ef66a74fe1dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 8 Mar 2024 12:09:36 +0000 Subject: [PATCH 14/98] service: Add Device interface --- .../agama/dbus/storage/interfaces/device.rb | 1 + .../dbus/storage/interfaces/device/block.rb | 10 +-- .../dbus/storage/interfaces/device/device.rb | 82 +++++++++++++++++++ .../dbus/storage/interfaces/device/lvm_vg.rb | 8 -- .../test/agama/dbus/storage/device_test.rb | 43 ++++++++++ .../interfaces/device/block_examples.rb | 6 -- .../interfaces/device/device_examples.rb | 59 +++++++++++++ .../interfaces/device/lvm_vg_examples.rb | 6 -- 8 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 service/lib/agama/dbus/storage/interfaces/device/device.rb create mode 100644 service/test/agama/dbus/storage/interfaces/device/device_examples.rb diff --git a/service/lib/agama/dbus/storage/interfaces/device.rb b/service/lib/agama/dbus/storage/interfaces/device.rb index f721da72ef..478b141bd0 100644 --- a/service/lib/agama/dbus/storage/interfaces/device.rb +++ b/service/lib/agama/dbus/storage/interfaces/device.rb @@ -33,6 +33,7 @@ module Device require "agama/dbus/storage/interfaces/device/block" require "agama/dbus/storage/interfaces/device/component" +require "agama/dbus/storage/interfaces/device/device" require "agama/dbus/storage/interfaces/device/drive" require "agama/dbus/storage/interfaces/device/filesystem" require "agama/dbus/storage/interfaces/device/lvm_vg" diff --git a/service/lib/agama/dbus/storage/interfaces/device/block.rb b/service/lib/agama/dbus/storage/interfaces/device/block.rb index fa7c3e301b..def101b5de 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/block.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/block.rb @@ -44,13 +44,6 @@ def self.apply?(storage_device) BLOCK_INTERFACE = "org.opensuse.Agama.Storage1.Block" private_constant :BLOCK_INTERFACE - # Name of the block device - # - # @return [String] e.g., "/dev/sda" - def block_name - storage_device.name - end - # Position of the first block of the region. # # @return [Integer] @@ -112,8 +105,7 @@ def block_systems def self.included(base) base.class_eval do - dbus_interface BLOCK_INTERFACE do - dbus_reader :block_name, "s", dbus_name: "Name" + dbus_interface BLOCK_INTERFACE do dbus_reader :block_start, "t", dbus_name: "Start" dbus_reader :block_active, "b", dbus_name: "Active" dbus_reader :block_encrypted, "b", dbus_name: "Encrypted" diff --git a/service/lib/agama/dbus/storage/interfaces/device/device.rb b/service/lib/agama/dbus/storage/interfaces/device/device.rb new file mode 100644 index 0000000000..f10b8a1829 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/device.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "dbus" +require "y2storage/device_description" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for a device. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device}. + module Device + # Whether this interface should be implemented for the given device. + # + # @note All devices implement this interface. + # + # @param _storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(_storage_device) + true + end + + DEVICE_INTERFACE = "org.opensuse.Agama.Storage1.Device" + private_constant :DEVICE_INTERFACE + + # sid of the device. + # + # @return [Integer] + def device_sid + storage_device.sid + end + + # Name of the device. + # + # @return [String] e.g., "/dev/sda". + def device_name + storage_device.name + end + + # Description of the device. + # + # @return [String] e.g., "EXT4 Partition". + def device_description + Y2Storage::DeviceDescription.new(storage_device).to_s + end + + def self.included(base) + base.class_eval do + dbus_interface DEVICE_INTERFACE do + dbus_reader :device_sid, "u", dbus_name: "SID" + dbus_reader :device_name, "s", dbus_name: "Name" + dbus_reader :device_description, "s", dbus_name: "Description" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb b/service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb index b69b84a5f5..8d219fc6dd 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/lvm_vg.rb @@ -44,13 +44,6 @@ def self.apply?(storage_device) VOLUME_GROUP_INTERFACE = "org.opensuse.Agama.Storage1.LVM.VolumeGroup" private_constant :VOLUME_GROUP_INTERFACE - # Name of the volume group - # - # @return [String] e.g., "/dev/mapper/vg0" - def lvm_vg_name - storage_device.name - end - # Size of the volume group in bytes # # @return [Integer] @@ -75,7 +68,6 @@ def lvm_vg_lvs def self.included(base) base.class_eval do dbus_interface VOLUME_GROUP_INTERFACE do - dbus_reader :lvm_vg_name, "s", dbus_name: "Name" dbus_reader :lvm_vg_size, "t", dbus_name: "Size" dbus_reader :lvm_vg_pvs, "ao", dbus_name: "PhysicalVolumes" dbus_reader :lvm_vg_lvs, "ao", dbus_name: "LogicalVolumes" diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index f67ecaaa8a..b7a8f42df6 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -23,6 +23,7 @@ require_relative "../../storage/storage_helpers" require_relative "./interfaces/device/block_examples" require_relative "./interfaces/device/component_examples" +require_relative "./interfaces/device/device_examples" require_relative "./interfaces/device/drive_examples" require_relative "./interfaces/device/filesystem_examples" require_relative "./interfaces/device/lvm_vg_examples" @@ -66,6 +67,10 @@ let(:device) { devicegraph.find_by_name("/dev/sda") } + it "defines the Device interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Device") + end + it "defines the Drive interface" do expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Drive") end @@ -80,6 +85,10 @@ let(:device) { devicegraph.dm_raids.first } + it "defines the Device interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Device") + end + it "defines the Drive interface" do expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Drive") end @@ -98,6 +107,10 @@ let(:device) { devicegraph.md_raids.first } + it "defines the Device interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Device") + end + it "does not define the Drive interface" do expect(subject).to_not include_dbus_interface("org.opensuse.Agama.Storage1.Drive") end @@ -116,6 +129,14 @@ let(:device) { devicegraph.find_by_name("/dev/vg0") } + it "defines the Device interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Device") + end + + it "does not define the Drive interface" do + expect(subject).to_not include_dbus_interface("org.opensuse.Agama.Storage1.Drive") + end + it "defines the LVM.VolumeGroup interface" do expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.LVM.VolumeGroup") end @@ -141,8 +162,30 @@ .to_not include_dbus_interface("org.opensuse.Agama.Storage1.PartitionTable") end end + + context "when the device is formatted" do + let(:scenario) { "multipath-formatted.xml" } + + let(:device) { devicegraph.find_by_name("/dev/mapper/0QEMU_QEMU_HARDDISK_mpath1") } + + it "defines the Filesystem interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Filesystem") + end + end + + context "when the device is no formatted" do + let(:scenario) { "partitioned_md.yml" } + + let(:device) { devicegraph.find_by_name("/dev/sda") } + + it "does not define the Filesystem interface" do + expect(subject).to_not include_dbus_interface("org.opensuse.Agama.Storage1.Filesystem") + end + end end + include_examples "Device interface" + include_examples "Drive interface" include_examples "RAID interface" diff --git a/service/test/agama/dbus/storage/interfaces/device/block_examples.rb b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb index 6211a2d913..ba3ec052a9 100644 --- a/service/test/agama/dbus/storage/interfaces/device/block_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb @@ -27,12 +27,6 @@ let(:device) { devicegraph.find_by_name("/dev/sda") } - describe "#block_name" do - it "returns the name of the device" do - expect(subject.block_name).to eq("/dev/sda") - end - end - describe "#block_start" do before do allow(device).to receive(:start).and_return(345) diff --git a/service/test/agama/dbus/storage/interfaces/device/device_examples.rb b/service/test/agama/dbus/storage/interfaces/device/device_examples.rb new file mode 100644 index 0000000000..2afc123152 --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/device/device_examples.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../../../test_helper" +require "y2storage/device_description" + +shared_examples "Device interface" do + describe "Device D-Bus interface" do + let(:scenario) { "partitioned_md.yml" } + + let(:device) { devicegraph.find_by_name("/dev/sda") } + + describe "#device_sid" do + before do + allow(device).to receive(:sid).and_return(123) + end + + it "returns the SID of the device" do + expect(subject.device_sid).to eq(123) + end + end + + describe "#device_name" do + it "returns the name of the device" do + expect(subject.device_name).to eq("/dev/sda") + end + end + + describe "#device_description" do + before do + allow(Y2Storage::DeviceDescription).to receive(:new).with(device).and_return(description) + end + + let(:description) { instance_double(Y2Storage::DeviceDescription, to_s: "test") } + + it "returns the description of the device" do + expect(subject.device_description).to eq("test") + end + end + end +end diff --git a/service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb b/service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb index ad646ca618..2e716acde5 100644 --- a/service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/lvm_vg_examples.rb @@ -27,12 +27,6 @@ let(:device) { devicegraph.find_by_name("/dev/vg0") } - describe "#lvm_vg_name" do - it "returns the name of the volume group" do - expect(subject.lvm_vg_name).to eq("/dev/vg0") - end - end - describe "#lvm_vg_size" do before do allow(device).to receive(:size).and_return(size) From f94680dd17b64a9e546b582e56167b2df64c4c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 8 Mar 2024 12:12:32 +0000 Subject: [PATCH 15/98] service: Add Partition interface --- .../agama/dbus/storage/interfaces/device.rb | 1 + .../storage/interfaces/device/partition.rb | 74 +++++++++++++++++++ .../test/agama/dbus/storage/device_test.rb | 25 +++++++ .../interfaces/device/partition_examples.rb | 48 ++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 service/lib/agama/dbus/storage/interfaces/device/partition.rb create mode 100644 service/test/agama/dbus/storage/interfaces/device/partition_examples.rb diff --git a/service/lib/agama/dbus/storage/interfaces/device.rb b/service/lib/agama/dbus/storage/interfaces/device.rb index 478b141bd0..374c0b6188 100644 --- a/service/lib/agama/dbus/storage/interfaces/device.rb +++ b/service/lib/agama/dbus/storage/interfaces/device.rb @@ -39,5 +39,6 @@ module Device require "agama/dbus/storage/interfaces/device/lvm_vg" require "agama/dbus/storage/interfaces/device/md" require "agama/dbus/storage/interfaces/device/multipath" +require "agama/dbus/storage/interfaces/device/partition" require "agama/dbus/storage/interfaces/device/partition_table" require "agama/dbus/storage/interfaces/device/raid" diff --git a/service/lib/agama/dbus/storage/interfaces/device/partition.rb b/service/lib/agama/dbus/storage/interfaces/device/partition.rb new file mode 100644 index 0000000000..050d836663 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/partition.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for partition. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Partition + # Whether this interface should be implemented for the given device. + # + # @note Partitions implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:partition) + end + + PARTITION_INTERFACE = "org.opensuse.Agama.Storage1.Partition" + private_constant :PARTITION_INTERFACE + + # Device hosting the partition table of this partition. + # + # @return [Array<::DBus::ObjectPath>] + def partition_device + tree.path_for(storage_device.partitionable) + end + + # Whether it is a (valid) EFI System partition + # + # @return [Boolean] + def partition_efi + storage_device.efi_system? + end + + def self.included(base) + base.class_eval do + dbus_interface PARTITION_INTERFACE do + dbus_reader :partition_device, "o", dbus_name: "Device" + dbus_reader :partition_efi, "b", dbus_name: "EFI" + end + end + end + end + end + end + end + end +end diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index b7a8f42df6..afa44307ff 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -29,6 +29,7 @@ require_relative "./interfaces/device/lvm_vg_examples" require_relative "./interfaces/device/md_examples" require_relative "./interfaces/device/multipath_examples" +require_relative "./interfaces/device/partition_examples" require_relative "./interfaces/device/partition_table_examples" require_relative "./interfaces/device/raid_examples" require "agama/dbus/storage/device" @@ -142,6 +143,28 @@ end end + context "when the given device is a partition" do + let(:scenario) { "partitioned_md.yml" } + + let(:device) { devicegraph.find_by_name("/dev/sda1") } + + it "defines the Device interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Device") + end + + it "defines the Block interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Block") + end + + it "does not define the Drive interface" do + expect(subject).to_not include_dbus_interface("org.opensuse.Agama.Storage1.Drive") + end + + it "defines the Partition interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Partition") + end + end + context "when the given device has a partition table" do let(:scenario) { "partitioned_md.yml" } @@ -198,6 +221,8 @@ include_examples "LVM.VolumeGroup interface" + include_examples "Partition interface" + include_examples "PartitionTable interface" include_examples "Filesystem interface" diff --git a/service/test/agama/dbus/storage/interfaces/device/partition_examples.rb b/service/test/agama/dbus/storage/interfaces/device/partition_examples.rb new file mode 100644 index 0000000000..f99b71d523 --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/device/partition_examples.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../../../test_helper" + +shared_examples "Partition interface" do + describe "Partition D-Bus interface" do + let(:scenario) { "partitioned_md.yml" } + + let(:device) { devicegraph.find_by_name("/dev/sda1") } + + describe "#partition_device" do + it "returns the path of the host device" do + sda = devicegraph.find_by_name("/dev/sda") + + expect(subject.partition_device).to eq(tree.path_for(sda)) + end + end + + describe "#partition_efi" do + before do + allow(device).to receive(:efi_system?).and_return(true) + end + + it "returns whether it is an EFI partition" do + expect(subject.partition_efi).to eq(true) + end + end + end +end From d2ce25e03b69a76e95e656d1363dd7863fba80cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 8 Mar 2024 12:13:19 +0000 Subject: [PATCH 16/98] service: Adapt Filesystem interface --- .../storage/interfaces/device/filesystem.rb | 19 +++++++--- .../interfaces/device/filesystem_examples.rb | 35 +++++++++++++++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb index 36ee17c2ea..ad066f2022 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require "dbus" +require "y2storage/filesystem_label" module Agama module DBus @@ -51,18 +52,26 @@ def filesystem_type storage_device.filesystem.type.to_s end - # Whether the filesystem contains the directory layout of an ESP partition. + # Mount path of the file system. # - # @return [Boolean] - def filesystem_efi? - storage_device.filesystem.efi? + # @return [String] Empty if not mounted. + def filesystem_mount_path + storage_device.filesystem.mount_path || "" + end + + # Label of the file system. + # + # @return [String] Empty if it has no label. + def filesystem_label + Y2Storage::FilesystemLabel.new(storage_device).to_s end def self.included(base) base.class_eval do dbus_interface FILESYSTEM_INTERFACE do dbus_reader :filesystem_type, "s", dbus_name: "Type" - dbus_reader :filesystem_efi?, "b", dbus_name: "EFI" + dbus_reader :filesystem_mount_path, "s", dbus_name: "MountPath" + dbus_reader :filesystem_label, "s", dbus_name: "Label" end end end diff --git a/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb index c581bd7668..84b8da4a10 100644 --- a/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require_relative "../../../../../test_helper" +require "y2storage/filesystem_label" shared_examples "Filesystem interface" do describe "Filesystem D-Bus interface" do @@ -33,9 +34,37 @@ end end - describe "#filesystem_efi?" do - it "returns whether the file system is an EFI" do - expect(subject.filesystem_efi?).to eq(false) + describe "#filesystem_mount_path" do + context "if the file system is mounted" do + before do + device.filesystem.mount_path = "/test" + end + + it "returns the mount path" do + expect(subject.filesystem_mount_path).to eq("/test") + end + end + + context "if the file system is not mounted" do + before do + device.filesystem.mount_path = "" + end + + it "returns empty string" do + expect(subject.filesystem_mount_path).to eq("") + end + end + end + + describe "#filesystem_label" do + before do + allow(Y2Storage::FilesystemLabel).to receive(:new).with(device).and_return(label) + end + + let(:label) { instance_double(Y2Storage::FilesystemLabel, to_s: "photos") } + + it "returns the label of the file system" do + expect(subject.filesystem_label).to eq("photos") end end end From 19f769a45a203fc76704ac8552e0949a64ac00ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 8 Mar 2024 12:13:45 +0000 Subject: [PATCH 17/98] service: Update dependency --- service/package/gem2rpm.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml index fef1bc79db..9a886072bd 100644 --- a/service/package/gem2rpm.yml +++ b/service/package/gem2rpm.yml @@ -38,8 +38,7 @@ Requires: yast2-iscsi-client >= 4.5.7 Requires: yast2-network Requires: yast2-proxy - # ProposalSettings#swap_reuse - Requires: yast2-storage-ng >= 5.0.3 + Requires: yast2-storage-ng >= 5.0.8 Requires: yast2-users %ifarch s390 s390x Requires: yast2-s390 >= 4.6.4 From 0653aaa4fb6d2a29954bb3c85e0535d87fc6d7ac Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Fri, 8 Mar 2024 13:42:15 +0100 Subject: [PATCH 18/98] Turned some unwrap calls into a more reasonable handling --- rust/agama-server/src/agama-web-server.rs | 77 ++++++++++++----------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 6e8665b48e..d205b509bb 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -67,25 +67,25 @@ async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { // peek() receives the data without removing it from the stream, // the data is not consumed, it will be read from the stream again later - stream.peek(&mut buf).await.unwrap(); - - // SSL3.0/TLS1.x starts with byte 0x16 - // SSL2 starts with 0x80 (but should not be used as it is considered) - // see https://stackoverflow.com/q/3897883 - // otherwise consider the stream as a plain HTTP stream possibly starting with - // "GET ... HTTP/1.1" or "POST ... HTTP/1.1" or a similar line - buf[0] == 0x16u8 || buf[0] == 0x80u8 + stream.peek(&mut buf) + .await + // SSL3.0/TLS1.x starts with byte 0x16 + // SSL2 starts with 0x80 (but should not be used as it is considered) + // see https://stackoverflow.com/q/3897883 + // otherwise consider the stream as a plain HTTP stream possibly starting with + // "GET ... HTTP/1.1" or "POST ... HTTP/1.1" or a similar line + .is_ok_and(|_| buf[0] == 0x16u8 || buf[0] == 0x80u8) } // build a SSL acceptor using a provided SSL certificate or generate a self-signed one -fn create_ssl_acceptor(cert_file: &String, key_file: &String) -> SslAcceptor { - let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server()).unwrap(); +fn create_ssl_acceptor(cert_file: &String, key_file: &String) -> Result { + let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?; if cert_file.is_empty() && key_file.is_empty() { - let (cert, key) = agama_dbus_server::cert::create_certificate().unwrap(); + let (cert, key) = agama_dbus_server::cert::create_certificate()?; tracing::info!("Generated self signed certificate: {:#?}", cert); - tls_builder.set_private_key(key.as_ref()).unwrap(); - tls_builder.set_certificate(cert.as_ref()).unwrap(); + tls_builder.set_private_key(key.as_ref())?; + tls_builder.set_certificate(cert.as_ref())?; // for debugging you might dump the certificate to a file: // use std::io::Write; @@ -96,19 +96,17 @@ fn create_ssl_acceptor(cert_file: &String, key_file: &String) -> SslAcceptor { } else { tracing::info!("Loading PEM certificate: {}", cert_file); tls_builder - .set_certificate_file(PathBuf::from(cert_file), SslFiletype::PEM) - .unwrap(); + .set_certificate_file(PathBuf::from(cert_file), SslFiletype::PEM)?; tracing::info!("Loading PEM key: {}", key_file); tls_builder - .set_private_key_file(PathBuf::from(key_file), SslFiletype::PEM) - .unwrap(); + .set_private_key_file(PathBuf::from(key_file), SslFiletype::PEM)?; } // check that the key belongs to the certificate - tls_builder.check_private_key().unwrap(); + tls_builder.check_private_key()?; - tls_builder.build() + Ok(tls_builder.build()) } // build a response for the HTTP -> HTTPS redirection @@ -119,7 +117,9 @@ fn redirect_https(host: &str, uri: &hyper::Uri) -> Response { .header("Location", format!("https://{}{}", host, uri)) .status(hyper::StatusCode::PERMANENT_REDIRECT); - builder.body(String::from("")).unwrap() + // according to documentation this can fail only if builder was previosly fed with data + // which failed to parse into an internal representation (e.g. invalid header) + builder.body(String::from("")).expect("Failed to create redirection request") } // build an error response for the HTTP -> HTTPS redirection when we cannot build @@ -129,7 +129,9 @@ fn redirect_error() -> Response { let builder = Response::builder().status(hyper::StatusCode::BAD_REQUEST); let msg = "HTTP protocol is not allowed for external requests, please use HTTPS.\n"; - builder.body(String::from(msg)).unwrap() + // according to documentation this can fail only if builder was previosly fed with data + // which failed to parse into an internal representation (e.g. invalid header) + builder.body(String::from(msg)).expect("Failed to create an error response") } // build a router for the HTTP -> HTTPS redirection @@ -138,18 +140,10 @@ fn redirect_error() -> Response { fn https_redirect() -> Router { // see https://docs.rs/axum/latest/axum/routing/struct.Router.html#example let redirect_service = tower::service_fn(|req: axum::extract::Request| async move { - let request_host = req.headers().get("host"); - match request_host { - // missing host in the request header, we cannot build the redirection URL, return error 400 - None => Ok(redirect_error()), - Some(host) => { - let host_string = host.to_str(); - match host_string { - // invalid host value in the request - Err(_) => Ok(redirect_error()), - Ok(host_str) => return Ok(redirect_https(host_str, req.uri())), - } - } + if let Some(host) = req.headers().get("host").and_then(|h| h.to_str().ok()) { + Ok(redirect_https(host, req.uri())) + } else { + Ok(redirect_error()) } }); @@ -178,8 +172,8 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto let redirector_service = redirector.clone(); let tls_acceptor = ssl_acceptor.clone(); - // Wait for a new tcp connection - let (tcp_stream, addr) = listener.accept().await.unwrap(); + // Wait for a new tcp connection; if it fails we cannot do much, so print an error and die + let (tcp_stream, addr) = listener.accept().await.expect("Failed to open port for listening"); tokio::spawn(async move { if is_ssl_stream(&tcp_stream).await { @@ -245,9 +239,16 @@ async fn serve_command( let (tx, _) = channel(16); run_monitor(tx.clone()).await?; - let config = web::ServiceConfig::load().unwrap(); - let service = web::service(config, tx); - let ssl_acceptor = create_ssl_acceptor(cert, key); + let service = if let Ok(config) = web::ServiceConfig::load() { + web::service(config, tx) + } else { + return Err(anyhow::anyhow!("Failed to load the service configuration")) + }; + let ssl_acceptor = if let Ok(ssl_acceptor) = create_ssl_acceptor(cert, key) { + ssl_acceptor + } else { + return Err(anyhow::anyhow!("SSL initialization failed")); + }; let mut servers = vec![]; servers.push(tokio::spawn(start_server( From 7b8c729d3cf4d396ff26bf92c76454305b51f7b6 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Fri, 8 Mar 2024 13:55:22 +0100 Subject: [PATCH 19/98] Formatting --- rust/agama-server/src/agama-web-server.rs | 29 +++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index d205b509bb..ddb57eb1ae 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -67,7 +67,8 @@ async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { // peek() receives the data without removing it from the stream, // the data is not consumed, it will be read from the stream again later - stream.peek(&mut buf) + stream + .peek(&mut buf) .await // SSL3.0/TLS1.x starts with byte 0x16 // SSL2 starts with 0x80 (but should not be used as it is considered) @@ -78,7 +79,10 @@ async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { } // build a SSL acceptor using a provided SSL certificate or generate a self-signed one -fn create_ssl_acceptor(cert_file: &String, key_file: &String) -> Result { +fn create_ssl_acceptor( + cert_file: &String, + key_file: &String +) -> Result { let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?; if cert_file.is_empty() && key_file.is_empty() { @@ -95,12 +99,10 @@ fn create_ssl_acceptor(cert_file: &String, key_file: &String) -> Result Response { // according to documentation this can fail only if builder was previosly fed with data // which failed to parse into an internal representation (e.g. invalid header) - builder.body(String::from("")).expect("Failed to create redirection request") + builder + .body(String::from("")) + .expect("Failed to create redirection request") } // build an error response for the HTTP -> HTTPS redirection when we cannot build @@ -131,7 +135,9 @@ fn redirect_error() -> Response { let msg = "HTTP protocol is not allowed for external requests, please use HTTPS.\n"; // according to documentation this can fail only if builder was previosly fed with data // which failed to parse into an internal representation (e.g. invalid header) - builder.body(String::from(msg)).expect("Failed to create an error response") + builder + .body(String::from(msg)) + .expect("Failed to create an error response") } // build a router for the HTTP -> HTTPS redirection @@ -173,7 +179,10 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto let tls_acceptor = ssl_acceptor.clone(); // Wait for a new tcp connection; if it fails we cannot do much, so print an error and die - let (tcp_stream, addr) = listener.accept().await.expect("Failed to open port for listening"); + let (tcp_stream, addr) = listener + .accept() + .await + .expect("Failed to open port for listening"); tokio::spawn(async move { if is_ssl_stream(&tcp_stream).await { @@ -242,7 +251,7 @@ async fn serve_command( let service = if let Ok(config) = web::ServiceConfig::load() { web::service(config, tx) } else { - return Err(anyhow::anyhow!("Failed to load the service configuration")) + return Err(anyhow::anyhow!("Failed to load the service configuration")); }; let ssl_acceptor = if let Ok(ssl_acceptor) = create_ssl_acceptor(cert, key) { ssl_acceptor From ba9304366cb2c822ad7277bcd983868b32916cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 8 Mar 2024 15:55:06 +0000 Subject: [PATCH 20/98] service: Use display_name --- service/lib/agama/dbus/storage/interfaces/device/device.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/lib/agama/dbus/storage/interfaces/device/device.rb b/service/lib/agama/dbus/storage/interfaces/device/device.rb index f10b8a1829..6aef6084b7 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/device.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/device.rb @@ -51,11 +51,11 @@ def device_sid storage_device.sid end - # Name of the device. + # Name to represent the device. # # @return [String] e.g., "/dev/sda". def device_name - storage_device.name + storage_device.display_name || "" end # Description of the device. From aa1e7a3ec813a1a4b563a0524477fa6d35a795fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 8 Mar 2024 15:55:57 +0000 Subject: [PATCH 21/98] service: Add LogicalVolume interface --- .../agama/dbus/storage/interfaces/device.rb | 1 + .../dbus/storage/interfaces/device/lvm_lv.rb | 66 +++++++++++++++++++ .../test/agama/dbus/storage/device_test.rb | 21 ++++++ .../interfaces/device/lvm_lv_examples.rb | 38 +++++++++++ 4 files changed, 126 insertions(+) create mode 100644 service/lib/agama/dbus/storage/interfaces/device/lvm_lv.rb create mode 100644 service/test/agama/dbus/storage/interfaces/device/lvm_lv_examples.rb diff --git a/service/lib/agama/dbus/storage/interfaces/device.rb b/service/lib/agama/dbus/storage/interfaces/device.rb index 374c0b6188..6376f6a0c4 100644 --- a/service/lib/agama/dbus/storage/interfaces/device.rb +++ b/service/lib/agama/dbus/storage/interfaces/device.rb @@ -36,6 +36,7 @@ module Device require "agama/dbus/storage/interfaces/device/device" require "agama/dbus/storage/interfaces/device/drive" require "agama/dbus/storage/interfaces/device/filesystem" +require "agama/dbus/storage/interfaces/device/lvm_lv" require "agama/dbus/storage/interfaces/device/lvm_vg" require "agama/dbus/storage/interfaces/device/md" require "agama/dbus/storage/interfaces/device/multipath" diff --git a/service/lib/agama/dbus/storage/interfaces/device/lvm_lv.rb b/service/lib/agama/dbus/storage/interfaces/device/lvm_lv.rb new file mode 100644 index 0000000000..93757ffdfc --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/lvm_lv.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for LVM logical volume. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module LvmLv + # Whether this interface should be implemented for the given device. + # + # @note LVM logical volumes implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:lvm_lv) + end + + LOGICAL_VOLUME_INTERFACE = "org.opensuse.Agama.Storage1.LVM.LogicalVolume" + private_constant :LOGICAL_VOLUME_INTERFACE + + # LVM volume group hosting the this logical volume. + # + # @return [Array<::DBus::ObjectPath>] + def lvm_lv_vg + tree.path_for(storage_device.lvm_vg) + end + + def self.included(base) + base.class_eval do + dbus_interface LOGICAL_VOLUME_INTERFACE do + dbus_reader :lvm_lv_vg, "o", dbus_name: "VolumeGroup" + end + end + end + end + end + end + end + end +end diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index afa44307ff..efb5ffe720 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -26,6 +26,7 @@ require_relative "./interfaces/device/device_examples" require_relative "./interfaces/device/drive_examples" require_relative "./interfaces/device/filesystem_examples" +require_relative "./interfaces/device/lvm_lv_examples" require_relative "./interfaces/device/lvm_vg_examples" require_relative "./interfaces/device/md_examples" require_relative "./interfaces/device/multipath_examples" @@ -143,6 +144,24 @@ end end + context "when the given device is a LVM logical volume" do + let(:scenario) { "trivial_lvm.yml" } + + let(:device) { devicegraph.find_by_name("/dev/vg0/lv1") } + + it "defines the Device interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.Device") + end + + it "does not define the Drive interface" do + expect(subject).to_not include_dbus_interface("org.opensuse.Agama.Storage1.Drive") + end + + it "defines the LVM.LogicalVolume interface" do + expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.LVM.LogicalVolume") + end + end + context "when the given device is a partition" do let(:scenario) { "partitioned_md.yml" } @@ -221,6 +240,8 @@ include_examples "LVM.VolumeGroup interface" + include_examples "LVM.LogicalVolume interface" + include_examples "Partition interface" include_examples "PartitionTable interface" diff --git a/service/test/agama/dbus/storage/interfaces/device/lvm_lv_examples.rb b/service/test/agama/dbus/storage/interfaces/device/lvm_lv_examples.rb new file mode 100644 index 0000000000..9f96a71ecb --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/device/lvm_lv_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../../../test_helper" + +shared_examples "LVM.LogicalVolume interface" do + describe "LVM.LogicalVolume D-Bus interface" do + let(:scenario) { "trivial_lvm.yml" } + + let(:device) { devicegraph.find_by_name("/dev/vg0/lv1") } + + describe "#lvm_lv_vg" do + it "returns the path of the host volume group" do + vg0 = devicegraph.find_by_name("/dev/vg0") + + expect(subject.lvm_lv_vg).to eq(tree.path_for(vg0)) + end + end + end +end From 3aea9b9f0ccd699d7f9652cbf922159b4d67f3ae Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 10 Mar 2024 02:43:52 +0000 Subject: [PATCH 22/98] Update web PO files Agama-weblate commit: 6e40e55f095086ac0577a8737169f99cde56c88c --- web/po/ca.po | 2223 +++++++++++++++++++++++++++++++++++++++++ web/po/cs.po | 57 +- web/po/de.po | 120 ++- web/po/es.po | 59 +- web/po/fr.po | 185 ++-- web/po/id.po | 59 +- web/po/ja.po | 70 +- web/po/ka.po | 59 +- web/po/mk.po | 64 +- web/po/nl.po | 59 +- web/po/pt_BR.po | 59 +- web/po/ru.po | 57 +- web/po/sv.po | 70 +- web/po/uk.po | 57 +- web/po/zh_Hans.po | 2163 +++++++++++++++++++++++++++++++++++++++ web/src/manifest.json | 9 +- 16 files changed, 4865 insertions(+), 505 deletions(-) create mode 100644 web/po/ca.po create mode 100644 web/po/zh_Hans.po diff --git a/web/po/ca.po b/web/po/ca.po new file mode 100644 index 0000000000..0471061d13 --- /dev/null +++ b/web/po/ca.po @@ -0,0 +1,2223 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR SuSE Linux Products GmbH, Nuernberg +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"PO-Revision-Date: 2024-03-08 20:42+0000\n" +"Last-Translator: David Medina \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9.1\n" + +#. TRANSLATORS: error message, %s is replaced by the server URL +#: src/DevServerWrapper.jsx:82 +#, c-format +msgid "The server at %s is not reachable." +msgstr "No es pot accedir al servidor a %s." + +#. TRANSLATORS: error message +#: src/DevServerWrapper.jsx:88 +msgid "Cannot connect to the Cockpit server" +msgstr "No es pot connectar amb el servidor Cockpit." + +#. TRANSLATORS: button label +#: src/DevServerWrapper.jsx:104 +msgid "Try Again" +msgstr "Torna-ho a provar" + +#: src/components/core/About.jsx:43 src/components/core/About.jsx:48 +msgid "About Agama" +msgstr "Quant a Agama" + +#. TRANSLATORS: content of the "About" popup (1/2) +#: src/components/core/About.jsx:53 +msgid "" +"Agama is an experimental installer for (open)SUSE systems. It is still under " +"development so, please, do not use it in production environments. If you " +"want to give it a try, we recommend using a virtual machine to prevent any " +"possible data loss." +msgstr "" +"L'Agama és un instal·lador experimental per a sistemes (open)SUSE. Encara " +"està en desenvolupament, així que, si us plau, no l'useu en entorns de " +"producció. Si voleu provar-lo, us recomanem que feu servir una màquina " +"virtual per evitar possibles pèrdues de dades." + +#. TRANSLATORS: content of the "About" popup (2/2) +#. %s is replaced by the project URL +#: src/components/core/About.jsx:65 +#, c-format +msgid "For more information, please visit the project's repository at %s." +msgstr "" +"Per obtenir-ne més informació, visiteu el repositori del projecte a %s." + +#. TRANSLATORS: button label +#: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 +#: src/components/core/IssuesDialog.jsx:119 src/components/core/Sidebar.jsx:157 +#: src/components/core/Terminal.jsx:48 +#: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 +msgid "Close" +msgstr "Tanca" + +#. TRANSLATORS: page title +#: src/components/core/DBusError.jsx:34 +msgid "D-Bus Error" +msgstr "Error de D-Bus" + +#: src/components/core/DBusError.jsx:38 +msgid "Cannot connect to D-Bus" +msgstr "No es pot connectar a D-Bus." + +#: src/components/core/DBusError.jsx:43 +msgid "" +"Could not connect to the D-Bus service. Please, check whether it is running." +msgstr "" +"No s'ha pogut connectar amb el servei D-Bus. Si us plau, comproveu si " +"s'executa." + +#. TRANSLATORS: button label +#: src/components/core/DBusError.jsx:51 +msgid "Reload" +msgstr "Torna a carregar" + +#: src/components/core/DevelopmentInfo.jsx:55 +msgid "Cockpit server" +msgstr "Servidor Cockpit" + +#: src/components/core/FileViewer.jsx:65 +msgid "Reading file..." +msgstr "Llegint el fitxer..." + +#: src/components/core/FileViewer.jsx:71 +msgid "Cannot read the file" +msgstr "No es pot llegir el fitxer." + +#: src/components/core/InstallButton.jsx:37 +msgid "" +"There are some reported issues. Please review them in the previous steps " +"before proceeding with the installation." +msgstr "" +"Hi ha alguns problemes reportats. Reviseu-los als passos anteriors abans de " +"continuar la instal·lació." + +#: src/components/core/InstallButton.jsx:49 +msgid "Confirm Installation" +msgstr "Confirmeu la instal·lació" + +#: src/components/core/InstallButton.jsx:55 +msgid "" +"If you continue, partitions on your hard disk will be modified according to " +"the provided installation settings." +msgstr "" +"Si continueu, les particions del disc dur es modificaran segons la " +"configuració d'instal·lació proporcionada." + +#: src/components/core/InstallButton.jsx:59 +msgid "Please, cancel and check the settings if you are unsure." +msgstr "" +"Si us plau, cancel·leu i comproveu-ne la configuració si no n'esteu segur." + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:65 src/components/core/Popup.jsx:128 +#: src/components/network/WifiConnectionForm.jsx:136 +msgid "Cancel" +msgstr "Cancel·la" + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:69 +msgid "Continue" +msgstr "Continua" + +#: src/components/core/InstallButton.jsx:78 +msgid "Problems Found" +msgstr "S'han trobat problemes." + +#: src/components/core/InstallButton.jsx:82 +msgid "" +"Some problems were found when trying to start the installation. Please, have " +"a look to the reported errors and try again." +msgstr "" +"S'han trobat alguns problemes en intentar iniciar la instal·lació. Si us " +"plau, mireu els errors i torneu-ho a provar." + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:89 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalDeviceSection.jsx:169 +#: src/components/storage/ProposalDeviceSection.jsx:364 +#: src/components/storage/ProposalSettingsSection.jsx:263 +#: src/components/storage/ProposalVolumes.jsx:148 +#: src/components/storage/ProposalVolumes.jsx:283 +#: src/components/storage/ZFCPPage.jsx:511 +msgid "Accept" +msgstr "Accepta-ho" + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:140 +msgid "Install" +msgstr "Instal·la" + +#: src/components/core/InstallationFinished.jsx:41 +msgid "TPM sealing requires the new system to be booted directly." +msgstr "El segellament TPM requereix que el sistema nou s'iniciï directament." + +#: src/components/core/InstallationFinished.jsx:46 +msgid "" +"If a local media was used to run this installer, remove it before the next " +"boot." +msgstr "" +"Si s'ha usat un mitjà local per executar aquest instal·lador, traieu-lo " +"abans de la propera arrencada." + +#: src/components/core/InstallationFinished.jsx:50 +msgid "Hide details" +msgstr "Amaga els detalls" + +#: src/components/core/InstallationFinished.jsx:50 +msgid "See more details" +msgstr "Mostra'n més detalls" + +#. TRANSLATORS: Do not translate 'abbr' and 'title', they are part of the HTML markup +#: src/components/core/InstallationFinished.jsx:55 +msgid "" +"The final step to configure the TPM to automatically open encrypted devices will take place during the " +"first boot of the new system. For that to work, the machine needs to boot " +"directly to the new boot loader." +msgstr "" +"El pas final per configurar el TPM per obrir automàticament dispositius encriptats es farà durant la " +"primera arrencada del sistema. Perquè això funcioni, la màquina ha " +"d'arrencar directament des del carregador d'arrencada nou." + +#. TRANSLATORS: page title +#: src/components/core/InstallationFinished.jsx:88 +msgid "Installation Finished" +msgstr "S'ha acabat la instal·lació." + +#: src/components/core/InstallationFinished.jsx:91 +msgid "Congratulations!" +msgstr "Enhorabona!" + +#: src/components/core/InstallationFinished.jsx:96 +msgid "The installation on your machine is complete." +msgstr "La instal·lació a la màquina s'ha completat." + +#: src/components/core/InstallationFinished.jsx:100 +msgid "At this point you can power off the machine." +msgstr "En aquest punt, podeu aturar la màquina." + +#: src/components/core/InstallationFinished.jsx:101 +msgid "At this point you can reboot the machine to log in to the new system." +msgstr "" +"En aquest punt, podeu reiniciar la màquina per iniciar sessió al sistema nou." + +#: src/components/core/InstallationFinished.jsx:113 +msgid "Finish" +msgstr "Acaba" + +#: src/components/core/InstallationFinished.jsx:113 +msgid "Reboot" +msgstr "Reinicia" + +#: src/components/core/InstallationProgress.jsx:32 +msgid "Installing" +msgstr "Instal·lant" + +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:215 +#: src/components/software/PatternSelector.jsx:216 +msgid "Search" +msgstr "Cerca" + +#: src/components/core/LogsButton.jsx:98 +msgid "Collecting logs..." +msgstr "Recopilant registres..." + +#: src/components/core/LogsButton.jsx:98 +msgid "Download logs" +msgstr "Baixa els registres" + +#: src/components/core/LogsButton.jsx:106 +msgid "" +"The browser will run the logs download as soon as they are ready. Please, be " +"patient." +msgstr "" +"El navegador executarà la baixada de registres així que estiguin a punt. Si " +"us plau, tingueu paciència." + +#: src/components/core/LogsButton.jsx:114 +msgid "Something went wrong while collecting logs. Please, try again." +msgstr "" +"Hi ha hagut un error durant la recopilació de registres. Torneu-ho a provar." + +#: src/components/core/Page.jsx:93 src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "Enrere" + +#: src/components/core/Page.jsx:201 +msgid "Show global options" +msgstr "Mostra les opcions globals" + +#: src/components/core/Page.jsx:215 +msgid "Page Actions" +msgstr "Accions de la pàgina" + +#: src/components/core/PasswordAndConfirmationInput.jsx:35 +msgid "Passwords do not match" +msgstr "Les contrasenyes no coincideixen." + +#: src/components/core/PasswordAndConfirmationInput.jsx:56 +#: src/components/network/WifiConnectionForm.jsx:125 +#: src/components/storage/iscsi/AuthFields.jsx:95 +#: src/components/storage/iscsi/AuthFields.jsx:100 +#: src/components/users/RootAuthMethods.jsx:163 +msgid "Password" +msgstr "Contrasenya" + +#: src/components/core/PasswordAndConfirmationInput.jsx:60 +msgid "User password" +msgstr "Contrasenya d'usuari" + +#: src/components/core/PasswordAndConfirmationInput.jsx:69 +msgid "Password confirmation" +msgstr "Confirmació de la contrasenya" + +#: src/components/core/PasswordAndConfirmationInput.jsx:74 +msgid "User password confirmation" +msgstr "Confirmació de la contrasenya d'usuari" + +#: src/components/core/PasswordInput.jsx:59 +msgid "Password visibility button" +msgstr "Botó de visibilitat de la contrasenya" + +#: src/components/core/Popup.jsx:90 +msgid "Confirm" +msgstr "Confirmeu-ho" + +#. TRANSLATORS: dropdown label +#: src/components/core/RowActions.jsx:66 +#: src/components/storage/ProposalVolumes.jsx:119 +#: src/components/storage/ProposalVolumes.jsx:310 +msgid "Actions" +msgstr "Accions" + +#: src/components/core/SectionSkeleton.jsx:29 +msgid "Waiting" +msgstr "Escrivint" + +#. TRANSLATORS: button label +#: src/components/core/ShowLogButton.jsx:47 +msgid "Show Logs" +msgstr "Mostra els registres" + +#. TRANSLATORS: popup dialog title +#: src/components/core/ShowLogButton.jsx:53 +msgid "YaST Logs" +msgstr "Registres del YaST" + +#. TRANSLATORS: button label +#: src/components/core/ShowTerminalButton.jsx:48 +msgid "Open Terminal" +msgstr "Obre el terminal" + +#. TRANSLATORS: sidebar header +#: src/components/core/Sidebar.jsx:113 src/components/core/Sidebar.jsx:121 +msgid "Installer Options" +msgstr "Opcions de l'instal·lador" + +#: src/components/core/Sidebar.jsx:128 +msgid "Hide installer options" +msgstr "Amaga les opcions de l'instal·lador" + +#: src/components/core/Sidebar.jsx:136 +msgid "Diagnostic tools" +msgstr "Eines de diagnòstic" + +#. TRANSLATORS: Titles used for the popup displaying found section issues +#: src/components/core/ValidationErrors.jsx:53 +msgid "Software issues" +msgstr "Problemes de programari" + +#: src/components/core/ValidationErrors.jsx:54 +msgid "Product issues" +msgstr "Problemes del producte" + +#: src/components/core/ValidationErrors.jsx:55 +msgid "Storage issues" +msgstr "Problemes d'emmagatzematge" + +#: src/components/core/ValidationErrors.jsx:57 +msgid "Found Issues" +msgstr "Problemes trobats" + +#. TRANSLATORS: %d is replaced with the number of errors found +#: src/components/core/ValidationErrors.jsx:77 +#, c-format +msgid "%d error found" +msgid_plural "%d errors found" +msgstr[0] "%d error trobat" +msgstr[1] "%d errors trobats" + +#. TRANSLATORS: label for keyboard layout selection +#: src/components/l10n/InstallerKeymapSwitcher.jsx:53 +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "Teclat" + +#. TRANSLATORS: label for keyboard layout selection +#: src/components/l10n/InstallerKeymapSwitcher.jsx:61 +msgid "keyboard" +msgstr "teclat" + +#. TRANSLATORS: +#: src/components/l10n/InstallerKeymapSwitcher.jsx:70 +msgid "Keyboard layout cannot be changed in remote installation" +msgstr "La disposició del teclat no es pot canviar a la instal·lació remota." + +#. TRANSLATORS: help text for the language selector in the sidebar, +#. %s will be replaced by the "Localization" page link +#: src/components/l10n/InstallerLocaleSwitcher.jsx:47 +#, c-format +msgid "" +"The language used by the installer. The language for the installed system " +"can be set in the %s page." +msgstr "" +"La llengua usada per l'instal·lador. La llengua del sistema instal·lat es " +"pot definir a la pàgina %s." + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/l10n/InstallerLocaleSwitcher.jsx:56 +#: src/components/l10n/L10nPage.jsx:384 +#: src/components/overview/L10nSection.jsx:52 +msgid "Localization" +msgstr "Localització" + +#: src/components/l10n/InstallerLocaleSwitcher.jsx:66 +#: src/components/l10n/L10nPage.jsx:246 +msgid "Language" +msgstr "Llengua" + +#: src/components/l10n/InstallerLocaleSwitcher.jsx:74 +msgid "language" +msgstr "llengua" + +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:53 +msgid "Filter by description or keymap code" +msgstr "Filtra per descripció o codi de mapa de tecles" + +#: src/components/l10n/KeymapSelector.jsx:64 +msgid "Available keymaps" +msgstr "Mapes de tecles disponibles" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "Seleccioneu la zona horària" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "%s usarà la zona horària seleccionada." + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "Zona horària" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "Canvia la zona horària" + +#: src/components/l10n/L10nPage.jsx:139 +msgid "Time zone not selected yet" +msgstr "La zona horària encara no s'ha seleccionat." + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +msgid "Select language" +msgstr "Seleccioneu una llengua" + +#: src/components/l10n/L10nPage.jsx:183 +#, c-format +msgid "%s will use the selected language." +msgstr "%s usarà la llengua seleccionada." + +#: src/components/l10n/L10nPage.jsx:252 +msgid "Change language" +msgstr "Canvia la llengua" + +#: src/components/l10n/L10nPage.jsx:257 +msgid "Language not selected yet" +msgstr "La llengua encara no s'ha seleccionat." + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +msgid "Select keyboard" +msgstr "Seleccioneu el teclat" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "%s usarà el teclat seleccionat." + +#: src/components/l10n/L10nPage.jsx:363 +msgid "Change keyboard" +msgstr "Canvia el teclat" + +#: src/components/l10n/L10nPage.jsx:368 +msgid "Keyboard not selected yet" +msgstr "El teclat encara no s'ha seleccionat." + +#: src/components/l10n/LocaleSelector.jsx:53 +msgid "Filter by language, territory or locale code" +msgstr "Filtra per llengua, territori o codi local" + +#: src/components/l10n/LocaleSelector.jsx:64 +msgid "Available locales" +msgstr "Llengües disponibles" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:75 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "Filtra per territori, codi de zona horària o desplaçament d'UTC" + +#: src/components/l10n/TimezoneSelector.jsx:86 +msgid "Available time zones" +msgstr "Zones horàries disponibles" + +#: src/components/layout/Loading.jsx:30 +msgid "Loading installation environment, please wait." +msgstr "Carregant l'entorn d'instal·lació. Espereu, si us plau." + +#. TRANSLATORS: button label +#: src/components/network/AddressesDataList.jsx:81 +#: src/components/network/DnsDataList.jsx:87 +msgid "Remove" +msgstr "Suprimeix" + +#. TRANSLATORS: input field name +#: src/components/network/AddressesDataList.jsx:93 +#: src/components/network/AddressesDataList.jsx:94 +#: src/components/network/IpAddressInput.jsx:33 +msgid "IP Address" +msgstr "Adreça IP" + +#. TRANSLATORS: input field name +#: src/components/network/AddressesDataList.jsx:102 +#: src/components/network/AddressesDataList.jsx:103 +msgid "Prefix length or netmask" +msgstr "Longitud del prefix o màscara de xarxa" + +#: src/components/network/AddressesDataList.jsx:119 +msgid "Add an address" +msgstr "Afegeix-hi una adreça" + +#. TRANSLATORS: button label +#: src/components/network/AddressesDataList.jsx:119 +msgid "Add another address" +msgstr "Afegeix-hi una altra adreça" + +#: src/components/network/AddressesDataList.jsx:124 +msgid "Addresses" +msgstr "Adreces" + +#: src/components/network/AddressesDataList.jsx:129 +msgid "Addresses data list" +msgstr "Llista de dades d'adreces" + +#. TRANSLATORS: input field for the iSCSI initiator name +#. TRANSLATORS: table header +#: src/components/network/ConnectionsTable.jsx:57 +#: src/components/network/ConnectionsTable.jsx:86 +#: src/components/storage/ZFCPPage.jsx:360 +#: src/components/storage/iscsi/InitiatorForm.jsx:52 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:68 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:85 +#: src/components/storage/iscsi/NodesPresenter.jsx:100 +#: src/components/storage/iscsi/NodesPresenter.jsx:121 +msgid "Name" +msgstr "Nom" + +#. TRANSLATORS: table header +#: src/components/network/ConnectionsTable.jsx:59 +#: src/components/network/ConnectionsTable.jsx:87 +msgid "IP addresses" +msgstr "Adreces IP" + +#: src/components/network/ConnectionsTable.jsx:67 +#: src/components/storage/ProposalVolumes.jsx:234 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:49 +#: src/components/storage/iscsi/NodesPresenter.jsx:73 +#: src/components/users/FirstUser.jsx:208 +msgid "Edit" +msgstr "Edita" + +#. TRANSLATORS: %s is replaced by a network connection name +#: src/components/network/ConnectionsTable.jsx:70 +#, c-format +msgid "Edit connection %s" +msgstr "Edita la connexió %s" + +#: src/components/network/ConnectionsTable.jsx:74 +msgid "Forget" +msgstr "Oblida-la" + +#. TRANSLATORS: %s is replaced by a network connection name +#: src/components/network/ConnectionsTable.jsx:77 +#, c-format +msgid "Forget connection %s" +msgstr "Oblida la connexió %s" + +#. TRANSLATORS: %s is replaced by a network connection name +#: src/components/network/ConnectionsTable.jsx:92 +#, c-format +msgid "Actions for connection %s" +msgstr "Accions per a la connexió %s" + +#. TRANSLATORS: input field name +#: src/components/network/DnsDataList.jsx:78 +#: src/components/network/DnsDataList.jsx:79 +msgid "Server IP" +msgstr "IP del servidor" + +#: src/components/network/DnsDataList.jsx:96 +msgid "Add DNS" +msgstr "Afegeix-hi un DNS" + +#. TRANSLATORS: button label +#: src/components/network/DnsDataList.jsx:96 +msgid "Add another DNS" +msgstr "Afegeix-hi un altre DNS" + +#: src/components/network/DnsDataList.jsx:101 +msgid "DNS" +msgstr "DNS" + +#. TRANSLATORS: input field name +#: src/components/network/IpPrefixInput.jsx:33 +msgid "IP prefix or netmask" +msgstr "Prefix IP o màscara de xarxa" + +#. TRANSLATORS: error message +#: src/components/network/IpSettingsForm.jsx:87 +msgid "At least one address must be provided for selected mode" +msgstr "S'ha de proporcionar almenys una adreça per al mode seleccionat." + +#. TRANSLATORS: %s is replaced by the iSCSI target node name +#: src/components/network/IpSettingsForm.jsx:133 +#: src/components/storage/iscsi/EditNodeForm.jsx:50 +#, c-format +msgid "Edit %s" +msgstr "Edita %s" + +#. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) +#: src/components/network/IpSettingsForm.jsx:136 +#: src/components/network/IpSettingsForm.jsx:141 +#: src/components/network/IpSettingsForm.jsx:143 +msgid "Mode" +msgstr "Mode" + +#: src/components/network/IpSettingsForm.jsx:147 +msgid "Automatic (DHCP)" +msgstr "Automàtic (DHCP)" + +#. TRANSLATORS: manual network configuration mode with a static IP address +#: src/components/network/IpSettingsForm.jsx:149 +#: src/components/storage/iscsi/NodeStartupOptions.js:25 +msgid "Manual" +msgstr "Manual" + +#. TRANSLATORS: network gateway configuration +#: src/components/network/IpSettingsForm.jsx:164 +#: src/components/network/IpSettingsForm.jsx:167 +msgid "Gateway" +msgstr "Passarel·la" + +#: src/components/network/NetworkPage.jsx:38 +msgid "No wired connections found." +msgstr "No s'ha trobat cap connexió amb fil." + +#: src/components/network/NetworkPage.jsx:53 +msgid "" +"The system has not been configured for connecting to a WiFi network yet." +msgstr "" +"El sistema encara no s'ha configurat per connectar-se a una xarxa WiFi." + +#: src/components/network/NetworkPage.jsx:54 +msgid "" +"The system does not support WiFi connections, probably because of missing or " +"disabled hardware." +msgstr "" +"El sistema no admet connexions WiFi, probablement a causa del maquinari que " +"manca o està desactivat." + +#: src/components/network/NetworkPage.jsx:58 +msgid "No WiFi connections found." +msgstr "No s'ha trobat cap connexió WiFi." + +#. TRANSLATORS: button label +#: src/components/network/NetworkPage.jsx:70 +#: src/components/network/NetworkPageMenu.jsx:46 +#: src/components/network/WifiSelector.jsx:134 +msgid "Connect to a Wi-Fi network" +msgstr "Connecteu-vos a una xarxa Wi-Fi" + +#. TRANSLATORS: page section title +#. TRANSLATORS: page title +#: src/components/network/NetworkPage.jsx:169 +#: src/components/overview/NetworkSection.jsx:83 +msgid "Network" +msgstr "Xarxa" + +#. TRANSLATORS: page section +#: src/components/network/NetworkPage.jsx:171 +msgid "Wired networks" +msgstr "Connexions amb fil" + +#. TRANSLATORS: page section +#: src/components/network/NetworkPage.jsx:176 +msgid "WiFi networks" +msgstr "Xarxes WiFi" + +#. TRANSLATORS: WiFi authentication mode +#: src/components/network/WifiConnectionForm.jsx:43 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:72 +msgid "None" +msgstr "Cap" + +#. TRANSLATORS: WiFi authentication mode +#: src/components/network/WifiConnectionForm.jsx:45 +msgid "WPA & WPA2 Personal" +msgstr "WPA i WPA2 personal" + +#: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 +#: src/components/storage/ZFCPDiskForm.jsx:112 +#: src/components/storage/iscsi/DiscoverForm.jsx:108 +#: src/components/storage/iscsi/LoginForm.jsx:72 +#: src/components/users/FirstUser.jsx:238 +msgid "Something went wrong" +msgstr "Alguna cosa ha anat malament." + +#: src/components/network/WifiConnectionForm.jsx:92 +msgid "Please, review provided settings and try again." +msgstr "" +"Si us plau, reviseu la configuració proporcionada i torneu-ho a provar." + +#. TRANSLATORS: SSID (Wifi network name) configuration +#: src/components/network/WifiConnectionForm.jsx:97 +#: src/components/network/WifiConnectionForm.jsx:101 +msgid "SSID" +msgstr "SSID" + +#. TRANSLATORS: Wifi security configuration (password protected or not) +#: src/components/network/WifiConnectionForm.jsx:109 +#: src/components/network/WifiConnectionForm.jsx:112 +msgid "Security" +msgstr "Seguretat" + +#. TRANSLATORS: WiFi password +#: src/components/network/WifiConnectionForm.jsx:121 +msgid "WPA Password" +msgstr "Contrasenya de WPA" + +#. TRANSLATORS: button label, connect to a WiFi network +#: src/components/network/WifiConnectionForm.jsx:133 +msgid "Connect" +msgstr "Connecta't" + +#. TRANSLATORS: button label +#: src/components/network/WifiHiddenNetworkForm.jsx:50 +msgid "Connect to hidden network" +msgstr "Connecta't a una xarxa oculta" + +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:41 +#: src/components/network/WifiNetworkListItem.jsx:99 +msgid "Connecting" +msgstr "Connectant" + +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:44 +msgid "Connected" +msgstr "Connectat" + +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:47 +msgid "Disconnecting" +msgstr "Desconnectant" + +#. TRANSLATORS: iSCSI connection status +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:50 +#: src/components/storage/iscsi/NodesPresenter.jsx:63 +msgid "Disconnected" +msgstr "Desconnectat" + +#. TRANSLATORS: %s is replaced by a WiFi network name +#: src/components/network/WifiNetworkListItem.jsx:97 +#, c-format +msgid "%s connection is waiting for an state change" +msgstr "La connexió %s espera un canvi d'estat." + +#. TRANSLATORS: menu label, remove the selected WiFi network settings +#: src/components/network/WifiNetworkMenu.jsx:55 +msgid "Forget network" +msgstr "Oblida la xarxa" + +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 +#, c-format +msgid "The system will use %s as its default language." +msgstr "El sistema usarà el %s com a llengua per defecte." + +#: src/components/overview/NetworkSection.jsx:59 +msgid "No network connections detected" +msgstr "No s'ha detectat cap connexió de xarxa." + +#. TRANSLATORS: header for the list of active network connections, +#. %d is replaced by the number of active connections +#: src/components/overview/NetworkSection.jsx:68 +#, c-format +msgid "%d connection set:" +msgid_plural "%d connections set:" +msgstr[0] "%d connexió establerta:" +msgstr[1] "%d connexions establertes:" + +#. TRANSLATORS: page title +#: src/components/overview/OverviewPage.jsx:48 +msgid "Installation Summary" +msgstr "Resum de la instal·lació" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "%s (registrat)" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:435 +msgid "Product" +msgstr "Producte" + +#: src/components/overview/SoftwareSection.jsx:38 +msgid "Reading software repositories" +msgstr "Lectura de repositoris de programari" + +#. TRANSLATORS: clickable link label +#: src/components/overview/SoftwareSection.jsx:133 +msgid "Refresh the repositories" +msgstr "Refresca els repositoris" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/overview/SoftwareSection.jsx:143 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "Programari" + +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalDeviceSection.jsx:129 +msgid "No device selected yet" +msgstr "Encara no s'ha seleccionat cap dispositiu." + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:52 +#, c-format +msgid "Install using device %s shrinking existing partitions as needed" +msgstr "" +"Instal·la al dispositiu %s reduint-ne les particions existents segons calgui" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:56 +#, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "Instal·la al dispositiu %s sense modificar-ne les particions existents" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 +#, c-format +msgid "Install using device %s and deleting all its content" +msgstr "Instal·la al dispositiu %s suprimint-ne tot el contingut" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, c-format +msgid "Install using device %s" +msgstr "Instal·la al dispositiu %s" + +#: src/components/overview/StorageSection.jsx:83 +msgid "Probing storage devices" +msgstr "Sondant els dispositius d'emmagatzematge" + +#. TRANSLATORS: page section title +#. TRANSLATORS: page title +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:243 +msgid "Storage" +msgstr "Emmagatzematge" + +#. TRANSLATORS: %s will be replaced by the user name +#: src/components/overview/UsersSection.jsx:80 +#, c-format +msgid "User %s will be created" +msgstr "Es crearà l'usuari %s" + +#: src/components/overview/UsersSection.jsx:87 +msgid "No user defined yet" +msgstr "Encara no s'ha definit cap usuari." + +#: src/components/overview/UsersSection.jsx:101 +msgid "Root authentication set for using both, password and public SSH Key" +msgstr "" +"Autenticació d'arrel establerta per usar contrasenya i clau SSH pública" + +#: src/components/overview/UsersSection.jsx:102 +msgid "No root authentication method defined" +msgstr "No s'ha definit cap mètode d'autenticació d'arrel." + +#: src/components/overview/UsersSection.jsx:103 +msgid "Root authentication set for using password" +msgstr "Autenticació d'arrel establerta per usar contrasenya" + +#: src/components/overview/UsersSection.jsx:104 +msgid "Root authentication set for using public SSH Key" +msgstr "Autenticació d'arrel establerta per usar una clau SSH pública" + +#. TRANSLATORS: page section title +#: src/components/overview/UsersSection.jsx:120 +#: src/components/users/UsersPage.jsx:30 +msgid "Users" +msgstr "Usuaris" + +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:65 +msgid "Choose a product" +msgstr "Trieu un producte" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "Registra %s" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "Dona de baixa %s" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "Voleu donar de baixa el producte %s?" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "Avís de registre" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "" +"El producte %s s'ha de donar de baixa abans de seleccionar un producte nou." + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "Canvia el producte" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "Registra" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "Dona de baixa el producte" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "Codi:" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "Adreça electrònica:" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "Registre" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "Aquest producte requereix registre." + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "Aquest producte no requereix registre." + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "Codi de registre" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "Adreça electrònica" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "Carregant els productes disponibles. Espereu, si us plau..." + +#. TRANSLATORS: page title +#: src/components/product/ProductSelectionPage.jsx:63 +msgid "Product selection" +msgstr "Selecció de producte" + +#: src/components/product/ProductSelectionPage.jsx:72 +msgid "Select" +msgstr "Selecciona" + +#: src/components/product/ProductSelector.jsx:36 +msgid "No products available for selection" +msgstr "No hi ha productes disponibles per a la selecció." + +#: src/components/product/ProductSelector.jsx:42 +msgid "Available products" +msgstr "Productes disponibles" + +#: src/components/questions/GenericQuestion.jsx:35 +#: src/components/questions/LuksActivationQuestion.jsx:60 +msgid "Question" +msgstr "Pregunta" + +#. TRANSLATORS: error message, user entered a wrong password +#: src/components/questions/LuksActivationQuestion.jsx:34 +msgid "Given encryption password didn't work" +msgstr "La contrasenya d'encriptació proporcionada no ha funcionat." + +#: src/components/questions/LuksActivationQuestion.jsx:59 +msgid "Encrypted Device" +msgstr "Dispositiu encriptat" + +#. TRANSLATORS: field label +#: src/components/questions/LuksActivationQuestion.jsx:69 +msgid "Encryption Password" +msgstr "Contrasenya d'encriptació" + +#. TRANSLATORS: pattern status, selected to install (by user) +#: src/components/software/PatternItem.jsx:63 +msgid "selected" +msgstr "seleccionat" + +#. TRANSLATORS: pattern status, selected to install (by dependencies) +#: src/components/software/PatternItem.jsx:66 +msgid "automatically selected" +msgstr "seleccionat automàticament" + +#. TRANSLATORS: pattern status, not selected to install +#: src/components/software/PatternItem.jsx:69 +msgid "not selected" +msgstr "no seleccionat" + +#: src/components/software/PatternSelector.jsx:210 +msgid "Software summary and filter options" +msgstr "Resum de programari i opcions de filtre" + +#. TRANSLATORS: %s will be replaced by the estimated installation size, +#. example: "728.8 MiB" +#: src/components/software/UsedSize.jsx:32 +#, c-format +msgid "Installation will take %s" +msgstr "La instal·lació necessitarà %s" + +#: src/components/storage/DASDFormatProgress.jsx:61 +msgid "Waiting for progress report" +msgstr "Esperant l'informe de progrés" + +#: src/components/storage/DASDFormatProgress.jsx:69 +msgid "Formatting DASD devices" +msgstr "Formatatge de dispositius DASD" + +#. TRANSLATORS: DASD = Direct Access Storage Device, IBM mainframe storage technology +#: src/components/storage/DASDPage.jsx:181 +msgid "Storage DASD" +msgstr "Emmagatzematge DASD" + +#: src/components/storage/DASDTable.jsx:57 +#: src/components/storage/ZFCPPage.jsx:318 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:71 +#: src/components/storage/iscsi/NodesPresenter.jsx:103 +msgid "No" +msgstr "No" + +#: src/components/storage/DASDTable.jsx:57 +#: src/components/storage/ZFCPPage.jsx:318 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:71 +#: src/components/storage/iscsi/NodesPresenter.jsx:103 +msgid "Yes" +msgstr "Sí" + +#: src/components/storage/DASDTable.jsx:64 +#: src/components/storage/ZFCPDiskForm.jsx:118 +#: src/components/storage/ZFCPPage.jsx:301 +#: src/components/storage/ZFCPPage.jsx:361 +msgid "Channel ID" +msgstr "Identificador del canal" + +#. TRANSLATORS: table header +#: src/components/storage/DASDTable.jsx:65 +#: src/components/storage/ZFCPPage.jsx:302 +#: src/components/storage/iscsi/NodesPresenter.jsx:104 +#: src/components/storage/iscsi/NodesPresenter.jsx:125 +#: src/components/users/RootAuthMethods.jsx:157 +msgid "Status" +msgstr "Estat" + +#. TRANSLATORS: The storage "Device" section's title +#: src/components/storage/DASDTable.jsx:66 +#: src/components/storage/ProposalDeviceSection.jsx:423 +msgid "Device" +msgstr "Dispositiu" + +#: src/components/storage/DASDTable.jsx:67 +msgid "Type" +msgstr "Tipus" + +#: src/components/storage/DASDTable.jsx:68 +msgid "Diag" +msgstr "Diag." + +#: src/components/storage/DASDTable.jsx:69 +msgid "Formatted" +msgstr "Formatat" + +#: src/components/storage/DASDTable.jsx:70 +msgid "Partition Info" +msgstr "Informació de la partició" + +#. TRANSLATORS: drop down menu label +#: src/components/storage/DASDTable.jsx:105 +msgid "Perform an action" +msgstr "Fes una acció" + +#. TRANSLATORS: drop down menu action, activate the device +#: src/components/storage/DASDTable.jsx:111 +#: src/components/storage/ZFCPPage.jsx:332 +msgid "Activate" +msgstr "Activa" + +#. TRANSLATORS: drop down menu action, deactivate the device +#: src/components/storage/DASDTable.jsx:113 +#: src/components/storage/ZFCPPage.jsx:374 +msgid "Deactivate" +msgstr "Desactiva" + +#. TRANSLATORS: drop down menu action, enable DIAG access method +#: src/components/storage/DASDTable.jsx:116 +msgid "Set DIAG On" +msgstr "Activa la diagnosi" + +#. TRANSLATORS: drop down menu action, disable DIAG access method +#: src/components/storage/DASDTable.jsx:118 +msgid "Set DIAG Off" +msgstr "Desactiva la diagnosi" + +#. TRANSLATORS: drop down menu action, format the disk +#: src/components/storage/DASDTable.jsx:121 +msgid "Format" +msgstr "Formata" + +#: src/components/storage/DASDTable.jsx:198 +#: src/components/storage/DASDTable.jsx:199 +msgid "Filter by min channel" +msgstr "Filtra per canal mínim" + +#: src/components/storage/DASDTable.jsx:206 +msgid "Remove min channel filter" +msgstr "Suprimeix el filtre del canal mínim" + +#: src/components/storage/DASDTable.jsx:219 +#: src/components/storage/DASDTable.jsx:220 +msgid "Filter by max channel" +msgstr "Filtra per canal màxim" + +#: src/components/storage/DASDTable.jsx:227 +msgid "Remove max channel filter" +msgstr "Suprimeix el filtre de canal màxim" + +#. TRANSLATORS: page title for iSCSI configuration +#: src/components/storage/ISCSIPage.jsx:31 +msgid "Storage iSCSI" +msgstr "Emmagatzematge iSCSI" + +#. TRANSLATORS: show/hide toggle action, this is a clickable link +#: src/components/storage/ProposalActionsSection.jsx:70 +#, c-format +msgid "Hide %d subvolume action" +msgid_plural "Hide %d subvolume actions" +msgstr[0] "Amaga %d acció de subvolum" +msgstr[1] "Amaga %d accions de subvolum" + +#. TRANSLATORS: show/hide toggle action, this is a clickable link +#: src/components/storage/ProposalActionsSection.jsx:72 +#, c-format +msgid "Show %d subvolume action" +msgid_plural "Show %d subvolume actions" +msgstr[0] "Mostra %d acció de subvolum" +msgstr[1] "Mostra %d accions de subvolum" + +#. TRANSLATORS: The storage "Planned Actions" section's title. The +#. section shows a list of planned actions for the selected device, e.g. +#. "delete partition A", "create partition B with filesystem C", ... +#: src/components/storage/ProposalActionsSection.jsx:124 +msgid "Planned Actions" +msgstr "Accions planificades" + +#. TRANSLATORS: The storage "Planned Actions" section's description +#: src/components/storage/ProposalActionsSection.jsx:126 +msgid "Actions to create the file systems and to ensure the new system boots." +msgstr "" +"Accions per crear els sistemes de fitxers i per garantir l'arrencada del " +"sistema nou." + +#: src/components/storage/ProposalDeviceSection.jsx:135 +msgid "Waiting for information about selected device" +msgstr "Esperant informació sobre el dispositiu seleccionat" + +#: src/components/storage/ProposalDeviceSection.jsx:138 +msgid "Select the device for installing the system." +msgstr "Seleccioneu el dispositiu on instal·lar el sistema." + +#: src/components/storage/ProposalDeviceSection.jsx:143 +#: src/components/storage/ProposalDeviceSection.jsx:147 +#: src/components/storage/ProposalDeviceSection.jsx:245 +msgid "Installation device" +msgstr "Dispositiu d'instal·lació" + +#: src/components/storage/ProposalDeviceSection.jsx:153 +msgid "No devices found." +msgstr "No s'ha trobat cap dispositiu." + +#: src/components/storage/ProposalDeviceSection.jsx:242 +msgid "Devices for creating the volume group" +msgstr "Dispositius per crear el grup de volums" + +#: src/components/storage/ProposalDeviceSection.jsx:251 +msgid "Custom devices" +msgstr "Dispositius personalitzats" + +#: src/components/storage/ProposalDeviceSection.jsx:315 +msgid "" +"Configuration of the system volume group. All the file systems will be " +"created in a logical volume of the system volume group." +msgstr "" +"Configuració del grup de volums del sistema. Tots els sistemes de fitxers es " +"crearan en un volum lògic del grup de volums del sistema." + +#: src/components/storage/ProposalDeviceSection.jsx:321 +msgid "Configure the LVM settings" +msgstr "Configuració dels paràmetres d'LVM" + +#: src/components/storage/ProposalDeviceSection.jsx:326 +#: src/components/storage/ProposalDeviceSection.jsx:346 +msgid "LVM settings" +msgstr "Paràmetres d'LVM" + +#: src/components/storage/ProposalDeviceSection.jsx:333 +msgid "Waiting for information about LVM" +msgstr "Esperant informació sobre LVM" + +#: src/components/storage/ProposalDeviceSection.jsx:339 +msgid "Use logical volume management (LVM)" +msgstr "Usa la gestió de volums lògics (LVM)" + +#: src/components/storage/ProposalDeviceSection.jsx:347 +msgid "System Volume Group" +msgstr "Grup de volums del sistema" + +#. TRANSLATORS: The storage "Device" sections's description. Do not +#. translate 'abbr' and 'title', they are part of the HTML markup. +#: src/components/storage/ProposalDeviceSection.jsx:414 +msgid "" +"Select the main disk or LVM " +"Volume Group for installation." +msgstr "" +"Seleccioneu el disc principal o el grup de volums LVM per a la instal·lació." + +#: src/components/storage/ProposalFileSystemsSection.jsx:70 +msgid "File systems" +msgstr "Sistemes de fitxers" + +#: src/components/storage/ProposalPageMenu.jsx:40 +msgid "Manage and format" +msgstr "Gestió i formatatge" + +#: src/components/storage/ProposalPageMenu.jsx:58 +msgid "Activate disks" +msgstr "Activa els discs" + +#: src/components/storage/ProposalPageMenu.jsx:60 +msgid "zFCP" +msgstr "zFCP" + +#: src/components/storage/ProposalPageMenu.jsx:76 +msgid "Connect to iSCSI targets" +msgstr "Connecta amb objectius iSCSI" + +#: src/components/storage/ProposalPageMenu.jsx:78 +msgid "iSCSI" +msgstr "iSCSI" + +#. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing +#. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. +#. Do not translate 'abbr' and 'title', they are part of the HTML markup. +#: src/components/storage/ProposalSettingsSection.jsx:87 +msgid "" +"The password will not be needed to boot and access the data if the TPM can verify the integrity of the " +"system. TPM sealing requires the new system to be booted directly on its " +"first run." +msgstr "" +"La contrasenya no serà necessària per arrencar i accedir a les dades si el " +"TPM pot verificar la integritat " +"del sistema. El segellament de TPM requereix que el sistema nou s'iniciï " +"directament a la primera execució." + +#: src/components/storage/ProposalSettingsSection.jsx:105 +msgid "Use the TPM to decrypt automatically on each boot" +msgstr "Usa el TPM per a la desencriptació automàtica a cada arrencada" + +#: src/components/storage/ProposalSettingsSection.jsx:148 +msgid "" +"Uses Btrfs for the root file system allowing to boot to a previous version " +"of the system after configuration changes or software upgrades." +msgstr "" +"Usa Btrfs per al sistema de fitxers d'arrel, que permet arrencar amb una " +"versió anterior del sistema després de canvis de configuració o " +"actualitzacions de programari." + +#: src/components/storage/ProposalSettingsSection.jsx:155 +msgid "Use Btrfs Snapshots" +msgstr "Usa instantànies de Btrfs" + +#: src/components/storage/ProposalSettingsSection.jsx:227 +msgid "Change encryption settings" +msgstr "Canvia la configuració de l'encriptació" + +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 +msgid "Encryption settings" +msgstr "Configuració de l'encriptació" + +#: src/components/storage/ProposalSettingsSection.jsx:246 +msgid "Use encryption" +msgstr "Usa encriptació" + +#: src/components/storage/ProposalSettingsSection.jsx:309 +msgid "Settings" +msgstr "Configuració" + +#: src/components/storage/ProposalSpacePolicySection.jsx:50 +msgid "Delete current content" +msgstr "Suprimeix el contingut actual" + +#: src/components/storage/ProposalSpacePolicySection.jsx:51 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "Totes les particions se suprimiran i es perdran les dades dels discs." + +#: src/components/storage/ProposalSpacePolicySection.jsx:55 +msgid "Shrink existing partitions" +msgstr "Redueix les particions existents" + +#: src/components/storage/ProposalSpacePolicySection.jsx:56 +msgid "The data is kept, but the current partitions will be resized as needed." +msgstr "" +"Les dades es conserven, però les particions actuals es canviaran de mida " +"segons calgui." + +#: src/components/storage/ProposalSpacePolicySection.jsx:60 +msgid "Use available space" +msgstr "Usa l'espai disponible" + +#: src/components/storage/ProposalSpacePolicySection.jsx:61 +msgid "" +"The data is kept. Only the space not assigned to any partition will be used." +msgstr "" +"Es conserven les dades. Només s'usarà l'espai no assignat a cap partició." + +#: src/components/storage/ProposalSpacePolicySection.jsx:65 +msgid "Custom" +msgstr "Personalitzat" + +#: src/components/storage/ProposalSpacePolicySection.jsx:66 +msgid "Select what to do with each partition." +msgstr "Seleccioneu què voleu fer amb cada partició." + +#: src/components/storage/ProposalSpacePolicySection.jsx:72 +msgid "Used device" +msgstr "Dispositiu usat" + +#: src/components/storage/ProposalSpacePolicySection.jsx:73 +msgid "Current content" +msgstr "Contingut actual" + +#: src/components/storage/ProposalSpacePolicySection.jsx:74 +#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/VolumeForm.jsx:741 +msgid "Size" +msgstr "Mida" + +#: src/components/storage/ProposalSpacePolicySection.jsx:75 +#: src/components/storage/ProposalVolumes.jsx:308 +msgid "Details" +msgstr "Detalls" + +#: src/components/storage/ProposalSpacePolicySection.jsx:76 +msgid "Action" +msgstr "Acció" + +#. TRANSLATORS: %s is replaced by partition table type (e.g., GPT) +#: src/components/storage/ProposalSpacePolicySection.jsx:110 +#, c-format +msgid "%s partition table" +msgstr "Taula de particions %s" + +#: src/components/storage/ProposalSpacePolicySection.jsx:121 +msgid "EFI system partition" +msgstr "Partició de sistema EFI" + +#. TRANSLATORS: %s is replaced by a file system type (e.g., btrfs). +#: src/components/storage/ProposalSpacePolicySection.jsx:124 +#, c-format +msgid "%s file system" +msgstr "Sistema de fitxers %s" + +#. TRANSLATORS: %s is replaced by a LVM volume group name (e.g., /dev/vg0). +#: src/components/storage/ProposalSpacePolicySection.jsx:131 +#, c-format +msgid "LVM physical volume of %s" +msgstr "Volum físic d'LVM %s" + +#. TRANSLATORS: %s is replaced by a RAID name (e.g., /dev/md0). +#: src/components/storage/ProposalSpacePolicySection.jsx:134 +#, c-format +msgid "Member of RAID %s" +msgstr "Membre de la RAID %s" + +#: src/components/storage/ProposalSpacePolicySection.jsx:136 +msgid "Not identified" +msgstr "Sense identificació" + +#. TRANSLATORS: %s is replaced by a disk size (e.g., 20 GiB) +#: src/components/storage/ProposalSpacePolicySection.jsx:173 +#, c-format +msgid "%s unused" +msgstr "%s sense ús" + +#. TRANSLATORS: %s is replaced by a disk size (e.g., 2 GiB) +#: src/components/storage/ProposalSpacePolicySection.jsx:186 +#, c-format +msgid "Shrinkable by %s" +msgstr "Es pot reduir %s" + +#. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) +#: src/components/storage/ProposalSpacePolicySection.jsx:221 +#, c-format +msgid "Space action selector for %s" +msgstr "Selector d'acció espacial per a %s" + +#: src/components/storage/ProposalSpacePolicySection.jsx:224 +#: src/components/storage/ProposalVolumes.jsx:229 +#: src/components/storage/iscsi/NodesPresenter.jsx:77 +msgid "Delete" +msgstr "Suprimeix" + +#: src/components/storage/ProposalSpacePolicySection.jsx:228 +msgid "Allow resize" +msgstr "Permet-ne el canvi de mida" + +#: src/components/storage/ProposalSpacePolicySection.jsx:230 +msgid "Do not modify" +msgstr "No la modifiquis" + +#: src/components/storage/ProposalSpacePolicySection.jsx:382 +msgid "Actions to find space" +msgstr "Accions per aconseguir espai" + +#. TRANSLATORS: The storage "Find Space" section's title +#: src/components/storage/ProposalSpacePolicySection.jsx:452 +msgid "Find Space" +msgstr "Aconseguir espai" + +#. TRANSLATORS: The storage "Find space" sections's description +#: src/components/storage/ProposalSpacePolicySection.jsx:454 +msgid "" +"Allocating the file systems might need to find free space in the devices " +"listed below. Choose how to do it." +msgstr "" +"L'assignació dels sistemes de fitxers pot necessitar trobar espai lliure als " +"dispositius que s'indiquen a continuació. Trieu com fer-ho." + +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "Sistema transaccional" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, fuzzy, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" +"%s és un sistema immutable amb actualitzacions atòmiques que usa un sistema " +"de fitxers d'arrel Btrfs només de lectura." + +#. TRANSLATORS: header for a list of items +#: src/components/storage/ProposalVolumes.jsx:59 +msgid "These limits are affected by:" +msgstr "Aquests límits estan afectats pel següent:" + +#. TRANSLATORS: list item, this affects the computed partition size limits +#: src/components/storage/ProposalVolumes.jsx:63 +msgid "The configuration of snapshots" +msgstr "La configuració de les instantànies" + +#. TRANSLATORS: list item, this affects the computed partition size limits +#. %s is replaced by a list of the volumes (like "/home, /boot") +#: src/components/storage/ProposalVolumes.jsx:67 +#, c-format +msgid "Presence of other volumes (%s)" +msgstr "La presència d'altres volums (%s)" + +#. TRANSLATORS: dropdown menu label +#: src/components/storage/ProposalVolumes.jsx:129 +msgid "Reset to defaults" +msgstr "Restableix els valors predeterminats" + +#. TRANSLATORS: dropdown menu label +#: src/components/storage/ProposalVolumes.jsx:137 +#: src/components/storage/ProposalVolumes.jsx:141 +msgid "Add file system" +msgstr "Afegeix-hi un sistema de fitxers" + +#. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" +#: src/components/storage/ProposalVolumes.jsx:191 +#, c-format +msgid "At least %s" +msgstr "Almenys %s" + +#. TRANSLATORS: device flag, the partition size is automatically computed +#: src/components/storage/ProposalVolumes.jsx:197 +msgid "auto" +msgstr "automàtica" + +#. TRANSLATORS: the filesystem uses a logical volume (LVM) +#: src/components/storage/ProposalVolumes.jsx:207 +msgid "logical volume" +msgstr "volum lògic" + +#: src/components/storage/ProposalVolumes.jsx:207 +msgid "partition" +msgstr "partició" + +#. TRANSLATORS: filesystem flag, it uses an encryption +#: src/components/storage/ProposalVolumes.jsx:216 +msgid "encrypted" +msgstr "encriptada" + +#. TRANSLATORS: filesystem flag, it allows creating snapshots +#: src/components/storage/ProposalVolumes.jsx:218 +msgid "with snapshots" +msgstr "amb instantànies" + +#. TRANSLATORS: flag for transactional file system +#: src/components/storage/ProposalVolumes.jsx:220 +msgid "transactional" +msgstr "transaccional" + +#: src/components/storage/ProposalVolumes.jsx:275 +msgid "Edit file system" +msgstr "Edita el sistema de fitxers" + +#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/VolumeForm.jsx:726 +#: src/components/storage/VolumeForm.jsx:731 +msgid "Mount point" +msgstr "Punt de muntatge" + +#: src/components/storage/ProposalVolumes.jsx:345 +msgid "Table with mount points" +msgstr "Taula amb punts de muntatge" + +#: src/components/storage/VolumeForm.jsx:102 +msgid "Select a value" +msgstr "Seleccioneu un valor" + +#. TRANSLATORS: info about possible file system types. +#: src/components/storage/VolumeForm.jsx:255 +msgid "" +"The options for the file system type depends on the product and the mount " +"point." +msgstr "" +"Les opcions per al tipus de sistema de fitxers depenen del producte i del " +"punt de muntatge." + +#: src/components/storage/VolumeForm.jsx:261 +msgid "More info for file system types" +msgstr "Més informació sobre els tipus de sistemes de fitxers" + +#. TRANSLATORS: label for the file system selector. +#: src/components/storage/VolumeForm.jsx:272 +msgid "File system type" +msgstr "Tipus de sistema de fitxers" + +#. TRANSLATORS: item which affects the final computed partition size +#: src/components/storage/VolumeForm.jsx:304 +msgid "the configuration of snapshots" +msgstr "la configuració de les instantànies" + +#. TRANSLATORS: item which affects the final computed partition size +#. %s is replaced by a list of mount points like "/home, /boot" +#: src/components/storage/VolumeForm.jsx:309 +#, c-format +msgid "the presence of the file system for %s" +msgstr "la presència del sistema de fitxers per a %s" + +#. TRANSLATORS: conjunction for merging two list items +#: src/components/storage/VolumeForm.jsx:311 +msgid ", " +msgstr ", " + +#. TRANSLATORS: the %s is replaced by the items which affect the computed size +#: src/components/storage/VolumeForm.jsx:314 +#, c-format +msgid "The final size depends on %s." +msgstr "La mida final depèn de %s." + +#. TRANSLATORS: conjunction for merging two texts +#: src/components/storage/VolumeForm.jsx:316 +msgid " and " +msgstr " i " + +#. TRANSLATORS: the partition size is automatically computed +#: src/components/storage/VolumeForm.jsx:321 +msgid "Automatically calculated size according to the selected product." +msgstr "Mida calculada automàticament segons el producte seleccionat." + +#: src/components/storage/VolumeForm.jsx:341 +msgid "Exact size for the file system." +msgstr "Mida exacta per al sistema de fitxers." + +#. TRANSLATORS: requested partition size +#: src/components/storage/VolumeForm.jsx:353 +msgid "Exact size" +msgstr "Mida exacta" + +#. TRANSLATORS: units selector (like KiB, MiB, GiB...) +#: src/components/storage/VolumeForm.jsx:368 +msgid "Size unit" +msgstr "Unitat de mida" + +#: src/components/storage/VolumeForm.jsx:396 +msgid "" +"Limits for the file system size. The final size will be a value between the " +"given minimum and maximum. If no maximum is given then the file system will " +"be as big as possible." +msgstr "" +"Límits per a la mida del sistema de fitxers. La mida final serà un valor " +"entre el mínim i el màxim proporcionats. Si no hi ha cap màxim, el sistema " +"de fitxers serà el més gros possible." + +#. TRANSLATORS: the minimal partition size +#: src/components/storage/VolumeForm.jsx:403 +msgid "Minimum" +msgstr "Mínim" + +#. TRANSLATORS: the minium partition size +#: src/components/storage/VolumeForm.jsx:413 +msgid "Minimum desired size" +msgstr "Mida mínima desitjada" + +#: src/components/storage/VolumeForm.jsx:422 +msgid "Unit for the minimum size" +msgstr "Unitat per a la mida mínima" + +#. TRANSLATORS: the maximum partition size +#: src/components/storage/VolumeForm.jsx:433 +msgid "Maximum" +msgstr "Màxim" + +#. TRANSLATORS: the maximum partition size +#: src/components/storage/VolumeForm.jsx:444 +msgid "Maximum desired size" +msgstr "Mida màxima desitjada" + +#: src/components/storage/VolumeForm.jsx:452 +msgid "Unit for the maximum size" +msgstr "Unitat per a la mida màxima" + +#. TRANSLATORS: radio button label, fully automatically computed partition size, no user input +#: src/components/storage/VolumeForm.jsx:469 +msgid "Auto" +msgstr "Automàtica" + +#. TRANSLATORS: radio button label, exact partition size requested by user +#: src/components/storage/VolumeForm.jsx:471 +msgid "Fixed" +msgstr "Fixa" + +#. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits +#: src/components/storage/VolumeForm.jsx:473 +msgid "Range" +msgstr "Interval" + +#: src/components/storage/VolumeForm.jsx:681 +msgid "A size value is required" +msgstr "Cal un valor de mida" + +#: src/components/storage/VolumeForm.jsx:686 +msgid "Minimum size is required" +msgstr "Cal una mida mínima" + +#: src/components/storage/VolumeForm.jsx:690 +msgid "Maximum must be greater than minimum" +msgstr "El màxim ha de ser superior al mínim." + +#: src/components/storage/ZFCPDiskForm.jsx:113 +msgid "The zFCP disk was not activated." +msgstr "El disc zFCP no s'ha activat." + +#. TRANSLATORS: abbrev. World Wide Port Name +#: src/components/storage/ZFCPDiskForm.jsx:129 +#: src/components/storage/ZFCPPage.jsx:362 +msgid "WWPN" +msgstr "WWPN" + +#. TRANSLATORS: abbrev. Logical Unit Number +#: src/components/storage/ZFCPDiskForm.jsx:140 +#: src/components/storage/ZFCPPage.jsx:363 +msgid "LUN" +msgstr "LUN" + +#: src/components/storage/ZFCPPage.jsx:303 +msgid "Auto LUNs Scan" +msgstr "Escaneig automàtic de LUN" + +#: src/components/storage/ZFCPPage.jsx:314 +msgid "Activated" +msgstr "Activat" + +#: src/components/storage/ZFCPPage.jsx:314 +msgid "Deactivated" +msgstr "Desactivat" + +#: src/components/storage/ZFCPPage.jsx:417 +msgid "No zFCP controllers found." +msgstr "No s'ha trobat cap controlador de zFCP." + +#: src/components/storage/ZFCPPage.jsx:418 +msgid "Please, try to read the zFCP devices again." +msgstr "Si us plau, intenteu tornar a llegir els dispositius zFCP." + +#. TRANSLATORS: button label +#: src/components/storage/ZFCPPage.jsx:420 +msgid "Read zFCP devices" +msgstr "Llegeix els dispositius zFCP" + +#. TRANSLATORS: the text in the square brackets [] will be displayed in bold +#: src/components/storage/ZFCPPage.jsx:429 +msgid "" +"Automatic LUN scan is [enabled]. Activating a controller which is running in " +"NPIV mode will automatically configures all its LUNs." +msgstr "" +"L'exploració automàtica de LUN està [activada]. L'activació d'un controlador " +"que s'executa en mode NPIV configurarà automàticament tots els seus LUN." + +#. TRANSLATORS: the text in the square brackets [] will be displayed in bold +#: src/components/storage/ZFCPPage.jsx:432 +msgid "" +"Automatic LUN scan is [disabled]. LUNs have to be manually configured after " +"activating a controller." +msgstr "" +"L'exploració automàtica de LUN està [desactivada]. Els LUN s'han de " +"configurar manualment després d'activar un controlador." + +#: src/components/storage/ZFCPPage.jsx:498 +msgid "Activate a zFCP disk" +msgstr "Activa un disc zFCP" + +#: src/components/storage/ZFCPPage.jsx:537 +msgid "Please, try to activate a zFCP controller." +msgstr "Si us plau, proveu d'activar un controlador de zFCP." + +#: src/components/storage/ZFCPPage.jsx:544 +msgid "Please, try to activate a zFCP disk." +msgstr "Si us plau, proveu d'activar un disc zFCP." + +#. TRANSLATORS: button label +#: src/components/storage/ZFCPPage.jsx:546 +msgid "Activate zFCP disk" +msgstr "Activa el disc zFCP" + +#: src/components/storage/ZFCPPage.jsx:553 +msgid "No zFCP disks found." +msgstr "No s'ha trobat cap disc zFCP." + +#. TRANSLATORS: button label +#: src/components/storage/ZFCPPage.jsx:572 +msgid "Activate new disk" +msgstr "Activa el disc nou" + +#. TRANSLATORS: section title +#: src/components/storage/ZFCPPage.jsx:584 +msgid "Disks" +msgstr "Discs" + +#. TRANSLATORS: page title +#: src/components/storage/ZFCPPage.jsx:731 +msgid "Storage zFCP" +msgstr "Emmagatzematge zFCP" + +#. TRANSLATORS: multipath device type +#: src/components/storage/device-utils.jsx:82 +msgid "Multipath" +msgstr "Multicamí" + +#. TRANSLATORS: %s is replaced by the device bus ID +#: src/components/storage/device-utils.jsx:87 +#, c-format +msgid "DASD %s" +msgstr "DASD %s" + +#. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 +#: src/components/storage/device-utils.jsx:92 +#, c-format +msgid "Software %s" +msgstr "Programari %s" + +#: src/components/storage/device-utils.jsx:97 +msgid "SD Card" +msgstr "Targeta SD" + +#. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... +#: src/components/storage/device-utils.jsx:99 +#, c-format +msgid "Transport %s" +msgstr "Transport %s" + +#. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array +#: src/components/storage/device-utils.jsx:118 +#, c-format +msgid "Members: %s" +msgstr "Membres: %s" + +#. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array +#: src/components/storage/device-utils.jsx:127 +#, c-format +msgid "Devices: %s" +msgstr "Dispositius: %s" + +#. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device +#: src/components/storage/device-utils.jsx:136 +#, c-format +msgid "Wires: %s" +msgstr "Cables: %s" + +#. TRANSLATORS: disk partition info, %s is replaced by partition table +#. type (MS-DOS or GPT), %d is the number of the partitions +#: src/components/storage/device-utils.jsx:160 +#, c-format +msgid "%s with %d partitions" +msgstr "%s amb %d particions" + +#. TRANSLATORS: status message, no existing content was found on the disk, +#. i.e. the disk is completely empty +#: src/components/storage/device-utils.jsx:184 +msgid "No content found" +msgstr "No s'ha trobat contingut." + +#: src/components/storage/device-utils.jsx:258 +msgid "Available devices" +msgstr "Dispositius disponibles" + +#: src/components/storage/iscsi/AuthFields.jsx:70 +msgid "Only available if authentication by target is provided" +msgstr "Només està disponible si es proporciona l'autenticació per destinació." + +#: src/components/storage/iscsi/AuthFields.jsx:77 +msgid "Authentication by target" +msgstr "Autenticació per destinació" + +#: src/components/storage/iscsi/AuthFields.jsx:80 +#: src/components/storage/iscsi/AuthFields.jsx:85 +#: src/components/storage/iscsi/AuthFields.jsx:87 +#: src/components/storage/iscsi/AuthFields.jsx:112 +#: src/components/storage/iscsi/AuthFields.jsx:117 +#: src/components/storage/iscsi/AuthFields.jsx:119 +msgid "User name" +msgstr "Nom d'usuari" + +#: src/components/storage/iscsi/AuthFields.jsx:91 +#: src/components/storage/iscsi/AuthFields.jsx:124 +msgid "Incorrect user name" +msgstr "Nom d'usuari incorrecte" + +#: src/components/storage/iscsi/AuthFields.jsx:105 +#: src/components/storage/iscsi/AuthFields.jsx:139 +msgid "Incorrect password" +msgstr "Contrasenya incorrecta" + +#: src/components/storage/iscsi/AuthFields.jsx:108 +msgid "Authentication by initiator" +msgstr "Autenticació per iniciador" + +#: src/components/storage/iscsi/AuthFields.jsx:133 +msgid "Target Password" +msgstr "Contrasenya de destinació" + +#. TRANSLATORS: popup title +#: src/components/storage/iscsi/DiscoverForm.jsx:101 +msgid "Discover iSCSI Targets" +msgstr "Descobreix les destinacions iSCSI" + +#: src/components/storage/iscsi/DiscoverForm.jsx:110 +#: src/components/storage/iscsi/LoginForm.jsx:73 +msgid "Make sure you provide the correct values" +msgstr "Assegureu-vos que proporcioneu els valors correctes" + +#: src/components/storage/iscsi/DiscoverForm.jsx:115 +msgid "IP address" +msgstr "Adreça IP" + +#. TRANSLATORS: network address +#: src/components/storage/iscsi/DiscoverForm.jsx:122 +#: src/components/storage/iscsi/DiscoverForm.jsx:124 +msgid "Address" +msgstr "Adreça" + +#: src/components/storage/iscsi/DiscoverForm.jsx:129 +msgid "Incorrect IP address" +msgstr "Adreça IP incorrecta" + +#. TRANSLATORS: network port number +#: src/components/storage/iscsi/DiscoverForm.jsx:133 +#: src/components/storage/iscsi/DiscoverForm.jsx:140 +#: src/components/storage/iscsi/DiscoverForm.jsx:142 +msgid "Port" +msgstr "Port" + +#: src/components/storage/iscsi/DiscoverForm.jsx:147 +msgid "Incorrect port" +msgstr "Port incorrecte" + +#: src/components/storage/iscsi/InitiatorForm.jsx:42 +msgid "Edit iSCSI Initiator" +msgstr "Edita l'iniciador iSCSI" + +#. TRANSLATORS: iSCSI initiator name +#: src/components/storage/iscsi/InitiatorForm.jsx:49 +msgid "Initiator name" +msgstr "Nom de l'iniciador" + +#. TRANSLATORS: usually just keep the original text +#. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI +#: src/components/storage/iscsi/InitiatorPresenter.jsx:71 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:86 +#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:124 +msgid "iBFT" +msgstr "iBFT" + +#: src/components/storage/iscsi/InitiatorPresenter.jsx:72 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:87 +msgid "Offload card" +msgstr "Targeta de descàrrega" + +#. TRANSLATORS: iSCSI initiator section name +#: src/components/storage/iscsi/InitiatorSection.jsx:50 +msgid "Initiator" +msgstr "Iniciador" + +#. TRANSLATORS: %s is replaced by the iSCSI target name +#: src/components/storage/iscsi/LoginForm.jsx:69 +#, c-format +msgid "Login %s" +msgstr "Entrada per a %s" + +#. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) +#: src/components/storage/iscsi/LoginForm.jsx:76 +#: src/components/storage/iscsi/LoginForm.jsx:79 +msgid "Startup" +msgstr "Inici" + +#: src/components/storage/iscsi/NodeStartupOptions.js:26 +msgid "On boot" +msgstr "A l'arrencada" + +#: src/components/storage/iscsi/NodeStartupOptions.js:27 +msgid "Automatic" +msgstr "Automàtica" + +#. TRANSLATORS: iSCSI connection status, %s is replaced by node label +#: src/components/storage/iscsi/NodesPresenter.jsx:67 +#, c-format +msgid "Connected (%s)" +msgstr "Connectat (%s)" + +#: src/components/storage/iscsi/NodesPresenter.jsx:82 +msgid "Login" +msgstr "Entrada" + +#: src/components/storage/iscsi/NodesPresenter.jsx:86 +msgid "Logout" +msgstr "Sortida" + +#: src/components/storage/iscsi/NodesPresenter.jsx:101 +#: src/components/storage/iscsi/NodesPresenter.jsx:122 +msgid "Portal" +msgstr "Portal" + +#: src/components/storage/iscsi/NodesPresenter.jsx:102 +#: src/components/storage/iscsi/NodesPresenter.jsx:123 +msgid "Interface" +msgstr "Interfície" + +#: src/components/storage/iscsi/TargetsSection.jsx:139 +msgid "No iSCSI targets found." +msgstr "No s'ha trobat cap destinació iSCSI." + +#: src/components/storage/iscsi/TargetsSection.jsx:140 +msgid "" +"Please, perform an iSCSI discovery in order to find available iSCSI targets." +msgstr "" +"Si us plau, executeu un descobriment d'iSCSI per trobar destinacions iSCSI " +"disponibles." + +#. TRANSLATORS: button label, starts iSCSI discovery +#: src/components/storage/iscsi/TargetsSection.jsx:142 +msgid "Discover iSCSI targets" +msgstr "Descobreix destinacions iSCSI" + +#. TRANSLATORS: button label, starts iSCSI discovery +#: src/components/storage/iscsi/TargetsSection.jsx:153 +msgid "Discover" +msgstr "Descobreix" + +#. TRANSLATORS: iSCSI targets section title +#: src/components/storage/iscsi/TargetsSection.jsx:167 +msgid "Targets" +msgstr "Destinacions" + +#: src/components/storage/utils.js:49 +msgid "KiB" +msgstr "KiB" + +#: src/components/storage/utils.js:50 +msgid "MiB" +msgstr "MiB" + +#: src/components/storage/utils.js:51 +msgid "GiB" +msgstr "GiB" + +#: src/components/storage/utils.js:52 +msgid "TiB" +msgstr "TiB" + +#: src/components/storage/utils.js:53 +msgid "PiB" +msgstr "PiB" + +#: src/components/users/FirstUser.jsx:50 +msgid "No user defined yet." +msgstr "Encara no s'ha definit cap usuari." + +#: src/components/users/FirstUser.jsx:53 +msgid "" +"Please, be aware that a user must be defined before installing the system to " +"be able to log into it." +msgstr "" +"Si us plau, tingueu en compte que cal definir un usuari abans d'instal·lar " +"el sistema per poder-hi iniciar sessió." + +#. TRANSLATORS: push button label +#: src/components/users/FirstUser.jsx:57 +msgid "Define a user now" +msgstr "Definiu un usuari ara" + +#: src/components/users/FirstUser.jsx:67 src/components/users/FirstUser.jsx:242 +msgid "Full name" +msgstr "Nom complet" + +#: src/components/users/FirstUser.jsx:68 src/components/users/FirstUser.jsx:256 +#: src/components/users/FirstUser.jsx:264 +#: src/components/users/FirstUser.jsx:266 +msgid "Username" +msgstr "Nom d'usuari" + +#: src/components/users/FirstUser.jsx:88 +msgid "Username suggestion dropdown" +msgstr "Menú desplegable de suggeriments de nom d'usuari" + +#. TRANSLATORS: dropdown username suggestions +#: src/components/users/FirstUser.jsx:102 +msgid "Use suggested username" +msgstr "Usa el nom d'usuari suggerit" + +#: src/components/users/FirstUser.jsx:212 +#: src/components/users/RootAuthMethods.jsx:99 +#: src/components/users/RootAuthMethods.jsx:111 +msgid "Discard" +msgstr "Descarta'l" + +#: src/components/users/FirstUser.jsx:235 +msgid "Create user account" +msgstr "Crea un compte d'usuari" + +#: src/components/users/FirstUser.jsx:235 +msgid "Edit user account" +msgstr "Edita el compte d'usuari" + +#: src/components/users/FirstUser.jsx:246 +#: src/components/users/FirstUser.jsx:248 +msgid "User full name" +msgstr "Nom complet de l'usuari" + +#. TRANSLATORS: check box label +#: src/components/users/FirstUser.jsx:284 +#: src/components/users/FirstUser.jsx:288 +msgid "Edit password too" +msgstr "Edita també la contrasenya" + +#: src/components/users/FirstUser.jsx:301 +msgid "user autologin" +msgstr "entrada de sessió automàtica de l'usuari" + +#. TRANSLATORS: check box label +#: src/components/users/FirstUser.jsx:305 +msgid "Auto-login" +msgstr "Entrada automàtica" + +#: src/components/users/RootAuthMethods.jsx:35 +msgid "No root authentication method defined yet." +msgstr "Encara no s'ha definit cap mètode d'autenticació d'arrel." + +#: src/components/users/RootAuthMethods.jsx:38 +msgid "" +"Please, define at least one authentication method for logging into the " +"system as root." +msgstr "" +"Si us plau, definiu almenys un mètode d'autenticació per iniciar sessió al " +"sistema com a arrel." + +#. TRANSLATORS: push button label +#: src/components/users/RootAuthMethods.jsx:43 +msgid "Set a password" +msgstr "Establiu una contrasenya" + +#. TRANSLATORS: push button label +#: src/components/users/RootAuthMethods.jsx:45 +msgid "Upload a SSH Public Key" +msgstr "Carrega una clau pública SSH" + +#: src/components/users/RootAuthMethods.jsx:94 +#: src/components/users/RootAuthMethods.jsx:107 +msgid "Change" +msgstr "Canvia" + +#: src/components/users/RootAuthMethods.jsx:94 +#: src/components/users/RootAuthMethods.jsx:107 +msgid "Set" +msgstr "Estableix" + +#: src/components/users/RootAuthMethods.jsx:129 +msgid "Already set" +msgstr "Ja s'ha establert" + +#: src/components/users/RootAuthMethods.jsx:130 +#: src/components/users/RootAuthMethods.jsx:134 +msgid "Not set" +msgstr "No s'ha establert" + +#. TRANSLATORS: table header, user authentication method +#: src/components/users/RootAuthMethods.jsx:155 +msgid "Method" +msgstr "Mètode" + +#: src/components/users/RootAuthMethods.jsx:170 +msgid "SSH Key" +msgstr "Clau SSH" + +#: src/components/users/RootAuthMethods.jsx:187 +msgid "Change the root password" +msgstr "Canvia la contrasenya d'arrel" + +#: src/components/users/RootAuthMethods.jsx:187 +msgid "Set a root password" +msgstr "Establiu una contrasenya d'arrel" + +#: src/components/users/RootAuthMethods.jsx:194 +msgid "Add a SSH Public Key for root" +msgstr "Afegiu una clau pública SSH per a l'arrel" + +#: src/components/users/RootAuthMethods.jsx:194 +msgid "Edit the SSH Public Key for root" +msgstr "Edita la clau pública SSH per a l'arrel" + +#: src/components/users/RootPasswordPopup.jsx:42 +msgid "Root password" +msgstr "Contrasenya d'arrel" + +#: src/components/users/RootSSHKeyPopup.jsx:43 +msgid "Set root SSH public key" +msgstr "Estableix la clau pública SSH per a l'arrel" + +#: src/components/users/RootSSHKeyPopup.jsx:71 +msgid "Root SSH public key" +msgstr "Clau pública SSH per a l'arrel" + +#: src/components/users/RootSSHKeyPopup.jsx:76 +msgid "Upload, paste, or drop an SSH public key" +msgstr "Carregueu, enganxeu o deixeu-hi anar una clau pública SSH" + +#. TRANSLATORS: push button label +#: src/components/users/RootSSHKeyPopup.jsx:78 +msgid "Upload" +msgstr "Carrega" + +#. TRANSLATORS: push button label, clears the related input field +#: src/components/users/RootSSHKeyPopup.jsx:80 +msgid "Clear" +msgstr "Neteja" + +#: src/components/users/UsersPage.jsx:31 +msgid "User" +msgstr "Usuari" + +#: src/components/users/UsersPage.jsx:34 +msgid "Root authentication" +msgstr "Autenticació d'arrel" + +#~ msgid "Btrfs snapshots required by product." +#~ msgstr "Les instantànies de Btrfs són requerides pel producte." diff --git a/web/po/cs.po b/web/po/cs.po index 8b89a6254c..129ecca5b8 100644 --- a/web/po/cs.po +++ b/web/po/cs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: Ladislav Slezák \n" "Language-Team: Czech TPM can verify the integrity of the " @@ -1266,53 +1266,37 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:330 -msgid "Transactional system" -msgstr "" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "" @@ -1449,6 +1433,17 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +msgid "Transactional root file system" +msgstr "" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/de.po b/web/po/de.po index c3b1bb863f..07eb839225 100644 --- a/web/po/de.po +++ b/web/po/de.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" -"PO-Revision-Date: 2024-03-01 23:42+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"PO-Revision-Date: 2024-03-09 16:42+0000\n" "Last-Translator: Ettore Atalan \n" "Language-Team: German \n" @@ -28,7 +28,7 @@ msgstr "Der Server unter %s ist nicht erreichbar." #. TRANSLATORS: error message #: src/DevServerWrapper.jsx:88 msgid "Cannot connect to the Cockpit server" -msgstr "" +msgstr "Verbindung zum Cockpit-Server nicht möglich" #. TRANSLATORS: button label #: src/DevServerWrapper.jsx:104 @@ -47,6 +47,11 @@ msgid "" "want to give it a try, we recommend using a virtual machine to prevent any " "possible data loss." msgstr "" +"Agama ist ein experimentelles Installationsprogramm für (open)SUSE-Systeme. " +"Es befindet sich noch in der Entwicklung und sollte daher nicht in " +"Produktionsumgebungen verwendet werden. Wenn Sie es ausprobieren möchten, " +"empfehlen wir die Verwendung einer virtuellen Maschine, um einen möglichen " +"Datenverlust zu vermeiden." #. TRANSLATORS: content of the "About" popup (2/2) #. %s is replaced by the project URL @@ -54,6 +59,8 @@ msgstr "" #, c-format msgid "For more information, please visit the project's repository at %s." msgstr "" +"Für weitere Informationen besuchen Sie bitte das Repositorium des Projekts " +"unter %s." #. TRANSLATORS: button label #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 @@ -71,12 +78,14 @@ msgstr "D-Bus-Fehler" #: src/components/core/DBusError.jsx:38 msgid "Cannot connect to D-Bus" -msgstr "" +msgstr "Verbindung zum D-Bus nicht möglich" #: src/components/core/DBusError.jsx:43 msgid "" "Could not connect to the D-Bus service. Please, check whether it is running." msgstr "" +"Es konnte keine Verbindung mit dem D-Bus-Dienst hergestellt werden. Bitte " +"prüfen Sie, ob er läuft." #. TRANSLATORS: button label #: src/components/core/DBusError.jsx:51 @@ -100,6 +109,8 @@ msgid "" "There are some reported issues. Please review them in the previous steps " "before proceeding with the installation." msgstr "" +"Es gibt einige gemeldete Probleme. Bitte informieren Sie sich über diese in " +"den vorherigen Schritten, bevor Sie mit der Installation fortfahren." #: src/components/core/InstallButton.jsx:49 msgid "Confirm Installation" @@ -110,10 +121,14 @@ msgid "" "If you continue, partitions on your hard disk will be modified according to " "the provided installation settings." msgstr "" +"Wenn Sie fortfahren, werden die Partitionen auf Ihrer Festplatte " +"entsprechend den vorgegebenen Installationseinstellungen geändert." #: src/components/core/InstallButton.jsx:59 msgid "Please, cancel and check the settings if you are unsure." msgstr "" +"Bitte brechen Sie den Vorgang ab und überprüfen Sie die Einstellungen, wenn " +"Sie unsicher sind." #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:65 src/components/core/Popup.jsx:128 @@ -136,6 +151,9 @@ msgid "" "Some problems were found when trying to start the installation. Please, have " "a look to the reported errors and try again." msgstr "" +"Beim Versuch, die Installation zu starten, wurden einige Probleme " +"festgestellt. Bitte sehen Sie sich die gemeldeten Fehler an und versuchen " +"Sie es erneut." #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:89 src/components/l10n/L10nPage.jsx:75 @@ -145,7 +163,7 @@ msgstr "" #: src/components/product/ProductPage.jsx:207 #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 -#: src/components/storage/ProposalSettingsSection.jsx:275 +#: src/components/storage/ProposalSettingsSection.jsx:263 #: src/components/storage/ProposalVolumes.jsx:148 #: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:511 @@ -159,13 +177,15 @@ msgstr "Installieren" #: src/components/core/InstallationFinished.jsx:41 msgid "TPM sealing requires the new system to be booted directly." -msgstr "" +msgstr "Bei der TPM-Versiegelung muss das neue System direkt gebootet werden." #: src/components/core/InstallationFinished.jsx:46 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." msgstr "" +"Wenn ein lokales Medium zur Ausführung dieses Installationsprogramms " +"verwendet wurde, entfernen Sie es vor dem nächsten Start." #: src/components/core/InstallationFinished.jsx:50 msgid "Hide details" @@ -209,15 +229,15 @@ msgstr "" #: src/components/core/InstallationFinished.jsx:113 msgid "Finish" -msgstr "" +msgstr "Fertigstellen" #: src/components/core/InstallationFinished.jsx:113 msgid "Reboot" -msgstr "" +msgstr "Neustart" #: src/components/core/InstallationProgress.jsx:32 msgid "Installing" -msgstr "" +msgstr "Wird installiert" #. TRANSLATORS: search field placeholder text #: src/components/core/ListSearch.jsx:50 @@ -239,10 +259,14 @@ msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." msgstr "" +"Der Browser wird das Herunterladen der Protokolle starten, sobald sie bereit " +"sind. Bitte haben Sie Geduld." #: src/components/core/LogsButton.jsx:114 msgid "Something went wrong while collecting logs. Please, try again." msgstr "" +"Beim Sammeln der Protokolle ist etwas schiefgelaufen. Bitte versuchen Sie es " +"erneut." #: src/components/core/Page.jsx:93 src/components/users/UsersPage.jsx:30 msgid "Back" @@ -282,7 +306,7 @@ msgstr "Benutzerpasswort bestätigen" #: src/components/core/PasswordInput.jsx:59 msgid "Password visibility button" -msgstr "" +msgstr "Schaltfläche für die Sichtbarkeit des Passworts" #: src/components/core/Popup.jsx:90 msgid "Confirm" @@ -297,7 +321,7 @@ msgstr "Aktionen" #: src/components/core/SectionSkeleton.jsx:29 msgid "Waiting" -msgstr "" +msgstr "Warten" #. TRANSLATORS: button label #: src/components/core/ShowLogButton.jsx:47 @@ -377,9 +401,11 @@ msgid "" "The language used by the installer. The language for the installed system " "can be set in the %s page." msgstr "" +"Die vom Installationsprogramm verwendete Sprache. Die Sprache für das " +"installierte System kann auf der Seite %s eingestellt werden." -#. TRANSLATORS: page section #. TRANSLATORS: page title +#. TRANSLATORS: page section #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -398,11 +424,11 @@ msgstr "Sprache" #. TRANSLATORS: placeholder text for search input in the keyboard selector. #: src/components/l10n/KeymapSelector.jsx:53 msgid "Filter by description or keymap code" -msgstr "" +msgstr "Nach Beschreibung oder Tastenzuordnungscode filtern" #: src/components/l10n/KeymapSelector.jsx:64 msgid "Available keymaps" -msgstr "" +msgstr "Verfügbare Tastenzuordnungen" #: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 msgid "Select time zone" @@ -461,7 +487,7 @@ msgstr "Tastatur ist noch nicht ausgewählt" #: src/components/l10n/LocaleSelector.jsx:53 msgid "Filter by language, territory or locale code" -msgstr "" +msgstr "Nach Sprache, Gebiet oder Sprachumgebungscode filtern" #: src/components/l10n/LocaleSelector.jsx:64 msgid "Available locales" @@ -470,7 +496,7 @@ msgstr "Verfügbare Sprachumgebungen" #. TRANSLATORS: placeholder text for search input in the timezone selector. #: src/components/l10n/TimezoneSelector.jsx:75 msgid "Filter by territory, time zone code or UTC offset" -msgstr "" +msgstr "Nach Gebiet, Zeitzonencode oder UTC-Abweichung filtern" #: src/components/l10n/TimezoneSelector.jsx:86 msgid "Available time zones" @@ -593,6 +619,7 @@ msgstr "IP-Präfix oder Netzmaske" #: src/components/network/IpSettingsForm.jsx:87 msgid "At least one address must be provided for selected mode" msgstr "" +"Für den ausgewählten Modus muss mindestens eine Adresse angegeben werden" #. TRANSLATORS: %s is replaced by the iSCSI target node name #: src/components/network/IpSettingsForm.jsx:133 @@ -637,10 +664,13 @@ msgstr "" "konfiguriert." #: src/components/network/NetworkPage.jsx:54 +#, fuzzy msgid "" "The system does not support WiFi connections, probably because of missing or " "disabled hardware." msgstr "" +"Das System unterstützt keine WiFi-Verbindungen, wahrscheinlich wegen " +"fehlender oder deaktivierter Hardware." #: src/components/network/NetworkPage.jsx:58 #, fuzzy @@ -655,8 +685,8 @@ msgstr "Keine WiFi-Verbindungen gefunden." msgid "Connect to a Wi-Fi network" msgstr "Mit einem Wi-Fi-Netzwerk verbinden" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -752,7 +782,7 @@ msgstr "Getrennt" #: src/components/network/WifiNetworkListItem.jsx:97 #, c-format msgid "%s connection is waiting for an state change" -msgstr "" +msgstr "Verbindung %s wartet auf eine Zustandsänderung" #. TRANSLATORS: menu label, remove the selected WiFi network settings #: src/components/network/WifiNetworkMenu.jsx:55 @@ -850,10 +880,10 @@ msgstr "" msgid "Probing storage devices" msgstr "" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/overview/StorageSection.jsx:208 -#: src/components/storage/ProposalPage.jsx:239 +#: src/components/storage/ProposalPage.jsx:243 msgid "Storage" msgstr "Speicherung" @@ -1265,7 +1295,7 @@ msgstr "iSCSI" #. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing #. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. #. Do not translate 'abbr' and 'title', they are part of the HTML markup. -#: src/components/storage/ProposalSettingsSection.jsx:89 +#: src/components/storage/ProposalSettingsSection.jsx:87 msgid "" "The password will not be needed to boot and access the data if the TPM can verify the integrity of the " @@ -1273,54 +1303,37 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "TPM zur automatischen Entschlüsselung bei jedem Bootvorgang verwenden" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" msgstr "Btrfs-Schnappschüsse verwenden" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "Btrfs-Schnappschüsse sind für das Produkt erforderlich." - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "Verschlüsselungseinstellungen ändern" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "Verschlüsselungseinstellungen" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "Verschlüsselung verwenden" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "Einstellungen" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "transaktional" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "Aktuellen Inhalt löschen" @@ -1463,6 +1476,18 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "transaktional" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" @@ -2179,3 +2204,6 @@ msgstr "" #: src/components/users/UsersPage.jsx:34 msgid "Root authentication" msgstr "" + +#~ msgid "Btrfs snapshots required by product." +#~ msgstr "Btrfs-Schnappschüsse sind für das Produkt erforderlich." diff --git a/web/po/es.po b/web/po/es.po index 8eebd1aea2..e1448ca4dd 100644 --- a/web/po/es.po +++ b/web/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2024-02-25 20:42+0000\n" "Last-Translator: Victor hck \n" "Language-Team: Spanish TPM can verify the integrity of the " @@ -1307,55 +1307,38 @@ msgstr "" "integridad del sistema. El sellado TPM requiere que el nuevo sistema se " "inicie directamente en su primera ejecución." -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "Utilice el TPM para descifrar automáticamente en cada arranque" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 #, fuzzy msgid "Use Btrfs Snapshots" msgstr "con instantáneas" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "Cambiar las configuraciones de cifrado" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "Configuraciones del cifrado" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "Usar cifrado" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "Ajustes" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "transaccional" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "Eliminar el contenido actual" @@ -1501,6 +1484,18 @@ msgstr "" "espacio libre en los dispositivos que se enumeran a continuación. Elige cómo " "hacerlo." +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "transaccional" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/fr.po b/web/po/fr.po index 3bcabedf7b..8906a5a37a 100644 --- a/web/po/fr.po +++ b/web/po/fr.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" -"PO-Revision-Date: 2024-01-23 12:59+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"PO-Revision-Date: 2024-03-09 22:42+0000\n" "Last-Translator: faila fail \n" "Language-Team: French \n" @@ -41,7 +41,6 @@ msgstr "À propos d'Agama" #. TRANSLATORS: content of the "About" popup (1/2) #: src/components/core/About.jsx:53 -#, fuzzy msgid "" "Agama is an experimental installer for (open)SUSE systems. It is still under " "development so, please, do not use it in production environments. If you " @@ -158,7 +157,7 @@ msgstr "" #: src/components/product/ProductPage.jsx:207 #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 -#: src/components/storage/ProposalSettingsSection.jsx:275 +#: src/components/storage/ProposalSettingsSection.jsx:263 #: src/components/storage/ProposalVolumes.jsx:148 #: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:511 @@ -406,8 +405,8 @@ msgstr "" "La langue utilisée par l'installateur. La langue du système installé peut " "être définie dans la page %s." -#. TRANSLATORS: page section #. TRANSLATORS: page title +#. TRANSLATORS: page section #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -681,8 +680,8 @@ msgstr "Aucune connexion WiFi trouvée." msgid "Connect to a Wi-Fi network" msgstr "Se connecter à un réseau Wi-Fi" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -707,7 +706,7 @@ msgstr "Aucun" #. TRANSLATORS: WiFi authentication mode #: src/components/network/WifiConnectionForm.jsx:45 msgid "WPA & WPA2 Personal" -msgstr "WPA & WPA2 Personal" +msgstr "WPA & WPA2 Personnel" #: src/components/network/WifiConnectionForm.jsx:91 #: src/components/product/ProductPage.jsx:128 @@ -801,8 +800,8 @@ msgstr "Aucune connexion réseau détectée" #, c-format msgid "%d connection set:" msgid_plural "%d connections set:" -msgstr[0] "Connexion %d établie:" -msgstr[1] "Connexions %d établies:" +msgstr[0] "%d Connexion établie:" +msgstr[1] "%d Connexions établies:" #. TRANSLATORS: page title #: src/components/overview/OverviewPage.jsx:48 @@ -811,9 +810,9 @@ msgstr "Synthèse de l'installation" #. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) #: src/components/overview/ProductSection.jsx:48 -#, fuzzy, c-format +#, c-format msgid "%s (registered)" -msgstr "%s (inscrit)" +msgstr "%s (enregistré)" #. TRANSLATORS: page title #. TRANSLATORS: page section @@ -880,10 +879,10 @@ msgstr "Installer en utilisant le périphérique %s" msgid "Probing storage devices" msgstr "Sonde les périphériques de stockage" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/overview/StorageSection.jsx:208 -#: src/components/storage/ProposalPage.jsx:239 +#: src/components/storage/ProposalPage.jsx:243 msgid "Storage" msgstr "Stockage" @@ -927,9 +926,9 @@ msgid "Choose a product" msgstr "Choisir un produit" #: src/components/product/ProductPage.jsx:122 -#, fuzzy, c-format +#, c-format msgid "Register %s" -msgstr "Inscription %s" +msgstr "Enregistrer %s" #: src/components/product/ProductPage.jsx:188 #, c-format @@ -943,9 +942,8 @@ msgid "Do you want to deregister %s?" msgstr "Souhaitez-vous désinscrire %s ?" #: src/components/product/ProductPage.jsx:227 -#, fuzzy msgid "Registered warning" -msgstr "Avertissement inscrit" +msgstr "Avertissement enregistré" #. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) #: src/components/product/ProductPage.jsx:232 @@ -959,14 +957,12 @@ msgid "Change product" msgstr "Changer de produit" #: src/components/product/ProductPage.jsx:305 -#, fuzzy msgid "Register" -msgstr "Registre" +msgstr "Enregistrer" #: src/components/product/ProductPage.jsx:337 -#, fuzzy msgid "Deregister product" -msgstr "Désinscrire le produit" +msgstr "Radier le produit" #: src/components/product/ProductPage.jsx:370 msgid "Code:" @@ -978,19 +974,16 @@ msgstr "E-mail :" #. TRANSLATORS: section title. #: src/components/product/ProductPage.jsx:390 -#, fuzzy msgid "Registration" -msgstr "Inscription" +msgstr "Enregistrement" #: src/components/product/ProductPage.jsx:399 -#, fuzzy msgid "This product requires registration." -msgstr "Ce produit nécessite une inscription." +msgstr "Ce produit nécessite un enregistrement." #: src/components/product/ProductPage.jsx:405 -#, fuzzy msgid "This product does not require registration." -msgstr "Ce produit nécessite pas d'inscription." +msgstr "Ce produit ne nécessite pas d'enregistrement." #: src/components/product/ProductRegistrationForm.jsx:63 msgid "Registration code" @@ -1018,9 +1011,8 @@ msgid "No products available for selection" msgstr "Aucun produit disponible pour la sélection" #: src/components/product/ProductSelector.jsx:42 -#, fuzzy msgid "Available products" -msgstr "Localités disponibles" +msgstr "Produits disponibles" #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 @@ -1065,7 +1057,7 @@ msgstr "Synthèse logiciel et options de filtrage" #: src/components/software/UsedSize.jsx:32 #, c-format msgid "Installation will take %s" -msgstr "L'installation durera %s" +msgstr "L'installation utilisera %s" #: src/components/storage/DASDFormatProgress.jsx:61 msgid "Waiting for progress report" @@ -1212,15 +1204,14 @@ msgstr "Actions planifiées" #. TRANSLATORS: The storage "Planned Actions" section's description #: src/components/storage/ProposalActionsSection.jsx:126 -#, fuzzy msgid "Actions to create the file systems and to ensure the new system boots." msgstr "" -"Actions visant à créer les systèmes de fichiers et à garantir le démarrage " -"du système." +"Actions visant à créer les systèmes de fichiers et à assurer le démarrage du " +"système." #: src/components/storage/ProposalDeviceSection.jsx:135 msgid "Waiting for information about selected device" -msgstr "" +msgstr "En attente d'informations sur le périphérique sélectionné" #: src/components/storage/ProposalDeviceSection.jsx:138 msgid "Select the device for installing the system." @@ -1263,7 +1254,7 @@ msgstr "Paramètres LVM" #: src/components/storage/ProposalDeviceSection.jsx:333 msgid "Waiting for information about LVM" -msgstr "" +msgstr "En attente d'informations sur le LVM" #: src/components/storage/ProposalDeviceSection.jsx:339 msgid "Use logical volume management (LVM)" @@ -1280,11 +1271,12 @@ msgid "" "Select the main disk or LVM " "Volume Group for installation." msgstr "" +"Sélectionnez le disque principal ou LVM Groupe de volume pour l'installation." #: src/components/storage/ProposalFileSystemsSection.jsx:70 -#, fuzzy msgid "File systems" -msgstr "Type de système de fichiers" +msgstr "Système de fichiers" #: src/components/storage/ProposalPageMenu.jsx:40 msgid "Manage and format" @@ -1309,7 +1301,7 @@ msgstr "iSCSI" #. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing #. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. #. Do not translate 'abbr' and 'title', they are part of the HTML markup. -#: src/components/storage/ProposalSettingsSection.jsx:89 +#: src/components/storage/ProposalSettingsSection.jsx:87 msgid "" "The password will not be needed to boot and access the data if the TPM can verify the integrity of the " @@ -1321,55 +1313,40 @@ msgstr "" "l'intégrité du système. Le scellement par TPM exige que le nouveau système " "soit démarré directement lors de sa première exécution." -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "Utiliser le TPM pour décrypter automatiquement à chaque démarrage" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" +"Utilise Btrfs comme système de fichiers racine, ce qui permet de démarrer " +"sur une version précédente du système après des changements de configuration " +"ou des mises à jour logicielles." -#: src/components/storage/ProposalSettingsSection.jsx:159 -#, fuzzy +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" -msgstr "avec des clichés" +msgstr "Utilise les clichés Btrfs" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "Modifier les paramètres de chiffrement" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "Paramètres de chiffrement" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "Utiliser le chiffrement" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "Paramètres" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "transactionnel" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "Supprimer le contenu actuel" @@ -1385,42 +1362,37 @@ msgid "Shrink existing partitions" msgstr "Réduire les partitions existantes" #: src/components/storage/ProposalSpacePolicySection.jsx:56 -#, fuzzy msgid "The data is kept, but the current partitions will be resized as needed." msgstr "" "Les données sont conservées, mais les partitions actuelles seront " -"redimensionnées si nécessaire pour libérer de l'espace." +"redimensionnées si nécessaire." #: src/components/storage/ProposalSpacePolicySection.jsx:60 msgid "Use available space" msgstr "Utiliser l'espace disponible" #: src/components/storage/ProposalSpacePolicySection.jsx:61 -#, fuzzy msgid "" "The data is kept. Only the space not assigned to any partition will be used." msgstr "" -"Les données sont conservées et les partitions existantes ne seront pas " -"modifiées. Seul l'espace qui n'est attribué à aucune partition sera utilisé." +"Les données sont conservées. Seul l'espace qui n'est attribué à aucune " +"partition sera utilisé." #: src/components/storage/ProposalSpacePolicySection.jsx:65 -#, fuzzy msgid "Custom" -msgstr "Périphériques personnalisés" +msgstr "Personnalisé" #: src/components/storage/ProposalSpacePolicySection.jsx:66 msgid "Select what to do with each partition." -msgstr "" +msgstr "Sélectionnez ce qu'il faut faire pour chaque partition." #: src/components/storage/ProposalSpacePolicySection.jsx:72 -#, fuzzy msgid "Used device" -msgstr "Lire les périphériques zFCP" +msgstr "Périphérique utilisé" #: src/components/storage/ProposalSpacePolicySection.jsx:73 -#, fuzzy msgid "Current content" -msgstr "Supprimer le contenu actuel" +msgstr "Contenu actuel" #: src/components/storage/ProposalSpacePolicySection.jsx:74 #: src/components/storage/ProposalVolumes.jsx:309 @@ -1434,60 +1406,58 @@ msgid "Details" msgstr "Détails" #: src/components/storage/ProposalSpacePolicySection.jsx:76 -#, fuzzy msgid "Action" -msgstr "Actions" +msgstr "Action" #. TRANSLATORS: %s is replaced by partition table type (e.g., GPT) #: src/components/storage/ProposalSpacePolicySection.jsx:110 -#, fuzzy, c-format +#, c-format msgid "%s partition table" -msgstr "partition" +msgstr "Table de partitionnement %s" #: src/components/storage/ProposalSpacePolicySection.jsx:121 -#, fuzzy msgid "EFI system partition" -msgstr "partition" +msgstr "Partition système EFI" #. TRANSLATORS: %s is replaced by a file system type (e.g., btrfs). #: src/components/storage/ProposalSpacePolicySection.jsx:124 -#, fuzzy, c-format +#, c-format msgid "%s file system" -msgstr "Ajouter un système de fichiers" +msgstr "Système de fichiers %s" #. TRANSLATORS: %s is replaced by a LVM volume group name (e.g., /dev/vg0). #: src/components/storage/ProposalSpacePolicySection.jsx:131 #, c-format msgid "LVM physical volume of %s" -msgstr "" +msgstr "Volume physique LVM de %s" #. TRANSLATORS: %s is replaced by a RAID name (e.g., /dev/md0). #: src/components/storage/ProposalSpacePolicySection.jsx:134 -#, fuzzy, c-format +#, c-format msgid "Member of RAID %s" -msgstr "Membres : %s" +msgstr "Composition du RAID %s" #: src/components/storage/ProposalSpacePolicySection.jsx:136 msgid "Not identified" -msgstr "" +msgstr "Non identifié" #. TRANSLATORS: %s is replaced by a disk size (e.g., 20 GiB) #: src/components/storage/ProposalSpacePolicySection.jsx:173 #, c-format msgid "%s unused" -msgstr "" +msgstr "%s inutilisé" #. TRANSLATORS: %s is replaced by a disk size (e.g., 2 GiB) #: src/components/storage/ProposalSpacePolicySection.jsx:186 #, c-format msgid "Shrinkable by %s" -msgstr "" +msgstr "Rétrécissable de %s" #. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) #: src/components/storage/ProposalSpacePolicySection.jsx:221 #, c-format msgid "Space action selector for %s" -msgstr "" +msgstr "Sélecteur d'allocation d'espace pour %s" #: src/components/storage/ProposalSpacePolicySection.jsx:224 #: src/components/storage/ProposalVolumes.jsx:229 @@ -1497,20 +1467,18 @@ msgstr "Supprimer" #: src/components/storage/ProposalSpacePolicySection.jsx:228 msgid "Allow resize" -msgstr "" +msgstr "Permettre le redimensionnement" #: src/components/storage/ProposalSpacePolicySection.jsx:230 msgid "Do not modify" -msgstr "" +msgstr "Ne pas modifier" #: src/components/storage/ProposalSpacePolicySection.jsx:382 -#, fuzzy msgid "Actions to find space" -msgstr "Actions concernant la connexion %s" +msgstr "Actions pour trouver de l'espace" #. TRANSLATORS: The storage "Find Space" section's title #: src/components/storage/ProposalSpacePolicySection.jsx:452 -#, fuzzy msgid "Find Space" msgstr "Trouver de l'espace" @@ -1520,6 +1488,22 @@ msgid "" "Allocating the file systems might need to find free space in the devices " "listed below. Choose how to do it." msgstr "" +"Allouer les systèmes de fichiers peut nécessiter de trouver de l'espace " +"libre dans les périphériques listés ci-dessous. Choisissez comment procéder." + +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "Système transactionnel" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, fuzzy, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" +"%s est un système immuable avec des mises à jour atomiques utilisant un " +"système de fichiers racine Btrfs en lecture seule." #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 @@ -2107,12 +2091,12 @@ msgstr "Nom d'utilisateur" #: src/components/users/FirstUser.jsx:88 msgid "Username suggestion dropdown" -msgstr "" +msgstr "menu déroulant de noms d'utilisateur suggérés" #. TRANSLATORS: dropdown username suggestions #: src/components/users/FirstUser.jsx:102 msgid "Use suggested username" -msgstr "" +msgstr "Utiliser le nom d'utilisateur suggéré" #: src/components/users/FirstUser.jsx:212 #: src/components/users/RootAuthMethods.jsx:99 @@ -2248,6 +2232,9 @@ msgstr "Utilisateur" msgid "Root authentication" msgstr "Authentification root" +#~ msgid "Btrfs snapshots required by product." +#~ msgstr "Clichés Btrfs requis par le produit." + #~ msgid "" #~ "Allows rolling back any change done to the system and restoring its " #~ "previous state" diff --git a/web/po/id.po b/web/po/id.po index 9f8a56fb26..8832d6662c 100644 --- a/web/po/id.po +++ b/web/po/id.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2024-02-21 04:42+0000\n" "Last-Translator: Arif Budiman \n" "Language-Team: Indonesian TPM can verify the integrity of the " @@ -1292,56 +1292,39 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 #, fuzzy msgid "Use Btrfs Snapshots" msgstr "dengan snapshot" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 #, fuzzy msgid "Change encryption settings" msgstr "Pengaturan enkripsi" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "Pengaturan enkripsi" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "Gunakan enkripsi" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "Pengaturan" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "transaksional" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "Hapus konten saat ini" @@ -1491,6 +1474,18 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "transaksional" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/ja.po b/web/po/ja.po index b9d7c92779..e05648bc06 100644 --- a/web/po/ja.po +++ b/web/po/ja.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" -"PO-Revision-Date: 2024-02-29 04:42+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"PO-Revision-Date: 2024-03-04 01:11+0000\n" "Last-Translator: Yasuhiko Kamata \n" "Language-Team: Japanese \n" @@ -156,7 +156,7 @@ msgstr "" #: src/components/product/ProductPage.jsx:207 #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 -#: src/components/storage/ProposalSettingsSection.jsx:275 +#: src/components/storage/ProposalSettingsSection.jsx:263 #: src/components/storage/ProposalVolumes.jsx:148 #: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:511 @@ -396,8 +396,8 @@ msgstr "" "インストーラで使用する言語です。インストール先のシステムで使用する言語は %s " "ページで設定します。" -#. TRANSLATORS: page section #. TRANSLATORS: page title +#. TRANSLATORS: page section #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -672,8 +672,8 @@ msgstr "WiFi 接続が見つかりませんでした。" msgid "Connect to a Wi-Fi network" msgstr "Wi-Fi ネットワークへの接続" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -866,10 +866,10 @@ msgstr "デバイス %s を利用してインストールします" msgid "Probing storage devices" msgstr "ストレージデバイスを検出しています" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/overview/StorageSection.jsx:208 -#: src/components/storage/ProposalPage.jsx:239 +#: src/components/storage/ProposalPage.jsx:243 msgid "Storage" msgstr "ストレージ" @@ -1283,7 +1283,7 @@ msgstr "iSCSI" #. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing #. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. #. Do not translate 'abbr' and 'title', they are part of the HTML markup. -#: src/components/storage/ProposalSettingsSection.jsx:89 +#: src/components/storage/ProposalSettingsSection.jsx:87 msgid "" "The password will not be needed to boot and access the data if the TPM can verify the integrity of the " @@ -1295,11 +1295,11 @@ msgstr "" "シーリングを使用するには、新しいシステムの初回起動時に直接起動を行う必要があ" "ります。" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "毎回の起動時に TPM を利用して自動的に暗号化解除する" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." @@ -1307,44 +1307,27 @@ msgstr "" "ルートファイルシステムに btrfs を使用することで、設定変更前やソフトウエアの" "アップグレード前に戻して起動し直すことができるようになります。" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" msgstr "btrfs スナップショットを使用する" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "btrfs のスナップショットは製品側の要件で必須となっています。" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "暗号化設定の変更" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "暗号化の設定" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "暗号化を使用する" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "設定" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "トランザクション型" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "現在の内容を全て削除" @@ -1489,6 +1472,20 @@ msgstr "" "ファイルシステムを割り当てるには、下記のデバイス内に空き領域を確保する必要が" "あるかもしれません。ここではその方法を選択します。" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "トランザクション型システム" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, fuzzy, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" +"%s は、読み込み専用の btrfs ルートファイルシステムを利用して一括更新のできる" +"不可変なシステムです。" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" @@ -2072,12 +2069,12 @@ msgstr "ユーザ名" #: src/components/users/FirstUser.jsx:88 msgid "Username suggestion dropdown" -msgstr "" +msgstr "ユーザ名の提案ドロップダウン" #. TRANSLATORS: dropdown username suggestions #: src/components/users/FirstUser.jsx:102 msgid "Use suggested username" -msgstr "" +msgstr "提案されたユーザ名を使用する" #: src/components/users/FirstUser.jsx:212 #: src/components/users/RootAuthMethods.jsx:99 @@ -2211,6 +2208,9 @@ msgstr "ユーザ" msgid "Root authentication" msgstr "root の認証" +#~ msgid "Btrfs snapshots required by product." +#~ msgstr "btrfs のスナップショットは製品側の要件で必須となっています。" + #~ msgid "" #~ "Allows rolling back any change done to the system and restoring its " #~ "previous state" diff --git a/web/po/ka.po b/web/po/ka.po index c5e8a088fb..1fe5186001 100644 --- a/web/po/ka.po +++ b/web/po/ka.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2024-02-17 05:42+0000\n" "Last-Translator: Temuri Doghonadze \n" "Language-Team: Georgian TPM can verify the integrity of the " @@ -1262,55 +1262,38 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 #, fuzzy msgid "Use Btrfs Snapshots" msgstr "სწრაფი ასლებით" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "დაშიფვრის პარამეტრები" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "დაშიფვრის გამოყენება" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "მორგება" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "ტრანზაქციული" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "" @@ -1452,6 +1435,18 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "ტრანზაქციული" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/mk.po b/web/po/mk.po index c2b9071792..c1543b8d05 100644 --- a/web/po/mk.po +++ b/web/po/mk.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" -"PO-Revision-Date: 2023-12-31 20:39+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"PO-Revision-Date: 2024-03-05 08:42+0000\n" "Last-Translator: Kristijan Fremen Velkovski \n" "Language-Team: Macedonian \n" @@ -73,16 +73,17 @@ msgstr "Затвори" #. TRANSLATORS: page title #: src/components/core/DBusError.jsx:34 msgid "D-Bus Error" -msgstr "" +msgstr "Грешка во D-Bus" #: src/components/core/DBusError.jsx:38 msgid "Cannot connect to D-Bus" -msgstr "" +msgstr "Не може да се поврзе со D-Bus" #: src/components/core/DBusError.jsx:43 msgid "" "Could not connect to the D-Bus service. Please, check whether it is running." msgstr "" +"Не може да се поврзе со услугата D-Bus. Ве молиме, проверете дали работи." #. TRANSLATORS: button label #: src/components/core/DBusError.jsx:51 @@ -150,7 +151,7 @@ msgstr "" #: src/components/product/ProductPage.jsx:207 #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 -#: src/components/storage/ProposalSettingsSection.jsx:275 +#: src/components/storage/ProposalSettingsSection.jsx:263 #: src/components/storage/ProposalVolumes.jsx:148 #: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:511 @@ -380,8 +381,8 @@ msgid "" "can be set in the %s page." msgstr "" -#. TRANSLATORS: page section #. TRANSLATORS: page title +#. TRANSLATORS: page section #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -652,8 +653,8 @@ msgstr "" msgid "Connect to a Wi-Fi network" msgstr "" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -846,10 +847,10 @@ msgstr "" msgid "Probing storage devices" msgstr "" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/overview/StorageSection.jsx:208 -#: src/components/storage/ProposalPage.jsx:239 +#: src/components/storage/ProposalPage.jsx:243 msgid "Storage" msgstr "" @@ -1259,7 +1260,7 @@ msgstr "" #. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing #. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. #. Do not translate 'abbr' and 'title', they are part of the HTML markup. -#: src/components/storage/ProposalSettingsSection.jsx:89 +#: src/components/storage/ProposalSettingsSection.jsx:87 msgid "" "The password will not be needed to boot and access the data if the TPM can verify the integrity of the " @@ -1267,53 +1268,37 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:330 -msgid "Transactional system" -msgstr "" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "" @@ -1450,6 +1435,17 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +msgid "Transactional root file system" +msgstr "" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/nl.po b/web/po/nl.po index 66740ce157..1d24b191ad 100644 --- a/web/po/nl.po +++ b/web/po/nl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: anonymous \n" "Language-Team: Dutch TPM can verify the integrity of the " @@ -1317,56 +1317,39 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 #, fuzzy msgid "Use Btrfs Snapshots" msgstr "met snapshots" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 #, fuzzy msgid "Change encryption settings" msgstr "Encryptie instellingen" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "Encryptie instellingen" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "Gebruik encryptie" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "Instellingen" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "Voer een actie uit" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "" @@ -1509,6 +1492,18 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "Voer een actie uit" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/pt_BR.po b/web/po/pt_BR.po index efa789e203..87ba85bddf 100644 --- a/web/po/pt_BR.po +++ b/web/po/pt_BR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: anonymous \n" "Language-Team: Portuguese (Brazil) TPM can verify the integrity of the " @@ -1294,56 +1294,39 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 #, fuzzy msgid "Use Btrfs Snapshots" msgstr "com instantâneos" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 #, fuzzy msgid "Change encryption settings" msgstr "Usar criptografia" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "Usar criptografia" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "Configurações" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "transacional" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "" @@ -1483,6 +1466,18 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "transacional" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/ru.po b/web/po/ru.po index 4b40ce460c..154d552286 100644 --- a/web/po/ru.po +++ b/web/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: Milachew \n" "Language-Team: Russian TPM can verify the integrity of the " @@ -1308,53 +1308,37 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:330 -msgid "Transactional system" -msgstr "" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "" @@ -1493,6 +1477,17 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +msgid "Transactional root file system" +msgstr "" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/sv.po b/web/po/sv.po index ccba7c776b..008c4fd44c 100644 --- a/web/po/sv.po +++ b/web/po/sv.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" -"PO-Revision-Date: 2024-02-29 07:42+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"PO-Revision-Date: 2024-03-03 10:42+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish \n" @@ -155,7 +155,7 @@ msgstr "" #: src/components/product/ProductPage.jsx:207 #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 -#: src/components/storage/ProposalSettingsSection.jsx:275 +#: src/components/storage/ProposalSettingsSection.jsx:263 #: src/components/storage/ProposalVolumes.jsx:148 #: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:511 @@ -397,8 +397,8 @@ msgstr "" "Språket som används av installationsprogrammet. Språket för det installerade " "systemet kan ställas in på sidan %s." -#. TRANSLATORS: page section #. TRANSLATORS: page title +#. TRANSLATORS: page section #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -672,8 +672,8 @@ msgstr "Inga WiFi-anslutningar hittades." msgid "Connect to a Wi-Fi network" msgstr "Anslut till ett Wi-Fi nätverk" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -866,10 +866,10 @@ msgstr "Installera på enhet %s" msgid "Probing storage devices" msgstr "Undersöker lagringsenheter" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/overview/StorageSection.jsx:208 -#: src/components/storage/ProposalPage.jsx:239 +#: src/components/storage/ProposalPage.jsx:243 msgid "Storage" msgstr "Lagring" @@ -1286,7 +1286,7 @@ msgstr "iSCSI" #. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing #. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. #. Do not translate 'abbr' and 'title', they are part of the HTML markup. -#: src/components/storage/ProposalSettingsSection.jsx:89 +#: src/components/storage/ProposalSettingsSection.jsx:87 msgid "" "The password will not be needed to boot and access the data if the TPM can verify the integrity of the " @@ -1298,11 +1298,11 @@ msgstr "" "integritet. TPM-försegling kräver att det nya systemet startas upp direkt " "vid första körningen." -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "Använd TPM för att dekryptera automatiskt vid varje uppstart" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." @@ -1311,44 +1311,27 @@ msgstr "" "tidigare version av systemet före konfigurationsändringar eller " "programvaruuppgraderingar." -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" msgstr "Använd Btrfs ögonblicksavbilder" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "Btrfs ögonblicksavbilder krävs för produkt." - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "Ändra krypteringsinställningar" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "Krypteringsinställningar" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "Använd kryptering" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "Inställningar" -#: src/components/storage/ProposalSettingsSection.jsx:330 -#, fuzzy -msgid "Transactional system" -msgstr "transaktionell" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "Radera nuvarande innehåll" @@ -1493,6 +1476,20 @@ msgstr "" "Att allokera filsystemen kan behöva hitta ledigt utrymme i enheterna som " "anges nedan. Välj hur du vill göra det." +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "Transaktionellt system" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, fuzzy, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" +"%s är ett oföränderligt system med atomära uppdateringar som använder ett " +"skrivskyddat Btrfs-rootfilsystem." + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" @@ -2075,12 +2072,12 @@ msgstr "Användarnamn" #: src/components/users/FirstUser.jsx:88 msgid "Username suggestion dropdown" -msgstr "" +msgstr "Rullgardinsmeny för användarnamnsförslag" #. TRANSLATORS: dropdown username suggestions #: src/components/users/FirstUser.jsx:102 msgid "Use suggested username" -msgstr "" +msgstr "Använd föreslaget användarnamn" #: src/components/users/FirstUser.jsx:212 #: src/components/users/RootAuthMethods.jsx:99 @@ -2216,6 +2213,9 @@ msgstr "Användare" msgid "Root authentication" msgstr "Rootautentisering" +#~ msgid "Btrfs snapshots required by product." +#~ msgstr "Btrfs ögonblicksavbilder krävs för produkt." + #~ msgid "" #~ "Allows rolling back any change done to the system and restoring its " #~ "previous state" diff --git a/web/po/uk.po b/web/po/uk.po index 7325ccf845..ddcf3100c5 100644 --- a/web/po/uk.po +++ b/web/po/uk.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-03 02:07+0000\n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: Milachew \n" "Language-Team: Ukrainian TPM can verify the integrity of the " @@ -1266,53 +1266,37 @@ msgid "" "first run." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:107 +#: src/components/storage/ProposalSettingsSection.jsx:105 msgid "Use the TPM to decrypt automatically on each boot" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:152 +#: src/components/storage/ProposalSettingsSection.jsx:148 msgid "" "Uses Btrfs for the root file system allowing to boot to a previous version " "of the system after configuration changes or software upgrades." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:159 +#: src/components/storage/ProposalSettingsSection.jsx:155 msgid "Use Btrfs Snapshots" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:173 -msgid "Btrfs snapshots required by product." -msgstr "" - -#: src/components/storage/ProposalSettingsSection.jsx:239 +#: src/components/storage/ProposalSettingsSection.jsx:227 msgid "Change encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:244 -#: src/components/storage/ProposalSettingsSection.jsx:265 +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:258 +#: src/components/storage/ProposalSettingsSection.jsx:246 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:309 msgid "Settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:330 -msgid "Transactional system" -msgstr "" - -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/storage/ProposalSettingsSection.jsx:333 -#, c-format -msgid "" -"%s is an immutable system with atomic updates using a read-only Btrfs root " -"file system." -msgstr "" - #: src/components/storage/ProposalSpacePolicySection.jsx:50 msgid "Delete current content" msgstr "" @@ -1449,6 +1433,17 @@ msgid "" "listed below. Choose how to do it." msgstr "" +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +msgid "Transactional root file system" +msgstr "" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "" + #. TRANSLATORS: header for a list of items #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" diff --git a/web/po/zh_Hans.po b/web/po/zh_Hans.po new file mode 100644 index 0000000000..e8ef20f3fa --- /dev/null +++ b/web/po/zh_Hans.po @@ -0,0 +1,2163 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR SuSE Linux Products GmbH, Nuernberg +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"PO-Revision-Date: 2024-03-04 15:43+0000\n" +"Last-Translator: Monstorix \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.9.1\n" + +#. TRANSLATORS: error message, %s is replaced by the server URL +#: src/DevServerWrapper.jsx:82 +#, c-format +msgid "The server at %s is not reachable." +msgstr "无法访问位于 %s 上的服务器。" + +#. TRANSLATORS: error message +#: src/DevServerWrapper.jsx:88 +msgid "Cannot connect to the Cockpit server" +msgstr "无法连接到 Cockpit 服务器" + +#. TRANSLATORS: button label +#: src/DevServerWrapper.jsx:104 +msgid "Try Again" +msgstr "再试一次" + +#: src/components/core/About.jsx:43 src/components/core/About.jsx:48 +msgid "About Agama" +msgstr "关于 Agama" + +#. TRANSLATORS: content of the "About" popup (1/2) +#: src/components/core/About.jsx:53 +msgid "" +"Agama is an experimental installer for (open)SUSE systems. It is still under " +"development so, please, do not use it in production environments. If you " +"want to give it a try, we recommend using a virtual machine to prevent any " +"possible data loss." +msgstr "" +"Agama 是(open)SUSE 系统的实验性安装程序。其仍处于开发阶段,因此请勿将其用于" +"生产环境。如果您想试用,建议使用虚拟机以防止任何可能的数据丢失。" + +#. TRANSLATORS: content of the "About" popup (2/2) +#. %s is replaced by the project URL +#: src/components/core/About.jsx:65 +#, c-format +msgid "For more information, please visit the project's repository at %s." +msgstr "若想了解更多信息,请从 %s 访问项目仓库。" + +#. TRANSLATORS: button label +#: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 +#: src/components/core/IssuesDialog.jsx:119 src/components/core/Sidebar.jsx:157 +#: src/components/core/Terminal.jsx:48 +#: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 +msgid "Close" +msgstr "关闭" + +#. TRANSLATORS: page title +#: src/components/core/DBusError.jsx:34 +msgid "D-Bus Error" +msgstr "D-Bus 错误" + +#: src/components/core/DBusError.jsx:38 +msgid "Cannot connect to D-Bus" +msgstr "无法连接到 D-Bus" + +#: src/components/core/DBusError.jsx:43 +msgid "" +"Could not connect to the D-Bus service. Please, check whether it is running." +msgstr "无法连接到 D-Bus 服务,请检查它是否在运行。" + +#. TRANSLATORS: button label +#: src/components/core/DBusError.jsx:51 +msgid "Reload" +msgstr "重载" + +#: src/components/core/DevelopmentInfo.jsx:55 +msgid "Cockpit server" +msgstr "Cockpit 服务器" + +#: src/components/core/FileViewer.jsx:65 +msgid "Reading file..." +msgstr "正在读取文件……" + +#: src/components/core/FileViewer.jsx:71 +msgid "Cannot read the file" +msgstr "无法读取文件" + +#: src/components/core/InstallButton.jsx:37 +msgid "" +"There are some reported issues. Please review them in the previous steps " +"before proceeding with the installation." +msgstr "此处存在一些已报告的错误。请在继续安装前回顾之前步骤中的内容。" + +#: src/components/core/InstallButton.jsx:49 +msgid "Confirm Installation" +msgstr "确认安装" + +#: src/components/core/InstallButton.jsx:55 +msgid "" +"If you continue, partitions on your hard disk will be modified according to " +"the provided installation settings." +msgstr "如果继续,硬盘上的分区将会根据已提供的安装设置进行修改。" + +#: src/components/core/InstallButton.jsx:59 +msgid "Please, cancel and check the settings if you are unsure." +msgstr "如果不确定任何事项,请务必取消并检查已进行的设置。" + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:65 src/components/core/Popup.jsx:128 +#: src/components/network/WifiConnectionForm.jsx:136 +msgid "Cancel" +msgstr "取消" + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:69 +msgid "Continue" +msgstr "继续" + +#: src/components/core/InstallButton.jsx:78 +msgid "Problems Found" +msgstr "已发现问题" + +#: src/components/core/InstallButton.jsx:82 +msgid "" +"Some problems were found when trying to start the installation. Please, have " +"a look to the reported errors and try again." +msgstr "尝试开始安装时发现问题。请查看已报告的错误后重试。" + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:89 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalDeviceSection.jsx:169 +#: src/components/storage/ProposalDeviceSection.jsx:364 +#: src/components/storage/ProposalSettingsSection.jsx:263 +#: src/components/storage/ProposalVolumes.jsx:148 +#: src/components/storage/ProposalVolumes.jsx:283 +#: src/components/storage/ZFCPPage.jsx:511 +msgid "Accept" +msgstr "接受" + +#. TRANSLATORS: button label +#: src/components/core/InstallButton.jsx:140 +msgid "Install" +msgstr "安装" + +#: src/components/core/InstallationFinished.jsx:41 +msgid "TPM sealing requires the new system to be booted directly." +msgstr "TPM 密封过程要求新系统直接启动。" + +#: src/components/core/InstallationFinished.jsx:46 +msgid "" +"If a local media was used to run this installer, remove it before the next " +"boot." +msgstr "如果运行此次安装时使用了本地介质,请在下次启动前移除。" + +#: src/components/core/InstallationFinished.jsx:50 +msgid "Hide details" +msgstr "隐藏细节" + +#: src/components/core/InstallationFinished.jsx:50 +msgid "See more details" +msgstr "查看更多细节" + +#. TRANSLATORS: Do not translate 'abbr' and 'title', they are part of the HTML markup +#: src/components/core/InstallationFinished.jsx:55 +msgid "" +"The final step to configure the TPM to automatically open encrypted devices will take place during the " +"first boot of the new system. For that to work, the machine needs to boot " +"directly to the new boot loader." +msgstr "" +"配置 TPM 自动开启加密设备的最后" +"一步将在新系统首次启动时进行。为此,本机需要直接启动到新的引导加载程序。" + +#. TRANSLATORS: page title +#: src/components/core/InstallationFinished.jsx:88 +msgid "Installation Finished" +msgstr "安装完毕" + +#: src/components/core/InstallationFinished.jsx:91 +msgid "Congratulations!" +msgstr "恭喜!" + +#: src/components/core/InstallationFinished.jsx:96 +msgid "The installation on your machine is complete." +msgstr "在您机器上的安装过程已完成。" + +#: src/components/core/InstallationFinished.jsx:100 +msgid "At this point you can power off the machine." +msgstr "现在您可以关闭机器电源了。" + +#: src/components/core/InstallationFinished.jsx:101 +msgid "At this point you can reboot the machine to log in to the new system." +msgstr "现在您可以重启机器并登录到新系统。" + +#: src/components/core/InstallationFinished.jsx:113 +msgid "Finish" +msgstr "完成" + +#: src/components/core/InstallationFinished.jsx:113 +msgid "Reboot" +msgstr "重启" + +#: src/components/core/InstallationProgress.jsx:32 +msgid "Installing" +msgstr "正在安装" + +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:215 +#: src/components/software/PatternSelector.jsx:216 +msgid "Search" +msgstr "搜索" + +#: src/components/core/LogsButton.jsx:98 +msgid "Collecting logs..." +msgstr "正在收集日志……" + +#: src/components/core/LogsButton.jsx:98 +msgid "Download logs" +msgstr "下载日志" + +#: src/components/core/LogsButton.jsx:106 +msgid "" +"The browser will run the logs download as soon as they are ready. Please, be " +"patient." +msgstr "浏览器将会在日志收集完毕后启动下载过程,请耐心等待。" + +#: src/components/core/LogsButton.jsx:114 +msgid "Something went wrong while collecting logs. Please, try again." +msgstr "收集日志时出现问题,请重试。" + +#: src/components/core/Page.jsx:93 src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "返回" + +#: src/components/core/Page.jsx:201 +msgid "Show global options" +msgstr "显示全局选项" + +#: src/components/core/Page.jsx:215 +msgid "Page Actions" +msgstr "页面操作" + +#: src/components/core/PasswordAndConfirmationInput.jsx:35 +msgid "Passwords do not match" +msgstr "密码不匹配" + +#: src/components/core/PasswordAndConfirmationInput.jsx:56 +#: src/components/network/WifiConnectionForm.jsx:125 +#: src/components/storage/iscsi/AuthFields.jsx:95 +#: src/components/storage/iscsi/AuthFields.jsx:100 +#: src/components/users/RootAuthMethods.jsx:163 +msgid "Password" +msgstr "密码" + +#: src/components/core/PasswordAndConfirmationInput.jsx:60 +msgid "User password" +msgstr "用户密码" + +#: src/components/core/PasswordAndConfirmationInput.jsx:69 +msgid "Password confirmation" +msgstr "确认密码" + +#: src/components/core/PasswordAndConfirmationInput.jsx:74 +msgid "User password confirmation" +msgstr "用户确认密码" + +#: src/components/core/PasswordInput.jsx:59 +msgid "Password visibility button" +msgstr "密码可见性按钮" + +#: src/components/core/Popup.jsx:90 +msgid "Confirm" +msgstr "确认" + +#. TRANSLATORS: dropdown label +#: src/components/core/RowActions.jsx:66 +#: src/components/storage/ProposalVolumes.jsx:119 +#: src/components/storage/ProposalVolumes.jsx:310 +msgid "Actions" +msgstr "操作" + +#: src/components/core/SectionSkeleton.jsx:29 +msgid "Waiting" +msgstr "正在等候" + +#. TRANSLATORS: button label +#: src/components/core/ShowLogButton.jsx:47 +msgid "Show Logs" +msgstr "显示日志" + +#. TRANSLATORS: popup dialog title +#: src/components/core/ShowLogButton.jsx:53 +msgid "YaST Logs" +msgstr "YaST 日志" + +#. TRANSLATORS: button label +#: src/components/core/ShowTerminalButton.jsx:48 +msgid "Open Terminal" +msgstr "打开终端" + +#. TRANSLATORS: sidebar header +#: src/components/core/Sidebar.jsx:113 src/components/core/Sidebar.jsx:121 +msgid "Installer Options" +msgstr "安装程序选项" + +#: src/components/core/Sidebar.jsx:128 +msgid "Hide installer options" +msgstr "隐藏安装程序选项" + +#: src/components/core/Sidebar.jsx:136 +msgid "Diagnostic tools" +msgstr "诊断工具" + +#. TRANSLATORS: Titles used for the popup displaying found section issues +#: src/components/core/ValidationErrors.jsx:53 +msgid "Software issues" +msgstr "软件问题" + +#: src/components/core/ValidationErrors.jsx:54 +msgid "Product issues" +msgstr "产品问题" + +#: src/components/core/ValidationErrors.jsx:55 +msgid "Storage issues" +msgstr "存储问题" + +#: src/components/core/ValidationErrors.jsx:57 +msgid "Found Issues" +msgstr "发现问题" + +#. TRANSLATORS: %d is replaced with the number of errors found +#: src/components/core/ValidationErrors.jsx:77 +#, c-format +msgid "%d error found" +msgid_plural "%d errors found" +msgstr[0] "已发现 %d 个错误" + +#. TRANSLATORS: label for keyboard layout selection +#: src/components/l10n/InstallerKeymapSwitcher.jsx:53 +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "键盘" + +#. TRANSLATORS: label for keyboard layout selection +#: src/components/l10n/InstallerKeymapSwitcher.jsx:61 +msgid "keyboard" +msgstr "键盘" + +#. TRANSLATORS: +#: src/components/l10n/InstallerKeymapSwitcher.jsx:70 +msgid "Keyboard layout cannot be changed in remote installation" +msgstr "无法在远程安装中更改键盘布局" + +#. TRANSLATORS: help text for the language selector in the sidebar, +#. %s will be replaced by the "Localization" page link +#: src/components/l10n/InstallerLocaleSwitcher.jsx:47 +#, c-format +msgid "" +"The language used by the installer. The language for the installed system " +"can be set in the %s page." +msgstr "安装程序所使用的语言。可在 %s 页面设置已安装系统的语言。" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/l10n/InstallerLocaleSwitcher.jsx:56 +#: src/components/l10n/L10nPage.jsx:384 +#: src/components/overview/L10nSection.jsx:52 +msgid "Localization" +msgstr "本地化" + +#: src/components/l10n/InstallerLocaleSwitcher.jsx:66 +#: src/components/l10n/L10nPage.jsx:246 +msgid "Language" +msgstr "语言" + +#: src/components/l10n/InstallerLocaleSwitcher.jsx:74 +msgid "language" +msgstr "语言" + +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:53 +msgid "Filter by description or keymap code" +msgstr "按描述或键盘映射代码过滤" + +#: src/components/l10n/KeymapSelector.jsx:64 +msgid "Available keymaps" +msgstr "可用的键盘映射" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "选择时区" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "%s 将使用已选择的时区。" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "时区" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "更改时区" + +#: src/components/l10n/L10nPage.jsx:139 +msgid "Time zone not selected yet" +msgstr "尚未选择时区" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +msgid "Select language" +msgstr "选择语言" + +#: src/components/l10n/L10nPage.jsx:183 +#, c-format +msgid "%s will use the selected language." +msgstr "%s 将会使用已选择的语言。" + +#: src/components/l10n/L10nPage.jsx:252 +msgid "Change language" +msgstr "更改语言" + +#: src/components/l10n/L10nPage.jsx:257 +msgid "Language not selected yet" +msgstr "尚未选择语言" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +msgid "Select keyboard" +msgstr "选择键盘" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "%s 将会使用已选择的键盘。" + +#: src/components/l10n/L10nPage.jsx:363 +msgid "Change keyboard" +msgstr "更改键盘" + +#: src/components/l10n/L10nPage.jsx:368 +msgid "Keyboard not selected yet" +msgstr "尚未选择键盘" + +#: src/components/l10n/LocaleSelector.jsx:53 +msgid "Filter by language, territory or locale code" +msgstr "按语言、地区或区域设定代码过滤" + +#: src/components/l10n/LocaleSelector.jsx:64 +msgid "Available locales" +msgstr "可用区域设定" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:75 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "按地区、时区代码或 UTC 偏移量过滤" + +#: src/components/l10n/TimezoneSelector.jsx:86 +msgid "Available time zones" +msgstr "可用时区" + +#: src/components/layout/Loading.jsx:30 +msgid "Loading installation environment, please wait." +msgstr "正在载入安装环境,请稍候。" + +#. TRANSLATORS: button label +#: src/components/network/AddressesDataList.jsx:81 +#: src/components/network/DnsDataList.jsx:87 +msgid "Remove" +msgstr "移除" + +#. TRANSLATORS: input field name +#: src/components/network/AddressesDataList.jsx:93 +#: src/components/network/AddressesDataList.jsx:94 +#: src/components/network/IpAddressInput.jsx:33 +msgid "IP Address" +msgstr "IP 地址" + +#. TRANSLATORS: input field name +#: src/components/network/AddressesDataList.jsx:102 +#: src/components/network/AddressesDataList.jsx:103 +msgid "Prefix length or netmask" +msgstr "前缀长度或掩码" + +#: src/components/network/AddressesDataList.jsx:119 +msgid "Add an address" +msgstr "添加地址" + +#. TRANSLATORS: button label +#: src/components/network/AddressesDataList.jsx:119 +msgid "Add another address" +msgstr "添加另一个地址" + +#: src/components/network/AddressesDataList.jsx:124 +msgid "Addresses" +msgstr "地址" + +#: src/components/network/AddressesDataList.jsx:129 +msgid "Addresses data list" +msgstr "地址数据列表" + +#. TRANSLATORS: input field for the iSCSI initiator name +#. TRANSLATORS: table header +#: src/components/network/ConnectionsTable.jsx:57 +#: src/components/network/ConnectionsTable.jsx:86 +#: src/components/storage/ZFCPPage.jsx:360 +#: src/components/storage/iscsi/InitiatorForm.jsx:52 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:68 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:85 +#: src/components/storage/iscsi/NodesPresenter.jsx:100 +#: src/components/storage/iscsi/NodesPresenter.jsx:121 +msgid "Name" +msgstr "名称" + +#. TRANSLATORS: table header +#: src/components/network/ConnectionsTable.jsx:59 +#: src/components/network/ConnectionsTable.jsx:87 +msgid "IP addresses" +msgstr "IP 地址" + +#: src/components/network/ConnectionsTable.jsx:67 +#: src/components/storage/ProposalVolumes.jsx:234 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:49 +#: src/components/storage/iscsi/NodesPresenter.jsx:73 +#: src/components/users/FirstUser.jsx:208 +msgid "Edit" +msgstr "编辑" + +#. TRANSLATORS: %s is replaced by a network connection name +#: src/components/network/ConnectionsTable.jsx:70 +#, c-format +msgid "Edit connection %s" +msgstr "编辑连接 %s" + +#: src/components/network/ConnectionsTable.jsx:74 +msgid "Forget" +msgstr "忘掉" + +#. TRANSLATORS: %s is replaced by a network connection name +#: src/components/network/ConnectionsTable.jsx:77 +#, c-format +msgid "Forget connection %s" +msgstr "忘掉连接 %s" + +#. TRANSLATORS: %s is replaced by a network connection name +#: src/components/network/ConnectionsTable.jsx:92 +#, c-format +msgid "Actions for connection %s" +msgstr "对连接 %s 的操作" + +#. TRANSLATORS: input field name +#: src/components/network/DnsDataList.jsx:78 +#: src/components/network/DnsDataList.jsx:79 +msgid "Server IP" +msgstr "服务器 IP" + +#: src/components/network/DnsDataList.jsx:96 +msgid "Add DNS" +msgstr "添加 DNS" + +#. TRANSLATORS: button label +#: src/components/network/DnsDataList.jsx:96 +msgid "Add another DNS" +msgstr "添加另一个 DNS" + +#: src/components/network/DnsDataList.jsx:101 +msgid "DNS" +msgstr "DNS" + +#. TRANSLATORS: input field name +#: src/components/network/IpPrefixInput.jsx:33 +msgid "IP prefix or netmask" +msgstr "IP 前缀或掩码" + +#. TRANSLATORS: error message +#: src/components/network/IpSettingsForm.jsx:87 +msgid "At least one address must be provided for selected mode" +msgstr "所选模式要求至少提供一个地址" + +#. TRANSLATORS: %s is replaced by the iSCSI target node name +#: src/components/network/IpSettingsForm.jsx:133 +#: src/components/storage/iscsi/EditNodeForm.jsx:50 +#, c-format +msgid "Edit %s" +msgstr "编辑 %s" + +#. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) +#: src/components/network/IpSettingsForm.jsx:136 +#: src/components/network/IpSettingsForm.jsx:141 +#: src/components/network/IpSettingsForm.jsx:143 +msgid "Mode" +msgstr "模式" + +#: src/components/network/IpSettingsForm.jsx:147 +msgid "Automatic (DHCP)" +msgstr "自动(DHCP)" + +#. TRANSLATORS: manual network configuration mode with a static IP address +#: src/components/network/IpSettingsForm.jsx:149 +#: src/components/storage/iscsi/NodeStartupOptions.js:25 +msgid "Manual" +msgstr "手动" + +#. TRANSLATORS: network gateway configuration +#: src/components/network/IpSettingsForm.jsx:164 +#: src/components/network/IpSettingsForm.jsx:167 +msgid "Gateway" +msgstr "网关" + +#: src/components/network/NetworkPage.jsx:38 +msgid "No wired connections found." +msgstr "未找到有线连接。" + +#: src/components/network/NetworkPage.jsx:53 +msgid "" +"The system has not been configured for connecting to a WiFi network yet." +msgstr "系统尚未配置为连接到 WiFi 网络。" + +#: src/components/network/NetworkPage.jsx:54 +msgid "" +"The system does not support WiFi connections, probably because of missing or " +"disabled hardware." +msgstr "系统不支持 WiFi 连接,可能由于硬件缺失或已被禁用。" + +#: src/components/network/NetworkPage.jsx:58 +msgid "No WiFi connections found." +msgstr "未找到 WiFi 连接。" + +#. TRANSLATORS: button label +#: src/components/network/NetworkPage.jsx:70 +#: src/components/network/NetworkPageMenu.jsx:46 +#: src/components/network/WifiSelector.jsx:134 +msgid "Connect to a Wi-Fi network" +msgstr "连接到 Wi-Fi 网络" + +#. TRANSLATORS: page section title +#. TRANSLATORS: page title +#: src/components/network/NetworkPage.jsx:169 +#: src/components/overview/NetworkSection.jsx:83 +msgid "Network" +msgstr "网络" + +#. TRANSLATORS: page section +#: src/components/network/NetworkPage.jsx:171 +msgid "Wired networks" +msgstr "有线网络" + +#. TRANSLATORS: page section +#: src/components/network/NetworkPage.jsx:176 +msgid "WiFi networks" +msgstr "WiFi 网络" + +#. TRANSLATORS: WiFi authentication mode +#: src/components/network/WifiConnectionForm.jsx:43 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:72 +msgid "None" +msgstr "无" + +#. TRANSLATORS: WiFi authentication mode +#: src/components/network/WifiConnectionForm.jsx:45 +msgid "WPA & WPA2 Personal" +msgstr "WPA 与 WPA2 个人版" + +#: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 +#: src/components/storage/ZFCPDiskForm.jsx:112 +#: src/components/storage/iscsi/DiscoverForm.jsx:108 +#: src/components/storage/iscsi/LoginForm.jsx:72 +#: src/components/users/FirstUser.jsx:238 +msgid "Something went wrong" +msgstr "出了点问题" + +#: src/components/network/WifiConnectionForm.jsx:92 +msgid "Please, review provided settings and try again." +msgstr "请回顾之前提供的设定后再试。" + +#. TRANSLATORS: SSID (Wifi network name) configuration +#: src/components/network/WifiConnectionForm.jsx:97 +#: src/components/network/WifiConnectionForm.jsx:101 +msgid "SSID" +msgstr "SSID" + +#. TRANSLATORS: Wifi security configuration (password protected or not) +#: src/components/network/WifiConnectionForm.jsx:109 +#: src/components/network/WifiConnectionForm.jsx:112 +msgid "Security" +msgstr "安全性" + +#. TRANSLATORS: WiFi password +#: src/components/network/WifiConnectionForm.jsx:121 +msgid "WPA Password" +msgstr "WPA 密码" + +#. TRANSLATORS: button label, connect to a WiFi network +#: src/components/network/WifiConnectionForm.jsx:133 +msgid "Connect" +msgstr "连接" + +#. TRANSLATORS: button label +#: src/components/network/WifiHiddenNetworkForm.jsx:50 +msgid "Connect to hidden network" +msgstr "连接到隐藏网络" + +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:41 +#: src/components/network/WifiNetworkListItem.jsx:99 +msgid "Connecting" +msgstr "正在连接" + +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:44 +msgid "Connected" +msgstr "已连接" + +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:47 +msgid "Disconnecting" +msgstr "正在断开连接" + +#. TRANSLATORS: iSCSI connection status +#. TRANSLATORS: Wifi network status +#: src/components/network/WifiNetworkListItem.jsx:50 +#: src/components/storage/iscsi/NodesPresenter.jsx:63 +msgid "Disconnected" +msgstr "已断开连接" + +#. TRANSLATORS: %s is replaced by a WiFi network name +#: src/components/network/WifiNetworkListItem.jsx:97 +#, c-format +msgid "%s connection is waiting for an state change" +msgstr "连接 %s 正等待状态更改" + +#. TRANSLATORS: menu label, remove the selected WiFi network settings +#: src/components/network/WifiNetworkMenu.jsx:55 +msgid "Forget network" +msgstr "忘掉网络" + +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 +#, c-format +msgid "The system will use %s as its default language." +msgstr "系统会使用 %s 作为默认语言。" + +#: src/components/overview/NetworkSection.jsx:59 +msgid "No network connections detected" +msgstr "未检测到网络连接" + +#. TRANSLATORS: header for the list of active network connections, +#. %d is replaced by the number of active connections +#: src/components/overview/NetworkSection.jsx:68 +#, c-format +msgid "%d connection set:" +msgid_plural "%d connections set:" +msgstr[0] "已设定 %d 个连接:" + +#. TRANSLATORS: page title +#: src/components/overview/OverviewPage.jsx:48 +msgid "Installation Summary" +msgstr "安装概要" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "%s (已注册)" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:435 +msgid "Product" +msgstr "产品" + +#: src/components/overview/SoftwareSection.jsx:38 +msgid "Reading software repositories" +msgstr "正在读取软件仓库" + +#. TRANSLATORS: clickable link label +#: src/components/overview/SoftwareSection.jsx:133 +msgid "Refresh the repositories" +msgstr "刷新仓库" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/overview/SoftwareSection.jsx:143 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "软件" + +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalDeviceSection.jsx:129 +msgid "No device selected yet" +msgstr "尚未选择设备" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:52 +#, c-format +msgid "Install using device %s shrinking existing partitions as needed" +msgstr "使用设备 %s 进行安装,并在需要时缩小现有的分区" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:56 +#, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "使用设备 %s 进行安装且不修改已存在的分区" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 +#, c-format +msgid "Install using device %s and deleting all its content" +msgstr "使用设备 %s 进行安装并删除其上的现有内容" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, c-format +msgid "Install using device %s" +msgstr "使用设备 %s 安装" + +#: src/components/overview/StorageSection.jsx:83 +msgid "Probing storage devices" +msgstr "正在探测存储设备" + +#. TRANSLATORS: page section title +#. TRANSLATORS: page title +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:243 +msgid "Storage" +msgstr "存储" + +#. TRANSLATORS: %s will be replaced by the user name +#: src/components/overview/UsersSection.jsx:80 +#, c-format +msgid "User %s will be created" +msgstr "用户 %s 将被创建" + +#: src/components/overview/UsersSection.jsx:87 +msgid "No user defined yet" +msgstr "尚未设定用户" + +#: src/components/overview/UsersSection.jsx:101 +msgid "Root authentication set for using both, password and public SSH Key" +msgstr "Root 认证设定为同时使用密码与 SSH 公钥" + +#: src/components/overview/UsersSection.jsx:102 +msgid "No root authentication method defined" +msgstr "未设定 Root 认证方法" + +#: src/components/overview/UsersSection.jsx:103 +msgid "Root authentication set for using password" +msgstr "Root 认证设定为使用密码" + +#: src/components/overview/UsersSection.jsx:104 +msgid "Root authentication set for using public SSH Key" +msgstr "Root 认证设定为使用 SSH 公钥" + +#. TRANSLATORS: page section title +#: src/components/overview/UsersSection.jsx:120 +#: src/components/users/UsersPage.jsx:30 +msgid "Users" +msgstr "用户" + +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:65 +msgid "Choose a product" +msgstr "选择产品" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "注册 %s" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "取消注册 %s" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "您想要取消注册 %s 吗?" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "已注册警告" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "选择新产品之前,必须先取消注册产品 %s 。" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "更改产品" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "注册" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "取消注册产品" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "代码:" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "电子邮件:" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "注册" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "此产品要求注册。" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "此产品无需注册。" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "注册码" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "电子邮件" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "正在载入可用产品,请稍候……" + +#. TRANSLATORS: page title +#: src/components/product/ProductSelectionPage.jsx:63 +msgid "Product selection" +msgstr "选择产品" + +#: src/components/product/ProductSelectionPage.jsx:72 +msgid "Select" +msgstr "选择" + +#: src/components/product/ProductSelector.jsx:36 +msgid "No products available for selection" +msgstr "没有可供选择的产品" + +#: src/components/product/ProductSelector.jsx:42 +msgid "Available products" +msgstr "可用产品" + +#: src/components/questions/GenericQuestion.jsx:35 +#: src/components/questions/LuksActivationQuestion.jsx:60 +msgid "Question" +msgstr "问题" + +#. TRANSLATORS: error message, user entered a wrong password +#: src/components/questions/LuksActivationQuestion.jsx:34 +msgid "Given encryption password didn't work" +msgstr "输入的加密密码无效" + +#: src/components/questions/LuksActivationQuestion.jsx:59 +msgid "Encrypted Device" +msgstr "已加密设备" + +#. TRANSLATORS: field label +#: src/components/questions/LuksActivationQuestion.jsx:69 +msgid "Encryption Password" +msgstr "加密密码" + +#. TRANSLATORS: pattern status, selected to install (by user) +#: src/components/software/PatternItem.jsx:63 +msgid "selected" +msgstr "已选择" + +#. TRANSLATORS: pattern status, selected to install (by dependencies) +#: src/components/software/PatternItem.jsx:66 +msgid "automatically selected" +msgstr "已自动选择" + +#. TRANSLATORS: pattern status, not selected to install +#: src/components/software/PatternItem.jsx:69 +msgid "not selected" +msgstr "未选择" + +#: src/components/software/PatternSelector.jsx:210 +msgid "Software summary and filter options" +msgstr "软件概述与过滤选项" + +#. TRANSLATORS: %s will be replaced by the estimated installation size, +#. example: "728.8 MiB" +#: src/components/software/UsedSize.jsx:32 +#, c-format +msgid "Installation will take %s" +msgstr "安装将会占用 %s" + +#: src/components/storage/DASDFormatProgress.jsx:61 +msgid "Waiting for progress report" +msgstr "正在等待进度报告" + +#: src/components/storage/DASDFormatProgress.jsx:69 +msgid "Formatting DASD devices" +msgstr "正在格式化 DASD 设备" + +#. TRANSLATORS: DASD = Direct Access Storage Device, IBM mainframe storage technology +#: src/components/storage/DASDPage.jsx:181 +msgid "Storage DASD" +msgstr "存储 DASD" + +#: src/components/storage/DASDTable.jsx:57 +#: src/components/storage/ZFCPPage.jsx:318 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:71 +#: src/components/storage/iscsi/NodesPresenter.jsx:103 +msgid "No" +msgstr "否" + +#: src/components/storage/DASDTable.jsx:57 +#: src/components/storage/ZFCPPage.jsx:318 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:71 +#: src/components/storage/iscsi/NodesPresenter.jsx:103 +msgid "Yes" +msgstr "是" + +#: src/components/storage/DASDTable.jsx:64 +#: src/components/storage/ZFCPDiskForm.jsx:118 +#: src/components/storage/ZFCPPage.jsx:301 +#: src/components/storage/ZFCPPage.jsx:361 +msgid "Channel ID" +msgstr "通道 ID" + +#. TRANSLATORS: table header +#: src/components/storage/DASDTable.jsx:65 +#: src/components/storage/ZFCPPage.jsx:302 +#: src/components/storage/iscsi/NodesPresenter.jsx:104 +#: src/components/storage/iscsi/NodesPresenter.jsx:125 +#: src/components/users/RootAuthMethods.jsx:157 +msgid "Status" +msgstr "状态" + +#. TRANSLATORS: The storage "Device" section's title +#: src/components/storage/DASDTable.jsx:66 +#: src/components/storage/ProposalDeviceSection.jsx:423 +msgid "Device" +msgstr "设备" + +#: src/components/storage/DASDTable.jsx:67 +msgid "Type" +msgstr "类型" + +#: src/components/storage/DASDTable.jsx:68 +msgid "Diag" +msgstr "诊断" + +#: src/components/storage/DASDTable.jsx:69 +msgid "Formatted" +msgstr "已格式化" + +#: src/components/storage/DASDTable.jsx:70 +msgid "Partition Info" +msgstr "分区信息" + +#. TRANSLATORS: drop down menu label +#: src/components/storage/DASDTable.jsx:105 +msgid "Perform an action" +msgstr "执行操作" + +#. TRANSLATORS: drop down menu action, activate the device +#: src/components/storage/DASDTable.jsx:111 +#: src/components/storage/ZFCPPage.jsx:332 +msgid "Activate" +msgstr "激活" + +#. TRANSLATORS: drop down menu action, deactivate the device +#: src/components/storage/DASDTable.jsx:113 +#: src/components/storage/ZFCPPage.jsx:374 +msgid "Deactivate" +msgstr "停用" + +#. TRANSLATORS: drop down menu action, enable DIAG access method +#: src/components/storage/DASDTable.jsx:116 +msgid "Set DIAG On" +msgstr "开启 DIAG" + +#. TRANSLATORS: drop down menu action, disable DIAG access method +#: src/components/storage/DASDTable.jsx:118 +msgid "Set DIAG Off" +msgstr "关闭 DIAG" + +#. TRANSLATORS: drop down menu action, format the disk +#: src/components/storage/DASDTable.jsx:121 +msgid "Format" +msgstr "格式化" + +#: src/components/storage/DASDTable.jsx:198 +#: src/components/storage/DASDTable.jsx:199 +msgid "Filter by min channel" +msgstr "按最小通道过滤" + +#: src/components/storage/DASDTable.jsx:206 +msgid "Remove min channel filter" +msgstr "移除最小通道过滤器" + +#: src/components/storage/DASDTable.jsx:219 +#: src/components/storage/DASDTable.jsx:220 +msgid "Filter by max channel" +msgstr "按最大通道过滤" + +#: src/components/storage/DASDTable.jsx:227 +msgid "Remove max channel filter" +msgstr "移除最大通道过滤器" + +#. TRANSLATORS: page title for iSCSI configuration +#: src/components/storage/ISCSIPage.jsx:31 +msgid "Storage iSCSI" +msgstr "存储 iSCSI" + +#. TRANSLATORS: show/hide toggle action, this is a clickable link +#: src/components/storage/ProposalActionsSection.jsx:70 +#, c-format +msgid "Hide %d subvolume action" +msgid_plural "Hide %d subvolume actions" +msgstr[0] "隐藏 %d 个子卷操作" + +#. TRANSLATORS: show/hide toggle action, this is a clickable link +#: src/components/storage/ProposalActionsSection.jsx:72 +#, c-format +msgid "Show %d subvolume action" +msgid_plural "Show %d subvolume actions" +msgstr[0] "显示 %d 个子卷操作" + +#. TRANSLATORS: The storage "Planned Actions" section's title. The +#. section shows a list of planned actions for the selected device, e.g. +#. "delete partition A", "create partition B with filesystem C", ... +#: src/components/storage/ProposalActionsSection.jsx:124 +msgid "Planned Actions" +msgstr "计划执行的操作" + +#. TRANSLATORS: The storage "Planned Actions" section's description +#: src/components/storage/ProposalActionsSection.jsx:126 +msgid "Actions to create the file systems and to ensure the new system boots." +msgstr "创建文件系统并确保新系统启动的操作。" + +#: src/components/storage/ProposalDeviceSection.jsx:135 +msgid "Waiting for information about selected device" +msgstr "正在等待已选择设备的信息" + +#: src/components/storage/ProposalDeviceSection.jsx:138 +msgid "Select the device for installing the system." +msgstr "选择要进行系统安装的设备。" + +#: src/components/storage/ProposalDeviceSection.jsx:143 +#: src/components/storage/ProposalDeviceSection.jsx:147 +#: src/components/storage/ProposalDeviceSection.jsx:245 +msgid "Installation device" +msgstr "安装设备" + +#: src/components/storage/ProposalDeviceSection.jsx:153 +msgid "No devices found." +msgstr "未找到设备。" + +#: src/components/storage/ProposalDeviceSection.jsx:242 +msgid "Devices for creating the volume group" +msgstr "用于创建卷组的设备" + +#: src/components/storage/ProposalDeviceSection.jsx:251 +msgid "Custom devices" +msgstr "自定义设备" + +#: src/components/storage/ProposalDeviceSection.jsx:315 +msgid "" +"Configuration of the system volume group. All the file systems will be " +"created in a logical volume of the system volume group." +msgstr "系统卷组的设置。所有文件系统将创建于系统卷组下的逻辑卷中。" + +#: src/components/storage/ProposalDeviceSection.jsx:321 +msgid "Configure the LVM settings" +msgstr "配置 LVM 设定" + +#: src/components/storage/ProposalDeviceSection.jsx:326 +#: src/components/storage/ProposalDeviceSection.jsx:346 +msgid "LVM settings" +msgstr "LVM 设定" + +#: src/components/storage/ProposalDeviceSection.jsx:333 +msgid "Waiting for information about LVM" +msgstr "正在等待 LVM 信息" + +#: src/components/storage/ProposalDeviceSection.jsx:339 +msgid "Use logical volume management (LVM)" +msgstr "使用逻辑卷管理(LVM)" + +#: src/components/storage/ProposalDeviceSection.jsx:347 +msgid "System Volume Group" +msgstr "系统卷组" + +#. TRANSLATORS: The storage "Device" sections's description. Do not +#. translate 'abbr' and 'title', they are part of the HTML markup. +#: src/components/storage/ProposalDeviceSection.jsx:414 +msgid "" +"Select the main disk or LVM " +"Volume Group for installation." +msgstr "" +"选择主磁盘或 LVM 卷组进行安装。" + +#: src/components/storage/ProposalFileSystemsSection.jsx:70 +msgid "File systems" +msgstr "文件系统" + +#: src/components/storage/ProposalPageMenu.jsx:40 +msgid "Manage and format" +msgstr "管理与格式化" + +#: src/components/storage/ProposalPageMenu.jsx:58 +msgid "Activate disks" +msgstr "激活磁盘" + +#: src/components/storage/ProposalPageMenu.jsx:60 +msgid "zFCP" +msgstr "zFCP" + +#: src/components/storage/ProposalPageMenu.jsx:76 +msgid "Connect to iSCSI targets" +msgstr "连接到 iSCSI 目标" + +#: src/components/storage/ProposalPageMenu.jsx:78 +msgid "iSCSI" +msgstr "iSCSI" + +#. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing +#. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. +#. Do not translate 'abbr' and 'title', they are part of the HTML markup. +#: src/components/storage/ProposalSettingsSection.jsx:87 +msgid "" +"The password will not be needed to boot and access the data if the TPM can verify the integrity of the " +"system. TPM sealing requires the new system to be booted directly on its " +"first run." +msgstr "" +"若 TPM 可以验证系统的完整性,启" +"动和访问数据的时候将无需密码。 TPM 密封要求新系统在首次运行时直接引导。" + +#: src/components/storage/ProposalSettingsSection.jsx:105 +msgid "Use the TPM to decrypt automatically on each boot" +msgstr "使用 TPM 在每次启动时自动解密" + +#: src/components/storage/ProposalSettingsSection.jsx:148 +msgid "" +"Uses Btrfs for the root file system allowing to boot to a previous version " +"of the system after configuration changes or software upgrades." +msgstr "" +"使用 Btrfs 作为根文件系统,可以允许配置被改变或软件更新后回退启动到先前版本的" +"系统。" + +#: src/components/storage/ProposalSettingsSection.jsx:155 +msgid "Use Btrfs Snapshots" +msgstr "使用 Btrfs 快照" + +#: src/components/storage/ProposalSettingsSection.jsx:227 +msgid "Change encryption settings" +msgstr "更改加密设置" + +#: src/components/storage/ProposalSettingsSection.jsx:232 +#: src/components/storage/ProposalSettingsSection.jsx:253 +msgid "Encryption settings" +msgstr "加密设置" + +#: src/components/storage/ProposalSettingsSection.jsx:246 +msgid "Use encryption" +msgstr "使用加密" + +#: src/components/storage/ProposalSettingsSection.jsx:309 +msgid "Settings" +msgstr "设置" + +#: src/components/storage/ProposalSpacePolicySection.jsx:50 +msgid "Delete current content" +msgstr "删除当前内容" + +#: src/components/storage/ProposalSpacePolicySection.jsx:51 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "所有分区将被移除,磁盘上的数据将丢失。" + +#: src/components/storage/ProposalSpacePolicySection.jsx:55 +msgid "Shrink existing partitions" +msgstr "缩小现有分区" + +#: src/components/storage/ProposalSpacePolicySection.jsx:56 +msgid "The data is kept, but the current partitions will be resized as needed." +msgstr "数据将被保留,但当前分区的大小将会按需调整。" + +#: src/components/storage/ProposalSpacePolicySection.jsx:60 +msgid "Use available space" +msgstr "使用可用空间" + +#: src/components/storage/ProposalSpacePolicySection.jsx:61 +msgid "" +"The data is kept. Only the space not assigned to any partition will be used." +msgstr "数据将被保留。仅使用未分配给任何分区的空间。" + +#: src/components/storage/ProposalSpacePolicySection.jsx:65 +msgid "Custom" +msgstr "自定义" + +#: src/components/storage/ProposalSpacePolicySection.jsx:66 +msgid "Select what to do with each partition." +msgstr "选择对每个分区执行的操作。" + +#: src/components/storage/ProposalSpacePolicySection.jsx:72 +msgid "Used device" +msgstr "已使用的设备" + +#: src/components/storage/ProposalSpacePolicySection.jsx:73 +msgid "Current content" +msgstr "当前内容" + +#: src/components/storage/ProposalSpacePolicySection.jsx:74 +#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/VolumeForm.jsx:741 +msgid "Size" +msgstr "大小" + +#: src/components/storage/ProposalSpacePolicySection.jsx:75 +#: src/components/storage/ProposalVolumes.jsx:308 +msgid "Details" +msgstr "细节" + +#: src/components/storage/ProposalSpacePolicySection.jsx:76 +msgid "Action" +msgstr "操作" + +#. TRANSLATORS: %s is replaced by partition table type (e.g., GPT) +#: src/components/storage/ProposalSpacePolicySection.jsx:110 +#, c-format +msgid "%s partition table" +msgstr "%s 分区表" + +#: src/components/storage/ProposalSpacePolicySection.jsx:121 +msgid "EFI system partition" +msgstr "EFI 系统分区" + +#. TRANSLATORS: %s is replaced by a file system type (e.g., btrfs). +#: src/components/storage/ProposalSpacePolicySection.jsx:124 +#, c-format +msgid "%s file system" +msgstr "%s 文件系统" + +#. TRANSLATORS: %s is replaced by a LVM volume group name (e.g., /dev/vg0). +#: src/components/storage/ProposalSpacePolicySection.jsx:131 +#, c-format +msgid "LVM physical volume of %s" +msgstr "%s 中的 LVM 逻辑卷" + +#. TRANSLATORS: %s is replaced by a RAID name (e.g., /dev/md0). +#: src/components/storage/ProposalSpacePolicySection.jsx:134 +#, c-format +msgid "Member of RAID %s" +msgstr "RAID %s 的成员" + +#: src/components/storage/ProposalSpacePolicySection.jsx:136 +msgid "Not identified" +msgstr "未识别" + +#. TRANSLATORS: %s is replaced by a disk size (e.g., 20 GiB) +#: src/components/storage/ProposalSpacePolicySection.jsx:173 +#, c-format +msgid "%s unused" +msgstr "%s 未使用空间" + +#. TRANSLATORS: %s is replaced by a disk size (e.g., 2 GiB) +#: src/components/storage/ProposalSpacePolicySection.jsx:186 +#, c-format +msgid "Shrinkable by %s" +msgstr "可缩小 %s" + +#. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) +#: src/components/storage/ProposalSpacePolicySection.jsx:221 +#, c-format +msgid "Space action selector for %s" +msgstr "%s 的空间操作选择器" + +#: src/components/storage/ProposalSpacePolicySection.jsx:224 +#: src/components/storage/ProposalVolumes.jsx:229 +#: src/components/storage/iscsi/NodesPresenter.jsx:77 +msgid "Delete" +msgstr "删除" + +#: src/components/storage/ProposalSpacePolicySection.jsx:228 +msgid "Allow resize" +msgstr "允许调整大小" + +#: src/components/storage/ProposalSpacePolicySection.jsx:230 +msgid "Do not modify" +msgstr "不要修改" + +#: src/components/storage/ProposalSpacePolicySection.jsx:382 +msgid "Actions to find space" +msgstr "查找空间的操作" + +#. TRANSLATORS: The storage "Find Space" section's title +#: src/components/storage/ProposalSpacePolicySection.jsx:452 +msgid "Find Space" +msgstr "查找空间" + +#. TRANSLATORS: The storage "Find space" sections's description +#: src/components/storage/ProposalSpacePolicySection.jsx:454 +msgid "" +"Allocating the file systems might need to find free space in the devices " +"listed below. Choose how to do it." +msgstr "若要分配文件系统,或许需要在下列设备中找到可用空间。选择如何操作。" + +#: src/components/storage/ProposalTransactionalInfo.jsx:46 +#, fuzzy +msgid "Transactional root file system" +msgstr "事务性系统" + +#: src/components/storage/ProposalTransactionalInfo.jsx:49 +#, fuzzy, c-format +msgid "" +"%s is an immutable system with atomic updates. It uses a read-only Btrfs " +"file system updated via snapshots." +msgstr "%s 是使用只读 Btrfs 根文件系统和原子更新的不可变系统。" + +#. TRANSLATORS: header for a list of items +#: src/components/storage/ProposalVolumes.jsx:59 +msgid "These limits are affected by:" +msgstr "这些限制受以下因素影响:" + +#. TRANSLATORS: list item, this affects the computed partition size limits +#: src/components/storage/ProposalVolumes.jsx:63 +msgid "The configuration of snapshots" +msgstr "快照配置" + +#. TRANSLATORS: list item, this affects the computed partition size limits +#. %s is replaced by a list of the volumes (like "/home, /boot") +#: src/components/storage/ProposalVolumes.jsx:67 +#, c-format +msgid "Presence of other volumes (%s)" +msgstr "有其他卷存在(%s)" + +#. TRANSLATORS: dropdown menu label +#: src/components/storage/ProposalVolumes.jsx:129 +msgid "Reset to defaults" +msgstr "重设为默认" + +#. TRANSLATORS: dropdown menu label +#: src/components/storage/ProposalVolumes.jsx:137 +#: src/components/storage/ProposalVolumes.jsx:141 +msgid "Add file system" +msgstr "添加文件系统" + +#. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" +#: src/components/storage/ProposalVolumes.jsx:191 +#, c-format +msgid "At least %s" +msgstr "至少 %s" + +#. TRANSLATORS: device flag, the partition size is automatically computed +#: src/components/storage/ProposalVolumes.jsx:197 +msgid "auto" +msgstr "自动" + +#. TRANSLATORS: the filesystem uses a logical volume (LVM) +#: src/components/storage/ProposalVolumes.jsx:207 +msgid "logical volume" +msgstr "逻辑卷" + +#: src/components/storage/ProposalVolumes.jsx:207 +msgid "partition" +msgstr "分区" + +#. TRANSLATORS: filesystem flag, it uses an encryption +#: src/components/storage/ProposalVolumes.jsx:216 +msgid "encrypted" +msgstr "已加密" + +#. TRANSLATORS: filesystem flag, it allows creating snapshots +#: src/components/storage/ProposalVolumes.jsx:218 +msgid "with snapshots" +msgstr "带快照" + +#. TRANSLATORS: flag for transactional file system +#: src/components/storage/ProposalVolumes.jsx:220 +msgid "transactional" +msgstr "事务性" + +#: src/components/storage/ProposalVolumes.jsx:275 +msgid "Edit file system" +msgstr "编辑文件系统" + +#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/VolumeForm.jsx:726 +#: src/components/storage/VolumeForm.jsx:731 +msgid "Mount point" +msgstr "挂载点" + +#: src/components/storage/ProposalVolumes.jsx:345 +msgid "Table with mount points" +msgstr "挂载点列表" + +#: src/components/storage/VolumeForm.jsx:102 +msgid "Select a value" +msgstr "选择一个值" + +#. TRANSLATORS: info about possible file system types. +#: src/components/storage/VolumeForm.jsx:255 +msgid "" +"The options for the file system type depends on the product and the mount " +"point." +msgstr "文件系统类型的选项数量取决于产品和挂载点。" + +#: src/components/storage/VolumeForm.jsx:261 +msgid "More info for file system types" +msgstr "关于文件系统类型的更多信息" + +#. TRANSLATORS: label for the file system selector. +#: src/components/storage/VolumeForm.jsx:272 +msgid "File system type" +msgstr "文件系统类型" + +#. TRANSLATORS: item which affects the final computed partition size +#: src/components/storage/VolumeForm.jsx:304 +msgid "the configuration of snapshots" +msgstr "快照配置" + +#. TRANSLATORS: item which affects the final computed partition size +#. %s is replaced by a list of mount points like "/home, /boot" +#: src/components/storage/VolumeForm.jsx:309 +#, c-format +msgid "the presence of the file system for %s" +msgstr "在 %s 上存在文件系统" + +#. TRANSLATORS: conjunction for merging two list items +#: src/components/storage/VolumeForm.jsx:311 +msgid ", " +msgstr ", " + +#. TRANSLATORS: the %s is replaced by the items which affect the computed size +#: src/components/storage/VolumeForm.jsx:314 +#, c-format +msgid "The final size depends on %s." +msgstr "最终大小取决于 %s。" + +#. TRANSLATORS: conjunction for merging two texts +#: src/components/storage/VolumeForm.jsx:316 +msgid " and " +msgstr " 以及 " + +#. TRANSLATORS: the partition size is automatically computed +#: src/components/storage/VolumeForm.jsx:321 +msgid "Automatically calculated size according to the selected product." +msgstr "根据选定的产品自动计算大小。" + +#: src/components/storage/VolumeForm.jsx:341 +msgid "Exact size for the file system." +msgstr "文件系统的准确大小。" + +#. TRANSLATORS: requested partition size +#: src/components/storage/VolumeForm.jsx:353 +msgid "Exact size" +msgstr "准确大小" + +#. TRANSLATORS: units selector (like KiB, MiB, GiB...) +#: src/components/storage/VolumeForm.jsx:368 +msgid "Size unit" +msgstr "大小单位" + +#: src/components/storage/VolumeForm.jsx:396 +msgid "" +"Limits for the file system size. The final size will be a value between the " +"given minimum and maximum. If no maximum is given then the file system will " +"be as big as possible." +msgstr "" +"文件系统大小的限度。最终大小将会介于此处给定的最小值和最大值之间。如未给定最" +"大值,文件系统将尽可能扩大。" + +#. TRANSLATORS: the minimal partition size +#: src/components/storage/VolumeForm.jsx:403 +msgid "Minimum" +msgstr "最小" + +#. TRANSLATORS: the minium partition size +#: src/components/storage/VolumeForm.jsx:413 +msgid "Minimum desired size" +msgstr "所需最小尺寸" + +#: src/components/storage/VolumeForm.jsx:422 +msgid "Unit for the minimum size" +msgstr "最小尺寸的单位" + +#. TRANSLATORS: the maximum partition size +#: src/components/storage/VolumeForm.jsx:433 +msgid "Maximum" +msgstr "最大" + +#. TRANSLATORS: the maximum partition size +#: src/components/storage/VolumeForm.jsx:444 +msgid "Maximum desired size" +msgstr "所需的最大尺寸" + +#: src/components/storage/VolumeForm.jsx:452 +msgid "Unit for the maximum size" +msgstr "最大尺寸的单位" + +#. TRANSLATORS: radio button label, fully automatically computed partition size, no user input +#: src/components/storage/VolumeForm.jsx:469 +msgid "Auto" +msgstr "自动" + +#. TRANSLATORS: radio button label, exact partition size requested by user +#: src/components/storage/VolumeForm.jsx:471 +msgid "Fixed" +msgstr "固定尺寸" + +#. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits +#: src/components/storage/VolumeForm.jsx:473 +msgid "Range" +msgstr "按范围计算" + +#: src/components/storage/VolumeForm.jsx:681 +msgid "A size value is required" +msgstr "必须输入尺寸值" + +#: src/components/storage/VolumeForm.jsx:686 +msgid "Minimum size is required" +msgstr "需要指定最小尺寸" + +#: src/components/storage/VolumeForm.jsx:690 +msgid "Maximum must be greater than minimum" +msgstr "最大值必须高于最小值" + +#: src/components/storage/ZFCPDiskForm.jsx:113 +msgid "The zFCP disk was not activated." +msgstr "zFCP 磁盘未激活。" + +#. TRANSLATORS: abbrev. World Wide Port Name +#: src/components/storage/ZFCPDiskForm.jsx:129 +#: src/components/storage/ZFCPPage.jsx:362 +msgid "WWPN" +msgstr "WWPN" + +#. TRANSLATORS: abbrev. Logical Unit Number +#: src/components/storage/ZFCPDiskForm.jsx:140 +#: src/components/storage/ZFCPPage.jsx:363 +msgid "LUN" +msgstr "LUN" + +#: src/components/storage/ZFCPPage.jsx:303 +msgid "Auto LUNs Scan" +msgstr "自动扫描 LUN" + +#: src/components/storage/ZFCPPage.jsx:314 +msgid "Activated" +msgstr "已激活" + +#: src/components/storage/ZFCPPage.jsx:314 +msgid "Deactivated" +msgstr "已停用" + +#: src/components/storage/ZFCPPage.jsx:417 +msgid "No zFCP controllers found." +msgstr "未找到 zFCP 控制器。" + +#: src/components/storage/ZFCPPage.jsx:418 +msgid "Please, try to read the zFCP devices again." +msgstr "请尝试再次读取 zFCP 设备。" + +#. TRANSLATORS: button label +#: src/components/storage/ZFCPPage.jsx:420 +msgid "Read zFCP devices" +msgstr "读取 zFCP 设备" + +#. TRANSLATORS: the text in the square brackets [] will be displayed in bold +#: src/components/storage/ZFCPPage.jsx:429 +msgid "" +"Automatic LUN scan is [enabled]. Activating a controller which is running in " +"NPIV mode will automatically configures all its LUNs." +msgstr "" +"自动 LUN 扫描[已启用]。激活运行在 NPIV 模式下的控制器将自动配置其所有 LUN。" + +#. TRANSLATORS: the text in the square brackets [] will be displayed in bold +#: src/components/storage/ZFCPPage.jsx:432 +msgid "" +"Automatic LUN scan is [disabled]. LUNs have to be manually configured after " +"activating a controller." +msgstr "自动 LUN 扫描[已停用]。激活控制器后须手动配置 LUN。" + +#: src/components/storage/ZFCPPage.jsx:498 +msgid "Activate a zFCP disk" +msgstr "激活 zFCP 磁盘" + +#: src/components/storage/ZFCPPage.jsx:537 +msgid "Please, try to activate a zFCP controller." +msgstr "请尝试激活 zFCP 控制器。" + +#: src/components/storage/ZFCPPage.jsx:544 +msgid "Please, try to activate a zFCP disk." +msgstr "请尝试激活 zFCP 磁盘。" + +#. TRANSLATORS: button label +#: src/components/storage/ZFCPPage.jsx:546 +msgid "Activate zFCP disk" +msgstr "激活 zFCP 磁盘" + +#: src/components/storage/ZFCPPage.jsx:553 +msgid "No zFCP disks found." +msgstr "未发现 zFCP 磁盘。" + +#. TRANSLATORS: button label +#: src/components/storage/ZFCPPage.jsx:572 +msgid "Activate new disk" +msgstr "激活新磁盘" + +#. TRANSLATORS: section title +#: src/components/storage/ZFCPPage.jsx:584 +msgid "Disks" +msgstr "磁盘" + +#. TRANSLATORS: page title +#: src/components/storage/ZFCPPage.jsx:731 +msgid "Storage zFCP" +msgstr "存储 zFCP" + +#. TRANSLATORS: multipath device type +#: src/components/storage/device-utils.jsx:82 +msgid "Multipath" +msgstr "多路径" + +#. TRANSLATORS: %s is replaced by the device bus ID +#: src/components/storage/device-utils.jsx:87 +#, c-format +msgid "DASD %s" +msgstr "DASD %s" + +#. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 +#: src/components/storage/device-utils.jsx:92 +#, c-format +msgid "Software %s" +msgstr "软件 %s" + +#: src/components/storage/device-utils.jsx:97 +msgid "SD Card" +msgstr "SD 卡" + +#. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... +#: src/components/storage/device-utils.jsx:99 +#, c-format +msgid "Transport %s" +msgstr "传输 %s" + +#. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array +#: src/components/storage/device-utils.jsx:118 +#, c-format +msgid "Members: %s" +msgstr "成员:%s" + +#. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array +#: src/components/storage/device-utils.jsx:127 +#, c-format +msgid "Devices: %s" +msgstr "设备:%s" + +#. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device +#: src/components/storage/device-utils.jsx:136 +#, c-format +msgid "Wires: %s" +msgstr "连线:%s" + +#. TRANSLATORS: disk partition info, %s is replaced by partition table +#. type (MS-DOS or GPT), %d is the number of the partitions +#: src/components/storage/device-utils.jsx:160 +#, c-format +msgid "%s with %d partitions" +msgstr "%s (包含 %d 个分区)" + +#. TRANSLATORS: status message, no existing content was found on the disk, +#. i.e. the disk is completely empty +#: src/components/storage/device-utils.jsx:184 +msgid "No content found" +msgstr "未找到内容" + +#: src/components/storage/device-utils.jsx:258 +msgid "Available devices" +msgstr "可用设备" + +#: src/components/storage/iscsi/AuthFields.jsx:70 +msgid "Only available if authentication by target is provided" +msgstr "仅当目标提供身份认证时可用" + +#: src/components/storage/iscsi/AuthFields.jsx:77 +msgid "Authentication by target" +msgstr "目标身份认证" + +#: src/components/storage/iscsi/AuthFields.jsx:80 +#: src/components/storage/iscsi/AuthFields.jsx:85 +#: src/components/storage/iscsi/AuthFields.jsx:87 +#: src/components/storage/iscsi/AuthFields.jsx:112 +#: src/components/storage/iscsi/AuthFields.jsx:117 +#: src/components/storage/iscsi/AuthFields.jsx:119 +msgid "User name" +msgstr "用户名" + +#: src/components/storage/iscsi/AuthFields.jsx:91 +#: src/components/storage/iscsi/AuthFields.jsx:124 +msgid "Incorrect user name" +msgstr "用户名不正确" + +#: src/components/storage/iscsi/AuthFields.jsx:105 +#: src/components/storage/iscsi/AuthFields.jsx:139 +msgid "Incorrect password" +msgstr "密码不正确" + +#: src/components/storage/iscsi/AuthFields.jsx:108 +msgid "Authentication by initiator" +msgstr "发起者身份认证" + +#: src/components/storage/iscsi/AuthFields.jsx:133 +msgid "Target Password" +msgstr "目标密码" + +#. TRANSLATORS: popup title +#: src/components/storage/iscsi/DiscoverForm.jsx:101 +msgid "Discover iSCSI Targets" +msgstr "发现 iSCSI 目标" + +#: src/components/storage/iscsi/DiscoverForm.jsx:110 +#: src/components/storage/iscsi/LoginForm.jsx:73 +msgid "Make sure you provide the correct values" +msgstr "确保提供了正确的值" + +#: src/components/storage/iscsi/DiscoverForm.jsx:115 +msgid "IP address" +msgstr "IP 地址" + +#. TRANSLATORS: network address +#: src/components/storage/iscsi/DiscoverForm.jsx:122 +#: src/components/storage/iscsi/DiscoverForm.jsx:124 +msgid "Address" +msgstr "地址" + +#: src/components/storage/iscsi/DiscoverForm.jsx:129 +msgid "Incorrect IP address" +msgstr "不正确的 IP 地址" + +#. TRANSLATORS: network port number +#: src/components/storage/iscsi/DiscoverForm.jsx:133 +#: src/components/storage/iscsi/DiscoverForm.jsx:140 +#: src/components/storage/iscsi/DiscoverForm.jsx:142 +msgid "Port" +msgstr "端口" + +#: src/components/storage/iscsi/DiscoverForm.jsx:147 +msgid "Incorrect port" +msgstr "不正确的端口" + +#: src/components/storage/iscsi/InitiatorForm.jsx:42 +msgid "Edit iSCSI Initiator" +msgstr "编辑 iSCSI 发起者" + +#. TRANSLATORS: iSCSI initiator name +#: src/components/storage/iscsi/InitiatorForm.jsx:49 +msgid "Initiator name" +msgstr "发起者名称" + +#. TRANSLATORS: usually just keep the original text +#. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI +#: src/components/storage/iscsi/InitiatorPresenter.jsx:71 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:86 +#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:124 +msgid "iBFT" +msgstr "iBFT" + +#: src/components/storage/iscsi/InitiatorPresenter.jsx:72 +#: src/components/storage/iscsi/InitiatorPresenter.jsx:87 +msgid "Offload card" +msgstr "卸载卡" + +#. TRANSLATORS: iSCSI initiator section name +#: src/components/storage/iscsi/InitiatorSection.jsx:50 +msgid "Initiator" +msgstr "发起者" + +#. TRANSLATORS: %s is replaced by the iSCSI target name +#: src/components/storage/iscsi/LoginForm.jsx:69 +#, c-format +msgid "Login %s" +msgstr "登录 %s" + +#. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) +#: src/components/storage/iscsi/LoginForm.jsx:76 +#: src/components/storage/iscsi/LoginForm.jsx:79 +msgid "Startup" +msgstr "启动" + +#: src/components/storage/iscsi/NodeStartupOptions.js:26 +msgid "On boot" +msgstr "开机时" + +#: src/components/storage/iscsi/NodeStartupOptions.js:27 +msgid "Automatic" +msgstr "自动" + +#. TRANSLATORS: iSCSI connection status, %s is replaced by node label +#: src/components/storage/iscsi/NodesPresenter.jsx:67 +#, c-format +msgid "Connected (%s)" +msgstr "已连接(%s)" + +#: src/components/storage/iscsi/NodesPresenter.jsx:82 +msgid "Login" +msgstr "登录" + +#: src/components/storage/iscsi/NodesPresenter.jsx:86 +msgid "Logout" +msgstr "登出" + +#: src/components/storage/iscsi/NodesPresenter.jsx:101 +#: src/components/storage/iscsi/NodesPresenter.jsx:122 +msgid "Portal" +msgstr "门户" + +#: src/components/storage/iscsi/NodesPresenter.jsx:102 +#: src/components/storage/iscsi/NodesPresenter.jsx:123 +msgid "Interface" +msgstr "界面" + +#: src/components/storage/iscsi/TargetsSection.jsx:139 +msgid "No iSCSI targets found." +msgstr "未找到 iSCSI 目标。" + +#: src/components/storage/iscsi/TargetsSection.jsx:140 +msgid "" +"Please, perform an iSCSI discovery in order to find available iSCSI targets." +msgstr "请执行 iSCSI 发现以查找可用的 iSCSI 目标。" + +#. TRANSLATORS: button label, starts iSCSI discovery +#: src/components/storage/iscsi/TargetsSection.jsx:142 +msgid "Discover iSCSI targets" +msgstr "发现 iSCSI 目标" + +#. TRANSLATORS: button label, starts iSCSI discovery +#: src/components/storage/iscsi/TargetsSection.jsx:153 +msgid "Discover" +msgstr "发现" + +#. TRANSLATORS: iSCSI targets section title +#: src/components/storage/iscsi/TargetsSection.jsx:167 +msgid "Targets" +msgstr "目标" + +#: src/components/storage/utils.js:49 +msgid "KiB" +msgstr "KiB" + +#: src/components/storage/utils.js:50 +msgid "MiB" +msgstr "MiB" + +#: src/components/storage/utils.js:51 +msgid "GiB" +msgstr "GiB" + +#: src/components/storage/utils.js:52 +msgid "TiB" +msgstr "TiB" + +#: src/components/storage/utils.js:53 +msgid "PiB" +msgstr "PiB" + +#: src/components/users/FirstUser.jsx:50 +msgid "No user defined yet." +msgstr "尚未设定用户。" + +#: src/components/users/FirstUser.jsx:53 +msgid "" +"Please, be aware that a user must be defined before installing the system to " +"be able to log into it." +msgstr "请注意,在安装系统前必须设定用户才能登录。" + +#. TRANSLATORS: push button label +#: src/components/users/FirstUser.jsx:57 +msgid "Define a user now" +msgstr "现在设定用户" + +#: src/components/users/FirstUser.jsx:67 src/components/users/FirstUser.jsx:242 +msgid "Full name" +msgstr "全名" + +#: src/components/users/FirstUser.jsx:68 src/components/users/FirstUser.jsx:256 +#: src/components/users/FirstUser.jsx:264 +#: src/components/users/FirstUser.jsx:266 +msgid "Username" +msgstr "用户名" + +#: src/components/users/FirstUser.jsx:88 +msgid "Username suggestion dropdown" +msgstr "建议用户名下拉列表" + +#. TRANSLATORS: dropdown username suggestions +#: src/components/users/FirstUser.jsx:102 +msgid "Use suggested username" +msgstr "使用建议的用户名" + +#: src/components/users/FirstUser.jsx:212 +#: src/components/users/RootAuthMethods.jsx:99 +#: src/components/users/RootAuthMethods.jsx:111 +msgid "Discard" +msgstr "丢弃" + +#: src/components/users/FirstUser.jsx:235 +msgid "Create user account" +msgstr "创建用户账户" + +#: src/components/users/FirstUser.jsx:235 +msgid "Edit user account" +msgstr "编辑用户账户" + +#: src/components/users/FirstUser.jsx:246 +#: src/components/users/FirstUser.jsx:248 +msgid "User full name" +msgstr "用户全名" + +#. TRANSLATORS: check box label +#: src/components/users/FirstUser.jsx:284 +#: src/components/users/FirstUser.jsx:288 +msgid "Edit password too" +msgstr "同时编辑密码" + +#: src/components/users/FirstUser.jsx:301 +msgid "user autologin" +msgstr "用户自动登录" + +#. TRANSLATORS: check box label +#: src/components/users/FirstUser.jsx:305 +msgid "Auto-login" +msgstr "自动登录" + +#: src/components/users/RootAuthMethods.jsx:35 +msgid "No root authentication method defined yet." +msgstr "尚未设定 Root 认证方法。" + +#: src/components/users/RootAuthMethods.jsx:38 +msgid "" +"Please, define at least one authentication method for logging into the " +"system as root." +msgstr "请至少设定一个认证方法,以使用 Root 身份登录系统。" + +#. TRANSLATORS: push button label +#: src/components/users/RootAuthMethods.jsx:43 +msgid "Set a password" +msgstr "设置密码" + +#. TRANSLATORS: push button label +#: src/components/users/RootAuthMethods.jsx:45 +msgid "Upload a SSH Public Key" +msgstr "上传 SSH 公钥" + +#: src/components/users/RootAuthMethods.jsx:94 +#: src/components/users/RootAuthMethods.jsx:107 +msgid "Change" +msgstr "更改" + +#: src/components/users/RootAuthMethods.jsx:94 +#: src/components/users/RootAuthMethods.jsx:107 +msgid "Set" +msgstr "设置" + +#: src/components/users/RootAuthMethods.jsx:129 +msgid "Already set" +msgstr "已设定" + +#: src/components/users/RootAuthMethods.jsx:130 +#: src/components/users/RootAuthMethods.jsx:134 +msgid "Not set" +msgstr "未设定" + +#. TRANSLATORS: table header, user authentication method +#: src/components/users/RootAuthMethods.jsx:155 +msgid "Method" +msgstr "方法" + +#: src/components/users/RootAuthMethods.jsx:170 +msgid "SSH Key" +msgstr "SSH 密钥" + +#: src/components/users/RootAuthMethods.jsx:187 +msgid "Change the root password" +msgstr "修改 Root 密码" + +#: src/components/users/RootAuthMethods.jsx:187 +msgid "Set a root password" +msgstr "设定 Root 密码" + +#: src/components/users/RootAuthMethods.jsx:194 +msgid "Add a SSH Public Key for root" +msgstr "为 root 添加 SSH 公钥" + +#: src/components/users/RootAuthMethods.jsx:194 +msgid "Edit the SSH Public Key for root" +msgstr "编辑 root 的 SSH 公钥" + +#: src/components/users/RootPasswordPopup.jsx:42 +msgid "Root password" +msgstr "Root 密码" + +#: src/components/users/RootSSHKeyPopup.jsx:43 +msgid "Set root SSH public key" +msgstr "设定 root 的 SSH 公钥" + +#: src/components/users/RootSSHKeyPopup.jsx:71 +msgid "Root SSH public key" +msgstr "Root 的 SSH 公钥" + +#: src/components/users/RootSSHKeyPopup.jsx:76 +msgid "Upload, paste, or drop an SSH public key" +msgstr "上传、粘贴或拖入 SSH 公钥" + +#. TRANSLATORS: push button label +#: src/components/users/RootSSHKeyPopup.jsx:78 +msgid "Upload" +msgstr "上传" + +#. TRANSLATORS: push button label, clears the related input field +#: src/components/users/RootSSHKeyPopup.jsx:80 +msgid "Clear" +msgstr "清除" + +#: src/components/users/UsersPage.jsx:31 +msgid "User" +msgstr "用户" + +#: src/components/users/UsersPage.jsx:34 +msgid "Root authentication" +msgstr "Root 认证" + +#~ msgid "Btrfs snapshots required by product." +#~ msgstr "该产品要求启用 Btrfs 快照。" diff --git a/web/src/manifest.json b/web/src/manifest.json index 8450b73d9d..ca584985d0 100644 --- a/web/src/manifest.json +++ b/web/src/manifest.json @@ -10,10 +10,13 @@ }, "locales": { "en-us": "English", - "id-id": "Indonesia", "fr-fr": "Français", + "zh-Hans": "中文", + "ca-es": "Català", + "id-id": "Indonesia", + "sv-se": "Svenska", + "de-de": "Deutsch", "ja-jp": "日本語", - "es-es": "Español", - "sv-se": "Svenska" + "es-es": "Español" } } \ No newline at end of file From cc1bc843628a1f90522edcf5a02f905adbbcaf30 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 10 Mar 2024 02:46:10 +0000 Subject: [PATCH 23/98] Update service PO files Agama-weblate commit: 6e40e55f095086ac0577a8737169f99cde56c88c --- service/po/ca.po | 190 ++++++++++++++++++++++++++++++++++++++++++ service/po/fr.po | 12 ++- service/po/zh_Hans.po | 186 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 service/po/ca.po create mode 100644 service/po/zh_Hans.po diff --git a/service/po/ca.po b/service/po/ca.po new file mode 100644 index 0000000000..4ecd7f73a8 --- /dev/null +++ b/service/po/ca.po @@ -0,0 +1,190 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR SuSE Linux Products GmbH, Nuernberg +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-05 02:04+0000\n" +"PO-Revision-Date: 2024-03-05 12:42+0000\n" +"Last-Translator: David Medina \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9.1\n" + +#. Runs the config phase +#: service/lib/agama/manager.rb:88 +msgid "Probing Storage" +msgstr "Sondant l'emmagatzematge" + +#: service/lib/agama/manager.rb:89 +msgid "Probing Software" +msgstr "Sondant el programari" + +#. Runs the install phase +#. rubocop:disable Metrics/AbcSize +#: service/lib/agama/manager.rb:109 +msgid "Partitioning" +msgstr "Particions" + +#. propose software after /mnt is already separated, so it uses proper +#. target +#: service/lib/agama/manager.rb:117 +msgid "Installing Software" +msgstr "Instal·lant programari" + +#: service/lib/agama/manager.rb:120 +msgid "Writing Users" +msgstr "Escrivint els usuaris" + +#: service/lib/agama/manager.rb:121 +msgid "Writing Network Configuration" +msgstr "Escrivint la configuració de la xarxa" + +#: service/lib/agama/manager.rb:122 +msgid "Saving Language Settings" +msgstr "Desant els paràmetres de llengua" + +#: service/lib/agama/manager.rb:123 +msgid "Writing repositories information" +msgstr "Escrivint la informació dels repositoris" + +#: service/lib/agama/manager.rb:124 +msgid "Finishing storage configuration" +msgstr "Acabant la configuració de l'emmagatzematge" + +#. Callback to handle unsigned files +#. +#. @param filename [String] File name +#. @param repo_id [Integer] Repository ID. It might be -1 if there is not an associated repo. +#: service/lib/agama/software/callbacks/signature.rb:63 +#, perl-brace-format +msgid "The file %{filename} from repository %{repo_name} (%{repo_url})" +msgstr "El fitxer %{filename} del repositori %{repo_name} (%{repo_url})" + +#: service/lib/agama/software/callbacks/signature.rb:67 +#, perl-brace-format +msgid "The file %{filename}" +msgstr "El fitxer %{filename}" + +#: service/lib/agama/software/callbacks/signature.rb:71 +#, perl-brace-format +msgid "" +"%{source} is not digitally signed. The origin and integrity of the file cannot" +" be verified. Use it anyway?" +msgstr "" +"%{source} no està signat digitalment. L'origen i la integritat del fitxer no " +"es poden verificar. Voleu usar-lo tanmateix?" + +#. Callback to handle signature verification failures +#. +#. @param key [Hash] GPG key data (id, name, fingerprint, etc.) +#. @param _repo_id [Integer] Repository ID +#: service/lib/agama/software/callbacks/signature.rb:94 +#, perl-brace-format +msgid "" +"The key %{id} (%{name}) with fingerprint %{fingerprint} is unknown. Do you wan" +"t to trust this key?" +msgstr "" +"La clau %{id} (%{name}) amb l'empremta digital %{fingerprint} és " +"desconeguda. Voleu confiar en aquesta clau?" + +#. as we use liveDVD with normal like ENV, lets temporary switch to normal to use its repos +#: service/lib/agama/software/manager.rb:134 +msgid "Initializing target repositories" +msgstr "Iniciant els repositoris de destinació" + +#: service/lib/agama/software/manager.rb:135 +msgid "Initializing sources" +msgstr "Iniciant les fonts" + +#: service/lib/agama/software/manager.rb:140 +msgid "Refreshing repositories metadata" +msgstr "Refrescant les metadades dels repositoris" + +#: service/lib/agama/software/manager.rb:141 +msgid "Calculating the software proposal" +msgstr "Calculant la proposta de programari" + +#. Writes the repositories information to the installed system +#: service/lib/agama/software/manager.rb:190 +msgid "Writing repositories to the target system" +msgstr "Escrivint els repositoris al sistema de destinació" + +#: service/lib/agama/software/manager.rb:196 +msgid "Restoring original repositories" +msgstr "Restaurant els repositoris originals" + +#. Issues related to the software proposal. +#. +#. Repositories that could not be probed are reported as errors. +#. +#. @return [Array] +#: service/lib/agama/software/manager.rb:470 +#, c-format +msgid "Could not read repository \"%s\"" +msgstr "No s'ha pogut llegir el repositori %s." + +#. Issue when a product is missing +#. +#. @return [Agama::Issue] +#: service/lib/agama/software/manager.rb:480 +msgid "Product not selected yet" +msgstr "Encara no s'ha seleccionat el producte." + +#. Issue when a product requires registration but it is not registered yet. +#. +#. @return [Agama::Issue] +#: service/lib/agama/software/manager.rb:489 +msgid "Product must be registered" +msgstr "El producte ha d'estar registrat." + +#. Returns solver error messages from the last attempt +#. +#. @return [Array] Error messages +#: service/lib/agama/software/proposal.rb:227 +#, c-format +msgid "Found %s dependency issues." +msgstr "S'han trobat %s problemes de dependències." + +#. Probes storage devices and performs an initial proposal +#: service/lib/agama/storage/manager.rb:112 +msgid "Activating storage devices" +msgstr "Activant els dispositius d'emmagatzematge" + +#: service/lib/agama/storage/manager.rb:113 +msgid "Probing storage devices" +msgstr "Sondant els dispositius d'emmagatzematge" + +#: service/lib/agama/storage/manager.rb:114 +msgid "Calculating the storage proposal" +msgstr "Calculant la proposta d'emmagatzematge" + +#: service/lib/agama/storage/manager.rb:115 +msgid "Selecting Linux Security Modules" +msgstr "Seleccionant els mòduls de seguretat de Linux" + +#. Prepares the partitioning to install the system +#: service/lib/agama/storage/manager.rb:123 +msgid "Preparing bootloader proposal" +msgstr "Preparant la proposta de carregador d'arrencada" + +#. first make bootloader proposal to be sure that required packages are installed +#: service/lib/agama/storage/manager.rb:128 +msgid "Adding storage-related packages" +msgstr "Afegint paquets relacionats amb l'emmagatzematge" + +#: service/lib/agama/storage/manager.rb:129 +msgid "Preparing the storage devices" +msgstr "Preparant els dispositius d'emmagatzematge" + +#: service/lib/agama/storage/manager.rb:130 +msgid "Writing bootloader sysconfig" +msgstr "Escrivint la configuració de sistema del carregador d'arrencada" diff --git a/service/po/fr.po b/service/po/fr.po index 0e252cae41..c8ab08599a 100644 --- a/service/po/fr.po +++ b/service/po/fr.po @@ -8,10 +8,10 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-02-04 02:09+0000\n" -"PO-Revision-Date: 2024-02-01 14:42+0000\n" +"PO-Revision-Date: 2024-03-05 02:05+0000\n" "Last-Translator: faila fail \n" -"Language-Team: French \n" +"Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -143,7 +143,6 @@ msgstr "Le produit n'est pas encore sélectionné" #. #. @return [Agama::Issue] #: service/lib/agama/software/manager.rb:489 -#, fuzzy msgid "Product must be registered" msgstr "Le produit doit être enregistré" @@ -153,7 +152,7 @@ msgstr "Le produit doit être enregistré" #: service/lib/agama/software/proposal.rb:227 #, c-format msgid "Found %s dependency issues." -msgstr "%s problèmes de dépendance trouvé(s) (Need a plural option)" +msgstr "%s problème(s) de dépendance trouvé(s)." #. Probes storage devices and performs an initial proposal #: service/lib/agama/storage/manager.rb:112 @@ -165,9 +164,8 @@ msgid "Probing storage devices" msgstr "Sonde les périphériques de stockage" #: service/lib/agama/storage/manager.rb:114 -#, fuzzy msgid "Calculating the storage proposal" -msgstr "Calcul de la capacité de stockage envisagée" +msgstr "Calcul de la proposition de stockage" #: service/lib/agama/storage/manager.rb:115 msgid "Selecting Linux Security Modules" diff --git a/service/po/zh_Hans.po b/service/po/zh_Hans.po new file mode 100644 index 0000000000..2ef7c575a2 --- /dev/null +++ b/service/po/zh_Hans.po @@ -0,0 +1,186 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR SuSE Linux Products GmbH, Nuernberg +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-04 02:50+0000\n" +"PO-Revision-Date: 2024-03-04 15:43+0000\n" +"Last-Translator: Monstorix \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.9.1\n" + +#. Runs the config phase +#: service/lib/agama/manager.rb:88 +msgid "Probing Storage" +msgstr "正在探测存储" + +#: service/lib/agama/manager.rb:89 +msgid "Probing Software" +msgstr "正在探测软件" + +#. Runs the install phase +#. rubocop:disable Metrics/AbcSize +#: service/lib/agama/manager.rb:109 +msgid "Partitioning" +msgstr "正在分区" + +#. propose software after /mnt is already separated, so it uses proper +#. target +#: service/lib/agama/manager.rb:117 +msgid "Installing Software" +msgstr "正在安装软件" + +#: service/lib/agama/manager.rb:120 +msgid "Writing Users" +msgstr "正在写入用户" + +#: service/lib/agama/manager.rb:121 +msgid "Writing Network Configuration" +msgstr "正在写入网络配置" + +#: service/lib/agama/manager.rb:122 +msgid "Saving Language Settings" +msgstr "正在保存语言设置" + +#: service/lib/agama/manager.rb:123 +msgid "Writing repositories information" +msgstr "正在写入软件仓库信息" + +#: service/lib/agama/manager.rb:124 +msgid "Finishing storage configuration" +msgstr "正在完成存储配置" + +#. Callback to handle unsigned files +#. +#. @param filename [String] File name +#. @param repo_id [Integer] Repository ID. It might be -1 if there is not an associated repo. +#: service/lib/agama/software/callbacks/signature.rb:63 +#, perl-brace-format +msgid "The file %{filename} from repository %{repo_name} (%{repo_url})" +msgstr "来自 %{repo_name} 的文件 %{filename} (%{repo_url})" + +#: service/lib/agama/software/callbacks/signature.rb:67 +#, perl-brace-format +msgid "The file %{filename}" +msgstr "文件 %{filename}" + +#: service/lib/agama/software/callbacks/signature.rb:71 +#, perl-brace-format +msgid "" +"%{source} is not digitally signed. The origin and integrity of the file cannot" +" be verified. Use it anyway?" +msgstr "%{source} 未经过数字签名。因此无法验证文件来源和完整性。是否仍要使用它?" + +#. Callback to handle signature verification failures +#. +#. @param key [Hash] GPG key data (id, name, fingerprint, etc.) +#. @param _repo_id [Integer] Repository ID +#: service/lib/agama/software/callbacks/signature.rb:94 +#, perl-brace-format +msgid "" +"The key %{id} (%{name}) with fingerprint %{fingerprint} is unknown. Do you wan" +"t to trust this key?" +msgstr "密钥 %{id} (%{name})(指纹为 %{fingerprint}))未知。您想要信任该密钥吗?" + +#. as we use liveDVD with normal like ENV, lets temporary switch to normal to use its repos +#: service/lib/agama/software/manager.rb:134 +msgid "Initializing target repositories" +msgstr "正在初始化目标仓库" + +#: service/lib/agama/software/manager.rb:135 +msgid "Initializing sources" +msgstr "正在初始化软件源" + +#: service/lib/agama/software/manager.rb:140 +msgid "Refreshing repositories metadata" +msgstr "正在刷新软件仓库元数据" + +#: service/lib/agama/software/manager.rb:141 +msgid "Calculating the software proposal" +msgstr "正在计算软件提案" + +#. Writes the repositories information to the installed system +#: service/lib/agama/software/manager.rb:190 +msgid "Writing repositories to the target system" +msgstr "正在将软件仓库写入目标系统" + +#: service/lib/agama/software/manager.rb:196 +msgid "Restoring original repositories" +msgstr "正在还原初始软件仓库" + +#. Issues related to the software proposal. +#. +#. Repositories that could not be probed are reported as errors. +#. +#. @return [Array] +#: service/lib/agama/software/manager.rb:470 +#, c-format +msgid "Could not read repository \"%s\"" +msgstr "无法读取仓库 “ %s”" + +#. Issue when a product is missing +#. +#. @return [Agama::Issue] +#: service/lib/agama/software/manager.rb:480 +msgid "Product not selected yet" +msgstr "尚未选择产品" + +#. Issue when a product requires registration but it is not registered yet. +#. +#. @return [Agama::Issue] +#: service/lib/agama/software/manager.rb:489 +msgid "Product must be registered" +msgstr "产品必须注册" + +#. Returns solver error messages from the last attempt +#. +#. @return [Array] Error messages +#: service/lib/agama/software/proposal.rb:227 +#, c-format +msgid "Found %s dependency issues." +msgstr "找到 %s 个依赖问题。" + +#. Probes storage devices and performs an initial proposal +#: service/lib/agama/storage/manager.rb:112 +msgid "Activating storage devices" +msgstr "正在激活存储设备" + +#: service/lib/agama/storage/manager.rb:113 +msgid "Probing storage devices" +msgstr "正在探测存储设备" + +#: service/lib/agama/storage/manager.rb:114 +msgid "Calculating the storage proposal" +msgstr "正在计算存储提案" + +#: service/lib/agama/storage/manager.rb:115 +msgid "Selecting Linux Security Modules" +msgstr "正在选择 Linux 安全模块" + +#. Prepares the partitioning to install the system +#: service/lib/agama/storage/manager.rb:123 +msgid "Preparing bootloader proposal" +msgstr "正在准备引导加载程序提案" + +#. first make bootloader proposal to be sure that required packages are installed +#: service/lib/agama/storage/manager.rb:128 +msgid "Adding storage-related packages" +msgstr "正在添加存储相关软件包" + +#: service/lib/agama/storage/manager.rb:129 +msgid "Preparing the storage devices" +msgstr "正在准备存储设备" + +#: service/lib/agama/storage/manager.rb:130 +msgid "Writing bootloader sysconfig" +msgstr "正在写入引导加载程序 sysconfig" From 24d40cfb9d3c1b483a42efe9c014069ca0f72613 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 10 Mar 2024 02:51:36 +0000 Subject: [PATCH 24/98] Update translations in the product files Agama-weblate commit: 6e40e55f095086ac0577a8737169f99cde56c88c --- products.d/ALP-Dolomite.yaml | 8 ++++++++ products.d/microos-desktop.yaml | 6 ++++++ products.d/microos.yaml | 13 +++++++++++++ products.d/tumbleweed.yaml | 11 +++++++++++ 4 files changed, 38 insertions(+) diff --git a/products.d/ALP-Dolomite.yaml b/products.d/ALP-Dolomite.yaml index 60103a6393..93d796e730 100644 --- a/products.d/ALP-Dolomite.yaml +++ b/products.d/ALP-Dolomite.yaml @@ -11,9 +11,16 @@ description: 'SUSE ALP Dolomite is a minimum immutable OS core, focused on # Do not manually change any translations! See README.md for more details. translations: description: + ca: El SUSE ALP Dolomite és un nucli de sistema operatiu mínim immutable, + centrat en la seguretat per proporcionar el mínim necessari per executar + càrregues de treball i serveis com ara contenidors o màquines virtuals. cs: SUSE ALP Dolomite je minimální neměnitelný základní OS, zaměřený na bezpečnost pro poskytování úplného minima ke spuštění úloh a služeb v kontejnerech nebo virtuálních strojích. + de: SUSE ALP Dolomite ist ein minimaler, unveränderlicher Betriebssystemkern, + der sich auf die Sicherheit konzentriert und das absolute Minimum für die + Ausführung von Arbeitslasten und Diensten als Container oder virtuelle + Maschinen bietet. es: SUSE ALP Dolomite es un núcleo de sistema operativo mínimo e inmutable, centrado en la seguridad para proporcionar lo mínimo necesario para ejecutar cargas de trabajo y servicios como contenedores o máquinas @@ -33,6 +40,7 @@ translations: sv: SUSE ALP Dolomite är en minimal oföränderlig OS-kärna, fokuserad på säkerhet för att tillhandahålla det absoluta minimum för att köra arbetsbelastningar och tjänster som behållare eller virtuella maskiner. + zh_Hans: SUSE ALP Dolomite 是最小的不可变操作系统核心,专注于安全性,提供最低限度的容器化或虚拟机工作负载和服务。 software: mandatory_patterns: - alp_base_zypper diff --git a/products.d/microos-desktop.yaml b/products.d/microos-desktop.yaml index 97d351d864..8dafa1ac5b 100644 --- a/products.d/microos-desktop.yaml +++ b/products.d/microos-desktop.yaml @@ -12,6 +12,10 @@ description: 'A distribution for the desktop offering automatic updates and # Do not manually change any translations! See README.md for more details. translations: description: + ca: Una distribució per a l'escriptori que ofereix actualitzacions automàtiques + i retrocessos sobre els fonaments de l'openSUSE MicroSO. Inclou el Podman + Container Runtime i permet gestionar programari mitjançant el Programari + del Gnome o el KDE Discover. es: Una distribución para escritorio que ofrece actualizaciones automáticas y reversión sobre los fundamentos de openSUSE MicroOS. Incluye Podman Container Runtime y permite administrar software usando Gnome Software o @@ -30,6 +34,8 @@ translations: möjligheten att rulla tillbaka byggt ovanpå grunden av openSUSE MicroOS. Inkluderar Podman behållarkörtid och gör det möjligt att hantera programvara med Gnome Programvara eller KDE Discover. + zh_Hans: 在 openSUSE MicroOS 基础上提供自动更新和回滚的桌面设备发行版。包含 Podman 容器运行时,且允许透过 GNOME + Software 或 KDE Discover 管理软件。 software: installation_repositories: - url: https://download.opensuse.org/tumbleweed/repo/oss/ diff --git a/products.d/microos.yaml b/products.d/microos.yaml index a0cec5d775..596d60186b 100644 --- a/products.d/microos.yaml +++ b/products.d/microos.yaml @@ -12,6 +12,17 @@ description: 'A quick, small distribution designed to host container workloads # Do not manually change any translations! See README.md for more details. translations: description: + ca: Una distribució ràpida i petita dissenyada per allotjar càrregues de treball + de contenidors amb administració i pedaços automatitzats. L'openSUSE + MicroSO proporciona actualitzacions transaccionals (atòmiques) en un + sistema de fitxers d'arrel btrfs només de lectura. Com a distribució + contínua, el programari està sempre actualitzat. + de: Eine schnelle, kleine Distribution, die für den Betrieb von + Container-Arbeitslasten mit automatischer Verwaltung und automatisiertem + Patching entwickelt wurde. openSUSE MicroOS bietet transaktionale + (atomare) Aktualisierungen auf einem schreibgeschützten + btrfs-Wurzeldateisystem. Als Distribution mit rollierenden + Veröffentlichungen ist die Software immer auf dem neuesten Stand. es: Una distribución pequeña y rápida diseñada para alojar cargas de trabajo de contenedores con administración y parches automatizados. openSUSE MicroOS proporciona actualizaciones transaccionales (atómicas) en un sistema de @@ -35,6 +46,8 @@ translations: patchning. openSUSE MicroOS tillhandahåller transaktionella (atomära) uppdateringar på ett skrivskyddat btrfs-rootfilsystem. Som rullande releasedistribution är mjukvaran alltid uppdaterad. + zh_Hans: 一个快速、小型的发行版,旨在通过自动化管理和修补来托管容器工作负载。openSUSE MicroOS 提供基于只读 Btrfs + 根文件系统之上的事务性(原子)更新。作为滚动发行版,它的软件始终保持最新。 software: installation_repositories: - url: https://download.opensuse.org/tumbleweed/repo/oss/ diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index 6c5f0426a2..cebe4f493e 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -12,9 +12,18 @@ description: 'The Tumbleweed distribution is a pure rolling release version of # Do not manually change any translations! See README.md for more details. translations: description: + ca: La distribució Tumbleweed és una versió purament contínua d'openSUSE que + conté les últimes versions estables de tot el programari en lloc de + dependre de cicles de llançament periòdics rígids. El projecte fa això per + als usuaris que volen el programari estable més nou. cs: Tumbleweed je rolující verze distribuce openSUSE obsahující poslední "stabilní" verze veškerého software namísto pevných pravidelných vydání. Projekt je určen pro uživatele, kteří chtějí nejnovější stabilní software. + de: Die Tumbleweed-Distribution ist eine Version mit reinen rollierenden + Veröffentlichungen von openSUSE, die die neuesten „stabilen“ Versionen der + gesamten Software enthält, anstatt sich auf starre periodische + Veröffentlichungszyklen zu verlassen. Das Projekt tut dies für Benutzer, + die die neueste, stabile Software wünschen. es: La distribución Tumbleweed es una versión pura de lanzamiento continuo de openSUSE que contiene las últimas versiones "estables" de todo el software en lugar de depender de ciclos de lanzamiento periódicos menos flexibles. @@ -42,6 +51,8 @@ translations: innehåller de senaste "stabila" versionerna av all programvara istället för att förlita sig på stela periodiska utgivningscykler. Projektet gör detta för användare som vill ha den senaste stabila programvaran. + zh_Hans: Tumbleweed 发行版是 openSUSE + 的纯滚动发布版本,其并不依赖于严格的定时发布周期,而是持续包含所有最新“稳定”版本的软件。该项目为追求最新稳定软件的用户而生。 software: installation_repositories: - url: https://download.opensuse.org/tumbleweed/repo/oss/ From 09f3e08638b4d040202b8a4b304e9bcf72f485dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 11 Mar 2024 12:09:35 +0000 Subject: [PATCH 25/98] web: Adapt storage client to changes in D-Bus --- web/src/client/storage.js | 75 +++++- web/src/client/storage.test.js | 439 ++++++++++++++++++++++++++++++--- 2 files changed, 474 insertions(+), 40 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 66a92956bf..d10857431f 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -31,6 +31,7 @@ const STORAGE_IFACE = "org.opensuse.Agama.Storage1"; const STORAGE_JOBS_NAMESPACE = "/org/opensuse/Agama/Storage1/jobs"; const STORAGE_JOB_IFACE = "org.opensuse.Agama.Storage1.Job"; const STORAGE_SYSTEM_NAMESPACE = "/org/opensuse/Agama/Storage1/system"; +const STORAGE_STAGING_NAMESPACE = "/org/opensuse/Agama/Storage1/staging"; const PROPOSAL_IFACE = "org.opensuse.Agama.Storage1.Proposal"; const PROPOSAL_CALCULATOR_IFACE = "org.opensuse.Agama.Storage1.Proposal.Calculator"; const ISCSI_INITIATOR_IFACE = "org.opensuse.Agama.Storage1.ISCSI.Initiator"; @@ -107,7 +108,9 @@ class DevicesManager { * @returns {Promise} * * @typedef {object} StorageDevice - * @property {string} sid - Internal id that is used as D-Bus object basename + * @property {string} sid - Storage ID + * @property {string} name - Device name + * @property {string} description - Device description * @property {boolean} isDrive - Whether the device is a drive * @property {string} type - Type of device ("disk", "raid", "multipath", "dasd", "md") * @property {string} [vendor] @@ -122,8 +125,10 @@ class DevicesManager { * @property {string[]} [wires] - Multipath wires (only for "multipath" type) * @property {string} [level] - MD RAID level (only for "md" type) * @property {string} [uuid] + * @property {number} [start] - First block of the region (only for block devices) * @property {boolean} [active] - * @property {string} [name] - Block device name + * @property {boolean} [encrypted] - Whether the device is encrypted (only for block devices) + * @property {boolean} [isEFI] - Whether the device is an EFI partition (only for partition) * @property {number} [size] * @property {number} [recoverableSize] * @property {string[]} [systems] - Name of the installed systems @@ -131,18 +136,36 @@ class DevicesManager { * @property {string[]} [udevPaths] * @property {PartitionTable} [partitionTable] * @property {Filesystem} [filesystem] + * @property {Component} [component] - When it is used as component of other devices + * @property {StorageDevice[]} [physicalVolumes] - Only for LVM VGs + * @property {StorageDevice[]} [logicalVolumes] - Only for LVM VGs * * @typedef {object} PartitionTable * @property {string} type * @property {StorageDevice[]} partitions + * @property {PartitionSlot[]} unusedSlots * @property {number} unpartitionedSize - Total size not assigned to any partition * + * @typedef {object} PartitionSlot + * @property {number} start + * @property {number} size + * + * @typedef {object} Component + * @property {string} type + * @property {string[]} deviceNames + * * @typedef {object} Filesystem * @property {string} type - * @property {boolean} isEFI + * @property {string} [mountPath] */ async getDevices() { const buildDevice = (path, dbusDevices) => { + const addDeviceProperties = (device, dbusProperties) => { + device.sid = dbusProperties.SID.v; + device.name = dbusProperties.Name.v; + device.description = dbusProperties.Description.v; + }; + const addDriveProperties = (device, dbusProperties) => { device.isDrive = true; device.type = dbusProperties.Type.v; @@ -173,7 +196,8 @@ class DevicesManager { const addBlockProperties = (device, blockProperties) => { device.active = blockProperties.Active.v; - device.name = blockProperties.Name.v; + device.encrypted = blockProperties.Encrypted.v; + device.start = blockProperties.Start.v; device.size = blockProperties.Size.v; device.recoverableSize = blockProperties.RecoverableSize.v; device.systems = blockProperties.Systems.v; @@ -181,19 +205,40 @@ class DevicesManager { device.udevPaths = blockProperties.UdevPaths.v; }; + const addPartitionProperties = (device, partitionProperties) => { + device.type = "partition"; + device.isEFI = partitionProperties.EFI.v; + }; + + const addLvmVgProperties = (device, lvmVgProperties) => { + device.type = "lvmVg"; + device.size = lvmVgProperties.Size.v; + device.physicalVolumes = lvmVgProperties.PhysicalVolumes.v.map(d => buildDevice(d, dbusDevices)); + device.logicalVolumes = lvmVgProperties.LogicalVolumes.v.map(d => buildDevice(d, dbusDevices)); + }; + + const addLvmLvProperties = (device) => { + device.type = "lvmLv"; + }; + const addPtableProperties = (device, ptableProperties) => { + const buildPartitionSlot = ([start, size]) => ({ start, size }); const partitions = ptableProperties.Partitions.v.map(p => buildDevice(p, dbusDevices)); device.partitionTable = { type: ptableProperties.Type.v, partitions, - unpartitionedSize: device.size - partitions.reduce((s, p) => s + p.size, 0) + unpartitionedSize: device.size - partitions.reduce((s, p) => s + p.size, 0), + unusedSlots: ptableProperties.UnusedSlots.v.map(buildPartitionSlot) }; }; const addFilesystemProperties = (device, filesystemProperties) => { + const buildMountPath = path => path.length > 0 ? path : undefined; + const buildLabel = label => label.length > 0 ? label : undefined; device.filesystem = { type: filesystemProperties.Type.v, - isEFI: filesystemProperties.EFI.v + mountPath: buildMountPath(filesystemProperties.MountPath.v), + label: buildLabel(filesystemProperties.Label.v) }; }; @@ -206,6 +251,8 @@ class DevicesManager { const device = { sid: path.split("/").pop(), + name: "", + description: "", isDrive: false, type: "" }; @@ -213,6 +260,9 @@ class DevicesManager { const dbusDevice = dbusDevices[path]; if (!dbusDevice) return device; + const deviceProperties = dbusDevice["org.opensuse.Agama.Storage1.Device"]; + if (deviceProperties !== undefined) addDeviceProperties(device, deviceProperties); + const driveProperties = dbusDevice["org.opensuse.Agama.Storage1.Drive"]; if (driveProperties !== undefined) addDriveProperties(device, driveProperties); @@ -228,6 +278,15 @@ class DevicesManager { const blockProperties = dbusDevice["org.opensuse.Agama.Storage1.Block"]; if (blockProperties !== undefined) addBlockProperties(device, blockProperties); + const partitionProperties = dbusDevice["org.opensuse.Agama.Storage1.Partition"]; + if (partitionProperties !== undefined) addPartitionProperties(device, partitionProperties); + + const lvmVgProperties = dbusDevice["org.opensuse.Agama.Storage1.LVM.VolumeGroup"]; + if (lvmVgProperties !== undefined) addLvmVgProperties(device, lvmVgProperties); + + const lvmLvProperties = dbusDevice["org.opensuse.Agama.Storage1.LVM.LogicalVolume"]; + if (lvmLvProperties !== undefined) addLvmLvProperties(device); + const ptableProperties = dbusDevice["org.opensuse.Agama.Storage1.PartitionTable"]; if (ptableProperties !== undefined) addPtableProperties(device, ptableProperties); @@ -313,7 +372,7 @@ class ProposalManager { async getAvailableDevices() { const findDevice = (devices, path) => { const sid = path.split("/").pop(); - const device = devices.find(d => d.sid === sid); + const device = devices.find(d => d.sid === Number(sid)); if (device === undefined) console.log("D-Bus object not found: ", path); @@ -388,6 +447,7 @@ class ProposalManager { const buildAction = dbusAction => { return { + device: dbusAction.Device.v, text: dbusAction.Text.v, subvol: dbusAction.Subvol.v, delete: dbusAction.Delete.v @@ -1467,6 +1527,7 @@ class StorageBaseClient { constructor(address = undefined) { this.client = new DBusClient(StorageBaseClient.SERVICE, address); this.system = new DevicesManager(this.client, STORAGE_SYSTEM_NAMESPACE); + this.staging = new DevicesManager(this.client, STORAGE_STAGING_NAMESPACE); this.proposal = new ProposalManager(this.client, this.system); this.iscsi = new ISCSIManager(StorageBaseClient.SERVICE, address); this.dasd = new DASDManager(StorageBaseClient.SERVICE, address); diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 73610f9a1b..5f68771c79 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -33,10 +33,10 @@ const cockpitCallbacks = {}; let managedObjects = {}; -// Define devices +// System devices const sda = { - sid: "59", + sid: 59, isDrive: true, type: "disk", vendor: "Micron", @@ -49,7 +49,10 @@ const sda = { sdCard: true, active: true, name: "/dev/sda", + description: "", size: 1024, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], @@ -57,33 +60,41 @@ const sda = { }; const sda1 = { - sid: "60", + sid: 60, isDrive: false, - type: "", + type: "partition", active: true, name: "/dev/sda1", + description: "", size: 512, + start: 123, + encrypted: false, recoverableSize: 128, systems : [], udevIds: [], - udevPaths: [] + udevPaths: [], + isEFI: false }; const sda2 = { - sid: "61", + sid: 61, isDrive: false, - type: "", + type: "partition", active: true, name: "/dev/sda2", + description: "", size: 256, + start: 1789, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], - udevPaths: [] + udevPaths: [], + isEFI: false }; const sdb = { - sid: "62", + sid: 62, isDrive: true, type: "disk", vendor: "Samsung", @@ -96,7 +107,10 @@ const sdb = { sdCard: false, active: true, name: "/dev/sdb", + description: "", size: 2048, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], @@ -104,7 +118,7 @@ const sdb = { }; const sdc = { - sid: "63", + sid: 63, isDrive: true, type: "disk", vendor: "Disk", @@ -117,7 +131,10 @@ const sdc = { sdCard: false, active: true, name: "/dev/sdc", + description: "", size: 2048, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], @@ -125,7 +142,7 @@ const sdc = { }; const sdd = { - sid: "64", + sid: 64, isDrive: true, type: "disk", vendor: "Disk", @@ -138,7 +155,10 @@ const sdd = { sdCard: false, active: true, name: "/dev/sdd", + description: "", size: 2048, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], @@ -146,7 +166,7 @@ const sdd = { }; const sde = { - sid: "65", + sid: 65, isDrive: true, type: "disk", vendor: "Disk", @@ -159,7 +179,10 @@ const sde = { sdCard: false, active: true, name: "/dev/sde", + description: "", size: 2048, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], @@ -167,23 +190,26 @@ const sde = { }; const md0 = { - sid: "66", + sid: 66, isDrive: false, type: "md", level: "raid0", uuid: "12345:abcde", active: true, name: "/dev/md0", + description: "EXT4 RAID", size: 2048, + start: 0, + encrypted: false, recoverableSize: 0, systems : ["openSUSE Leap 15.2"], udevIds: [], udevPaths: [], - filesystem: { type: "ext4", isEFI: false } + filesystem: { type: "ext4", mountPath: "/test", label: "system" } }; const raid = { - sid: "67", + sid: 67, isDrive: true, type: "raid", vendor: "Dell", @@ -196,7 +222,10 @@ const raid = { sdCard: false, active: true, name: "/dev/mapper/isw_ddgdcbibhd_244", + description: "", size: 2048, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], @@ -204,7 +233,7 @@ const raid = { }; const multipath = { - sid: "68", + sid: 68, isDrive: true, type: "multipath", vendor: "", @@ -217,7 +246,10 @@ const multipath = { sdCard: false, active: true, name: "/dev/mapper/36005076305ffc73a00000000000013b4", + description: "", size: 2048, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], @@ -225,7 +257,7 @@ const multipath = { }; const dasd = { - sid: "69", + sid: 69, isDrive: true, type: "dasd", vendor: "IBM", @@ -238,7 +270,76 @@ const dasd = { sdCard: false, active: true, name: "/dev/dasda", + description: "", size: 2048, + start: 0, + encrypted: false, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sdf = { + sid: 70, + isDrive: true, + type: "disk", + vendor: "Disk", + model: "", + driver: [], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdf", + description: "", + size: 2048, + start: 0, + encrypted: false, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sdf1 = { + sid: 71, + isDrive: false, + type: "partition", + active: true, + name: "/dev/sdf1", + description: "PV of vg0", + size: 512, + start: 1024, + encrypted: true, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [], + isEFI: false +}; + +const lvmVg = { + sid: 72, + isDrive: false, + type: "lvmVg", + name: "/dev/vg0", + description: "LVM", + size: 512 +}; + +const lvmLv1 = { + sid: 73, + isDrive: false, + type: "lvmLv", + active: true, + name: "/dev/vg0/lv1", + description: "", + size: 512, + start: 0, + encrypted: false, recoverableSize: 0, systems : [], udevIds: [], @@ -250,7 +351,8 @@ const dasd = { sda.partitionTable = { type: "gpt", partitions: [sda1, sda2], - unpartitionedSize: 256 + unpartitionedSize: 256, + unusedSlots: [{ start: 1234, size: 256 }] }; sda1.component = { @@ -283,13 +385,60 @@ sde.component = { deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"] }; +sdf.partitionTable = { + type: "gpt", + partitions: [sdf1], + unpartitionedSize: 1536, + unusedSlots: [] +}; + +sdf1.component = { + type: "physical_volume", + deviceNames: ["/dev/vg0"] +}; + md0.devices = [sda1, sda2]; raid.devices = [sdb, sdc]; multipath.wires = [sdd, sde]; -const systemDevices = { sda, sda1, sda2, sdb, sdc, sdd, sde, md0, raid, multipath, dasd }; +lvmVg.logicalVolumes = [lvmLv1]; +lvmVg.physicalVolumes = [sdf1]; + +const systemDevices = { + sda, sda1, sda2, sdb, sdc, sdd, sde, md0, raid, multipath, dasd, sdf, sdf1, lvmVg, lvmLv1 +}; + +// Staging devices +// +// Using a single device because most of the checks are already done with system devices. + +const sdbStaging = { + sid: 62, + isDrive: true, + type: "disk", + vendor: "Samsung", + model: "Samsung Evo 8 Pro", + driver: ["ahci"], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdb", + description: "", + size: 2048, + start: 0, + encrypted: false, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: ["pci-0000:00-19"] +}; + +const stagingDevices = { sdb: sdbStaging }; const contexts = { withoutProposal: () => { @@ -356,6 +505,7 @@ const contexts = { ], Actions: [ { + Device: { t: "u", v: 2 }, Text: { t: "s", v: "Mount /dev/sdb1 as root" }, Subvol: { t: "b", v: false }, Delete: { t: "b", v: false } @@ -477,6 +627,11 @@ const contexts = { }, withSystemDevices: () => { managedObjects["/org/opensuse/Agama/Storage1/system/59"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 59 }, + Name: { t: "s", v: "/dev/sda" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "disk" }, Vendor: { t: "s", v: "Micron" }, @@ -489,8 +644,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sda" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 1024 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"] }, @@ -501,14 +657,24 @@ const contexts = { Partitions: { t: "as", v: ["/org/opensuse/Agama/Storage1/system/60", "/org/opensuse/Agama/Storage1/system/61"] - } + }, + UnusedSlots: { t: "a(tt)", v: [[1234, 256]] } } }; managedObjects["/org/opensuse/Agama/Storage1/system/60"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 60 }, + Name: { t: "s", v: "/dev/sda1" }, + Description: { t: "s", v: "" } + }, + "org.opensuse.Agama.Storage1.Partition": { + EFI: { t: "b", v: false } + }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sda1" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 512 }, + Start: { t: "t", v: 123 }, RecoverableSize: { t: "x", v: 128 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -521,10 +687,19 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/61"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 61 }, + Name: { t: "s", v: "/dev/sda2" }, + Description: { t: "s", v: "" } + }, + "org.opensuse.Agama.Storage1.Partition": { + EFI: { t: "b", v: false } + }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sda2" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 256 }, + Start: { t: "t", v: 1789 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -537,6 +712,11 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/62"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 62 }, + Name: { t: "s", v: "/dev/sdb" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "disk" }, Vendor: { t: "s", v: "Samsung" }, @@ -549,8 +729,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sdb" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -563,6 +744,11 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/63"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 63 }, + Name: { t: "s", v: "/dev/sdc" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "disk" }, Vendor: { t: "s", v: "Disk" }, @@ -575,8 +761,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sdc" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -589,6 +776,11 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/64"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 64 }, + Name: { t: "s", v: "/dev/sdd" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "disk" }, Vendor: { t: "s", v: "Disk" }, @@ -601,8 +793,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sdd" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -615,6 +808,11 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/65"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 65 }, + Name: { t: "s", v: "/dev/sde" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "disk" }, Vendor: { t: "s", v: "Disk" }, @@ -627,8 +825,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sde" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -641,6 +840,11 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/66"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 66 }, + Name: { t: "s", v: "/dev/md0" }, + Description: { t: "s", v: "EXT4 RAID" } + }, "org.opensuse.Agama.Storage1.MD": { Level: { t: "s", v: "raid0" }, UUID: { t: "s", v: "12345:abcde" }, @@ -651,8 +855,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/md0" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: ["openSUSE Leap 15.2"] }, UdevIds: { t: "as", v: [] }, @@ -660,10 +865,16 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Filesystem": { Type: { t: "s", v: "ext4" }, - EFI: { t: "b", v: false } + MountPath: { t: "s", v: "/test" }, + Label: { t: "s", v: "system" } } }; managedObjects["/org/opensuse/Agama/Storage1/system/67"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 67 }, + Name: { t: "s", v: "/dev/mapper/isw_ddgdcbibhd_244" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "raid" }, Vendor: { t: "s", v: "Dell" }, @@ -682,8 +893,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/mapper/isw_ddgdcbibhd_244" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -691,6 +903,11 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/68"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 68 }, + Name: { t: "s", v: "/dev/mapper/36005076305ffc73a00000000000013b4" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "multipath" }, Vendor: { t: "s", v: "" }, @@ -709,8 +926,9 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/mapper/36005076305ffc73a00000000000013b4" }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, @@ -718,6 +936,11 @@ const contexts = { } }; managedObjects["/org/opensuse/Agama/Storage1/system/69"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 69 }, + Name: { t: "s", v: "/dev/dasda" }, + Description: { t: "s", v: "" } + }, "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "dasd" }, Vendor: { t: "s", v: "IBM" }, @@ -730,12 +953,135 @@ const contexts = { }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/dasda" }, + Encrypted: { t: "b", v: false }, + Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/70"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 70 }, + Name: { t: "s", v: "/dev/sdf" }, + Description: { t: "s", v: "" } + }, + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Disk" }, + Model: { t: "s", v: "" }, + Driver: { t: "as", v: [] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Encrypted: { t: "b", v: false }, Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.PartitionTable": { + Type: { t: "s", v: "gpt" }, + Partitions: { + t: "as", + v: ["/org/opensuse/Agama/Storage1/system/71"] + }, + UnusedSlots: { t: "a(tt)", v: [] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/71"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 71 }, + Name: { t: "s", v: "/dev/sdf1" }, + Description: { t: "s", v: "PV of vg0" } + }, + "org.opensuse.Agama.Storage1.Partition": { + EFI: { t: "b", v: false } + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Encrypted: { t: "b", v: true }, + Size: { t: "x", v: 512 }, + Start: { t: "t", v: 1024 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "physical_volume" }, + DeviceNames: { t: "as", v: ["/dev/vg0"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/72"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/72"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 72 }, + Name: { t: "s", v: "/dev/vg0" }, + Description: { t: "s", v: "LVM" } + }, + "org.opensuse.Agama.Storage1.LVM.VolumeGroup": { + Type: { t: "s", v: "physical_volume" }, + Size: { t: "x", v: 512 }, + PhysicalVolumes: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/71"] }, + LogicalVolumes: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/73"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/73"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 73 }, + Name: { t: "s", v: "/dev/vg0/lv1" }, + Description: { t: "s", v: "" } + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Encrypted: { t: "b", v: false }, + Size: { t: "x", v: 512 }, + Start: { t: "t", v: 0 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.LVM.LogicalVolume": { + VolumeGroup: { t: "o", v: "/org/opensuse/Agama/Storage1/system/72" } + } + }; + }, + withStagingDevices: () => { + managedObjects["/org/opensuse/Agama/Storage1/staging/62"] = { + "org.opensuse.Agama.Storage1.Device": { + SID: { t: "u", v: 62 }, + Name: { t: "s", v: "/dev/sdb" }, + Description: { t: "s", v: "" } + }, + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Samsung" }, + Model: { t: "s", v: "Samsung Evo 8 Pro" }, + Driver: { t: "as", v: ["ahci"] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Encrypted: { t: "b", v: false }, + Size: { t: "x", v: 2048 }, + Start: { t: "t", v: 0 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: ["pci-0000:00-19"] } } }; } @@ -971,6 +1317,33 @@ describe("#system", () => { }); }); +describe("#staging", () => { + describe("#getDevices", () => { + describe("when there are devices", () => { + beforeEach(() => { + contexts.withStagingDevices(); + client = new StorageClient(); + }); + + it("returns the staging devices", async () => { + const devices = await client.staging.getDevices(); + expect(devices).toEqual(Object.values(stagingDevices)); + }); + }); + + describe("when there are not devices", () => { + beforeEach(() => { + client = new StorageClient(); + }); + + it("returns an empty list", async () => { + const devices = await client.staging.getDevices(); + expect(devices).toEqual([]); + }); + }); + }); +}); + describe("#proposal", () => { describe("#getAvailableDevices", () => { beforeEach(() => { @@ -1166,7 +1539,7 @@ describe("#proposal", () => { ); expect(actions).toStrictEqual([ - { text: "Mount /dev/sdb1 as root", subvol: false, delete: false } + { device: 2, text: "Mount /dev/sdb1 as root", subvol: false, delete: false } ]); }); }); From e40435ce7b7411a4ab1f1bdb5328cea344b789c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 11 Mar 2024 12:30:38 +0000 Subject: [PATCH 26/98] web: Load devices in the proposal page --- web/src/components/storage/ProposalPage.jsx | 23 +++++++++++++++++-- .../components/storage/ProposalPage.test.jsx | 6 +++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index 51ab17e7bf..ccb3bfdf4c 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -41,6 +41,8 @@ const initialState = { volumeTemplates: [], encryptionMethods: [], settings: {}, + system: [], + staging: [], actions: [], errors: [] }; @@ -80,6 +82,11 @@ const reducer = (state, action) => { return { ...state, settings }; } + case "UPDATE_DEVICES": { + const { system, staging } = action.payload; + return { ...state, system, staging }; + } + case "UPDATE_ERRORS": { const { errors } = action.payload; return { ...state, errors }; @@ -120,6 +127,12 @@ export default function ProposalPage() { return await cancellablePromise(client.proposal.getResult()); }, [client, cancellablePromise]); + const loadDevices = useCallback(async () => { + const system = await cancellablePromise(client.system.getDevices()) || []; + const staging = await cancellablePromise(client.staging.getDevices()) || []; + return { system, staging }; + }, [client, cancellablePromise]); + const loadErrors = useCallback(async () => { const issues = await cancellablePromise(client.getErrors()); return issues.map(toValidationError); @@ -151,11 +164,14 @@ export default function ProposalPage() { const result = await loadProposalResult(); if (result !== undefined) dispatch({ type: "UPDATE_RESULT", payload: { result } }); + const devices = await loadDevices(); + dispatch({ type: "UPDATE_DEVICES", payload: devices }); + const errors = await loadErrors(); dispatch({ type: "UPDATE_ERRORS", payload: { errors } }); if (result !== undefined) dispatch({ type: "STOP_LOADING" }); - }, [calculateProposal, cancellablePromise, client, loadAvailableDevices, loadEncryptionMethods, loadErrors, loadProposalResult, loadVolumeTemplates]); + }, [calculateProposal, cancellablePromise, client, loadAvailableDevices, loadDevices, loadEncryptionMethods, loadErrors, loadProposalResult, loadVolumeTemplates]); const calculate = useCallback(async (settings) => { dispatch({ type: "START_LOADING" }); @@ -165,11 +181,14 @@ export default function ProposalPage() { const result = await loadProposalResult(); dispatch({ type: "UPDATE_RESULT", payload: { result } }); + const devices = await loadDevices(); + dispatch({ type: "UPDATE_DEVICES", payload: devices }); + const errors = await loadErrors(); dispatch({ type: "UPDATE_ERRORS", payload: { errors } }); dispatch({ type: "STOP_LOADING" }); - }, [calculateProposal, loadErrors, loadProposalResult]); + }, [calculateProposal, loadDevices, loadErrors, loadProposalResult]); useEffect(() => { load().catch(console.error); diff --git a/web/src/components/storage/ProposalPage.test.jsx b/web/src/components/storage/ProposalPage.test.jsx index 3b8464f84b..a8ea0d53d6 100644 --- a/web/src/components/storage/ProposalPage.test.jsx +++ b/web/src/components/storage/ProposalPage.test.jsx @@ -86,6 +86,12 @@ const storageMock = { defaultVolume: jest.fn(mountPath => Promise.resolve({ mountPath })), calculate: jest.fn().mockResolvedValue(0) }, + system: { + getDevices: jest.fn().mockResolvedValue([vda, vdb]) + }, + staging: { + getDevices: jest.fn().mockResolvedValue([vda]) + }, getErrors: jest.fn().mockResolvedValue([]), isDeprecated: jest.fn().mockResolvedValue(false), onDeprecate: jest.fn(), From 08b586fbd2375b998afbbeacb4eca35133f3f27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 13 Mar 2024 10:43:52 +0100 Subject: [PATCH 27/98] Code review fixes --- rust/agama-server/src/cert.rs | 4 +--- rust/package/agama.changes | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/rust/agama-server/src/cert.rs b/rust/agama-server/src/cert.rs index d2a3eb422f..ecaf1c520f 100644 --- a/rust/agama-server/src/cert.rs +++ b/rust/agama-server/src/cert.rs @@ -1,13 +1,11 @@ use openssl::error::ErrorStack; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; -use openssl::x509::extension::{KeyUsage, SubjectAlternativeName}; use openssl::x509::{X509NameBuilder, X509}; - use openssl::asn1::Asn1Time; use openssl::bn::{BigNum, MsbOption}; use openssl::hash::MessageDigest; -use openssl::x509::extension::{BasicConstraints, SubjectKeyIdentifier}; +use openssl::x509::extension::{BasicConstraints, SubjectKeyIdentifier, KeyUsage, SubjectAlternativeName}; // Generate a self-signed SSL certificate // see https://github.com/sfackler/rust-openssl/blob/master/openssl/examples/mk_certs.rs diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 1ec250c748..44559ff4e3 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -2,8 +2,8 @@ Thu Feb 29 09:49:18 UTC 2024 - Ladislav Slezák - Web server: - - Accept also IPv6 connections - - Added SSL (HTTPS) support + - Accept also IPv6 connections (gh#openSUSE/agama#1057) + - Added SSL (HTTPS) support (gh#openSUSE/agama#1062) - Use either the cerfificate specified via command line arguments or generate a self-signed certificate - Redirect external HTTP requests to HTTPS From 8a6948cbc2b06430cf0c078fe1566e46457a51da Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg Date: Thu, 8 Feb 2024 14:29:27 +0100 Subject: [PATCH 28/98] Add infiniband to network model --- rust/agama-server/src/network/model.rs | 41 +++++++++ rust/agama-server/src/network/nm/dbus.rs | 109 ++++++++++++++++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 551c353496..624618094a 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -469,6 +469,7 @@ pub enum ConnectionConfig { Bond(BondConfig), Vlan(VlanConfig), Bridge(BridgeConfig), + Infiniband(InfinibandConfig), } #[derive(Default, Debug, PartialEq, Clone)] @@ -999,3 +1000,43 @@ pub struct BridgePortConfig { pub priority: Option, pub path_cost: Option, } + +#[derive(Default, Debug, PartialEq, Clone)] +pub struct InfinibandConfig { + pub p_key: Option, + pub parent: Option, + pub transport_mode: InfinibandTransportMode, +} + +#[derive(Default, Debug, PartialEq, Clone)] +pub enum InfinibandTransportMode { + #[default] + Datagram, + Connected, +} + +#[derive(Debug, Error)] +#[error("Invalid infiniband transport-mode: {0}")] +pub struct InvalidInfinibandTransportMode(String); + +impl FromStr for InfinibandTransportMode { + type Err = InvalidInfinibandTransportMode; + + fn from_str(s: &str) -> Result { + match s { + "datagram" => Ok(Self::Datagram), + "connected" => Ok(Self::Connected), + _ => Err(InvalidInfinibandTransportMode(s.to_string())), + } + } +} + +impl fmt::Display for InfinibandTransportMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + InfinibandTransportMode::Datagram => "datagram", + InfinibandTransportMode::Connected => "connected", + }; + write!(f, "{}", name) + } +} diff --git a/rust/agama-server/src/network/nm/dbus.rs b/rust/agama-server/src/network/nm/dbus.rs index 75876ad163..7630c8aaca 100644 --- a/rust/agama-server/src/network/nm/dbus.rs +++ b/rust/agama-server/src/network/nm/dbus.rs @@ -23,6 +23,7 @@ const DUMMY_KEY: &str = "dummy"; const VLAN_KEY: &str = "vlan"; const BRIDGE_KEY: &str = "bridge"; const BRIDGE_PORT_KEY: &str = "bridge-port"; +const INFINIBAND_KEY: &str = "infiniband"; /// Converts a connection struct into a HashMap that can be sent over D-Bus. /// @@ -97,6 +98,10 @@ pub fn connection_to_dbus<'a>( connection_dbus.insert("type", BRIDGE_KEY.into()); result.insert(BRIDGE_KEY, bridge_config_to_dbus(bridge)); } + ConnectionConfig::Infiniband(infiniband) => { + connection_dbus.insert("type", INFINIBAND_KEY.into()); + result.insert(INFINIBAND_KEY, infiniband_config_to_dbus(infiniband)); + } _ => {} } @@ -141,6 +146,11 @@ pub fn connection_from_dbus(conn: OwnedNestedHash) -> Option { return Some(connection); } + if let Some(infiniband_config) = infiniband_config_from_dbus(&conn) { + connection.config = ConnectionConfig::Infiniband(infiniband_config); + return Some(connection); + } + if conn.get(DUMMY_KEY).is_some() { connection.config = ConnectionConfig::Dummy; return Some(connection); @@ -477,6 +487,45 @@ fn bridge_port_config_from_dbus(conn: &OwnedNestedHash) -> Option HashMap<&str, zvariant::Value> { + let mut infiniband_config: HashMap<&str, zvariant::Value> = HashMap::from([ + ( + "transport-mode", + Value::new(config.transport_mode.to_string()), + ), + ("p-key", Value::new(config.p_key.unwrap_or(-1))), + ]); + + if let Some(parent) = &config.parent { + infiniband_config.insert("parent", parent.into()); + } + + infiniband_config +} + +fn infiniband_config_from_dbus(conn: &OwnedNestedHash) -> Option { + let Some(infiniband) = conn.get(INFINIBAND_KEY) else { + return None; + }; + + let mut infiniband_config = InfinibandConfig::default(); + + if let Some(p_key) = infiniband.get("p-key") { + infiniband_config.p_key = Some(*p_key.downcast_ref::()?); + } + + if let Some(parent) = infiniband.get("parent") { + infiniband_config.parent = Some(parent.downcast_ref::()?.to_string()); + } + + if let Some(transport_mode) = infiniband.get("transport-mode") { + infiniband_config.transport_mode = + InfinibandTransportMode::from_str(transport_mode.downcast_ref::()?).ok()?; + } + + Some(infiniband_config) +} + /// Converts a MatchConfig struct into a HashMap that can be sent over D-Bus. /// /// * `match_config`: MatchConfig to convert. @@ -852,7 +901,7 @@ mod test { }; use crate::network::{ model::*, - nm::dbus::{BOND_KEY, ETHERNET_KEY, WIRELESS_KEY, WIRELESS_SECURITY_KEY}, + nm::dbus::{BOND_KEY, ETHERNET_KEY, INFINIBAND_KEY, WIRELESS_KEY, WIRELESS_SECURITY_KEY}, }; use agama_lib::network::types::{BondMode, SSID}; use cidr::IpInet; @@ -1080,6 +1129,64 @@ mod test { } } + #[test] + fn test_connection_from_dbus_infiniband() { + let uuid = Uuid::new_v4().to_string(); + let connection_section = HashMap::from([ + ("id".to_string(), Value::new("ib0").to_owned()), + ("uuid".to_string(), Value::new(uuid).to_owned()), + ]); + + let infiniband_section = HashMap::from([ + ("p-key".to_string(), Value::new(0x8001 as i32).to_owned()), + ("parent".to_string(), Value::new("ib0").to_owned()), + ( + "transport-mode".to_string(), + Value::new("datagram").to_owned(), + ), + ]); + + let dbus_conn = HashMap::from([ + ("connection".to_string(), connection_section), + (INFINIBAND_KEY.to_string(), infiniband_section), + ]); + + let connection = connection_from_dbus(dbus_conn).unwrap(); + let ConnectionConfig::Infiniband(infiniband) = &connection.config else { + panic!("Wrong connection type") + }; + assert_eq!(infiniband.p_key, Some(0x8001)); + assert_eq!(infiniband.parent, Some("ib0".to_string())); + assert_eq!(infiniband.transport_mode, InfinibandTransportMode::Datagram); + } + + #[test] + fn test_dbus_from_infiniband_connection() { + let config = InfinibandConfig { + p_key: Some(0x8002), + parent: Some("ib1".to_string()), + transport_mode: InfinibandTransportMode::Connected, + }; + let mut infiniband = build_base_connection(); + infiniband.config = ConnectionConfig::Infiniband(config); + let infiniband_dbus = connection_to_dbus(&infiniband, None); + + let infiniband = infiniband_dbus.get(INFINIBAND_KEY).unwrap(); + let p_key: i32 = *infiniband.get("p-key").unwrap().downcast_ref().unwrap(); + assert_eq!(p_key, 0x8002); + let parent: &str = infiniband.get("parent").unwrap().downcast_ref().unwrap(); + assert_eq!(parent, "ib1"); + let transport_mode: &str = infiniband + .get("transport-mode") + .unwrap() + .downcast_ref() + .unwrap(); + assert_eq!( + transport_mode, + InfinibandTransportMode::Connected.to_string() + ); + } + #[test] fn test_dbus_from_wireless_connection() { let config = WirelessConfig { From c75ed020bc62c7db77f325b3e479c9060dbe388d Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg Date: Thu, 8 Feb 2024 15:45:04 +0100 Subject: [PATCH 29/98] Update agama-cli.changes --- rust/package/agama.changes | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 8e64449ffe..7a14494058 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Wed Mar 13 12:42:58 UTC 2024 - Jorik Cronenberg + +- Add infiniband to network model (gh#openSUSE/agama#1032). + ------------------------------------------------------------------- Thu Mar 7 10:52:58 UTC 2024 - Michal Filka From 108f16d9fd8f2d246a7147e7895a1eb19722d130 Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Tue, 12 Mar 2024 16:50:30 +0000 Subject: [PATCH 30/98] WIP: SpacePolicy as pop-up --- .../storage/ProposalFileSystemsSection.jsx | 80 ----- .../ProposalFileSystemsSection.test.jsx | 55 ---- web/src/components/storage/ProposalPage.jsx | 12 - .../components/storage/ProposalPage.test.jsx | 4 +- .../storage/ProposalSettingsSection.jsx | 35 ++ .../storage/ProposalSettingsSection.test.jsx | 27 +- ...ction.jsx => ProposalSpacePolicyField.jsx} | 208 ++++++++---- .../storage/ProposalSpacePolicyField.test.jsx | 299 ++++++++++++++++++ .../ProposalSpacePolicySection.test.jsx | 266 ---------------- .../components/storage/ProposalVolumes.jsx | 3 + web/src/components/storage/index.js | 3 +- 11 files changed, 521 insertions(+), 471 deletions(-) delete mode 100644 web/src/components/storage/ProposalFileSystemsSection.jsx delete mode 100644 web/src/components/storage/ProposalFileSystemsSection.test.jsx rename web/src/components/storage/{ProposalSpacePolicySection.jsx => ProposalSpacePolicyField.jsx} (65%) create mode 100644 web/src/components/storage/ProposalSpacePolicyField.test.jsx delete mode 100644 web/src/components/storage/ProposalSpacePolicySection.test.jsx diff --git a/web/src/components/storage/ProposalFileSystemsSection.jsx b/web/src/components/storage/ProposalFileSystemsSection.jsx deleted file mode 100644 index 0b0e5f632d..0000000000 --- a/web/src/components/storage/ProposalFileSystemsSection.jsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { _ } from "~/i18n"; -import { Section } from "~/components/core"; -import { ProposalVolumes } from "~/components/storage"; -import { noop } from "~/utils"; - -/** - * @typedef {import ("~/client/storage").ProposalManager.ProposalSettings} ProposalSettings - * @typedef {import ("~/client/storage").ProposalManager.Volume} Volume - */ - -/** - * Section for editing the proposal file systems - * @component - * - * @callback onChangeFn - * @param {object} settings - * - * @param {object} props - * @param {ProposalSettings} props.settings - * @param {Volume[]} [props.volumeTemplates=[]] - * @param {boolean} [props.isLoading=false] - * @param {onChangeFn} [props.onChange=noop] - * - */ -export default function ProposalFileSystemsSection({ - settings, - volumeTemplates = [], - isLoading = false, - onChange = noop -}) { - const { volumes = [] } = settings; - - const changeVolumes = (volumes) => { - onChange({ volumes }); - }; - - // Templates for already existing mount points are filtered out - const usefulTemplates = () => { - const mountPaths = volumes.map(v => v.mountPath); - return volumeTemplates.filter(t => ( - t.mountPath.length > 0 && !mountPaths.includes(t.mountPath) - )); - }; - - const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0; - - return ( -
- -
- ); -} diff --git a/web/src/components/storage/ProposalFileSystemsSection.test.jsx b/web/src/components/storage/ProposalFileSystemsSection.test.jsx deleted file mode 100644 index 0b1493a5ff..0000000000 --- a/web/src/components/storage/ProposalFileSystemsSection.test.jsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import { ProposalFileSystemsSection } from "~/components/storage"; - -const props = { - settings: {}, - isLoading: false, - onChange: jest.fn() -}; - -describe("ProposalFileSystemsSection", () => { - it("renders a section holding file systems related stuff", () => { - plainRender(); - screen.getByRole("region", { name: "File systems" }); - screen.getByRole("grid", { name: /mount points/ }); - }); - - it("requests a volume change when onChange callback is triggered", async () => { - const { user } = plainRender(); - const button = screen.getByRole("button", { name: "Actions" }); - - await user.click(button); - - const menu = screen.getByRole("menu"); - const reset = within(menu).getByRole("menuitem", { name: /Reset/ }); - - await user.click(reset); - - expect(props.onChange).toHaveBeenCalledWith( - { volumes: expect.any(Array) } - ); - }); -}); diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index 85e29c5b41..c3d4753623 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -29,9 +29,7 @@ import { ProposalActionsSection, ProposalPageMenu, ProposalSettingsSection, - ProposalSpacePolicySection, ProposalDeviceSection, - ProposalFileSystemsSection, ProposalTransactionalInfo } from "~/components/storage"; import { IDLE } from "~/client/status"; @@ -233,17 +231,7 @@ export default function ProposalPage() { - - { await screen.findByText(/\/dev\/vda/); }); -it("renders the settings, find space and actions sections", async () => { +it("renders the device, settings and actions sections", async () => { installerRender(); + await screen.findByText(/Device/); await screen.findByText(/Settings/); - await screen.findByText(/Find Space/); await screen.findByText(/Planned Actions/); }); diff --git a/web/src/components/storage/ProposalSettingsSection.jsx b/web/src/components/storage/ProposalSettingsSection.jsx index abc6830308..969d862b1f 100644 --- a/web/src/components/storage/ProposalSettingsSection.jsx +++ b/web/src/components/storage/ProposalSettingsSection.jsx @@ -24,6 +24,7 @@ import { Checkbox, Form, Skeleton, Switch, Tooltip } from "@patternfly/react-cor import { _ } from "~/i18n"; import { If, PasswordAndConfirmationInput, Section, Popup } from "~/components/core"; +import { ProposalVolumes, ProposalSpacePolicyField } from "~/components/storage"; import { Icon } from "~/components/layout"; import { noop } from "~/utils"; import { hasFS } from "~/components/storage/utils"; @@ -283,6 +284,8 @@ const EncryptionField = ({ export default function ProposalSettingsSection({ settings, encryptionMethods = [], + volumeTemplates = [], + isLoading = false, onChange = noop }) { const changeEncryption = ({ password, method }) => { @@ -302,8 +305,26 @@ export default function ProposalSettingsSection({ onChange({ volumes: settings.volumes }); }; + const changeVolumes = (volumes) => { + onChange({ volumes }); + }; + + const changeSpacePolicy = (policy, actions) => { + onChange({ spacePolicy: policy, spaceActions: actions }); + }; + const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0; + const { volumes = [] } = settings; + + // Templates for already existing mount points are filtered out + const usefulTemplates = () => { + const mountPaths = volumes.map(v => v.mountPath); + return volumeTemplates.filter(t => ( + t.mountPath.length > 0 && !mountPaths.includes(t.mountPath) + )); + }; + return ( <>
@@ -319,6 +340,20 @@ export default function ProposalSettingsSection({ isLoading={settings.encryptionPassword === undefined} onChange={changeEncryption} /> + +
); diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index 3077cd7903..29f7fc4723 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -36,7 +36,10 @@ jest.mock("@patternfly/react-core", () => { let props; beforeEach(() => { - props = {}; + props = { + settings: {}, + onChange: jest.fn() + }; }); const rootVolume = { mountPath: "/", fsType: "Btrfs", outline: { snapshotsConfigurable: true } }; @@ -65,6 +68,28 @@ describe("if snapshots are not configurable", () => { }); }); +it("renders a section holding file systems related stuff", () => { + plainRender(); + screen.getByRole("grid", { name: "Table with mount points" }); + screen.getByRole("grid", { name: /mount points/ }); +}); + +it("requests a volume change when onChange callback is triggered", async () => { + const { user } = plainRender(); + const button = screen.getByRole("button", { name: "Actions" }); + + await user.click(button); + + const menu = screen.getByRole("menu"); + const reset = within(menu).getByRole("menuitem", { name: /Reset/ }); + + await user.click(reset); + + expect(props.onChange).toHaveBeenCalledWith( + { volumes: expect.any(Array) } + ); +}); + describe("Encryption field", () => { describe("if encryption password setting is not set yet", () => { beforeEach(() => { diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicyField.jsx similarity index 65% rename from web/src/components/storage/ProposalSpacePolicySection.jsx rename to web/src/components/storage/ProposalSpacePolicyField.jsx index c08135c0e5..d9fc0479ab 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicyField.jsx @@ -19,12 +19,12 @@ * find current contact information at www.suse.com. */ -import React, { useEffect } from "react"; -import { FormSelect, FormSelectOption } from "@patternfly/react-core"; +import React, { useEffect, useState } from "react"; +import { Button, Form, FormSelect, FormSelectOption, Skeleton } from "@patternfly/react-core"; -import { _, N_ } from "~/i18n"; +import { _, N_, n_ } from "~/i18n"; import { deviceSize } from '~/components/storage/utils'; -import { If, OptionsPicker, Section, SectionSkeleton } from "~/components/core"; +import { If, OptionsPicker, Popup, SectionSkeleton } from "~/components/core"; import { noop, useLocalStorage } from "~/utils"; import { sprintf } from "sprintf-js"; import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/react-table'; @@ -48,22 +48,48 @@ const SPACE_POLICIES = [ { id: "delete", label: N_("Delete current content"), - description: N_("All partitions will be removed and any data in the disks will be lost.") + description: N_("All partitions will be removed and any data in the disks will be lost."), + summaryLabels: [ + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space deleting all content[...]" + N_("deleting all content of the installation device"), + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space deleting all content[...]" + N_("deleting all content of the %d selected disks") + ] }, { id: "resize", label: N_("Shrink existing partitions"), - description: N_("The data is kept, but the current partitions will be resized as needed.") + description: N_("The data is kept, but the current partitions will be resized as needed."), + summaryLabels: [ + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space shrinking partitions[...]" + N_("shrinking partitions of the installation device"), + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space shrinking partitions[...]" + N_("shrinking partitions of the %d selected disks") + ] }, { id: "keep", label: N_("Use available space"), - description: N_("The data is kept. Only the space not assigned to any partition will be used.") + description: N_("The data is kept. Only the space not assigned to any partition will be used."), + summaryLabels: [ + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space without modifying any partition". + N_("without modifying any partition") + ] }, { id: "custom", label: N_("Custom"), - description: N_("Select what to do with each partition.") + description: N_("Select what to do with each partition."), + summaryLabels: [ + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space performing a custom set of actions". + N_("performing a custom set of actions") + ] } ]; @@ -250,7 +276,8 @@ const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noo */ const DeviceRow = ({ device, - settings, + policy, + actions, rowIndex, level = 1, setSize = 0, @@ -273,19 +300,20 @@ const DeviceRow = ({ } }; - const spaceAction = settings.spaceActions.find(a => a.device === device.name); - const isDisabled = settings.spacePolicy !== "custom"; + const spaceAction = actions.find(a => a.device === device.name); + const isDisabled = policy.id !== "custom"; const showAction = !device.partitionTable; return ( - + {/* eslint-disable agama-i18n/string-literals */} + - - - - + + + + + {/* eslint-enable agama-i18n/string-literals */} ); }; @@ -310,31 +339,30 @@ const DeviceRow = ({ * @param {ProposalSettings} props.settings * @param {(action: SpaceAction) => void} [props.onChange] */ -const SpaceActionsTable = ({ settings, onChange = noop }) => { +const SpaceActionsTable = ({ policy, actions, devices, onChange = noop }) => { const [expandedDevices, setExpandedDevices] = useLocalStorage("storage-space-actions-expanded", []); const [autoExpanded, setAutoExpanded] = useLocalStorage("storage-space-actions-auto-expanded", false); useEffect(() => { - const policy = settings.spacePolicy; - const devices = settings.installationDevices.map(d => d.name); - let currentExpanded = devices.filter(d => expandedDevices.includes(d)); + const dev_names = devices.map(d => d.name); + let currentExpanded = dev_names.filter(d => expandedDevices.includes(d)); - if (policy === "custom" && !autoExpanded) { - currentExpanded = [...devices]; + if (policy.id === "custom" && !autoExpanded) { + currentExpanded = [...dev_names]; setAutoExpanded(true); - } else if (policy !== "custom" && autoExpanded) { + } else if (policy.id !== "custom" && autoExpanded) { setAutoExpanded(false); } if (currentExpanded.sort().toString() !== expandedDevices.sort().toString()) { setExpandedDevices(currentExpanded); } - }, [autoExpanded, expandedDevices, setAutoExpanded, setExpandedDevices, settings]); + }, [autoExpanded, expandedDevices, setAutoExpanded, setExpandedDevices, policy, devices]); const renderRows = () => { const rows = []; - settings.installationDevices?.forEach((device, index) => { + devices?.forEach((device, index) => { const isExpanded = expandedDevices.includes(device.name); const children = device.partitionTable?.partitions; @@ -348,7 +376,8 @@ const SpaceActionsTable = ({ settings, onChange = noop }) => { { { const SpacePolicyPicker = ({ currentPolicy, onChange = noop }) => { return ( + {/* eslint-disable agama-i18n/string-literals */} {SPACE_POLICIES.map((policy) => { return ( onChange(policy.id)} + title={_(policy.label)} + body={_(policy.description)} + onClick={() => onChange(policy)} isSelected={currentPolicy?.id === policy.id} /> ); })} + {/* eslint-enable agama-i18n/string-literals */} ); }; @@ -428,45 +460,115 @@ const SpacePolicyPicker = ({ currentPolicy, onChange = noop }) => { * @param {boolean} [isLoading=false] * @param {(settings: ProposalSettings) => void} [onChange] */ -export default function ProposalSpacePolicySection({ - settings, +const SpacePolicyForm = ({ + id, + currentPolicy, + currentActions, + devices, isLoading = false, - onChange = noop -}) { - const changeSpacePolicy = (policy) => { - onChange({ spacePolicy: policy }); - }; + onSubmit = noop +}) => { + const [policy, setPolicy] = useState(currentPolicy); + const [actions, setActions] = useState(currentActions); - const changeSpaceActions = (spaceAction) => { - const spaceActions = settings.spaceActions.filter(a => a.device !== spaceAction.device); + const changeActions = (spaceAction) => { + const spaceActions = actions.filter(a => a.device !== spaceAction.device); if (spaceAction.action !== "keep") spaceActions.push(spaceAction); - onChange({ spaceActions }); + setActions(spaceActions); }; - const currentPolicy = SPACE_POLICIES.find(p => p.id === settings.spacePolicy) || SPACE_POLICIES[0]; + const submitForm = (e) => { + e.preventDefault(); + if (policy !== undefined) onSubmit(policy, actions); + }; return ( -
+
} else={ <> - + 0} - then={} + condition={devices.length > 0} + then={} /> } /> -
+ + ); +}; + +const SpacePolicyButton = ({ policy, devices, onClick = noop }) => { + const Text = () => { + // eslint-disable-next-line agama-i18n/string-literals + if (policy.summaryLabels.length === 1) return _(policy.summaryLabels[0]); + + // eslint-disable-next-line agama-i18n/string-literals + return sprintf(n_(policy.summaryLabels[0], policy.summaryLabels[1], devices.length), devices.length); + }; + + return ; +}; + +export default function ProposalSpacePolicyField({ + policy, + actions = [], + devices = [], + isLoading = false, + onChange = noop +}) { + const spacePolicy = SPACE_POLICIES.find(p => p.id === policy) || SPACE_POLICIES[0]; + const [isFormOpen, setIsFormOpen] = useState(false); + + const openForm = () => setIsFormOpen(true); + const closeForm = () => setIsFormOpen(false); + + const acceptForm = (spacePolicy, actions) => { + closeForm(); + onChange(spacePolicy.id, actions); + }; + + if (isLoading) { + return ; + } + + const description = _("Allocating the file systems might need to find free space \ +in the devices listed below. Choose how to do it."); + + return ( +
+ {/* TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" */} + {_("Find space")} + + +
+ +
+ + + {_("Accept")} + + + +
+
); } diff --git a/web/src/components/storage/ProposalSpacePolicyField.test.jsx b/web/src/components/storage/ProposalSpacePolicyField.test.jsx new file mode 100644 index 0000000000..1094feefa6 --- /dev/null +++ b/web/src/components/storage/ProposalSpacePolicyField.test.jsx @@ -0,0 +1,299 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { plainRender, resetLocalStorage } from "~/test-utils"; +import { ProposalSpacePolicyField } from "~/components/storage"; + +const sda = { + sid: "59", + isDrive: true, + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + dellBOSS: false, + sdCard: true, + active: true, + name: "/dev/sda", + size: 1024, + recoverableSize: 0, + systems : [], + udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], +}; + +const sda1 = { + sid: "60", + isDrive: false, + type: "", + active: true, + name: "/dev/sda1", + size: 512, + recoverableSize: 128, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sda2 = { + sid: "61", + isDrive: false, + type: "", + active: true, + name: "/dev/sda2", + size: 512, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +sda.partitionTable = { + type: "gpt", + partitions: [sda1, sda2], + unpartitionedSize: 512 +}; + +const sdb = { + sid: "62", + isDrive: true, + type: "disk", + vendor: "Samsung", + model: "Samsung Evo 8 Pro", + driver: ["ahci"], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdb", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: ["pci-0000:00-19"] +}; + +let policy; +let devices; +let actions; + +const openPopup = async (props = {}) => { + const allProps = { policy, devices, actions, ...props }; + const { user } = plainRender(); + const button = screen.getByRole("button"); + + await user.click(button); + const dialog = screen.getByRole("dialog", { name: "Find Space" }); + return { user, dialog }; +}; + +beforeEach(() => { + devices = [sda, sdb]; + policy = "keep"; + actions = [ + { device: "/dev/sda1", action: "force_delete" }, + { device: "/dev/sda2", action: "resize" } + ]; + + resetLocalStorage(); +}); + +describe("ProposalSpacePolicyField", () => { + it("renders a button for opening the space policy dialog", async () => { + const { user } = plainRender(); + const button = screen.getByRole("button"); + + await user.click(button); + + screen.getByRole("dialog", { name: "Find Space" }); + }); + + it("renders the button with a text according to given policy", () => { + const { rerender } = plainRender(); + screen.getByRole("button", { name: /deleting/ }); + rerender(); + screen.getByRole("button", { name: /shrinking/ }); + }); + + describe("within the dialog", () => { + it("renders the space policy picker", async () => { + const { dialog } = await openPopup(); + const picker = within(dialog).getByRole("listbox"); + within(picker).getByRole("option", { name: /delete/i }); + within(picker).getByRole("option", { name: /resize/i }); + within(picker).getByRole("option", { name: /available/i }); + within(picker).getByRole("option", { name: /custom/i }); + }); + + describe("when there are no installation devices", () => { + beforeEach(() => { + devices = []; + }); + + it("does not render the policy actions", async () => { + const { dialog } = await openPopup(); + const actionsTree = within(dialog).queryByRole("treegrid", { name: "Actions to find space" }); + expect(actionsTree).toBeNull(); + }); + }); + + describe("when there are installation devices", () => { + it("renders the policy actions", async () => { + const { dialog } = await openPopup(); + within(dialog).getByRole("treegrid", { name: "Actions to find space" }); + }); + }); + + describe.each([ + { id: 'delete', nameRegexp: /delete/i }, + { id: 'resize', nameRegexp: /shrink/i }, + { id: 'keep', nameRegexp: /the space not assigned/i } + ])("when space policy is '$id'", ({ id, nameRegexp }) => { + beforeEach(() => { + policy = id; + }); + + it("only renders '$id' option as selected", async () => { + const { dialog } = await openPopup(); + const picker = within(dialog).getByRole("listbox"); + within(picker).getByRole("option", { name: nameRegexp, selected: true }); + expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); + }); + + it("does not allow to modify the space actions", async () => { + const { dialog } = await openPopup(); + // NOTE: HTML `disabled` attribute removes the element from the a11y tree. + // That's why the test is using `hidden: true` here to look for disabled actions. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled + // https://testing-library.com/docs/queries/byrole/#hidden + // TODO: use a more inclusive way to disable the actions. + // https://css-tricks.com/making-disabled-buttons-more-inclusive/ + const spaceActions = within(dialog).getAllByRole("combobox", { name: /Space action selector/, hidden: true }); + expect(spaceActions.length).toEqual(3); + }); + }); + + describe("when space policy is 'custom'", () => { + beforeEach(() => { + policy = "custom"; + }); + + it("only renders 'custom' option as selected", async () => { + const { dialog } = await openPopup(); + const picker = within(dialog).getByRole("listbox"); + within(picker).getByRole("option", { name: /custom/i, selected: true }); + expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); + }); + + it("allows to modify the space actions", async () => { + const { dialog } = await openPopup(); + const spaceActions = within(dialog).getAllByRole("combobox", { name: /Space action selector/ }); + expect(spaceActions.length).toEqual(3); + }); + }); + + describe("DeviceActionColumn", () => { + it("renders the space actions selector for devices without partition table", async () => { + const { dialog } = await openPopup(); + const sdaRow = within(dialog).getByRole("row", { name: /sda/ }); + const sdaActionsSelector = within(sdaRow).queryByRole("combobox", { name: "Space action selector for /dev/sda" }); + // sda has partition table, the selector shouldn't be found + expect(sdaActionsSelector).toBeNull(); + const sdbRow = screen.getByRole("row", { name: /sdb/ }); + // sdb does not have partition table, selector should be there + within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); + }); + + it("does not renders the 'resize' option for drives", async () => { + const { dialog } = await openPopup(); + const sdbRow = within(dialog).getByRole("row", { name: /sdb/ }); + const spaceActionsSelector = within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); + const resizeOption = within(spaceActionsSelector).queryByRole("option", { name: /resize/ }); + expect(resizeOption).toBeNull(); + }); + + it("renders the 'resize' option for devices other than drives", async () => { + const { user, dialog } = await openPopup(); + const sdaRow = within(dialog).getByRole("row", { name: /sda/ }); + const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); + await user.click(sdaToggler); + const sda1Row = screen.getByRole("row", { name: /sda1/ }); + const spaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); + within(spaceActionsSelector).getByRole("option", { name: /resize/ }); + }); + + it("renders as selected the option matching the given device space action", async () => { + const { user, dialog } = await openPopup(); + const sdaRow = within(dialog).getByRole("row", { name: /sda/ }); + const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); + await user.click(sdaToggler); + const sda1Row = screen.getByRole("row", { name: /sda1/ }); + const sda1SpaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); + within(sda1SpaceActionsSelector).getByRole("option", { name: /delete/i, selected: true }); + const sda2Row = screen.getByRole("row", { name: /sda2/ }); + const sda2SpaceActionsSelector = within(sda2Row).getByRole("combobox", { name: "Space action selector for /dev/sda2" }); + within(sda2SpaceActionsSelector).getByRole("option", { name: /resize/i, selected: true }); + }); + }); + }); + + it("triggers the onChange callback when user accepts the dialog", async () => { + const onChangeFn = jest.fn(); + const { user, dialog } = await openPopup({ onChange: onChangeFn }); + + // Select 'custom' + const picker = within(dialog).getByRole("listbox"); + await user.selectOptions( + picker, + within(picker).getByRole("option", { name: /custom/i }) + ); + + // Select custom actions + const sda1Row = within(dialog).getByRole("row", { name: /sda1/ }); + const sda1Select = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); + await user.selectOptions( + sda1Select, + within(sda1Select).getByRole("option", { name: /delete/i }) + ); + const sda2Row = within(dialog).getByRole("row", { name: /sda2/ }); + const sda2Select = within(sda2Row).getByRole("combobox", { name: "Space action selector for /dev/sda2" }); + await user.selectOptions( + sda2Select, + within(sda2Select).getByRole("option", { name: /resize/i }) + ); + + // Accept the result + const acceptButton = within(dialog).getByRole("button", { name: "Accept" }); + await user.click(acceptButton); + + expect(onChangeFn).toHaveBeenCalledWith( + "custom", + expect.arrayContaining([{ action: "resize", device: "/dev/sda2" }, { action: "force_delete", device: "/dev/sda1" }]) + ); + }); +}); diff --git a/web/src/components/storage/ProposalSpacePolicySection.test.jsx b/web/src/components/storage/ProposalSpacePolicySection.test.jsx deleted file mode 100644 index 07f842ab8b..0000000000 --- a/web/src/components/storage/ProposalSpacePolicySection.test.jsx +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen, within } from "@testing-library/react"; -import { plainRender, resetLocalStorage } from "~/test-utils"; -import { ProposalSpacePolicySection } from "~/components/storage"; - -const sda = { - sid: "59", - isDrive: true, - type: "disk", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, - name: "/dev/sda", - size: 1024, - recoverableSize: 0, - systems : [], - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], -}; - -const sda1 = { - sid: "60", - isDrive: false, - type: "", - active: true, - name: "/dev/sda1", - size: 512, - recoverableSize: 128, - systems : [], - udevIds: [], - udevPaths: [] -}; - -const sda2 = { - sid: "61", - isDrive: false, - type: "", - active: true, - name: "/dev/sda2", - size: 512, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: [] -}; - -sda.partitionTable = { - type: "gpt", - partitions: [sda1, sda2], - unpartitionedSize: 512 -}; - -const sdb = { - sid: "62", - isDrive: true, - type: "disk", - vendor: "Samsung", - model: "Samsung Evo 8 Pro", - driver: ["ahci"], - bus: "IDE", - busId: "", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/sdb", - size: 2048, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: ["pci-0000:00-19"] -}; - -let settings; - -beforeEach(() => { - settings = { - installationDevices: [sda, sdb], - spacePolicy: "keep", - spaceActions: [ - { device: "/dev/sda1", action: "force_delete" }, - { device: "/dev/sda2", action: "resize" } - ], - }; - - resetLocalStorage(); -}); - -describe("ProposalSpacePolicySection", () => { - it("renders the space policy picker", () => { - plainRender(); - const picker = screen.getByRole("listbox"); - within(picker).getByRole("option", { name: /delete/i }); - within(picker).getByRole("option", { name: /resize/i }); - within(picker).getByRole("option", { name: /available/i }); - within(picker).getByRole("option", { name: /custom/i }); - }); - - it("triggers the onChange callback when user changes selected space policy", async () => { - const onChangeFn = jest.fn(); - const { user } = plainRender(); - const picker = screen.getByRole("listbox"); - await user.selectOptions( - picker, - within(picker).getByRole("option", { name: /custom/i }) - ); - expect(onChangeFn).toHaveBeenCalledWith({ spacePolicy: "custom" }); - }); - - describe("when there are no installation devices", () => { - beforeEach(() => { - settings.installationDevices = []; - }); - - it("does not render the policy actions", () => { - plainRender(); - const actions = screen.queryByRole("treegrid", { name: "Actions to find space" }); - expect(actions).toBeNull(); - }); - }); - - describe("when there are installation devices", () => { - it("renders the policy actions", () => { - plainRender(); - screen.getByRole("treegrid", { name: "Actions to find space" }); - }); - }); - - describe.each([ - { id: 'delete', nameRegexp: /delete/i }, - { id: 'resize', nameRegexp: /shrink/i }, - { id: 'keep', nameRegexp: /the space not assigned/i } - ])("when space policy is '$id'", ({ id, nameRegexp }) => { - beforeEach(() => { - settings.spacePolicy = id; - }); - - it("only renders '$id' option as selected", () => { - plainRender(); - const picker = screen.getByRole("listbox"); - within(picker).getByRole("option", { name: nameRegexp, selected: true }); - expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); - }); - - it("does not allow to modify the space actions", () => { - plainRender(); - // NOTE: HTML `disabled` attribute removes the element from the a11y tree. - // That's why the test is using `hidden: true` here to look for disabled actions. - // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled - // https://testing-library.com/docs/queries/byrole/#hidden - // TODO: use a more inclusive way to disable the actions. - // https://css-tricks.com/making-disabled-buttons-more-inclusive/ - const spaceActions = screen.getAllByRole("combobox", { name: /Space action selector/, hidden: true }); - expect(spaceActions.length).toEqual(3); - }); - }); - - describe("when space policy is 'custom'", () => { - beforeEach(() => { - settings.spacePolicy = "custom"; - }); - - it("only renders 'custom' option as selected", () => { - plainRender(); - const picker = screen.getByRole("listbox"); - within(picker).getByRole("option", { name: /custom/i, selected: true }); - expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); - }); - - it("allows to modify the space actions", () => { - plainRender(); - const spaceActions = screen.getAllByRole("combobox", { name: /Space action selector/ }); - expect(spaceActions.length).toEqual(3); - }); - }); - - describe("DeviceActionColumn", () => { - it("renders the space actions selector for devices without partition table", () => { - plainRender(); - const sdaRow = screen.getByRole("row", { name: /sda/ }); - const sdaActionsSelector = within(sdaRow).queryByRole("combobox", { name: "Space action selector for /dev/sda" }); - // sda has partition table, the selector shouldn't be found - expect(sdaActionsSelector).toBeNull(); - const sdbRow = screen.getByRole("row", { name: /sdb/ }); - // sdb does not have partition table, selector should be there - within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); - }); - - it("does not renders the 'resize' option for drives", () => { - plainRender(); - const sdbRow = screen.getByRole("row", { name: /sdb/ }); - const spaceActionsSelector = within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); - const resizeOption = within(spaceActionsSelector).queryByRole("option", { name: /resize/ }); - expect(resizeOption).toBeNull(); - }); - - it("renders the 'resize' option for devices other than drives", async () => { - const { user } = plainRender(); - const sdaRow = screen.getByRole("row", { name: /sda/ }); - const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); - await user.click(sdaToggler); - const sda1Row = screen.getByRole("row", { name: /sda1/ }); - const spaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); - within(spaceActionsSelector).getByRole("option", { name: /resize/ }); - }); - - it("renders as selected the option matching the given device space action", async () => { - const { user } = plainRender(); - const sdaRow = screen.getByRole("row", { name: /sda/ }); - const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); - await user.click(sdaToggler); - const sda1Row = screen.getByRole("row", { name: /sda1/ }); - const sda1SpaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); - within(sda1SpaceActionsSelector).getByRole("option", { name: /delete/i, selected: true }); - const sda2Row = screen.getByRole("row", { name: /sda2/ }); - const sda2SpaceActionsSelector = within(sda2Row).getByRole("combobox", { name: "Space action selector for /dev/sda2" }); - within(sda2SpaceActionsSelector).getByRole("option", { name: /resize/i, selected: true }); - }); - - it("triggers the onChange callback when user changes space action", async () => { - const onChangeFn = jest.fn(); - const { user } = plainRender( - - ); - - const sda1Row = screen.getByRole("row", { name: /sda1/ }); - const selector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); - await user.selectOptions( - selector, - within(selector).getByRole("option", { name: /resize/i, selected: false }) - ); - expect(onChangeFn).toHaveBeenCalledWith( - expect.objectContaining({ - spaceActions: expect.arrayContaining([{ action: "resize", device: "/dev/sda1" }]) - }) - ); - }); - }); -}); diff --git a/web/src/components/storage/ProposalVolumes.jsx b/web/src/components/storage/ProposalVolumes.jsx index c7254ed6b5..a3e2f873ae 100644 --- a/web/src/components/storage/ProposalVolumes.jsx +++ b/web/src/components/storage/ProposalVolumes.jsx @@ -408,6 +408,9 @@ export default function ProposalVolumes({ <> + + {_("File systems to create in your system")} + Date: Thu, 14 Mar 2024 12:31:58 +0000 Subject: [PATCH 31/98] web: Select action according to the policy --- .../storage/ProposalSpacePolicyField.jsx | 48 +++++++++-- .../storage/ProposalSpacePolicyField.test.jsx | 82 ++++++++++++++++--- 2 files changed, 110 insertions(+), 20 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicyField.jsx b/web/src/components/storage/ProposalSpacePolicyField.jsx index d9fc0479ab..a03931f327 100644 --- a/web/src/components/storage/ProposalSpacePolicyField.jsx +++ b/web/src/components/storage/ProposalSpacePolicyField.jsx @@ -287,6 +287,18 @@ const DeviceRow = ({ onCollapse = noop, onChange = noop }) => { + // Generates the action value according to the policy. + const action = () => { + if (policy.id === "custom") + return actions.find(a => a.device === device.name)?.action || "keep"; + + const policyAction = { delete: "force_delete", resize: "resize", keep: "keep" }; + return policyAction[policy.id]; + }; + + const isDisabled = policy.id !== "custom"; + const showAction = !device.partitionTable; + const treeRow = { onCollapse, rowIndex, @@ -300,10 +312,6 @@ const DeviceRow = ({ } }; - const spaceAction = actions.find(a => a.device === device.name); - const isDisabled = policy.id !== "custom"; - const showAction = !device.partitionTable; - return ( {/* eslint-disable agama-i18n/string-literals */} @@ -319,7 +327,7 @@ const DeviceRow = ({ then={ @@ -344,11 +352,11 @@ const SpaceActionsTable = ({ policy, actions, devices, onChange = noop }) => { const [autoExpanded, setAutoExpanded] = useLocalStorage("storage-space-actions-auto-expanded", false); useEffect(() => { - const dev_names = devices.map(d => d.name); - let currentExpanded = dev_names.filter(d => expandedDevices.includes(d)); + const devNames = devices.map(d => d.name); + let currentExpanded = devNames.filter(d => expandedDevices.includes(d)); if (policy.id === "custom" && !autoExpanded) { - currentExpanded = [...dev_names]; + currentExpanded = [...devNames]; setAutoExpanded(true); } else if (policy.id !== "custom" && autoExpanded) { setAutoExpanded(false); @@ -470,6 +478,21 @@ const SpacePolicyForm = ({ }) => { const [policy, setPolicy] = useState(currentPolicy); const [actions, setActions] = useState(currentActions); + const [customUsed, setCustomUsed] = useState(false); + + // The selectors for the space action have to be initialized always to the same value + // (e.g., "keep") when the custom policy is selected for first time. The following two useEffect + // ensures that. + + // Stores whether the custom policy has been used. + useEffect(() => { + if (policy.id === "custom" && !customUsed) setCustomUsed(true); + }, [policy, customUsed, setCustomUsed]); + + // Resets actions (i.e., sets everything to "keep") if the custom policy has not been used yet. + useEffect(() => { + if (policy.id !== "custom" && !customUsed) setActions([]); + }, [policy, customUsed, setActions]); const changeActions = (spaceAction) => { const spaceActions = actions.filter(a => a.device !== spaceAction.device); @@ -493,7 +516,14 @@ const SpacePolicyForm = ({ 0} - then={} + then={ + + } /> } diff --git a/web/src/components/storage/ProposalSpacePolicyField.test.jsx b/web/src/components/storage/ProposalSpacePolicyField.test.jsx index 1094feefa6..a8b5bc9bc8 100644 --- a/web/src/components/storage/ProposalSpacePolicyField.test.jsx +++ b/web/src/components/storage/ProposalSpacePolicyField.test.jsx @@ -112,6 +112,20 @@ const openPopup = async (props = {}) => { return { user, dialog }; }; +const expandRow = async (user, dialog, name) => { + const row = within(dialog).getByRole("row", { name }); + const toggler = within(row).getByRole("button", { name: /expand/i }); + await user.click(toggler); +}; + +const checkSpaceActions = async (deviceActions) => { + deviceActions.forEach(({ name, action }) => { + const row = screen.getByRole("row", { name }); + const selector = within(row).getByRole("combobox", { name }); + within(selector).getByRole("option", { name: action, selected: true }); + }); +}; + beforeEach(() => { devices = [sda, sdb]; policy = "keep"; @@ -247,17 +261,63 @@ describe("ProposalSpacePolicyField", () => { within(spaceActionsSelector).getByRole("option", { name: /resize/ }); }); - it("renders as selected the option matching the given device space action", async () => { - const { user, dialog } = await openPopup(); - const sdaRow = within(dialog).getByRole("row", { name: /sda/ }); - const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); - await user.click(sdaToggler); - const sda1Row = screen.getByRole("row", { name: /sda1/ }); - const sda1SpaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); - within(sda1SpaceActionsSelector).getByRole("option", { name: /delete/i, selected: true }); - const sda2Row = screen.getByRole("row", { name: /sda2/ }); - const sda2SpaceActionsSelector = within(sda2Row).getByRole("combobox", { name: "Space action selector for /dev/sda2" }); - within(sda2SpaceActionsSelector).getByRole("option", { name: /resize/i, selected: true }); + describe("when space policy is 'delete'", () => { + beforeEach(() => { + policy = "delete"; + }); + + it("renders as selected the delete option", async () => { + const { user, dialog } = await openPopup(); + await expandRow(user, dialog, /sda/); + await checkSpaceActions([ + { name: /sda1/, action: /delete/i }, + { name: /sda2/, action: /delete/i } + ]); + }); + }); + + describe("when space policy is 'resize'", () => { + beforeEach(() => { + policy = "resize"; + }); + + it("renders as selected the resize option", async () => { + const { user, dialog } = await openPopup(); + await expandRow(user, dialog, /sda/); + await checkSpaceActions([ + { name: /sda1/, action: /resize/i }, + { name: /sda2/, action: /resize/i } + ]); + }); + }); + + describe("when space policy is 'keep'", () => { + beforeEach(() => { + policy = "keep"; + }); + + it("renders as selected the keep option", async () => { + const { user, dialog } = await openPopup(); + await expandRow(user, dialog, /sda/); + await checkSpaceActions([ + { name: /sda1/, action: /not modify/i }, + { name: /sda2/, action: /not modify/i } + ]); + }); + }); + + describe("when space policy is 'custom'", () => { + beforeEach(() => { + policy = "custom"; + }); + + it("renders as selected the option matching the given device space action", async () => { + await openPopup(); + await checkSpaceActions([ + { name: /sda1/, action: /delete/i }, + { name: /sda2/, action: /resize/i } + ]); + }); }); }); }); From 3af5a298b27d366b53617fb4cbd06968599d35f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2024 13:16:55 +0000 Subject: [PATCH 32/98] web: Do not use local storage - The state about expanded devices can be directly stored in the component. - There are no unmounts when changing the policy or actions, so there is no need of remembering the state of an unmounted component. --- web/src/components/storage/ProposalSpacePolicyField.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicyField.jsx b/web/src/components/storage/ProposalSpacePolicyField.jsx index a03931f327..75bb643b84 100644 --- a/web/src/components/storage/ProposalSpacePolicyField.jsx +++ b/web/src/components/storage/ProposalSpacePolicyField.jsx @@ -25,7 +25,7 @@ import { Button, Form, FormSelect, FormSelectOption, Skeleton } from "@patternfl import { _, N_, n_ } from "~/i18n"; import { deviceSize } from '~/components/storage/utils'; import { If, OptionsPicker, Popup, SectionSkeleton } from "~/components/core"; -import { noop, useLocalStorage } from "~/utils"; +import { noop } from "~/utils"; import { sprintf } from "sprintf-js"; import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/react-table'; @@ -348,8 +348,8 @@ const DeviceRow = ({ * @param {(action: SpaceAction) => void} [props.onChange] */ const SpaceActionsTable = ({ policy, actions, devices, onChange = noop }) => { - const [expandedDevices, setExpandedDevices] = useLocalStorage("storage-space-actions-expanded", []); - const [autoExpanded, setAutoExpanded] = useLocalStorage("storage-space-actions-auto-expanded", false); + const [expandedDevices, setExpandedDevices] = useState([]); + const [autoExpanded, setAutoExpanded] = useState(false); useEffect(() => { const devNames = devices.map(d => d.name); From ca49bc315afe37e80807196c3e479d3bd9b19931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2024 13:36:26 +0000 Subject: [PATCH 33/98] web: Improve table of space actions - Use description from backend. - Simplify columns. --- .../storage/ProposalSpacePolicyField.jsx | 78 ++++--------------- 1 file changed, 14 insertions(+), 64 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicyField.jsx b/web/src/components/storage/ProposalSpacePolicyField.jsx index 75bb643b84..17455afb4b 100644 --- a/web/src/components/storage/ProposalSpacePolicyField.jsx +++ b/web/src/components/storage/ProposalSpacePolicyField.jsx @@ -96,9 +96,9 @@ const SPACE_POLICIES = [ // Names of the columns for the policy actions. const columnNames = { device: N_("Used device"), - content: N_("Current content"), + content: N_("Details"), size: N_("Size"), - details: N_("Details"), + details: N_("Size details"), action: N_("Action") }; @@ -110,63 +110,23 @@ const columnNames = { * @param {StorageDevice} props.device */ const DeviceDescriptionColumn = ({ device }) => { - return ( - <> -
{device.name}
- {`${device.vendor} ${device.model}`}} - /> - - ); + if (device.isDrive || device.type === "lvmVg") return device.name; + + return device.name.split("/").pop(); }; /** - * Column content with information about the current content of the device. + * Column content with details about the device. * @component * * @param {object} props * @param {StorageDevice} props.device */ const DeviceContentColumn = ({ device }) => { - const PartitionTableContent = () => { - return ( -
- {/* TRANSLATORS: %s is replaced by partition table type (e.g., GPT) */} - {sprintf(_("%s partition table"), device.partitionTable.type.toUpperCase())} -
- ); - }; - - const BlockContent = () => { - const renderContent = () => { - const systems = device.systems; - if (systems.length > 0) return systems.join(", "); + const systems = device.systems; + if (systems.length > 0) return systems.join(", "); - const filesystem = device.filesystem; - if (filesystem?.isEFI) return _("EFI system partition"); - if (filesystem) { - // TRANSLATORS: %s is replaced by a file system type (e.g., btrfs). - return sprintf(_("%s file system"), filesystem?.type); - } - - const component = device.component; - switch (component?.type) { - case "physical_volume": - // TRANSLATORS: %s is replaced by a LVM volume group name (e.g., /dev/vg0). - return sprintf(_("LVM physical volume of %s"), component.deviceNames[0]); - case "md_device": - // TRANSLATORS: %s is replaced by a RAID name (e.g., /dev/md0). - return sprintf(_("Member of RAID %s"), component.deviceNames[0]); - default: - return _("Not identified"); - } - }; - - return
{renderContent()}
; - }; - - return (device.partitionTable ? : ); + return device.description; }; /** @@ -177,7 +137,7 @@ const DeviceContentColumn = ({ device }) => { * @param {StorageDevice} props.device */ const DeviceSizeColumn = ({ device }) => { - return
{deviceSize(device.size)}
; + return deviceSize(device.size); }; /** @@ -192,26 +152,16 @@ const DeviceDetailsColumn = ({ device }) => { if (device.filesystem) return null; const unused = device.partitionTable?.unpartitionedSize || 0; - - return ( -
- {/* TRANSLATORS: %s is replaced by a disk size (e.g., 20 GiB) */} - {sprintf(_("%s unused"), deviceSize(unused))} -
- ); + // TRANSLATORS: %s is replaced by a disk size (e.g., 20 GiB) + return sprintf(_("%s unused"), deviceSize(unused)); }; const RecoverableSize = () => { const size = device.recoverableSize; - if (size === 0) return null; - return ( -
- {/* TRANSLATORS: %s is replaced by a disk size (e.g., 2 GiB) */} - {sprintf(_("Shrinkable by %s"), deviceSize(device.recoverableSize))} -
- ); + // TRANSLATORS: %s is replaced by a disk size (e.g., 2 GiB) + return sprintf(_("Shrinkable by %s"), deviceSize(device.recoverableSize)); }; return ( From 036e467524901eb92a6f76eb6499a2195a9cef24 Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Thu, 14 Mar 2024 15:03:05 +0100 Subject: [PATCH 34/98] Update storage_ui.md to reflect recent decisions --- doc/images/storage_ui/agama_guided.png | Bin 160290 -> 130980 bytes doc/images/storage_ui/transactional.png | Bin 0 -> 51525 bytes doc/storage_ui.md | 77 ++++++++++++------------ 3 files changed, 40 insertions(+), 37 deletions(-) create mode 100644 doc/images/storage_ui/transactional.png diff --git a/doc/images/storage_ui/agama_guided.png b/doc/images/storage_ui/agama_guided.png index 9344740c88789ed57a75ae276e3f53cd80f62e6e..f37d111c2d223836fd482162de6f210ab5d8afac 100644 GIT binary patch literal 130980 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
cjr#DQjPrqM);$UDIQo>Oa*O2xtjG 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 J%u@#U{tGF9AM5}C literal 160290 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 Mo#%-^LqY;^QvdWhNjucl%jeJ| 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�sn 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

#ht@mg^%Ybppi6mA}!+f8pK&w^XdeMR0( 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 U?U3_b34g4|O|d*@?y&iP0K+v7+yDRo literal 0 HcmV?d00001 diff --git a/doc/storage_ui.md b/doc/storage_ui.md index 9001757d85..1393cd3271 100644 --- a/doc/storage_ui.md +++ b/doc/storage_ui.md @@ -21,16 +21,12 @@ the selection of the operating system to install, "product" in Agama jargon. This document describes the Agama interface for a traditional (non-transactional) system. See the section "interface changes for transactional systems" for the alternative. -### Representation of the Actions to Perform +### Representation of the Result -Another important point to consider is that currently the list of (libstorage-ng) actions is the -only way we have to represent the result of a given proposal. That representation is far from ideal. -It doesn't offer a convenient high-level view of the final layout or of the really significant -actions (it includes too many intermediate steps by default). - -A complete design for a more convenient representation of the result is out of the scope of this -proposal. Nevertheless, small changes (like grouping the actions based on the operating system -they affect) are somehow suggested in some of the upcoming sections and mock-ups. +The result of the storage setup is represented in the mockups of this document as a section of the +storage page titled "result", which includes a list of (libstorage-ng) actions and a table +representing the final state of the affected devices. That is far from ideal, but a complete design +for a more convenient representation of the result is out of the scope of this proposal. In the long term, we may need to come with a better alternative to show the result. @@ -71,18 +67,18 @@ user. ![Initial storage screen](images/storage_ui/agama_guided.png) Every change to any of the configuration options will result in an immediate re-calculation of the -"planned actions" section which represents the result. Changes in the configuration of encryption, -btrfs snapshots or the target devices can also imply refreshing the description of the file systems. -In a similar way, changes in those volumes or the target device may result in some disk being -included or excluded in the section "find space". +section that presents the result. Changes in the configuration of encryption, btrfs snapshots or the +target devices can also imply refreshing the description of the file systems. In a similar way, +changes in those volumes or the target device may result in a change in the number of disks +mentioned in the sentence about finding space. The table with the file systems actually represents the volumes used as input for the Agama variant of the `GuidedProposal`. Compared to YaST, Agama turns the volumes into a much more visible concept. The users will be able to see and adjust most of their attributes. Users could even define new volumes that are not initially part of the configuration of the selected product. -Pop-up dialogs will be used to modify the target device(s), the encryption configuration or the -booting setup, as well as to add or edit a given volume. +Pop-up dialogs will be used to modify the target device(s), the encryption configuration, the +booting setup or the strategy to find free space, as well as to add or edit a given volume. All file systems will be created by default at the chosen target disk or at the default LVM volume group (in the case of LVM-based proposal). The user will be able to manually overwrite the location @@ -92,9 +88,9 @@ system at the `vdb1` partition. Continue reading to understand all the possible Defining the settings and the list of volumes also defines, as a direct consequence, the disks affected by the installation process. It may be needed to make some space in those disks. That -deserves a dedicated section in the proposal page that is described below. +deserves a dedicated "find space" setting that is described below. -### Device Selection and General Settings +### Device Selection As seen on the image above, the main device to install the system can be chosen at the very top of the storage proposal page. Although a Linux installation can extend over several disks, the storage @@ -109,29 +105,21 @@ which devices will be partitioned in order to allocate the physical volumes of t In that case, the file systems will be created by default as new LVM logical volumes at that new volume group. +### General Settings + The device selection is followed by some global settings that define how the installation is going to look and what are the possibilities in terms of booting and structuring the file systems. Those settings include the usage of btrfs snapshots, which in YaST is presented relatively hidden as one of the configuration options for the root file system. -One of the main features of the `GuidedProposal` is its ability to automatically determine any extra -partition that may be needed for booting the new system, like PReP, EFI, Zipl or any other described -at the [corresponding YaST -document](https://github.com/yast/yast-storage-ng/blob/master/doc/boot-requirements.md). The -algorithm can create those partitions or reuse existing ones that are already in the system if the -user wants to keep them (see the section about finding space). The behavior of that feature can be -also be tweaked in the "settings" section of the page. - -![Dialog to configure booting](images/storage_ui/boot_config_popup.png) - ### File Systems -The next section contains the table that displays the file systems to be created, volumes in YaST -jargon. The size of each volume is specified as a couple of lower and upper limits (the upper one is -optional in all cases). With the current approach of the YaST `GuidedProposal` there are some -volumes that may need to recalculate those limits based on the proposal configuration (eg. whether -Btrfs snapshots are enabled) or its relationship with others volumes. Their limits will be set as -"auto-calculated" by default. For more details, see the corresponding section below. +The "settings" section also contains the table that displays the file systems to be created, volumes +in YaST jargon. The size of each volume is specified as a couple of lower and upper limits (the +upper one is optional in all cases). With the current approach of the YaST `GuidedProposal` there +are some volumes that may need to recalculate those limits based on the proposal configuration (eg. +whether Btrfs snapshots are enabled) or its relationship with others volumes. Their limits will be +set as "auto-calculated" by default. For more details, see the corresponding section below. If btrfs is used for the root file system, it will be possible to define subvolumes for it. Those subvolumes are represented in the same table, nested on the entry of the root file system. They can @@ -155,6 +143,18 @@ specify an an alternative location using the following form that offers several When the option to reuse an existing device is chosen, size limits cannot be adjusted. The size of the reused device will be displayed in the table of file systems in the corresponding column. +### Configuration of Booting Partitions + +One of the main features of the `GuidedProposal` is its ability to automatically determine any extra +partition that may be needed for booting the new system, like PReP, EFI, Zipl or any other described +at the [corresponding YaST +document](https://github.com/yast/yast-storage-ng/blob/master/doc/boot-requirements.md). The +algorithm can create those partitions or reuse existing ones that are already in the system if the +user wants to keep them (see the section about finding space). The behavior of that feature can be +also be tweaked in the "settings" section of the page. + +![Dialog to configure booting](images/storage_ui/boot_config_popup.png) + ### Finding Space for the Volumes Similar to YaST, Agama will offer by default the option to automatically make space for the new @@ -164,7 +164,8 @@ three automatic modes. As an alternative, the Agama proposal will offer a custom mode in which the user will explicitly select which partitions to keep, delete or resize. -That will result in up to four possibilities presented in the corresponding section. +That will result in up to four possibilities presented at a pop-up dialog if the user clicks on the +corresponding option at the botton of the "settings" section. - Delete everything in the disk(s). Obviously, all previous data is removed. - Shrink existing partition(s). The information is kept, but partitions are resized as needed to make @@ -249,9 +250,11 @@ In those systems, it makes no sense to disable Btrfs snapshots, which are requir functionality. Is not only that snapshots are mandatory in transactional systems, they are actually used with a different purpose when compared to read-write systems. -Thus, if the system being installed is transactional, that will be clearly stated at the "settings" -section of the storage proposal page. The setting to use btrfs snapshots will not be there and the -root file system will be labeled as "transactional" in the corresponding table. +Thus, if the system being installed is transactional, that will be clearly stated at the top of the +page. The setting to use btrfs snapshots will not be there and the root file system will be labeled as +"transactional" in the corresponding table. + +![Interface changes for transactional systems](images/storage_ui/transactional.png) ## Advanced Preparations From c2ed4f74c80bbf23e0711d87ea19d02ab72e735e Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 17 Mar 2024 02:44:11 +0000 Subject: [PATCH 35/98] Update web PO files Agama-weblate commit: 90c181f94088757e2c63da42f7198ae398800511 --- web/po/ca.po | 122 +++++++++++++++------------- web/po/cs.po | 111 +++++++++++++------------ web/po/de.po | 111 +++++++++++++------------ web/po/es.po | 111 +++++++++++++------------ web/po/fr.po | 122 +++++++++++++++------------- web/po/id.po | 111 +++++++++++++------------ web/po/ja.po | 122 +++++++++++++++------------- web/po/ka.po | 111 +++++++++++++------------ web/po/mk.po | 111 +++++++++++++------------ web/po/nl.po | 183 ++++++++++++++++++++++-------------------- web/po/pt_BR.po | 111 +++++++++++++------------ web/po/ru.po | 111 +++++++++++++------------ web/po/sv.po | 122 +++++++++++++++------------- web/po/uk.po | 111 +++++++++++++------------ web/po/zh_Hans.po | 111 +++++++++++++------------ web/src/manifest.json | 11 +-- 16 files changed, 978 insertions(+), 814 deletions(-) diff --git a/web/po/ca.po b/web/po/ca.po index 0471061d13..535d9ef0bf 100644 --- a/web/po/ca.po +++ b/web/po/ca.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-10 02:10+0000\n" -"PO-Revision-Date: 2024-03-08 20:42+0000\n" +"POT-Creation-Date: 2024-03-14 02:05+0000\n" +"PO-Revision-Date: 2024-03-12 11:42+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan \n" @@ -159,8 +159,8 @@ msgstr "" #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 #: src/components/storage/ProposalSettingsSection.jsx:263 -#: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:283 +#: src/components/storage/ProposalVolumes.jsx:152 +#: src/components/storage/ProposalVolumes.jsx:287 #: src/components/storage/ZFCPPage.jsx:511 msgid "Accept" msgstr "Accepta-ho" @@ -311,8 +311,8 @@ msgstr "Confirmeu-ho" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 -#: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:310 +#: src/components/storage/ProposalVolumes.jsx:123 +#: src/components/storage/ProposalVolumes.jsx:314 msgid "Actions" msgstr "Accions" @@ -400,8 +400,8 @@ msgstr "" "La llengua usada per l'instal·lador. La llengua del sistema instal·lat es " "pot definir a la pàgina %s." -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -538,8 +538,8 @@ msgstr "Adreces" msgid "Addresses data list" msgstr "Llista de dades d'adreces" -#. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header +#. TRANSLATORS: input field for the iSCSI initiator name #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:360 @@ -558,7 +558,7 @@ msgid "IP addresses" msgstr "Adreces IP" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:234 +#: src/components/storage/ProposalVolumes.jsx:238 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:208 @@ -675,8 +675,8 @@ msgstr "No s'ha trobat cap connexió WiFi." msgid "Connect to a Wi-Fi network" msgstr "Connecteu-vos a una xarxa Wi-Fi" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -761,8 +761,8 @@ msgstr "Connectat" msgid "Disconnecting" msgstr "Desconnectant" -#. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status +#. TRANSLATORS: iSCSI connection status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -826,8 +826,8 @@ msgstr "Lectura de repositoris de programari" msgid "Refresh the repositories" msgstr "Refresca els repositoris" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:143 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -871,8 +871,8 @@ msgstr "Instal·la al dispositiu %s" msgid "Probing storage devices" msgstr "Sondant els dispositius d'emmagatzematge" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:208 #: src/components/storage/ProposalPage.jsx:243 msgid "Storage" @@ -1383,13 +1383,13 @@ msgid "Current content" msgstr "Contingut actual" #: src/components/storage/ProposalSpacePolicySection.jsx:74 -#: src/components/storage/ProposalVolumes.jsx:309 -#: src/components/storage/VolumeForm.jsx:741 +#: src/components/storage/ProposalVolumes.jsx:313 +#: src/components/storage/VolumeForm.jsx:745 msgid "Size" msgstr "Mida" #: src/components/storage/ProposalSpacePolicySection.jsx:75 -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:312 msgid "Details" msgstr "Detalls" @@ -1448,7 +1448,7 @@ msgid "Space action selector for %s" msgstr "Selector d'acció espacial per a %s" #: src/components/storage/ProposalSpacePolicySection.jsx:224 -#: src/components/storage/ProposalVolumes.jsx:229 +#: src/components/storage/ProposalVolumes.jsx:233 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Suprimeix" @@ -1480,20 +1480,19 @@ msgstr "" "dispositius que s'indiquen a continuació. Trieu com fer-ho." #: src/components/storage/ProposalTransactionalInfo.jsx:46 -#, fuzzy msgid "Transactional root file system" -msgstr "Sistema transaccional" +msgstr "Sistema de fitxers d'arrel transaccional" #: src/components/storage/ProposalTransactionalInfo.jsx:49 -#, fuzzy, c-format +#, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " "file system updated via snapshots." msgstr "" -"%s és un sistema immutable amb actualitzacions atòmiques que usa un sistema " -"de fitxers d'arrel Btrfs només de lectura." +"%s és un sistema immutable amb actualitzacions atòmiques. Usa un sistema de " +"fitxers Btrfs només de lectura actualitzat a través d'instantànies." -#. TRANSLATORS: header for a list of items +#. TRANSLATORS: header for a list of items referring to size limits for file systems #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" msgstr "Aquests límits estan afectats pel següent:" @@ -1510,63 +1509,69 @@ msgstr "La configuració de les instantànies" msgid "Presence of other volumes (%s)" msgstr "La presència d'altres volums (%s)" +#. TRANSLATORS: list item, describes a factor that affects the computed size of a +#. file system; eg. adjusting the size of the swap +#: src/components/storage/ProposalVolumes.jsx:71 +msgid "The amount of RAM in the system" +msgstr "La quantitat de RAM del sistema" + #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:129 +#: src/components/storage/ProposalVolumes.jsx:133 msgid "Reset to defaults" msgstr "Restableix els valors predeterminats" #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:137 #: src/components/storage/ProposalVolumes.jsx:141 +#: src/components/storage/ProposalVolumes.jsx:145 msgid "Add file system" msgstr "Afegeix-hi un sistema de fitxers" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/ProposalVolumes.jsx:191 +#: src/components/storage/ProposalVolumes.jsx:195 #, c-format msgid "At least %s" msgstr "Almenys %s" #. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/ProposalVolumes.jsx:197 +#: src/components/storage/ProposalVolumes.jsx:201 msgid "auto" msgstr "automàtica" #. TRANSLATORS: the filesystem uses a logical volume (LVM) -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "logical volume" msgstr "volum lògic" -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "partition" msgstr "partició" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:216 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "encrypted" msgstr "encriptada" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:218 +#: src/components/storage/ProposalVolumes.jsx:222 msgid "with snapshots" msgstr "amb instantànies" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:220 +#: src/components/storage/ProposalVolumes.jsx:224 msgid "transactional" msgstr "transaccional" -#: src/components/storage/ProposalVolumes.jsx:275 +#: src/components/storage/ProposalVolumes.jsx:279 msgid "Edit file system" msgstr "Edita el sistema de fitxers" -#: src/components/storage/ProposalVolumes.jsx:307 -#: src/components/storage/VolumeForm.jsx:726 -#: src/components/storage/VolumeForm.jsx:731 +#: src/components/storage/ProposalVolumes.jsx:311 +#: src/components/storage/VolumeForm.jsx:730 +#: src/components/storage/VolumeForm.jsx:735 msgid "Mount point" msgstr "Punt de muntatge" -#: src/components/storage/ProposalVolumes.jsx:345 +#: src/components/storage/ProposalVolumes.jsx:349 msgid "Table with mount points" msgstr "Taula amb punts de muntatge" @@ -1609,37 +1614,42 @@ msgstr "la presència del sistema de fitxers per a %s" msgid ", " msgstr ", " +#. TRANSLATORS: item which affects the final computed partition size +#: src/components/storage/VolumeForm.jsx:315 +msgid "the amount of RAM in the system" +msgstr "la quantitat de RAM del sistema" + #. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeForm.jsx:314 +#: src/components/storage/VolumeForm.jsx:318 #, c-format msgid "The final size depends on %s." msgstr "La mida final depèn de %s." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeForm.jsx:316 +#: src/components/storage/VolumeForm.jsx:320 msgid " and " msgstr " i " #. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeForm.jsx:321 +#: src/components/storage/VolumeForm.jsx:325 msgid "Automatically calculated size according to the selected product." msgstr "Mida calculada automàticament segons el producte seleccionat." -#: src/components/storage/VolumeForm.jsx:341 +#: src/components/storage/VolumeForm.jsx:345 msgid "Exact size for the file system." msgstr "Mida exacta per al sistema de fitxers." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeForm.jsx:353 +#: src/components/storage/VolumeForm.jsx:357 msgid "Exact size" msgstr "Mida exacta" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeForm.jsx:368 +#: src/components/storage/VolumeForm.jsx:372 msgid "Size unit" msgstr "Unitat de mida" -#: src/components/storage/VolumeForm.jsx:396 +#: src/components/storage/VolumeForm.jsx:400 msgid "" "Limits for the file system size. The final size will be a value between the " "given minimum and maximum. If no maximum is given then the file system will " @@ -1650,57 +1660,57 @@ msgstr "" "de fitxers serà el més gros possible." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeForm.jsx:403 +#: src/components/storage/VolumeForm.jsx:407 msgid "Minimum" msgstr "Mínim" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeForm.jsx:413 +#: src/components/storage/VolumeForm.jsx:417 msgid "Minimum desired size" msgstr "Mida mínima desitjada" -#: src/components/storage/VolumeForm.jsx:422 +#: src/components/storage/VolumeForm.jsx:426 msgid "Unit for the minimum size" msgstr "Unitat per a la mida mínima" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:433 +#: src/components/storage/VolumeForm.jsx:437 msgid "Maximum" msgstr "Màxim" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:444 +#: src/components/storage/VolumeForm.jsx:448 msgid "Maximum desired size" msgstr "Mida màxima desitjada" -#: src/components/storage/VolumeForm.jsx:452 +#: src/components/storage/VolumeForm.jsx:456 msgid "Unit for the maximum size" msgstr "Unitat per a la mida màxima" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeForm.jsx:469 +#: src/components/storage/VolumeForm.jsx:473 msgid "Auto" msgstr "Automàtica" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeForm.jsx:471 +#: src/components/storage/VolumeForm.jsx:475 msgid "Fixed" msgstr "Fixa" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeForm.jsx:473 +#: src/components/storage/VolumeForm.jsx:477 msgid "Range" msgstr "Interval" -#: src/components/storage/VolumeForm.jsx:681 +#: src/components/storage/VolumeForm.jsx:685 msgid "A size value is required" msgstr "Cal un valor de mida" -#: src/components/storage/VolumeForm.jsx:686 +#: src/components/storage/VolumeForm.jsx:690 msgid "Minimum size is required" msgstr "Cal una mida mínima" -#: src/components/storage/VolumeForm.jsx:690 +#: src/components/storage/VolumeForm.jsx:694 msgid "Maximum must be greater than minimum" msgstr "El màxim ha de ser superior al mínim." diff --git a/web/po/cs.po b/web/po/cs.po index 129ecca5b8..3489070db5 100644 --- a/web/po/cs.po +++ b/web/po/cs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"POT-Creation-Date: 2024-03-14 02:05+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: Ladislav Slezák \n" "Language-Team: Czech \n" "Language-Team: German \n" "Language-Team: Spanish \n" "Language-Team: French \n" @@ -158,8 +158,8 @@ msgstr "" #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 #: src/components/storage/ProposalSettingsSection.jsx:263 -#: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:283 +#: src/components/storage/ProposalVolumes.jsx:152 +#: src/components/storage/ProposalVolumes.jsx:287 #: src/components/storage/ZFCPPage.jsx:511 msgid "Accept" msgstr "Accepter" @@ -314,8 +314,8 @@ msgstr "Confirmer" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 -#: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:310 +#: src/components/storage/ProposalVolumes.jsx:123 +#: src/components/storage/ProposalVolumes.jsx:314 msgid "Actions" msgstr "Actions" @@ -405,8 +405,8 @@ msgstr "" "La langue utilisée par l'installateur. La langue du système installé peut " "être définie dans la page %s." -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -543,8 +543,8 @@ msgstr "Adresses" msgid "Addresses data list" msgstr "Liste des données d'adresses" -#. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header +#. TRANSLATORS: input field for the iSCSI initiator name #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:360 @@ -563,7 +563,7 @@ msgid "IP addresses" msgstr "Adresses IP" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:234 +#: src/components/storage/ProposalVolumes.jsx:238 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:208 @@ -680,8 +680,8 @@ msgstr "Aucune connexion WiFi trouvée." msgid "Connect to a Wi-Fi network" msgstr "Se connecter à un réseau Wi-Fi" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -765,8 +765,8 @@ msgstr "Connecté" msgid "Disconnecting" msgstr "Déconnexion" -#. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status +#. TRANSLATORS: iSCSI connection status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -830,8 +830,8 @@ msgstr "Lecture des dépôts de logiciels" msgid "Refresh the repositories" msgstr "Rafraîchir les dépôts" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:143 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -879,8 +879,8 @@ msgstr "Installer en utilisant le périphérique %s" msgid "Probing storage devices" msgstr "Sonde les périphériques de stockage" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:208 #: src/components/storage/ProposalPage.jsx:243 msgid "Storage" @@ -1395,13 +1395,13 @@ msgid "Current content" msgstr "Contenu actuel" #: src/components/storage/ProposalSpacePolicySection.jsx:74 -#: src/components/storage/ProposalVolumes.jsx:309 -#: src/components/storage/VolumeForm.jsx:741 +#: src/components/storage/ProposalVolumes.jsx:313 +#: src/components/storage/VolumeForm.jsx:745 msgid "Size" msgstr "Taille" #: src/components/storage/ProposalSpacePolicySection.jsx:75 -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:312 msgid "Details" msgstr "Détails" @@ -1460,7 +1460,7 @@ msgid "Space action selector for %s" msgstr "Sélecteur d'allocation d'espace pour %s" #: src/components/storage/ProposalSpacePolicySection.jsx:224 -#: src/components/storage/ProposalVolumes.jsx:229 +#: src/components/storage/ProposalVolumes.jsx:233 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Supprimer" @@ -1492,20 +1492,19 @@ msgstr "" "libre dans les périphériques listés ci-dessous. Choisissez comment procéder." #: src/components/storage/ProposalTransactionalInfo.jsx:46 -#, fuzzy msgid "Transactional root file system" -msgstr "Système transactionnel" +msgstr "Système de fichiers root transactionnel" #: src/components/storage/ProposalTransactionalInfo.jsx:49 -#, fuzzy, c-format +#, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " "file system updated via snapshots." msgstr "" -"%s est un système immuable avec des mises à jour atomiques utilisant un " -"système de fichiers racine Btrfs en lecture seule." +"%s est un système immuable avec des mises à jour atomiques. Il utilise un " +"système de fichiers Btrfs en lecture seule mis à jour via des clichés." -#. TRANSLATORS: header for a list of items +#. TRANSLATORS: header for a list of items referring to size limits for file systems #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" msgstr "Ces limites sont affectées par:" @@ -1522,63 +1521,69 @@ msgstr "La configuration des clichés" msgid "Presence of other volumes (%s)" msgstr "Présence d'autres volumes (%s)" +#. TRANSLATORS: list item, describes a factor that affects the computed size of a +#. file system; eg. adjusting the size of the swap +#: src/components/storage/ProposalVolumes.jsx:71 +msgid "The amount of RAM in the system" +msgstr "" + #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:129 +#: src/components/storage/ProposalVolumes.jsx:133 msgid "Reset to defaults" msgstr "Rétablir les valeurs par défaut" #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:137 #: src/components/storage/ProposalVolumes.jsx:141 +#: src/components/storage/ProposalVolumes.jsx:145 msgid "Add file system" msgstr "Ajouter un système de fichiers" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/ProposalVolumes.jsx:191 +#: src/components/storage/ProposalVolumes.jsx:195 #, c-format msgid "At least %s" msgstr "Au moins %s" #. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/ProposalVolumes.jsx:197 +#: src/components/storage/ProposalVolumes.jsx:201 msgid "auto" msgstr "auto" #. TRANSLATORS: the filesystem uses a logical volume (LVM) -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "logical volume" msgstr "volume logique" -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "partition" msgstr "partition" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:216 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "encrypted" msgstr "chiffré" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:218 +#: src/components/storage/ProposalVolumes.jsx:222 msgid "with snapshots" msgstr "avec des clichés" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:220 +#: src/components/storage/ProposalVolumes.jsx:224 msgid "transactional" msgstr "transactionnel" -#: src/components/storage/ProposalVolumes.jsx:275 +#: src/components/storage/ProposalVolumes.jsx:279 msgid "Edit file system" msgstr "Modifier le système de fichiers" -#: src/components/storage/ProposalVolumes.jsx:307 -#: src/components/storage/VolumeForm.jsx:726 -#: src/components/storage/VolumeForm.jsx:731 +#: src/components/storage/ProposalVolumes.jsx:311 +#: src/components/storage/VolumeForm.jsx:730 +#: src/components/storage/VolumeForm.jsx:735 msgid "Mount point" msgstr "Point de montage" -#: src/components/storage/ProposalVolumes.jsx:345 +#: src/components/storage/ProposalVolumes.jsx:349 msgid "Table with mount points" msgstr "Table avec points de montage" @@ -1621,38 +1626,43 @@ msgstr "la présence du système de fichiers pour %s" msgid ", " msgstr ", " +#. TRANSLATORS: item which affects the final computed partition size +#: src/components/storage/VolumeForm.jsx:315 +msgid "the amount of RAM in the system" +msgstr "" + #. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeForm.jsx:314 +#: src/components/storage/VolumeForm.jsx:318 #, c-format msgid "The final size depends on %s." msgstr "La taille définitive dépend de %s." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeForm.jsx:316 +#: src/components/storage/VolumeForm.jsx:320 msgid " and " msgstr " et " #. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeForm.jsx:321 +#: src/components/storage/VolumeForm.jsx:325 msgid "Automatically calculated size according to the selected product." msgstr "" "La taille est automatiquement calculée en fonction du produit sélectionné." -#: src/components/storage/VolumeForm.jsx:341 +#: src/components/storage/VolumeForm.jsx:345 msgid "Exact size for the file system." msgstr "Taille exacte du système de fichiers." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeForm.jsx:353 +#: src/components/storage/VolumeForm.jsx:357 msgid "Exact size" msgstr "Taille exacte" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeForm.jsx:368 +#: src/components/storage/VolumeForm.jsx:372 msgid "Size unit" msgstr "Unité de mesure" -#: src/components/storage/VolumeForm.jsx:396 +#: src/components/storage/VolumeForm.jsx:400 msgid "" "Limits for the file system size. The final size will be a value between the " "given minimum and maximum. If no maximum is given then the file system will " @@ -1663,57 +1673,57 @@ msgstr "" "n'est indiqué, le système de fichiers sera aussi grand que possible." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeForm.jsx:403 +#: src/components/storage/VolumeForm.jsx:407 msgid "Minimum" msgstr "Minimum" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeForm.jsx:413 +#: src/components/storage/VolumeForm.jsx:417 msgid "Minimum desired size" msgstr "Taille minimale souhaitée" -#: src/components/storage/VolumeForm.jsx:422 +#: src/components/storage/VolumeForm.jsx:426 msgid "Unit for the minimum size" msgstr "Unité de la taille minimale" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:433 +#: src/components/storage/VolumeForm.jsx:437 msgid "Maximum" msgstr "Maximum" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:444 +#: src/components/storage/VolumeForm.jsx:448 msgid "Maximum desired size" msgstr "Taille maximale désirée" -#: src/components/storage/VolumeForm.jsx:452 +#: src/components/storage/VolumeForm.jsx:456 msgid "Unit for the maximum size" msgstr "Unité de la taille maximale" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeForm.jsx:469 +#: src/components/storage/VolumeForm.jsx:473 msgid "Auto" msgstr "Auto" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeForm.jsx:471 +#: src/components/storage/VolumeForm.jsx:475 msgid "Fixed" msgstr "Fixe" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeForm.jsx:473 +#: src/components/storage/VolumeForm.jsx:477 msgid "Range" msgstr "Portée" -#: src/components/storage/VolumeForm.jsx:681 +#: src/components/storage/VolumeForm.jsx:685 msgid "A size value is required" msgstr "Une valeur de taille est requise" -#: src/components/storage/VolumeForm.jsx:686 +#: src/components/storage/VolumeForm.jsx:690 msgid "Minimum size is required" msgstr "Une taille minimale est requise" -#: src/components/storage/VolumeForm.jsx:690 +#: src/components/storage/VolumeForm.jsx:694 msgid "Maximum must be greater than minimum" msgstr "Le maximum doit être supérieur au minimum" diff --git a/web/po/id.po b/web/po/id.po index 8832d6662c..782c90d14d 100644 --- a/web/po/id.po +++ b/web/po/id.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"POT-Creation-Date: 2024-03-14 02:05+0000\n" "PO-Revision-Date: 2024-02-21 04:42+0000\n" "Last-Translator: Arif Budiman \n" "Language-Team: Indonesian \n" "Language-Team: Japanese \n" @@ -157,8 +157,8 @@ msgstr "" #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 #: src/components/storage/ProposalSettingsSection.jsx:263 -#: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:283 +#: src/components/storage/ProposalVolumes.jsx:152 +#: src/components/storage/ProposalVolumes.jsx:287 #: src/components/storage/ZFCPPage.jsx:511 msgid "Accept" msgstr "受け入れる" @@ -308,8 +308,8 @@ msgstr "確認" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 -#: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:310 +#: src/components/storage/ProposalVolumes.jsx:123 +#: src/components/storage/ProposalVolumes.jsx:314 msgid "Actions" msgstr "処理" @@ -396,8 +396,8 @@ msgstr "" "インストーラで使用する言語です。インストール先のシステムで使用する言語は %s " "ページで設定します。" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -534,8 +534,8 @@ msgstr "アドレス" msgid "Addresses data list" msgstr "アドレスデータの一覧" -#. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header +#. TRANSLATORS: input field for the iSCSI initiator name #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:360 @@ -554,7 +554,7 @@ msgid "IP addresses" msgstr "IP アドレス" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:234 +#: src/components/storage/ProposalVolumes.jsx:238 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:208 @@ -672,8 +672,8 @@ msgstr "WiFi 接続が見つかりませんでした。" msgid "Connect to a Wi-Fi network" msgstr "Wi-Fi ネットワークへの接続" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -757,8 +757,8 @@ msgstr "接続済み" msgid "Disconnecting" msgstr "切断中" -#. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status +#. TRANSLATORS: iSCSI connection status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -821,8 +821,8 @@ msgstr "ソフトウエアリポジトリを読み込んでいます" msgid "Refresh the repositories" msgstr "リポジトリの更新" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:143 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -866,8 +866,8 @@ msgstr "デバイス %s を利用してインストールします" msgid "Probing storage devices" msgstr "ストレージデバイスを検出しています" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:208 #: src/components/storage/ProposalPage.jsx:243 msgid "Storage" @@ -1376,13 +1376,13 @@ msgid "Current content" msgstr "現在の内容" #: src/components/storage/ProposalSpacePolicySection.jsx:74 -#: src/components/storage/ProposalVolumes.jsx:309 -#: src/components/storage/VolumeForm.jsx:741 +#: src/components/storage/ProposalVolumes.jsx:313 +#: src/components/storage/VolumeForm.jsx:745 msgid "Size" msgstr "サイズ" #: src/components/storage/ProposalSpacePolicySection.jsx:75 -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:312 msgid "Details" msgstr "詳細" @@ -1441,7 +1441,7 @@ msgid "Space action selector for %s" msgstr "%s に対する領域処理の選択" #: src/components/storage/ProposalSpacePolicySection.jsx:224 -#: src/components/storage/ProposalVolumes.jsx:229 +#: src/components/storage/ProposalVolumes.jsx:233 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "削除" @@ -1473,20 +1473,19 @@ msgstr "" "あるかもしれません。ここではその方法を選択します。" #: src/components/storage/ProposalTransactionalInfo.jsx:46 -#, fuzzy msgid "Transactional root file system" -msgstr "トランザクション型システム" +msgstr "トランザクション型のルートファイルシステム" #: src/components/storage/ProposalTransactionalInfo.jsx:49 -#, fuzzy, c-format +#, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " "file system updated via snapshots." msgstr "" -"%s は、読み込み専用の btrfs ルートファイルシステムを利用して一括更新のできる" -"不可変なシステムです。" +"%s は一括更新のできる不可変なシステムです。読み込み専用の btrfs ルートファイ" +"ルシステムを利用して更新を適用します。" -#. TRANSLATORS: header for a list of items +#. TRANSLATORS: header for a list of items referring to size limits for file systems #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" msgstr "これらの制限は下記による影響を受けます:" @@ -1503,63 +1502,69 @@ msgstr "スナップショットの設定" msgid "Presence of other volumes (%s)" msgstr "その他のボリューム (%s) の存在" +#. TRANSLATORS: list item, describes a factor that affects the computed size of a +#. file system; eg. adjusting the size of the swap +#: src/components/storage/ProposalVolumes.jsx:71 +msgid "The amount of RAM in the system" +msgstr "システムに搭載された RAM の量" + #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:129 +#: src/components/storage/ProposalVolumes.jsx:133 msgid "Reset to defaults" msgstr "既定値に戻す" #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:137 #: src/components/storage/ProposalVolumes.jsx:141 +#: src/components/storage/ProposalVolumes.jsx:145 msgid "Add file system" msgstr "ファイルシステムの追加" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/ProposalVolumes.jsx:191 +#: src/components/storage/ProposalVolumes.jsx:195 #, c-format msgid "At least %s" msgstr "少なくとも %s" #. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/ProposalVolumes.jsx:197 +#: src/components/storage/ProposalVolumes.jsx:201 msgid "auto" msgstr "自動" #. TRANSLATORS: the filesystem uses a logical volume (LVM) -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "logical volume" msgstr "論理ボリューム" -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "partition" msgstr "パーティション" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:216 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "encrypted" msgstr "暗号化" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:218 +#: src/components/storage/ProposalVolumes.jsx:222 msgid "with snapshots" msgstr "スナップショット有り" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:220 +#: src/components/storage/ProposalVolumes.jsx:224 msgid "transactional" msgstr "トランザクション型" -#: src/components/storage/ProposalVolumes.jsx:275 +#: src/components/storage/ProposalVolumes.jsx:279 msgid "Edit file system" msgstr "ファイルシステムの編集" -#: src/components/storage/ProposalVolumes.jsx:307 -#: src/components/storage/VolumeForm.jsx:726 -#: src/components/storage/VolumeForm.jsx:731 +#: src/components/storage/ProposalVolumes.jsx:311 +#: src/components/storage/VolumeForm.jsx:730 +#: src/components/storage/VolumeForm.jsx:735 msgid "Mount point" msgstr "マウントポイント" -#: src/components/storage/ProposalVolumes.jsx:345 +#: src/components/storage/ProposalVolumes.jsx:349 msgid "Table with mount points" msgstr "マウントポイントの一覧" @@ -1601,37 +1606,42 @@ msgstr "%s に対するファイルシステムの存在" msgid ", " msgstr ", " +#. TRANSLATORS: item which affects the final computed partition size +#: src/components/storage/VolumeForm.jsx:315 +msgid "the amount of RAM in the system" +msgstr "システムに搭載された RAM の量" + #. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeForm.jsx:314 +#: src/components/storage/VolumeForm.jsx:318 #, c-format msgid "The final size depends on %s." msgstr "最終的なサイズは %s に依存します。" #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeForm.jsx:316 +#: src/components/storage/VolumeForm.jsx:320 msgid " and " msgstr " および " #. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeForm.jsx:321 +#: src/components/storage/VolumeForm.jsx:325 msgid "Automatically calculated size according to the selected product." msgstr "選択した製品に合わせてサイズを自動計算します。" -#: src/components/storage/VolumeForm.jsx:341 +#: src/components/storage/VolumeForm.jsx:345 msgid "Exact size for the file system." msgstr "ファイルシステムに対して正確なサイズを設定します。" #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeForm.jsx:353 +#: src/components/storage/VolumeForm.jsx:357 msgid "Exact size" msgstr "正確なサイズ" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeForm.jsx:368 +#: src/components/storage/VolumeForm.jsx:372 msgid "Size unit" msgstr "サイズの単位" -#: src/components/storage/VolumeForm.jsx:396 +#: src/components/storage/VolumeForm.jsx:400 msgid "" "Limits for the file system size. The final size will be a value between the " "given minimum and maximum. If no maximum is given then the file system will " @@ -1642,57 +1652,57 @@ msgstr "" "きくなるように設定されます。" #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeForm.jsx:403 +#: src/components/storage/VolumeForm.jsx:407 msgid "Minimum" msgstr "最小" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeForm.jsx:413 +#: src/components/storage/VolumeForm.jsx:417 msgid "Minimum desired size" msgstr "最小必須サイズ" -#: src/components/storage/VolumeForm.jsx:422 +#: src/components/storage/VolumeForm.jsx:426 msgid "Unit for the minimum size" msgstr "最小サイズの単位" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:433 +#: src/components/storage/VolumeForm.jsx:437 msgid "Maximum" msgstr "最大" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:444 +#: src/components/storage/VolumeForm.jsx:448 msgid "Maximum desired size" msgstr "最大要求サイズ" -#: src/components/storage/VolumeForm.jsx:452 +#: src/components/storage/VolumeForm.jsx:456 msgid "Unit for the maximum size" msgstr "最大サイズの単位" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeForm.jsx:469 +#: src/components/storage/VolumeForm.jsx:473 msgid "Auto" msgstr "自動" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeForm.jsx:471 +#: src/components/storage/VolumeForm.jsx:475 msgid "Fixed" msgstr "固定値" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeForm.jsx:473 +#: src/components/storage/VolumeForm.jsx:477 msgid "Range" msgstr "範囲" -#: src/components/storage/VolumeForm.jsx:681 +#: src/components/storage/VolumeForm.jsx:685 msgid "A size value is required" msgstr "サイズを指定する必要があります" -#: src/components/storage/VolumeForm.jsx:686 +#: src/components/storage/VolumeForm.jsx:690 msgid "Minimum size is required" msgstr "最小サイズを指定する必要があります" -#: src/components/storage/VolumeForm.jsx:690 +#: src/components/storage/VolumeForm.jsx:694 msgid "Maximum must be greater than minimum" msgstr "最大サイズは最小サイズより大きくなければなりません" diff --git a/web/po/ka.po b/web/po/ka.po index 1fe5186001..c494aebe72 100644 --- a/web/po/ka.po +++ b/web/po/ka.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"POT-Creation-Date: 2024-03-14 02:05+0000\n" "PO-Revision-Date: 2024-02-17 05:42+0000\n" "Last-Translator: Temuri Doghonadze \n" "Language-Team: Georgian \n" "Language-Team: Macedonian \n" +"POT-Creation-Date: 2024-03-14 02:05+0000\n" +"PO-Revision-Date: 2024-03-13 09:42+0000\n" +"Last-Translator: Natasha Ament \n" "Language-Team: Dutch \n" "Language: nl\n" @@ -23,18 +23,17 @@ msgstr "" #: src/DevServerWrapper.jsx:82 #, c-format msgid "The server at %s is not reachable." -msgstr "" +msgstr "De server op %s is niet bereikbaar." #. TRANSLATORS: error message #: src/DevServerWrapper.jsx:88 -#, fuzzy msgid "Cannot connect to the Cockpit server" -msgstr "Kan niet verbinden met D-Bus" +msgstr "Kan niet verbinden met de Cockpit server" #. TRANSLATORS: button label #: src/DevServerWrapper.jsx:104 msgid "Try Again" -msgstr "" +msgstr "Probeer opnieuw" #: src/components/core/About.jsx:43 src/components/core/About.jsx:48 msgid "About Agama" @@ -93,7 +92,7 @@ msgstr "Herladen" #: src/components/core/DevelopmentInfo.jsx:55 msgid "Cockpit server" -msgstr "" +msgstr "Cockpit server" #: src/components/core/FileViewer.jsx:65 msgid "Reading file..." @@ -104,13 +103,12 @@ msgid "Cannot read the file" msgstr "Kan het bestand niet lezen" #: src/components/core/InstallButton.jsx:37 -#, fuzzy msgid "" "There are some reported issues. Please review them in the previous steps " "before proceeding with the installation." msgstr "" -"Er zijn enkele geraporteerde issues. Check alstublieft [de lijst met issues] " -"voor het doorgaan met de installatie." +"Er zijn enkele geraporteerde issues. Check ze alstublieft in de voorgaande " +"stappen voordat u doorgaat met de installatie." #: src/components/core/InstallButton.jsx:49 msgid "Confirm Installation" @@ -121,6 +119,8 @@ msgid "" "If you continue, partitions on your hard disk will be modified according to " "the provided installation settings." msgstr "" +"Indien u doorgaat zullen de partities op de harde schijf worden gewijzigd op " +"basis van de opgegeven installatie instellingen." #: src/components/core/InstallButton.jsx:59 msgid "Please, cancel and check the settings if you are unsure." @@ -147,6 +147,8 @@ msgid "" "Some problems were found when trying to start the installation. Please, have " "a look to the reported errors and try again." msgstr "" +"Enkele problemen zijn geconstateerd tijdens het starten van de instalatie. " +"Check alstublieft de gemelde fouten en probeer opnieuw." #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:89 src/components/l10n/L10nPage.jsx:75 @@ -157,8 +159,8 @@ msgstr "" #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 #: src/components/storage/ProposalSettingsSection.jsx:263 -#: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:283 +#: src/components/storage/ProposalVolumes.jsx:152 +#: src/components/storage/ProposalVolumes.jsx:287 #: src/components/storage/ZFCPPage.jsx:511 msgid "Accept" msgstr "Accepteren" @@ -170,22 +172,23 @@ msgstr "Installeren" #: src/components/core/InstallationFinished.jsx:41 msgid "TPM sealing requires the new system to be booted directly." -msgstr "" +msgstr "TPM verzegeling vereist dat het nieuwe systeem direct wordt gestart." #: src/components/core/InstallationFinished.jsx:46 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." msgstr "" +"Wanneer u gebruik maakt van lokale media om de installer te starten, " +"verwijder deze voor de volgende herstart." #: src/components/core/InstallationFinished.jsx:50 -#, fuzzy msgid "Hide details" -msgstr "Details" +msgstr "Verberg details" #: src/components/core/InstallationFinished.jsx:50 msgid "See more details" -msgstr "" +msgstr "Meer details" #. TRANSLATORS: Do not translate 'abbr' and 'title', they are part of the HTML markup #: src/components/core/InstallationFinished.jsx:55 @@ -195,6 +198,11 @@ msgid "" "first boot of the new system. For that to work, the machine needs to boot " "directly to the new boot loader." msgstr "" +"De laatste stap om TPM te " +"configureren zodat versleutelde apparaten automatisch worden geopend zal " +"plaatsvinden bij de eerste opstart van het nieuwe systeem. Om dat te laten " +"werken is het nodig om de machine direct te laten starten naar de nieuwe " +"boot loader." #. TRANSLATORS: page title #: src/components/core/InstallationFinished.jsx:88 @@ -234,7 +242,7 @@ msgstr "Installeren" #: src/components/software/PatternSelector.jsx:215 #: src/components/software/PatternSelector.jsx:216 msgid "Search" -msgstr "" +msgstr "Zoeken" #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." @@ -266,9 +274,8 @@ msgid "Show global options" msgstr "Toon algemene opties" #: src/components/core/Page.jsx:215 -#, fuzzy msgid "Page Actions" -msgstr "Geplande acties" +msgstr "Pagina acties" #: src/components/core/PasswordAndConfirmationInput.jsx:35 msgid "Passwords do not match" @@ -296,7 +303,7 @@ msgstr "Gebruikers wachtwoord bevestiging" #: src/components/core/PasswordInput.jsx:59 msgid "Password visibility button" -msgstr "" +msgstr "Wachtwoord zichtbaarheidsknop" #: src/components/core/Popup.jsx:90 msgid "Confirm" @@ -304,8 +311,8 @@ msgstr "Bevestigen" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 -#: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:310 +#: src/components/storage/ProposalVolumes.jsx:123 +#: src/components/storage/ProposalVolumes.jsx:314 msgid "Actions" msgstr "Acties" @@ -330,14 +337,12 @@ msgstr "Open terminal" #. TRANSLATORS: sidebar header #: src/components/core/Sidebar.jsx:113 src/components/core/Sidebar.jsx:121 -#, fuzzy msgid "Installer Options" -msgstr "Installeren" +msgstr "Installatie opties" #: src/components/core/Sidebar.jsx:128 -#, fuzzy msgid "Hide installer options" -msgstr "Installeren" +msgstr "Verberg installer opties" #: src/components/core/Sidebar.jsx:136 msgid "Diagnostic tools" @@ -345,24 +350,20 @@ msgstr "Diagnostische hulpmiddelen" #. TRANSLATORS: Titles used for the popup displaying found section issues #: src/components/core/ValidationErrors.jsx:53 -#, fuzzy msgid "Software issues" -msgstr "Software %s" +msgstr "Software problemen" #: src/components/core/ValidationErrors.jsx:54 -#, fuzzy msgid "Product issues" -msgstr "Product selectie" +msgstr "Problemen van het product" #: src/components/core/ValidationErrors.jsx:55 -#, fuzzy msgid "Storage issues" -msgstr "Toon issues" +msgstr "Opslag problemen" #: src/components/core/ValidationErrors.jsx:57 -#, fuzzy msgid "Found Issues" -msgstr "Toon issues" +msgstr "Gevonden problemen" #. TRANSLATORS: %d is replaced with the number of errors found #: src/components/core/ValidationErrors.jsx:77 @@ -376,18 +377,18 @@ msgstr[1] "%d fouten gevonden" #: src/components/l10n/InstallerKeymapSwitcher.jsx:53 #: src/components/l10n/L10nPage.jsx:357 msgid "Keyboard" -msgstr "" +msgstr "Toetsenbord" #. TRANSLATORS: label for keyboard layout selection #: src/components/l10n/InstallerKeymapSwitcher.jsx:61 -#, fuzzy msgid "keyboard" -msgstr "Selecteer" +msgstr "toetsenbord" #. TRANSLATORS: #: src/components/l10n/InstallerKeymapSwitcher.jsx:70 msgid "Keyboard layout cannot be changed in remote installation" msgstr "" +"Toetsenbordindeling kan niet worden gewijzigd in een installatie op afstand" #. TRANSLATORS: help text for the language selector in the sidebar, #. %s will be replaced by the "Localization" page link @@ -397,9 +398,11 @@ msgid "" "The language used by the installer. The language for the installed system " "can be set in the %s page." msgstr "" +"De taal gebruikt door de installer. De taal voor het geïnstalleerde systeem " +"kan worden ingesteld in de %s pagina." -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -408,9 +411,8 @@ msgstr "Lokalisatie" #: src/components/l10n/InstallerLocaleSwitcher.jsx:66 #: src/components/l10n/L10nPage.jsx:246 -#, fuzzy msgid "Language" -msgstr "taal" +msgstr "Taal" #: src/components/l10n/InstallerLocaleSwitcher.jsx:74 msgid "language" @@ -547,8 +549,8 @@ msgstr "Adressen" msgid "Addresses data list" msgstr "Adressen data lijst" -#. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header +#. TRANSLATORS: input field for the iSCSI initiator name #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:360 @@ -567,7 +569,7 @@ msgid "IP addresses" msgstr "IP adressen" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:234 +#: src/components/storage/ProposalVolumes.jsx:238 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:208 @@ -687,8 +689,8 @@ msgstr "Geen WiFi verbindingen gevonden" msgid "Connect to a Wi-Fi network" msgstr "Verbind met een WiFi netwerk" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -772,8 +774,8 @@ msgstr "Verbonden" msgid "Disconnecting" msgstr "Verbinding verbreken" -#. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status +#. TRANSLATORS: iSCSI connection status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -838,8 +840,8 @@ msgstr "Lezen van software opslagruimtes" msgid "Refresh the repositories" msgstr "Ververs de opslagruimtes" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:143 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -882,8 +884,8 @@ msgstr "Installatie apparaat" msgid "Probing storage devices" msgstr "Onderzoeken van opslagapparaten" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:208 #: src/components/storage/ProposalPage.jsx:243 msgid "Storage" @@ -1395,13 +1397,13 @@ msgid "Current content" msgstr "" #: src/components/storage/ProposalSpacePolicySection.jsx:74 -#: src/components/storage/ProposalVolumes.jsx:309 -#: src/components/storage/VolumeForm.jsx:741 +#: src/components/storage/ProposalVolumes.jsx:313 +#: src/components/storage/VolumeForm.jsx:745 msgid "Size" msgstr "Grootte" #: src/components/storage/ProposalSpacePolicySection.jsx:75 -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:312 msgid "Details" msgstr "Details" @@ -1462,7 +1464,7 @@ msgid "Space action selector for %s" msgstr "" #: src/components/storage/ProposalSpacePolicySection.jsx:224 -#: src/components/storage/ProposalVolumes.jsx:229 +#: src/components/storage/ProposalVolumes.jsx:233 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Verwijderen" @@ -1504,7 +1506,7 @@ msgid "" "file system updated via snapshots." msgstr "" -#. TRANSLATORS: header for a list of items +#. TRANSLATORS: header for a list of items referring to size limits for file systems #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" msgstr "Deze limieten worden beïnvloed door:" @@ -1521,64 +1523,70 @@ msgstr "De configuratie van snapshots" msgid "Presence of other volumes (%s)" msgstr "Aanwezigheid van andere volumes (%s)" +#. TRANSLATORS: list item, describes a factor that affects the computed size of a +#. file system; eg. adjusting the size of the swap +#: src/components/storage/ProposalVolumes.jsx:71 +msgid "The amount of RAM in the system" +msgstr "" + #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:129 +#: src/components/storage/ProposalVolumes.jsx:133 msgid "Reset to defaults" msgstr "Reset naar standaard" #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:137 #: src/components/storage/ProposalVolumes.jsx:141 +#: src/components/storage/ProposalVolumes.jsx:145 msgid "Add file system" msgstr "Voeg bestandssysteem toe" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/ProposalVolumes.jsx:191 +#: src/components/storage/ProposalVolumes.jsx:195 #, c-format msgid "At least %s" msgstr "Tenminste %s" #. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/ProposalVolumes.jsx:197 +#: src/components/storage/ProposalVolumes.jsx:201 msgid "auto" msgstr "auto" #. TRANSLATORS: the filesystem uses a logical volume (LVM) -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "logical volume" msgstr "logisch volume" -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "partition" msgstr "partitie" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:216 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "encrypted" msgstr "versleuteld" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:218 +#: src/components/storage/ProposalVolumes.jsx:222 msgid "with snapshots" msgstr "met snapshots" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:220 +#: src/components/storage/ProposalVolumes.jsx:224 #, fuzzy msgid "transactional" msgstr "Voer een actie uit" -#: src/components/storage/ProposalVolumes.jsx:275 +#: src/components/storage/ProposalVolumes.jsx:279 msgid "Edit file system" msgstr "Wijzig bestandssysteem" -#: src/components/storage/ProposalVolumes.jsx:307 -#: src/components/storage/VolumeForm.jsx:726 -#: src/components/storage/VolumeForm.jsx:731 +#: src/components/storage/ProposalVolumes.jsx:311 +#: src/components/storage/VolumeForm.jsx:730 +#: src/components/storage/VolumeForm.jsx:735 msgid "Mount point" msgstr "Koppelpunt" -#: src/components/storage/ProposalVolumes.jsx:345 +#: src/components/storage/ProposalVolumes.jsx:349 msgid "Table with mount points" msgstr "Tabel met koppelpunten" @@ -1621,37 +1629,42 @@ msgstr "de aanwezigheid van het bestandssysteem voor %s" msgid ", " msgstr ", " +#. TRANSLATORS: item which affects the final computed partition size +#: src/components/storage/VolumeForm.jsx:315 +msgid "the amount of RAM in the system" +msgstr "" + #. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeForm.jsx:314 +#: src/components/storage/VolumeForm.jsx:318 #, c-format msgid "The final size depends on %s." msgstr "De uiteindelijke grootte hangt af van %s." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeForm.jsx:316 +#: src/components/storage/VolumeForm.jsx:320 msgid " and " msgstr " en " #. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeForm.jsx:321 +#: src/components/storage/VolumeForm.jsx:325 msgid "Automatically calculated size according to the selected product." msgstr "Automatisch berekend aan de hand van het geselecteerde product." -#: src/components/storage/VolumeForm.jsx:341 +#: src/components/storage/VolumeForm.jsx:345 msgid "Exact size for the file system." msgstr "Exacte grootte voor het bestandssysteem." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeForm.jsx:353 +#: src/components/storage/VolumeForm.jsx:357 msgid "Exact size" msgstr "Exacte grootte" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeForm.jsx:368 +#: src/components/storage/VolumeForm.jsx:372 msgid "Size unit" msgstr "Grootte eenheid" -#: src/components/storage/VolumeForm.jsx:396 +#: src/components/storage/VolumeForm.jsx:400 msgid "" "Limits for the file system size. The final size will be a value between the " "given minimum and maximum. If no maximum is given then the file system will " @@ -1662,57 +1675,57 @@ msgstr "" "geen maximum is gegeven dan zal het bestandssysteem zo groot mogelijk zijn." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeForm.jsx:403 +#: src/components/storage/VolumeForm.jsx:407 msgid "Minimum" msgstr "Minimum" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeForm.jsx:413 +#: src/components/storage/VolumeForm.jsx:417 msgid "Minimum desired size" msgstr "Minimum gewenste grootte" -#: src/components/storage/VolumeForm.jsx:422 +#: src/components/storage/VolumeForm.jsx:426 msgid "Unit for the minimum size" msgstr "Eenheid voor gewenste grootte" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:433 +#: src/components/storage/VolumeForm.jsx:437 msgid "Maximum" msgstr "Maximum" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:444 +#: src/components/storage/VolumeForm.jsx:448 msgid "Maximum desired size" msgstr "Maximaal gewenste grootte" -#: src/components/storage/VolumeForm.jsx:452 +#: src/components/storage/VolumeForm.jsx:456 msgid "Unit for the maximum size" msgstr "Eenheid voor maximale grootte" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeForm.jsx:469 +#: src/components/storage/VolumeForm.jsx:473 msgid "Auto" msgstr "Auto" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeForm.jsx:471 +#: src/components/storage/VolumeForm.jsx:475 msgid "Fixed" msgstr "Vast" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeForm.jsx:473 +#: src/components/storage/VolumeForm.jsx:477 msgid "Range" msgstr "Reikwijdte" -#: src/components/storage/VolumeForm.jsx:681 +#: src/components/storage/VolumeForm.jsx:685 msgid "A size value is required" msgstr "Een grootheids waarde is nodig" -#: src/components/storage/VolumeForm.jsx:686 +#: src/components/storage/VolumeForm.jsx:690 msgid "Minimum size is required" msgstr "Minimum grootte is vereist" -#: src/components/storage/VolumeForm.jsx:690 +#: src/components/storage/VolumeForm.jsx:694 msgid "Maximum must be greater than minimum" msgstr "Maximum moet groter zijn dan het minimum" diff --git a/web/po/pt_BR.po b/web/po/pt_BR.po index 87ba85bddf..bd9e2ab1ce 100644 --- a/web/po/pt_BR.po +++ b/web/po/pt_BR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"POT-Creation-Date: 2024-03-14 02:05+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: anonymous \n" "Language-Team: Portuguese (Brazil) \n" "Language-Team: Russian \n" "Language-Team: Swedish \n" @@ -156,8 +156,8 @@ msgstr "" #: src/components/storage/ProposalDeviceSection.jsx:169 #: src/components/storage/ProposalDeviceSection.jsx:364 #: src/components/storage/ProposalSettingsSection.jsx:263 -#: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:283 +#: src/components/storage/ProposalVolumes.jsx:152 +#: src/components/storage/ProposalVolumes.jsx:287 #: src/components/storage/ZFCPPage.jsx:511 msgid "Accept" msgstr "Acceptera" @@ -308,8 +308,8 @@ msgstr "Bekräfta" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 -#: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:310 +#: src/components/storage/ProposalVolumes.jsx:123 +#: src/components/storage/ProposalVolumes.jsx:314 msgid "Actions" msgstr "Åtgärder" @@ -397,8 +397,8 @@ msgstr "" "Språket som används av installationsprogrammet. Språket för det installerade " "systemet kan ställas in på sidan %s." -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/l10n/InstallerLocaleSwitcher.jsx:56 #: src/components/l10n/L10nPage.jsx:384 #: src/components/overview/L10nSection.jsx:52 @@ -535,8 +535,8 @@ msgstr "Adresser" msgid "Addresses data list" msgstr "Adresser data lista" -#. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header +#. TRANSLATORS: input field for the iSCSI initiator name #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:360 @@ -555,7 +555,7 @@ msgid "IP addresses" msgstr "IP adresser" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:234 +#: src/components/storage/ProposalVolumes.jsx:238 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:208 @@ -672,8 +672,8 @@ msgstr "Inga WiFi-anslutningar hittades." msgid "Connect to a Wi-Fi network" msgstr "Anslut till ett Wi-Fi nätverk" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/network/NetworkPage.jsx:169 #: src/components/overview/NetworkSection.jsx:83 msgid "Network" @@ -757,8 +757,8 @@ msgstr "Ansluten" msgid "Disconnecting" msgstr "Kopplar från" -#. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status +#. TRANSLATORS: iSCSI connection status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -822,8 +822,8 @@ msgstr "Läser programvaruförråd" msgid "Refresh the repositories" msgstr "Uppdatera förråden" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:143 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -866,8 +866,8 @@ msgstr "Installera på enhet %s" msgid "Probing storage devices" msgstr "Undersöker lagringsenheter" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:208 #: src/components/storage/ProposalPage.jsx:243 msgid "Storage" @@ -1380,13 +1380,13 @@ msgid "Current content" msgstr "Nuvarande innehåll" #: src/components/storage/ProposalSpacePolicySection.jsx:74 -#: src/components/storage/ProposalVolumes.jsx:309 -#: src/components/storage/VolumeForm.jsx:741 +#: src/components/storage/ProposalVolumes.jsx:313 +#: src/components/storage/VolumeForm.jsx:745 msgid "Size" msgstr "Storlek" #: src/components/storage/ProposalSpacePolicySection.jsx:75 -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:312 msgid "Details" msgstr "Detaljer" @@ -1445,7 +1445,7 @@ msgid "Space action selector for %s" msgstr "Platsåtgärdsväljare för %s" #: src/components/storage/ProposalSpacePolicySection.jsx:224 -#: src/components/storage/ProposalVolumes.jsx:229 +#: src/components/storage/ProposalVolumes.jsx:233 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Ta bort" @@ -1477,20 +1477,19 @@ msgstr "" "anges nedan. Välj hur du vill göra det." #: src/components/storage/ProposalTransactionalInfo.jsx:46 -#, fuzzy msgid "Transactional root file system" -msgstr "Transaktionellt system" +msgstr "Transaktionellt root filsystem" #: src/components/storage/ProposalTransactionalInfo.jsx:49 -#, fuzzy, c-format +#, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " "file system updated via snapshots." msgstr "" -"%s är ett oföränderligt system med atomära uppdateringar som använder ett " -"skrivskyddat Btrfs-rootfilsystem." +"%s är ett oföränderligt system med atomära uppdateringar. Det använder ett " +"skrivskyddat Btrfs filsystem som uppdateras via ögonblicksavbilder." -#. TRANSLATORS: header for a list of items +#. TRANSLATORS: header for a list of items referring to size limits for file systems #: src/components/storage/ProposalVolumes.jsx:59 msgid "These limits are affected by:" msgstr "Dessa gränser påverkas av:" @@ -1507,63 +1506,69 @@ msgstr "Konfigurationen för ögonblicksavbilder" msgid "Presence of other volumes (%s)" msgstr "Närvaro av andra volymer (%s)" +#. TRANSLATORS: list item, describes a factor that affects the computed size of a +#. file system; eg. adjusting the size of the swap +#: src/components/storage/ProposalVolumes.jsx:71 +msgid "The amount of RAM in the system" +msgstr "Mängden RAM i systemet" + #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:129 +#: src/components/storage/ProposalVolumes.jsx:133 msgid "Reset to defaults" msgstr "Återställ till standard" #. TRANSLATORS: dropdown menu label -#: src/components/storage/ProposalVolumes.jsx:137 #: src/components/storage/ProposalVolumes.jsx:141 +#: src/components/storage/ProposalVolumes.jsx:145 msgid "Add file system" msgstr "Lägg till filsystem" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/ProposalVolumes.jsx:191 +#: src/components/storage/ProposalVolumes.jsx:195 #, c-format msgid "At least %s" msgstr "Åtminstone %s" #. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/ProposalVolumes.jsx:197 +#: src/components/storage/ProposalVolumes.jsx:201 msgid "auto" msgstr "auto" #. TRANSLATORS: the filesystem uses a logical volume (LVM) -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "logical volume" msgstr "logisk volym" -#: src/components/storage/ProposalVolumes.jsx:207 +#: src/components/storage/ProposalVolumes.jsx:211 msgid "partition" msgstr "partition" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:216 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "encrypted" msgstr "krypterad" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:218 +#: src/components/storage/ProposalVolumes.jsx:222 msgid "with snapshots" msgstr "med ögonblicksavbilder" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:220 +#: src/components/storage/ProposalVolumes.jsx:224 msgid "transactional" msgstr "transaktionell" -#: src/components/storage/ProposalVolumes.jsx:275 +#: src/components/storage/ProposalVolumes.jsx:279 msgid "Edit file system" msgstr "Redigera filsystem" -#: src/components/storage/ProposalVolumes.jsx:307 -#: src/components/storage/VolumeForm.jsx:726 -#: src/components/storage/VolumeForm.jsx:731 +#: src/components/storage/ProposalVolumes.jsx:311 +#: src/components/storage/VolumeForm.jsx:730 +#: src/components/storage/VolumeForm.jsx:735 msgid "Mount point" msgstr "Monteringspunkt" -#: src/components/storage/ProposalVolumes.jsx:345 +#: src/components/storage/ProposalVolumes.jsx:349 msgid "Table with mount points" msgstr "Tabell med monteringspunkter" @@ -1605,37 +1610,42 @@ msgstr "närvaron av filsystemet för %s" msgid ", " msgstr ", " +#. TRANSLATORS: item which affects the final computed partition size +#: src/components/storage/VolumeForm.jsx:315 +msgid "the amount of RAM in the system" +msgstr "mängden RAM i systemet" + #. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeForm.jsx:314 +#: src/components/storage/VolumeForm.jsx:318 #, c-format msgid "The final size depends on %s." msgstr "Den slutliga storleken beror på %s." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeForm.jsx:316 +#: src/components/storage/VolumeForm.jsx:320 msgid " and " msgstr " och " #. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeForm.jsx:321 +#: src/components/storage/VolumeForm.jsx:325 msgid "Automatically calculated size according to the selected product." msgstr "Automatiskt beräknad storlek enligt vald produkt." -#: src/components/storage/VolumeForm.jsx:341 +#: src/components/storage/VolumeForm.jsx:345 msgid "Exact size for the file system." msgstr "Exakt storlek för filsystemet." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeForm.jsx:353 +#: src/components/storage/VolumeForm.jsx:357 msgid "Exact size" msgstr "Exakt storlek" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeForm.jsx:368 +#: src/components/storage/VolumeForm.jsx:372 msgid "Size unit" msgstr "Storleksenhet" -#: src/components/storage/VolumeForm.jsx:396 +#: src/components/storage/VolumeForm.jsx:400 msgid "" "Limits for the file system size. The final size will be a value between the " "given minimum and maximum. If no maximum is given then the file system will " @@ -1646,57 +1656,57 @@ msgstr "" "filsystemet att vara så stort som möjligt." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeForm.jsx:403 +#: src/components/storage/VolumeForm.jsx:407 msgid "Minimum" msgstr "Minst" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeForm.jsx:413 +#: src/components/storage/VolumeForm.jsx:417 msgid "Minimum desired size" msgstr "Minsta önskade storlek" -#: src/components/storage/VolumeForm.jsx:422 +#: src/components/storage/VolumeForm.jsx:426 msgid "Unit for the minimum size" msgstr "Enhet för minsta storlek" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:433 +#: src/components/storage/VolumeForm.jsx:437 msgid "Maximum" msgstr "Maximal" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeForm.jsx:444 +#: src/components/storage/VolumeForm.jsx:448 msgid "Maximum desired size" msgstr "Maximal önskad storlek" -#: src/components/storage/VolumeForm.jsx:452 +#: src/components/storage/VolumeForm.jsx:456 msgid "Unit for the maximum size" msgstr "Enhet för maximal storlek" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeForm.jsx:469 +#: src/components/storage/VolumeForm.jsx:473 msgid "Auto" msgstr "Auto" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeForm.jsx:471 +#: src/components/storage/VolumeForm.jsx:475 msgid "Fixed" msgstr "Fast" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeForm.jsx:473 +#: src/components/storage/VolumeForm.jsx:477 msgid "Range" msgstr "Räckvidd" -#: src/components/storage/VolumeForm.jsx:681 +#: src/components/storage/VolumeForm.jsx:685 msgid "A size value is required" msgstr "Ett storleksvärde krävs" -#: src/components/storage/VolumeForm.jsx:686 +#: src/components/storage/VolumeForm.jsx:690 msgid "Minimum size is required" msgstr "Minsta storlek krävs" -#: src/components/storage/VolumeForm.jsx:690 +#: src/components/storage/VolumeForm.jsx:694 msgid "Maximum must be greater than minimum" msgstr "Maximalt måste vara större än minimalt" diff --git a/web/po/uk.po b/web/po/uk.po index ddcf3100c5..a1c3d0c553 100644 --- a/web/po/uk.po +++ b/web/po/uk.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-10 02:10+0000\n" +"POT-Creation-Date: 2024-03-14 02:05+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: Milachew \n" "Language-Team: Ukrainian \n" "Language-Team: Chinese (Simplified) Date: Sun, 17 Mar 2024 22:35:56 +0100 Subject: [PATCH 36/98] Revert "Tumbleweed configuration: enable adjust_by_ram for swap" This reverts commit 9f537c65cfd08cba670fcea809b5f454bdc60228. --- products.d/tumbleweed.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index a4d5a11d71..cebe4f493e 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -165,12 +165,10 @@ storage: - mount_path: "swap" filesystem: swap size: - auto: true + auto: false + min: 1 GiB + max: 2 GiB outline: - auto_size: - base_min: 1 GiB - base_max: 2 GiB - adjust_by_ram: true required: false filesystems: - swap From 32c4639674380478cd02a91002002c146c41f22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Mon, 18 Mar 2024 09:18:55 +0000 Subject: [PATCH 37/98] web: add Result section to Storage page (#1088) Replace the `Actions` section in the storage proposal for a `Result` one which presents the proposal result in a more appealing way displaying how the storage would look after installation instead of just the list of actions. Among others bits, it hints the user about things like file-systems are going to be created, resized actions and a subtle emphasis in deletions, if any. The full, plain list of actions is now available in a modal dialog linked right before the new introduced table. --- .../storage/interfaces/device/filesystem.rb | 12 +- .../interfaces/device/filesystem_examples.rb | 6 + service/test/fixtures/trivial_lvm.yml | 4 +- web/.eslintignore | 1 + web/cspell.json | 3 +- web/src/assets/styles/blocks.scss | 156 +- web/src/assets/styles/variables.scss | 4 + web/src/client/storage.js | 194 +-- web/src/client/storage.test.js | 3 +- web/src/components/core/Reminder.jsx | 6 +- web/src/components/core/Reminder.test.jsx | 6 + web/src/components/core/Tag.jsx | 41 + web/src/components/core/Tag.test.jsx | 33 + web/src/components/core/TreeTable.jsx | 128 ++ web/src/components/core/TreeTable.test.jsx | 24 + web/src/components/core/index.js | 2 + web/src/components/storage/DevicesManager.js | 215 +++ .../components/storage/DevicesManager.test.js | 433 +++++ ...sSection.jsx => ProposalActionsDialog.jsx} | 99 +- .../storage/ProposalActionsDialog.test.jsx | 144 ++ .../storage/ProposalActionsSection.test.jsx | 135 -- .../storage/ProposalDeviceSection.jsx | 14 +- web/src/components/storage/ProposalPage.jsx | 61 +- .../components/storage/ProposalPage.test.jsx | 6 +- .../storage/ProposalResultSection.jsx | 296 ++++ .../storage/ProposalResultSection.test.jsx | 133 ++ web/src/components/storage/index.js | 3 +- .../storage/test-data/full-result-example.js | 1446 +++++++++++++++++ web/src/components/storage/utils.js | 34 +- web/src/components/storage/utils.test.js | 61 + web/src/utils.js | 26 +- web/src/utils.test.js | 18 +- 32 files changed, 3395 insertions(+), 352 deletions(-) create mode 100644 web/src/components/core/Tag.jsx create mode 100644 web/src/components/core/Tag.test.jsx create mode 100644 web/src/components/core/TreeTable.jsx create mode 100644 web/src/components/core/TreeTable.test.jsx create mode 100644 web/src/components/storage/DevicesManager.js create mode 100644 web/src/components/storage/DevicesManager.test.js rename web/src/components/storage/{ProposalActionsSection.jsx => ProposalActionsDialog.jsx} (54%) create mode 100644 web/src/components/storage/ProposalActionsDialog.test.jsx delete mode 100644 web/src/components/storage/ProposalActionsSection.test.jsx create mode 100644 web/src/components/storage/ProposalResultSection.jsx create mode 100644 web/src/components/storage/ProposalResultSection.test.jsx create mode 100644 web/src/components/storage/test-data/full-result-example.js diff --git a/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb index ad066f2022..545a179813 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb @@ -45,6 +45,15 @@ def self.apply?(storage_device) FILESYSTEM_INTERFACE = "org.opensuse.Agama.Storage1.Filesystem" private_constant :FILESYSTEM_INTERFACE + # SID of the file system. + # + # It is useful to detect whether a file system is new. + # + # @return [Integer] + def filesystem_sid + storage_device.filesystem.sid + end + # File system type. # # @return [String] e.g., "ext4" @@ -68,7 +77,8 @@ def filesystem_label def self.included(base) base.class_eval do - dbus_interface FILESYSTEM_INTERFACE do + dbus_interface FILESYSTEM_INTERFACE do + dbus_reader :filesystem_sid, "u", dbus_name: "SID" dbus_reader :filesystem_type, "s", dbus_name: "Type" dbus_reader :filesystem_mount_path, "s", dbus_name: "MountPath" dbus_reader :filesystem_label, "s", dbus_name: "Label" diff --git a/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb index 84b8da4a10..71694a3ab7 100644 --- a/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb @@ -28,6 +28,12 @@ let(:device) { devicegraph.find_by_name("/dev/mapper/0QEMU_QEMU_HARDDISK_mpath1") } + describe "#filesystem_sid" do + it "returns the file system SID" do + expect(subject.filesystem_sid).to eq(45) + end + end + describe "#filesystem_type" do it "returns the file system type" do expect(subject.filesystem_type).to eq("ext4") diff --git a/service/test/fixtures/trivial_lvm.yml b/service/test/fixtures/trivial_lvm.yml index 86f3fc904e..ff7aec2699 100644 --- a/service/test/fixtures/trivial_lvm.yml +++ b/service/test/fixtures/trivial_lvm.yml @@ -6,7 +6,7 @@ partitions: - partition: - size: unlimited + size: 100 GiB name: /dev/sda1 id: lvm @@ -18,7 +18,7 @@ lvm_lvs: - lvm_lv: - size: unlimited + size: 100 GiB lv_name: lv1 file_system: btrfs mount_point: / diff --git a/web/.eslintignore b/web/.eslintignore index 8faa0e3fd2..fb9357ef5e 100644 --- a/web/.eslintignore +++ b/web/.eslintignore @@ -1,2 +1,3 @@ node_modules/* src/lib/* +src/**/test-data/* diff --git a/web/cspell.json b/web/cspell.json index 9c7619f4a9..3a2944fcda 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -5,7 +5,8 @@ "ignorePaths": [ "src/lib/cockpit.js", "src/lib/cockpit-po-plugin.js", - "src/manifest.json" + "src/manifest.json", + "src/**/test-data/*" ], "import": [ "@cspell/dict-css/cspell-ext.json", diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index e568ae24db..43b336bf31 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -403,6 +403,143 @@ ul[data-type="agama/list"][role="grid"] { } } +[data-type="agama/tag"] { + font-size: var(--fs-small); + + &[data-variant="teal"] { + color: var(--color-teal); + } + + &[data-variant="orange"] { + color: var(--color-orange); + } + + &[data-variant="gray-highlight"] { + padding: var(--spacer-smaller); + color: var(--color-gray-darkest); + background: var(--color-gray); + border: 1px solid var(--color-gray-dark); + border-radius: 5px; + margin-inline-start: var(--spacer-smaller); + } +} + +table[data-type="agama/tree-table"] { + th:first-child { + block-size: fit-content; + padding-inline-end: var(--spacer-normal); + } + + th.fit-content { + block-size: fit-content; + overflow: visible; + text-overflow: unset; + } + + /** + * Temporary PF/Table overrides for small devices + **/ + @media (width <= 768px) { + &.pf-m-tree-view-grid-md.pf-v5-c-table tr[aria-level="1"] td { + padding-inline-start: var(--spacer-medium); + } + + &.pf-m-tree-view-grid-md.pf-v5-c-table tr[aria-level="2"] th { + padding-inline-start: calc(var(--spacer-large) * 1.1); + } + + &.pf-m-tree-view-grid-md.pf-v5-c-table tr[aria-level="2"] td { + padding-inline-start: calc(var(--spacer-large) * 1.4); + } + + &.pf-m-tree-view-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr).pf-m-tree-view-details-expanded { + padding-block-end: var(--spacer-smaller); + } + + &.pf-m-tree-view-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr) td:empty, + &.pf-m-tree-view-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr) td *:empty, + &.pf-m-tree-view-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr) td:has(> *:empty) { + display: none; + } + + &.pf-m-tree-view-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr) td:has(> *:not(:empty)) { + display: inherit; + } + + &.pf-m-tree-view-grid-md.pf-v5-c-table tbody:where(.pf-v5-c-table__tbody) tr:where(.pf-v5-c-table__tr)::before { + inset-inline-start: 0; + } + + &.pf-v5-c-table.pf-m-compact tr:where(.pf-v5-c-table__tr):not(.pf-v5-c-table__expandable-row) > *:last-child { + padding-inline-end: 8px; + } + + tbody th:first-child { + font-size: var(--fs-large); + padding-block-start: var(--spacer-small); + } + } +} + +table.proposal-result { + tr.dimmed-row { + background-color: #fff; + opacity: 0.8; + background: repeating-linear-gradient( -45deg, #fcfcff, #fcfcff 3px, #fff 3px, #fff 10px ); + + td { + color: var(--color-gray-dimmed); + padding-block: 0; + } + + } + + /** + * Temporary hack because the collapse/expand callback was not given to the + * tree table + **/ + th button { + display: none; + } + + tbody th .pf-v5-c-table__tree-view-main { + padding-inline-start: var(--pf-v5-c-table--m-compact--cell--first-last-child--PaddingLeft); + cursor: auto; + } + + tbody tr[aria-level="2"] th .pf-v5-c-table__tree-view-main { + padding-inline-start: calc( + var(--pf-v5-c-table--m-compact--cell--first-last-child--PaddingLeft) + var(--spacer-large) + ); + } + /** End of temporary hack */ + + @media (width > 768px) { + th.details-column { + padding-inline-start: calc(60px + var(--spacer-smaller) * 2); + } + + td.details-column { + display: grid; + gap: var(--spacer-smaller); + grid-template-columns: 60px 1fr; + + :first-child { + text-align: end; + } + } + + th.sizes-column, + td.sizes-column { + text-align: end; + + div.split { + justify-content: flex-end; + } + } + } +} + // compact lists in popover .pf-v5-c-popover li + li { margin: 0; @@ -486,7 +623,24 @@ ul[data-type="agama/list"][role="grid"] { h4 { color: var(--accent-color); - margin-block-end: var(--spacer-smaller); + } + + h4 ~ * { + margin-block-start: var(--spacer-small); + } +} + +section [data-type="agama/reminder"] { + margin-inline: 0; +} + +[data-type="agama/reminder"][data-variant="subtle"] { + --accent-color: var(--color-primary); + padding-block: 0; + border-inline-start-width: 1px; + + h4 { + font-size: var(--fs-normal); } } diff --git a/web/src/assets/styles/variables.scss b/web/src/assets/styles/variables.scss index b25bfcbad3..490429fe54 100644 --- a/web/src/assets/styles/variables.scss +++ b/web/src/assets/styles/variables.scss @@ -51,8 +51,12 @@ --color-gray: #f2f2f2; --color-gray-dark: #efefef; // Fog --color-gray-darker: #999; + --color-gray-darkest: #333; --color-gray-dimmed: #888; --color-gray-dimmest: #666; + --color-teal: #279c9c; + --color-blue: #0d4ea6; + --color-orange: #e86427; --color-link: #0c322c; --color-link-hover: #30ba78; diff --git a/web/src/client/storage.js b/web/src/client/storage.js index b7d6135f4b..4c2b2153bd 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -47,6 +47,103 @@ const ZFCP_CONTROLLER_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Controller"; const ZFCP_DISKS_NAMESPACE = "/org/opensuse/Agama/Storage1/zfcp_disks"; const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk"; +/** + * @typedef {object} StorageDevice + * @property {number} sid - Storage ID + * @property {string} name - Device name + * @property {string} description - Device description + * @property {boolean} isDrive - Whether the device is a drive + * @property {string} type - Type of device (e.g., "disk", "raid", "multipath", "dasd", "md") + * @property {string} [vendor] + * @property {string} [model] + * @property {string[]} [driver] + * @property {string} [bus] + * @property {string} [busId] - DASD Bus ID (only for "dasd" type) + * @property {string} [transport] + * @property {boolean} [sdCard] + * @property {boolean} [dellBOOS] + * @property {string[]} [devices] - RAID devices (only for "raid" and "md" types) + * @property {string[]} [wires] - Multipath wires (only for "multipath" type) + * @property {string} [level] - MD RAID level (only for "md" type) + * @property {string} [uuid] + * @property {number} [start] - First block of the region (only for block devices) + * @property {boolean} [active] + * @property {boolean} [encrypted] - Whether the device is encrypted (only for block devices) + * @property {boolean} [isEFI] - Whether the device is an EFI partition (only for partition) + * @property {number} [size] + * @property {number} [recoverableSize] + * @property {string[]} [systems] - Name of the installed systems + * @property {string[]} [udevIds] + * @property {string[]} [udevPaths] + * @property {PartitionTable} [partitionTable] + * @property {Filesystem} [filesystem] + * @property {Component} [component] - When it is used as component of other devices + * @property {StorageDevice[]} [physicalVolumes] - Only for LVM VGs + * @property {StorageDevice[]} [logicalVolumes] - Only for LVM VGs + * + * @typedef {object} PartitionTable + * @property {string} type + * @property {StorageDevice[]} partitions + * @property {PartitionSlot[]} unusedSlots + * @property {number} unpartitionedSize - Total size not assigned to any partition + * + * @typedef {object} PartitionSlot + * @property {number} start + * @property {number} size + * + * @typedef {object} Component + * @property {string} type + * @property {string[]} deviceNames + * + * @typedef {object} Filesystem + * @property {number} sid + * @property {string} type + * @property {string} [mountPath] + * + * @typedef {object} ProposalResult + * @property {ProposalSettings} settings + * @property {Action[]} actions + * + * @typedef {object} Action + * @property {number} device + * @property {string} text + * @property {boolean} subvol + * @property {boolean} delete + * + * @typedef {object} ProposalSettings + * @property {string} bootDevice + * @property {string} encryptionPassword + * @property {string} encryptionMethod + * @property {boolean} lvm + * @property {string} spacePolicy + * @property {SpaceAction[]} spaceActions + * @property {string[]} systemVGDevices + * @property {Volume[]} volumes + * @property {StorageDevice[]} installationDevices + * + * @typedef {object} SpaceAction + * @property {string} device + * @property {string} action + * + * @typedef {object} Volume + * @property {string} mountPath + * @property {string} fsType + * @property {number} minSize + * @property {number} [maxSize] + * @property {boolean} autoSize + * @property {boolean} snapshots + * @property {boolean} transactional + * @property {VolumeOutline} outline + * + * @typedef {object} VolumeOutline + * @property {boolean} required + * @property {string[]} fsTypes + * @property {boolean} supportAutoSize + * @property {boolean} snapshotsConfigurable + * @property {boolean} snapshotsAffectSizes + * @property {string[]} sizeRelevantVolumes + */ + /** * Enum for the encryption method values * @@ -106,57 +203,6 @@ class DevicesManager { * Gets all the exported devices * * @returns {Promise} - * - * @typedef {object} StorageDevice - * @property {string} sid - Storage ID - * @property {string} name - Device name - * @property {string} description - Device description - * @property {boolean} isDrive - Whether the device is a drive - * @property {string} type - Type of device ("disk", "raid", "multipath", "dasd", "md") - * @property {string} [vendor] - * @property {string} [model] - * @property {string[]} [driver] - * @property {string} [bus] - * @property {string} [busId] - DASD Bus ID (only for "dasd" type) - * @property {string} [transport] - * @property {boolean} [sdCard] - * @property {boolean} [dellBOOS] - * @property {string[]} [devices] - RAID devices (only for "raid" and "md" types) - * @property {string[]} [wires] - Multipath wires (only for "multipath" type) - * @property {string} [level] - MD RAID level (only for "md" type) - * @property {string} [uuid] - * @property {number} [start] - First block of the region (only for block devices) - * @property {boolean} [active] - * @property {boolean} [encrypted] - Whether the device is encrypted (only for block devices) - * @property {boolean} [isEFI] - Whether the device is an EFI partition (only for partition) - * @property {number} [size] - * @property {number} [recoverableSize] - * @property {string[]} [systems] - Name of the installed systems - * @property {string[]} [udevIds] - * @property {string[]} [udevPaths] - * @property {PartitionTable} [partitionTable] - * @property {Filesystem} [filesystem] - * @property {Component} [component] - When it is used as component of other devices - * @property {StorageDevice[]} [physicalVolumes] - Only for LVM VGs - * @property {StorageDevice[]} [logicalVolumes] - Only for LVM VGs - * - * @typedef {object} PartitionTable - * @property {string} type - * @property {StorageDevice[]} partitions - * @property {PartitionSlot[]} unusedSlots - * @property {number} unpartitionedSize - Total size not assigned to any partition - * - * @typedef {object} PartitionSlot - * @property {number} start - * @property {number} size - * - * @typedef {object} Component - * @property {string} type - * @property {string[]} deviceNames - * - * @typedef {object} Filesystem - * @property {string} type - * @property {string} [mountPath] */ async getDevices() { const buildDevice = (path, dbusDevices) => { @@ -236,6 +282,7 @@ class DevicesManager { const buildMountPath = path => path.length > 0 ? path : undefined; const buildLabel = label => label.length > 0 ? label : undefined; device.filesystem = { + sid: filesystemProperties.SID.v, type: filesystemProperties.Type.v, mountPath: buildMountPath(filesystemProperties.MountPath.v), label: buildLabel(filesystemProperties.Label.v) @@ -329,42 +376,6 @@ class ProposalManager { }; } - /** - * @typedef {object} ProposalSettings - * @property {string} bootDevice - * @property {string} encryptionPassword - * @property {string} encryptionMethod - * @property {boolean} lvm - * @property {string} spacePolicy - * @property {SpaceAction[]} spaceActions - * @property {string[]} systemVGDevices - * @property {Volume[]} volumes - * @property {StorageDevice[]} installationDevices - * - * @typedef {object} SpaceAction - * @property {string} device - * @property {string} action - * - * @typedef {object} Volume - * @property {string} mountPath - * @property {string} fsType - * @property {number} minSize - * @property {number} [maxSize] - * @property {boolean} autoSize - * @property {boolean} snapshots - * @property {boolean} transactional - * @property {VolumeOutline} outline - * - * @typedef {object} VolumeOutline - * @property {boolean} required - * @property {string[]} fsTypes - * @property {boolean} supportAutoSize - * @property {boolean} adjustByRam - * @property {boolean} snapshotsConfigurable - * @property {boolean} snapshotsAffectSizes - * @property {string[]} sizeRelevantVolumes - */ - /** * Gets the list of available devices * @@ -421,15 +432,6 @@ class ProposalManager { * Gets the values of the current proposal * * @return {Promise} - * - * @typedef {object} ProposalResult - * @property {ProposalSettings} settings - * @property {Action[]} actions - * - * @typedef {object} Action - * @property {string} text - * @property {boolean} subvol - * @property {boolean} delete */ async getResult() { const proxy = await this.proposalProxy(); diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index d78dc64263..1604c04449 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -205,7 +205,7 @@ const md0 = { systems : ["openSUSE Leap 15.2"], udevIds: [], udevPaths: [], - filesystem: { type: "ext4", mountPath: "/test", label: "system" } + filesystem: { sid: 100, type: "ext4", mountPath: "/test", label: "system" } }; const raid = { @@ -866,6 +866,7 @@ const contexts = { UdevPaths: { t: "as", v: [] } }, "org.opensuse.Agama.Storage1.Filesystem": { + SID: { t: "u", v: 100 }, Type: { t: "s", v: "ext4" }, MountPath: { t: "s", v: "/test" }, Label: { t: "s", v: "system" } diff --git a/web/src/components/core/Reminder.jsx b/web/src/components/core/Reminder.jsx index 997e870a91..cd4e0943ad 100644 --- a/web/src/components/core/Reminder.jsx +++ b/web/src/components/core/Reminder.jsx @@ -62,7 +62,8 @@ const ReminderTitle = ({ children }) => { * @param {object} props * @param {string} [props.icon] - The name of desired icon. * @param {JSX.Element|string} [props.title] - The content for the title. - * @param {string} [props.role="status"] - The reminder's role, "status" by + * @param {string} [props.role="status"] - The reminder's role, "status" by default. + * @param {("subtle")} [props.variant] - The reminder's variant, none by default. * default. See {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_role} * @param {JSX.Element} [props.children] - The content for the description. */ @@ -70,10 +71,11 @@ export default function Reminder ({ icon, title, role = "status", + variant, children }) { return ( -

+
{title} diff --git a/web/src/components/core/Reminder.test.jsx b/web/src/components/core/Reminder.test.jsx index 24527cf2d0..41ebfaf300 100644 --- a/web/src/components/core/Reminder.test.jsx +++ b/web/src/components/core/Reminder.test.jsx @@ -37,6 +37,12 @@ describe("Reminder", () => { within(reminder).getByText("Example"); }); + it("renders a region with given data-variant, if any", () => { + plainRender(Example); + const reminder = screen.getByRole("alert"); + expect(reminder).toHaveAttribute("data-variant", "subtle"); + }); + it("renders given title", () => { plainRender( Kindly reminder}> diff --git a/web/src/components/core/Tag.jsx b/web/src/components/core/Tag.jsx new file mode 100644 index 0000000000..c910d07295 --- /dev/null +++ b/web/src/components/core/Tag.jsx @@ -0,0 +1,41 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +// @ts-check + +import React from "react"; + +/** + * Simple component that helps wrapped content stand out visually. The variant + * prop determines what kind of enhancement is applied. + * @component + * + * @param {object} props + * @param {("simple"|"teal"|"orange"|"gray-highlight")} [props.variant="simple"] + * @param {React.ReactNode} props.children + */ +export default function Tag ({ variant = "simple", children }) { + return ( + + {children} + + ); +} diff --git a/web/src/components/core/Tag.test.jsx b/web/src/components/core/Tag.test.jsx new file mode 100644 index 0000000000..e4a9739abf --- /dev/null +++ b/web/src/components/core/Tag.test.jsx @@ -0,0 +1,33 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { Tag } from "~/components/core"; + +describe("Tag", () => { + it("renders its children in a node with data-type='agama/tag' attribute", () => { + plainRender(New); + const node = screen.getByText("New"); + expect(node).toHaveAttribute("data-type", "agama/tag"); + }); +}); diff --git a/web/src/components/core/TreeTable.jsx b/web/src/components/core/TreeTable.jsx new file mode 100644 index 0000000000..5f7612adb8 --- /dev/null +++ b/web/src/components/core/TreeTable.jsx @@ -0,0 +1,128 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +// @ts-check + +import React from "react"; +import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/react-table'; + +/** + * @typedef {import("@patternfly/react-table").TableProps} TableProps + */ + +/** + * @typedef {object} TreeTableColumn + * @property {string} title + * @property {(any) => React.ReactNode} content + * @property {string} [classNames] + */ + +/** + * @typedef {object} TreeTableBaseProps + * @property {TreeTableColumn[]} columns=[] + * @property {object[]} items=[] + * @property {(any) => array} [itemChildren] + * @property {(any) => string} [rowClassNames] + */ + +/** + * Table built on top of PF/Table + * @component + * + * FIXME: omitting `ref` here to avoid a TypeScript error but keep component as + * typed as possible. Further investigation is needed. + * + * @typedef {TreeTableBaseProps & Omit} TreeTableProps + * + * @param {TreeTableProps} props + */ +export default function TreeTable({ + columns = [], + items = [], + itemChildren = () => [], + rowClassNames = () => "", + ...tableProps +}) { + const renderColumns = (item, treeRow) => { + return columns.map((c, cIdx) => { + const props = { + dataLabel: c.title, + className: c.classNames + }; + + if (cIdx === 0) props.treeRow = treeRow; + + return ( + {c.content(item)} + ); + }); + }; + + const renderRows = (items, level) => { + if (items?.length <= 0) return; + + return ( + items.map((item, itemIdx) => { + const children = itemChildren(item); + + const treeRow = { + props: { + isExpanded: true, + isDetailsExpanded: true, + "aria-level": level, + "aria-posinset": itemIdx + 1, + "aria-setsize": children?.length || 0 + } + }; + + const rowProps = { + row: { props: treeRow.props }, + className: rowClassNames(item) + }; + + return ( + + {renderColumns(item, treeRow)} + { renderRows(children, level + 1)} + + ); + }) + ); + }; + + return ( + + + + { columns.map((c, i) => ) } + + + + { renderRows(items, 1) } + +
{c.title}
+ ); +} diff --git a/web/src/components/core/TreeTable.test.jsx b/web/src/components/core/TreeTable.test.jsx new file mode 100644 index 0000000000..c1c858da16 --- /dev/null +++ b/web/src/components/core/TreeTable.test.jsx @@ -0,0 +1,24 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +describe("TreeTable", () => { + it.todo("add examples for testing core/TreeTable component"); +}); diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index 6bdac86248..f6448c03d3 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -58,3 +58,5 @@ export { default as DevelopmentInfo } from "./DevelopmentInfo"; export { default as Selector } from "./Selector"; export { default as OptionsPicker } from "./OptionsPicker"; export { default as Reminder } from "./Reminder"; +export { default as Tag } from "./Tag"; +export { default as TreeTable } from "./TreeTable"; diff --git a/web/src/components/storage/DevicesManager.js b/web/src/components/storage/DevicesManager.js new file mode 100644 index 0000000000..5f70318564 --- /dev/null +++ b/web/src/components/storage/DevicesManager.js @@ -0,0 +1,215 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +// @ts-check + +import { compact, uniq } from "~/utils"; + +/** + * @typedef {import ("~/client/storage").Action} Action + * @typedef {import ("~/client/storage").StorageDevice} StorageDevice + */ + +/** + * Class for managing storage devices. + */ +export default class DevicesManager { + /** + * @param {StorageDevice[]} system - Devices representing the current state of the system. + * @param {StorageDevice[]} staging - Devices representing the target state of the system. + * @param {Action[]} actions - Actions to perform from system to staging. + */ + constructor(system, staging, actions) { + this.system = system; + this.staging = staging; + this.actions = actions; + } + + /** + * System device with the given SID. + * + * @param {Number} sid + * @returns {StorageDevice|undefined} + */ + systemDevice(sid) { + return this.#device(sid, this.system); + } + + /** + * Staging device with the given SID. + * + * @param {Number} sid + * @returns {StorageDevice|undefined} + */ + stagingDevice(sid) { + return this.#device(sid, this.staging); + } + + /** + * Whether the given device exists in system. + * + * @param {StorageDevice} device + * @returns {Boolean} + */ + existInSystem(device) { + return this.#exist(device, this.system); + } + + /** + * Whether the given device exists in staging. + * + * @param {StorageDevice} device + * @returns {Boolean} + */ + existInStaging(device) { + return this.#exist(device, this.staging); + } + + /** + * Whether the given device is going to be formatted. + * + * @param {StorageDevice} device + * @returns {Boolean} + */ + hasNewFilesystem(device) { + if (!device.filesystem) return false; + + const systemDevice = this.systemDevice(device.sid); + const systemFilesystemSID = systemDevice?.filesystem?.sid; + + return device.filesystem.sid !== systemFilesystemSID; + } + + /** + * Whether the given device is going to be shrunk. + * + * @param {StorageDevice} device + * @returns {Boolean} + */ + isShrunk(device) { + return this.shrinkSize(device) > 0; + } + + /** + * Amount of bytes the given device is going to be shrunk. + * + * @param {StorageDevice} device + * @returns {Number} + */ + shrinkSize(device) { + const systemDevice = this.systemDevice(device.sid); + const stagingDevice = this.stagingDevice(device.sid); + + if (!systemDevice || !stagingDevice) return 0; + + const amount = systemDevice.size - stagingDevice.size; + return amount > 0 ? amount : 0; + } + + /** + * Disk devices and LVM volume groups used for the installation. + * + * @note The used devices are extracted from the actions. + * + * @returns {StorageDevice[]} + */ + usedDevices() { + const isTarget = (device) => device.isDrive || device.type === "lvmVg"; + + // Check in system devices to detect removals. + const targetSystem = this.system.filter(isTarget); + const targetStaging = this.staging.filter(isTarget); + + const sids = targetSystem.concat(targetStaging) + .filter(d => this.#isUsed(d)) + .map(d => d.sid); + + return compact(uniq(sids).map(sid => this.stagingDevice(sid))); + } + + /** + * Devices deleted. + * + * @note The devices are extracted from the actions. + * + * @returns {StorageDevice[]} + */ + deletedDevices() { + return this.#deleteActionsDevice().filter(d => !d.isDrive); + } + + /** + * Systems deleted. + * + * @returns {string[]} + */ + deletedSystems() { + const systems = this.#deleteActionsDevice() + .filter(d => !d.partitionTable) + .map(d => d.systems) + .flat(); + return compact(systems); + } + + /** + * @param {number} sid + * @param {StorageDevice[]} source + * @returns {StorageDevice|undefined} + */ + #device(sid, source) { + return source.find(d => d.sid === sid); + } + + /** + * @param {StorageDevice} device + * @param {StorageDevice[]} source + * @returns {boolean} + */ + #exist(device, source) { + return this.#device(device.sid, source) !== undefined; + } + + /** + * @param {StorageDevice} device + * @returns {boolean} + */ + #isUsed(device) { + const sids = uniq(compact(this.actions.map(a => a.device))); + + const partitions = device.partitionTable?.partitions || []; + const lvmLvs = device.logicalVolumes || []; + + return sids.includes(device.sid) || + partitions.find(p => this.#isUsed(p)) !== undefined || + lvmLvs.find(l => this.#isUsed(l)) !== undefined; + } + + /** + * @returns {StorageDevice[]} + */ + #deleteActionsDevice() { + const sids = this.actions + .filter(a => a.delete) + .map(a => a.device); + const devices = sids.map(sid => this.systemDevice(sid)); + return compact(devices); + } +} diff --git a/web/src/components/storage/DevicesManager.test.js b/web/src/components/storage/DevicesManager.test.js new file mode 100644 index 0000000000..23636fc106 --- /dev/null +++ b/web/src/components/storage/DevicesManager.test.js @@ -0,0 +1,433 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import DevicesManager from "./DevicesManager"; + +let system; +let staging; +let actions; + +beforeEach(() => { + system = []; + staging = []; + actions = []; +}); + +describe("systemDevice", () => { + beforeEach(() => { + staging = [{ sid: 60, name: "/dev/sda" }]; + }); + + describe("if there is no system device with the given SID", () => { + it("returns undefined", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.systemDevice(60)).toBeUndefined(); + }); + }); + + describe("if there is a system device with the given SID", () => { + beforeEach(() => { + system = [{ sid: 60, name: "/dev/sdb" }]; + }); + + it("returns the system device", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.systemDevice(60).name).toEqual("/dev/sdb"); + }); + }); +}); + +describe("stagingDevice", () => { + beforeEach(() => { + system = [{ sid: 60, name: "/dev/sda" }]; + }); + + describe("if there is no staging device with the given SID", () => { + it("returns undefined", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.stagingDevice(60)).toBeUndefined(); + }); + }); + + describe("if there is a staging device with the given SID", () => { + beforeEach(() => { + staging = [{ sid: 60, name: "/dev/sdb" }]; + }); + + it("returns the staging device", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.stagingDevice(60).name).toEqual("/dev/sdb"); + }); + }); +}); + +describe("existInSystem", () => { + beforeEach(() => { + system = [{ sid: 61, name: "/dev/sda2" }]; + staging = [{ sid: 60, name: "/dev/sda1" }]; + }); + + describe("if the given device does not exist in system", () => { + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.existInSystem({ sid: 60 })).toEqual(false); + }); + }); + + describe("if the given device exists in system", () => { + it("returns true", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.existInSystem({ sid: 61 })).toEqual(true); + }); + }); +}); + +describe("existInStaging", () => { + beforeEach(() => { + system = [{ sid: 61, name: "/dev/sda2" }]; + staging = [{ sid: 60, name: "/dev/sda1" }]; + }); + + describe("if the given device does not exist in staging", () => { + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.existInStaging({ sid: 61 })).toEqual(false); + }); + }); + + describe("if the given device exists in staging", () => { + it("returns true", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.existInStaging({ sid: 60 })).toEqual(true); + }); + }); +}); + +describe("hasNewFilesystem", () => { + describe("if the given device has no file system", () => { + beforeEach(() => { + system = [{ sid: 60, name: "/dev/sda1", filesystem: { sid: 61 } }]; + staging = [{ sid: 60, name: "/dev/sda1" }]; + }); + + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.hasNewFilesystem(device)).toEqual(false); + }); + }); + + describe("if the given device has no new file system", () => { + beforeEach(() => { + system = [{ sid: 60, name: "/dev/sda1", filesystem: { sid: 61 } }]; + staging = [{ sid: 60, name: "/dev/sda1", filesystem: { sid: 61 } }]; + }); + + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.hasNewFilesystem(device)).toEqual(false); + }); + }); + + describe("if the given device has a new file system", () => { + beforeEach(() => { + system = [{ sid: 60, name: "/dev/sda1", filesystem: { sid: 61 } }]; + staging = [{ sid: 60, name: "/dev/sda1", filesystem: { sid: 62 } }]; + }); + + it("returns true", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.hasNewFilesystem(device)).toEqual(true); + }); + }); +}); + +describe("isShrunk", () => { + describe("if the device is new", () => { + beforeEach(() => { + system = []; + staging = [{ sid: 60, size: 2048 }]; + }); + + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.isShrunk(device)).toEqual(false); + }); + }); + + describe("if the device does not exist anymore", () => { + beforeEach(() => { + system = [{ sid: 60, size: 2048 }]; + staging = []; + }); + + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.systemDevice(60); + expect(manager.isShrunk(device)).toEqual(false); + }); + }); + + describe("if the size is kept", () => { + beforeEach(() => { + system = [{ sid: 60, size: 1024 }]; + staging = [{ sid: 60, size: 1024 }]; + }); + + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.isShrunk(device)).toEqual(false); + }); + }); + + describe("if the size is more than initially", () => { + beforeEach(() => { + system = [{ sid: 60, size: 1024 }]; + staging = [{ sid: 60, size: 2048 }]; + }); + + it("returns false", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.isShrunk(device)).toEqual(false); + }); + }); + + describe("if the size is less than initially", () => { + beforeEach(() => { + system = [{ sid: 60, size: 1024 }]; + staging = [{ sid: 60, size: 512 }]; + }); + + it("returns true", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.isShrunk(device)).toEqual(true); + }); + }); +}); + +describe("shrinkSize", () => { + describe("if the device is new", () => { + beforeEach(() => { + system = []; + staging = [{ sid: 60, size: 2048 }]; + }); + + it("returns 0", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.shrinkSize(device)).toEqual(0); + }); + }); + + describe("if the device does not exist anymore", () => { + beforeEach(() => { + system = [{ sid: 60, size: 2048 }]; + staging = []; + }); + + it("returns 0", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.systemDevice(60); + expect(manager.shrinkSize(device)).toEqual(0); + }); + }); + + describe("if the size is kept", () => { + beforeEach(() => { + system = [{ sid: 60, size: 1024 }]; + staging = [{ sid: 60, size: 1024 }]; + }); + + it("returns 0", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.shrinkSize(device)).toEqual(0); + }); + }); + + describe("if the size is more than initially", () => { + beforeEach(() => { + system = [{ sid: 60, size: 1024 }]; + staging = [{ sid: 60, size: 2048 }]; + }); + + it("returns 0", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.shrinkSize(device)).toEqual(0); + }); + }); + + describe("if the size is less than initially", () => { + beforeEach(() => { + system = [{ sid: 60, size: 1024 }]; + staging = [{ sid: 60, size: 512 }]; + }); + + it("returns the shrink amount", () => { + const manager = new DevicesManager(system, staging, actions); + const device = manager.stagingDevice(60); + expect(manager.shrinkSize(device)).toEqual(512); + }); + }); +}); + +describe("usedDevices", () => { + beforeEach(() => { + system = [ + { sid: 60, isDrive: false }, + { sid: 61, isDrive: true }, + { sid: 62, isDrive: true, partitionTable: { partitions: [{ sid: 67 }] } }, + { sid: 63, isDrive: true, partitionTable: { partitions: [] } }, + { sid: 64, isDrive: false, type: "lvmVg", logicalVolumes: [] }, + { sid: 65, isDrive: false, type: "lvmVg", logicalVolumes: [] }, + { sid: 66, isDrive: false, type: "lvmVg", logicalVolumes: [{ sid: 68 }] } + ]; + staging = [ + { sid: 60, isDrive: false }, + // Partition removed + { sid: 62, isDrive: true, partitionTable: { partitions: [] } }, + // Partition added + { sid: 63, isDrive: true, partitionTable: { partitions: [{ sid: 69 }] } }, + { sid: 64, isDrive: false, type: "lvmVg", logicalVolumes: [] }, + // Logical volume added + { sid: 65, isDrive: false, type: "lvmVg", logicalVolumes: [{ sid: 70 }, { sid: 71 }] }, + // Logical volume removed + { sid: 66, isDrive: false, type: "lvmVg", logicalVolumes: [] } + ]; + }); + + describe("if there are no actions", () => { + beforeEach(() => { + actions = []; + }); + + it("returns an empty list", () => { + const manager = new DevicesManager(system, staging, actions); + expect(manager.usedDevices()).toEqual([]); + }); + }); + + describe("if there are actions", () => { + beforeEach(() => { + actions = [ + // This device is ignored because it is neither a drive nor a LVM VG. + { device: 60 }, + // This device was removed. + { device: 61 }, + // This partition was removed (belongs to device 62). + { device: 67 }, + // This logical volume was removed (belongs to device 66). + { device: 68 }, + // This partition was added (belongs to device 63). + { device: 69 }, + // This logical volume was added (belongs to device 65). + { device: 70 }, + // This logical volume was added (belongs to device 65). + { device: 71 } + ]; + }); + + it("does not include removed disk devices or LVM volume groups", () => { + const manager = new DevicesManager(system, staging, actions); + const sids = manager.usedDevices().map(d => d.sid) + .sort(); + expect(sids).not.toContain(61); + }); + + it("includes all disk devices and LVM volume groups affected by the actions", () => { + const manager = new DevicesManager(system, staging, actions); + const sids = manager.usedDevices().map(d => d.sid) + .sort(); + expect(sids).toEqual([62, 63, 65, 66]); + }); + }); +}); + +describe("deletedDevices", () => { + beforeEach(() => { + system = [ + { sid: 60 }, + { sid: 62 }, + { sid: 63 }, + { sid: 64 }, + { sid: 65, isDrive: true } + ]; + actions = [ + { device: 60, delete: true }, + // This device does not exist in system. + { device: 61, delete: true }, + { device: 62, delete: false }, + { device: 63, delete: true }, + { device: 65, delete: true } + ]; + }); + + it("includes all deleted devices", () => { + const manager = new DevicesManager(system, staging, actions); + const sids = manager.deletedDevices().map(d => d.sid) + .sort(); + expect(sids).toEqual([60, 63]); + }); +}); + +describe("deletedSystems", () => { + beforeEach(() => { + system = [ + { sid: 60, systems: ["Windows XP"] }, + { sid: 62, systems: ["Ubuntu"] }, + { + sid: 63, + systems: ["openSUSE Leap", "openSUSE Tumbleweed"], + partitionTable: { + partitions: [{ sid: 65 }, { sid: 66 }] + } + }, + { sid: 64 }, + { sid: 65, systems: ["openSUSE Leap"] }, + { sid: 66, systems: ["openSUSE Tumbleweed"] } + ]; + actions = [ + { device: 60, delete: true }, + // This device does not exist in system. + { device: 61, delete: true }, + { device: 62, delete: false }, + { device: 63, delete: true }, + { device: 65, delete: true }, + { device: 66, delete: true } + ]; + }); + + it("includes all deleted systems", () => { + const manager = new DevicesManager(system, staging, actions); + const systems = manager.deletedSystems(); + expect(systems.length).toEqual(3); + expect(systems).toContain("Windows XP"); + expect(systems).toContain("openSUSE Leap"); + expect(systems).toContain("openSUSE Tumbleweed"); + }); +}); diff --git a/web/src/components/storage/ProposalActionsSection.jsx b/web/src/components/storage/ProposalActionsDialog.jsx similarity index 54% rename from web/src/components/storage/ProposalActionsSection.jsx rename to web/src/components/storage/ProposalActionsDialog.jsx index 8d35563afd..053f9d852c 100644 --- a/web/src/components/storage/ProposalActionsSection.jsx +++ b/web/src/components/storage/ProposalActionsDialog.jsx @@ -20,21 +20,12 @@ */ import React, { useState } from "react"; -import { - List, - ListItem, - ExpandableSection, - Skeleton, -} from "@patternfly/react-core"; +import { List, ListItem, ExpandableSection, } from "@patternfly/react-core"; import { sprintf } from "sprintf-js"; - import { _, n_ } from "~/i18n"; -import { If, Section } from "~/components/core"; import { partition } from "~/utils"; +import { If, Popup } from "~/components/core"; -// TODO: would be nice adding an aria-description to these lists, but aria-description still in -// draft yet and aria-describedby should be used... which id not ideal right now -// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-description const ActionsList = ({ actions }) => { // Some actions (e.g., deleting a LV) are reported as several actions joined by a line break const actionItems = (action, id) => { @@ -53,15 +44,21 @@ const ActionsList = ({ actions }) => { }; /** - * Renders the list of actions to perform in the system + * Renders a dialog with the given list of actions * @component * * @param {object} props - * @param {object[]} [props.actions=[]] + * @param {object[]} [props.actions=[]] - The actions to perform in the system. + * @param {boolean} [props.isOpen=false] - Whether the dialog is visible or not. + * @param {function} props.onClose - Whether the dialog is visible or not. */ -const ProposalActions = ({ actions = [] }) => { +export default function ProposalActionsDialog({ actions = [], isOpen = false, onClose }) { const [isExpanded, setIsExpanded] = useState(false); + if (typeof onClose !== 'function') { + console.error("Missing ProposalActionsDialog#onClose callback"); + } + if (actions.length === 0) return null; const [generalActions, subvolActions] = partition(actions, a => !a.subvol); @@ -72,66 +69,32 @@ const ProposalActions = ({ actions = [] }) => { : sprintf(n_("Show %d subvolume action", "Show %d subvolume actions", subvolActions.length), subvolActions.length); return ( - <> - - {subvolActions.length > 0 && ( - setIsExpanded(!isExpanded)} - toggleText={toggleText} - className="expandable-actions" - > - - - )} - - ); -}; - -/** - * @todo Create a component for rendering a customized skeleton - */ -const ActionsSkeleton = () => { - return ( - <> - - - - - - - ); -}; - -/** - * Section with the actions to perform in the system - * @component - * - * @param {object} props - * @param {object[]} [props.actions=[]] - * @param {string[]} [props.errors=[]] - * @param {boolean} [props.isLoading=false] - Whether the section content should be rendered as loading - */ -export default function ProposalActionsSection({ actions = [], errors = [], isLoading = false }) { - if (isLoading) errors = []; - - return ( -
+ } - else={} + condition={subvolActions.length > 0} + then={ + setIsExpanded(!isExpanded)} + toggleText={toggleText} + className="expandable-actions" + > + + + } /> -
+ + {_("Close")} + + ); } diff --git a/web/src/components/storage/ProposalActionsDialog.test.jsx b/web/src/components/storage/ProposalActionsDialog.test.jsx new file mode 100644 index 0000000000..cde3a95c51 --- /dev/null +++ b/web/src/components/storage/ProposalActionsDialog.test.jsx @@ -0,0 +1,144 @@ +/* + * Copyright (c) [2022-2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within, waitForElementToBeRemoved } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { ProposalActionsDialog } from "~/components/storage"; + +const actions = [ + { text: 'Create GPT on /dev/vdc', subvol: false, delete: false }, + { text: 'Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition', subvol: false, delete: false }, + { text: 'Create encrypted partition /dev/vdc2 (29.99 GiB) as LVM physical volume', subvol: false, delete: false }, + { text: 'Create volume group system0 (29.98 GiB) with /dev/mapper/cr_vdc2 (29.99 GiB)', subvol: false, delete: false }, + { text: 'Create LVM logical volume /dev/system0/root (20.00 GiB) on volume group system0 for / with btrfs', subvol: false, delete: false }, +]; + +const subvolumeActions = [ + { text: 'Create subvolume @ on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/var on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/usr/local on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/srv on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/root on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/opt on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/home on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/boot/writable on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/boot/grub2/x86_64-efi on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, + { text: 'Create subvolume @/boot/grub2/i386-pc on /dev/system0/root (20.00 GiB)', subvol: true, delete: false } +]; + +const destructiveAction = { text: 'Delete ext4 on /dev/vdc', subvol: false, delete: true }; + +const onCloseFn = jest.fn(); + +it("renders nothing by default", () => { + const { container } = plainRender(); + expect(container).toBeEmptyDOMElement(); +}); + +it("renders nothing when isOpen=false", () => { + const { container } = plainRender( + + ); + expect(container).toBeEmptyDOMElement(); +}); + +describe("when isOpen", () => { + it("renders nothing if there are no actions", () => { + plainRender(); + + expect(screen.queryAllByText(/Delete/)).toEqual([]); + expect(screen.queryAllByText(/Create/)).toEqual([]); + expect(screen.queryAllByText(/Show/)).toEqual([]); + }); + + describe("and there are actions", () => { + it("renders a dialog with the list of actions", () => { + plainRender(); + + const dialog = screen.getByRole("dialog", { name: "Planned Actions" }); + const actionsList = within(dialog).getByRole("list"); + const actionsListItems = within(actionsList).getAllByRole("listitem"); + expect(actionsListItems.map(i => i.textContent)).toEqual(actions.map(a => a.text)); + }); + + it("triggers the onClose callback when user clicks the Close button", async () => { + const { user } = plainRender(); + const closeButton = screen.getByRole("button", { name: "Close" }); + + await user.click(closeButton); + + expect(onCloseFn).toHaveBeenCalled(); + }); + + describe("when there is a destructive action", () => { + it("emphasizes the action", () => { + plainRender( + + ); + + // https://stackoverflow.com/a/63080940 + const actionItems = screen.getAllByRole("listitem"); + const destructiveActionItem = actionItems.find(item => item.textContent === destructiveAction.text); + + expect(destructiveActionItem).toHaveClass("proposal-action--delete"); + }); + }); + + describe("when there are subvolume actions", () => { + it("does not render the subvolume actions", () => { + plainRender( + + ); + + // For now, we know that there are two lists and the subvolume list is the second one. + // The test could be simplified once we have aria-descriptions for the lists. + const [genericList, subvolList] = screen.getAllByRole("list", { hidden: true }); + expect(genericList).not.toBeNull(); + expect(subvolList).not.toBeNull(); + const subvolItems = within(subvolList).queryAllByRole("listitem"); + expect(subvolItems).toEqual([]); + }); + + it("renders the subvolume actions after clicking on 'show subvolumes'", async () => { + const { user } = plainRender( + + ); + + const link = screen.getByText(/Show.*subvolume actions/); + + expect(screen.getAllByRole("list").length).toEqual(1); + + await user.click(link); + + waitForElementToBeRemoved(link); + screen.getByText(/Hide.*subvolume actions/); + + // For now, we know that there are two lists and the subvolume list is the second one. + // The test could be simplified once we have aria-descriptions for the lists. + const [, subvolList] = screen.getAllByRole("list"); + const subvolItems = within(subvolList).getAllByRole("listitem"); + + expect(subvolItems.map(i => i.textContent)).toEqual(subvolumeActions.map(a => a.text)); + }); + }); + }); +}); diff --git a/web/src/components/storage/ProposalActionsSection.test.jsx b/web/src/components/storage/ProposalActionsSection.test.jsx deleted file mode 100644 index 9864391d0d..0000000000 --- a/web/src/components/storage/ProposalActionsSection.test.jsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen, within, waitForElementToBeRemoved } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import { ProposalActionsSection } from "~/components/storage"; - -jest.mock("@patternfly/react-core", () => { - const original = jest.requireActual("@patternfly/react-core"); - - return { - ...original, - Skeleton: () =>
PFSkeleton
- }; -}); - -const actions = [ - { text: 'Create GPT on /dev/vdc', subvol: false, delete: false }, - { text: 'Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition', subvol: false, delete: false }, - { text: 'Create encrypted partition /dev/vdc2 (29.99 GiB) as LVM physical volume', subvol: false, delete: false }, - { text: 'Create volume group system0 (29.98 GiB) with /dev/mapper/cr_vdc2 (29.99 GiB)', subvol: false, delete: false }, - { text: 'Create LVM logical volume /dev/system0/root (20.00 GiB) on volume group system0 for / with btrfs', subvol: false, delete: false }, -]; - -const subvolumeActions = [ - { text: 'Create subvolume @ on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/var on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/usr/local on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/srv on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/root on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/opt on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/home on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/boot/writable on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/boot/grub2/x86_64-efi on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/boot/grub2/i386-pc on /dev/system0/root (20.00 GiB)', subvol: true, delete: false } -]; - -const destructiveAction = { text: 'Delete ext4 on /dev/vdc', subvol: false, delete: true }; - -it("renders skeleton while loading", () => { - plainRender(); - - screen.getAllByText(/PFSkeleton/); -}); - -it("renders nothing when there is no actions", () => { - plainRender(); - - expect(screen.queryAllByText(/Delete/)).toEqual([]); - expect(screen.queryAllByText(/Create/)).toEqual([]); - expect(screen.queryAllByText(/Show/)).toEqual([]); -}); - -describe("when there are actions", () => { - it("renders an explanatory text", () => { - plainRender(); - - screen.getByText(/Actions to create/); - }); - - it("renders the list of actions", () => { - plainRender(); - - const actionsList = screen.getByRole("list"); - const actionsListItems = within(actionsList).getAllByRole("listitem"); - expect(actionsListItems.map(i => i.textContent)).toEqual(actions.map(a => a.text)); - }); - - describe("when there is a destructive action", () => { - it("emphasizes the action", () => { - plainRender(); - - // https://stackoverflow.com/a/63080940 - const actionItems = screen.getAllByRole("listitem"); - const destructiveActionItem = actionItems.find(item => item.textContent === destructiveAction.text); - - expect(destructiveActionItem).toHaveClass("proposal-action--delete"); - }); - }); - - describe("when there are subvolume actions", () => { - it("does not render the subvolume actions", () => { - plainRender(); - - // For now, we know that there are two lists and the subvolume list is the second one. - // The test could be simplified once we have aria-descriptions for the lists. - const [genericList, subvolList] = screen.getAllByRole("list", { hidden: true }); - expect(genericList).not.toBeNull(); - expect(subvolList).not.toBeNull(); - const subvolItems = within(subvolList).queryAllByRole("listitem"); - expect(subvolItems).toEqual([]); - }); - - it("renders the subvolume actions after clicking on 'show subvolumes'", async () => { - const { user } = plainRender( - - ); - - const link = screen.getByText(/Show.*subvolume actions/); - - expect(screen.getAllByRole("list").length).toEqual(1); - - await user.click(link); - - waitForElementToBeRemoved(link); - screen.getByText(/Hide.*subvolume actions/); - - // For now, we know that there are two lists and the subvolume list is the second one. - // The test could be simplified once we have aria-descriptions for the lists. - const [, subvolList] = screen.getAllByRole("list"); - const subvolItems = within(subvolList).getAllByRole("listitem"); - - expect(subvolItems.map(i => i.textContent)).toEqual(subvolumeActions.map(a => a.text)); - }); - }); -}); diff --git a/web/src/components/storage/ProposalDeviceSection.jsx b/web/src/components/storage/ProposalDeviceSection.jsx index df37eaf304..a39f4e333a 100644 --- a/web/src/components/storage/ProposalDeviceSection.jsx +++ b/web/src/components/storage/ProposalDeviceSection.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Form, @@ -101,7 +101,7 @@ const InstallationDeviceField = ({ isLoading = false, onChange = noop }) => { - const [device, setDevice] = useState(devices.find(d => d.name === current)); + const [device, setDevice] = useState(); const [isFormOpen, setIsFormOpen] = useState(false); const openForm = () => setIsFormOpen(true); @@ -114,6 +114,10 @@ const InstallationDeviceField = ({ onChange(selectedDevice); }; + useEffect(() => { + setDevice(devices.find(d => d.name === current)); + }, [current, devices, setDevice]); + /** * Renders a button that allows changing selected device * @@ -292,7 +296,7 @@ const LVMField = ({ isLoading = false, onChange: onChangeProp = noop }) => { - const [isChecked, setIsChecked] = useState(isCheckedProp); + const [isChecked, setIsChecked] = useState(); const [isFormOpen, setIsFormOpen] = useState(false); const [isFormValid, setIsFormValid] = useState(true); @@ -312,6 +316,10 @@ const LVMField = ({ onChangeProp({ vgDevices }); }; + useEffect(() => { + setIsChecked(isCheckedProp); + }, [isCheckedProp, setIsChecked]); + const description = _("Configuration of the system volume group. All the file systems will be \ created in a logical volume of the system volume group."); diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index c3d4753623..f17568c08b 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -26,11 +26,11 @@ import { useInstallerClient } from "~/context/installer"; import { toValidationError, useCancellablePromise } from "~/utils"; import { Page } from "~/components/core"; import { - ProposalActionsSection, ProposalPageMenu, - ProposalSettingsSection, ProposalDeviceSection, - ProposalTransactionalInfo + ProposalTransactionalInfo, + ProposalSettingsSection, + ProposalResultSection } from "~/components/storage"; import { IDLE } from "~/client/status"; @@ -216,40 +216,33 @@ export default function ProposalPage() { calculate(newSettings).catch(console.error); }; - const PageContent = () => { - return ( - <> - - - - - - ); - }; - return ( - // TRANSLATORS: page title + // TRANSLATORS: Storage page title - + + + + ); } diff --git a/web/src/components/storage/ProposalPage.test.jsx b/web/src/components/storage/ProposalPage.test.jsx index e47906c18b..7bd4f44a31 100644 --- a/web/src/components/storage/ProposalPage.test.jsx +++ b/web/src/components/storage/ProposalPage.test.jsx @@ -84,7 +84,7 @@ const storageMock = { getProductMountPoints: jest.fn().mockResolvedValue([]), getResult: jest.fn().mockResolvedValue(undefined), defaultVolume: jest.fn(mountPath => Promise.resolve({ mountPath })), - calculate: jest.fn().mockResolvedValue(0) + calculate: jest.fn().mockResolvedValue(0), }, system: { getDevices: jest.fn().mockResolvedValue([vda, vdb]) @@ -129,12 +129,12 @@ it("loads the proposal data", async () => { await screen.findByText(/\/dev\/vda/); }); -it("renders the device, settings and actions sections", async () => { +it("renders the device, settings, find space and result sections", async () => { installerRender(); await screen.findByText(/Device/); await screen.findByText(/Settings/); - await screen.findByText(/Planned Actions/); + await screen.findByText(/Result/); }); describe("when the storage devices become deprecated", () => { diff --git a/web/src/components/storage/ProposalResultSection.jsx b/web/src/components/storage/ProposalResultSection.jsx new file mode 100644 index 0000000000..3759c25740 --- /dev/null +++ b/web/src/components/storage/ProposalResultSection.jsx @@ -0,0 +1,296 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +// @ts-check + +import React, { useState } from "react"; +import { Button, Skeleton } from "@patternfly/react-core"; +import { sprintf } from "sprintf-js"; +import { _, n_ } from "~/i18n"; +import { deviceChildren, deviceSize } from "~/components/storage/utils"; +import DevicesManager from "~/components/storage/DevicesManager"; +import { If, Section, Reminder, Tag, TreeTable } from "~/components/core"; +import { ProposalActionsDialog } from "~/components/storage"; + +/** + * @typedef {import ("~/client/storage").Action} Action + * @typedef {import ("~/client/storage").StorageDevice} StorageDevice + * @typedef {import("~/client/mixins").ValidationError} ValidationError + */ + +/** + * Renders information about planned actions, allowing to check all of them and warning with a + * summary about the deletion ones, if any. + * @component + * + * @param {object} props + * @param {Action[]} props.actions + * @param {string[]} props.systems + */ +const DeletionsInfo = ({ actions, systems }) => { + const total = actions.length; + + if (total === 0) return; + + // TRANSLATORS: %d will be replaced by the amount of destructive actions + const warningTitle = sprintf(n_( + "There is %d destructive action planned", + "There are %d destructive actions planned", + total + ), total); + + // FIXME: Use the Intl.ListFormat instead of the `join(", ")` used below. + // Most probably, a `listFormat` or similar wrapper should live in src/i18n.js or so. + // Read https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat + return ( + + 0} + then={ +

+ { + // TRANSLATORS: This is part of a sentence to hint the user about affected systems. + // Eg. "Affecting Windows 11, openSUSE Leap 15, and Ubuntu 22.04" + } + {_("Affecting")} {systems.join(", ")} +

+ } + /> +
+ ); +}; + +/** + * Renders needed UI elements to allow user check the proposal planned actions + * @component + * + * @param {object} props + * @param {Action[]} props.actions + */ +const ActionsInfo = ({ actions }) => { + const [showActions, setShowActions] = useState(false); + const onOpen = () => setShowActions(true); + const onClose = () => setShowActions(false); + + return ( + <> + + + + ); +}; + +/** + * Renders a TreeTable rendering the devices proposal result. + * @component + * + * @param {object} props + * @param {DevicesManager} props.devicesManager + */ +const DevicesTreeTable = ({ devicesManager }) => { + const renderDeviceName = (item) => { + let name = item.sid && item.name; + // NOTE: returning a fragment here to avoid a weird React complaint when using a PF/Table + + // treeRow props. + if (!name) return <>; + + if (["partition", "lvmLv"].includes(item.type)) + name = name.split("/").pop(); + + return ( +
+ {name} +
+ ); + }; + + const renderNewLabel = (item) => { + if (!item.sid) return; + + // FIXME New PVs over a disk is not detected as new. + if (!devicesManager.existInSystem(item) || devicesManager.hasNewFilesystem(item)) + return {_("New")}; + }; + + const renderContent = (item) => { + if (!item.sid) + return _("Unused space"); + if (!item.partitionTable && item.systems?.length > 0) + return item.systems.join(", "); + + return item.description; + }; + + const renderFilesystemLabel = (item) => { + const label = item.filesystem?.label; + if (label) return {label}; + }; + + const renderPTableType = (item) => { + // TODO: Create a map for partition table types and use an here. + const type = item.partitionTable?.type; + if (type) return {type.toUpperCase()}; + }; + + const renderDetails = (item) => { + return ( + <> +
{renderNewLabel(item)}
+
{renderContent(item)} {renderFilesystemLabel(item)} {renderPTableType(item)}
+ + ); + }; + + const renderResizedLabel = (item) => { + if (!item.sid || !devicesManager.isShrunk(item)) return; + + return ( + + { + // TRANSLATORS: a label to show how much a device was resized. %s will be + // replaced with such a size, including the unit. E.g., 508 MiB + sprintf(_("Resized %s"), deviceSize(devicesManager.shrinkSize(item))) + } + + ); + }; + + const renderSize = (item) => { + return ( +
+ {renderResizedLabel(item)} + {deviceSize(item.size)} +
+ ); + }; + + const renderMountPoint = (item) => item.sid && {item.filesystem?.mountPath}; + + return ( + deviceChildren(d)} + rowClassNames={(item) => { + if (!item.sid) return "dimmed-row"; + }} + className="proposal-result" + /> + ); +}; + +/** + * @todo Create a component for rendering a customized skeleton + */ +const ResultSkeleton = () => { + return ( + <> + + + + + ); +}; + +/** + * Content of the section. + * @component + * + * @param {object} props + * @param {StorageDevice[]} props.system + * @param {StorageDevice[]} props.staging + * @param {Action[]} props.actions + * @param {ValidationError[]} props.errors + */ +const SectionContent = ({ system, staging, actions, errors }) => { + if (errors.length) return; + + const devicesManager = new DevicesManager(system, staging, actions); + + return ( + <> + a.delete && !a.subvol)} + systems={devicesManager.deletedSystems()} + /> + + + + ); +}; + +/** + * Section holding the proposal result and actions to perform in the system + * @component + * + * @param {object} props + * @param {StorageDevice[]} [props.system=[]] + * @param {StorageDevice[]} [props.staging=[]] + * @param {Action[]} [props.actions=[]] + * @param {ValidationError[]} [props.errors=[]] - Validation errors + * @param {boolean} [props.isLoading=false] - Whether the section content should be rendered as loading + */ +export default function ProposalResultSection({ + system = [], + staging = [], + actions = [], + errors = [], + isLoading = false +}) { + if (isLoading) errors = []; + const totalActions = actions.length; + + // TRANSLATORS: The description for the Result section in storage proposal + // page. %d will be replaced by the number of proposal actions. + const description = sprintf(n_( + "During installation, %d action will be performed to configure the system as displayed below", + "During installation, %d actions will be performed to configure the system as displayed below", + totalActions + ), totalActions); + + return ( +
+ } + else={ + + } + /> +
+ ); +} diff --git a/web/src/components/storage/ProposalResultSection.test.jsx b/web/src/components/storage/ProposalResultSection.test.jsx new file mode 100644 index 0000000000..7e96795516 --- /dev/null +++ b/web/src/components/storage/ProposalResultSection.test.jsx @@ -0,0 +1,133 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { ProposalResultSection } from "~/components/storage"; +import { devices, actions } from "./test-data/full-result-example"; + +const errorMessage = "Something went wrong, proposal not possible"; +const errors = [{ severity: 0, message: errorMessage }]; +const defaultProps = { system: devices.system, staging: devices.staging, actions }; + +describe("ProposalResultSection", () => { + describe("when there are errors (proposal was not possible)", () => { + it("renders given errors", () => { + plainRender(); + expect(screen.queryByText(errorMessage)).toBeInTheDocument(); + }); + + it("does not render a warning for delete actions", () => { + plainRender(); + expect(screen.queryByText(/Warning alert:/)).toBeNull(); + }); + + it("does not render a treegrid node", () => { + plainRender(); + expect(screen.queryByRole("treegrid")).toBeNull(); + }); + + it("does not render the link for opening the planned actions dialog", () => { + plainRender(); + expect(screen.queryByRole("button", { name: /planned actions/ })).toBeNull(); + }); + }); + + describe("when there are no errors (proposal was possible)", () => { + it("does not render a warning when there are not delete actions", () => { + const props = { + ...defaultProps, + actions: defaultProps.actions.filter(a => !a.delete) + }; + + plainRender(); + expect(screen.queryByText(/Warning alert:/)).toBeNull(); + }); + + it("renders a reminder when there are delete actions", () => { + plainRender(); + const reminder = screen.getByRole("status"); + within(reminder).getByText(/4 destructive/); + }); + + it("renders the affected systems in the deletion reminder, if any", () => { + // NOTE: simulate the deletion of vdc2 (sid: 79) for checking that + // affected systems are rendered in the warning summary + const props = { + ...defaultProps, + actions: [{ device: 79, delete: true }] + }; + + plainRender(); + // FIXME: below line reveals that warning wrapper deserves a role or + // something + const reminder = screen.getByRole("status"); + within(reminder).getByText(/openSUSE/); + }); + + it("renders a treegrid including all relevant information about final result", () => { + plainRender(); + const treegrid = screen.getByRole("treegrid"); + /** + * Expected rows for full-result-example + * -------------------------------------------------- + * "/dev/vdc Disk GPT 30 GiB" + * "vdc1 New BIOS Boot Partition 8 MiB" + * "vdc3 swap New Swap Partition 1.5 GiB" + * "Unused space 3.49 GiB" + * "vdc2 openSUSE Leap 15.2, Fedora 10.30 5 GiB" + * "Unused space 1 GiB" + * "vdc4 Linux Resized 514 MiB 1.5 GiB" + * "vdc5 / New Btrfs Partition 17.5 GiB" + * + * Device Mount point Details Size + * ------------------------------------------------------------------------- + * /dev/vdc Disk GPT 30 GiB + * vdc1 New BIOS Boot Partition 8 MiB + * vdc3 swap New Swap Partition 1.5 GiB + * Unused space 3.49 GiB + * vdc2 openSUSE Leap 15.2, Fedora 10.30 5 GiB + * Unused space 1 GiB + * vdc4 Linux Resized 514 MiB 1.5 GiB + * vdc5 / New Btrfs Partition 17.5 GiB + * ------------------------------------------------------------------------- + */ + within(treegrid).getByRole("row", { name: "/dev/vdc Disk GPT 30 GiB" }); + within(treegrid).getByRole("row", { name: "vdc1 New BIOS Boot Partition 8 MiB" }); + within(treegrid).getByRole("row", { name: "vdc3 swap New Swap Partition 1.5 GiB" }); + within(treegrid).getByRole("row", { name: "Unused space 3.49 GiB" }); + within(treegrid).getByRole("row", { name: "vdc2 openSUSE Leap 15.2, Fedora 10.30 5 GiB" }); + within(treegrid).getByRole("row", { name: "Unused space 1 GiB" }); + within(treegrid).getByRole("row", { name: "vdc4 Linux Resized 514 MiB 1.5 GiB" }); + within(treegrid).getByRole("row", { name: "vdc5 / New Btrfs Partition 17.5 GiB" }); + }); + + it("renders a button for opening the planned actions dialog", async () => { + const { user } = plainRender(); + const button = screen.getByRole("button", { name: /planned actions/ }); + + await user.click(button); + + screen.getByRole("dialog", { name: "Planned Actions" }); + }); + }); +}); diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.js index 8a99b0180d..063d513dab 100644 --- a/web/src/components/storage/index.js +++ b/web/src/components/storage/index.js @@ -24,8 +24,9 @@ export { default as ProposalPageMenu } from "./ProposalPageMenu"; export { default as ProposalSettingsSection } from "./ProposalSettingsSection"; export { default as ProposalSpacePolicyField } from "./ProposalSpacePolicyField"; export { default as ProposalDeviceSection } from "./ProposalDeviceSection"; -export { default as ProposalActionsSection } from "./ProposalActionsSection"; export { default as ProposalTransactionalInfo } from "./ProposalTransactionalInfo"; +export { default as ProposalActionsDialog } from "./ProposalActionsDialog"; +export { default as ProposalResultSection } from "./ProposalResultSection"; export { default as ProposalVolumes } from "./ProposalVolumes"; export { default as DASDPage } from "./DASDPage"; export { default as DASDTable } from "./DASDTable"; diff --git a/web/src/components/storage/test-data/full-result-example.js b/web/src/components/storage/test-data/full-result-example.js new file mode 100644 index 0000000000..9013994320 --- /dev/null +++ b/web/src/components/storage/test-data/full-result-example.js @@ -0,0 +1,1446 @@ +export const settings = { + "bootDevice": "/dev/vdc", + "lvm": false, + "spacePolicy": "custom", + "spaceActions": [ + { + "device": "/dev/vdc3", + "action": "force_delete" + }, + { + "device": "/dev/vdc4", + "action": "resize" + }, + { + "device": "/dev/vdc1", + "action": "force_delete" + } + ], + "systemVGDevices": [], + "encryptionPassword": "", + "encryptionMethod": "luks2", + "volumes": [ + { + "mountPath": "/", + "fsType": "Btrfs", + "minSize": 18790481920, + "autoSize": true, + "snapshots": true, + "transactional": false, + "outline": { + "required": true, + "fsTypes": [ + "Btrfs", + "Ext2", + "Ext3", + "Ext4", + "XFS" + ], + "supportAutoSize": true, + "snapshotsConfigurable": true, + "snapshotsAffectSizes": true, + "sizeRelevantVolumes": [ + "/home" + ] + } + }, + { + "mountPath": "swap", + "fsType": "Swap", + "minSize": 1610612736, + "maxSize": 1610612736, + "autoSize": false, + "snapshots": false, + "transactional": false, + "outline": { + "required": false, + "fsTypes": [ + "Swap" + ], + "supportAutoSize": false, + "snapshotsConfigurable": false, + "snapshotsAffectSizes": false, + "sizeRelevantVolumes": [] + } + } + ], + "installationDevices": [ + { + "sid": 70, + "name": "/dev/vdc", + "description": "Disk", + "isDrive": true, + "type": "disk", + "vendor": "", + "model": "Disk", + "driver": [ + "virtio-pci", + "virtio_blk" + ], + "bus": "None", + "busId": "", + "transport": "unknown", + "sdCard": false, + "dellBOSS": false, + "active": true, + "encrypted": false, + "start": 0, + "size": 32212254720, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0" + ], + "partitionTable": { + "type": "gpt", + "partitions": [ + { + "sid": 78, + "name": "/dev/vdc1", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 5368709120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part1" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + }, + { + "sid": 79, + "name": "/dev/vdc2", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 10487808, + "size": 5368709120, + "recoverableSize": 0, + "systems": [ + "openSUSE Leap 15.2", + "Fedora 10.30" + ], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + }, + { + "sid": 80, + "name": "/dev/vdc3", + "description": "XFS Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 20973568, + "size": 1073741824, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part3" + ], + "isEFI": false, + "filesystem": { + "sid": 92, + "type": "xfs" + } + }, + { + "sid": 81, + "name": "/dev/vdc4", + "description": "Linux", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 23070720, + "size": 2147483648, + "recoverableSize": 2147483136, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part4" + ], + "isEFI": false + } + ], + "unpartitionedSize": 18253611008, + "unusedSlots": [ + { + "start": 27265024, + "size": 18252545536 + } + ] + } + } + ] +}; + +export const devices = { + "system": [ + { + "sid": 71, + "name": "/dev/vda", + "description": "Disk", + "isDrive": true, + "type": "disk", + "vendor": "", + "model": "Disk", + "driver": [ + "virtio-pci", + "virtio_blk" + ], + "bus": "None", + "busId": "", + "transport": "unknown", + "sdCard": false, + "dellBOSS": false, + "active": true, + "encrypted": false, + "start": 0, + "size": 53687091200, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0" + ], + "partitionTable": { + "type": "gpt", + "partitions": [ + { + "sid": 83, + "name": "/dev/vda1", + "description": "BIOS Boot Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 8388608, + "recoverableSize": 8388096, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part1" + ], + "isEFI": false + }, + { + "sid": 84, + "name": "/dev/vda2", + "description": "PV of system", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 53677637120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "physical_volume", + "deviceNames": [ + "/dev/system" + ] + } + } + ], + "unpartitionedSize": 1065472, + "unusedSlots": [] + } + }, + { + "sid": 69, + "name": "/dev/vdb", + "description": "Ext4 Disk", + "isDrive": true, + "type": "disk", + "vendor": "", + "model": "Disk", + "driver": [ + "virtio-pci", + "virtio_blk" + ], + "bus": "None", + "busId": "", + "transport": "unknown", + "sdCard": false, + "dellBOSS": false, + "active": true, + "encrypted": false, + "start": 0, + "size": 5368709120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:08:00.0" + ], + "filesystem": { + "sid": 87, + "type": "ext4" + } + }, + { + "sid": 70, + "name": "/dev/vdc", + "description": "Disk", + "isDrive": true, + "type": "disk", + "vendor": "", + "model": "Disk", + "driver": [ + "virtio-pci", + "virtio_blk" + ], + "bus": "None", + "busId": "", + "transport": "unknown", + "sdCard": false, + "dellBOSS": false, + "active": true, + "encrypted": false, + "start": 0, + "size": 32212254720, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0" + ], + "partitionTable": { + "type": "gpt", + "partitions": [ + { + "sid": 78, + "name": "/dev/vdc1", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 5368709120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part1" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + }, + { + "sid": 79, + "name": "/dev/vdc2", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 10487808, + "size": 5368709120, + "recoverableSize": 0, + "systems": [ + "openSUSE Leap 15.2", + "Fedora 10.30" + ], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + }, + { + "sid": 80, + "name": "/dev/vdc3", + "description": "XFS Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 20973568, + "size": 1073741824, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part3" + ], + "isEFI": false, + "filesystem": { + "sid": 92, + "type": "xfs" + } + }, + { + "sid": 81, + "name": "/dev/vdc4", + "description": "Linux", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 23070720, + "size": 2147483648, + "recoverableSize": 2147483136, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part4" + ], + "isEFI": false + } + ], + "unpartitionedSize": 18253611008, + "unusedSlots": [ + { + "start": 27265024, + "size": 18252545536 + } + ] + } + }, + { + "sid": 72, + "name": "/dev/md0", + "description": "Disk", + "isDrive": false, + "type": "md", + "level": "raid0", + "uuid": "644aeee1:5f5b946a:4da99758:3f85b3ea", + "devices": [ + { + "sid": 78, + "name": "/dev/vdc1", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 5368709120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part1" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + }, + { + "sid": 79, + "name": "/dev/vdc2", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 10487808, + "size": 5368709120, + "recoverableSize": 0, + "systems": [ + "openSUSE Leap 15.2", + "Fedora 10.30" + ], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + } + ], + "active": true, + "encrypted": false, + "start": 0, + "size": 10737287168, + "recoverableSize": 0, + "systems": [], + "udevIds": [ + "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea" + ], + "udevPaths": [], + "partitionTable": { + "type": "gpt", + "partitions": [ + { + "sid": 86, + "name": "/dev/md0p1", + "description": "Ext4 Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 2147483648, + "recoverableSize": 2040147968, + "systems": [], + "udevIds": [ + "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1" + ], + "udevPaths": [], + "isEFI": false, + "filesystem": { + "sid": 93, + "type": "ext4" + } + } + ], + "unpartitionedSize": 8589803520, + "unusedSlots": [ + { + "start": 4196352, + "size": 8588738048 + } + ] + } + }, + { + "sid": 73, + "name": "/dev/system", + "description": "LVM", + "isDrive": false, + "type": "lvmVg", + "size": 53674508288, + "physicalVolumes": [ + { + "sid": 84, + "name": "/dev/vda2", + "description": "PV of system", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 53677637120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "physical_volume", + "deviceNames": [ + "/dev/system" + ] + } + } + ], + "logicalVolumes": [ + { + "sid": 75, + "name": "/dev/system/root", + "description": "Ext4 LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 51527024640, + "recoverableSize": 30647779328, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 88, + "type": "ext4", + "mountPath": "/" + } + }, + { + "sid": 76, + "name": "/dev/system/swap", + "description": "Swap LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 2147483648, + "recoverableSize": 2143289344, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 90, + "type": "swap", + "mountPath": "swap" + } + } + ] + }, + { + "sid": 75, + "name": "/dev/system/root", + "description": "Ext4 LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 51527024640, + "recoverableSize": 30647779328, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 88, + "type": "ext4", + "mountPath": "/" + } + }, + { + "sid": 76, + "name": "/dev/system/swap", + "description": "Swap LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 2147483648, + "recoverableSize": 2143289344, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 90, + "type": "swap", + "mountPath": "swap" + } + }, + { + "sid": 83, + "name": "/dev/vda1", + "description": "BIOS Boot Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 8388608, + "recoverableSize": 8388096, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part1" + ], + "isEFI": false + }, + { + "sid": 84, + "name": "/dev/vda2", + "description": "PV of system", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 53677637120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "physical_volume", + "deviceNames": [ + "/dev/system" + ] + } + }, + { + "sid": 78, + "name": "/dev/vdc1", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 5368709120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part1" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + }, + { + "sid": 79, + "name": "/dev/vdc2", + "description": "Part of md0", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 10487808, + "size": 5368709120, + "recoverableSize": 0, + "systems": [ + "openSUSE Leap 15.2", + "Fedora 10.30" + ], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "md_device", + "deviceNames": [ + "/dev/md0" + ] + } + }, + { + "sid": 80, + "name": "/dev/vdc3", + "description": "XFS Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 20973568, + "size": 1073741824, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part3" + ], + "isEFI": false, + "filesystem": { + "sid": 92, + "type": "xfs" + } + }, + { + "sid": 81, + "name": "/dev/vdc4", + "description": "Linux", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 23070720, + "size": 2147483648, + "recoverableSize": 2147483136, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part4" + ], + "isEFI": false + }, + { + "sid": 86, + "name": "/dev/md0p1", + "description": "Ext4 Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 2147483648, + "recoverableSize": 2040147968, + "systems": [], + "udevIds": [ + "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1" + ], + "udevPaths": [], + "isEFI": false, + "filesystem": { + "sid": 93, + "type": "ext4" + } + } + ], + "staging": [ + { + "sid": 71, + "name": "/dev/vda", + "description": "Disk", + "isDrive": true, + "type": "disk", + "vendor": "", + "model": "Disk", + "driver": [ + "virtio-pci", + "virtio_blk" + ], + "bus": "None", + "busId": "", + "transport": "unknown", + "sdCard": false, + "dellBOSS": false, + "active": true, + "encrypted": false, + "start": 0, + "size": 53687091200, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0" + ], + "partitionTable": { + "type": "gpt", + "partitions": [ + { + "sid": 83, + "name": "/dev/vda1", + "description": "BIOS Boot Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 8388608, + "recoverableSize": 8388096, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part1" + ], + "isEFI": false + }, + { + "sid": 84, + "name": "/dev/vda2", + "description": "PV of system", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 53677637120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "physical_volume", + "deviceNames": [ + "/dev/system" + ] + } + } + ], + "unpartitionedSize": 1065472, + "unusedSlots": [] + } + }, + { + "sid": 69, + "name": "/dev/vdb", + "description": "Ext4 Disk", + "isDrive": true, + "type": "disk", + "vendor": "", + "model": "Disk", + "driver": [ + "virtio-pci", + "virtio_blk" + ], + "bus": "None", + "busId": "", + "transport": "unknown", + "sdCard": false, + "dellBOSS": false, + "active": true, + "encrypted": false, + "start": 0, + "size": 5368709120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:08:00.0" + ], + "filesystem": { + "sid": 87, + "type": "ext4" + } + }, + { + "sid": 70, + "name": "/dev/vdc", + "description": "Disk", + "isDrive": true, + "type": "disk", + "vendor": "", + "model": "Disk", + "driver": [ + "virtio-pci", + "virtio_blk" + ], + "bus": "None", + "busId": "", + "transport": "unknown", + "sdCard": false, + "dellBOSS": false, + "active": true, + "encrypted": false, + "start": 0, + "size": 32212254720, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0" + ], + "partitionTable": { + "type": "gpt", + "partitions": [ + { + "sid": 79, + "name": "/dev/vdc2", + "description": "Linux RAID", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 10487808, + "size": 5368709120, + "recoverableSize": 5368708608, + "systems": [ + "openSUSE Leap 15.2", + "Fedora 10.30" + ], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part2" + ], + "isEFI": false + }, + { + "sid": 81, + "name": "/dev/vdc4", + "description": "Linux", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 23070720, + "size": 1608515584, + "recoverableSize": 1608515072, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part4" + ], + "isEFI": false + }, + { + "sid": 459, + "name": "/dev/vdc1", + "description": "BIOS Boot Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 8388608, + "recoverableSize": 8388096, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part1" + ], + "isEFI": false + }, + { + "sid": 460, + "name": "/dev/vdc3", + "description": "Swap Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 1610612736, + "recoverableSize": 1610571776, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part3" + ], + "isEFI": false, + "filesystem": { + "sid": 461, + "type": "swap", + "mountPath": "swap" + } + }, + { + "sid": 463, + "name": "/dev/vdc5", + "description": "Btrfs Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 26212352, + "size": 18791513600, + "recoverableSize": 18523078144, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part5" + ], + "isEFI": false, + "filesystem": { + "sid": 464, + "type": "btrfs", + "mountPath": "/" + } + } + ], + "unpartitionedSize": 4824515072, + "unusedSlots": [ + { + "start": 3164160, + "size": 3749707776 + }, + { + "start": 20973568, + "size": 1073741824 + } + ] + } + }, + { + "sid": 73, + "name": "/dev/system", + "description": "LVM", + "isDrive": false, + "type": "lvmVg", + "size": 53674508288, + "physicalVolumes": [ + { + "sid": 84, + "name": "/dev/vda2", + "description": "PV of system", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 53677637120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "physical_volume", + "deviceNames": [ + "/dev/system" + ] + } + } + ], + "logicalVolumes": [ + { + "sid": 75, + "name": "/dev/system/root", + "description": "Ext4 LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 51527024640, + "recoverableSize": 30647779328, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 88, + "type": "ext4", + "mountPath": "/" + } + }, + { + "sid": 76, + "name": "/dev/system/swap", + "description": "Swap LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 2147483648, + "recoverableSize": 2143289344, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 90, + "type": "swap", + "mountPath": "swap" + } + } + ] + }, + { + "sid": 75, + "name": "/dev/system/root", + "description": "Ext4 LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 51527024640, + "recoverableSize": 30647779328, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 88, + "type": "ext4", + "mountPath": "/" + } + }, + { + "sid": 76, + "name": "/dev/system/swap", + "description": "Swap LV", + "isDrive": false, + "type": "lvmLv", + "active": true, + "encrypted": false, + "start": 0, + "size": 2147483648, + "recoverableSize": 2143289344, + "systems": [], + "udevIds": [], + "udevPaths": [], + "filesystem": { + "sid": 90, + "type": "swap", + "mountPath": "swap" + } + }, + { + "sid": 83, + "name": "/dev/vda1", + "description": "BIOS Boot Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 8388608, + "recoverableSize": 8388096, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part1" + ], + "isEFI": false + }, + { + "sid": 84, + "name": "/dev/vda2", + "description": "PV of system", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 53677637120, + "recoverableSize": 0, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:04:00.0-part2" + ], + "isEFI": false, + "component": { + "type": "physical_volume", + "deviceNames": [ + "/dev/system" + ] + } + }, + { + "sid": 79, + "name": "/dev/vdc2", + "description": "Linux RAID", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 10487808, + "size": 5368709120, + "recoverableSize": 5368708608, + "systems": [ + "openSUSE Leap 15.2", + "Fedora 10.30" + ], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part2" + ], + "isEFI": false + }, + { + "sid": 81, + "name": "/dev/vdc4", + "description": "Linux", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 23070720, + "size": 1608515584, + "recoverableSize": 1608515072, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part4" + ], + "isEFI": false + }, + { + "sid": 459, + "name": "/dev/vdc1", + "description": "BIOS Boot Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 2048, + "size": 8388608, + "recoverableSize": 8388096, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part1" + ], + "isEFI": false + }, + { + "sid": 460, + "name": "/dev/vdc3", + "description": "Swap Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 18432, + "size": 1610612736, + "recoverableSize": 1610571776, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part3" + ], + "isEFI": false, + "filesystem": { + "sid": 461, + "type": "swap", + "mountPath": "swap" + } + }, + { + "sid": 463, + "name": "/dev/vdc5", + "description": "Btrfs Partition", + "isDrive": false, + "type": "partition", + "active": true, + "encrypted": false, + "start": 26212352, + "size": 18791513600, + "recoverableSize": 18523078144, + "systems": [], + "udevIds": [], + "udevPaths": [ + "pci-0000:09:00.0-part5" + ], + "isEFI": false, + "filesystem": { + "sid": 464, + "type": "btrfs", + "mountPath": "/" + } + } + ] +}; + +export const actions = [ + { + "device": 86, + "text": "Delete partition /dev/md0p1 (2.00 GiB)", + "subvol": false, + "delete": true + }, + { + "device": 72, + "text": "Delete RAID0 /dev/md0 (10.00 GiB)", + "subvol": false, + "delete": true + }, + { + "device": 80, + "text": "Delete partition /dev/vdc3 (1.00 GiB)", + "subvol": false, + "delete": true + }, + { + "device": 78, + "text": "Delete partition /dev/vdc1 (5.00 GiB)", + "subvol": false, + "delete": true + }, + { + "device": 81, + "text": "Shrink partition /dev/vdc4 from 2.00 GiB to 1.50 GiB", + "subvol": false, + "delete": false + }, + { + "device": 459, + "text": "Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition", + "subvol": false, + "delete": false + }, + { + "device": 460, + "text": "Create partition /dev/vdc3 (1.50 GiB) for swap", + "subvol": false, + "delete": false + }, + { + "device": 463, + "text": "Create partition /dev/vdc5 (17.50 GiB) for / with btrfs", + "subvol": false, + "delete": false + }, + { + "device": 467, + "text": "Create subvolume @ on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 482, + "text": "Create subvolume @/boot/grub2/x86_64-efi on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 480, + "text": "Create subvolume @/boot/grub2/i386-pc on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 478, + "text": "Create subvolume @/var on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 476, + "text": "Create subvolume @/usr/local on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 474, + "text": "Create subvolume @/srv on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 472, + "text": "Create subvolume @/root on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 470, + "text": "Create subvolume @/opt on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + }, + { + "device": 468, + "text": "Create subvolume @/home on /dev/vdc5 (17.50 GiB)", + "subvol": true, + "delete": false + } +]; diff --git a/web/src/components/storage/utils.js b/web/src/components/storage/utils.js index 082756b08c..7817acd4e1 100644 --- a/web/src/components/storage/utils.js +++ b/web/src/components/storage/utils.js @@ -19,6 +19,7 @@ * find current contact information at www.suse.com. */ +// @ts-check // cspell:ignore xbytes import xbytes from "xbytes"; @@ -27,7 +28,8 @@ import { N_ } from "~/i18n"; /** * @typedef {import ("~/client/storage").Volume} Volume - * @typedef {import ("~/clients/storage").StorageDevice} StorageDevice + * @typedef {import ("~/client/storage").StorageDevice} StorageDevice + * @typedef {import ("~/client/storage").PartitionSlot} PartitionSlot */ /** @@ -121,7 +123,7 @@ const deviceSize = (size) => { const parseToBytes = (size) => { if (!size || size === undefined || size === "") return 0; - const value = xbytes.parseSize(size, { iec: true }) || parseInt(size); + const value = xbytes.parseSize(size.toString(), { iec: true }) || parseInt(size.toString()); // Avoid decimals resulting from the conversion. D-Bus iface only accepts integer return Math.trunc(value); @@ -140,6 +142,33 @@ const deviceLabel = (device) => { return size ? `${name}, ${deviceSize(size)}` : name; }; +/** + * Sorted list of children devices (i.e., partitions and unused slots or logical volumes). + * @function + * + * @note This method could be directly provided by the device object. For now, the method is kept + * here because the elements considered as children (e.g., partitions + unused slots) is not a + * semantic storage concept but a helper for UI components. + * + * @param {StorageDevice} device + * @returns {(StorageDevice|PartitionSlot)[]} + */ +const deviceChildren = (device) => { + const partitionTableChildren = (partitionTable) => { + const { partitions, unusedSlots } = partitionTable; + const children = partitions.concat(unusedSlots); + return children.sort((a, b) => a.start < b.start ? -1 : 1); + }; + + const lvmVgChildren = (lvmVg) => { + return lvmVg.logicalVolumes.sort((a, b) => a.name < b.name ? -1 : 1); + }; + + if (device.partitionTable) return partitionTableChildren(device.partitionTable); + if (device.type === "lvmVg") return lvmVgChildren(device); + return []; +}; + /** * Checks if volume uses given fs. This method works same as in backend * case insensitive. @@ -193,6 +222,7 @@ export { SIZE_METHODS, SIZE_UNITS, deviceLabel, + deviceChildren, deviceSize, parseToBytes, splitSize, diff --git a/web/src/components/storage/utils.test.js b/web/src/components/storage/utils.test.js index e5eb3cf0aa..c144b123a0 100644 --- a/web/src/components/storage/utils.test.js +++ b/web/src/components/storage/utils.test.js @@ -22,6 +22,7 @@ import { deviceSize, deviceLabel, + deviceChildren, parseToBytes, splitSize, hasFS, @@ -49,6 +50,66 @@ describe("deviceLabel", () => { }); }); +describe("deviceChildren", () => { + let device; + + describe("if the device has partition table", () => { + beforeEach(() => { + device = { + sid: 60, + partitionTable: { + partitions: [ + { sid: 61 }, + { sid: 62 }, + ], + unusedSlots: [ + { start: 1, size: 1024 }, + { start: 2345, size: 512 } + ] + } + }; + }); + + it("returns the partitions and unused slots", () => { + const children = deviceChildren(device); + expect(children.length).toEqual(4); + device.partitionTable.partitions.forEach(p => expect(children).toContainEqual(p)); + device.partitionTable.unusedSlots.forEach(s => expect(children).toContainEqual(s)); + }); + }); + + describe("if the device is a LVM volume group", () => { + beforeEach(() => { + device = { + sid: 60, + type: "lvmVg", + logicalVolumes: [ + { sid: 61 }, + { sid: 62 }, + { sid: 63 } + ] + }; + }); + + it("returns the logical volumes", () => { + const children = deviceChildren(device); + expect(children.length).toEqual(3); + device.logicalVolumes.forEach(l => expect(children).toContainEqual(l)); + }); + }); + + describe("if the device has neither partition table nor logical volumes", () => { + beforeEach(() => { + device = { sid: 60 }; + }); + + it("returns an empty list", () => { + const children = deviceChildren(device); + expect(children.length).toEqual(0); + }); + }); +}); + describe("parseToBytes", () => { it("returns bytes from given input", () => { expect(parseToBytes(1024)).toEqual(1024); diff --git a/web/src/utils.js b/web/src/utils.js index 5969268cf1..f9645ab238 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -66,6 +66,28 @@ const partition = (collection, filter) => { return [pass, fail]; }; +/** + * Generates a new array without null and undefined values. + * @function + * + * @param {Array} collection + * @returns {Array} + */ +function compact(collection) { + return collection.filter(e => e !== null && e !== undefined); +} + +/** + * Generates a new array without duplicates. + * @function + * + * @param {Array} collection + * @returns {Array} + */ +function uniq(collection) { + return [...new Set(collection)]; +} + /** * Simple utility function to help building className conditionally * @@ -355,6 +377,8 @@ export { noop, isObject, partition, + compact, + uniq, classNames, useCancellablePromise, useLocalStorage, diff --git a/web/src/utils.test.js b/web/src/utils.test.js index 8be48355cb..5d340828f8 100644 --- a/web/src/utils.test.js +++ b/web/src/utils.test.js @@ -20,7 +20,7 @@ */ import { - classNames, partition, noop, toValidationError, + classNames, partition, compact, uniq, noop, toValidationError, localConnection, remoteConnection, isObject } from "./utils"; @@ -41,6 +41,22 @@ describe("partition", () => { }); }); +describe("compact", () => { + it("removes null and undefined values", () => { + expect(compact([])).toEqual([]); + expect(compact([undefined, null, "", 0, 1, NaN, false, true])) + .toEqual(["", 0, 1, NaN, false, true]); + }); +}); + +describe("uniq", () => { + it("removes duplicated values", () => { + expect(uniq([])).toEqual([]); + expect(uniq([undefined, null, null, 0, 1, NaN, false, true, false, "test"])) + .toEqual([undefined, null, 0, 1, NaN, false, true, "test"]); + }); +}); + describe("classNames", () => { it("join given arguments, ignoring falsy values", () => { expect(classNames( From 613b1d0b30409dcc8e5025966e4fe9c8c7004add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 10:52:02 +0000 Subject: [PATCH 38/98] web: Indicate size before resizing --- web/src/components/storage/ProposalResultSection.jsx | 8 +++++--- web/src/components/storage/ProposalResultSection.test.jsx | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/components/storage/ProposalResultSection.jsx b/web/src/components/storage/ProposalResultSection.jsx index 3759c25740..675bdbe48b 100644 --- a/web/src/components/storage/ProposalResultSection.jsx +++ b/web/src/components/storage/ProposalResultSection.jsx @@ -162,12 +162,14 @@ const DevicesTreeTable = ({ devicesManager }) => { const renderResizedLabel = (item) => { if (!item.sid || !devicesManager.isShrunk(item)) return; + const sizeBefore = devicesManager.systemDevice(item.sid).size; + return ( { - // TRANSLATORS: a label to show how much a device was resized. %s will be - // replaced with such a size, including the unit. E.g., 508 MiB - sprintf(_("Resized %s"), deviceSize(devicesManager.shrinkSize(item))) + // TRANSLATORS: Label to indicate the device size before resizing, where %s is replaced by + // the original size (e.g., 3.00 GiB). + sprintf(_("Before %s"), deviceSize(sizeBefore)) } ); diff --git a/web/src/components/storage/ProposalResultSection.test.jsx b/web/src/components/storage/ProposalResultSection.test.jsx index 7e96795516..95f098f10e 100644 --- a/web/src/components/storage/ProposalResultSection.test.jsx +++ b/web/src/components/storage/ProposalResultSection.test.jsx @@ -96,7 +96,7 @@ describe("ProposalResultSection", () => { * "Unused space 3.49 GiB" * "vdc2 openSUSE Leap 15.2, Fedora 10.30 5 GiB" * "Unused space 1 GiB" - * "vdc4 Linux Resized 514 MiB 1.5 GiB" + * "vdc4 Linux Before 2 GiB 1.5 GiB" * "vdc5 / New Btrfs Partition 17.5 GiB" * * Device Mount point Details Size @@ -107,7 +107,7 @@ describe("ProposalResultSection", () => { * Unused space 3.49 GiB * vdc2 openSUSE Leap 15.2, Fedora 10.30 5 GiB * Unused space 1 GiB - * vdc4 Linux Resized 514 MiB 1.5 GiB + * vdc4 Linux Before 2 GiB 1.5 GiB * vdc5 / New Btrfs Partition 17.5 GiB * ------------------------------------------------------------------------- */ @@ -117,7 +117,7 @@ describe("ProposalResultSection", () => { within(treegrid).getByRole("row", { name: "Unused space 3.49 GiB" }); within(treegrid).getByRole("row", { name: "vdc2 openSUSE Leap 15.2, Fedora 10.30 5 GiB" }); within(treegrid).getByRole("row", { name: "Unused space 1 GiB" }); - within(treegrid).getByRole("row", { name: "vdc4 Linux Resized 514 MiB 1.5 GiB" }); + within(treegrid).getByRole("row", { name: "vdc4 Linux Before 2 GiB 1.5 GiB" }); within(treegrid).getByRole("row", { name: "vdc5 / New Btrfs Partition 17.5 GiB" }); }); From 06345b17cc76e8434c70b728fa757bcd50f3aae5 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Tue, 19 Mar 2024 10:18:11 +0100 Subject: [PATCH 39/98] Minor tweaks to documentation --- rust/agama-server/src/agama-web-server.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index ddb57eb1ae..ab25fbb589 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -60,7 +60,7 @@ struct Cli { pub command: Commands, } -// check whether the connection uses SSL or not +/// Checks whether the connection uses SSL or not async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { // a buffer for reading the first byte from the TCP connection let mut buf = [0u8; 1]; @@ -78,7 +78,7 @@ async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { .is_ok_and(|_| buf[0] == 0x16u8 || buf[0] == 0x80u8) } -// build a SSL acceptor using a provided SSL certificate or generate a self-signed one +/// Builds an SSL acceptor using a provided SSL certificate or generates a self-signed one fn create_ssl_acceptor( cert_file: &String, key_file: &String @@ -111,8 +111,8 @@ fn create_ssl_acceptor( Ok(tls_builder.build()) } -// build a response for the HTTP -> HTTPS redirection -// returns 308 permanent redirect +/// Builds a response for the HTTP -> HTTPS redirection +/// returns (HTTP response status code) 308 permanent redirect fn redirect_https(host: &str, uri: &hyper::Uri) -> Response { let builder = Response::builder() // build the redirection target URL @@ -126,9 +126,9 @@ fn redirect_https(host: &str, uri: &hyper::Uri) -> Response { .expect("Failed to create redirection request") } -// build an error response for the HTTP -> HTTPS redirection when we cannot build -// the redirect response from the original request -// returns error 400 +/// Builds an error response for the HTTP -> HTTPS redirection when we cannot build +/// the redirect response from the original request +/// returns error 400 fn redirect_error() -> Response { let builder = Response::builder().status(hyper::StatusCode::BAD_REQUEST); @@ -140,9 +140,9 @@ fn redirect_error() -> Response { .expect("Failed to create an error response") } -// build a router for the HTTP -> HTTPS redirection -// if the redirection URL cannot be built from the original request it returns error 400 -// instead of the redirection +/// Builds a router for the HTTP -> HTTPS redirection +/// if the redirection URL cannot be built from the original request it returns error 400 +/// instead of the redirection fn https_redirect() -> Router { // see https://docs.rs/axum/latest/axum/routing/struct.Router.html#example let redirect_service = tower::service_fn(|req: axum::extract::Request| async move { @@ -159,7 +159,7 @@ fn https_redirect() -> Router { .route_service("/*path", redirect_service) } -// start the web server +/// Starts the web server async fn start_server(address: String, service: Router, ssl_acceptor: SslAcceptor) { tracing::info!("Starting Agama web server at {}", address); From f959ff953cf940406c0263b9bab9a68fbf6fba20 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Tue, 19 Mar 2024 10:34:10 +0100 Subject: [PATCH 40/98] Formatting --- rust/agama-server/src/agama-web-server.rs | 2 +- rust/agama-server/src/cert.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index ab25fbb589..c9b1968a5c 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -81,7 +81,7 @@ async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { /// Builds an SSL acceptor using a provided SSL certificate or generates a self-signed one fn create_ssl_acceptor( cert_file: &String, - key_file: &String + key_file: &String, ) -> Result { let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?; diff --git a/rust/agama-server/src/cert.rs b/rust/agama-server/src/cert.rs index ecaf1c520f..dad1099283 100644 --- a/rust/agama-server/src/cert.rs +++ b/rust/agama-server/src/cert.rs @@ -1,11 +1,13 @@ +use openssl::asn1::Asn1Time; +use openssl::bn::{BigNum, MsbOption}; use openssl::error::ErrorStack; +use openssl::hash::MessageDigest; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; +use openssl::x509::extension::{ + BasicConstraints, SubjectKeyIdentifier, KeyUsage, SubjectAlternativeName +}; use openssl::x509::{X509NameBuilder, X509}; -use openssl::asn1::Asn1Time; -use openssl::bn::{BigNum, MsbOption}; -use openssl::hash::MessageDigest; -use openssl::x509::extension::{BasicConstraints, SubjectKeyIdentifier, KeyUsage, SubjectAlternativeName}; // Generate a self-signed SSL certificate // see https://github.com/sfackler/rust-openssl/blob/master/openssl/examples/mk_certs.rs From c85a5d58e64875ca61b46ca55538752c31e914d8 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Tue, 19 Mar 2024 10:54:05 +0100 Subject: [PATCH 41/98] Minor tweaks in documentation and formatting --- rust/agama-server/src/cert.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/agama-server/src/cert.rs b/rust/agama-server/src/cert.rs index dad1099283..6051e5dbc0 100644 --- a/rust/agama-server/src/cert.rs +++ b/rust/agama-server/src/cert.rs @@ -5,12 +5,12 @@ use openssl::hash::MessageDigest; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use openssl::x509::extension::{ - BasicConstraints, SubjectKeyIdentifier, KeyUsage, SubjectAlternativeName + BasicConstraints, SubjectKeyIdentifier, KeyUsage, SubjectAlternativeName, }; use openssl::x509::{X509NameBuilder, X509}; -// Generate a self-signed SSL certificate -// see https://github.com/sfackler/rust-openssl/blob/master/openssl/examples/mk_certs.rs +/// Generates a self-signed SSL certificate +/// see https://github.com/sfackler/rust-openssl/blob/master/openssl/examples/mk_certs.rs pub fn create_certificate() -> Result<(X509, PKey), ErrorStack> { let rsa = Rsa::generate(2048)?; let key = PKey::from_rsa(rsa)?; From 4ddb24d54e3b739adfae37d3cf8e74372d663113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 14:19:22 +0000 Subject: [PATCH 42/98] service: Changelog --- service/package/rubygem-agama-yast.changes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 5f51032451..0f8d5e6c71 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Tue Mar 19 14:09:54 UTC 2024 - José Iván López González + +- Extend the storage D-Bus API: export LVM volume groups and + logical volumes, export staging devices, add Device and Partition + interfaces, export unused slots (gh#openSUSE/agama#1104). + ------------------------------------------------------------------- Tue Feb 27 15:53:46 UTC 2024 - Imobach Gonzalez Sosa From d1b2c94da24a28a17ddbdfcc88ce1b265acd98f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 14:19:47 +0000 Subject: [PATCH 43/98] web: Changelog --- web/package/cockpit-agama.changes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/package/cockpit-agama.changes b/web/package/cockpit-agama.changes index 87fbb37d54..13a4e5d7df 100644 --- a/web/package/cockpit-agama.changes +++ b/web/package/cockpit-agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Tue Mar 19 14:15:30 UTC 2024 - José Iván López González + +- In storage page, replace Planned Actions section by a new Result + section, unify File Systems and Settings sections, and move + Find Space section to a popup (gh#openSUSE/agama#1104). + ------------------------------------------------------------------- Fri Mar 1 10:56:35 UTC 2024 - José Iván López González From a9f9b1d30cc4953b6b0f933c446eefcdcb2d9be9 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Tue, 19 Mar 2024 14:19:37 +0100 Subject: [PATCH 44/98] Use struct for command agama-server serve options --- rust/agama-server/src/agama-web-server.rs | 65 ++++++++++++----------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index c9b1968a5c..91746827b1 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -6,7 +6,7 @@ use axum::{ http::{Request, Response}, Router, }; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use futures_util::pin_mut; use hyper::body::Incoming; use hyper_util::rt::{TokioExecutor, TokioIo}; @@ -22,30 +22,7 @@ use utoipa::OpenApi; #[derive(Subcommand, Debug)] enum Commands { /// Start the API server. - Serve { - // Address/port to listen on (":::3000" listens for both IPv6 and IPv4 - // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only) - #[arg(long, default_value = ":::3000", help = "Primary address to listen on")] - address: String, - #[arg( - long, - default_value = "", - help = "Optional secondary address to listen on" - )] - address2: String, - #[arg( - long, - default_value = "", - help = "Path to the SSL private key file in PEM format" - )] - key: String, - #[arg( - long, - default_value = "", - help = "Path to the SSL certificate file in PEM format" - )] - cert: String, - }, + Serve(ServeArgs), /// Display the API documentation in OpenAPI format. Openapi, } @@ -60,6 +37,32 @@ struct Cli { pub command: Commands, } +#[derive(Args, Debug)] +struct ServeArgs { + // Address/port to listen on (":::3000" listens for both IPv6 and IPv4 + // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only) + #[arg(long, default_value = ":::3000", help = "Primary address to listen on")] + address: String, + #[arg( + long, + default_value = "", + help = "Optional secondary address to listen on" + )] + address2: String, + #[arg( + long, + default_value = "", + help = "Path to the SSL private key file in PEM format" + )] + key: String, + #[arg( + long, + default_value = "", + help = "Path to the SSL certificate file in PEM format" + )] + cert: String, +} + /// Checks whether the connection uses SSL or not async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { // a buffer for reading the first byte from the TCP connection @@ -288,12 +291,12 @@ fn openapi_command() -> anyhow::Result<()> { async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { - Commands::Serve { - address, - address2, - key, - cert, - } => serve_command(&address, &address2, &cert, &key).await, + Commands::Serve(options) => serve_command( + &options.address, + &options.address2, + &options.cert, + &options.key, + ).await, Commands::Openapi => openapi_command(), } } From ba448782a4440b334a982682e7ecd4c2f5bb2785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 20 Mar 2024 09:23:45 +0100 Subject: [PATCH 45/98] Added comments --- rust/agama-server/src/cert.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/agama-server/src/cert.rs b/rust/agama-server/src/cert.rs index 6051e5dbc0..0c3a4a1e27 100644 --- a/rust/agama-server/src/cert.rs +++ b/rust/agama-server/src/cert.rs @@ -47,7 +47,9 @@ pub fn create_certificate() -> Result<(X509, PKey), ErrorStack> { builder.append_extension( SubjectAlternativeName::new() + // use the default Agama host name .dns("agama") + // use the default name for the mDNS/Avahi .dns("agama.local") .build(&builder.x509v3_context(None, None))?, )?; From 5799151a812c091fc12a9bdbce5fc15402b172b4 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 20 Mar 2024 10:36:22 +0100 Subject: [PATCH 46/98] Refactoring. Added a bit of OOP. Simplified some repetetive actions --- rust/agama-server/src/agama-web-server.rs | 105 ++++++++++------------ 1 file changed, 45 insertions(+), 60 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 91746827b1..9fb34fe692 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -63,6 +63,40 @@ struct ServeArgs { cert: String, } +impl ServeArgs { + /// Builds an SSL acceptor using a provided SSL certificate or generates a self-signed one + fn ssl_acceptor(&self) -> Result { + let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?; + + if self.cert.is_empty() && self.key.is_empty() { + let (cert, key) = agama_dbus_server::cert::create_certificate()?; + + tracing::info!("Generated self signed certificate: {:#?}", cert); + + tls_builder.set_private_key(&key)?; + tls_builder.set_certificate(&cert)?; + + // for debugging you might dump the certificate to a file: + // use std::io::Write; + // let mut cert_file = std::fs::File::create("agama_cert.pem").unwrap(); + // let mut key_file = std::fs::File::create("agama_key.pem").unwrap(); + // cert_file.write_all(cert.to_pem().unwrap().as_ref()).unwrap(); + // key_file.write_all(key.private_key_to_pem_pkcs8().unwrap().as_ref()).unwrap(); + } else { + tracing::info!("Loading PEM certificate: {}", self.cert); + tls_builder.set_certificate_file(PathBuf::from(self.cert.clone()), SslFiletype::PEM)?; + + tracing::info!("Loading PEM key: {}", self.key); + tls_builder.set_private_key_file(PathBuf::from(self.key.clone()), SslFiletype::PEM)?; + } + + // check that the key belongs to the certificate + tls_builder.check_private_key()?; + + Ok(tls_builder.build()) + } +} + /// Checks whether the connection uses SSL or not async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { // a buffer for reading the first byte from the TCP connection @@ -81,39 +115,6 @@ async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { .is_ok_and(|_| buf[0] == 0x16u8 || buf[0] == 0x80u8) } -/// Builds an SSL acceptor using a provided SSL certificate or generates a self-signed one -fn create_ssl_acceptor( - cert_file: &String, - key_file: &String, -) -> Result { - let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?; - - if cert_file.is_empty() && key_file.is_empty() { - let (cert, key) = agama_dbus_server::cert::create_certificate()?; - tracing::info!("Generated self signed certificate: {:#?}", cert); - tls_builder.set_private_key(key.as_ref())?; - tls_builder.set_certificate(cert.as_ref())?; - - // for debugging you might dump the certificate to a file: - // use std::io::Write; - // let mut cert_file = std::fs::File::create("agama_cert.pem").unwrap(); - // let mut key_file = std::fs::File::create("agama_key.pem").unwrap(); - // cert_file.write_all(cert.to_pem().unwrap().as_ref()).unwrap(); - // key_file.write_all(key.private_key_to_pem_pkcs8().unwrap().as_ref()).unwrap(); - } else { - tracing::info!("Loading PEM certificate: {}", cert_file); - tls_builder.set_certificate_file(PathBuf::from(cert_file), SslFiletype::PEM)?; - - tracing::info!("Loading PEM key: {}", key_file); - tls_builder.set_private_key_file(PathBuf::from(key_file), SslFiletype::PEM)?; - } - - // check that the key belongs to the certificate - tls_builder.check_private_key()?; - - Ok(tls_builder.build()) -} - /// Builds a response for the HTTP -> HTTPS redirection /// returns (HTTP response status code) 308 permanent redirect fn redirect_https(host: &str, uri: &hyper::Uri) -> Response { @@ -239,12 +240,7 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto } /// Start serving the API. -async fn serve_command( - address: &str, - address2: &str, - cert: &String, - key: &String, -) -> anyhow::Result<()> { +async fn serve_command(options: ServeArgs) -> anyhow::Result<()> { let journald = tracing_journald::layer().expect("could not connect to journald"); tracing_subscriber::registry().with(journald).init(); @@ -256,28 +252,22 @@ async fn serve_command( } else { return Err(anyhow::anyhow!("Failed to load the service configuration")); }; - let ssl_acceptor = if let Ok(ssl_acceptor) = create_ssl_acceptor(cert, key) { + let ssl_acceptor = if let Ok(ssl_acceptor) = options.ssl_acceptor() { ssl_acceptor } else { return Err(anyhow::anyhow!("SSL initialization failed")); }; - let mut servers = vec![]; - servers.push(tokio::spawn(start_server( - address.to_owned(), - service.clone(), - ssl_acceptor.clone(), - ))); - - // optionally listen on the secondary address/port - if !address2.is_empty() { - servers.push(tokio::spawn(start_server( - address2.to_owned(), - service.clone(), - ssl_acceptor.clone(), - ))); + let mut addresses = vec![options.address]; + + if !options.address2.is_empty() { + addresses.push(options.address2); } + let servers: Vec<_> = addresses.iter().map(|a| + tokio::spawn(start_server(a.clone(), service.clone(), ssl_acceptor.clone()) + )).collect(); + futures_util::future::join_all(servers).await; Ok(()) @@ -291,12 +281,7 @@ fn openapi_command() -> anyhow::Result<()> { async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { - Commands::Serve(options) => serve_command( - &options.address, - &options.address2, - &options.cert, - &options.key, - ).await, + Commands::Serve(options) => serve_command(options).await, Commands::Openapi => openapi_command(), } } From 52322694f1bada328cb0f2f519ce6ef95080000c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 20 Mar 2024 10:45:03 +0100 Subject: [PATCH 47/98] Small fixes --- rust/agama-server/src/agama-web-server.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 9fb34fe692..aa4a9296e9 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -10,6 +10,7 @@ use clap::{Args, Parser, Subcommand}; use futures_util::pin_mut; use hyper::body::Incoming; use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::server::conn::auto::Builder; use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod}; use std::process::{ExitCode, Termination}; use std::{path::PathBuf, pin::Pin}; @@ -108,7 +109,7 @@ async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { .peek(&mut buf) .await // SSL3.0/TLS1.x starts with byte 0x16 - // SSL2 starts with 0x80 (but should not be used as it is considered) + // SSL2 starts with 0x80 (but should not be used as it is considered insecure) // see https://stackoverflow.com/q/3897883 // otherwise consider the stream as a plain HTTP stream possibly starting with // "GET ... HTTP/1.1" or "POST ... HTTP/1.1" or a similar line @@ -203,7 +204,7 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto tower_service.clone().call(request) }); - let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + let ret = Builder::new(TokioExecutor::new()) .serve_connection_with_upgrades(stream, hyper_service) .await; @@ -227,7 +228,7 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto } }); - let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + let ret = Builder::new(TokioExecutor::new()) .serve_connection_with_upgrades(stream, hyper_service) .await; From d3870eb6109e75d3ac59bf7c073439032d8f8fc4 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 20 Mar 2024 10:46:23 +0100 Subject: [PATCH 48/98] Formatting --- rust/agama-server/src/agama-web-server.rs | 13 ++++++++++--- rust/agama-server/src/cert.rs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index aa4a9296e9..4797777185 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -265,9 +265,16 @@ async fn serve_command(options: ServeArgs) -> anyhow::Result<()> { addresses.push(options.address2); } - let servers: Vec<_> = addresses.iter().map(|a| - tokio::spawn(start_server(a.clone(), service.clone(), ssl_acceptor.clone()) - )).collect(); + let servers: Vec<_> = addresses + .iter() + .map(|a| { + tokio::spawn(start_server( + a.clone(), + service.clone(), + ssl_acceptor.clone(), + )) + }) + .collect(); futures_util::future::join_all(servers).await; diff --git a/rust/agama-server/src/cert.rs b/rust/agama-server/src/cert.rs index 0c3a4a1e27..22dd492e70 100644 --- a/rust/agama-server/src/cert.rs +++ b/rust/agama-server/src/cert.rs @@ -5,7 +5,7 @@ use openssl::hash::MessageDigest; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use openssl::x509::extension::{ - BasicConstraints, SubjectKeyIdentifier, KeyUsage, SubjectAlternativeName, + BasicConstraints, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, }; use openssl::x509::{X509NameBuilder, X509}; From c302e1e6a6e481cd4a157caeead79499a4b30114 Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Tue, 19 Mar 2024 15:41:21 +0100 Subject: [PATCH 49/98] [Service] Alternative location for volumes --- .../storage/volume_conversion/from_dbus.rb | 12 ++- .../dbus/storage/volume_conversion/to_dbus.rb | 4 +- .../to_y2storage.rb | 5 +- service/lib/agama/storage/volume.rb | 13 +-- .../volume_conversion/from_y2storage.rb | 14 ++- .../storage/volume_conversion/to_y2storage.rb | 28 +++++- service/lib/agama/storage/volume_location.rb | 78 +++++++++++++++ .../to_dbus_test.rb | 2 +- .../volume_conversion/from_dbus_test.rb | 24 ++++- .../storage/volume_conversion/to_dbus_test.rb | 8 +- .../to_y2storage_test.rb | 5 +- .../volume_conversion/from_y2storage_test.rb | 18 ++-- .../volume_conversion/to_y2storage_test.rb | 98 ++++++++++++++++++- 13 files changed, 266 insertions(+), 43 deletions(-) create mode 100644 service/lib/agama/storage/volume_location.rb diff --git a/service/lib/agama/dbus/storage/volume_conversion/from_dbus.rb b/service/lib/agama/dbus/storage/volume_conversion/from_dbus.rb index 3499c599fc..e66f361a3a 100644 --- a/service/lib/agama/dbus/storage/volume_conversion/from_dbus.rb +++ b/service/lib/agama/dbus/storage/volume_conversion/from_dbus.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require "agama/storage/volume" +require "agama/storage/volume_location" require "agama/storage/volume_templates_builder" require "y2storage/disk_size" require "y2storage/filesystems/type" @@ -67,8 +68,8 @@ def convert CONVERSIONS = { "MountPath" => :mount_path_conversion, "MountOptions" => :mount_options_conversion, + "Target" => :target_conversion, "TargetDevice" => :target_device_conversion, - "TargetVG" => :target_vg_conversion, "FsType" => :fs_type_conversion, "MinSize" => :min_size_conversion, "MaxSize" => :max_size_conversion, @@ -92,13 +93,16 @@ def mount_options_conversion(target, value) # @param target [Agama::Storage::Volume] # @param value [String] def target_device_conversion(target, value) - target.device = value + target.location.device = value end # @param target [Agama::Storage::Volume] # @param value [String] - def target_vg_conversion(target, value) - target.separate_vg_name = value + def target_conversion(target, value) + target_value = value.downcase.to_sym + return unless Agama::Storage::VolumeLocation.targets.include?(target_value) + + target.location.target = target_value end # @param target [Agama::Storage::Volume] diff --git a/service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb b/service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb index 3f586f7d2e..9c4c51533b 100644 --- a/service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb +++ b/service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb @@ -37,8 +37,8 @@ def convert { "MountPath" => volume.mount_path.to_s, "MountOptions" => volume.mount_options, - "TargetDevice" => volume.device.to_s, - "TargetVG" => volume.separate_vg_name.to_s, + "Target" => volume.location.target.to_s, + "TargetDevice" => volume.location.device.to_s, "FsType" => volume.fs_type&.to_human_string || "", "MinSize" => volume.min_size&.to_i, "AutoSize" => volume.auto_size?, diff --git a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb b/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb index e7ca55e481..0f695d0774 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb +++ b/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb @@ -83,7 +83,8 @@ def lvm_conversion(target) lvm = settings.lvm.enabled? target.lvm = lvm - target.separate_vgs = lvm + # Activate support for dedicated volume groups + target.separate_vgs = true # Prevent VG reuse target.lvm_vg_reuse = false end @@ -170,7 +171,7 @@ def find_max_size_fallback(mount_path) def all_devices devices = [settings.boot_device] + settings.lvm.system_vg_devices + - settings.volumes.map(&:device) + settings.volumes.map(&:location).reject(&:reuse_device?).map(&:device) devices.compact.uniq.map { |d| device_or_partitions(d) }.flatten end diff --git a/service/lib/agama/storage/volume.rb b/service/lib/agama/storage/volume.rb index 6b6c231dc2..2ff73300d7 100644 --- a/service/lib/agama/storage/volume.rb +++ b/service/lib/agama/storage/volume.rb @@ -23,6 +23,7 @@ require "y2storage/disk_size" require "agama/storage/btrfs_settings" require "agama/storage/volume_outline" +require "agama/storage/volume_location" module Agama module Storage @@ -58,15 +59,10 @@ class Volume # @return [Array] attr_accessor :mount_options - # Used to locate the volume in a separate disk + # Location of the volume # - # @return [String, nil] - attr_accessor :device - - # Used to locate the volume in a separate VG - # - # @return [String, nil] - attr_accessor :separate_vg_name + # @return [VolumeLocation] + attr_accessor :location # Min size for the volume # @@ -98,6 +94,7 @@ def initialize(mount_path) @max_size = Y2Storage::DiskSize.unlimited @btrfs = BtrfsSettings.new @outline = VolumeOutline.new + @location = VolumeLocation.new end def_delegators :outline, diff --git a/service/lib/agama/storage/volume_conversion/from_y2storage.rb b/service/lib/agama/storage/volume_conversion/from_y2storage.rb index 364531699c..ee4dd9651d 100644 --- a/service/lib/agama/storage/volume_conversion/from_y2storage.rb +++ b/service/lib/agama/storage/volume_conversion/from_y2storage.rb @@ -41,13 +41,12 @@ def convert volume = VolumeTemplatesBuilder.new_from_config(config).for(spec.mount_point || "") volume.tap do |target| - target.device = spec.device - target.separate_vg_name = spec.separate_vg_name target.mount_options = spec.mount_options target.fs_type = spec.fs_type sizes_conversion(target) btrfs_conversion(target) + location_conversion(target) end end @@ -83,6 +82,17 @@ def btrfs_conversion(target) target.btrfs.read_only = spec.btrfs_read_only end + # @param target [Agama::Storage::Volume] + def location_conversion(target) + if spec.reuse? + target.location.target = spec.reformat? ? :device : :filesystem + target.location.device = spec.reuse_name + elsif !!spec.device + target.location.target = spec.separate_vg? ? :new_vg : :new_partition + target.location.device = spec.device + end + end + # Planned device for the given mount path. # @param mount_path [String] diff --git a/service/lib/agama/storage/volume_conversion/to_y2storage.rb b/service/lib/agama/storage/volume_conversion/to_y2storage.rb index 605da35778..0e62261f3c 100644 --- a/service/lib/agama/storage/volume_conversion/to_y2storage.rb +++ b/service/lib/agama/storage/volume_conversion/to_y2storage.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -37,8 +37,6 @@ def initialize(volume) # @return [Y2Storage::VolumeSpecification] def convert # rubocop:disable Metrics/AbcSize Y2Storage::VolumeSpecification.new({}).tap do |target| - target.device = volume.device - target.separate_vg_name = volume.separate_vg_name target.mount_point = volume.mount_path target.mount_options = volume.mount_options.join(",") target.proposed = true @@ -50,6 +48,7 @@ def convert # rubocop:disable Metrics/AbcSize sizes_conversion(target) btrfs_conversion(target) + location_conversion(target) end end @@ -88,6 +87,29 @@ def btrfs_conversion(target) target.btrfs_default_subvolume = volume.btrfs.default_subvolume target.btrfs_read_only = volume.btrfs.read_only? end + + # @param target [Y2Storage::VolumeSpecification] + def location_conversion(target) + location = volume.location + return if location.default? + + if location.reuse_device? + target.reuse_name = location.device + target.reformat = location.target == :device + return + end + + target.device = location.device + target.separate_vg_name = vg_name(target) if location.target == :new_vg + end + + # Name to be used as separate_vg_name for the given Y2Storage volume + # + # @param target [Y2Storage::VolumeSpecification] + def vg_name(target) + mount_point = target.root? ? "root" : target.mount_point.sub(%r{^/}, "") + "vg-#{mount_point.tr("/", "_")}" + end end end end diff --git a/service/lib/agama/storage/volume_location.rb b/service/lib/agama/storage/volume_location.rb new file mode 100644 index 0000000000..1d0a5656b4 --- /dev/null +++ b/service/lib/agama/storage/volume_location.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +module Agama + module Storage + # Settings specifying what device should be used for a given Volume and how + class VolumeLocation + # @see .targets + TARGETS = [:default, :new_partition, :new_vg, :device, :filesystem].freeze + private_constant :TARGETS + + # What to do to allocate the volume + # + # @return [Symbol] see .targets + attr_reader :target + + # Concrete device to allocate the volume, the exact meaning depends on {#target} + # + # @return [String, nil] + attr_accessor :device + + # All possible values for #target: + # + # - :default new partition or logical volume in the default device + # - :new_partition new partition at the disk pointed by {#device} + # - :new_vg new LV in a new dedicated VG created at a the disk pointed by {#device} + # - :device the existing block device specified by {#device} is used and reformatted + # - :filesystem: the existing filesystem on the device specified by {#device} is mounted + # + # @return [Array] + def self.targets + TARGETS + end + + # Constructor + def initialize + @target = :default + end + + # Sets the value of {#target} ensuring it is valid + def target=(value) + return unless TARGETS.include?(value) + + @target = value + end + + # @return [Boolean] + def default? + target == :default + end + + # Whether the chosen target implies reusing an existing device (formatting it or not) + # + # @return [Boolean] + def reuse_device? + [:device, :filesystem].include?(target) + end + end + end +end diff --git a/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb b/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb index 44ef43b0ca..c5961e1585 100644 --- a/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb +++ b/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb @@ -80,7 +80,7 @@ "MountPath" => "/test", "MountOptions" => [], "TargetDevice" => "", - "TargetVG" => "", + "Target" => "default", "FsType" => "", "MinSize" => 0, "AutoSize" => false, diff --git a/service/test/agama/dbus/storage/volume_conversion/from_dbus_test.rb b/service/test/agama/dbus/storage/volume_conversion/from_dbus_test.rb index c441879372..5512783c33 100644 --- a/service/test/agama/dbus/storage/volume_conversion/from_dbus_test.rb +++ b/service/test/agama/dbus/storage/volume_conversion/from_dbus_test.rb @@ -67,7 +67,7 @@ "MountPath" => "/test", "MountOptions" => ["rw", "default"], "TargetDevice" => "/dev/sda", - "TargetVG" => "/dev/system", + "Target" => "new_vg", "FsType" => "Ext4", "MinSize" => 1024, "MaxSize" => 2048, @@ -89,8 +89,8 @@ expect(volume).to be_a(Agama::Storage::Volume) expect(volume.mount_path).to eq("/test") expect(volume.mount_options).to contain_exactly("rw", "default") - expect(volume.device).to eq("/dev/sda") - expect(volume.separate_vg_name).to eq("/dev/system") + expect(volume.location.device).to eq("/dev/sda") + expect(volume.location.target).to eq(:new_vg) expect(volume.fs_type).to eq(Y2Storage::Filesystems::Type::EXT4) expect(volume.auto_size?).to eq(false) expect(volume.min_size.to_i).to eq(1024) @@ -107,8 +107,7 @@ expect(volume).to be_a(Agama::Storage::Volume) expect(volume.mount_path).to eq("/test") expect(volume.mount_options).to contain_exactly("data=ordered") - expect(volume.device).to be_nil - expect(volume.separate_vg_name).to be_nil + expect(volume.location.target).to eq :default expect(volume.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS) expect(volume.auto_size?).to eq(false) expect(volume.min_size.to_i).to eq(5 * (1024**3)) @@ -243,5 +242,20 @@ expect(volume.btrfs.snapshots?).to eq(false) end end + + context "when the D-Bus settings provide a Target that makes no sense" do + let(:dbus_volume) do + { + "MountPath" => "/test", + "Target" => "new_disk" + } + end + + it "ignores the Target value provided from D-Bus and uses :default" do + volume = subject.convert + + expect(volume.location.target).to eq :default + end + end end end diff --git a/service/test/agama/dbus/storage/volume_conversion/to_dbus_test.rb b/service/test/agama/dbus/storage/volume_conversion/to_dbus_test.rb index 9fffa181a2..2356c0c94e 100644 --- a/service/test/agama/dbus/storage/volume_conversion/to_dbus_test.rb +++ b/service/test/agama/dbus/storage/volume_conversion/to_dbus_test.rb @@ -47,8 +47,8 @@ volume.btrfs.snapshots = true volume.btrfs.read_only = true volume.mount_options = ["rw", "default"] - volume.device = "/dev/sda" - volume.separate_vg_name = "/dev/system" + volume.location.device = "/dev/sda" + volume.location.target = :new_partition volume.min_size = Y2Storage::DiskSize.new(1024) volume.max_size = Y2Storage::DiskSize.new(2048) volume.auto_size = true @@ -61,7 +61,7 @@ "MountPath" => "/test", "MountOptions" => [], "TargetDevice" => "", - "TargetVG" => "", + "Target" => "default", "FsType" => "", "MinSize" => 0, "AutoSize" => false, @@ -82,7 +82,7 @@ "MountPath" => "/test", "MountOptions" => ["rw", "default"], "TargetDevice" => "/dev/sda", - "TargetVG" => "/dev/system", + "Target" => "new_partition", "FsType" => "Ext4", "MinSize" => 1024, "MaxSize" => 2048, diff --git a/service/test/agama/storage/proposal_settings_conversion/to_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversion/to_y2storage_test.rb index 404cb716d6..8eae303204 100644 --- a/service/test/agama/storage/proposal_settings_conversion/to_y2storage_test.rb +++ b/service/test/agama/storage/proposal_settings_conversion/to_y2storage_test.rb @@ -44,7 +44,10 @@ settings.encryption.pbkd_function = Y2Storage::PbkdFunction::ARGON2ID settings.space.policy = :custom settings.space.actions = { "/dev/sda" => :force_delete } - volume = Agama::Storage::Volume.new("/test").tap { |v| v.device = "/dev/sdc" } + volume = Agama::Storage::Volume.new("/test").tap do |vol| + vol.location.target = :new_partition + vol.location.device = "/dev/sdc" + end settings.volumes = [volume] end end diff --git a/service/test/agama/storage/volume_conversion/from_y2storage_test.rb b/service/test/agama/storage/volume_conversion/from_y2storage_test.rb index c21495500d..22108805de 100644 --- a/service/test/agama/storage/volume_conversion/from_y2storage_test.rb +++ b/service/test/agama/storage/volume_conversion/from_y2storage_test.rb @@ -57,14 +57,16 @@ expect(volume).to be_a(Agama::Storage::Volume) expect(volume).to have_attributes( - mount_path: "/", - device: "/dev/sda", - separate_vg_name: "/dev/vg0", - mount_options: contain_exactly("defaults", "ro"), - fs_type: Y2Storage::Filesystems::Type::BTRFS, - min_size: Y2Storage::DiskSize.GiB(5), - max_size: Y2Storage::DiskSize.GiB(20), - btrfs: an_object_having_attributes( + mount_path: "/", + location: an_object_having_attributes( + device: "/dev/sda", + target: :new_vg + ), + mount_options: contain_exactly("defaults", "ro"), + fs_type: Y2Storage::Filesystems::Type::BTRFS, + min_size: Y2Storage::DiskSize.GiB(5), + max_size: Y2Storage::DiskSize.GiB(20), + btrfs: an_object_having_attributes( snapshots?: true, subvolumes: contain_exactly("@/home", "@/var"), default_subvolume: "@", diff --git a/service/test/agama/storage/volume_conversion/to_y2storage_test.rb b/service/test/agama/storage/volume_conversion/to_y2storage_test.rb index ba8263aaca..a08ce862eb 100644 --- a/service/test/agama/storage/volume_conversion/to_y2storage_test.rb +++ b/service/test/agama/storage/volume_conversion/to_y2storage_test.rb @@ -29,8 +29,8 @@ describe "#convert" do let(:volume) do Agama::Storage::Volume.new("/").tap do |volume| - volume.device = "/dev/sda" - volume.separate_vg_name = "/dev/vg0" + volume.location.device = "/dev/sda" + volume.location.target = :new_vg volume.mount_options = ["defaults"] volume.fs_type = btrfs volume.auto_size = false @@ -59,7 +59,7 @@ expect(spec).to be_a(Y2Storage::VolumeSpecification) expect(spec).to have_attributes( device: "/dev/sda", - separate_vg_name: "/dev/vg0", + separate_vg_name: "vg-root", mount_point: "/", mount_options: "defaults", proposed?: true, @@ -102,5 +102,97 @@ ) end end + + context "when the default target is used" do + before { volume.location.target = :default } + + it "sets both #device and #reuse_name to nil" do + spec = subject.convert + + expect(spec.device).to be_nil + expect(spec.reuse_name).to be_nil + end + end + + context "when the target is a new dedicated partition" do + before { volume.location.target = :new_partition } + + it "sets #device to the expected disk name" do + expect(subject.convert.device).to eq "/dev/sda" + end + + it "sets #separate_vg_name and #reuse_name to nil" do + spec = subject.convert + + expect(spec.reuse_name).to be_nil + expect(spec.separate_vg_name).to be_nil + end + end + + context "when the target is a new dedicated volume group" do + before { volume.location.target = :new_vg } + + context "when the mount point is /" do + it "sets #device, #separate_vg_name and #reuse_name to the expected values" do + spec = subject.convert + + expect(spec.device).to eq "/dev/sda" + expect(spec.reuse_name).to be_nil + expect(spec.separate_vg_name).to eq "vg-root" + end + end + + context "when the mount point is not the root one" do + let(:volume) do + Agama::Storage::Volume.new("/var/log").tap do |volume| + volume.location.device = "/dev/sda" + end + end + + it "sets #device, #separate_vg_name and #reuse_name to the expected values" do + spec = subject.convert + + expect(spec.device).to eq "/dev/sda" + expect(spec.reuse_name).to be_nil + expect(spec.separate_vg_name).to eq "vg-var_log" + end + end + end + + context "when the target is an existing block device" do + before { volume.location.target = :device } + + it "sets #reuse_name and #reformat to the proper values" do + spec = subject.convert + + expect(spec.reuse_name).to eq "/dev/sda" + expect(spec.reformat).to eq true + end + + it "sets #device and #separate_vg_name to nil" do + spec = subject.convert + + expect(spec.device).to be_nil + expect(spec.separate_vg_name).to be_nil + end + end + + context "when the target is an existing file system" do + before { volume.location.target = :filesystem } + + it "sets #reuse_name and #reformat to the proper values" do + spec = subject.convert + + expect(spec.reuse_name).to eq "/dev/sda" + expect(spec.reformat).to eq false + end + + it "sets #device and #separate_vg_name to nil" do + spec = subject.convert + + expect(spec.device).to be_nil + expect(spec.separate_vg_name).to be_nil + end + end end end From 7d23a24040fd22746f52d9e230397a79020f6d7d Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Wed, 20 Mar 2024 15:02:52 +0100 Subject: [PATCH 50/98] [Service] Update D-Bus documentation --- .../org.opensuse.Agama.Storage1.Proposal.Calculator.doc.xml | 4 ++-- doc/dbus/org.opensuse.Agama.Storage1.Proposal.doc.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/dbus/org.opensuse.Agama.Storage1.Proposal.Calculator.doc.xml b/doc/dbus/org.opensuse.Agama.Storage1.Proposal.Calculator.doc.xml index 6b5130f2f6..254aa14124 100644 --- a/doc/dbus/org.opensuse.Agama.Storage1.Proposal.Calculator.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Storage1.Proposal.Calculator.doc.xml @@ -18,8 +18,8 @@ - + + diff --git a/doc/dbus/org.opensuse.Agama.Storage1.Proposal.doc.xml b/doc/dbus/org.opensuse.Agama.Storage1.Proposal.doc.xml index cf473ac671..9ee58dba88 100644 --- a/doc/dbus/org.opensuse.Agama.Storage1.Proposal.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Storage1.Proposal.doc.xml @@ -4,26 +4,35 @@ Interfaces with the properties of the calculated proposal. --> - - - - - - - - +