diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml index ab40754fc9..92061f6418 100644 --- a/.github/ISSUE_TEMPLATE/01_bug.yml +++ b/.github/ISSUE_TEMPLATE/01_bug.yml @@ -41,8 +41,8 @@ body: attributes: value: > **Note**: Assuming you have network connectivity, - you can easily post the installation log using the following command: - `curl -F'file=@/var/log/archinstall/install.log' https://0x0.st` + you can easily upload the installation log and get a shareable URL by running: + `archinstall share-log` - type: textarea id: freeform diff --git a/README.md b/README.md index 95d1570252..4e6847f406 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,9 @@ If you come across any issues, kindly submit your issue here on GitHub or post y When submitting an issue, please: * Provide the stacktrace of the output if applicable * Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you! - * To extract the log from the ISO image, one way is to use
+ * To upload the log from the ISO image and get a shareable URL, run
```shell - curl -F'file=@/var/log/archinstall/install.log' https://0x0.st + archinstall share-log ``` diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index 8d78f7e6d8..d8aaaa83fe 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -431,7 +431,6 @@ def _define_arguments(self) -> ArgumentParser: default=False, help='Enabled verbose options', ) - return parser def _parse_args(self) -> Arguments: diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index bc4bb11399..3a972dfb08 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -1,6 +1,8 @@ import logging import os import sys +import urllib.error +import urllib.request from collections.abc import Callable from dataclasses import asdict, is_dataclass from datetime import UTC, datetime @@ -333,3 +335,51 @@ def log( if level != logging.DEBUG: print(text) + + +def share_install_log( + paste_url: str = 'https://paste.rs', + max_size: int = 10 * 1024 * 1024, + confirm: Callable[[str], bool] = lambda _: True, +) -> int: + log_path = logger.path + + if not log_path.exists(): + info(f'Log file not found: {log_path}') + return 1 + + size = log_path.stat().st_size + if size == 0: + info(f'Log file is empty: {log_path}') + return 1 + + if size > max_size: + info(f'Log file exceeds {max_size} bytes, uploading last {max_size} bytes') + content = log_path.read_bytes()[-max_size:] + else: + content = log_path.read_bytes() + + header = f'About to upload {log_path} ({len(content)} bytes) to {paste_url}\n\n' + header += 'The log may contain hostname, mirror URLs, package list and partition layout.\n' + header += 'The uploaded paste is public.\n\n' + header += 'Continue?' + + if not confirm(header): + info('Cancelled.') + return 1 + + try: + req = urllib.request.Request(paste_url, data=content) + with urllib.request.urlopen(req) as response: + url = response.read().decode().strip() + except urllib.error.URLError as e: + info(f'Upload failed: {e}') + return 1 + + if not url.startswith('http'): + info(f'Unexpected response from {paste_url}: {url[:200]!r}') + return 1 + + # raw print so the URL is pipe-friendly (no ANSI colors, no log prefix) + print(url) + return 0 diff --git a/archinstall/main.py b/archinstall/main.py index 505652a4f7..9b91681b00 100644 --- a/archinstall/main.py +++ b/archinstall/main.py @@ -11,14 +11,16 @@ from archinstall.lib.args import ArchConfigHandler from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.hardware import SysInfo +from archinstall.lib.menu.helpers import Confirmation from archinstall.lib.network.wifi_handler import WifiHandler from archinstall.lib.networking import ping -from archinstall.lib.output import debug, error, info, warn +from archinstall.lib.output import debug, error, info, share_install_log, warn from archinstall.lib.packages.util import check_version_upgrade from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.translationhandler import tr, translation_handler from archinstall.lib.utils.util import running_from_iso from archinstall.tui.components import tui +from archinstall.tui.menu_item import MenuItemGroup def _log_sys_info() -> None: @@ -73,12 +75,28 @@ def _list_scripts() -> str: return '\n'.join(lines) +def _tui_confirm(header: str) -> bool: + async def _ask() -> bool: + result = await Confirmation( + group=MenuItemGroup.yes_no(), + header=header, + allow_skip=False, + preset=False, + ).show() + return result.get_value() + + return tui.run(_ask) + + def run() -> int: """ This can either be run as the compiled and installed application: python setup.py install OR straight as a module: python -m archinstall In any case we will be attempting to load the provided script to be run from the scripts/ folder """ + if 'share-log' in sys.argv: + return share_install_log(confirm=_tui_confirm) + arch_config_handler = ArchConfigHandler() if '--help' in sys.argv or '-h' in sys.argv: @@ -141,8 +159,8 @@ def _error_message(exc: Exception) -> None: Archinstall experienced the above error. If you think this is a bug, please report it to https://github.com/archlinux/archinstall and include the log file "/var/log/archinstall/install.log". - Hint: To extract the log from a live ISO - curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st + Hint: To upload the log and get a shareable URL, run + archinstall share-log """ ) warn(text) diff --git a/docs/help/report_bug.rst b/docs/help/report_bug.rst index bacaeb4cc2..b11027ab3e 100644 --- a/docs/help/report_bug.rst +++ b/docs/help/report_bug.rst @@ -15,7 +15,7 @@ When submitting a help ticket, please include the :code:`/var/log/archinstall/in It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in. .. tip:: - | An easy way to submit logs is ``curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st``. + | An easy way to submit logs is ``archinstall share-log``, which uploads ``install.log`` to paste.rs and prints a shareable URL. | Use caution when submitting other log files, but ``archinstall`` pledges to keep ``install.log`` safe for posting publicly! There are additional log files under ``/var/log/archinstall/`` that can be useful: diff --git a/tests/test_share_log.py b/tests/test_share_log.py new file mode 100644 index 0000000000..c1c3ca84d7 --- /dev/null +++ b/tests/test_share_log.py @@ -0,0 +1,94 @@ +# pylint: disable=redefined-outer-name +import urllib.error +from io import BytesIO +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from archinstall.lib.output import share_install_log + + +@pytest.fixture() +def log_file(tmp_path: Path) -> Path: + log_dir = tmp_path / 'archinstall' + log_dir.mkdir() + return log_dir / 'install.log' + + +def _fake_logger(log_file: Path) -> MagicMock: + mock = MagicMock() + mock.path = log_file + return mock + + +def test_file_not_found(tmp_path: Path) -> None: + missing = tmp_path / 'no-such' / 'install.log' + with patch('archinstall.lib.output.logger', _fake_logger(missing)): + assert share_install_log() == 1 + + +def test_empty_file(log_file: Path) -> None: + log_file.write_bytes(b'') + with patch('archinstall.lib.output.logger', _fake_logger(log_file)): + assert share_install_log() == 1 + + +def test_user_cancels(log_file: Path) -> None: + log_file.write_text('some log content') + with patch('archinstall.lib.output.logger', _fake_logger(log_file)): + assert share_install_log(confirm=lambda _: False) == 1 + + +def test_successful_upload(log_file: Path) -> None: + log_file.write_text('some log content') + fake_response = BytesIO(b'https://paste.rs/abc.def') + + with ( + patch('archinstall.lib.output.logger', _fake_logger(log_file)), + patch('urllib.request.urlopen', return_value=fake_response) as mock_open, + ): + result = share_install_log() + + assert result == 0 + req = mock_open.call_args[0][0] + assert req.data == b'some log content' + + +def test_truncation(log_file: Path) -> None: + max_size = 100 + content = b'A' * 50 + b'B' * 80 + log_file.write_bytes(content) + fake_response = BytesIO(b'https://paste.rs/abc.def') + + with ( + patch('archinstall.lib.output.logger', _fake_logger(log_file)), + patch('urllib.request.urlopen', return_value=fake_response) as mock_open, + ): + result = share_install_log(max_size=max_size) + + assert result == 0 + req = mock_open.call_args[0][0] + assert len(req.data) == max_size + assert req.data == content[-max_size:] + + +def test_network_error(log_file: Path) -> None: + log_file.write_text('some log content') + + with ( + patch('archinstall.lib.output.logger', _fake_logger(log_file)), + patch('urllib.request.urlopen', side_effect=urllib.error.URLError('no network')), + ): + assert share_install_log() == 1 + + +def test_unexpected_response(log_file: Path) -> None: + log_file.write_text('some log content') + fake_response = BytesIO(b'ERROR: something went wrong') + + with ( + patch('archinstall.lib.output.logger', _fake_logger(log_file)), + patch('urllib.request.urlopen', return_value=fake_response), + ): + assert share_install_log() == 1