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:
parent
7b5dddf34b
commit
ef1cfe7b56
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<br>
|
||||
* To upload the log from the ISO image and get a shareable URL, run<br>
|
||||
```shell
|
||||
curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
|
||||
archinstall share-log
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -431,7 +431,6 @@ class ArchConfigHandler:
|
|||
default=False,
|
||||
help='Enabled verbose options',
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def _parse_args(self) -> Arguments:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ from pathlib import Path
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue