Skip to content

Commit

Permalink
Added manager route for downloading logs (#1216)
Browse files Browse the repository at this point in the history
## Problem

In the Cockpit times, Agama allowed to download the logs using the web
UI. However, after the switch to the HTTP/JSON API, that’s not the case
anymore. Agama relied on Cockpit’s file API which, for obvious reasons,
is not available anymore.

Therefore the option to download the logs using our agama-web-server
should be implemented bringing the functionality back.

- Trello card: -
https://trello.com/c/xExmF6a2/3660-3-allow-downloading-the-logs-using-the-web-ui

## Solution

The agama-web-server has been adapted adding the /logs route in the
manager HTTP/JSON API. The Web UI has been also adapted bringing back
the fetchLogs() method.

Last but not least the options for showing the logs and the terminal
have been removed as them were also relying on Cockpit and therefore are
currently broken.
  • Loading branch information
teclator authored May 16, 2024
2 parents 791857a + 0c493ef commit 0a10e9a
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 33 deletions.
2 changes: 2 additions & 0 deletions rust/agama-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use zbus::{self, zvariant};

#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Cannot generate Agama logs: {0}")]
CannotGenerateLogs(String),
#[error("D-Bus service error: {0}")]
DBus(#[from] zbus::Error),
#[error("Could not connect to Agama bus at '{0}': {1}")]
Expand Down
43 changes: 40 additions & 3 deletions rust/agama-server/src/manager/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@
//! * `manager_service` which returns the Axum service.
//! * `manager_stream` which offers an stream that emits the manager events coming from D-Bus.

use std::pin::Pin;

use agama_lib::{
error::ServiceError,
manager::{InstallationPhase, ManagerClient},
proxies::Manager1Proxy,
};
use axum::{
extract::State,
extract::{Request, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use rand::distributions::{Alphanumeric, DistString};
use serde::Serialize;
use std::{pin::Pin, process::Command};
use tokio_stream::{Stream, StreamExt};
use tower_http::services::ServeFile;

use crate::{
error::Error,
Expand Down Expand Up @@ -90,6 +93,7 @@ pub async fn manager_service(dbus: zbus::Connection) -> Result<Router, ServiceEr
.route("/install", post(install_action))
.route("/finish", post(finish_action))
.route("/installer", get(installer_status))
.route("/logs", get(download_logs))
.merge(status_router)
.merge(progress_router)
.with_state(state))
Expand Down Expand Up @@ -163,3 +167,36 @@ async fn installer_status(
};
Ok(Json(status))
}

/// Returns agama logs
#[utoipa::path(get, path = "/api/manager/logs", responses(
(status = 200, description = "Download logs blob.")
))]

pub async fn download_logs() -> impl IntoResponse {
let path = generate_logs().await;
let Ok(path) = path else {
return (StatusCode::INTERNAL_SERVER_ERROR).into_response();
};

match ServeFile::new(path)
.try_call(Request::new(axum::body::Body::empty()))
.await
{
Ok(res) => res.into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR).into_response(),
}
}

async fn generate_logs() -> Result<String, Error> {
let random_name: String = Alphanumeric.sample_string(&mut rand::thread_rng(), 8);
let path = format!("/run/agama/logs_{random_name}");

Command::new("agama")
.args(["logs", "store", "-d", path.as_str()])
.status()
.map_err(|e| ServiceError::CannotGenerateLogs(e.to_string()))?;

let full_path = format!("{path}.tar.bz2");
Ok(full_path)
}
36 changes: 36 additions & 0 deletions rust/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"network": {
"connections": [
{
"id": "eth0",
"method4": "auto",
"method6": "auto",
"addresses": [
"192.168.0.101/24"
],
"interface": "eth0",
"status": "up"
},
{
"id": "Wired connection 1",
"method4": "auto",
"method6": "auto",
"interface": "enp5s0f4u2c2",
"status": "up"
},
{
"id": "Sarambeque",
"method4": "auto",
"method6": "auto",
"wireless": {
"password": "vikingo.pass",
"security": "wpa-psk",
"ssid": "Sarambeque",
"mode": "infrastructure"
},
"status": "up"
}
]
}
}

6 changes: 6 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Tue May 16 12:48:42 UTC 2024 - Knut Anderssen <kanderssen@suse.com>

- Allow to download Agama los throgh the manager HTTP API
(gh#openSUSE/1216).

-------------------------------------------------------------------
Thu May 16 12:34:43 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
6 changes: 6 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Thu May 16 12:48:27 UTC 2024 - Knut Anderssen <kanderssen@suse.com>

- Fix the download logs action in the web UI and drop the broken
actions show logs and show terminal (gh#openSUSE/agama#1216).

-------------------------------------------------------------------
Tue May 14 12:24:23 UTC 2024 - José Iván López González <jlopez@suse.com>

Expand Down
7 changes: 4 additions & 3 deletions web/src/client/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ class ManagerBaseClient {
* Returns the binary content of the YaST logs file
*
* @todo Implement a mechanism to get the logs.
* @return {Promise<void>}
* @return {Promise<Response>}
*/
async fetchLogs() {
// TODO
const response = await fetch(`${this.client.baseUrl}/manager/logs`);
return response;
}

/**
Expand Down Expand Up @@ -147,6 +148,6 @@ class ManagerClient extends WithProgress(
WithStatus(ManagerBaseClient, "/manager/status", MANAGER_SERVICE),
"/manager/progress",
MANAGER_SERVICE,
) {}
) { }

export { ManagerClient };
21 changes: 9 additions & 12 deletions web/src/components/core/LogsButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { Icon } from "~/components/layout";
import { _ } from "~/i18n";

const FILENAME = "agama-installation-logs.tar.bzip2";
const FILETYPE = "application/x-xz";

/**
* Button for collecting and downloading YaST logs
Expand All @@ -43,16 +42,14 @@ const LogsButton = ({ ...props }) => {
const [error, setError] = useState(null);

/**
* Helper function for creating the blob and triggering the download automatically
* Helper function for triggering the download automatically
*
* @note Based on the article "Programmatic file downloads in the browser" found at
* https://blog.logrocket.com/programmatic-file-downloads-in-the-browser-9a5186298d5c
*
* @param {Uint8Array} data - binary data for creating a {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob Blob}
* @param {string} url - the file location to download from
*/
const download = (data) => {
const blob = new Blob([data], { type: FILETYPE });
const url = URL.createObjectURL(blob);
const autoDownload = (url) => {
const a = document.createElement('a');
a.href = url;
a.download = FILENAME;
Expand All @@ -79,8 +76,8 @@ const LogsButton = ({ ...props }) => {
const collectAndDownload = () => {
setError(null);
setIsCollecting(true);
cancellablePromise(client.manager.fetchLogs())
.then(download)
cancellablePromise(client.manager.fetchLogs().then(response => URL.createObjectURL(response.blob())))
.then(autoDownload)
.catch(setError)
.finally(() => setIsCollecting(false));
};
Expand All @@ -98,21 +95,21 @@ const LogsButton = ({ ...props }) => {
{isCollecting ? _("Collecting logs...") : _("Download logs")}
</Button>

{ isCollecting &&
{isCollecting &&
<Alert
isInline
isPlain
variant="info"
title={_("The browser will run the logs download as soon as they are ready. Please, be patient.")}
/> }
/>}

{ error &&
{error &&
<Alert
isInline
isPlain
variant="warning"
title={_("Something went wrong while collecting logs. Please, try again.")}
/> }
/>}
</>
);
};
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/core/LogsButton.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ describe("LogsButton", () => {

describe("and logs are collected successfully", () => {
beforeEach(() => {
// new TextEncoder().encode("Hello logs!")
const data = new Uint8Array([72, 101, 108, 108, 111, 32, 108, 111, 103, 115, 33]);
fetchLogsFn.mockResolvedValue(data);
fetchLogsFn.mockResolvedValue({
blob: jest.fn().mockResolvedValue(new Blob(["testing"]))
});
});

it("triggers the download", async () => {
Expand Down
12 changes: 2 additions & 10 deletions web/src/components/core/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@ import { Icon } from "~/components/layout";
import { InstallerKeymapSwitcher, InstallerLocaleSwitcher } from "~/components/l10n";
import {
About,
Disclosure,
// FIXME: unify names here by renaming LogsButton -> LogButton or ShowLogButton -> ShowLogsButton
LogsButton,
ShowLogButton,
ShowTerminalButton,
} from "~/components/core";
import { noop } from "~/utils";
import { _ } from "~/i18n";
Expand All @@ -47,7 +43,7 @@ import useNodeSiblings from "~/hooks/useNodeSiblings";
*
* @param {SidebarProps}
*/
export default function Sidebar ({ isOpen, onClose = noop, children }) {
export default function Sidebar({ isOpen, onClose = noop, children }) {
const asideRef = useRef(null);
const closeButtonRef = useRef(null);
const [addAttribute, removeAttribute] = useNodeSiblings(asideRef.current);
Expand Down Expand Up @@ -136,11 +132,7 @@ export default function Sidebar ({ isOpen, onClose = noop, children }) {

<div className="flex-stack justify-between" onClick={onClick}>
<div className="flex-stack">
<Disclosure label={_("Diagnostic tools")} data-keep-sidebar-open>
<ShowLogButton />
<LogsButton data-keep-sidebar-open="true" />
<ShowTerminalButton />
</Disclosure>
<LogsButton data-keep-sidebar-open="true" />
{children}
<About />
</div>
Expand Down
2 changes: 0 additions & 2 deletions web/src/components/core/Sidebar.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ it("renders expected options", () => {
screen.getByText("Installer keymap switcher mock");
screen.getByText("Installer locale switcher mock");
screen.getByText("LogsButton mock");
screen.getByText("ShowLogButton mock");
screen.getByText("ShowTerminalButton mock");
screen.getByText("About link mock");
});

Expand Down

0 comments on commit 0a10e9a

Please sign in to comment.