Skip to content
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/01_bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```


Expand Down
1 change: 0 additions & 1 deletion archinstall/lib/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,6 @@ def _define_arguments(self) -> ArgumentParser:
default=False,
help='Enabled verbose options',
)

return parser

def _parse_args(self) -> Arguments:
Expand Down
50 changes: 50 additions & 0 deletions archinstall/lib/output.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
24 changes: 21 additions & 3 deletions archinstall/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/help/report_bug.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
94 changes: 94 additions & 0 deletions tests/test_share_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# pylint: disable=redefined-outer-name
Comment thread
Softer marked this conversation as resolved.
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