# Copyright (C) 2005-2025 Splunk Inc. All Rights Reserved. import json from ITOA.setup_logging import getLogger import splunk.rest as rest import http.client from .splunk_license_suitification_state_machine import SplunkLicenseSuitificationStateMachine class SplunkLicenseStateMaintainer(object): """ This class analyzes internal license signals, and applies detected transitions to the state machine. """ # ****************** WARNING: these hashes are used elsewhere in the code ******************* # These license hashes are also defined in /apps/SA-ITSI-Licensechecker/lib/itsi_internal_licenses.py # and should be kept in sync. PLUS_LICENSE_SIGNAL_HASHES = ["00281640570D92DDCECA2D4BA904476503C8AF47D30831138C59A326C6B4B62A", "FB79890C9C2463A31A620E3329A302BE264C8418B0681C9DE403E919F7D598A9"] EXPIRE_LICENSE_SIGNAL_HASHES = ["DC49789D8AAEAC933C29458918661405660AF88A2236B256B9012BE20F10A428", "247B4903C2EECFC2AE04A93EAD2085BB183AED6BFF851E4B72D0C38959A81252"] # ***************************************************************************************** LICENSE_ENDPOINT_LOCAL = '/services/licenser/localslave' LICENSE_ENDPOINT_LOCAL_PEER = '/services/licenser/localpeer' LICENSE_ENDPOINT = '/services/licenser/licenses' # ****************** WARNING: these GUIDs are used elsewhere in the code ******************* # These license GUIDs are also defined in /apps/SA-ITSI-Licensechecker/lib/itsi_internal_licenses.py # and should be kept in sync. ITSI_INTERNAL_LICENSE_GUIDS = [ '37589432-3563-4467-98D6-79D71CBF1801', # old_itsi_internal_ea_license '2AEECCCF-EDBC-499E-862C-8C79844114D4', # itsi_internal_license 'B05DBFD6-D8A0-4DA4-B238-B981EA553954', # plus_suite_signaling_license '784417C4-631B-4DE2-80AD-9987859BB023', # license_expiration_signaling_license 'D3C8E133-5424-4127-8156-AD3623789BB0', # itsi_internal_license_devtest '6B95FFBB-EBB4-4A1C-9EFC-B483487875F9', # plus_suite_signaling_license_devtest 'E6CF109F-E521-4D07-BEC3-99329B3FD047', # license_expiration_signaling_license_devtest ] # ***************************************************************************************** # ****************** WARNING: the FUTURE_LICENSE_STATUS variable is used elsewhere in the code ******************* # This variable is also defined in /apps/SA-ITSI-Licensechecker/lib/license.py # and should be kept in sync. FUTURE_LICENSE_STATUS = 'FROM_THE_FUTURE' def __init__(self, session_key): self.logger = getLogger(logger_name="itsi.feature_flagging.SplunkLicenseStateMaintainer") self.session_key = session_key def maintain(self): self._validate_itsi_licenses_and_markers() def _get_license_hashes(self): try: try: response, contents = rest.simpleRequest( path=self.LICENSE_ENDPOINT_LOCAL_PEER, getargs={'output_mode': 'json'}, sessionKey=self.session_key) except Exception as e: self.logger.info( "Failed to get license info from {}, reason: {}.\ Now trying {}".format(self.LICENSE_ENDPOINT_LOCAL_PEER, e, self.LICENSE_ENDPOINT_LOCAL)) response, contents = rest.simpleRequest( path=self.LICENSE_ENDPOINT_LOCAL, getargs={'output_mode': 'json'}, sessionKey=self.session_key) if response.status != http.client.OK: e = Exception('Failed to get local license information. Response={} Contents={}'.format( response, contents)) self.logger.exception(e) raise e license_hashes = [] for entry in json.loads(contents).get('entry', None): content = entry.get('content', None) if content is None: continue hashes = content.get('license_keys') if hashes is not None: for hash in hashes: license_hashes.append(hash) return set(license_hashes) except Exception as e: e = Exception('Failed to retrieve the license information.') self.logger.exception(e) raise e def _check_for_user_installed_itsi_license(self): """ Evaluates the presence of a user installed ITSI license. Standalone Deployments: Returns True if found, else returns False. Distributed Deployments: Always returns False """ response, contents = rest.simpleRequest( path=self.LICENSE_ENDPOINT, getargs={'output_mode': 'json', 'count': 0, 'search': '*itsi*'}, sessionKey=self.session_key) if response.status != http.client.OK: e = Exception('Failed to get license information from {}. Response={} Contents={}'.format( self.LICENSE_ENDPOINT, response, contents)) self.logger.exception(e) raise e for entry in json.loads(contents).get('entry', None): content = entry.get('content', None) if content is None: continue add_ons = content.get('add_ons') status = content.get('status') if add_ons is None: continue if status == self.FUTURE_LICENSE_STATUS: self.logger.info('Ignoring Future ITSI licenses.') continue if 'itsi' in add_ons.keys(): self.logger.info('ITSI license detected.') guid = content.get('guid') if guid in self.ITSI_INTERNAL_LICENSE_GUIDS: self.logger.info('ITSI license is an internal license.') continue self.logger.info('User installed ITSI license was detected.') return True self.logger.info('User installed ITSI license was not detected.') return False def _validate_itsi_licenses_and_markers(self): sm = SplunkLicenseSuitificationStateMachine.getInstance(self.session_key) license_hashes = self._get_license_hashes() plus_is_signaled = any(lic for lic in self.PLUS_LICENSE_SIGNAL_HASHES if lic in license_hashes) expired_is_signaled = any(lic for lic in self.EXPIRE_LICENSE_SIGNAL_HASHES if lic in license_hashes) # ITSI-15617: Handle plus marker propagation issues on standalone by checking with LICENSE_ENDPOINT itsi_license_installed = False if not plus_is_signaled: itsi_license_installed = self._check_for_user_installed_itsi_license() self.logger.info("Transitioning state machine...") # Transitioning state machine if expired_is_signaled: self.logger.info("Expired license is signaled") if sm.current_state is not sm.expired: self.logger.info("Performing expiration") sm.expire() elif plus_is_signaled: self.logger.info("Plus license is signaled") if sm.current_state is not sm.plus: self.logger.info("Performing upgrade") sm.upgrade() elif itsi_license_installed: self.logger.info("ITSI License detected, Plus marker pending") if sm.current_state is not sm.plus: self.logger.info("Performing upgrade") sm.upgrade() else: self.logger.info("None of licenses are signaled") if sm.current_state is not sm.standard: self.logger.info("Performing downgrade") sm.downgrade() # give the change for current state machine state to perform its periodic update. sm.current_state.update(sm) self.logger.info("Transitioning state machine completed")