mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-16 15:29:56 +00:00
refactor: move logs rotation to separated trigger which is enabled by default
Previous solution, well, worked kinda fine-ish, though we have much better mechanisms to do so
This commit is contained in:
21
docs/ahriman.core.housekeeping.rst
Normal file
21
docs/ahriman.core.housekeeping.rst
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
ahriman.core.housekeeping package
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
ahriman.core.housekeeping.logs\_rotation\_trigger module
|
||||||
|
--------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.housekeeping.logs_rotation_trigger
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.housekeeping
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
@ -15,6 +15,7 @@ Subpackages
|
|||||||
ahriman.core.distributed
|
ahriman.core.distributed
|
||||||
ahriman.core.formatters
|
ahriman.core.formatters
|
||||||
ahriman.core.gitremote
|
ahriman.core.gitremote
|
||||||
|
ahriman.core.housekeeping
|
||||||
ahriman.core.http
|
ahriman.core.http
|
||||||
ahriman.core.log
|
ahriman.core.log
|
||||||
ahriman.core.report
|
ahriman.core.report
|
||||||
|
@ -40,6 +40,7 @@ This package contains everything required for the most of application actions an
|
|||||||
* ``ahriman.core.distributed`` package with triggers and helpers for distributed build system.
|
* ``ahriman.core.distributed`` package with triggers and helpers for distributed build system.
|
||||||
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
|
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
|
||||||
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
|
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
|
||||||
|
* ``ahriman.core.housekeeping`` package provides few triggers for removing old data.
|
||||||
* ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes.
|
* ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes.
|
||||||
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and some wrappers.
|
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and some wrappers.
|
||||||
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
|
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
|
||||||
|
@ -81,7 +81,6 @@ Base configuration settings.
|
|||||||
* ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually.
|
* ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually.
|
||||||
* ``database`` - path to the application SQLite database, string, required.
|
* ``database`` - path to the application SQLite database, string, required.
|
||||||
* ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order.
|
* ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order.
|
||||||
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
|
|
||||||
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
|
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
|
||||||
|
|
||||||
``alpm:*`` groups
|
``alpm:*`` groups
|
||||||
@ -180,7 +179,7 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
|
|||||||
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
|
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
|
||||||
|
|
||||||
``keyring`` group
|
``keyring`` group
|
||||||
--------------------
|
-----------------
|
||||||
|
|
||||||
Keyring package generator plugin.
|
Keyring package generator plugin.
|
||||||
|
|
||||||
@ -198,6 +197,13 @@ Keyring generator plugin
|
|||||||
* ``revoked`` - list of revoked packagers keys, space separated list of strings, optional.
|
* ``revoked`` - list of revoked packagers keys, space separated list of strings, optional.
|
||||||
* ``trusted`` - list of master keys, space separated list of strings, optional, if not set, the ``key`` option from ``sign`` group will be used.
|
* ``trusted`` - list of master keys, space separated list of strings, optional, if not set, the ``key`` option from ``sign`` group will be used.
|
||||||
|
|
||||||
|
``housekeeping`` group
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
This section describes settings for the ``ahriman.core.housekeeping.LogsRotationTrigger`` plugin.
|
||||||
|
|
||||||
|
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
|
||||||
|
|
||||||
``mirrorlist`` group
|
``mirrorlist`` group
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ package_ahriman-core() {
|
|||||||
'rsync: sync by using rsync')
|
'rsync: sync by using rsync')
|
||||||
install="$pkgbase.install"
|
install="$pkgbase.install"
|
||||||
backup=('etc/ahriman.ini'
|
backup=('etc/ahriman.ini'
|
||||||
|
'etc/ahriman.ini.d/00-housekeeping.ini'
|
||||||
'etc/ahriman.ini.d/logging.ini')
|
'etc/ahriman.ini.d/logging.ini')
|
||||||
|
|
||||||
cd "$pkgbase-$pkgver"
|
cd "$pkgbase-$pkgver"
|
||||||
@ -49,6 +50,7 @@ package_ahriman-core() {
|
|||||||
|
|
||||||
# keep usr/share configs as reference and copy them to /etc
|
# keep usr/share configs as reference and copy them to /etc
|
||||||
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"
|
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"
|
||||||
|
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini.d/00-housekeeping.ini" "$pkgdir/etc/ahriman.ini.d/00-housekeeping.ini"
|
||||||
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini"
|
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini"
|
||||||
|
|
||||||
install -Dm644 "$srcdir/$pkgbase.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgbase.conf"
|
install -Dm644 "$srcdir/$pkgbase.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgbase.conf"
|
||||||
|
@ -7,8 +7,6 @@ logging = ahriman.ini.d/logging.ini
|
|||||||
;apply_migrations = yes
|
;apply_migrations = yes
|
||||||
; Path to the application SQLite database.
|
; Path to the application SQLite database.
|
||||||
database = ${repository:root}/ahriman.db
|
database = ${repository:root}/ahriman.db
|
||||||
; Keep last build logs for each package
|
|
||||||
keep_last_logs = 5
|
|
||||||
|
|
||||||
[alpm]
|
[alpm]
|
||||||
; Path to pacman system database cache.
|
; Path to pacman system database cache.
|
||||||
@ -45,9 +43,11 @@ triggers[] = ahriman.core.gitremote.RemotePullTrigger
|
|||||||
triggers[] = ahriman.core.report.ReportTrigger
|
triggers[] = ahriman.core.report.ReportTrigger
|
||||||
triggers[] = ahriman.core.upload.UploadTrigger
|
triggers[] = ahriman.core.upload.UploadTrigger
|
||||||
triggers[] = ahriman.core.gitremote.RemotePushTrigger
|
triggers[] = ahriman.core.gitremote.RemotePushTrigger
|
||||||
|
triggers[] = ahriman.core.housekeeping.LogsRotationTrigger
|
||||||
; List of well-known triggers. Used only for configuration purposes.
|
; List of well-known triggers. Used only for configuration purposes.
|
||||||
triggers_known[] = ahriman.core.gitremote.RemotePullTrigger
|
triggers_known[] = ahriman.core.gitremote.RemotePullTrigger
|
||||||
triggers_known[] = ahriman.core.gitremote.RemotePushTrigger
|
triggers_known[] = ahriman.core.gitremote.RemotePushTrigger
|
||||||
|
triggers_known[] = ahriman.core.housekeeping.LogsRotationTrigger
|
||||||
triggers_known[] = ahriman.core.report.ReportTrigger
|
triggers_known[] = ahriman.core.report.ReportTrigger
|
||||||
triggers_known[] = ahriman.core.upload.UploadTrigger
|
triggers_known[] = ahriman.core.upload.UploadTrigger
|
||||||
; Maximal age in seconds of the VCS packages before their version will be updated with its remote source.
|
; Maximal age in seconds of the VCS packages before their version will be updated with its remote source.
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
[logs-rotation]
|
||||||
|
; Keep last build logs for each package
|
||||||
|
keep_last_logs = 5
|
@ -45,11 +45,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"path_exists": True,
|
"path_exists": True,
|
||||||
"path_type": "dir",
|
"path_type": "dir",
|
||||||
},
|
},
|
||||||
"keep_last_logs": {
|
|
||||||
"type": "integer",
|
|
||||||
"coerce": "integer",
|
|
||||||
"min": 0,
|
|
||||||
},
|
|
||||||
"logging": {
|
"logging": {
|
||||||
"type": "path",
|
"type": "path",
|
||||||
"coerce": "absolute_path",
|
"coerce": "absolute_path",
|
||||||
|
20
src/ahriman/core/housekeeping/__init__.py
Normal file
20
src/ahriman/core/housekeeping/__init__.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2025 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.core.housekeeping.logs_rotation_trigger import LogsRotationTrigger
|
87
src/ahriman/core/housekeeping/logs_rotation_trigger.py
Normal file
87
src/ahriman/core/housekeeping/logs_rotation_trigger.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2025 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.core import context
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.status import Client
|
||||||
|
from ahriman.core.triggers import Trigger
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
|
class LogsRotationTrigger(Trigger):
|
||||||
|
"""
|
||||||
|
rotate logs after build processes
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
keep_last_records(int): number of last records to keep
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONFIGURATION_SCHEMA = {
|
||||||
|
"logs-rotation": {
|
||||||
|
"type": "dict",
|
||||||
|
"schema": {
|
||||||
|
"keep_last_logs": {
|
||||||
|
"type": "integer",
|
||||||
|
"required": True,
|
||||||
|
"coerce": "integer",
|
||||||
|
"min": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
"""
|
||||||
|
Trigger.__init__(self, repository_id, configuration)
|
||||||
|
|
||||||
|
section = next(iter(self.configuration_sections(configuration)))
|
||||||
|
self.keep_last_records = configuration.getint( # read old-style first and then fallback to new style
|
||||||
|
"settings", "keep_last_logs",
|
||||||
|
fallback=configuration.getint(section, "keep_last_logs"))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def configuration_sections(cls, configuration: Configuration) -> list[str]:
|
||||||
|
"""
|
||||||
|
extract configuration sections from configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: read configuration sections belong to this trigger
|
||||||
|
"""
|
||||||
|
return list(cls.CONFIGURATION_SCHEMA.keys())
|
||||||
|
|
||||||
|
def on_result(self, result: Result, packages: list[Package]) -> None:
|
||||||
|
"""
|
||||||
|
run trigger
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result(Result): build result
|
||||||
|
packages(list[Package]): list of all available packages
|
||||||
|
"""
|
||||||
|
ctx = context.get()
|
||||||
|
reporter = ctx.get(Client)
|
||||||
|
reporter.logs_rotate(self.keep_last_records)
|
@ -17,7 +17,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import atexit
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@ -37,7 +36,6 @@ class HttpLogHandler(logging.Handler):
|
|||||||
method
|
method
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
keep_last_records(int): number of last records to keep
|
|
||||||
reporter(Client): build status reporter instance
|
reporter(Client): build status reporter instance
|
||||||
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
|
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
|
||||||
"""
|
"""
|
||||||
@ -56,7 +54,6 @@ class HttpLogHandler(logging.Handler):
|
|||||||
|
|
||||||
self.reporter = Client.load(repository_id, configuration, report=report)
|
self.reporter = Client.load(repository_id, configuration, report=report)
|
||||||
self.suppress_errors = suppress_errors
|
self.suppress_errors = suppress_errors
|
||||||
self.keep_last_records = configuration.getint("settings", "keep_last_logs", fallback=0)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
|
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
|
||||||
@ -83,7 +80,6 @@ class HttpLogHandler(logging.Handler):
|
|||||||
root.addHandler(handler)
|
root.addHandler(handler)
|
||||||
|
|
||||||
LogRecordId.DEFAULT_PROCESS_ID = str(uuid.uuid4()) # assign default process identifier for log records
|
LogRecordId.DEFAULT_PROCESS_ID = str(uuid.uuid4()) # assign default process identifier for log records
|
||||||
atexit.register(handler.rotate)
|
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
@ -104,9 +100,3 @@ class HttpLogHandler(logging.Handler):
|
|||||||
if self.suppress_errors:
|
if self.suppress_errors:
|
||||||
return
|
return
|
||||||
self.handleError(record)
|
self.handleError(record)
|
||||||
|
|
||||||
def rotate(self) -> None:
|
|
||||||
"""
|
|
||||||
rotate log records, removing older ones
|
|
||||||
"""
|
|
||||||
self.reporter.logs_rotate(self.keep_last_records)
|
|
||||||
|
19
tests/ahriman/core/housekeeping/conftest.py
Normal file
19
tests/ahriman/core/housekeeping/conftest.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.housekeeping import LogsRotationTrigger
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def logs_rotation_trigger(configuration: Configuration) -> LogsRotationTrigger:
|
||||||
|
"""
|
||||||
|
logs roration trigger fixture
|
||||||
|
|
||||||
|
Args:
|
||||||
|
configuration(Configuration): configuration fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LogsRotationTrigger: logs rotation trigger test instance
|
||||||
|
"""
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
return LogsRotationTrigger(repository_id, configuration)
|
@ -0,0 +1,26 @@
|
|||||||
|
from pytest_mock import MockerFixture
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.housekeeping import LogsRotationTrigger
|
||||||
|
from ahriman.core.status import Client
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration_sections(configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
must correctly parse target list
|
||||||
|
"""
|
||||||
|
assert LogsRotationTrigger.configuration_sections(configuration) == ["logs-rotation"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_rotate(logs_rotation_trigger: LogsRotationTrigger, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must rotate logs
|
||||||
|
"""
|
||||||
|
client_mock = MagicMock()
|
||||||
|
context_mock = mocker.patch("ahriman.core._Context.get", return_value=client_mock)
|
||||||
|
|
||||||
|
logs_rotation_trigger.on_result(Result(), [])
|
||||||
|
context_mock.assert_called_once_with(Client)
|
||||||
|
client_mock.logs_rotate.assert_called_once_with(logs_rotation_trigger.keep_last_records)
|
@ -20,14 +20,12 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
|
|||||||
|
|
||||||
add_mock = mocker.patch("logging.Logger.addHandler")
|
add_mock = mocker.patch("logging.Logger.addHandler")
|
||||||
load_mock = mocker.patch("ahriman.core.status.Client.load")
|
load_mock = mocker.patch("ahriman.core.status.Client.load")
|
||||||
atexit_mock = mocker.patch("atexit.register")
|
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
handler = HttpLogHandler.load(repository_id, configuration, report=False)
|
handler = HttpLogHandler.load(repository_id, configuration, report=False)
|
||||||
assert handler
|
assert handler
|
||||||
add_mock.assert_called_once_with(handler)
|
add_mock.assert_called_once_with(handler)
|
||||||
load_mock.assert_called_once_with(repository_id, configuration, report=False)
|
load_mock.assert_called_once_with(repository_id, configuration, report=False)
|
||||||
atexit_mock.assert_called_once_with(handler.rotate)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_exist(configuration: Configuration) -> None:
|
def test_load_exist(configuration: Configuration) -> None:
|
||||||
@ -96,16 +94,3 @@ def test_emit_skip(configuration: Configuration, log_record: logging.LogRecord,
|
|||||||
|
|
||||||
handler.emit(log_record)
|
handler.emit(log_record)
|
||||||
log_mock.assert_not_called()
|
log_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_rotate(configuration: Configuration, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must rotate logs
|
|
||||||
"""
|
|
||||||
rotate_mock = mocker.patch("ahriman.core.status.Client.logs_rotate")
|
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
handler = HttpLogHandler(repository_id, configuration, report=False, suppress_errors=False)
|
|
||||||
|
|
||||||
handler.rotate()
|
|
||||||
rotate_mock.assert_called_once_with(handler.keep_last_records)
|
|
||||||
|
@ -37,6 +37,9 @@ target =
|
|||||||
[keyring]
|
[keyring]
|
||||||
target = keyring
|
target = keyring
|
||||||
|
|
||||||
|
[logs-rotation]
|
||||||
|
keep_last_logs = 5
|
||||||
|
|
||||||
[mirrorlist]
|
[mirrorlist]
|
||||||
target = mirrorlist
|
target = mirrorlist
|
||||||
servers = http://localhost
|
servers = http://localhost
|
||||||
|
Reference in New Issue
Block a user