Skip to content

Commit 4698987

Browse files
committed
Add a utility script to help migrating the activation keys and clm projects to new client tools
1 parent 99ae5fc commit 4698987

File tree

1 file changed

+305
-0
lines changed

1 file changed

+305
-0
lines changed
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
"""
2+
# (c) 2019 SUSE Linux GmbH, Germany.
3+
# GNU Public License. No warranty. No Support
4+
# For question/suggestions/bugs mail: [email protected]
5+
#
6+
# Version: 2025-10-14
7+
#
8+
# Created by: Abid Mehmood
9+
#
10+
# Using this cscript you can update your actiavation keys and CLM projects by removing the old client tools and switching to new client tools.
11+
# This script assumes that new client tools have been already syned in your MLM/Uyuni instance.
12+
#
13+
# Releasmt.session:
14+
# 2017-01-2 Abid - initial release.
15+
16+
"""
17+
import xmlrpc.client
18+
import time
19+
import sys
20+
import argparse
21+
from argparse import RawTextHelpFormatter
22+
23+
# --- Configuration ---
24+
SUSE_MULTI_LINUX_MANAGER_SERVER = "<your-server>"
25+
USERNAME = "<username>"
26+
PASSWORD = "<password>"
27+
28+
def log(message):
29+
print(f"[INFO] {message}")
30+
31+
def dry_run_log(message):
32+
print(f"[DRY-RUN] {message}")
33+
34+
def connect_and_login():
35+
"""Connects to the XML-RPC API and returns a session key."""
36+
try:
37+
log(f"Connecting to {SUSE_MULTI_LINUX_MANAGER_SERVER}...")
38+
client = xmlrpc.client.Server(f"http://{SUSE_MULTI_LINUX_MANAGER_SERVER}/rpc/api")
39+
key = client.auth.login(USERNAME, PASSWORD)
40+
log("Successfully logged in.")
41+
return client, key
42+
except Exception as e:
43+
print(f"[ERROR] Failed to connect or login: {e}")
44+
return None, None
45+
46+
def list_and_find_base_channels(client, key):
47+
"""Lists all software channels and returns a list of base channel labels."""
48+
channels = client.channel.listSoftwareChannels(key)
49+
base_channels = [ch["label"] for ch in channels if not ch.get("parent_label")]
50+
return base_channels
51+
52+
def process_clm_project(client, key, project_label, base_channels, dry_run):
53+
"""Processes a single CLM project, updating channels and promoting environments."""
54+
log(f"\n=== Processing Project: {project_label} ===")
55+
56+
sources = client.contentmanagement.listProjectSources(key, project_label)
57+
# It's assumed that old client tools channels contain 'manager-tools' and new ones 'managertools' in their labels
58+
old_tools = [s['channelLabel'] for s in sources if 'manager-tools' in s.get('channelLabel', '').lower()]
59+
new_tools = [s['channelLabel'] for s in sources if 'managertools' in s.get('channelLabel', '').lower()]
60+
61+
if not old_tools and not new_tools:
62+
log("No old client tools channels to detach or new ones to attach. Skipping project promotion.")
63+
return
64+
65+
log(f"Old client tools channels (to be detached): {old_tools}")
66+
log(f"New client tools channels already present: {new_tools}")
67+
68+
if old_tools:
69+
log("\n=== Detaching Old Client Tools Channels ===")
70+
for old in old_tools:
71+
if dry_run:
72+
dry_run_log(f"Would detach old client tools channel: {old}")
73+
else:
74+
log(f"Detaching old client tools channel: {old}")
75+
client.contentmanagement.detachSource(key, project_label, 'software', old)
76+
else:
77+
log("No old client tools channels to detach.")
78+
79+
if not new_tools:
80+
log("\n=== Attaching New Client Tools Channel ===")
81+
source_labels = [s.get('channelLabel', '') for s in sources]
82+
base_channel_label = next((lbl for lbl in source_labels if lbl in base_channels), None)
83+
84+
if base_channel_label:
85+
log(f"Base channel determined for project: {base_channel_label}")
86+
children = client.channel.software.listChildren(key, base_channel_label)
87+
managertools_labels = [c['label'] for c in children if c.get('channel_family_label') == 'SLE-M-T']
88+
89+
if managertools_labels:
90+
for label in managertools_labels:
91+
if dry_run:
92+
dry_run_log(f"Would attach new client tools: {label}")
93+
else:
94+
log(f"Attaching new client tools: {label}")
95+
client.contentmanagement.attachSource(key, project_label, 'software', label)
96+
else:
97+
log("No client tools channels found for the matched base channel. Skipping attachment.")
98+
else:
99+
log("Could not determine a base channel for this project. Skipping new tools attachment.")
100+
else:
101+
log("New client tools channel already present in project sources. Skipping attachment.")
102+
103+
log("\n=== Building and Promoting Selected Environments ===")
104+
all_envs = client.contentmanagement.listProjectEnvironments(key, project_label)
105+
106+
if not all_envs:
107+
log("No environments found for this project.")
108+
return
109+
110+
first_env_label = all_envs[0]['label']
111+
112+
for i, env in enumerate(all_envs):
113+
env_label = env['label']
114+
is_first_env = (env_label == first_env_label)
115+
116+
if is_first_env:
117+
description = "Build for new client tools channels."
118+
if dry_run:
119+
dry_run_log(f"Would build initial environment {env_label}")
120+
else:
121+
log(f"Building initial environment (label: {env_label})")
122+
client.contentmanagement.buildProject(key, project_label, description)
123+
if not wait_for_completion(client, key, project_label, env_label):
124+
log("Build failed or timed out. Aborting promotion process.")
125+
return
126+
else:
127+
prev_env_label = env['previousEnvironmentLabel']
128+
if dry_run:
129+
dry_run_log(f"Would promote the environment {prev_env_label} to {env_label}")
130+
else:
131+
log(f"Promoting the environment {prev_env_label} to {env_label}")
132+
client.contentmanagement.promoteProject(key, project_label, prev_env_label)
133+
if not wait_for_completion(client, key, project_label, prev_env_label):
134+
log("Promotion failed or timed out. Aborting promotion process.")
135+
return
136+
137+
if not dry_run and i < len(all_envs) - 1:
138+
log("Waiting 30 seconds before next promotion...")
139+
time.sleep(30)
140+
141+
142+
def wait_for_completion(client, key, project_label, env_label, wait_interval=30):
143+
"""Polls the project environment status until it is 'built' or an error occurs."""
144+
log(f"Waiting for environment '{env_label}' to complete its operation...")
145+
while True:
146+
try:
147+
current_env = client.contentmanagement.lookupEnvironment(key, project_label, env_label)
148+
149+
if not current_env:
150+
log(f"Environment '{env_label}' not found, assuming an issue occurred.")
151+
return False
152+
153+
status = current_env['status']
154+
log(f"Current status for '{env_label}': {status}")
155+
156+
if status == "built":
157+
log(f"Environment '{env_label}' successfully built.")
158+
return True
159+
else:
160+
log(f"Still building environment '{env_label}' with status: {status}.")
161+
time.sleep(wait_interval)
162+
except Exception as e:
163+
print(f"[ERROR] Polling failed: {e}")
164+
return False
165+
166+
# --- Skeleton Functions for other Components ---
167+
168+
def process_activation_keys(client, key, activation_keys, dry_run):
169+
"""Function to process one or more activation keys."""
170+
log("\n=== Processing Activation Keys ===")
171+
172+
# We need a list of all channels to dynamically find the 'managertools' channel
173+
all_channels = client.channel.listSoftwareChannels(key)
174+
175+
for ak_key in activation_keys:
176+
log(f"Processing activation key: {ak_key}")
177+
178+
try:
179+
detail = client.activationkey.getDetails(key, ak_key)
180+
child_channel_labels = detail.get('child_channel_labels', [])
181+
except xmlrpc.client.Fault as e:
182+
log(f"Failed to get details for activation key {ak_key}: {e}. Skipping.")
183+
continue
184+
185+
old_tools = [label for label in child_channel_labels if 'manager-tools' in label.lower()]
186+
187+
channels_to_attach = []
188+
# Find the new 'managertools' channel based on the base channel of the activation key
189+
base_channel_label = detail.get('base_channel_label')
190+
191+
if base_channel_label:
192+
# Find children of the base channel
193+
children = client.channel.software.listChildren(key, base_channel_label)
194+
# Filter for the new client tools channel
195+
new_tools = [c['label'] for c in children if c.get('channel_family_label') == 'SLE-M-T']
196+
197+
# Condition: Only proceed if there are old tools to remove and new tools to add.
198+
if old_tools and new_tools:
199+
channels_to_attach = new_tools
200+
elif old_tools and not new_tools:
201+
log(f"No new client tools channel found for base channel {base_channel_label}. Skipping update for key {ak_key}.")
202+
continue
203+
else:
204+
log(f"No old client tools channels found for key {ak_key}. Skipping update.")
205+
continue
206+
else:
207+
log(f"Could not determine base channel for key {ak_key}. Skipping update.")
208+
continue
209+
210+
if dry_run:
211+
if old_tools:
212+
dry_run_log(f"Would remove the old client tools channels {old_tools} from key {ak_key}")
213+
if channels_to_attach:
214+
dry_run_log(f"Would add new client tools channels {channels_to_attach} to key {ak_key}")
215+
else:
216+
log(f"Updating channels for activation key {ak_key}...")
217+
if old_tools:
218+
log(f"Detaching channels: {old_tools}")
219+
client.activationkey.removeChildChannels(key, ak_key, old_tools)
220+
if channels_to_attach:
221+
log(f"Attaching channels: {channels_to_attach}")
222+
client.activationkey.addChildChannels(key, ak_key, channels_to_attach)
223+
224+
def process_autoinstallation_profiles(client, key, profiles_to_process, dry_run):
225+
"""Skeleton function to process one or more autoinstallation profiles."""
226+
log("\n=== Not implemented yet ===")
227+
228+
def main():
229+
parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description='''
230+
Usage:
231+
script_name.py -c <component> <label> [--no-dry-run]
232+
233+
Specify the component and the label(s) to process.
234+
235+
Components:
236+
- clmprojects: Process CLM projects. Provide 'all' or a project label.
237+
- activationkeys: Process activation keys. Provide 'all' or a key.
238+
- autoinstallprofiles: Process autoinstallation profiles. Provide 'all' or a label.
239+
240+
The script runs in dry-run mode by default.
241+
242+
Examples:
243+
- Process a single CLM project and all its environments:
244+
python3 script_name.py -c clmprojects clm2
245+
246+
- Process all CLM projects:
247+
python3 script_name.py -c clmprojects all
248+
249+
- Process a single activation key:
250+
python3 script_name.py -c activationkeys 1-sles15sp4-x86_64
251+
252+
- Process all autoinstallation profiles with actual changes:
253+
python3 script_name.py -c autoinstallprofiles all --no-dry-run
254+
''')
255+
256+
parser.add_argument("-c", "--component", choices=['clmprojects', 'activationkeys', 'autoinstallprofiles'], required=True, help="The component to process.")
257+
parser.add_argument("labels", nargs='+', help="The label(s) of the component to process, or 'all'.")
258+
parser.add_argument("--no-dry-run", action='store_true', help="Perform actual changes instead of a dry run.")
259+
260+
args = parser.parse_args()
261+
262+
dry_run = not args.no_dry_run
263+
labels_to_process = args.labels[0].split(',') if args.labels[0].lower() != 'all' else ['all']
264+
265+
client, key = connect_and_login()
266+
if not client:
267+
sys.exit(1)
268+
269+
try:
270+
if args.component == 'clmprojects':
271+
if 'all' in labels_to_process:
272+
projects_to_process = [p['label'] for p in client.contentmanagement.listProjects(key)]
273+
else:
274+
projects_to_process = labels_to_process
275+
276+
base_channels = list_and_find_base_channels(client, key)
277+
for project_label in projects_to_process:
278+
if not any(p['label'] == project_label for p in client.contentmanagement.listProjects(key)):
279+
log(f"Project '{project_label}' not found. Skipping.")
280+
continue
281+
process_clm_project(client, key, project_label, base_channels, dry_run)
282+
283+
elif args.component == 'activationkeys':
284+
if 'all' in labels_to_process:
285+
ak_to_process = [k['key'] for k in client.activationkey.listActivationKeys(key)]
286+
else:
287+
ak_to_process = labels_to_process
288+
process_activation_keys(client, key, ak_to_process, dry_run)
289+
290+
elif args.component == 'autoinstallprofiles':
291+
if 'all' in labels_to_process:
292+
profiles_to_process = [p['label'] for p in client.autoinstallation.listProfiles(key)]
293+
else:
294+
profiles_to_process = labels_to_process
295+
process_autoinstallation_profiles(client, key, profiles_to_process, dry_run)
296+
297+
except Exception as e:
298+
print(f"[ERROR] An unexpected error occurred: {e}")
299+
finally:
300+
if key:
301+
client.auth.logout(key)
302+
log("Logged out successfully.")
303+
304+
if __name__ == "__main__":
305+
main()

0 commit comments

Comments
 (0)