1+ import base64
12import json
3+ import logging
24from collections .abc import Iterable
35from dataclasses import dataclass
46
57from databricks .sdk import WorkspaceClient
68from databricks .sdk .errors import NotFound
7- from databricks .sdk .service .compute import ClusterDetails , ClusterSource , Policy
9+ from databricks .sdk .service .compute import (
10+ ClusterDetails ,
11+ ClusterSource ,
12+ InitScriptInfo ,
13+ Policy ,
14+ )
815
916from databricks .labs .ucx .assessment .crawlers import (
1017 _AZURE_SP_CONF_FAILURE_MSG ,
18+ _INIT_SCRIPT_DBFS_PATH ,
1119 INCOMPATIBLE_SPARK_CONFIG_KEYS ,
12- _azure_sp_conf_in_init_scripts ,
1320 _azure_sp_conf_present_check ,
14- _get_init_script_data ,
15- logger ,
1621 spark_version_compatibility ,
1722)
23+ from databricks .labs .ucx .assessment .init_scripts import CheckInitScriptMixin
1824from databricks .labs .ucx .framework .crawlers import CrawlerBase , SqlBackend
1925
26+ logger = logging .getLogger (__name__ )
27+
2028
2129@dataclass
2230class ClusterInfo :
@@ -27,7 +35,7 @@ class ClusterInfo:
2735 creator : str | None = None
2836
2937
30- class ClustersMixin :
38+ class CheckClusterMixin ( CheckInitScriptMixin ) :
3139 _ws : WorkspaceClient
3240
3341 def _safe_get_cluster_policy (self , policy_id : str ) -> Policy | None :
@@ -37,62 +45,77 @@ def _safe_get_cluster_policy(self, policy_id: str) -> Policy | None:
3745 logger .warning (f"The cluster policy was deleted: { policy_id } " )
3846 return None
3947
40- def _check_spark_conf (self , cluster , failures ):
48+ def _check_cluster_policy (self , policy_id : str , source : str ) -> list [str ]:
49+ failures : list [str ] = []
50+ policy = self ._safe_get_cluster_policy (policy_id )
51+ if policy :
52+ if policy .definition :
53+ if _azure_sp_conf_present_check (json .loads (policy .definition )):
54+ failures .append (f"{ _AZURE_SP_CONF_FAILURE_MSG } { source } ." )
55+ if policy .policy_family_definition_overrides :
56+ if _azure_sp_conf_present_check (json .loads (policy .policy_family_definition_overrides )):
57+ failures .append (f"{ _AZURE_SP_CONF_FAILURE_MSG } { source } ." )
58+ return failures
59+
60+ def _get_init_script_data (self , init_script_info : InitScriptInfo ) -> str | None :
61+ if init_script_info .dbfs is not None and init_script_info .dbfs .destination is not None :
62+ if len (init_script_info .dbfs .destination .split (":" )) == _INIT_SCRIPT_DBFS_PATH :
63+ file_api_format_destination = init_script_info .dbfs .destination .split (":" )[1 ]
64+ if file_api_format_destination :
65+ try :
66+ data = self ._ws .dbfs .read (file_api_format_destination ).data
67+ if data is not None :
68+ return base64 .b64decode (data ).decode ("utf-8" )
69+ except NotFound :
70+ return None
71+ if init_script_info .workspace is not None and init_script_info .workspace .destination is not None :
72+ workspace_file_destination = init_script_info .workspace .destination
73+ try :
74+ data = self ._ws .workspace .export (workspace_file_destination ).content
75+ if data is not None :
76+ return base64 .b64decode (data ).decode ("utf-8" )
77+ except NotFound :
78+ return None
79+ return None
80+
81+ def _check_cluster_init_script (self , init_scripts : list [InitScriptInfo ], source : str ) -> list [str ]:
82+ failures : list [str ] = []
83+ for init_script_info in init_scripts :
84+ init_script_data = self ._get_init_script_data (init_script_info )
85+ failures .extend (self .check_init_script (init_script_data , source ))
86+ return failures
87+
88+ def check_spark_conf (self , conf : dict [str , str ], source : str ) -> list [str ]:
89+ failures : list [str ] = []
4190 for k in INCOMPATIBLE_SPARK_CONFIG_KEYS :
42- if k in cluster . spark_conf :
91+ if k in conf :
4392 failures .append (f"unsupported config: { k } " )
44- for value in cluster . spark_conf .values ():
93+ for value in conf .values ():
4594 if "dbfs:/mnt" in value or "/dbfs/mnt" in value :
4695 failures .append (f"using DBFS mount in configuration: { value } " )
4796 # Checking if Azure cluster config is present in spark config
48- if _azure_sp_conf_present_check (cluster .spark_conf ):
49- failures .append (f"{ _AZURE_SP_CONF_FAILURE_MSG } cluster." )
97+ if _azure_sp_conf_present_check (conf ):
98+ failures .append (f"{ _AZURE_SP_CONF_FAILURE_MSG } { source } ." )
99+ return failures
50100
51- def _check_cluster_policy (self , cluster , failures ):
52- policy = self ._safe_get_cluster_policy (cluster .policy_id )
53- if policy :
54- if policy .definition :
55- if _azure_sp_conf_present_check (json .loads (policy .definition )):
56- failures .append (f"{ _AZURE_SP_CONF_FAILURE_MSG } cluster." )
57- if policy .policy_family_definition_overrides :
58- if _azure_sp_conf_present_check (json .loads (policy .policy_family_definition_overrides )):
59- failures .append (f"{ _AZURE_SP_CONF_FAILURE_MSG } cluster." )
101+ def check_cluster_failures (self , cluster : ClusterDetails , source : str ) -> list [str ]:
102+ failures : list [str ] = []
60103
61- def _check_init_scripts (self , cluster , failures ):
62- for init_script_info in cluster .init_scripts :
63- init_script_data = _get_init_script_data (self ._ws , init_script_info )
64- if not init_script_data :
65- continue
66- if not _azure_sp_conf_in_init_scripts (init_script_data ):
67- continue
68- failures .append (f"{ _AZURE_SP_CONF_FAILURE_MSG } cluster." )
69-
70- def _check_cluster_failures (self , cluster : ClusterDetails ):
71- failures = []
72- cluster_info = ClusterInfo (
73- cluster_id = cluster .cluster_id if cluster .cluster_id else "" ,
74- cluster_name = cluster .cluster_name ,
75- creator = cluster .creator_user_name ,
76- success = 1 ,
77- failures = "[]" ,
78- )
79104 support_status = spark_version_compatibility (cluster .spark_version )
80105 if support_status != "supported" :
81106 failures .append (f"not supported DBR: { cluster .spark_version } " )
82107 if cluster .spark_conf is not None :
83- self ._check_spark_conf (cluster , failures )
108+ failures . extend ( self .check_spark_conf (cluster . spark_conf , source ) )
84109 # Checking if Azure cluster config is present in cluster policies
85- if cluster .policy_id :
86- self ._check_cluster_policy (cluster , failures )
87- if cluster .init_scripts :
88- self ._check_init_scripts (cluster , failures )
89- cluster_info .failures = json .dumps (failures )
90- if len (failures ) > 0 :
91- cluster_info .success = 0
92- return cluster_info
110+ if cluster .policy_id is not None :
111+ failures .extend (self ._check_cluster_policy (cluster .policy_id , source ))
112+ if cluster .init_scripts is not None :
113+ failures .extend (self ._check_cluster_init_script (cluster .init_scripts , source ))
114+
115+ return failures
93116
94117
95- class ClustersCrawler (CrawlerBase [ClusterInfo ], ClustersMixin ):
118+ class ClustersCrawler (CrawlerBase [ClusterInfo ], CheckClusterMixin ):
96119 def __init__ (self , ws : WorkspaceClient , sbe : SqlBackend , schema ):
97120 super ().__init__ (sbe , "hive_metastore" , schema , "clusters" , ClusterInfo )
98121 self ._ws = ws
@@ -110,7 +133,18 @@ def _assess_clusters(self, all_clusters):
110133 f"Cluster { cluster .cluster_id } have Unknown creator, it means that the original creator "
111134 f"has been deleted and should be re-created"
112135 )
113- yield self ._check_cluster_failures (cluster )
136+ cluster_info = ClusterInfo (
137+ cluster_id = cluster .cluster_id if cluster .cluster_id else "" ,
138+ cluster_name = cluster .cluster_name ,
139+ creator = cluster .creator_user_name ,
140+ success = 1 ,
141+ failures = "[]" ,
142+ )
143+ failures = self .check_cluster_failures (cluster , "cluster" )
144+ if len (failures ) > 0 :
145+ cluster_info .success = 0
146+ cluster_info .failures = json .dumps (failures )
147+ yield cluster_info
114148
115149 def snapshot (self ) -> Iterable [ClusterInfo ]:
116150 return self ._snapshot (self ._try_fetch , self ._crawl )
0 commit comments