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 }  )
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 } { env_label }  )
130+             else :
131+                 log (f"Promoting the environment { prev_env_label } { 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 }  )
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 }  )
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 }  )
158+                 return  True 
159+             else :
160+                 log (f"Still building environment '{ env_label } { 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 }  )
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 } { ak_key }  )
202+                  continue 
203+             else :
204+                  log (f"No old client tools channels found for key { ak_key }  )
205+                  continue 
206+         else :
207+             log (f"Could not determine base channel for key { ak_key }  )
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 } { ak_key }  )
213+             if  channels_to_attach :
214+                 dry_run_log (f"Would add new client tools channels { channels_to_attach } { 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 }  )
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