From ae36e6fd775a09c1f590ef8cc73591d2340dc370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 5 Mar 2024 16:07:11 +0000 Subject: [PATCH 01/38] rust: fix the agama-server crate name --- rust/Cargo.lock | 86 +++++++++++----------- rust/agama-server/Cargo.toml | 2 +- rust/agama-server/src/agama-dbus-server.rs | 2 +- rust/agama-server/src/agama-web-server.rs | 4 +- rust/agama-server/tests/l10n.rs | 2 +- rust/agama-server/tests/network.rs | 10 +-- rust/agama-server/tests/service.rs | 2 +- 7 files changed, 54 insertions(+), 54 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index cc576c6eaf..7d48a4957c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -41,49 +41,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "agama-dbus-server" -version = "0.1.0" -dependencies = [ - "agama-lib", - "agama-locale-data", - "anyhow", - "async-trait", - "axum", - "axum-extra", - "chrono", - "cidr", - "clap", - "config", - "gettext-rs", - "http-body-util", - "jsonwebtoken", - "log", - "macaddr", - "once_cell", - "pam", - "rand", - "regex", - "serde", - "serde_json", - "serde_with", - "serde_yaml", - "simplelog", - "systemd-journal-logger", - "thiserror", - "tokio", - "tokio-stream", - "tower", - "tower-http", - "tracing", - "tracing-journald", - "tracing-subscriber", - "utoipa", - "uuid", - "zbus", - "zbus_macros", -] - [[package]] name = "agama-derive" version = "1.0.0" @@ -129,6 +86,49 @@ dependencies = [ "thiserror", ] +[[package]] +name = "agama-server" +version = "0.1.0" +dependencies = [ + "agama-lib", + "agama-locale-data", + "anyhow", + "async-trait", + "axum", + "axum-extra", + "chrono", + "cidr", + "clap", + "config", + "gettext-rs", + "http-body-util", + "jsonwebtoken", + "log", + "macaddr", + "once_cell", + "pam", + "rand", + "regex", + "serde", + "serde_json", + "serde_with", + "serde_yaml", + "simplelog", + "systemd-journal-logger", + "thiserror", + "tokio", + "tokio-stream", + "tower", + "tower-http", + "tracing", + "tracing-journald", + "tracing-subscriber", + "utoipa", + "uuid", + "zbus", + "zbus_macros", +] + [[package]] name = "agama-settings" version = "1.0.0" diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 25cc8ccba5..def882c4a2 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agama-dbus-server" +name = "agama-server" version = "0.1.0" edition = "2021" rust-version.workspace = true diff --git a/rust/agama-server/src/agama-dbus-server.rs b/rust/agama-server/src/agama-dbus-server.rs index 3145228fd3..7643d6e0ec 100644 --- a/rust/agama-server/src/agama-dbus-server.rs +++ b/rust/agama-server/src/agama-dbus-server.rs @@ -1,4 +1,4 @@ -use agama_dbus_server::{ +use agama_server::{ l10n::{self, helpers}, network, questions, }; diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index af96034b34..53f1e217c1 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -1,10 +1,10 @@ use std::process::{ExitCode, Termination}; -use agama_dbus_server::{ +use agama_lib::connection_to; +use agama_server::{ l10n::helpers, web::{self, run_monitor}, }; -use agama_lib::connection_to; use clap::{Args, Parser, Subcommand}; use tokio::sync::broadcast::channel; use tracing_subscriber::prelude::*; diff --git a/rust/agama-server/tests/l10n.rs b/rust/agama-server/tests/l10n.rs index f2d0291c29..75d0e76109 100644 --- a/rust/agama-server/tests/l10n.rs +++ b/rust/agama-server/tests/l10n.rs @@ -1,6 +1,6 @@ pub mod common; -use agama_dbus_server::l10n::web::l10n_service; +use agama_server::l10n::web::l10n_service; use axum::{ body::Body, http::{Request, StatusCode}, diff --git a/rust/agama-server/tests/network.rs b/rust/agama-server/tests/network.rs index ed4b0037b2..e437331d9d 100644 --- a/rust/agama-server/tests/network.rs +++ b/rust/agama-server/tests/network.rs @@ -1,16 +1,16 @@ pub mod common; use self::common::{async_retry, DBusServer}; -use agama_dbus_server::network::{ - self, - model::{self, Ipv4Method, Ipv6Method}, - Adapter, NetworkAdapterError, NetworkService, NetworkState, -}; use agama_lib::network::{ settings::{self}, types::DeviceType, NetworkClient, }; +use agama_server::network::{ + self, + model::{self, Ipv4Method, Ipv6Method}, + Adapter, NetworkAdapterError, NetworkService, NetworkState, +}; use async_trait::async_trait; use cidr::IpInet; use std::error::Error; diff --git a/rust/agama-server/tests/service.rs b/rust/agama-server/tests/service.rs index 14c5c4ae0d..daa453af02 100644 --- a/rust/agama-server/tests/service.rs +++ b/rust/agama-server/tests/service.rs @@ -1,6 +1,6 @@ pub mod common; -use agama_dbus_server::{ +use agama_server::{ service, web::{generate_token, MainServiceBuilder, ServiceConfig}, }; From 6252eebf4e7c00f33ee420f1e5b8f5b875b3167f Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Wed, 6 Mar 2024 09:54:04 +0000 Subject: [PATCH 02/38] [web] WIP: move transactional information --- web/src/components/storage/ProposalPage.jsx | 6 +- .../storage/ProposalSettingsSection.jsx | 68 ++++++------------- .../storage/ProposalSettingsSection.test.jsx | 17 ++--- .../storage/ProposalTransactionalInfo.jsx | 66 ++++++++++++++++++ .../ProposalTransactionalInfo.test.jsx | 64 +++++++++++++++++ web/src/components/storage/index.js | 1 + 6 files changed, 162 insertions(+), 60 deletions(-) create mode 100644 web/src/components/storage/ProposalTransactionalInfo.jsx create mode 100644 web/src/components/storage/ProposalTransactionalInfo.test.jsx diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index 51ab17e7bf..f39a4d9db6 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -31,7 +31,8 @@ import { ProposalSettingsSection, ProposalSpacePolicySection, ProposalDeviceSection, - ProposalFileSystemsSection + ProposalFileSystemsSection, + ProposalTransactionalInfo } from "~/components/storage"; import { IDLE } from "~/client/status"; @@ -201,6 +202,9 @@ export default function ProposalPage() { const PageContent = () => { return ( <> + { - const explanation = _("Uses Btrfs for the root file system allowing to boot to a previous \ + const explanation = _("Uses Btrfs for the root file system allowing to boot to a previous \ version of the system after configuration changes or software upgrades."); - return ( - <> - + return ( + - {explanation} + +
+ {explanation} +
- - ); - }; - - return ( -
- - } /> -
+ } + /> ); }; @@ -297,8 +290,6 @@ export default function ProposalSettingsSection({ encryptionMethods = [], onChange = noop }) { - const { selectedProduct } = useProduct(); - const changeEncryption = ({ password, method }) => { onChange({ encryptionPassword: password, encryptionMethod: method }); }; @@ -318,29 +309,12 @@ export default function ProposalSettingsSection({ const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0; - const transactional = isTransactionalSystem(settings?.volumes || []); - return ( <>
- - -
- {/* TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) */} - {sprintf(_("%s is an immutable system with atomic updates using a read-only Btrfs \ -root file system."), selectedProduct.name)} -
- - } - else={ - - } + { }; }); -jest.mock("~/context/product", () => ({ - ...jest.requireActual("~/context/product"), - useProduct: () => ({ - selectedProduct : { name: "Test" } - }) -})); - let props; beforeEach(() => { @@ -48,7 +41,7 @@ beforeEach(() => { const rootVolume = { mountPath: "/", fsType: "Btrfs", outline: { snapshotsConfigurable: true } }; -describe("if the system is not transactional", () => { +describe("if snapshots are configurable", () => { beforeEach(() => { props.settings = { volumes: [rootVolume] }; }); @@ -60,15 +53,15 @@ describe("if the system is not transactional", () => { }); }); -describe("if the system is transactional", () => { +describe("if snapshots are not configurable", () => { beforeEach(() => { - props.settings = { volumes: [{ ...rootVolume, transactional: true }] }; + props.settings = { volumes: [{ ...rootVolume, outline: { ...rootVolume.outline, snapshotsConfigurable: false } }] }; }); - it("renders explanation about transactional system", () => { + it("renders the snapshots switch", () => { plainRender(); - screen.getByText("Transactional system"); + expect(screen.queryByRole("checkbox", { name: "Use Btrfs Snapshots" })).toBeNull(); }); }); diff --git a/web/src/components/storage/ProposalTransactionalInfo.jsx b/web/src/components/storage/ProposalTransactionalInfo.jsx new file mode 100644 index 0000000000..3e46a2c021 --- /dev/null +++ b/web/src/components/storage/ProposalTransactionalInfo.jsx @@ -0,0 +1,66 @@ +/* + * 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 { Alert } from "@patternfly/react-core"; +import { sprintf } from "sprintf-js"; + +import { _ } from "~/i18n"; +import { If, Section } from "~/components/core"; +import { isTransactionalSystem } from "~/components/storage/utils"; +import { useProduct } from "~/context/product"; + +/** + * @typedef {import ("~/client/storage").ProposalManager.ProposalSettings} ProposalSettings + */ + +/** + * Information about the system being transactional, if needed + * @component + * + * @param {object} props + * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. + * @param {object} settings + */ +export default function ProposalTransactionalInfo({ settings }) { + const transactional = isTransactionalSystem(settings?.volumes || []); + const { selectedProduct } = useProduct(); + + /* TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) */ + const description = sprintf( + _("%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots."), + selectedProduct.name + ); + const title = _("Transactional root file system"); + + return ( + + + {description} + +
+ } + /> + ); +} diff --git a/web/src/components/storage/ProposalTransactionalInfo.test.jsx b/web/src/components/storage/ProposalTransactionalInfo.test.jsx new file mode 100644 index 0000000000..8dd6838cd5 --- /dev/null +++ b/web/src/components/storage/ProposalTransactionalInfo.test.jsx @@ -0,0 +1,64 @@ +/* + * 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 { ProposalTransactionalInfo } from "~/components/storage"; + +jest.mock("~/context/product", () => ({ + ...jest.requireActual("~/context/product"), + useProduct: () => ({ + selectedProduct : { name: "Test" } + }) +})); + +let props; + +beforeEach(() => { + props = {}; +}); + +const rootVolume = { mountPath: "/", fsType: "Btrfs" }; + +describe("if the system is not transactional", () => { + beforeEach(() => { + props.settings = { volumes: [rootVolume] }; + }); + + it("does not render any explanation about transactional system", () => { + plainRender(); + + expect(screen.queryByText("Transactional root file system")).toBeNull(); + }); +}); + +describe("if the system is transactional", () => { + beforeEach(() => { + props.settings = { volumes: [{ ...rootVolume, transactional: true }] }; + }); + + it("renders an explanation about the transactional system", () => { + plainRender(); + + screen.getByText("Transactional root file system"); + }); +}); diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.js index 6c518c51a0..d4dd30a180 100644 --- a/web/src/components/storage/index.js +++ b/web/src/components/storage/index.js @@ -26,6 +26,7 @@ export { default as ProposalSpacePolicySection } from "./ProposalSpacePolicySect export { default as ProposalDeviceSection } from "./ProposalDeviceSection"; export { default as ProposalFileSystemsSection } from "./ProposalFileSystemsSection"; export { default as ProposalActionsSection } from "./ProposalActionsSection"; +export { default as ProposalTransactionalInfo } from "./ProposalTransactionalInfo"; export { default as ProposalVolumes } from "./ProposalVolumes"; export { default as DASDPage } from "./DASDPage"; export { default as DASDTable } from "./DASDTable"; From ff2ef244dc7b47748c06fab6dfdba25c1bc59b1c Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Fri, 23 Feb 2024 10:27:42 +0100 Subject: [PATCH 03/38] Skeleton for agama-cli Server command --- rust/agama-cli/src/commands.rs | 4 ++++ rust/agama-cli/src/main.rs | 3 +++ rust/agama-cli/src/server.rs | 10 ++++++++++ 3 files changed, 17 insertions(+) create mode 100644 rust/agama-cli/src/server.rs diff --git a/rust/agama-cli/src/commands.rs b/rust/agama-cli/src/commands.rs index 99d51ece9f..52b3aaf1d2 100644 --- a/rust/agama-cli/src/commands.rs +++ b/rust/agama-cli/src/commands.rs @@ -1,5 +1,6 @@ use crate::config::ConfigCommands; use crate::logs::LogsCommands; +use crate::server::ServerCommands; use crate::profile::ProfileCommands; use crate::questions::QuestionsCommands; use clap::Subcommand; @@ -35,4 +36,7 @@ pub enum Commands { /// Collects logs #[command(subcommand)] Logs(LogsCommands), + /// Request login with the web server + #[command(subcommand)] + Server(ServerCommands), } diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index 52a3fa4a89..d5da2244f8 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -8,6 +8,7 @@ mod printers; mod profile; mod progress; mod questions; +mod server; use crate::error::CliError; use agama_lib::error::ServiceError; @@ -20,6 +21,7 @@ use printers::Format; use profile::run as run_profile_cmd; use progress::InstallerProgress; use questions::run as run_questions_cmd; +use server::run as run_server_cmd; use std::{ process::{ExitCode, Termination}, thread::sleep, @@ -135,6 +137,7 @@ async fn run_command(cli: Cli) -> anyhow::Result<()> { } Commands::Questions(subcommand) => run_questions_cmd(subcommand).await, Commands::Logs(subcommand) => run_logs_cmd(subcommand).await, + Commands::Server(subcommand) => run_server_cmd(subcommand).await, _ => unimplemented!(), } } diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs new file mode 100644 index 0000000000..2690279872 --- /dev/null +++ b/rust/agama-cli/src/server.rs @@ -0,0 +1,10 @@ +use clap::Subcommand; + +#[derive(Subcommand, Debug)] +pub enum ServerCommands { +} + +/// Main entry point called from agama CLI main loop +pub async fn run(_subcommand: ServerCommands) -> anyhow::Result<()> { + Err(anyhow::anyhow!("Not implemented")) +} From 916c07d29baa768255f1c3455919ca04b09dcbfe Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Mon, 26 Feb 2024 20:31:08 +0100 Subject: [PATCH 04/38] Skeleton for Server/{Login,Logout} subcommands --- rust/agama-cli/src/server.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 2690279872..bac4a41436 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -2,6 +2,15 @@ use clap::Subcommand; #[derive(Subcommand, Debug)] pub enum ServerCommands { + /// Login with defined server. Result is JWT stored and used in all subsequent commands + Login { + #[clap(long, short = 'u')] + user: Option, + #[clap(long, short = 'p')] + password: Option, + }, + /// Release currently stored JWT + Logout, } /// Main entry point called from agama CLI main loop From 0bc558c06ba12c8f50f63df223d28dc582cca2bf Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Tue, 27 Feb 2024 13:49:16 +0100 Subject: [PATCH 05/38] Subcommand match skeleton --- rust/agama-cli/src/server.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index bac4a41436..5ee9979665 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -14,6 +14,35 @@ pub enum ServerCommands { } /// Main entry point called from agama CLI main loop -pub async fn run(_subcommand: ServerCommands) -> anyhow::Result<()> { +pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { + match subcommand { + ServerCommands::Login { + user, + password, + }=> { + // actions to do: + // 1) somehow obtain credentials (interactive, commandline, from a file) + // 2) pass credentials to the web server + // 3) receive JWT + // 4) store the JWT in a well known location + login(user.unwrap_or(String::new()), password.unwrap_or(String::new())) + }, + ServerCommands::Logout => { + // actions to do: + // 1) release JWT from the well known location if any + logout() + }, + } +} + +/// Asks user to enter user name / password or read it from a file +fn get_credentials() { +} + +fn login(user: String, password: String) -> anyhow::Result<()> { + Err(anyhow::anyhow!("Not implemented")) +} + +fn logout() -> anyhow::Result<()> { Err(anyhow::anyhow!("Not implemented")) } From 8e42568ef3821b9c846c70da925f1ac1e9a9e9ba Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 28 Feb 2024 13:55:48 +0100 Subject: [PATCH 06/38] Skeleton for handling credentials --- rust/agama-cli/src/server.rs | 45 ++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 5ee9979665..21a6749934 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -1,4 +1,5 @@ use clap::Subcommand; +use std::path::{PathBuf}; #[derive(Subcommand, Debug)] pub enum ServerCommands { @@ -13,6 +14,11 @@ pub enum ServerCommands { Logout, } +struct Credentials { + user: String, + password: String, +} + /// Main entry point called from agama CLI main loop pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { match subcommand { @@ -22,10 +28,18 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { }=> { // actions to do: // 1) somehow obtain credentials (interactive, commandline, from a file) + // credentials are handled in this way (in descending priority) + // "command line options" -> "read from file" -> "ask to the user" // 2) pass credentials to the web server // 3) receive JWT // 4) store the JWT in a well known location - login(user.unwrap_or(String::new()), password.unwrap_or(String::new())) + + // little bit tricky way of error conversion to deal with + // errors reported for anyhow::Error when using '?' + let credentials = get_credentials(user, password, None) + .ok_or(Err(())).map_err(|_err: Result<(), ()>| anyhow::anyhow!("Missing credentials"))?; + + login(credentials.user, credentials.password) }, ServerCommands::Logout => { // actions to do: @@ -35,11 +49,34 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { } } -/// Asks user to enter user name / password or read it from a file -fn get_credentials() { +/// Reads credentials from a given file (if exists) +fn get_credentials_from_file(_file: PathBuf) -> Option { + None +} + +/// Asks user to enter credentials interactively +fn get_credentials_from_user() -> Option { + None +} + +/// Handles various ways how to get user name / password from user or read it from a file +fn get_credentials(user: Option, password: Option, file: Option) -> Option { + match (user, password) { + // explicitly provided user + password + (Some(u), Some(p)) => Some(Credentials { + user: u, + password: p, + }), + _ => match file { + // try to read user + password from a file + Some(f) => get_credentials_from_file(f), + // last instance - ask user to enter user + password interactively + _ => get_credentials_from_user(), + }, + } } -fn login(user: String, password: String) -> anyhow::Result<()> { +fn login(_user: String, _password: String) -> anyhow::Result<()> { Err(anyhow::anyhow!("Not implemented")) } From 02b0f860ea3c83f0d460939078b7ddd9b8b16fe7 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Thu, 29 Feb 2024 10:39:59 +0100 Subject: [PATCH 07/38] Interactive input of credentials --- rust/agama-cli/src/server.rs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 21a6749934..fbc4bd3c2b 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -1,4 +1,5 @@ use clap::Subcommand; +use std::io; use std::path::{PathBuf}; #[derive(Subcommand, Debug)] @@ -37,7 +38,7 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { // little bit tricky way of error conversion to deal with // errors reported for anyhow::Error when using '?' let credentials = get_credentials(user, password, None) - .ok_or(Err(())).map_err(|_err: Result<(), ()>| anyhow::anyhow!("Missing credentials"))?; + .ok_or(Err(())).map_err(|_err: Result<(), ()>| anyhow::anyhow!("Wrong credentials"))?; login(credentials.user, credentials.password) }, @@ -54,9 +55,32 @@ fn get_credentials_from_file(_file: PathBuf) -> Option { None } +fn read_credential(caption: String) -> Option { + let mut cred = String::new(); + + println!("{}: ", caption); + + if io::stdin().read_line(&mut cred).is_err() { + return None; + } + if cred.pop().is_none() || cred.is_empty() { + return None; + } + + Some(cred) +} + /// Asks user to enter credentials interactively fn get_credentials_from_user() -> Option { - None + println!("Enter credentials needed for accessing installation server"); + + let user = read_credential("User".to_string())?; + let password = read_credential("Password".to_string())?; + + Some(Credentials { + user: user, + password: password, + }) } /// Handles various ways how to get user name / password from user or read it from a file @@ -76,7 +100,12 @@ fn get_credentials(user: Option, password: Option, file: Option< } } -fn login(_user: String, _password: String) -> anyhow::Result<()> { +fn login(user: String, password: String) -> anyhow::Result<()> { + // 1) ask web server for JWT + // 2) if successful store the JWT for later use + println!("Loging with credentials:"); + println!("({}, {})", user, password); + Err(anyhow::anyhow!("Not implemented")) } From fb200a303fb45b7ed1ce23519f380d900dfab044 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Fri, 1 Mar 2024 10:01:49 +0100 Subject: [PATCH 08/38] Reading credentials from a file --- rust/agama-cli/src/server.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index fbc4bd3c2b..201533d9b3 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -1,5 +1,7 @@ use clap::Subcommand; use std::io; +use std::io::{BufRead, BufReader}; +use std::fs::File; use std::path::{PathBuf}; #[derive(Subcommand, Debug)] @@ -10,6 +12,8 @@ pub enum ServerCommands { user: Option, #[clap(long, short = 'p')] password: Option, + #[clap(long, short = 'f')] + file: Option, }, /// Release currently stored JWT Logout, @@ -26,6 +30,7 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { ServerCommands::Login { user, password, + file, }=> { // actions to do: // 1) somehow obtain credentials (interactive, commandline, from a file) @@ -37,7 +42,7 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { // little bit tricky way of error conversion to deal with // errors reported for anyhow::Error when using '?' - let credentials = get_credentials(user, password, None) + let credentials = get_credentials(user, password, file) .ok_or(Err(())).map_err(|_err: Result<(), ()>| anyhow::anyhow!("Wrong credentials"))?; login(credentials.user, credentials.password) @@ -51,7 +56,22 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { } /// Reads credentials from a given file (if exists) -fn get_credentials_from_file(_file: PathBuf) -> Option { +fn get_credentials_from_file(path: PathBuf) -> Option { + if !path.as_path().exists() { + return None; + } + + if let Ok(file) = File::open(path) { + let line = BufReader::new(file).lines().next()?; + + if let Ok(password) = line { + return Some(Credentials { + user: "not needed".to_string(), + password: password, + }) + } + } + None } From 48b11ba8813313380f97e1ff860e1db3c7f56a3c Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Fri, 1 Mar 2024 10:06:32 +0100 Subject: [PATCH 09/38] Do not deal with user name. For now only root is expected. --- rust/agama-cli/src/server.rs | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 201533d9b3..05260f9722 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -8,8 +8,6 @@ use std::path::{PathBuf}; pub enum ServerCommands { /// Login with defined server. Result is JWT stored and used in all subsequent commands Login { - #[clap(long, short = 'u')] - user: Option, #[clap(long, short = 'p')] password: Option, #[clap(long, short = 'f')] @@ -20,7 +18,6 @@ pub enum ServerCommands { } struct Credentials { - user: String, password: String, } @@ -28,7 +25,6 @@ struct Credentials { pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { match subcommand { ServerCommands::Login { - user, password, file, }=> { @@ -42,10 +38,10 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { // little bit tricky way of error conversion to deal with // errors reported for anyhow::Error when using '?' - let credentials = get_credentials(user, password, file) + let credentials = get_credentials(password, file) .ok_or(Err(())).map_err(|_err: Result<(), ()>| anyhow::anyhow!("Wrong credentials"))?; - login(credentials.user, credentials.password) + login(credentials.password) }, ServerCommands::Logout => { // actions to do: @@ -66,7 +62,6 @@ fn get_credentials_from_file(path: PathBuf) -> Option { if let Ok(password) = line { return Some(Credentials { - user: "not needed".to_string(), password: password, }) } @@ -94,21 +89,18 @@ fn read_credential(caption: String) -> Option { fn get_credentials_from_user() -> Option { println!("Enter credentials needed for accessing installation server"); - let user = read_credential("User".to_string())?; let password = read_credential("Password".to_string())?; Some(Credentials { - user: user, password: password, }) } /// Handles various ways how to get user name / password from user or read it from a file -fn get_credentials(user: Option, password: Option, file: Option) -> Option { - match (user, password) { +fn get_credentials(password: Option, file: Option) -> Option { + match password { // explicitly provided user + password - (Some(u), Some(p)) => Some(Credentials { - user: u, + Some(p) => Some(Credentials { password: p, }), _ => match file { @@ -120,11 +112,11 @@ fn get_credentials(user: Option, password: Option, file: Option< } } -fn login(user: String, password: String) -> anyhow::Result<()> { +fn login(password: String) -> anyhow::Result<()> { // 1) ask web server for JWT // 2) if successful store the JWT for later use println!("Loging with credentials:"); - println!("({}, {})", user, password); + println!("({})", password); Err(anyhow::anyhow!("Not implemented")) } From 5fbf7bb098b6372ef4130865a18c09eb3839638d Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Fri, 1 Mar 2024 10:35:57 +0100 Subject: [PATCH 10/38] Refactoring: Some renaming. Do not messup input and output data format --- rust/agama-cli/src/server.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 05260f9722..ac31cb5e54 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -17,8 +17,9 @@ pub enum ServerCommands { Logout, } -struct Credentials { - password: String, +struct LoginOptions { + password: Option, + file: Option, } /// Main entry point called from agama CLI main loop @@ -36,12 +37,17 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { // 3) receive JWT // 4) store the JWT in a well known location + let options = LoginOptions { + password: password, + file: file, + }; + // little bit tricky way of error conversion to deal with // errors reported for anyhow::Error when using '?' - let credentials = get_credentials(password, file) + let password = parse_login_options(options) .ok_or(Err(())).map_err(|_err: Result<(), ()>| anyhow::anyhow!("Wrong credentials"))?; - login(credentials.password) + login(password) }, ServerCommands::Logout => { // actions to do: @@ -52,7 +58,7 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { } /// Reads credentials from a given file (if exists) -fn get_credentials_from_file(path: PathBuf) -> Option { +fn get_credentials_from_file(path: PathBuf) -> Option { if !path.as_path().exists() { return None; } @@ -61,9 +67,7 @@ fn get_credentials_from_file(path: PathBuf) -> Option { let line = BufReader::new(file).lines().next()?; if let Ok(password) = line { - return Some(Credentials { - password: password, - }) + return Some(password) } } @@ -86,24 +90,20 @@ fn read_credential(caption: String) -> Option { } /// Asks user to enter credentials interactively -fn get_credentials_from_user() -> Option { +fn get_credentials_from_user() -> Option { println!("Enter credentials needed for accessing installation server"); let password = read_credential("Password".to_string())?; - Some(Credentials { - password: password, - }) + Some(password) } /// Handles various ways how to get user name / password from user or read it from a file -fn get_credentials(password: Option, file: Option) -> Option { - match password { +fn parse_login_options(options: LoginOptions) -> Option { + match options.password { // explicitly provided user + password - Some(p) => Some(Credentials { - password: p, - }), - _ => match file { + Some(p) => Some(p), + _ => match options.file { // try to read user + password from a file Some(f) => get_credentials_from_file(f), // last instance - ask user to enter user + password interactively From 25ce87d9da75998bb110a15a62c322ab33ec1239 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Fri, 1 Mar 2024 13:22:35 +0100 Subject: [PATCH 11/38] Refactoring: Turned into objects --- rust/agama-cli/src/server.rs | 119 +++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 47 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index ac31cb5e54..84d7073c80 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -17,11 +17,6 @@ pub enum ServerCommands { Logout, } -struct LoginOptions { - password: Option, - file: Option, -} - /// Main entry point called from agama CLI main loop pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { match subcommand { @@ -42,12 +37,7 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { file: file, }; - // little bit tricky way of error conversion to deal with - // errors reported for anyhow::Error when using '?' - let password = parse_login_options(options) - .ok_or(Err(())).map_err(|_err: Result<(), ()>| anyhow::anyhow!("Wrong credentials"))?; - - login(password) + login(LoginOptions::parse(options).password()?) }, ServerCommands::Logout => { // actions to do: @@ -57,59 +47,94 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { } } -/// Reads credentials from a given file (if exists) -fn get_credentials_from_file(path: PathBuf) -> Option { - if !path.as_path().exists() { - return None; - } - - if let Ok(file) = File::open(path) { - let line = BufReader::new(file).lines().next()?; +struct LoginOptions { + password: Option, + file: Option, +} - if let Ok(password) = line { - return Some(password) +impl LoginOptions { + fn parse(options: LoginOptions) -> Box { + match options.password { + // explicitly provided user + password + Some(p) => Box::new(KnownCredentials { password: p }), + _ => match options.file { + // try to read user + password from a file + Some(f) => Box::new(FileCredentials { path: f }), + // last instance - ask user to enter user + password interactively + _ => Box::new(MissingCredentials {}), + }, } } +} + +struct MissingCredentials; - None +struct FileCredentials { + path: PathBuf, } -fn read_credential(caption: String) -> Option { - let mut cred = String::new(); +struct KnownCredentials { + password: String, +} - println!("{}: ", caption); +trait Credentials { + fn password(&self) -> io::Result; +} - if io::stdin().read_line(&mut cred).is_err() { - return None; - } - if cred.pop().is_none() || cred.is_empty() { - return None; +impl Credentials for KnownCredentials { + fn password(&self) -> io::Result { + Ok(self.password.clone()) } +} + +impl Credentials for FileCredentials { + fn password(&self) -> io::Result { + if !&self.path.as_path().exists() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Cannot find the file with credentials.", + )); + } - Some(cred) + if let Ok(file) = File::open(&self.path) { + let line = BufReader::new(file).lines().next(); + + if let Some(password) = line { + return Ok(password?); + } + } + + Err(io::Error::new( + io::ErrorKind::Other, + "Failed to open the file", + )) + } } -/// Asks user to enter credentials interactively -fn get_credentials_from_user() -> Option { - println!("Enter credentials needed for accessing installation server"); +impl Credentials for MissingCredentials { + fn password(&self) -> io::Result { + println!("Enter credentials needed for accessing installation server"); - let password = read_credential("Password".to_string())?; + let password = read_credential("Password".to_string())?; - Some(password) + Ok(password) + } } -/// Handles various ways how to get user name / password from user or read it from a file -fn parse_login_options(options: LoginOptions) -> Option { - match options.password { - // explicitly provided user + password - Some(p) => Some(p), - _ => match options.file { - // try to read user + password from a file - Some(f) => get_credentials_from_file(f), - // last instance - ask user to enter user + password interactively - _ => get_credentials_from_user(), - }, +fn read_credential(caption: String) -> io::Result { + let mut cred = String::new(); + + println!("{}: ", caption); + + io::stdin().read_line(&mut cred)?; + if cred.pop().is_none() || cred.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to read {}", caption), + )); } + + Ok(cred) } fn login(password: String) -> anyhow::Result<()> { From 262f48cf36f0f319aebf9c0bd0067e6ca1f7d4c5 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Sat, 2 Mar 2024 10:18:05 +0100 Subject: [PATCH 12/38] Documentation --- rust/agama-cli/src/server.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 84d7073c80..3585d9e30b 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -47,12 +47,15 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { } } +/// Stores user provided configuration for login command struct LoginOptions { password: Option, file: Option, } impl LoginOptions { + /// Transforms user provided options into internal representation + /// See Credentials trait fn parse(options: LoginOptions) -> Box { match options.password { // explicitly provided user + password @@ -67,16 +70,20 @@ impl LoginOptions { } } +/// Placeholder for no configuration provided by user struct MissingCredentials; +/// Stores whatever is needed for reading credentials from a file struct FileCredentials { path: PathBuf, } +/// Stores credentials as provided by the user directly struct KnownCredentials { password: String, } +/// Transforms credentials from user's input into format used internaly trait Credentials { fn password(&self) -> io::Result; } @@ -97,6 +104,8 @@ impl Credentials for FileCredentials { } if let Ok(file) = File::open(&self.path) { + // cares only of first line, take everything. No comments + // or something like that supported let line = BufReader::new(file).lines().next(); if let Some(password) = line { @@ -121,6 +130,7 @@ impl Credentials for MissingCredentials { } } +/// Asks user to provide a line of input. Displays a prompt. fn read_credential(caption: String) -> io::Result { let mut cred = String::new(); @@ -137,6 +147,7 @@ fn read_credential(caption: String) -> io::Result { Ok(cred) } +/// Logs into the installation web server and stores JWT for later use. fn login(password: String) -> anyhow::Result<()> { // 1) ask web server for JWT // 2) if successful store the JWT for later use @@ -146,6 +157,7 @@ fn login(password: String) -> anyhow::Result<()> { Err(anyhow::anyhow!("Not implemented")) } +/// Releases JWT fn logout() -> anyhow::Result<()> { Err(anyhow::anyhow!("Not implemented")) } From c4b45b2240a1feeeaa7b9760ca6240f1be44e30a Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 10:02:55 +0100 Subject: [PATCH 13/38] Implemented Login / Logout subcommands --- rust/Cargo.lock | 361 ++++++++++++++++++++++++++++++++--- rust/agama-cli/Cargo.toml | 1 + rust/agama-cli/src/server.rs | 46 ++++- 3 files changed, 378 insertions(+), 30 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7d48a4957c..53fc17a44e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -32,6 +32,7 @@ dependencies = [ "indicatif", "log", "nix 0.27.1", + "reqwest", "serde", "serde_json", "serde_yaml", @@ -425,10 +426,10 @@ dependencies = [ "base64 0.21.7", "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "http-body-util", - "hyper", + "hyper 1.1.0", "hyper-util", "itoa", "matchit", @@ -460,8 +461,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "http-body-util", "mime", "pin-project-lite", @@ -483,8 +484,8 @@ dependencies = [ "bytes", "futures-util", "headers", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "http-body-util", "mime", "pin-project-lite", @@ -843,6 +844,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -1016,6 +1027,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "enumflags2" version = "0.7.8" @@ -1132,6 +1152,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" @@ -1296,6 +1331,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.2" @@ -1307,7 +1361,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 1.0.0", "indexmap 2.1.0", "slab", "tokio", @@ -1342,7 +1396,7 @@ dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http", + "http 1.0.0", "httpdate", "mime", "sha1", @@ -1354,7 +1408,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.0.0", ] [[package]] @@ -1384,6 +1438,17 @@ dependencies = [ "digest", ] +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.0.0" @@ -1395,6 +1460,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.0" @@ -1402,7 +1478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http", + "http 1.0.0", ] [[package]] @@ -1413,8 +1489,8 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" dependencies = [ "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -1430,6 +1506,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.1.0" @@ -1439,9 +1539,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", "httparse", "httpdate", "itoa", @@ -1449,6 +1549,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.28", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.3" @@ -1457,9 +1570,9 @@ checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", "pin-project-lite", "socket2 0.5.5", "tokio", @@ -1559,6 +1672,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "iso8601" version = "0.5.1" @@ -1820,6 +1939,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.26.4" @@ -2014,6 +2151,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" @@ -2022,9 +2185,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", @@ -2462,6 +2625,46 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "reqwest" +version = "0.11.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.17.7" @@ -2537,6 +2740,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -2564,6 +2776,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.196" @@ -2848,6 +3083,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "systemd-journal-logger" version = "1.0.0" @@ -3011,6 +3267,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -3119,8 +3385,8 @@ dependencies = [ "bitflags 2.4.1", "bytes", "futures-core", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "http-body-util", "pin-project-lite", "tokio", @@ -3211,6 +3477,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tungstenite" version = "0.21.0" @@ -3220,7 +3492,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.0.0", "httparse", "log", "rand", @@ -3396,6 +3668,15 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3427,6 +3708,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.91" @@ -3456,6 +3749,16 @@ version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +[[package]] +name = "web-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3712,6 +4015,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "xdg-home" version = "1.0.0" diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index 51873d5e41..18bd1bd63d 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -25,6 +25,7 @@ nix = { version = "0.27.1", features = ["user"] } zbus = { version = "3", default-features = false, features = ["tokio"] } tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } async-trait = "0.1.77" +reqwest = { version = "0.11", features = ["json"] } [[bin]] name = "agama" diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 3585d9e30b..bb6d777ebc 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -1,9 +1,12 @@ use clap::Subcommand; +use reqwest::header::{HeaderMap, CONTENT_TYPE, HeaderValue}; use std::io; use std::io::{BufRead, BufReader}; use std::fs::File; use std::path::{PathBuf}; +const DEFAULT_JWT_FILE: &str = "/tmp/agama-jwt"; + #[derive(Subcommand, Debug)] pub enum ServerCommands { /// Login with defined server. Result is JWT stored and used in all subsequent commands @@ -37,7 +40,7 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { file: file, }; - login(LoginOptions::parse(options).password()?) + login(LoginOptions::parse(options).password()?).await }, ServerCommands::Logout => { // actions to do: @@ -147,17 +150,48 @@ fn read_credential(caption: String) -> io::Result { Ok(cred) } +/// Necessary http request header for authenticate +fn authenticate_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + headers +} + +async fn get_jwt(password: String) -> anyhow::Result { + let client = reqwest::Client::new(); + let response = client + .post("http://localhost:3000/authenticate") + .headers(authenticate_headers()) + .body(format!("{{\"password\": \"{}\"}}", password)) + .send() + .await?; + let body = response.json::>() + .await?; + let value = body.get(&"token".to_string()); + + if let Some(token) = value { + return Ok(token.clone()) + } + + Err(anyhow::anyhow!("Failed to get JWT token")) +} + /// Logs into the installation web server and stores JWT for later use. -fn login(password: String) -> anyhow::Result<()> { +async fn login(password: String) -> anyhow::Result<()> { // 1) ask web server for JWT + let res = get_jwt(password).await?; + // 2) if successful store the JWT for later use - println!("Loging with credentials:"); - println!("({})", password); + std::fs::write(DEFAULT_JWT_FILE, res)?; - Err(anyhow::anyhow!("Not implemented")) + Ok(()) } /// Releases JWT fn logout() -> anyhow::Result<()> { - Err(anyhow::anyhow!("Not implemented")) + std::fs::remove_file(DEFAULT_JWT_FILE)?; + + Ok(()) } From a47caee0d6e7620753b157fff87897d7296f240a Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 10:01:44 +0100 Subject: [PATCH 14/38] Small cleanup --- rust/agama-cli/src/server.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index bb6d777ebc..089701c9f7 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -6,6 +6,7 @@ use std::fs::File; use std::path::{PathBuf}; const DEFAULT_JWT_FILE: &str = "/tmp/agama-jwt"; +const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; #[derive(Subcommand, Debug)] pub enum ServerCommands { @@ -159,10 +160,11 @@ fn authenticate_headers() -> HeaderMap { headers } -async fn get_jwt(password: String) -> anyhow::Result { +/// Query web server for JWT +async fn get_jwt(url: String, password: String) -> anyhow::Result { let client = reqwest::Client::new(); let response = client - .post("http://localhost:3000/authenticate") + .post(url) .headers(authenticate_headers()) .body(format!("{{\"password\": \"{}\"}}", password)) .send() @@ -181,7 +183,7 @@ async fn get_jwt(password: String) -> anyhow::Result { /// Logs into the installation web server and stores JWT for later use. async fn login(password: String) -> anyhow::Result<()> { // 1) ask web server for JWT - let res = get_jwt(password).await?; + let res = get_jwt(DEFAULT_AUTH_URL.to_string(), password).await?; // 2) if successful store the JWT for later use std::fs::write(DEFAULT_JWT_FILE, res)?; @@ -191,7 +193,5 @@ async fn login(password: String) -> anyhow::Result<()> { /// Releases JWT fn logout() -> anyhow::Result<()> { - std::fs::remove_file(DEFAULT_JWT_FILE)?; - - Ok(()) + Ok(std::fs::remove_file(DEFAULT_JWT_FILE)?) } From cfaaa2f208e44184eb57b0268e112f8e972ed03c Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Tue, 5 Mar 2024 08:04:31 +0100 Subject: [PATCH 15/38] Formatting --- rust/agama-cli/src/commands.rs | 4 ++-- rust/agama-cli/src/server.rs | 34 ++++++++++++++++------------------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/rust/agama-cli/src/commands.rs b/rust/agama-cli/src/commands.rs index 52b3aaf1d2..a2a3c6bce6 100644 --- a/rust/agama-cli/src/commands.rs +++ b/rust/agama-cli/src/commands.rs @@ -1,8 +1,8 @@ use crate::config::ConfigCommands; use crate::logs::LogsCommands; -use crate::server::ServerCommands; use crate::profile::ProfileCommands; use crate::questions::QuestionsCommands; +use crate::server::ServerCommands; use clap::Subcommand; #[derive(Subcommand, Debug)] @@ -36,7 +36,7 @@ pub enum Commands { /// Collects logs #[command(subcommand)] Logs(LogsCommands), - /// Request login with the web server + /// Request an action on the web server like Login / Logout #[command(subcommand)] Server(ServerCommands), } diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 089701c9f7..b150f09ea6 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -1,9 +1,9 @@ use clap::Subcommand; -use reqwest::header::{HeaderMap, CONTENT_TYPE, HeaderValue}; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use std::fs::File; use std::io; use std::io::{BufRead, BufReader}; -use std::fs::File; -use std::path::{PathBuf}; +use std::path::PathBuf; const DEFAULT_JWT_FILE: &str = "/tmp/agama-jwt"; const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; @@ -12,10 +12,10 @@ const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; pub enum ServerCommands { /// Login with defined server. Result is JWT stored and used in all subsequent commands Login { - #[clap(long, short = 'p')] - password: Option, - #[clap(long, short = 'f')] - file: Option, + #[clap(long, short = 'p')] + password: Option, + #[clap(long, short = 'f')] + file: Option, }, /// Release currently stored JWT Logout, @@ -24,10 +24,7 @@ pub enum ServerCommands { /// Main entry point called from agama CLI main loop pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { match subcommand { - ServerCommands::Login { - password, - file, - }=> { + ServerCommands::Login { password, file } => { // actions to do: // 1) somehow obtain credentials (interactive, commandline, from a file) // credentials are handled in this way (in descending priority) @@ -42,12 +39,12 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { }; login(LoginOptions::parse(options).password()?).await - }, + } ServerCommands::Logout => { // actions to do: // 1) release JWT from the well known location if any logout() - }, + } } } @@ -104,7 +101,7 @@ impl Credentials for FileCredentials { return Err(io::Error::new( io::ErrorKind::Other, "Cannot find the file with credentials.", - )); + )); } if let Ok(file) = File::open(&self.path) { @@ -120,7 +117,7 @@ impl Credentials for FileCredentials { Err(io::Error::new( io::ErrorKind::Other, "Failed to open the file", - )) + )) } } @@ -145,7 +142,7 @@ fn read_credential(caption: String) -> io::Result { return Err(io::Error::new( io::ErrorKind::Other, format!("Failed to read {}", caption), - )); + )); } Ok(cred) @@ -169,12 +166,13 @@ async fn get_jwt(url: String, password: String) -> anyhow::Result { .body(format!("{{\"password\": \"{}\"}}", password)) .send() .await?; - let body = response.json::>() + let body = response + .json::>() .await?; let value = body.get(&"token".to_string()); if let Some(token) = value { - return Ok(token.clone()) + return Ok(token.clone()); } Err(anyhow::anyhow!("Failed to get JWT token")) From b1ae6cb97584c4875b6006ba99e9bc3b455e0c99 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Tue, 5 Mar 2024 08:27:10 +0100 Subject: [PATCH 16/38] Decrease noisyness --- rust/agama-cli/src/server.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index b150f09ea6..c8438c5456 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -123,8 +123,6 @@ impl Credentials for FileCredentials { impl Credentials for MissingCredentials { fn password(&self) -> io::Result { - println!("Enter credentials needed for accessing installation server"); - let password = read_credential("Password".to_string())?; Ok(password) From fd28d84cdbc7dee3f4a667ec01d10bf7acd3af05 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 09:02:59 +0100 Subject: [PATCH 17/38] A cleanup according to the review - mostly comments --- rust/agama-cli/src/server.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index c8438c5456..921a0f7a92 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -10,7 +10,9 @@ const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; #[derive(Subcommand, Debug)] pub enum ServerCommands { - /// Login with defined server. Result is JWT stored and used in all subsequent commands + /// Login with defined server. Result is JWT stored locally and made available to + /// further use. Password can be provided by commandline option, from a file or it fallbacks + /// into an interactive prompt. Login { #[clap(long, short = 'p')] password: Option, @@ -25,20 +27,12 @@ pub enum ServerCommands { pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { match subcommand { ServerCommands::Login { password, file } => { - // actions to do: - // 1) somehow obtain credentials (interactive, commandline, from a file) - // credentials are handled in this way (in descending priority) - // "command line options" -> "read from file" -> "ask to the user" - // 2) pass credentials to the web server - // 3) receive JWT - // 4) store the JWT in a well known location - let options = LoginOptions { password: password, file: file, }; - login(LoginOptions::parse(options).password()?).await + login(LoginOptions::proceed(options).password()?).await } ServerCommands::Logout => { // actions to do: @@ -57,7 +51,7 @@ struct LoginOptions { impl LoginOptions { /// Transforms user provided options into internal representation /// See Credentials trait - fn parse(options: LoginOptions) -> Box { + fn proceed(options: LoginOptions) -> Box { match options.password { // explicitly provided user + password Some(p) => Box::new(KnownCredentials { password: p }), @@ -100,7 +94,7 @@ impl Credentials for FileCredentials { if !&self.path.as_path().exists() { return Err(io::Error::new( io::ErrorKind::Other, - "Cannot find the file with credentials.", + "Cannot find the file containing the credentials.", )); } From a737cd404b5ed3e21ba08b2a38d4f613ffa47cc6 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 11:33:55 +0100 Subject: [PATCH 18/38] Restrict permissions on file with JWT --- rust/agama-cli/src/server.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 921a0f7a92..f34a1ef483 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -1,12 +1,15 @@ use clap::Subcommand; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use std::fs; use std::fs::File; use std::io; use std::io::{BufRead, BufReader}; +use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; const DEFAULT_JWT_FILE: &str = "/tmp/agama-jwt"; const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; +const DEFAULT_FILE_MODE: u32 = 0o600; #[derive(Subcommand, Debug)] pub enum ServerCommands { @@ -140,6 +143,19 @@ fn read_credential(caption: String) -> io::Result { Ok(cred) } +/// Sets the archive owner to root:root. Also sets the file permissions to read/write for the +/// owner only. +fn set_file_permissions(file: &String) -> io::Result<()> { + let attr = fs::metadata(file)?; + let mut permissions = attr.permissions(); + + // set the file file permissions to -rw------- + permissions.set_mode(DEFAULT_FILE_MODE); + fs::set_permissions(file, permissions)?; + + Ok(()) +} + /// Necessary http request header for authenticate fn authenticate_headers() -> HeaderMap { let mut headers = HeaderMap::new(); @@ -177,6 +193,7 @@ async fn login(password: String) -> anyhow::Result<()> { // 2) if successful store the JWT for later use std::fs::write(DEFAULT_JWT_FILE, res)?; + set_file_permissions(&DEFAULT_JWT_FILE.to_string())?; Ok(()) } From c5a3d4d004d51c02b66378d38c774ba63e313743 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 11:06:37 +0100 Subject: [PATCH 19/38] Use clap Args for struct with options to look more cool --- rust/agama-cli/src/server.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index f34a1ef483..1d9b31e554 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -1,4 +1,4 @@ -use clap::Subcommand; +use clap::{arg, Args, Subcommand}; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use std::fs; use std::fs::File; @@ -16,12 +16,7 @@ pub enum ServerCommands { /// Login with defined server. Result is JWT stored locally and made available to /// further use. Password can be provided by commandline option, from a file or it fallbacks /// into an interactive prompt. - Login { - #[clap(long, short = 'p')] - password: Option, - #[clap(long, short = 'f')] - file: Option, - }, + Login(LoginOptions), /// Release currently stored JWT Logout, } @@ -29,25 +24,21 @@ pub enum ServerCommands { /// Main entry point called from agama CLI main loop pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { match subcommand { - ServerCommands::Login { password, file } => { - let options = LoginOptions { - password: password, - file: file, - }; - + ServerCommands::Login(options) => { login(LoginOptions::proceed(options).password()?).await } ServerCommands::Logout => { - // actions to do: - // 1) release JWT from the well known location if any logout() } } } /// Stores user provided configuration for login command -struct LoginOptions { +#[derive(Args, Debug)] +pub struct LoginOptions { + #[arg(long, short = 'p')] password: Option, + #[arg(long, short = 'f')] file: Option, } From 2fa0f443a3aa69e0474cd9c65b10e2096ccb092b Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 11:16:24 +0100 Subject: [PATCH 20/38] Do not raise false error when called logout without previous login --- rust/agama-cli/src/server.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/server.rs index 1d9b31e554..46eea99960 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/server.rs @@ -5,7 +5,7 @@ use std::fs::File; use std::io; use std::io::{BufRead, BufReader}; use std::os::unix::fs::PermissionsExt; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const DEFAULT_JWT_FILE: &str = "/tmp/agama-jwt"; const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; @@ -16,7 +16,7 @@ pub enum ServerCommands { /// Login with defined server. Result is JWT stored locally and made available to /// further use. Password can be provided by commandline option, from a file or it fallbacks /// into an interactive prompt. - Login(LoginOptions), + Login(LoginArgs), /// Release currently stored JWT Logout, } @@ -25,7 +25,7 @@ pub enum ServerCommands { pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { match subcommand { ServerCommands::Login(options) => { - login(LoginOptions::proceed(options).password()?).await + login(LoginArgs::proceed(options).password()?).await } ServerCommands::Logout => { logout() @@ -35,17 +35,17 @@ pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { /// Stores user provided configuration for login command #[derive(Args, Debug)] -pub struct LoginOptions { +pub struct LoginArgs { #[arg(long, short = 'p')] password: Option, #[arg(long, short = 'f')] file: Option, } -impl LoginOptions { +impl LoginArgs { /// Transforms user provided options into internal representation /// See Credentials trait - fn proceed(options: LoginOptions) -> Box { + fn proceed(options: LoginArgs) -> Box { match options.password { // explicitly provided user + password Some(p) => Box::new(KnownCredentials { password: p }), @@ -191,5 +191,10 @@ async fn login(password: String) -> anyhow::Result<()> { /// Releases JWT fn logout() -> anyhow::Result<()> { + // mask if the file with the JWT doesn't exist (most probably no login before logout) + if !Path::new(DEFAULT_JWT_FILE).exists() { + return Ok(()) + } + Ok(std::fs::remove_file(DEFAULT_JWT_FILE)?) } From 47a928a3d8a1f95d9ab317e150a4f25247916ca2 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 11:25:40 +0100 Subject: [PATCH 21/38] Renamed command from 'Server' to 'Auth' to save first one for later use --- rust/agama-cli/src/{server.rs => auth.rs} | 8 ++++---- rust/agama-cli/src/commands.rs | 4 ++-- rust/agama-cli/src/main.rs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename rust/agama-cli/src/{server.rs => auth.rs} (97%) diff --git a/rust/agama-cli/src/server.rs b/rust/agama-cli/src/auth.rs similarity index 97% rename from rust/agama-cli/src/server.rs rename to rust/agama-cli/src/auth.rs index 46eea99960..1ae064c3df 100644 --- a/rust/agama-cli/src/server.rs +++ b/rust/agama-cli/src/auth.rs @@ -12,7 +12,7 @@ const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; const DEFAULT_FILE_MODE: u32 = 0o600; #[derive(Subcommand, Debug)] -pub enum ServerCommands { +pub enum AuthCommands { /// Login with defined server. Result is JWT stored locally and made available to /// further use. Password can be provided by commandline option, from a file or it fallbacks /// into an interactive prompt. @@ -22,12 +22,12 @@ pub enum ServerCommands { } /// Main entry point called from agama CLI main loop -pub async fn run(subcommand: ServerCommands) -> anyhow::Result<()> { +pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { match subcommand { - ServerCommands::Login(options) => { + AuthCommands::Login(options) => { login(LoginArgs::proceed(options).password()?).await } - ServerCommands::Logout => { + AuthCommands::Logout => { logout() } } diff --git a/rust/agama-cli/src/commands.rs b/rust/agama-cli/src/commands.rs index a2a3c6bce6..46615a4abe 100644 --- a/rust/agama-cli/src/commands.rs +++ b/rust/agama-cli/src/commands.rs @@ -1,8 +1,8 @@ +use crate::auth::AuthCommands; use crate::config::ConfigCommands; use crate::logs::LogsCommands; use crate::profile::ProfileCommands; use crate::questions::QuestionsCommands; -use crate::server::ServerCommands; use clap::Subcommand; #[derive(Subcommand, Debug)] @@ -38,5 +38,5 @@ pub enum Commands { Logs(LogsCommands), /// Request an action on the web server like Login / Logout #[command(subcommand)] - Server(ServerCommands), + Auth(AuthCommands), } diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index d5da2244f8..33117ae481 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -1,5 +1,6 @@ use clap::Parser; +mod auth; mod commands; mod config; mod error; @@ -8,12 +9,12 @@ mod printers; mod profile; mod progress; mod questions; -mod server; use crate::error::CliError; use agama_lib::error::ServiceError; use agama_lib::manager::ManagerClient; use agama_lib::progress::ProgressMonitor; +use auth::run as run_auth_cmd; use commands::Commands; use config::run as run_config_cmd; use logs::run as run_logs_cmd; @@ -21,7 +22,6 @@ use printers::Format; use profile::run as run_profile_cmd; use progress::InstallerProgress; use questions::run as run_questions_cmd; -use server::run as run_server_cmd; use std::{ process::{ExitCode, Termination}, thread::sleep, @@ -137,7 +137,7 @@ async fn run_command(cli: Cli) -> anyhow::Result<()> { } Commands::Questions(subcommand) => run_questions_cmd(subcommand).await, Commands::Logs(subcommand) => run_logs_cmd(subcommand).await, - Commands::Server(subcommand) => run_server_cmd(subcommand).await, + Commands::Auth(subcommand) => run_auth_cmd(subcommand).await, _ => unimplemented!(), } } From 40f41e171905331f17781017c89b2fc2db0ef1bc Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 11:42:15 +0100 Subject: [PATCH 22/38] Formatting --- rust/agama-cli/src/auth.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 1ae064c3df..4d13235239 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -24,12 +24,8 @@ pub enum AuthCommands { /// Main entry point called from agama CLI main loop pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { match subcommand { - AuthCommands::Login(options) => { - login(LoginArgs::proceed(options).password()?).await - } - AuthCommands::Logout => { - logout() - } + AuthCommands::Login(options) => login(LoginArgs::proceed(options).password()?).await, + AuthCommands::Logout => logout(), } } @@ -193,7 +189,7 @@ async fn login(password: String) -> anyhow::Result<()> { fn logout() -> anyhow::Result<()> { // mask if the file with the JWT doesn't exist (most probably no login before logout) if !Path::new(DEFAULT_JWT_FILE).exists() { - return Ok(()) + return Ok(()); } Ok(std::fs::remove_file(DEFAULT_JWT_FILE)?) From b752439105fd0d10e5b36f1dd3f37f2fac555fb0 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 12:22:27 +0100 Subject: [PATCH 23/38] Introduced public function for getting stored JWT + new subcommand Show --- rust/agama-cli/src/auth.rs | 67 ++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 4d13235239..008605ac7d 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -19,6 +19,8 @@ pub enum AuthCommands { Login(LoginArgs), /// Release currently stored JWT Logout, + /// Prints currently stored JWT to stdout + Show, } /// Main entry point called from agama CLI main loop @@ -26,6 +28,17 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { match subcommand { AuthCommands::Login(options) => login(LoginArgs::proceed(options).password()?).await, AuthCommands::Logout => logout(), + AuthCommands::Show => show(), + } +} + +pub fn jwt() -> anyhow::Result { + let jwt = read_line_from_file(&Path::new(DEFAULT_JWT_FILE)); + + if let Ok(token) = jwt { + return Ok(token); + } else { + return Err(anyhow::anyhow!("JWT not available")); } } @@ -81,27 +94,7 @@ impl Credentials for KnownCredentials { impl Credentials for FileCredentials { fn password(&self) -> io::Result { - if !&self.path.as_path().exists() { - return Err(io::Error::new( - io::ErrorKind::Other, - "Cannot find the file containing the credentials.", - )); - } - - if let Ok(file) = File::open(&self.path) { - // cares only of first line, take everything. No comments - // or something like that supported - let line = BufReader::new(file).lines().next(); - - if let Some(password) = line { - return Ok(password?); - } - } - - Err(io::Error::new( - io::ErrorKind::Other, - "Failed to open the file", - )) + read_line_from_file(&self.path.as_path()) } } @@ -113,6 +106,31 @@ impl Credentials for MissingCredentials { } } +/// Reads first line from given file +fn read_line_from_file(path: &Path) -> io::Result { + if !&path.exists() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Cannot find the file containing the credentials.", + )); + } + + if let Ok(file) = File::open(&path) { + // cares only of first line, take everything. No comments + // or something like that supported + let raw = BufReader::new(file).lines().next(); + + if let Some(line) = raw { + return Ok(line?); + } + } + + Err(io::Error::new( + io::ErrorKind::Other, + "Failed to open the file", + )) +} + /// Asks user to provide a line of input. Displays a prompt. fn read_credential(caption: String) -> io::Result { let mut cred = String::new(); @@ -194,3 +212,10 @@ fn logout() -> anyhow::Result<()> { Ok(std::fs::remove_file(DEFAULT_JWT_FILE)?) } + +/// Shows stored JWT on stdout +fn show() -> anyhow::Result<()> { + println!("{}", jwt()?); + + Ok(()) +} From 53a5baedb7236ff7da03d31e791966b0454946f3 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 20:11:40 +0100 Subject: [PATCH 24/38] Modified handling of path where JWT is stored --- rust/Cargo.lock | 10 +++++++++ rust/agama-cli/Cargo.toml | 1 + rust/agama-cli/src/auth.rs | 45 +++++++++++++++++++++++++++----------- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 53fc17a44e..9d27a50a88 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "console", "convert_case", "fs_extra", + "home", "indicatif", "log", "nix 0.27.1", @@ -1438,6 +1439,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.11" diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index 18bd1bd63d..8a073f259e 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -26,6 +26,7 @@ zbus = { version = "3", default-features = false, features = ["tokio"] } tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } async-trait = "0.1.77" reqwest = { version = "0.11", features = ["json"] } +home = "0.5.9" [[bin]] name = "agama" diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 008605ac7d..d6d73f093b 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -1,4 +1,5 @@ use clap::{arg, Args, Subcommand}; +use home; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use std::fs; use std::fs::File; @@ -7,7 +8,7 @@ use std::io::{BufRead, BufReader}; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; -const DEFAULT_JWT_FILE: &str = "/tmp/agama-jwt"; +const DEFAULT_JWT_FILE: &str = ".agama/agama-jwt"; const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; const DEFAULT_FILE_MODE: u32 = 0o600; @@ -33,13 +34,13 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { } pub fn jwt() -> anyhow::Result { - let jwt = read_line_from_file(&Path::new(DEFAULT_JWT_FILE)); - - if let Ok(token) = jwt { - return Ok(token); - } else { - return Err(anyhow::anyhow!("JWT not available")); + if let Some(file) = jwt_file() { + if let Ok(token) = read_line_from_file(&file.as_path()) { + return Ok(token); + } } + + return Err(anyhow::anyhow!("JWT not available")); } /// Stores user provided configuration for login command @@ -106,6 +107,11 @@ impl Credentials for MissingCredentials { } } +/// Path to file where JWT is stored +fn jwt_file() -> Option { + Some(home::home_dir()?.join(DEFAULT_JWT_FILE)) +} + /// Reads first line from given file fn read_line_from_file(path: &Path) -> io::Result { if !&path.exists() { @@ -150,7 +156,7 @@ fn read_credential(caption: String) -> io::Result { /// Sets the archive owner to root:root. Also sets the file permissions to read/write for the /// owner only. -fn set_file_permissions(file: &String) -> io::Result<()> { +fn set_file_permissions(file: &Path) -> io::Result<()> { let attr = fs::metadata(file)?; let mut permissions = attr.permissions(); @@ -197,20 +203,33 @@ async fn login(password: String) -> anyhow::Result<()> { let res = get_jwt(DEFAULT_AUTH_URL.to_string(), password).await?; // 2) if successful store the JWT for later use - std::fs::write(DEFAULT_JWT_FILE, res)?; - set_file_permissions(&DEFAULT_JWT_FILE.to_string())?; + if let Some(path) = jwt_file() { + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir)?; + } else { + return Err(anyhow::anyhow!("Cannot store the JWT token")); + } + + std::fs::write(path.as_path(), res)?; + set_file_permissions(path.as_path())?; + } + Ok(()) } /// Releases JWT fn logout() -> anyhow::Result<()> { - // mask if the file with the JWT doesn't exist (most probably no login before logout) - if !Path::new(DEFAULT_JWT_FILE).exists() { + let path = jwt_file(); + + if !&path.clone().is_some_and(|p| p.as_path().exists()) { + // mask if the file with the JWT doesn't exist (most probably no login before logout) return Ok(()); } - Ok(std::fs::remove_file(DEFAULT_JWT_FILE)?) + let file = path.expect("Cannot locate stored JWT"); + + return Ok(std::fs::remove_file(file)?) } /// Shows stored JWT on stdout From fa4c21109ad5e561adadde5160b74cf02f33f915 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Wed, 6 Mar 2024 20:46:44 +0100 Subject: [PATCH 25/38] Minor cleanup. Some docs. --- rust/agama-cli/src/auth.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index d6d73f093b..08a9c47c85 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -33,6 +33,7 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { } } +/// Reads stored JWT token and returns it pub fn jwt() -> anyhow::Result { if let Some(file) = jwt_file() { if let Ok(token) = read_line_from_file(&file.as_path()) { @@ -205,16 +206,15 @@ async fn login(password: String) -> anyhow::Result<()> { // 2) if successful store the JWT for later use if let Some(path) = jwt_file() { if let Some(dir) = path.parent() { - std::fs::create_dir_all(dir)?; + fs::create_dir_all(dir)?; } else { return Err(anyhow::anyhow!("Cannot store the JWT token")); } - std::fs::write(path.as_path(), res)?; + fs::write(path.as_path(), res)?; set_file_permissions(path.as_path())?; } - Ok(()) } @@ -222,19 +222,25 @@ async fn login(password: String) -> anyhow::Result<()> { fn logout() -> anyhow::Result<()> { let path = jwt_file(); - if !&path.clone().is_some_and(|p| p.as_path().exists()) { + if !&path.clone().is_some_and(|p| p.exists()) { // mask if the file with the JWT doesn't exist (most probably no login before logout) return Ok(()); } + // panicking is right thing to do if expect fails, becase it was already checked twice that + // the path exists let file = path.expect("Cannot locate stored JWT"); - return Ok(std::fs::remove_file(file)?) + Ok(fs::remove_file(file)?) } /// Shows stored JWT on stdout fn show() -> anyhow::Result<()> { - println!("{}", jwt()?); + // we do not care if jwt() fails or not. If there is something to print, show it otherwise + // stay silent + if let Ok(token) = jwt() { + println!("{}", token); + } Ok(()) } From 9042a193e9f1d3c4a0a687c710b309bfd627e274 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Thu, 7 Mar 2024 10:44:26 +0100 Subject: [PATCH 26/38] Updates according to the review --- rust/agama-cli/src/auth.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 08a9c47c85..9965bdd7fc 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -9,7 +9,7 @@ use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; const DEFAULT_JWT_FILE: &str = ".agama/agama-jwt"; -const DEFAULT_AUTH_URL: &str = "http://localhost:3000/authenticate"; +const DEFAULT_AUTH_URL: &str = "http://localhost:3000/api/authenticate"; const DEFAULT_FILE_MODE: u32 = 0o600; #[derive(Subcommand, Debug)] @@ -33,7 +33,7 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { } } -/// Reads stored JWT token and returns it +/// Reads stored token and returns it pub fn jwt() -> anyhow::Result { if let Some(file) = jwt_file() { if let Ok(token) = read_line_from_file(&file.as_path()) { @@ -41,7 +41,7 @@ pub fn jwt() -> anyhow::Result { } } - return Err(anyhow::anyhow!("JWT not available")); + Err(anyhow::anyhow!("JWT not available")) } /// Stores user provided configuration for login command @@ -57,15 +57,12 @@ impl LoginArgs { /// Transforms user provided options into internal representation /// See Credentials trait fn proceed(options: LoginArgs) -> Box { - match options.password { - // explicitly provided user + password - Some(p) => Box::new(KnownCredentials { password: p }), - _ => match options.file { - // try to read user + password from a file - Some(f) => Box::new(FileCredentials { path: f }), - // last instance - ask user to enter user + password interactively - _ => Box::new(MissingCredentials {}), - }, + if let Some(password) = options.password { + Box::new(KnownCredentials { password }) + } else if let Some(path) = options.file { + Box::new(FileCredentials { path }) + } else { + Box::new(MissingCredentials {}) } } } @@ -115,7 +112,7 @@ fn jwt_file() -> Option { /// Reads first line from given file fn read_line_from_file(path: &Path) -> io::Result { - if !&path.exists() { + if !path.exists() { return Err(io::Error::new( io::ErrorKind::Other, "Cannot find the file containing the credentials.", @@ -195,7 +192,7 @@ async fn get_jwt(url: String, password: String) -> anyhow::Result { return Ok(token.clone()); } - Err(anyhow::anyhow!("Failed to get JWT token")) + Err(anyhow::anyhow!("Failed to get authentication token")) } /// Logs into the installation web server and stores JWT for later use. @@ -208,7 +205,7 @@ async fn login(password: String) -> anyhow::Result<()> { if let Some(dir) = path.parent() { fs::create_dir_all(dir)?; } else { - return Err(anyhow::anyhow!("Cannot store the JWT token")); + return Err(anyhow::anyhow!("Cannot store the authentication token")); } fs::write(path.as_path(), res)?; From 72aa16c95bcc5d469a38a9c2b5b0d40bf5cd3ec5 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Thu, 7 Mar 2024 11:38:32 +0100 Subject: [PATCH 27/38] Reformated message --- rust/agama-cli/src/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 9965bdd7fc..796e8ccc44 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -41,7 +41,7 @@ pub fn jwt() -> anyhow::Result { } } - Err(anyhow::anyhow!("JWT not available")) + Err(anyhow::anyhow!("Authentication token not available")) } /// Stores user provided configuration for login command From b8991fe76f3bf1175847049a71b5ad428826e7d4 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Thu, 7 Mar 2024 11:40:24 +0100 Subject: [PATCH 28/38] Minor leftovers from the review --- rust/agama-cli/src/auth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 796e8ccc44..0e1c34bf13 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -34,7 +34,7 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { } /// Reads stored token and returns it -pub fn jwt() -> anyhow::Result { +fn jwt() -> anyhow::Result { if let Some(file) = jwt_file() { if let Ok(token) = read_line_from_file(&file.as_path()) { return Ok(token); @@ -125,7 +125,7 @@ fn read_line_from_file(path: &Path) -> io::Result { let raw = BufReader::new(file).lines().next(); if let Some(line) = raw { - return Ok(line?); + return line; } } From 4754ad93ef71251a59f31b4e54d1d13939ff7986 Mon Sep 17 00:00:00 2001 From: Michal Filka Date: Thu, 7 Mar 2024 11:55:15 +0100 Subject: [PATCH 29/38] Updated changelog --- rust/package/agama.changes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 656cd074eb..8e64449ffe 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Thu Mar 7 10:52:58 UTC 2024 - Michal Filka + +- CLI: added auth command with login / logout / show subcommands + for handling authentication token management with new agama web + server + ------------------------------------------------------------------- Tue Feb 27 15:55:28 UTC 2024 - Imobach Gonzalez Sosa From 17a40da47792eac909273131061e74c352a61632 Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Thu, 7 Mar 2024 11:50:52 +0000 Subject: [PATCH 30/38] [web] Fixes from code review --- web/src/components/storage/ProposalTransactionalInfo.jsx | 1 - .../storage/ProposalTransactionalInfo.test.jsx | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/web/src/components/storage/ProposalTransactionalInfo.jsx b/web/src/components/storage/ProposalTransactionalInfo.jsx index 3e46a2c021..daabd72b08 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.jsx +++ b/web/src/components/storage/ProposalTransactionalInfo.jsx @@ -38,7 +38,6 @@ import { useProduct } from "~/context/product"; * * @param {object} props * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. - * @param {object} settings */ export default function ProposalTransactionalInfo({ settings }) { const transactional = isTransactionalSystem(settings?.volumes || []); diff --git a/web/src/components/storage/ProposalTransactionalInfo.test.jsx b/web/src/components/storage/ProposalTransactionalInfo.test.jsx index 8dd6838cd5..e9556107fa 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.test.jsx +++ b/web/src/components/storage/ProposalTransactionalInfo.test.jsx @@ -20,7 +20,7 @@ */ import React from "react"; -import { screen, within } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalTransactionalInfo } from "~/components/storage"; @@ -44,10 +44,9 @@ describe("if the system is not transactional", () => { props.settings = { volumes: [rootVolume] }; }); - it("does not render any explanation about transactional system", () => { - plainRender(); - - expect(screen.queryByText("Transactional root file system")).toBeNull(); + it("renders nothing", () => { + const { container } = plainRender(); + expect(container).toBeEmptyDOMElement(); }); }); From 0acb332ad409cbca3878622dfdd6c4e850756ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 7 Mar 2024 13:35:23 +0000 Subject: [PATCH 31/38] web: Add core/Reminder component Inspired in PF/Alert component, but much more simpler. --- web/src/assets/styles/blocks.scss | 21 ++++++ web/src/components/core/Reminder.jsx | 84 +++++++++++++++++++++++ web/src/components/core/Reminder.test.jsx | 65 ++++++++++++++++++ web/src/components/core/index.js | 1 + 4 files changed, 171 insertions(+) create mode 100644 web/src/components/core/Reminder.jsx create mode 100644 web/src/components/core/Reminder.test.jsx diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index 30e95e4aa4..e568ae24db 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -469,6 +469,27 @@ ul[data-type="agama/list"][role="grid"] { } } +[data-type="agama/reminder"] { + --accent-color: var(--color-primary-lighter); + --inline-margin: calc(var(--header-icon-size) + var(--spacer-small)); + + display: flex; + gap: var(--spacer-small); + margin-inline: var(--inline-margin); + margin-block-end: var(--spacer-normal); + padding: var(--spacer-smaller) var(--spacer-small); + border-inline-start: 3px solid var(--accent-color); + + svg { + fill: var(--accent-color); + } + + h4 { + color: var(--accent-color); + margin-block-end: var(--spacer-smaller); + } +} + [role="dialog"] { section:not([class^="pf-c"]) { > svg:first-child { diff --git a/web/src/components/core/Reminder.jsx b/web/src/components/core/Reminder.jsx new file mode 100644 index 0000000000..997e870a91 --- /dev/null +++ b/web/src/components/core/Reminder.jsx @@ -0,0 +1,84 @@ +/* + * 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 { Icon } from "~/components/layout"; + +/** + * Internal component for rendering the icon + * + * @param {object} props + * @params {string} [props.name] - The icon name. + */ +const ReminderIcon = ({ name }) => { + if (!name?.length) return; + + return ( +
+ +
+ ); +}; + +/** + * Internal component for rendering the title + * + * @param {object} props + * @params {JSX.Element|string} [props.children] - The title content. + */ +const ReminderTitle = ({ children }) => { + if (!children) return; + if (typeof children === "string" && !children.length) return; + + return ( +

{children}

+ ); +}; + +/** + * Renders a reminder with given role, status by default + * @component + * + * @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 + * 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. + */ +export default function Reminder ({ + icon, + title, + role = "status", + children +}) { + return ( +
+ +
+ {title} + { children } +
+
+ ); +} diff --git a/web/src/components/core/Reminder.test.jsx b/web/src/components/core/Reminder.test.jsx new file mode 100644 index 0000000000..24527cf2d0 --- /dev/null +++ b/web/src/components/core/Reminder.test.jsx @@ -0,0 +1,65 @@ +/* + * Copyright (c) [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 } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { Reminder } from "~/components/core"; + +describe("Reminder", () => { + it("renders a status region by default", () => { + plainRender(Example); + const reminder = screen.getByRole("status"); + within(reminder).getByText("Example"); + }); + + it("renders a region with given role", () => { + plainRender(Example); + const reminder = screen.getByRole("alert"); + within(reminder).getByText("Example"); + }); + + it("renders given title", () => { + plainRender( + Kindly reminder}> + Visit the settings section + + ); + screen.getByRole("heading", { name: "Kindly reminder", level: 4 }); + }); + + it("does not render a heading if title is not given", () => { + plainRender(Without title); + expect(screen.queryByRole("heading")).toBeNull(); + }); + + it("does not render a heading if title is an empty string", () => { + plainRender(Without title); + expect(screen.queryByRole("heading")).toBeNull(); + }); + + it("renders given children", () => { + plainRender( + Visit the settings section + ); + screen.getByRole("link", { name: "Visit the settings section" }); + }); +}); diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index 0213c66215..6bdac86248 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -57,3 +57,4 @@ export { default as PasswordInput } from "./PasswordInput"; export { default as DevelopmentInfo } from "./DevelopmentInfo"; export { default as Selector } from "./Selector"; export { default as OptionsPicker } from "./OptionsPicker"; +export { default as Reminder } from "./Reminder"; From 9ab4bf15786565fc8a04231cdf92bd5fb0a5fb97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 7 Mar 2024 15:16:12 +0000 Subject: [PATCH 32/38] web: Refactor ProposalTransactionalInfo component To make use of core/Reminder and return undefined as soon as possible. Commit also add few changes to helper method. --- .../storage/ProposalTransactionalInfo.jsx | 23 +++++-------------- web/src/components/storage/utils.js | 8 +++++-- web/src/components/storage/utils.test.js | 8 +++++++ 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/web/src/components/storage/ProposalTransactionalInfo.jsx b/web/src/components/storage/ProposalTransactionalInfo.jsx index daabd72b08..aa20477a6d 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.jsx +++ b/web/src/components/storage/ProposalTransactionalInfo.jsx @@ -20,11 +20,10 @@ */ import React from "react"; -import { Alert } from "@patternfly/react-core"; -import { sprintf } from "sprintf-js"; +import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -import { If, Section } from "~/components/core"; +import { Reminder } from "~/components/core"; import { isTransactionalSystem } from "~/components/storage/utils"; import { useProduct } from "~/context/product"; @@ -40,26 +39,16 @@ import { useProduct } from "~/context/product"; * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. */ export default function ProposalTransactionalInfo({ settings }) { - const transactional = isTransactionalSystem(settings?.volumes || []); const { selectedProduct } = useProduct(); + if (!isTransactionalSystem(settings?.volumes)) return; + + const title = _("Transactional root file system"); /* TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) */ const description = sprintf( _("%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots."), selectedProduct.name ); - const title = _("Transactional root file system"); - return ( - - - {description} - - - } - /> - ); + return {description}; } diff --git a/web/src/components/storage/utils.js b/web/src/components/storage/utils.js index d2322bc167..082756b08c 100644 --- a/web/src/components/storage/utils.js +++ b/web/src/components/storage/utils.js @@ -180,8 +180,12 @@ const isTransactionalRoot = (volume) => { * @param {Volume[]} volumes * @returns {boolean} */ -const isTransactionalSystem = (volumes) => { - return volumes.find(v => isTransactionalRoot(v)) !== undefined; +const isTransactionalSystem = (volumes = []) => { + try { + return volumes?.find(v => isTransactionalRoot(v)) !== undefined; + } catch { + return false; + } }; export { diff --git a/web/src/components/storage/utils.test.js b/web/src/components/storage/utils.test.js index a973e27a0a..e5eb3cf0aa 100644 --- a/web/src/components/storage/utils.test.js +++ b/web/src/components/storage/utils.test.js @@ -139,6 +139,14 @@ describe("isTransactionalRoot", () => { }); describe("isTransactionalSystem", () => { + it("returns false when a list of volumes is not given", () => { + expect(isTransactionalSystem(false)).toBe(false); + expect(isTransactionalSystem(undefined)).toBe(false); + expect(isTransactionalSystem(null)).toBe(false); + expect(isTransactionalSystem([])).toBe(false); + expect(isTransactionalSystem("fake")).toBe(false); + }); + it("returns false if volumes does not include a transactional root", () => { expect(isTransactionalSystem([])).toBe(false); From 338b8e50ed9c15de17cb4989f57a658fec7c697c Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Thu, 7 Mar 2024 15:47:48 +0000 Subject: [PATCH 33/38] [web] Small code improvement at ProposalSettingsSection --- .../storage/ProposalSettingsSection.jsx | 31 ++++++++----------- .../storage/ProposalSettingsSection.test.jsx | 2 +- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/web/src/components/storage/ProposalSettingsSection.jsx b/web/src/components/storage/ProposalSettingsSection.jsx index c898e82d48..abc6830308 100644 --- a/web/src/components/storage/ProposalSettingsSection.jsx +++ b/web/src/components/storage/ProposalSettingsSection.jsx @@ -143,29 +143,24 @@ const SnapshotsField = ({ onChange({ active: checked, settings }); }; - const configurableSnapshots = rootVolume.outline.snapshotsConfigurable; + if (!rootVolume.outline.snapshotsConfigurable) return; const explanation = _("Uses Btrfs for the root file system allowing to boot to a previous \ version of the system after configuration changes or software upgrades."); return ( - - -
- {explanation} -
- - } - /> +
+ +
+ {explanation} +
+
); }; diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index 96b22c4404..3077cd7903 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -58,7 +58,7 @@ describe("if snapshots are not configurable", () => { props.settings = { volumes: [{ ...rootVolume, outline: { ...rootVolume.outline, snapshotsConfigurable: false } }] }; }); - it("renders the snapshots switch", () => { + it("does not render the snapshots switch", () => { plainRender(); expect(screen.queryByRole("checkbox", { name: "Use Btrfs Snapshots" })).toBeNull(); From c262df1970b8560b80e79f3bee990174c5898a9a Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Fri, 8 Mar 2024 15:51:00 +0000 Subject: [PATCH 34/38] [Service] Export adjust_by_ram at D-Bus --- service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb | 1 + .../dbus/storage/proposal_settings_conversion/to_dbus_test.rb | 1 + .../test/agama/dbus/storage/volume_conversion/to_dbus_test.rb | 3 +++ 3 files changed, 5 insertions(+) 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 dd03df6dd8..3f586f7d2e 100644 --- a/service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb +++ b/service/lib/agama/dbus/storage/volume_conversion/to_dbus.rb @@ -70,6 +70,7 @@ def outline_conversion(target) "Required" => outline.required?, "FsTypes" => outline.filesystems.map(&:to_human_string), "SupportAutoSize" => outline.adaptive_sizes?, + "AdjustByRam" => outline.adjust_by_ram?, "SnapshotsConfigurable" => outline.snapshots_configurable?, "SnapshotsAffectSizes" => outline.snapshots_affect_sizes?, "SizeRelevantVolumes" => outline.size_relevant_volumes 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 8cc085e5ff..44ef43b0ca 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 @@ -92,6 +92,7 @@ "SupportAutoSize" => false, "SnapshotsConfigurable" => false, "SnapshotsAffectSizes" => false, + "AdjustByRam" => false, "SizeRelevantVolumes" => [] } } 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 5b412ce508..9fffa181a2 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 @@ -38,6 +38,7 @@ outline.snapshots_configurable = true outline.snapshots_size = Y2Storage::DiskSize.new(1000) outline.snapshots_percentage = 10 + outline.adjust_by_ram = true end Agama::Storage::Volume.new("/test").tap do |volume| @@ -70,6 +71,7 @@ "Required" => false, "FsTypes" => [], "SupportAutoSize" => false, + "AdjustByRam" => false, "SnapshotsConfigurable" => false, "SnapshotsAffectSizes" => false, "SizeRelevantVolumes" => [] @@ -90,6 +92,7 @@ "Outline" => { "Required" => true, "FsTypes" => ["Ext3", "Ext4"], + "AdjustByRam" => true, "SupportAutoSize" => true, "SnapshotsConfigurable" => true, "SnapshotsAffectSizes" => true, From 6a42917f167e7e0c423a9c2c55f1ebe50c2cd6df Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Fri, 8 Mar 2024 15:52:35 +0000 Subject: [PATCH 35/38] [Web] Display explanation about adjustByRam --- web/src/client/storage.js | 2 ++ web/src/client/storage.test.js | 6 ++++++ web/src/components/storage/ProposalVolumes.jsx | 8 +++++--- web/src/components/storage/VolumeForm.jsx | 4 ++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 66a92956bf..488862d9a1 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -300,6 +300,7 @@ class ProposalManager { * @property {boolean} required * @property {string[]} fsTypes * @property {boolean} supportAutoSize + * @property {boolean} adjustByRam * @property {boolean} snapshotsConfigurable * @property {boolean} snapshotsAffectSizes * @property {string[]} sizeRelevantVolumes @@ -543,6 +544,7 @@ class ProposalManager { required: dbusOutline.Required.v, fsTypes: dbusOutline.FsTypes.v.map(val => val.v), supportAutoSize: dbusOutline.SupportAutoSize.v, + adjustByRam: dbusOutline.AdjustByRam.v, snapshotsConfigurable: dbusOutline.SnapshotsConfigurable.v, snapshotsAffectSizes: dbusOutline.SnapshotsAffectSizes.v, sizeRelevantVolumes: dbusOutline.SizeRelevantVolumes.v.map(val => val.v) diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 73610f9a1b..6b797556a2 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -329,6 +329,7 @@ const contexts = { SupportAutoSize: { t: "b", v: true }, SnapshotsConfigurable: { t: "b", v: true }, SnapshotsAffectSizes: { t: "b", v: true }, + AdjustByRam: { t: "b", v: false }, SizeRelevantVolumes: { t: "as", v: [{ t: "s", v: "/home" }] } } } @@ -349,6 +350,7 @@ const contexts = { SupportAutoSize: { t: "b", v: false }, SnapshotsConfigurable: { t: "b", v: false }, SnapshotsAffectSizes: { t: "b", v: false }, + AdjustByRam: { t: "b", v: false }, SizeRelevantVolumes: { t: "as", v: [] } } } @@ -1017,6 +1019,7 @@ describe("#proposal", () => { SupportAutoSize: { t: "b", v: false }, SnapshotsConfigurable: { t: "b", v: false }, SnapshotsAffectSizes: { t: "b", v: false }, + AdjustByRam: { t: "b", v: false }, SizeRelevantVolumes: { t: "as", v: [] } } } @@ -1037,6 +1040,7 @@ describe("#proposal", () => { SupportAutoSize: { t: "b", v: false }, SnapshotsConfigurable: { t: "b", v: false }, SnapshotsAffectSizes: { t: "b", v: false }, + AdjustByRam: { t: "b", v: false }, SizeRelevantVolumes: { t: "as", v: [] } } } @@ -1064,6 +1068,7 @@ describe("#proposal", () => { supportAutoSize: false, snapshotsConfigurable: false, snapshotsAffectSizes: false, + adjustByRam: false, sizeRelevantVolumes: [] } }); @@ -1084,6 +1089,7 @@ describe("#proposal", () => { supportAutoSize: false, snapshotsConfigurable: false, snapshotsAffectSizes: false, + adjustByRam: false, sizeRelevantVolumes: [] } }); diff --git a/web/src/components/storage/ProposalVolumes.jsx b/web/src/components/storage/ProposalVolumes.jsx index be080ece93..4015589800 100644 --- a/web/src/components/storage/ProposalVolumes.jsx +++ b/web/src/components/storage/ProposalVolumes.jsx @@ -46,10 +46,10 @@ import { noop } from "~/utils"; * @returns {(ReactComponent|null)} component to display (can be `null`) */ const AutoCalculatedHint = (volume) => { - // no hint, the size is not affected by snapshots or other volumes - const { snapshotsAffectSizes = false, sizeRelevantVolumes = [] } = volume.outline; + const { snapshotsAffectSizes = false, sizeRelevantVolumes = [], adjustByRam } = volume.outline; - if (!snapshotsAffectSizes && sizeRelevantVolumes.length === 0) { + // no hint, the size is not affected by known criteria + if (!snapshotsAffectSizes && !adjustByRam && sizeRelevantVolumes.length === 0) { return null; } @@ -65,6 +65,8 @@ const AutoCalculatedHint = (volume) => { // TRANSLATORS: list item, this affects the computed partition size limits // %s is replaced by a list of the volumes (like "/home, /boot") {sprintf(_("Presence of other volumes (%s)"), sizeRelevantVolumes.join(", "))}} + {adjustByRam && + {_("The amount of RAM in the system")}} ); diff --git a/web/src/components/storage/VolumeForm.jsx b/web/src/components/storage/VolumeForm.jsx index b93f3bae11..b106d9925d 100644 --- a/web/src/components/storage/VolumeForm.jsx +++ b/web/src/components/storage/VolumeForm.jsx @@ -310,6 +310,10 @@ const SizeAuto = ({ volume }) => { // TRANSLATORS: conjunction for merging two list items volume.outline.sizeRelevantVolumes.join(_(", ")))); + if (volume.outline.adjustByRam) + // TRANSLATORS: item which affects the final computed partition size + conditions.push(_("the amount of RAM in the system")); + // TRANSLATORS: the %s is replaced by the items which affect the computed size const conditionsText = sprintf(_("The final size depends on %s."), // TRANSLATORS: conjunction for merging two texts From 9f537c65cfd08cba670fcea809b5f454bdc60228 Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Fri, 8 Mar 2024 15:53:16 +0000 Subject: [PATCH 36/38] Tumbleweed configuration: enable adjust_by_ram for swap --- products.d/tumbleweed.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index 6c5f0426a2..590008e631 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -154,10 +154,12 @@ storage: - mount_path: "swap" filesystem: swap size: - auto: false - min: 1 GiB - max: 2 GiB + auto: true outline: + auto_size: + base_min: 1 GiB + base_max: 2 GiB + adjust_by_ram: true required: false filesystems: - swap From d38a79d0121d3a25a2c05d88579fa8fb6af2a2f2 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 37/38] 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 133076d4c04e4f12f324cb1ae629776c0f95d9ec Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Mon, 11 Mar 2024 08:51:12 +0000 Subject: [PATCH 38/38] [web] Improve comment for translators --- web/src/components/storage/ProposalVolumes.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/components/storage/ProposalVolumes.jsx b/web/src/components/storage/ProposalVolumes.jsx index 4015589800..c7254ed6b5 100644 --- a/web/src/components/storage/ProposalVolumes.jsx +++ b/web/src/components/storage/ProposalVolumes.jsx @@ -55,7 +55,7 @@ const AutoCalculatedHint = (volume) => { return ( <> - {/* TRANSLATORS: header for a list of items */} + {/* TRANSLATORS: header for a list of items referring to size limits for file systems */} {_("These limits are affected by:")} {snapshotsAffectSizes && @@ -66,6 +66,8 @@ const AutoCalculatedHint = (volume) => { // %s is replaced by a list of the volumes (like "/home, /boot") {sprintf(_("Presence of other volumes (%s)"), sizeRelevantVolumes.join(", "))}} {adjustByRam && + // TRANSLATORS: list item, describes a factor that affects the computed size of a + // file system; eg. adjusting the size of the swap {_("The amount of RAM in the system")}}