Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 73 additions & 36 deletions uyuni-tools/migrate_to_new_client_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
# This script assumes that new client tools have been already synced in your SUSE Multi-Linux Manager and Uyuni instance. One can use sync_client_tools.py script to sync the new client tools.
#
# 2025-10-14 Abid - initial release.
# 2025-10-29 Michael Brookhuis - added --frozen parameter to freeze channels from newer patches in clm projects.
# 2025-10-20 Abid - Added the promote parameter to promote only if specified.


"""
import xmlrpc.client
import time
import sys
import argparse
import random
from datetime import datetime
from argparse import RawTextHelpFormatter

# --- Configuration ---
Expand Down Expand Up @@ -50,7 +55,7 @@ def list_and_find_base_channels(client, key):
base_channels = [ch["label"] for ch in channels if not ch.get("parent_label")]
return base_channels

def process_clm_project(client, key, project_label, base_channels, dry_run):
def process_clm_project(client, key, project_label, base_channels, dry_run, promote, frozen):
"""Processes a single CLM project, updating channels and promoting environments."""
log(f"\n=== Processing Project: {project_label} ===")

Expand Down Expand Up @@ -106,40 +111,57 @@ def process_clm_project(client, key, project_label, base_channels, dry_run):
all_envs = client.contentmanagement.listProjectEnvironments(key, project_label)

if not all_envs:
log("No environments found for this project.")
log("[WARNING] No environments found for this project.")
return

first_env_label = all_envs[0]['label']

for i, env in enumerate(all_envs):
env_label = env['label']
is_first_env = (env_label == first_env_label)

if is_first_env:
description = "Build for new client tools channels."
if frozen and not dry_run:
log("Parameter frozen is provided, not updating channels with newer patches by adding a new filter based on last build date.")
project_data = client.contentmanagement.lookupProject(key, project_label)
dt_object = datetime.strptime(str(project_data.get('lastBuildDate')), '%Y%m%dT%H:%M:%S')
filter_time = dt_object.strftime("%Y-%m-%dT%H:%M:%SZ")
random_number = random.randint(1111,9999)
create_filter = client.contentmanagement.createFilter(key, f"frozen_{project_label}_{random_number}", "deny", "erratum", {'matcher': "greatereq", 'field': "issue_date", 'value': filter_time})
added_filter = client.contentmanagement.attachFilter(key, project_label, create_filter['id'])
log(f"Filter -> {create_filter.get('name')} has been created and attached to project {project_label} to freeze the channels from newer patches.")
else:
log("Parameter frozen is not given, newer patches will be promoted in the channels")

# Only promote if specified
needsbuilding = False
if promote:
sources = client.contentmanagement.listProjectSources(key, project_label)
needsbuilding = any(s.get('state', '').upper() in ('ATTACHED', 'DETACHED') for s in sources)
# Only build/promote if there are changes
if needsbuilding:
for i, env in enumerate(all_envs):
env_label = env['label']
is_first_env = (env_label == first_env_label)

if is_first_env:
description = "Build for new client tools channels."
if dry_run:
dry_run_log(f"Would build initial environment {env_label}")
else:
log(f"Building initial environment (label: {env_label})")
client.contentmanagement.buildProject(key, project_label, description)
if not wait_for_completion(client, key, project_label, env_label):
log("Build failed or timed out. Aborting promotion process.")
return
else:
prev_env_label = env['previousEnvironmentLabel']
if dry_run:
dry_run_log(f"Would build initial environment {env_label}")
dry_run_log(f"Would promote the environment {prev_env_label} to {env_label}")
else:
log(f"Building initial environment (label: {env_label})")
client.contentmanagement.buildProject(key, project_label, description)
if not wait_for_completion(client, key, project_label, env_label):
log("Build failed or timed out. Aborting promotion process.")
log(f"Promoting the environment {prev_env_label} to {env_label}")
client.contentmanagement.promoteProject(key, project_label, prev_env_label)
if not wait_for_completion(client, key, project_label, prev_env_label):
log("Promotion failed or timed out. Aborting promotion process.")
return
else:
prev_env_label = env['previousEnvironmentLabel']
if dry_run:
dry_run_log(f"Would promote the environment {prev_env_label} to {env_label}")
else:
log(f"Promoting the environment {prev_env_label} to {env_label}")
client.contentmanagement.promoteProject(key, project_label, prev_env_label)
if not wait_for_completion(client, key, project_label, prev_env_label):
log("Promotion failed or timed out. Aborting promotion process.")
return

if not dry_run and i < len(all_envs) - 1:
log("Waiting 30 seconds before next promotion...")
time.sleep(30)

if not dry_run and i < len(all_envs) - 1:
log("Waiting 30 seconds before next promotion...")
time.sleep(30)

def wait_for_completion(client, key, project_label, env_label, wait_interval=30):
"""Polls the project environment status until it is 'built' or an error occurs."""
Expand Down Expand Up @@ -208,7 +230,6 @@ def process_activation_keys(client, key, activation_keys, dry_run):
else:
log(f"Could not determine base channel for key {ak_key}. Skipping update.")
continue

if dry_run:
if old_tools:
dry_run_log(f"Would remove the old client tools channels {old_tools} from key {ak_key}")
Expand Down Expand Up @@ -243,25 +264,41 @@ def main():

Examples:
- Process a single CLM project and all its environments:
python3 script_name.py -c clmprojects clm2
python3 migrate_to_new_client_tools.py -c clmprojects clm2 --promote --frozen --no-dry-run

- Process all CLM projects:
python3 script_name.py -c clmprojects all
- Process a single CLM project but don't promote changes to environments:
python3 migrate_to_new_client_tools.py -c clmprojects clm2 --frozen --no-dry-run

- Process all CLM projects but don't promote changes to environments:
python3 migrate_to_new_client_tools.py -c clmprojects all --frozen --no-dry-run

- Process a single activation key:
python3 script_name.py -c activationkeys 1-sles15sp4-x86_64
python3 migrate_to_new_client_tools.py -c activationkeys 1-sles15sp4-x86_64

- Process all autoinstallation profiles with actual changes:
python3 script_name.py -c autoinstallprofiles all --no-dry-run
python3 migrate_to_new_client_tools.py -c autoinstallprofiles all --no-dry-run
''')

parser.add_argument("-c", "--component", choices=['clmprojects', 'activationkeys', 'autoinstallprofiles'], required=True, help="The component to process.")
parser.add_argument("labels", nargs='+', help="The label(s) of the component to process, or 'all'.")
parser.add_argument("--promote", action='store_true', default=False, help="(CLM only) - Only promote changes to environments if mentioned explictly'.")
parser.add_argument("--frozen", action='store_true', default=False, help="(CLM only) - If fixed channels are used, don't update the channels with newer patches in clm projects. Defaults to 'False'.")
parser.add_argument("--no-dry-run", action='store_true', help="Perform actual changes instead of a dry run.")

args = parser.parse_args()

if args.component != 'clmprojects' and args.frozen:
print(f"[WARNING] The **--frozen** argument is only applicable to 'clmprojects' and has no effect for '{args.component}'.")
if args.component != 'clmprojects' and args.promote:
print(f"[WARNING] The **--promote** argument is only applicable to 'clmprojects' and has no effect for '{args.component}'.")

dry_run = not args.no_dry_run

if dry_run:
print(f"=========================================================")
print(f"[INFO] RUNNING IN DRY-RUN MODE. NO CHANGES WILL BE MADE.")
print(f"=========================================================")

labels_to_process = args.labels[0].split(',') if args.labels[0].lower() != 'all' else ['all']

client, key = connect_and_login()
Expand All @@ -280,7 +317,7 @@ def main():
if not any(p['label'] == project_label for p in client.contentmanagement.listProjects(key)):
log(f"Project '{project_label}' not found. Skipping.")
continue
process_clm_project(client, key, project_label, base_channels, dry_run)
process_clm_project(client, key, project_label, base_channels, dry_run, args.promote, args.frozen)

elif args.component == 'activationkeys':
if 'all' in labels_to_process:
Expand Down
4 changes: 2 additions & 2 deletions uyuni-tools/sync_client_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ def add_client_tools_channels(client, key, extensions, dry_run):
def main():
"""Main function to run the entire workflow."""
parser = argparse.ArgumentParser(description="A script to add client tools channels to SUSE Manager.",
formatter_class=argparse.RawTextHelpFormatter)
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("--no-dry-run", action="store_true",
help="Perform actual changes instead of a dry run.\n"
"The script runs in dry-run mode by default.")
"The script runs in dry-run mode by default.")
args = parser.parse_args()
dry_run = not args.no_dry_run

Expand Down