From dfc43a436de45394d2481e1d45159a9783ce14ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Gounon?= <aurelien.gounon@cern.ch>
Date: Fri, 14 Jan 2022 16:13:00 +0100
Subject: [PATCH] improve cta-versionlock script

---
 .gitignore                  |   1 +
 ReleaseNotes.md             |   1 +
 cta-release/CMakeLists.txt  |  23 +++-
 cta-release/cta-versionlock | 241 +++++++++++++++++++++++-------------
 cta.spec.in                 |   6 +-
 5 files changed, 182 insertions(+), 90 deletions(-)

diff --git a/.gitignore b/.gitignore
index 4aad91b50c..d6134c3cef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ nbproject/
 workbench.xmi
 .vscode/
 cta-release/RPM-GPG-KEY-*
+cta-release/versionlock.cta
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index afb8878b05..ab7872b300 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -5,6 +5,7 @@
 ## Upgrade Instructions
 
 ## Features
+- Improve cta-versionlock script
 - cta/CTA#1091 - New Drive State table in CTA Catalogue
 - cta/CTA#1054 - Fix filing of disk buffer when recalling from tapeservers with RAO
 - cta/CTA#1076 - Retrieve fails if disk system configuration is removed
diff --git a/cta-release/CMakeLists.txt b/cta-release/CMakeLists.txt
index caf27a197a..dd5ebc59a3 100644
--- a/cta-release/CMakeLists.txt
+++ b/cta-release/CMakeLists.txt
@@ -55,6 +55,24 @@ safedl("https://yum.oracle.com/RPM-GPG-KEY-oracle-ol${OSV}"
   "${CMAKE_CURRENT_SOURCE_DIR}/RPM-GPG-KEY-oracle"
 )
 
+# generate versionlock file
+execute_process(
+    COMMAND grep "%package" ../cta.spec.in
+    COMMAND awk "{print $3}"
+    COMMAND grep -v "cta-release"
+    OUTPUT_VARIABLE RESULT OUTPUT_STRIP_TRAILING_WHITESPACE
+    )
+
+string(REPLACE "\n" ";" RESULTS ${RESULT})
+
+foreach(PNAME ${RESULTS})
+    string(APPEND CTAVERSIONLOCK "0:${PNAME}-${CTA_VERSION}-${CTA_RELEASE}${RPMTools_RPMBUILD_DIST}.*\n")
+endforeach()
+
+file(READ ../continuousintegration/docker/ctafrontend/cc7/etc/yum/pluginconf.d/versionlock.list VERSIONLOCK)
+string(APPEND CTAVERSIONLOCK "${VERSIONLOCK}")
+file(GENERATE OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/versionlock.cta CONTENT "${CTAVERSIONLOCK}")
+
 # Repos files
 file (GLOB REPO_FILES
   "${CMAKE_CURRENT_SOURCE_DIR}/*.repo"
@@ -70,8 +88,7 @@ install (FILES ${REPO_FILES}
   DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/yum.repos.d)
 install (FILES ${KEY_FILES}
   DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/pki/rpm-gpg)
-install (FILES ../continuousintegration/docker/ctafrontend/cc7/etc/yum/pluginconf.d/versionlock.list
-  DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/yum/pluginconf.d
-  RENAME versionlock.cta)
+install (FILES ${CMAKE_CURRENT_SOURCE_DIR}/versionlock.cta
+  DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/yum/pluginconf.d)
 install (FILES ${CMAKE_CURRENT_SOURCE_DIR}/cta-versionlock
   DESTINATION usr/${CMAKE_INSTALL_BINDIR})
diff --git a/cta-release/cta-versionlock b/cta-release/cta-versionlock
index bfa561569e..b2bf348dac 100755
--- a/cta-release/cta-versionlock
+++ b/cta-release/cta-versionlock
@@ -3,6 +3,8 @@
 import os.path
 import sys
 import re
+import json
+import rpm
 from rpmUtils.miscutils import splitFilename
 from collections import defaultdict
 
@@ -11,10 +13,10 @@ vfiles = {
   'cta': '/etc/yum/pluginconf.d/versionlock.cta', 
   'yum': '/etc/yum/pluginconf.d/versionlock.list'
 }
-versions = defaultdict(dict)
-summary = defaultdict(list)
-actions = ('check', 'apply', 'forceupdate', 'clear')
+actions = ('help', 'check', 'apply', 'forceupdate', 'checkpkg', 'remove')
 retcode = 0
+jsonoutput = False
+no_vlock = '/etc/cta.novlock'
 
 try:
   FileNotFoundError
@@ -25,31 +27,71 @@ except NameError:
 def usage():
   print("\n%s: command line tool to manage cta packages versionlock\n \
 \n \
-usage: %s check|apply|forceupdate|clear\n \
-  check: show cta versionlock status\n \
-  apply: add cta versions to versionlock file\n \
-  forceupdate: add cta versions to versionlock file and overwrite already defined versions\n \
-  clear: remove all cta versions from versionlock file\n \
-" % (sys.argv[0], sys.argv[0]))
+usage: %s help|check|apply|forceupdate|checkpkg|remove [--json]\n \
+  help: print this message\n \
+  check: show consistency of versionlock.list with cta versionlock requirements\n \
+  apply: add cta versions to versionlock.list\n \
+  forceupdate: add cta versions to versionlock.list and overwrite already defined packages\n \
+  checkpkg: check consistency between installed packages and cta versionlock requirements\n \
+  remove: remove all cta packages and dependencies from versionlock.list\n \
+  [--json]: format output in json\n \
+  \n \
+  to prevent cta-versionlock to alter versionlock.list create a %s file.\n \
+  " % (sys.argv[0], sys.argv[0], no_vlock))
   exit(1)
 
+def _exit():
+  printer(message)
+  exit(retcode)
 
-# Compare versions in both version lists
-def matchPkg(pkglist):
+# output formatting
+def printer(msg):
+  if jsonoutput:
+    print(json.dumps(msg))
+  else:
+    print("\n%s" % msg.pop('title'))
+    for status in ['ok', 'wrong_version', 'missing', 'adding', 'updating', 'removing']:
+      if msg[status]:
+        print("\n==> %s: %s/%s" % (status, msg[status]['count'], len(versions['cta'])))
+        if (status == 'wrong_version' or status == 'updating') and msg[status]['count'] > 0:
+          print("\n".join(['%s: %s' % (key, value) for (key, value) in msg[status]['content'].items()]))
+        else:
+          print("\n".join(msg[status]['content']))
+    print("\n")
+
+# read versionlock files
+def readVer(filelist):
+  versions = defaultdict(dict)
+  for fname, vfile in filelist.items():
+    if not os.path.isfile(vfile):
+      raise FileNotFoundError("file %s not found" % vfile)
+
+    with open(vfile) as f:
+      plist = f.read().splitlines()
+      for p in plist:
+        if p == "" or p.startswith('#'):
+          continue
+        (n, v, r, e, a) = splitFilename(p)
+        versions[fname][n] = [e, v, r, a]
+  return versions
+
+# Compare versions in both versionlock lists
+def checkVfile(pkglist):
+  result = defaultdict(dict)
   for p, version in pkglist.items():
     try: 
       versions['yum'][p]
     except:
-      summary['missing'].append(p)
+      result.setdefault('missing', []).append(p)
       continue
     else:
       if versions['yum'][p][:-1] == version[:-1]:
-        summary['present'].append(p)
+        result.setdefault('ok',[]).append(p)
       else:
-        summary['wrong_version'].append(p)
-  
+        result['wrong_version'][p]= {'current': ("%s:%s-%s" % tuple(versions['yum'][p][:-1])), 'required': ("%s:%s-%s" % tuple(version[:-1]))}
+  return result
 
-# add cta versions to yum versionlock file
+# add CTA packages to versionlock.list
 def addtoVfile(pkglist):
   with open(vfiles['yum'], 'a' ) as f:
     for p in pkglist:
@@ -57,8 +99,7 @@ def addtoVfile(pkglist):
       package = ("%s:%s-%s-%s.%s" % (e, p, v, r, a))
       f.write(package + '\n')
 
-
-# force update existing versions in yum versionlock with cta one
+# update existing packages in versionlock.list with CTA versions required
 def updateVfile(pkglist):
   with open(vfiles['yum'], 'r+') as f:
     content = f.read()
@@ -73,9 +114,23 @@ def updateVfile(pkglist):
     f.write(content)
     f.truncate()
 
+# check installed packages version
+def checkPkg(pkglist):
+  result = defaultdict(dict)
+  ts = rpm.TransactionSet()
+  for pname, verlist in pkglist.items():
+    mi = ts.dbMatch('name', pname)
+    for item in mi:
+      ie = str("0" if item['epoch'] is None else item['epoch'])
+      (e, v, r, a) = verlist
+      if (e, v, r) == (ie, item['version'], item['release']):
+        result['ok'][pname] = ("%s:%s-%s" % (e, v, r))
+      else:
+        result['wrong_version'][pname] = {'required': ("%s:%s-%s" % (e, v, r)), 'installed': ("%s:%s-%s" % (ie, item['version'], item['release']))}
+  return result
 
-# clear cta versions from yum versionlock
-def clearPkgs(pkglist):
+# remove CTA packages from versionlock.list
+def clearVfile(pkglist):
   with open(vfiles['yum'], 'r+') as f:
     content = f.read()
     for p in pkglist:
@@ -89,82 +144,100 @@ def clearPkgs(pkglist):
 
 
 # check arguments
-if len(sys.argv) != 2:
-  usage()
-
-action = sys.argv[1]
-if action not in actions:
-  print("Error: option %s is not valid" % sys.argv[1])
+if not 2 <= len(sys.argv) <= 3:
   usage()
 
+for arg in sys.argv[1:]:
+  if arg == '--json':
+    jsonoutput = True
+  elif arg in actions:
+    action = arg
+  else:
+    print("Error: option %s is not valid" % sys.argv[1])
+    usage()
 
-# read version files
-for fname, vfile in vfiles.items():
-  if not os.path.isfile(vfile):
-    raise FileNotFoundError("file %s not found" % vfile)
-
-  with open(vfile) as f:
-    plist = f.read().splitlines()
-    for p in plist:
-      if p == "" or p.startswith('#'):
-        continue
-      (n, v, r, e, a) = splitFilename(p)
-      versions[fname][n] = [e, v, r, a]
-
-
-# check if packages versions exist in yum versionlock file (ignore arch)
-matchPkg(versions['cta'])
+# check if CTA packages exist in versionlock.list (ignore arch)
+versions = readVer(vfiles)
+versionlock = checkVfile(versions['cta'])
+message = defaultdict(dict)
 
+if (action == 'help'):
+  usage()
 
-# return summary
+# return versionlock summary
 if (action == 'check'):
-  for status, content in summary.items():
-    print("\n=> %s: (%s/%s)" % (status, len(content), len(versions['cta'])))
-    print("\n".join(content))
-  if ("missing" in summary or "wrong_version" in summary):
+  message['title'] = "Yum versionlock status for CTA packages and dependencies"
+  for status, content in versionlock.items():
+    message[status]['count'] = len(content)
+    message[status]['content'] = content
+  if versionlock['missing'] or versionlock['wrong_version']:
     retcode = 2
 
-
-# add cta packages to versionlock file
+# add CTA packages to versionlock.list
 elif (action == 'apply'):
-  if "missing" in summary:
-    print("\nAdding %s packages to version lock file:" % len(summary['missing']))
-    print("\n".join(summary['missing']))
-    addtoVfile(summary['missing'])
+  message['title'] = "Adding CTA packages and dependencies to versionlock.list"
+  if os.path.isfile(no_vlock):
+    message['adding']['count'] = 0
+    message['adding']['content'] = [ "cta_novlock file present, doing nothing" ]
+    retcode = 1
+    _exit()
+  if versionlock['missing']:
+    message['adding']['count'] = len(versionlock['missing'])
+    message['adding']['content'] = versionlock['missing']
+    addtoVfile(versionlock['missing'])
   else:
-    print("\nNothing to do")
-
-  if "wrong_version" in summary:
-    print("\nWARNING: the following packages have a different version specified in versionlock file:")
-    print('\n'.join(summary['wrong_version']))
-    print("\nThey will not be changed unless you run %s with the 'forceupdate' option" % sys.argv[0])
+    message['adding']['count'] = 0
+    message['adding']['content'] = [ "Nothing to do" ]
+  if versionlock['wrong_version']:
+    message['title'] += "\nWARNING: some packages have a different version specified in versionlock.list than required by CTA, it will not be changed unless you use the 'forceupdate' option"
+    message['wrong_version']['count'] = len(versionlock['wrong_version'])
+    message['wrong_version']['content'] = versionlock['wrong_version']
     retcode = 2
 
-
-# overwrite existing versions in versionlock file
+# add CTA packages and overwrite existing versions in versionlock.list
 elif (action == 'forceupdate'):
-  if "wrong_version" in summary:
-    print("\nUpdating %s packages version in versionlock file:" % len(summary['wrong_version']))
-    for p in summary['wrong_version']:
-      print("%s: previous %s, new %s" % (p, ':'.join(versions['yum'][p]), ':'.join(versions['cta'][p])))
-    updateVfile(summary['wrong_version'])
-  if "missing" in summary:
-    print("\nAdding %s packages to version lock file:" % len(summary['missing']))
-    print("\n".join(summary['missing']))
-    addtoVfile(summary['missing'])
-  if (not summary['missing'] and not summary['wrong_version']):
-    print("\nNothing to do")
-
-
-# remove cta versions from versionlock file
-elif (action == 'clear'):
-  if versions['cta']:
-    clearPkgs(versions['cta'])
-    print("\nRemoving %s packages from versionlock file:" % len(versions['cta']))
-    print("\n".join(versions['cta']))
-  else:
-    print("\nNothing to do")
+  message['title'] = "Adding and updating CTA packages and dependencies in versionlock.list"
+  if os.path.isfile(no_vlock):
+    message['updating']['count'] = 0
+    message['updating']['content'] = [ "cta_novlock file present, doing nothing" ]
+    retcode = 1
+    _exit()
+  if versionlock['wrong_version']:
+    message['updating']['count'] = len(versionlock['wrong_version'])
+    message['updating']['content'] = versionlock['wrong_version']
+    updateVfile(versionlock['wrong_version'])
+  if versionlock['missing']:
+    message['adding']['count'] = len(versionlock['missing'])
+    message['adding']['content'] = versionlock['missing']
+    addtoVfile(versionlock['missing'])
+  if (not versionlock['missing'] and not versionlock['wrong_version']):
+    message['updating']['count'] = 0
+    message['updating']['content'] = [ "Nothing to do" ]
+
+# check version of currently installed packages
+elif (action == 'checkpkg'):
+  message['title'] = "CTA packages and dependencies versions currently installed"
+  packages = checkPkg(versions['cta'])
+  for status, content in packages.items():
+    message[status]['count'] = len(content)
+    message[status]['content'] = content
+  if packages['wrong_version']:
+    retcode = 2
 
+# remove CTA related packages from versionlock.list
+elif (action == 'remove'):
+  message['title'] = "Removing CTA packages and dependencies from versionlock.list"
+  if os.path.isfile(no_vlock):
+    message['removing']['count'] = 0
+    message['removing']['content'] = [ "cta_novlock file present, doing nothing" ]
+    retcode = 1
+    _exit()
+  if versionlock['ok']:
+    message['removing']['count'] = len(versionlock['ok'])
+    message['removing']['content'] = versionlock['ok']
+    clearVfile(versionlock['ok'])
+  else:
+    message['removing']['count'] = 0
+    message['removing']['content'] = [ "Nothing to do" ]
 
-print('\n')
-exit(retcode)
+_exit()
diff --git a/cta.spec.in b/cta.spec.in
index cb5a00d8a3..e7f5f7ef08 100644
--- a/cta.spec.in
+++ b/cta.spec.in
@@ -508,11 +508,11 @@ Currently contains a helper for the client-ar script, which should be installed
 %attr(0755,root,root) /usr/bin/cta-client-ar-abortPrepare
 
 %package -n cta-release
-Summary: Repository configuration for CTA dependencies
+Summary: Repository configuration for CTA and its dependencies
 Group: Application/CTA
 Requires: yum-plugin-versionlock
 %description -n cta-release
-Repository configuration for CTA dependencies
+Repository configuration for CTA and its dependencies
 This package contains .repo files, gpg keys and yum-versionlock configuration for CTA
 %files -n cta-release
 %defattr(-,root,root)
@@ -525,7 +525,7 @@ This package contains .repo files, gpg keys and yum-versionlock configuration fo
 /usr/bin/cta-versionlock apply
 
 %preun -n cta-release
-/usr/bin/cta-versionlock clear
+/usr/bin/cta-versionlock remove
 
 
 %changelog
-- 
GitLab