Add share-log subcommand to upload install.log to paste.rs (#4511)

* Add --share-log flag to upload install.log to paste.rs

* Apply ruff-format to share_log.py

* Rework share-log per review: subcommand, TUI confirmation, truncate large logs

* Replace curl/SysCommand with urllib.request, remove defensive try/except

Per review: use stdlib urllib instead of shelling out to curl,
drop unnecessary try/except around TUI confirmation,
remove tempfile (content is passed directly as bytes).

* Fix TUI imports after ui module was pulled one level up (#4515)

* Decouple share_install_log from TUI module

* Add unit tests for share_install_log function

* Update docs to use share-log subcommand syntax
This commit is contained in:
Softer 2026-05-07 03:18:56 +03:00 committed by GitHub
parent 7b5dddf34b
commit ef1cfe7b56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 170 additions and 9 deletions

View File

@ -41,8 +41,8 @@ body:
attributes: attributes:
value: > value: >
**Note**: Assuming you have network connectivity, **Note**: Assuming you have network connectivity,
you can easily post the installation log using the following command: you can easily upload the installation log and get a shareable URL by running:
`curl -F'file=@/var/log/archinstall/install.log' https://0x0.st` `archinstall share-log`
- type: textarea - type: textarea
id: freeform id: freeform

View File

@ -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: When submitting an issue, please:
* Provide the stacktrace of the output if applicable * Provide the stacktrace of the output if applicable
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you! * 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<br> * To upload the log from the ISO image and get a shareable URL, run<br>
```shell ```shell
curl -F'file=@/var/log/archinstall/install.log' https://0x0.st archinstall share-log
``` ```

View File

@ -431,7 +431,6 @@ class ArchConfigHandler:
default=False, default=False,
help='Enabled verbose options', help='Enabled verbose options',
) )
return parser return parser
def _parse_args(self) -> Arguments: def _parse_args(self) -> Arguments:

View File

@ -1,6 +1,8 @@
import logging import logging
import os import os
import sys import sys
import urllib.error
import urllib.request
from collections.abc import Callable from collections.abc import Callable
from dataclasses import asdict, is_dataclass from dataclasses import asdict, is_dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
@ -333,3 +335,51 @@ def log(
if level != logging.DEBUG: if level != logging.DEBUG:
print(text) 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

View File

@ -11,14 +11,16 @@ from pathlib import Path
from archinstall.lib.args import ArchConfigHandler from archinstall.lib.args import ArchConfigHandler
from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.hardware import SysInfo from archinstall.lib.hardware import SysInfo
from archinstall.lib.menu.helpers import Confirmation
from archinstall.lib.network.wifi_handler import WifiHandler from archinstall.lib.network.wifi_handler import WifiHandler
from archinstall.lib.networking import ping 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.packages.util import check_version_upgrade
from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr, translation_handler from archinstall.lib.translationhandler import tr, translation_handler
from archinstall.lib.utils.util import running_from_iso from archinstall.lib.utils.util import running_from_iso
from archinstall.tui.components import tui from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItemGroup
def _log_sys_info() -> None: def _log_sys_info() -> None:
@ -73,12 +75,28 @@ def _list_scripts() -> str:
return '\n'.join(lines) 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: def run() -> int:
""" """
This can either be run as the compiled and installed application: python setup.py install This can either be run as the compiled and installed application: python setup.py install
OR straight as a module: python -m archinstall 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 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() arch_config_handler = ArchConfigHandler()
if '--help' in sys.argv or '-h' in sys.argv: 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 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". https://github.com/archlinux/archinstall and include the log file "/var/log/archinstall/install.log".
Hint: To extract the log from a live ISO Hint: To upload the log and get a shareable URL, run
curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st archinstall share-log
""" """
) )
warn(text) warn(text)

View File

@ -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. It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in.
.. tip:: .. 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! | 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: There are additional log files under ``/var/log/archinstall/`` that can be useful:

94
tests/test_share_log.py Normal file
View File

@ -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