Skip to content

Commit

Permalink
add the posibility to upgrade with a local repository
Browse files Browse the repository at this point in the history
Upgrade with a local repository required to host the repository locally
for it to be visible from target user-space container during the
upgrade. The added actor ensures that the local repository
will be visible from the container by adjusting the path to it simply by
prefixing a host root mount bind '/installroot' to it. The
local_repos_inhibit actor is no longer needed, thus was removed.
  • Loading branch information
PeterMocary authored and pirat89 committed Nov 14, 2023
1 parent 60190ff commit e9f899c
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 172 deletions.
48 changes: 48 additions & 0 deletions repos/system_upgrade/common/actors/adjustlocalrepos/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from leapp.actors import Actor
from leapp.libraries.actor import adjustlocalrepos
from leapp.libraries.common import mounting
from leapp.libraries.stdlib import api
from leapp.models import (
TargetOSInstallationImage,
TargetUserSpaceInfo,
TMPTargetRepositoriesFacts,
UsedTargetRepositories
)
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag


class AdjustLocalRepos(Actor):
"""
Adjust local repositories to the target user-space container.
Changes the path of local file urls (starting with 'file://') for 'baseurl' and
'mirrorlist' fields to the container space for the used repositories. This is
done by prefixing host root mount bind ('/installroot') to the path. It ensures
that the files will be accessible from the container and thus proper functionality
of the local repository.
"""

name = 'adjust_local_repos'
consumes = (TargetOSInstallationImage,
TargetUserSpaceInfo,
TMPTargetRepositoriesFacts,
UsedTargetRepositories)
produces = ()
tags = (IPUWorkflowTag, TargetTransactionChecksPhaseTag)

def process(self):
target_userspace_info = next(self.consume(TargetUserSpaceInfo), None)
used_target_repos = next(self.consume(UsedTargetRepositories), None)
target_repos_facts = next(self.consume(TMPTargetRepositoriesFacts), None)
target_iso = next(self.consume(TargetOSInstallationImage), None)

if not all([target_userspace_info, used_target_repos, target_repos_facts]):
api.current_logger().error("Missing required information to proceed!")
return

target_repos_facts = target_repos_facts.repositories
iso_repoids = set(repo.repoid for repo in target_iso.repositories) if target_iso else set()
used_target_repoids = set(repo.repoid for repo in used_target_repos.repos)

with mounting.NspawnActions(base_dir=target_userspace_info.path) as context:
adjustlocalrepos.process(context, target_repos_facts, iso_repoids, used_target_repoids)
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import os

from leapp.libraries.stdlib import api

HOST_ROOT_MOUNT_BIND_PATH = '/installroot'
LOCAL_FILE_URL_PREFIX = 'file://'


def _adjust_local_file_url(repo_file_line):
"""
Adjusts a local file url to the target user-space container in a provided
repo file line by prefixing host root mount bind '/installroot' to it
when needed.
:param str repo_file_line: a line from a repo file
:returns str: adjusted line or the provided line if no changes are needed
"""
adjust_fields = ['baseurl', 'mirrorlist']

if LOCAL_FILE_URL_PREFIX in repo_file_line and not repo_file_line.startswith('#'):
entry_field, entry_value = repo_file_line.strip().split('=', 1)
if not any(entry_field.startswith(field) for field in adjust_fields):
return repo_file_line

entry_value = entry_value.strip('\'\"')
path = entry_value[len(LOCAL_FILE_URL_PREFIX):]
new_entry_value = LOCAL_FILE_URL_PREFIX + os.path.join(HOST_ROOT_MOUNT_BIND_PATH, path.lstrip('/'))
new_repo_file_line = entry_field + '=' + new_entry_value
return new_repo_file_line
return repo_file_line


def _extract_repos_from_repofile(context, repo_file):
"""
Generator function that extracts repositories from a repo file in the given context
and yields them as list of lines that belong to the repository.
:param context: target user-space context
:param str repo_file: path to repository file (inside the provided context)
"""
with context.open(repo_file, 'r') as rf:
repo_file_lines = rf.readlines()

# Detect repo and remove lines before first repoid
repo_found = False
for idx, line in enumerate(repo_file_lines):
if line.startswith('['):
repo_file_lines = repo_file_lines[idx:]
repo_found = True
break

if not repo_found:
return

current_repo = []
for line in repo_file_lines:
line = line.strip()

if line.startswith('[') and current_repo:
yield current_repo
current_repo = []

current_repo.append(line)
yield current_repo


def _adjust_local_repos_to_container(context, repo_file, local_repoids):
new_repo_file = []
for repo in _extract_repos_from_repofile(context, repo_file):
repoid = repo[0].strip('[]')
adjusted_repo = repo
if repoid in local_repoids:
adjusted_repo = [_adjust_local_file_url(line) for line in repo]
new_repo_file.append(adjusted_repo)

# Combine the repo file contents into a string and write it back to the file
new_repo_file = ['\n'.join(repo) for repo in new_repo_file]
new_repo_file = '\n'.join(new_repo_file)
with context.open(repo_file, 'w') as rf:
rf.write(new_repo_file)


def process(context, target_repos_facts, iso_repoids, used_target_repoids):
for repo_file_facts in target_repos_facts:
repo_file_path = repo_file_facts.file
local_repoids = set()
for repo in repo_file_facts.data:
# Skip repositories that aren't used or are provided by ISO
if repo.repoid not in used_target_repoids or repo.repoid in iso_repoids:
continue
# Note repositories that contain local file url
if repo.baseurl and LOCAL_FILE_URL_PREFIX in repo.baseurl or \
repo.mirrorlist and LOCAL_FILE_URL_PREFIX in repo.mirrorlist:
local_repoids.add(repo.repoid)

if local_repoids:
api.current_logger().debug(
'Adjusting following repos in the repo file - {}: {}'.format(repo_file_path,
', '.join(local_repoids)))
_adjust_local_repos_to_container(context, repo_file_path, local_repoids)
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import pytest

from leapp.libraries.actor import adjustlocalrepos

REPO_FILE_1_LOCAL_REPOIDS = ['myrepo1']
REPO_FILE_1 = [['[myrepo1]',
'name=mylocalrepo',
'baseurl=file:///home/user/.local/myrepos/repo1'
]]
REPO_FILE_1_ADJUSTED = [['[myrepo1]',
'name=mylocalrepo',
'baseurl=file:///installroot/home/user/.local/myrepos/repo1'
]]

REPO_FILE_2_LOCAL_REPOIDS = ['myrepo3']
REPO_FILE_2 = [['[myrepo2]',
'name=mynotlocalrepo',
'baseurl=https://www.notlocal.com/packages'
],
['[myrepo3]',
'name=mylocalrepo',
'baseurl=file:///home/user/.local/myrepos/repo3',
'mirrorlist=file:///home/user/.local/mymirrors/repo3.txt'
]]
REPO_FILE_2_ADJUSTED = [['[myrepo2]',
'name=mynotlocalrepo',
'baseurl=https://www.notlocal.com/packages'
],
['[myrepo3]',
'name=mylocalrepo',
'baseurl=file:///installroot/home/user/.local/myrepos/repo3',
'mirrorlist=file:///installroot/home/user/.local/mymirrors/repo3.txt'
]]

REPO_FILE_3_LOCAL_REPOIDS = ['myrepo4', 'myrepo5']
REPO_FILE_3 = [['[myrepo4]',
'name=myrepowithlocalgpgkey',
'baseurl="file:///home/user/.local/myrepos/repo4"',
'gpgkey=file:///home/user/.local/pki/gpgkey',
'gpgcheck=1'
],
['[myrepo5]',
'name=myrepowithcomment',
'baseurl=file:///home/user/.local/myrepos/repo5',
'#baseurl=file:///home/user/.local/myotherrepos/repo5',
'enabled=1',
'exclude=sed']]
REPO_FILE_3_ADJUSTED = [['[myrepo4]',
'name=myrepowithlocalgpgkey',
'baseurl=file:///installroot/home/user/.local/myrepos/repo4',
'gpgkey=file:///home/user/.local/pki/gpgkey',
'gpgcheck=1'
],
['[myrepo5]',
'name=myrepowithcomment',
'baseurl=file:///installroot/home/user/.local/myrepos/repo5',
'#baseurl=file:///home/user/.local/myotherrepos/repo5',
'enabled=1',
'exclude=sed']]
REPO_FILE_EMPTY = []


@pytest.mark.parametrize('repo_file_line, expected_adjusted_repo_file_line',
[('baseurl=file:///home/user/.local/repositories/repository',
'baseurl=file:///installroot/home/user/.local/repositories/repository'),
('baseurl="file:///home/user/my-repo"',
'baseurl=file:///installroot/home/user/my-repo'),
('baseurl=https://notlocal.com/packages',
'baseurl=https://notlocal.com/packages'),
('mirrorlist=file:///some_mirror_list.txt',
'mirrorlist=file:///installroot/some_mirror_list.txt'),
('gpgkey=file:///etc/pki/some.key',
'gpgkey=file:///etc/pki/some.key'),
('#baseurl=file:///home/user/my-repo',
'#baseurl=file:///home/user/my-repo'),
('', ''),
('[repoid]', '[repoid]')])
def test_adjust_local_file_url(repo_file_line, expected_adjusted_repo_file_line):
adjusted_repo_file_line = adjustlocalrepos._adjust_local_file_url(repo_file_line)
if 'file://' not in repo_file_line:
assert adjusted_repo_file_line == repo_file_line
return
assert adjusted_repo_file_line == expected_adjusted_repo_file_line


class MockedFileDescriptor(object):

def __init__(self, repo_file, expected_new_repo_file):
self.repo_file = repo_file
self.expected_new_repo_file = expected_new_repo_file

@staticmethod
def _create_repo_file_lines(repo_file):
repo_file_lines = []
for repo in repo_file:
repo = [line+'\n' for line in repo]
repo_file_lines += repo
return repo_file_lines

def __enter__(self):
return self

def __exit__(self, *args, **kwargs):
return

def readlines(self):
return self._create_repo_file_lines(self.repo_file)

def write(self, new_contents):
assert self.expected_new_repo_file
repo_file_lines = self._create_repo_file_lines(self.expected_new_repo_file)
expected_repo_file_contents = ''.join(repo_file_lines).rstrip('\n')
assert expected_repo_file_contents == new_contents


class MockedContext(object):

def __init__(self, repo_contents, expected_repo_contents):
self.repo_contents = repo_contents
self.expected_repo_contents = expected_repo_contents

def open(self, path, mode):
return MockedFileDescriptor(self.repo_contents, self.expected_repo_contents)


@pytest.mark.parametrize('repo_file, local_repoids, expected_repo_file',
[(REPO_FILE_1, REPO_FILE_1_LOCAL_REPOIDS, REPO_FILE_1_ADJUSTED),
(REPO_FILE_2, REPO_FILE_2_LOCAL_REPOIDS, REPO_FILE_2_ADJUSTED),
(REPO_FILE_3, REPO_FILE_3_LOCAL_REPOIDS, REPO_FILE_3_ADJUSTED)])
def test_adjust_local_repos_to_container(repo_file, local_repoids, expected_repo_file):
# The checks for expected_repo_file comparison to a adjusted form of the
# repo_file can be found in the MockedFileDescriptor.write().
context = MockedContext(repo_file, expected_repo_file)
adjustlocalrepos._adjust_local_repos_to_container(context, '<some_repo_file_path>', local_repoids)


@pytest.mark.parametrize('expected_repo_file, add_empty_lines', [(REPO_FILE_EMPTY, False),
(REPO_FILE_1, False),
(REPO_FILE_2, True)])
def test_extract_repos_from_repofile(expected_repo_file, add_empty_lines):
repo_file = expected_repo_file[:]
if add_empty_lines: # add empty lines before the first repo
repo_file[0] = ['', ''] + repo_file[0]

context = MockedContext(repo_file, None)
repo_gen = adjustlocalrepos._extract_repos_from_repofile(context, '<some_repo_file_path>')

for repo in expected_repo_file:
assert repo == next(repo_gen, None)

assert next(repo_gen, None) is None
89 changes: 0 additions & 89 deletions repos/system_upgrade/common/actors/localreposinhibit/actor.py

This file was deleted.

Loading

0 comments on commit e9f899c

Please sign in to comment.