Skip to content

XCP-ng 9 fixes - #570

Open
glehmann wants to merge 4 commits into
masterfrom
gln/9
Open

XCP-ng 9 fixes#570
glehmann wants to merge 4 commits into
masterfrom
gln/9

Conversation

@glehmann

@glehmann glehmann commented Jun 4, 2026

Copy link
Copy Markdown
Member

Adapt to XCP-ng 9 specificities

Comment thread lib/host.py Outdated

self.rescan_block_devices_info()

if self.file_exists('/usr/bin/yum'):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woultnt it make sense to prefer dnf over yum ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d say that for now, yum is the default, since we’re on version 8.3 and are adding dnf alongside it for 9.0. And as we don't have dnf on the 8.3 (and before).

If we wanted dnf to be the default over yum for 9.0, we would need to base that on version detection first, before the presence of the file.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no yum command on XCP-ng 9, so it doesn't matter much in practice :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is actually a yum compatibility wrapper (in package yum). It's surely safer to look for dnf first, just in case.

Comment thread lib/host.py Outdated
try:
history_str = self.ssh('yum history list --noplugins')
history_str = self.ssh(f'{self.package_manager} history list --noplugins')
except commands.SSHCommandFailed:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not directly related to your PR, and is probably code I wrote initially, so I just make it a suggestion: we should probably only tolerate failures that we can verify to be actually caused by an empty history, rather than any command failure that could happen for whatever reason. And let the exception through in the other cases.

Comment thread lib/host.py Outdated
if not history:
# we need a transaction to already exist. Install a small package with no deps, and remove it immediately.
logging.info(f"[{self}] Install and remove a small package to workaround empty yum history.")
small_pkg = 'gpm-libs' if self.xcp_version.major == 8 else 'bubblewrap'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest dummypkg, a package that must be present in our repos because other tests rely on it. If not present in XCP-ng 9, we should add it there too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not in xcp-ng 9 yet
We could use the same package on xcp-ng 9 and xcp-ng 8.3 👍

Comment thread conftest.py Outdated
pytest.skip(f"This test requires an XCP-ng < {version_str} host")

@pytest.fixture(scope='session')
@pytest.fixture(scope='function')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most host_xxx fixtures in this global conftest.py are session-scoped. I think that to change the scope we should change the fixture name too to avoid wrong expectations. And/or bring the fixture closer to where it's used.

Comment thread lib/common.py
ret = fn()
if not invert and ret:
return
return ret

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: it took me time to understand this part of the commit message: "Enhance wait_for to return the callback value ", because I thought it meant you returned the callback itself, as a value.

Due to the use of word "callback", I thought there was a function we'd call back once returned, but you just mean you return the value produced by the function executed by wait_for.

Suggestion: "Enhance wait_for to return the polled function's result"

Comment thread lib/vm.py Outdated
tmp_file = res_host.ssh('mktemp')
session = f"detached-cat-{self.uuid}"
ret = False
# screen install is broken on RHEL, see https://bugzilla.redhat.com/show_bug.cgi?id=2385964

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, another "I got confused by the wording" item: I thought there was something about display issues 😂.

But you're talking about screen, the tool.

Suggestion:

The `screen` tool may be broken on RHEL, see https://bugzilla.redhat.com/show_bug.cgi?id=2385964

Comment thread lib/common.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to ask: was the PR tested on XCP-ng 8 too? Probably so but it would be best to mention it in the PR description so that we don't ask :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested both for the tests I modified, but I haven't run a complete run on XCP-ng 8.3 yet.

@glehmann

Copy link
Copy Markdown
Member Author

I integrated the requested changes and updated on master

Comment thread lib/host.py Outdated

self.rescan_block_devices_info()

if self.file_exists('/usr/bin/yum'):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is actually a yum compatibility wrapper (in package yum). It's surely safer to look for dnf first, just in case.

Comment thread lib/host.py
Comment on lines 642 to 678
"""
try:
history_str = self.ssh('yum history list --noplugins')
except commands.SSHCommandFailed:
# yum history list fails if the list is empty, and it's also not possible to rollback
# to before the first transaction, so "0" would not be appropriate as last transaction.
# To workaround this, create transactions: install and remove a small package.
logging.info(f"[{self}] Install and remove a small package to workaround empty yum history.")
self.yum_install(['gpm-libs'])
self.yum_remove(['gpm-libs'])
history_str = self.ssh('yum history list --noplugins')

history = history_str.splitlines()
line_index = None
for i in range(len(history)):
if history[i].startswith('--------'):
line_index = i
break
history_str = self.ssh(f'{self.package_manager} history list --noplugins')
except commands.SSHCommandFailed as e:
if 'Error: Failed history list' not in e.stdout:
# this is not the expected error for an empty history list, re-raise the exception
raise e
# yum history list fails on xcp-ng 8 if the list is empty, but dnf history list works properly on xcp-ng 9.
# Just use a fake empty value to deal with both versions in the same way.
if self.package_manager == 'yum':
history_str = '''ID | Command line | Date and time | Action(s) | Altered
-------------------------------------------------------------------------------'''
else:
raise

if line_index is None:
raise Exception('Unable to get yum transactions')
def split_history(history_str: str) -> list[str]:
history = history_str.splitlines()
line_index = None
for i in range(len(history)):
if history[i].startswith('--------'):
line_index = i
break
if line_index is None:
raise Exception('Unable to get yum transactions')
return history[line_index + 1:]

history = split_history(history_str)
if not history:
# we need a transaction to already exist. Install a small package with no deps, and remove it immediately.
logging.info(f"[{self}] Install and remove a small package to workaround empty yum history.")
self.yum_install(['dummypkg'])
self.yum_remove(['dummypkg'])
history = split_history(self.ssh(f'{self.package_manager} history list --noplugins'))

try:
return int(history[line_index + 1].split()[0])
return int(history[0].split()[0])
except ValueError:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nitpick?) This change could have been easier to read by introducing split_history() in a preliminary commit (which involves some reindentation), and then the effective difference specific to v9 would have stood out more clearly in the diff.

Comment thread lib/host.py Outdated
Comment on lines +645 to +648
except commands.SSHCommandFailed as e:
if 'Error: Failed history list' not in e.stdout:
# this is not the expected error for an empty history list, re-raise the exception
raise e

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise without argument would be more correct I think

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, because it would avoid changing the stack trace 👍

Comment thread lib/host.py
logging.info(f"[{self}] Install and remove a small package to workaround empty yum history.")
self.yum_install(['dummypkg'])
self.yum_remove(['dummypkg'])
history = split_history(self.ssh(f'{self.package_manager} history list --noplugins'))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few words in the commit message about that particular behavior change and how it is restructured would be nice (yes the can read the code and actually understand, but a "reading guide" would actually allow the reader to spend less time)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. This feels very much not useful to me, but if that's useful to you…

Comment thread lib/host.py
Comment on lines -703 to +729
self.ssh(
f'yum history rollback --enablerepo=xcp-ng-base,xcp-ng-testing,xcp-ng-updates {self.saved_rollback_id} -y'
)
repositories = ['xcp-ng-base']
if self.xcp_version.major == 8:
# TODO: activate those repositories in xcp-ng 9. For now they are not available.
repositories += ['xcp-ng-testing', 'xcp-ng-updates']
self.ssh(f'{self.package_manager} history rollback'
f' --enablerepo={",".join(repositories)} {self.saved_rollback_id} -y')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also is not implied by "use the right package manager". Sure this is v9 support, and relates to repositories, so likely has a place grouped as such, but would be nice in the commit message

Comment thread lib/host.py
Comment on lines -642 to -643
self.yum_install(['gpm-libs'])
self.yum_remove(['gpm-libs'])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switching to dummypkg because this package is not available in v9?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure about that? I added it yesterday :)

Comment thread tests/misc/test_export.py Outdated
Comment on lines +22 to +30
def check_file_type(expected: str) -> None:
assert host.ssh(f'file --mime-type -b {filepath}') == expected
def check_file_type(expected: list[str]) -> None:
assert host.ssh(f'file --mime-type -b {filepath}') in expected

if compress == 'none':
check_file_type('application/x-tar')
check_file_type(['application/x-tar'])
elif compress == 'gzip':
check_file_type('application/x-gzip')
check_file_type(['application/x-gzip', 'application/gzip'])
elif compress == 'zstd':
check_file_type('application/octet-stream')
check_file_type(['application/octet-stream', 'application/zstd'])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree this makes things "more robust". application/octet-stream is "I did not recognize this file", which should be an error when the more specific type is known. I like my "test_export: support modern distro knowing about zstd and better gzip" patch in ydi/9 better 😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replace this commit with yours

Comment thread lib/installer.py
raise InstallationFailed(failed)
return cmd()
return wait_for(inner, msg, timeout_secs=timeout_secs)
wait_for(inner, msg, timeout_secs=timeout_secs)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels awkward to have wait_for gain a real return value, and just not propagate it here.
Any reason not to change this function to behave similarly to ` (possibly by just changing the return type annotation)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the function to call returns a bool (Callable[[], bool]), so, given the behavior of wait_for, the only possible returned value is Literal[True], which is not very useful.
I'm fine with both versions, though

Comment thread lib/common.py
Comment on lines -195 to +196
def wait_for(fn: Callable[[], object], msg: str | None = None, timeout_secs: int = 2 * 60, retry_delay_secs: int = 2,
invert: bool = False) -> None:
def wait_for(fn: Callable[[], T], msg: str | None = None, timeout_secs: int = 2 * 60, retry_delay_secs: int = 2,
invert: bool = False) -> T:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This use of the return value should be documented, a function docstring would seem adequate.

Especially the behavior with invert=True may need some notice, that only False-like values will be returned in this case (so not very useful).

Comment thread lib/vm.py Outdated
tmp_file = res_host.ssh('mktemp')
session = f"detached-cat-{self.uuid}"
ret = False
# The screen package installation is broken on RHEL, see https://bugzilla.redhat.com/show_bug.cgi?id=2385964

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/RHEL/el10/ ?

glehmann and others added 4 commits August 28, 2026 11:58
This is required for XCP-ng 9, which uses dnf.

Reading guide for get_last_yum_history_tid():
- Behavior change: the empty-history workaround now only runs when the
  parsed history is genuinely empty, instead of on any failure of
  "history list". Other errors now propagate (previously they were
  masked by the workaround). dnf succeeds on an empty history (only the
  header is output) while yum fails with "Error: Failed history list",
  so a fake header row is used for yum to handle both the same way.
- Restructure: output parsing was extracted into a split_history()
  helper, and the workaround (install/remove a dummy package to create
  a transaction) is now triggered by the empty parsed output rather
  than by the command failing.

Also yum_restore_saved_state() doesn't activate xcp-ng-testing and
xcp-ng-updates repositories, as they are not yet available.

Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
Signed-off-by: Yann Dirson <yann.dirson@vates.tech>
Enhance wait_for to return the polled function's result and use it to
retry file server header retrieval until the server is ready.
Change host_with_hsts fixture to function scope for test isolation,
and move it in the module where it's used, to make clear it's not a
session scope anymore.

Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants