Repository: SUSE-Enceladus/cloud-regionsrv-client
Pull request #244: Handle state inconsistency
Author: login: rjschwei
Created: 2025-04-15T20:08:24Z | Merged: 2025-04-22T13:37:39Z

It is possible that our state file disappears. In this case we need to still handle the state transition between billing flavors correctly. We do not want the user to have to mess withe registration even if our cache disappears.


Diff:
diff --git a/cloud-regionsrv-client.changes b/cloud-regionsrv-client.changes
index 108e827c..477afd1a 100644
--- a/cloud-regionsrv-client.changes
+++ b/cloud-regionsrv-client.changes
@@ -10,7 +10,8 @@ NEXT version
   + Support flexible licenses in GCE (jsc#PCT-531)
   + Drop the azure-addon package it is geting replaced by the
     license-watcher package which has a generic implementation of the
-    same functionality. (bsc#1218345)
+    same functionality.
+  + Handle cache inconsistencies (bsc#1218345)
     
 -------------------------------------------------------------------
 Tue Dec  3 17:21:52 UTC 2024 - Jesús Bermúdez Velázquez <jesus.bv@suse.com>
diff --git a/usr/sbin/cloudguest-lic-watcher b/usr/sbin/cloudguest-lic-watcher
index 8f14c9a7..3f726c43 100755
--- a/usr/sbin/cloudguest-lic-watcher
+++ b/usr/sbin/cloudguest-lic-watcher
@@ -20,12 +20,14 @@ import instance_billing_flavor_check.utils as inst_flvr_utils
 import cloudregister.registerutils as utils
 
 CACHE_LICENSE_PATH = os.path.join(utils.get_state_dir(), 'cached_license')
+SERVICE_NAME = 'guestregister.service'
 
 def update_license_cache(license_type):
     """Update the cache to track the license type"""
     with open(CACHE_LICENSE_PATH, 'w') as cached_license:
         cached_license.write(license_type)
 
+
 def has_license_changed(license_type):
     """Check license type changes against metadata info"""
     if os.path.exists(CACHE_LICENSE_PATH):
@@ -34,29 +36,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
-    if current_flavor == 'PAYG':
-        base_msg = 'Detected flavor change to PAYG, {status}'
+def maybe_drop_registration(license_type):
+    """Clean up registration data if needed"""
+    if license_type == 'BYOS':
+        if not utils.get_current_smt():
+            # There is no target registration server, nothing to do
+            return
+        if not utils.uses_rmt_as_scc_proxy():
+            logging.info(
+                'Detected flavor change to BYOS, clean up registration')
+            utils.clean_all()
+            utils.exec_subprocess(['systemctl', 'disable', SERVICE_NAME])
+
+
+def maybe_register_system(license_type):
+    """Register the system if needed handle the folowing cases
+       - System is not registered at all
+       - System is registered as BYOS to update server
+       - System is registered to SCC
+    """
+    if license_type == 'PAYG':
         current_target = utils.get_current_smt()
+        base_msg = 'Detected flavor change to PAYG, {status}'
+        if current_target and utils.uses_rmt_as_scc_proxy():
+            # The system is registered to the update infrastructure using
+            # a registration code. Now that the system is PAYG we have to
+            # clean up that registration
+            utils.clean_all()
+            logging.info(base_msg.format(
+                status='removed registration to update infra as BYOS')
+            )
+            current_target = None
+        if not current_target and utils.is_scc_connected():
+            # The system is registered to the SUSE Customer center. Now
+            # that the system is PAYG we have to clean up that registration
+            utils.clean_all()
+            logging.info(base_msg.format(
+                status='removed registration to SCC as BYOS')
+            )
+            current_target = None
         if (
                 not current_target or not
                 utils.is_registered(current_target.get_FQDN())
         ):
-                logging.info(base_msg.format(status='registering'))
-                utils.exec_subprocess(['registercloudguest'])
-                utils.exec_subprocess(['systemctl', 'enable', service_name])
+            # The system is not registered to the update infrastructure
+            logging.info(base_msg.format(status='registering'))
+            utils.exec_subprocess(['registercloudguest'])
+            utils.exec_subprocess(['systemctl', 'enable', SERVICE_NAME])
+            return
+        # Everything is as it is expected to be
         logging.info(base_msg.format(
             status='already registered, nothing to do')
         )
+
+utils.start_logging()
+
+current_flavor = inst_flvr_utils.check_payg_byos()[0]
+if has_license_changed(current_flavor):
+    maybe_drop_registration(current_flavor)
+    maybe_register_system(current_flavor)
     update_license_cache(current_flavor)
         
         


Reviews:
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T09:27:48Z
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T09:31:34Z
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T09:32:37Z
user: login: jesusbv
body: `current_flavor` does not seem to exist in that scope (inside `maybe_drop_registration` method), that would produce an error
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T09:35:35Z
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:10:12Z
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:14:42Z
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:16:18Z
user: login: rjschwei
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T11:14:10Z
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T11:44:09Z
user: login: rjschwei
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:05:11Z
user: login: jesusbv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:15:01Z
user: login: jesusbv
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:47:24Z
user: login: schaefi
body: LGTM

I approve only for the state of the code not for functionality as I have no idea how to properly integration test this part
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:35:11Z

Review comments:
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
+def maybe_drop_registration(license_type):
+    """Clean up registration data if needed"""
+    if current_flavor == 'BYOS':
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: `current_flavor` does not exist in this scope ?
created_at: 2025-04-22T09:27:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: those two lines are a bit weird, wouldn't it be better to have one method that decides 
whether to de-register or register depending on the condition than having two methods
"`may drop registration` hold on, `may register`" ?

something like
`update_registration_state(license_type)`
and call either of the two methods inside, depending on condition

also, it would be nice to rename the methods to `drop_registration_if_x` and `register_system_if_x`, for example
created_at: 2025-04-22T09:31:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
+def maybe_drop_registration(license_type):
+    """Clean up registration data if needed"""
+    if current_flavor == 'BYOS':
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: odd this error did not show up in the tests that passed or this change was untested
created_at: 2025-04-22T09:32:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
+def maybe_drop_registration(license_type):
+    """Clean up registration data if needed"""
+    if current_flavor == 'BYOS':
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: looks like `current_flavor` is set before the call in line 97, this still needs changing as the call in line 38 does not set the value

also, if we are setting the value of `current_flavor` outside the method to use it inside, why do both of the methods have a param `license_type` unused inside ?
created_at: 2025-04-22T10:10:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
+def maybe_drop_registration(license_type):
+    """Clean up registration data if needed"""
+    if current_flavor == 'BYOS':
+        if not utils.get_current_smt():
+            # There is no target registration server, nothing to do
+            return
+        if not utils.uses_rmt_as_scc_proxy():
+            logging.info(
+                'Detected flavor change to BYOS, clean up registration')
+            utils.clean_all()
+            utils.exec_subprocess(['systemctl', 'disable', service_name])
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: same for `service_name`, it is set in line 96 but not in the call in line 38
created_at: 2025-04-22T10:14:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
+def maybe_drop_registration(license_type):
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: ```suggestion
def maybe_drop_registration(license_type, service_name='guestregister.service'):
```
created_at: 2025-04-22T10:16:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
path: usr/sbin/cloudguest-lic-watcher
user: login: rjschwei
body: The condition to drop or register is in the function implementation to avoid having to repeat the condition here and and in another part of the code. Since the outcome for the function is not deterministic, `maybe` is the appropriat ename.
created_at: 2025-04-22T11:05:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
+def maybe_drop_registration(license_type):
+    """Clean up registration data if needed"""
+    if current_flavor == 'BYOS':
path: usr/sbin/cloudguest-lic-watcher
user: login: rjschwei
body: The code was tested, unless I made a mistake when creating the package. Anyway, yes good catch this needs fixing. Thanks.
created_at: 2025-04-22T11:09:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
 
 
-utils.start_logging()
-service_name = 'guestregister.service'
-current_flavor = inst_flvr_utils.check_payg_byos()[0]
-if has_license_changed(current_flavor):
-    if current_flavor == 'BYOS' and not utils.uses_rmt_as_scc_proxy():
-        logging.info('Detected flavor change to BYOS, clean up registration')
-        utils.clean_all()
-        utils.exec_subprocess(['systemctl', 'disable', service_name])
+def maybe_drop_registration(license_type):
+    """Clean up registration data if needed"""
+    if current_flavor == 'BYOS':
+        if not utils.get_current_smt():
+            # There is no target registration server, nothing to do
+            return
+        if not utils.uses_rmt_as_scc_proxy():
+            logging.info(
+                'Detected flavor change to BYOS, clean up registration')
+            utils.clean_all()
+            utils.exec_subprocess(['systemctl', 'disable', service_name])
path: usr/sbin/cloudguest-lic-watcher
user: login: rjschwei
body: Created a constant
created_at: 2025-04-22T11:14:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: can we get them, at least, in `update_registration_state` method ?
created_at: 2025-04-22T11:44:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
path: usr/sbin/cloudguest-lic-watcher
user: login: rjschwei
body: A function to call 2 other functions and do nothing else? A bit of an overkill to me. If it were a whole process yes, but for 2 I don't see the point.
created_at: 2025-04-22T12:05:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,29 +35,69 @@ def has_license_changed(license_type):
             return license_type != old_license
     else:
         update_license_cache(license_type)
+        maybe_drop_registration(license_type)
+        maybe_register_system(license_type)
path: usr/sbin/cloudguest-lic-watcher
user: login: jesusbv
body: https://github.com/SUSE-Enceladus/cloud-regionsrv-client/blob/master/lib/cloudregister/registerutils.py#L1097

https://github.com/SUSE-Enceladus/cloud-regionsrv-client/blob/master/lib/cloudregister/registerutils.py#L2178

we use that pattern with no issue before, not sure why we can't use the same now
created_at: 2025-04-22T12:15:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: rjschwei
created_at: 2025-04-15T20:39:21Z
body: Test cases completed:
  - PAYG instance installed lic-watcher (no cache) present, this represents
    initial boot use case as well, run cloudguest-lic-watcher
    + Cache created
      ~ # cat /var/cache/cloudregister/cached_license
          PAYG
    + No changes to repos
      ~ zypper lr
        1 | Basesystem_Module_x86_64:
        .....
    + Result: Pass
  - Stop PAYG instance switch to BYOS, run cloudguest-lic-watcher
    + Cache updated
      ~ # cat /var/cache/cloudregister/cached_license
          BYOS
    + Repos removed
      ~ zypper lr
        1 | google-cloud
    + Result: Pass
  - Remove cache file, register with SCC, stop instance switch to PAYG, run cloudguest-lic-watcher
    + rm /var/cache/cloudregister/cached_license
    + SUSEConnect -r XXXXXXXXX
      Announcing system to https://scc.suse.com ...

      Activating SLES 15.6 x86_64 ...
      -> Adding service to system ...

    + zypper lr
      1 | Basesystem_Module_15_SP6_x86_64
      .....
    + Cache created
      ~ # cat /var/cache/cloudregister/cached_license
          PAYG
    + Repos switched to update infra
      ~ zypper lr
        1 | Basesystem_Module_x86_64:
      ~ grep susecloud 
        # grep susecloud /etc/zypp/services.d/*
/etc/zypp/services.d/Basesystem_Module_x86_64.service:url = plugin:/susecloud?c
    + Result: Pass
  - Remove cache file, stop instance switch to BYOS, run cloudguest-lic-watcher
    + rm /var/cache/cloudregister/cached_license
    + Cache created
      ~ # cat /var/cache/cloudregister/cached_license
          BYOS
    + Repos removed
      ~ zypper lr
        1 | google-cloud
    + Result: Pass
  - Stop instance switch to PAYG, run cloudguest-lic-watcher
    + Cache updated
      ~ # cat /var/cache/cloudregister/cached_license
          PAYG
    + Registered instance to the update infrastructure
      ~ zypper lr
        1 | Basesystem_Module_x86_64:
        .....
      ~ grep susecloud 
        # grep susecloud /etc/zypp/services.d/*
    + Result: Pass

author_association: CONTRIBUTOR
user: login: rjschwei
created_at: 2025-04-22T12:03:27Z
body: Re-ran the tests from the original sequence. All worked as expected.
author_association: CONTRIBUTOR
user: login: rjschwei
created_at: 2025-04-22T13:33:04Z
body: Also ran the tests on SLE 12 for sanity checking, all passed.
author_association: CONTRIBUTOR