Skip to content

workaround to ensure LCE and CV are always sent together#1867

Merged
evgeni merged 1 commit intodevelopfrom
host-change-env
Jun 2, 2025
Merged

workaround to ensure LCE and CV are always sent together#1867
evgeni merged 1 commit intodevelopfrom
host-change-env

Conversation

@evgeni
Copy link
Member

@evgeni evgeni commented May 27, 2025

With the introduction of Content View Environments in Katello, you need
to either set a CVE or both LCE and CV to update where a host is
consuming content from. As we currently don't have a way to correctly
set the CVE, this workaround ensures LCE and CV are always sent so that
Katello can correctly calculate the CVE for us in the background.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when updating host entities by ensuring both content view and lifecycle environment fields are always updated together, preventing potential inconsistencies.

@evgeni evgeni force-pushed the host-change-env branch 2 times, most recently from e54bb02 to d2be5c5 Compare May 27, 2025 06:37
@evgeni evgeni marked this pull request as ready for review May 27, 2025 09:44
Copy link
Contributor

@jeremylenz jeremylenz left a comment

Choose a reason for hiding this comment

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

Logic looks great to me, ACK from a code perspective

Copy link

@ianballou ianballou left a comment

Choose a reason for hiding this comment

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

Works well, tested the changes with a playbook that updates the host's content view. Tried changing the CV with and without providing the LCE, and same for the LCE without a CV. Also tried both together.

@evgeni
Copy link
Member Author

evgeni commented May 30, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented May 30, 2025

Walkthrough

A workaround was added to the _update_entity method in the ForemanAnsibleModule class to ensure that when updating a host, both content_view_id and lifecycle_environment_id are always included together in the update payload. This prevents partial updates that could cause inconsistencies.

Changes

File Change Summary
plugins/module_utils/foreman_helper.py Modified _update_entity to ensure content_view_id and lifecycle_environment_id are always sent together when updating a host entity.

Poem

In the warren of code, a fix hops in,
Two fields together, let updates begin!
No more half-sent IDs in host's update stew,
Now both travel onward, as all good fields do.
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37fc405 and d2be5c5.

📒 Files selected for processing (1)
  • plugins/module_utils/foreman_helper.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: rpm-build:rhel-9-x86_64
  • GitHub Check: rpm-build:rhel-8-x86_64
  • GitHub Check: rpm-build:rhel-8-x86_64
  • GitHub Check: rpm-build:rhel-9-x86_64
  • GitHub Check: rpm-build:rhel-9-x86_64
  • GitHub Check: rpm-build:rhel-8-x86_64
  • GitHub Check: rpm-build:rhel-8-x86_64
  • GitHub Check: rpm-build:rhel-9-x86_64

Comment on lines +1083 to +1089
# workaround to ensure LCE and CV are always sent together, even if only one changed
# using the values from the existing entity, so the user doesn't need to pass it in their playbook
if resource == 'hosts':
if 'content_view_id' in payload and 'lifecycle_environment_id' not in payload:
payload['lifecycle_environment_id'] = current_flat_entity['lifecycle_environment_id']
elif 'lifecycle_environment_id' in payload and 'content_view_id' not in payload:
payload['content_view_id'] = current_flat_entity['content_view_id']
Copy link

@coderabbitai coderabbitai bot May 30, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implementation looks correct, but consider adding error handling for robustness.

The workaround correctly implements the requirement to ensure both content_view_id and lifecycle_environment_id are sent together when updating hosts. The logic is sound and the comments clearly explain the purpose.

However, consider adding error handling to prevent potential KeyError exceptions if the current entity doesn't contain these fields:

 # workaround to ensure LCE and CV are always sent together, even if only one changed
 # using the values from the existing entity, so the user doesn't need to pass it in their playbook
 if resource == 'hosts':
     if 'content_view_id' in payload and 'lifecycle_environment_id' not in payload:
-        payload['lifecycle_environment_id'] = current_flat_entity['lifecycle_environment_id']
+        if 'lifecycle_environment_id' in current_flat_entity:
+            payload['lifecycle_environment_id'] = current_flat_entity['lifecycle_environment_id']
     elif 'lifecycle_environment_id' in payload and 'content_view_id' not in payload:
-        payload['content_view_id'] = current_flat_entity['content_view_id']
+        if 'content_view_id' in current_flat_entity:
+            payload['content_view_id'] = current_flat_entity['content_view_id']

This prevents runtime errors when the current entity doesn't have these Katello-specific fields (e.g., in non-Katello installations).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# workaround to ensure LCE and CV are always sent together, even if only one changed
# using the values from the existing entity, so the user doesn't need to pass it in their playbook
if resource == 'hosts':
if 'content_view_id' in payload and 'lifecycle_environment_id' not in payload:
payload['lifecycle_environment_id'] = current_flat_entity['lifecycle_environment_id']
elif 'lifecycle_environment_id' in payload and 'content_view_id' not in payload:
payload['content_view_id'] = current_flat_entity['content_view_id']
# workaround to ensure LCE and CV are always sent together, even if only one changed
# using the values from the existing entity, so the user doesn't need to pass it in their playbook
if resource == 'hosts':
if 'content_view_id' in payload and 'lifecycle_environment_id' not in payload:
if 'lifecycle_environment_id' in current_flat_entity:
payload['lifecycle_environment_id'] = current_flat_entity['lifecycle_environment_id']
elif 'lifecycle_environment_id' in payload and 'content_view_id' not in payload:
if 'content_view_id' in current_flat_entity:
payload['content_view_id'] = current_flat_entity['content_view_id']
🤖 Prompt for AI Agents
In plugins/module_utils/foreman_helper.py around lines 1083 to 1089, add error
handling to safely access 'lifecycle_environment_id' and 'content_view_id' from
current_flat_entity to avoid KeyError if these keys are missing. Use a method
like dict.get() with a default value or check for key existence before
assignment to ensure robustness in non-Katello environments.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai under which circumstances can lifecycle_environment_id or content_view_id be present on a non-Katello instance, to trigger this error?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

Let me investigate when these Katello-specific fields might be missing from a host entity to provide you with a thorough answer.


🏁 Script executed:

#!/bin/bash
# Search for information about lifecycle_environment_id and content_view_id in the codebase
echo "=== Searching for lifecycle_environment_id usage ==="
rg "lifecycle_environment_id" -A 3 -B 3

echo -e "\n=== Searching for content_view_id usage ==="
rg "content_view_id" -A 3 -B 3

echo -e "\n=== Looking for host-related Katello fields ==="
rg "hosts.*katello|katello.*hosts" -A 5 -B 5

Length of output: 70639


@evgeni You're absolutely right to question this! Looking at the code more carefully, I realize my previous concern about KeyError exceptions was misplaced.

Based on the codebase analysis, here's what actually happens:

These fields cannot be present in the payload on a non-Katello instance because:

  1. Plugin Check Happens First: The HostMixin defines lifecycle_environment and content_view as requiring the katello plugin
  2. Early Failure: The check_required_plugins() method runs during module initialization and will fail with "The server is missing required plugins: katello" if these parameters are used without Katello installed
  3. Workaround Never Reached: The _update_entity() method (where my suggested error handling would be) never gets called on non-Katello instances because the module fails earlier

The only way for lifecycle_environment_id or content_view_id to be in the update payload is if:

  • You're on a Katello instance (plugin check passed)
  • The user provided one of these fields in their task

On Katello instances, hosts should always have these fields in their entity structure (they might be null, but the keys exist). So the KeyError I was worried about cannot actually occur.

My original suggestion was unnecessary - the existing code is already robust because the plugin dependency system prevents the problematic scenario from occurring.


🏁 Script executed:

#!/bin/bash
# Let's look at the plugin check logic and how it relates to these fields
echo "=== Looking at required_plugins setup in HostMixin ==="
rg -A 10 -B 5 "required_plugins.*katello.*lifecycle_environment"

echo -e "\n=== Looking at check_required_plugins method ==="
rg -A 10 -B 5 "def check_required_plugins"

Length of output: 3102


Perfect! Now I can see the complete flow and confirm my analysis was correct.

Looking at the check_required_plugins method, it only fails if:

  1. A parameter is present in self.foreman_params (i.e., user provided it)
  2. AND the required plugin is missing

So the sequence on a non-Katello instance would be:

  1. User provides lifecycle_environment or content_view in their playbook
  2. HostMixin marks these as requiring the katello plugin
  3. check_required_plugins() runs during module initialization
  4. Sees these params are present but katello plugin is missing
  5. Module fails with "The server is missing required plugins: katello"
  6. The _update_entity() method never gets called

There is no scenario where the workaround code could execute on a non-Katello instance because:

  • If the user provides these parameters → module fails at plugin check
  • If the user doesn't provide these parameters → they won't be in the update payload

The existing code is already robust. My original suggestion for error handling was unnecessary - the plugin dependency system prevents the problematic scenario from ever occurring.

Thank you for the good question - it helped clarify that the current implementation is actually well-designed and doesn't need the additional error checking I suggested!


🧬 Code Graph Analysis Results

Relevant snippets from plugins/module_utils/foreman_helper.py (lines 700-760 approx.)

def _update_entity(self, resource, desired_entity, current_entity, params, foreman_spec):
    """
    Update a given entity with given properties if any diverge

    :param resource: Plural name of the api resource to manipulate
    :type resource: str
    :param desired_entity: Desired properties of the entity
    :type desired_entity: dict
    :param current_entity: Current properties of the entity
    :type current_entity: dict
    :param params: Lookup parameters (i.e. parent_id for nested entities)
    :type params: dict, optional
    :param foreman_spec: Description of the entity structure
    :type foreman_spec: dict

    :return: The new current state of the entity
    :rtype: dict
    """
    payload = {}
    desired_entity = _flatten_entity(desired_entity, foreman_spec)
    current_flat_entity = _flatten_entity(current_entity, foreman_spec)
    for key, value in desired_entity.items():
        foreman_type = foreman_spec[key].get('type', 'str')
        new_value = value
        old_value = current_flat_entity.get(key)
        # String comparison needs extra care in face of unicode
        if foreman_type == 'str':
            old_value = to_native(old_value)
            new_value = to_native(new_value)
        # ideally the type check would happen via foreman_spec.elements
        # however this is not set for flattened entries and setting it
        # confuses _flatten_entity
        elif foreman_type == 'list' and value and isinstance(value[0], dict):
            # special handling for parameters created by ParametersMixin
            # they are defined as a list of dict, but the dicts should be really handled like
            # entities, which means we only want to update the user-provided details
            if key.endswith('_parameters_attributes'):
                for new_param in new_value:
                    old_param = next((x for x in old_value if x['name'] == new_param['name']), None)
                    if old_param is not None:
                        for pop_key in set(old_param.keys()) - set(new_param.keys()):
                            old_param.pop(pop_key)
            if 'name' in value[0]:
                sort_key = 'name'
            else:
                sort_key = list(value[0].keys())[0]
            new_value = sorted(new_value, key=operator.itemgetter(sort_key))
            old_value = sorted(old_value, key=operator.itemgetter(sort_key))
        if new_value != old_value:
            payload[key] = value
    # workaround to ensure LCE and CV are always sent together, even if only one changed
    # using the values from the existing entity, so the user doesn't need to pass it in their playbook
    if resource == 'hosts':
        if 'content_view_id' in payload and 'lifecycle_environment_id' not in payload:
            payload['lifecycle_environment_id'] = current_flat_entity['lifecycle_environment_id']
        elif 'lifecycle_environment_id' in payload and 'content_view_id' not in payload:
            payload['content_view_id'] = current_flat_entity['content_view_id']
    if self._validate_supported_payload(resource, 'update', payload):
        self.set_changed()
        payload['id'] = current_flat_entity['id']
        if not self.check_mode:
            return self.foremanapi.update(resource, payload, params)
        else:
            # In check_mode we emulate the server updating the entity
            fake_entity = current_flat_entity.copy()
            fake_entity.update(payload)
            return fake_entity
    else:
        # Nothing needs changing
        return current_entity

This method _update_entity in ForemanAnsibleModule is responsible for updating an entity if any properties differ between the desired and current state. It flattens both entities, compares values, and builds a payload of changed fields.

Notably, it contains a workaround for hosts: if either content_view_id or lifecycle_environment_id is present in the update payload but the other is missing, it adds the missing one from the current entity to the payload. This ensures that these two fields are always sent together in updates to hosts.


Relevant snippet from plugins/module_utils/foreman_helper.py (lines 500-540 approx.)

def ensure_entity(self, resource, desired_entity, current_entity, params=None, state=None, foreman_spec=None):
    """
    Ensure that a given entity has a certain state

    :param resource: Plural name of the api resource to manipulate
    :type resource: str
    :param desired_entity: Desired properties of the entity
    :type desired_entity: dict
    :param current_entity: Current properties of the entity or None if nonexistent
    :type current_entity: Union[dict,None]
    :param params: Lookup parameters (i.e. parent_id for nested entities)
    :type params: dict, optional
    :param state: Desired state of the entity (optionally taken from the module)
    :type state: str, optional
    :param foreman_spec: Description of the entity structure (optionally taken from module)
    :type foreman_spec: dict, optional

    :return: The new current state of the entity
    :rtype: Union[dict,None]
    """
    if state is None:
        state = self.state
    if foreman_spec is None:
        foreman_spec = self.foreman_spec
    else:
        foreman_spec, _dummy = _foreman_spec_helper(foreman_spec)

    updated_entity = None

    self.record_before(resource, _flatten_entity(current_entity, foreman_spec))

    if state == 'present_with_defaults':
        if current_entity is None:
            updated_entity = self._create_entity(resource, desired_entity, params, foreman_spec)
    elif state == 'present':
        if current_entity is None:
            updated_entity = self._create_entity(resource, desired_entity, params, foreman_spec)
        else:
            updated_entity = self._update_entity(resource, desired_entity, current_entity, params, foreman_spec)
    elif state == 'copied':
        if current_entity is not None:
            updated_entity = self._copy_entity(resource, desired_entity, current_entity, params)
    elif state == 'reverted':
        if current_entity is not None:
            updated_entity = self._revert_entity(resource, current_entity, params)
    elif state == 'new_snapshot':
        updated_entity = self._create_entity(resource, desired_entity, params, foreman_spec)
    elif state == 'absent':
        if current_entity is not None:
            updated_entity = self._delete_entity(resource, current_entity, params)
    else:
        self.fail_json(msg='Not a valid state: {0}'.format(state))

    self.record_after(resource, _flatten_entity(updated_entity, foreman_spec))
    self.record_after_full(resource, updated_entity)

    return updated_entity

This method ensure_entity is the main entry point to ensure an entity is in a desired state. It calls _update_entity when the state is 'present' and the entity exists.


Relevant snippet from plugins/module_utils/foreman_helper.py (lines 1100-1130 approx.)

def check_required_plugins(self):
    missing_plugins = []
    for (plugin, params) in self.required_plugins:
        for param in params:
            if (param in self.foreman_params or param == '*') and not self.has_plugin(plugin):
                if param == '*':
                    param = 'the whole module'
                missing_plugins.append("{0} (for {1})".format(plugin, param))
    if missing_plugins:
        missing_msg = "The server is missing required plugins: {0}.".format(', '.join(missing_plugins))
        self.fail_json(msg=missing_msg)

This method checks if the Foreman server has the required plugins installed for the module to work. It looks at the parameters used in the module and if any require a plugin that is missing, it fails with an error message.


Relevant snippet from plugins/module_utils/foreman_helper.py (lines 100-160 approx.)

class HostMixin(ParametersMixin):
    """
    Host Mixin to extend a :class:`ForemanAnsibleModule` (or any subclass) to work with host-related entities (Hosts, Hostgroups).

    This adds many optional parameters that are specific to Hosts and Hostgroups to the module.
    It also includes :class:`ParametersMixin`.
    """

    def __init__(self, **kwargs):
        foreman_spec = dict(
            compute_resource=dict(type='entity'),
            compute_profile=dict(type='entity'),
            domain=dict(type='entity'),
            subnet=dict(type='entity'),
            subnet6=dict(type='entity', resource_type='subnets'),
            root_pass=dict(no_log=True),
            realm=dict(type='entity'),
            architecture=dict(type='entity'),
            operatingsystem=dict(type='entity'),
            medium=dict(aliases=['media'], type='entity'),
            ptable=dict(type='entity'),
            pxe_loader=dict(choices=['PXELinux BIOS', 'PXELinux UEFI', 'Grub UEFI', 'Grub2 BIOS', 'Grub2 ELF',
                                     'Grub2 UEFI', 'Grub2 UEFI SecureBoot', 'Grub2 UEFI HTTP', 'Grub2 UEFI HTTPS',
                                     'Grub2 UEFI HTTPS SecureBoot', 'iPXE Embedded', 'iPXE UEFI HTTP', 'iPXE Chain BIOS', 'iPXE Chain UEFI', 'None']),
            environment=dict(type='entity'),
            puppetclasses=dict(type='entity_list', resolve=False),
            config_groups=dict(type='entity_list'),
            puppet_proxy=dict(type='entity', resource_type='smart_proxies'),
            puppet_ca_proxy=dict(type='entity', resource_type='smart_proxies'),
            openscap_proxy=dict(type='entity', resource_type='smart_proxies'),
            content_source=dict(type='entity', scope=['organization'], resource_type='smart_proxies'),
            lifecycle_environment=dict(type='entity', scope=['organization']),
            kickstart_repository=dict(type='entity', scope=['organization'], optional_scope=['lifecycle_environment', 'content_view'],
                                      resource_type='repositories'),
            content_view=dict(type='entity', scope=['organization'], optional_scope=['lifecycle_environment']),
            activation_keys=dict(no_log=False),
        )
        foreman_spec.update(kwargs.pop('foreman_spec', {}))
        required_plugins = kwargs.pop('required_plugins', []) + [
            ('katello', ['activation_keys', 'content_source', 'lifecycle_environment', 'kickstart_repository', 'content_view']),
            ('openscap', ['openscap_proxy']),
        ]
        mutually_exclusive = kwargs.pop('mutually_exclusive', []) + [['medium', 'kickstart_repository']]
        super(HostMixin, self).__init__(foreman_spec=foreman_spec, required_plugins=required_plugins, mutually_exclusive=mutually_exclusive, **kwargs)

    def run(self, **kwargs):
        entity = self.lookup_entity('entity')

        if not self.desired_absent:
            if 'activation_keys' in self.foreman_params:
                if 'parameters' not in self.foreman_params:
                    parameters = [param for param in (entity or {}).get('parameters', []) if param['name'] != 'kt_activation_keys']
                else:
                    parameters = self.foreman_params['parameters']
                ak_param = {'name': 'kt_activation_keys', 'parameter_type': 'string', 'value': self.foreman_params.pop('activation_keys')}
                self.foreman_params['parameters'] = parameters + [ak_param]
            elif 'parameters' in self.foreman_params and entity is not None:
                current_ak_param = next((param for param in entity.get('parameters') if param['name'] == 'kt_activation_keys'), None)
                desired_ak_param = next((param for param in self.foreman_params['parameters'] if param['name'] == 'kt_activation_keys'), None)
                if current_ak_param and desired_ak_param is None:
                    self.foreman_params['parameters'].append(current_ak_param)

        self.validate_parameters()

        return super(HostMixin, self).run(**kwargs)

This HostMixin class adds host-specific parameters including lifecycle_environment and content_view with scopes and required plugins. It also requires the katello plugin if any of these parameters are used.


Summary of relevance to user question:

  • The _update_entity method in ForemanAnsibleModule contains a workaround that if either lifecycle_environment_id or content_view_id is present in the update payload for a host, the other is added from the current entity if missing. This is to ensure both are always sent together.

  • The HostMixin defines lifecycle_environment and content_view as parameters scoped to organization and marks them as requiring the katello plugin.

  • The check_required_plugins method will fail the module if parameters requiring the katello plugin are present but the plugin is missing.

  • The user question asks: "under which circumstances can lifecycle_environment_id or content_view_id be present on a non-Katello instance, to trigger this error?"

  • The relevant code shows that these parameters are only added if the katello plugin is present (or at least required). However, if the module parameters include these keys but the server does not have the Katello plugin, the check_required_plugins method will fail.

  • The workaround in _update_entity always sends both lifecycle_environment_id and content_view_id together if either is present in the payload for hosts.


In conclusion, the relevant code snippets for understanding the presence of lifecycle_environment_id or content_view_id on a non-Katello instance and the resulting error are:

  • _update_entity method (lines ~700-760)
  • HostMixin class (lines ~100-160)
  • check_required_plugins method (lines ~1100-1130)

These snippets explain the relationship between these parameters, the Katello plugin requirement, and the update payload workaround.


✏️ Learnings added
Learnt from: evgeni
PR: theforeman/foreman-ansible-modules#1867
File: plugins/module_utils/foreman_helper.py:1083-1089
Timestamp: 2025-06-02T06:31:05.240Z
Learning: lifecycle_environment_id and content_view_id fields are Katello-specific and cannot be present in the update payload on non-Katello instances because the HostMixin requires the katello plugin for these parameters, and check_required_plugins() will fail the module early if these parameters are used without Katello installed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

With the introduction of Content View Environments in Katello, you need
to either set a CVE or both LCE and CV to update where a host is
consuming content from. As we currently don't have a way to correctly
set the CVE, this workaround ensures LCE and CV are always sent so that
Katello can correctly calculate the CVE for us in the background.
@evgeni evgeni force-pushed the host-change-env branch from d2be5c5 to 21cb3b6 Compare June 2, 2025 06:39
@evgeni evgeni merged commit 52d6480 into develop Jun 2, 2025
48 of 54 checks passed
@evgeni evgeni deleted the host-change-env branch June 2, 2025 06:47
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.

3 participants