57
57
from .constants import CFG_CLOUD_PROVIDER
58
58
from .constants import CFG_CP_GCP_PROJECT , CFG_CP_GCP_REGION , CFG_CP_GCP_ZONE
59
59
from .constants import CFG_CP_GCP_NETWORK , CFG_CP_GCP_SUBNETWORK
60
+ from .constants import CFG_CP_GCP_GKE_VERSION
60
61
from .constants import CFG_CP_AWS_REGION , CFG_CP_AWS_VPC , CFG_CP_AWS_SUBNET
61
62
from .constants import CFG_CP_AWS_JOB_ROLE , CFG_CP_AWS_BATCH_SERVICE_ROLE
62
63
from .constants import CFG_CP_AWS_INSTANCE_ROLE , CFG_CP_AWS_SPOT_FLEET_ROLE
82
83
from .constants import ELB_DFLT_AWS_NUM_CPUS , ELB_DFLT_GCP_NUM_CPUS
83
84
from .constants import ELB_S3_PREFIX , ELB_GCS_PREFIX , ELB_UNKNOWN_MAX_NUMBER_OF_CONCURRENT_JOBS
84
85
from .constants import AWS_ROLE_PREFIX , CFG_CP_AWS_AUTO_SHUTDOWN_ROLE
85
- from .constants import BLASTDB_ERROR , ELB_UNKNOWN
86
+ from .constants import BLASTDB_ERROR , ELB_UNKNOWN , ELB_JANITOR_SCHEDULE
87
+ from .constants import ELB_DFLT_GCP_REGION , ELB_DFLT_GCP_ZONE
88
+ from .constants import ELB_DFLT_AWS_REGION , ELB_UNKNOWN_GCP_PROJECT
86
89
from .util import validate_gcp_string , check_aws_region_for_invalid_characters
87
90
from .util import validate_gke_cluster_name , ElbSupportedPrograms
88
91
from .util import get_query_batch_size
@@ -210,12 +213,15 @@ class CloudProviderBaseConfig:
210
213
@dataclass
211
214
class GCPConfig (CloudProviderBaseConfig , ConfigParserToDataclassMapper ):
212
215
"""GCP config for ElasticBLAST"""
213
- project : GCPString
214
- region : GCPString
215
- zone : GCPString
216
+ region : GCPString = GCPString ( ELB_DFLT_GCP_REGION )
217
+ project : GCPString = GCPString ( ELB_UNKNOWN_GCP_PROJECT )
218
+ zone : GCPString = GCPString ( ELB_DFLT_GCP_ZONE )
216
219
network : Optional [str ] = None
217
220
subnet : Optional [str ] = None
218
221
user : Optional [str ] = None
222
+ # FIXME: This is a temporary fix for EB-1530. gke_version should be set to
223
+ # None once the proper fix is implemented.
224
+ gke_version : Optional [str ] = '1.21'
219
225
220
226
# mapping to class attributes to ConfigParser parameters so that objects
221
227
# can be initialized from ConfigParser objects
@@ -225,7 +231,8 @@ class GCPConfig(CloudProviderBaseConfig, ConfigParserToDataclassMapper):
225
231
'cloud' : None ,
226
232
'user' : None ,
227
233
'network' : ParamInfo (CFG_CLOUD_PROVIDER , CFG_CP_GCP_NETWORK ),
228
- 'subnet' : ParamInfo (CFG_CLOUD_PROVIDER , CFG_CP_GCP_SUBNETWORK )}
234
+ 'subnet' : ParamInfo (CFG_CLOUD_PROVIDER , CFG_CP_GCP_SUBNETWORK ),
235
+ 'gke_version' : ParamInfo (CFG_CLOUD_PROVIDER , CFG_CP_GCP_GKE_VERSION )}
229
236
230
237
def __post_init__ (self ):
231
238
self .cloud = CSP .GCP
@@ -235,6 +242,13 @@ def __post_init__(self):
235
242
if p .stdout :
236
243
self .user = p .stdout .decode ('utf-8' ).rstrip ()
237
244
245
+ if self .project == ELB_UNKNOWN_GCP_PROJECT :
246
+ proj = get_gcp_project ()
247
+ if not proj :
248
+ raise ValueError (f'GCP project is unset, please invoke gcloud config set project REPLACE_WITH_YOUR_PROJECT_NAME_HERE' )
249
+ else :
250
+ self .project = GCPString (proj )
251
+
238
252
def validate (self , errors : List [str ], task : ElbCommand ):
239
253
"""Validate config"""
240
254
if bool (self .network ) != bool (self .subnet ):
@@ -244,7 +258,7 @@ def validate(self, errors: List[str], task: ElbCommand):
244
258
@dataclass
245
259
class AWSConfig (CloudProviderBaseConfig , ConfigParserToDataclassMapper ):
246
260
"""AWS config for ElasticBLAST"""
247
- region : AWSRegion
261
+ region : AWSRegion = AWSRegion ( ELB_DFLT_AWS_REGION )
248
262
vpc : Optional [str ] = None
249
263
subnet : Optional [str ] = None
250
264
security_group : Optional [str ] = None
@@ -589,7 +603,8 @@ def __init__(self, *args, **kwargs):
589
603
# post-init activities
590
604
591
605
try :
592
- self .cloud_provider .region .validate (dry_run )
606
+ if self .cloud_provider .region :
607
+ self .cloud_provider .region .validate (dry_run )
593
608
except ValueError as err :
594
609
raise UserReportError (returncode = INPUT_ERROR , message = str (err ))
595
610
@@ -713,7 +728,17 @@ def _init_from_ConfigParser(self, cfg: configparser.ConfigParser,
713
728
714
729
self ._validate_config_parser (cfg )
715
730
_validate_csp (cfg )
731
+ self .cluster = ClusterConfig .create_from_cfg (cfg )
732
+
733
+ # determine cloud provider, first by user config, then results bucket
716
734
if sum ([i .startswith ('aws' ) for i in cfg [CFG_CLOUD_PROVIDER ]]) > 0 :
735
+ cloud = CSP .AWS
736
+ elif sum ([i .startswith ('gcp' ) for i in cfg [CFG_CLOUD_PROVIDER ]]) > 0 :
737
+ cloud = CSP .GCP
738
+ else :
739
+ cloud = self .cluster .results .get_cloud_provider ()
740
+
741
+ if cloud == CSP .AWS :
717
742
self .cloud_provider = AWSConfig .create_from_cfg (cfg )
718
743
# for mypy
719
744
self .aws = cast (AWSConfig , self .cloud_provider )
@@ -722,8 +747,6 @@ def _init_from_ConfigParser(self, cfg: configparser.ConfigParser,
722
747
# for mypy
723
748
self .gcp = cast (GCPConfig , self .cloud_provider )
724
749
725
- self .cluster = ClusterConfig .create_from_cfg (cfg )
726
-
727
750
if task == ElbCommand .SUBMIT :
728
751
self .blast = BlastConfig .create_from_cfg (cfg )
729
752
@@ -853,6 +876,13 @@ def validate(self, task: ElbCommand = ElbCommand.SUBMIT, dry_run=False):
853
876
if instance_props .memory - SYSTEM_MEMORY_RESERVE < bytes_to_cache_gb :
854
877
errors .append (f'BLAST database { self .blast .db } memory requirements exceed memory available on selected machine type "{ self .cluster .machine_type } ". Please select machine type with at least { bytes_to_cache_gb + SYSTEM_MEMORY_RESERVE } GB available memory.' )
855
878
879
+ # validate janitor schedule if provided
880
+ if ELB_JANITOR_SCHEDULE in os .environ :
881
+ try :
882
+ validate_janitor_schedule (os .environ [ELB_JANITOR_SCHEDULE ], self .cloud_provider .cloud )
883
+ except ValueError as err :
884
+ errors .append (str (err ))
885
+
856
886
if errors :
857
887
raise UserReportError (returncode = INPUT_ERROR ,
858
888
message = '\n ' .join (errors ))
@@ -1108,6 +1138,31 @@ def get_instance_props(cloud_provider: CSP, region: str, machine_type: str) -> I
1108
1138
return instance_props
1109
1139
1110
1140
1141
+ def validate_janitor_schedule (val : str , cloud_provider : CSP ) -> None :
1142
+ """Validate cron schedule for janitor job. Raises ValueError if validation fails."""
1143
+ special = r'@(yearly|annually|monthly|weekly|daily|midnight|hourly)'
1144
+ minute = r'\*|(\*|([1-5]?[0-9]))((,(\*|([1-5]?[0-9])))*([/-][1-5]?[0-9])?)*'
1145
+ hour = r'\*|(\*|([1-2]?[0-9]))((,(\*|([1-2]?[0-9])))*([/-][1-2]?[0-9])?)*'
1146
+ day_of_month_gcp = r'\*|(\*|([1-3]?[0-9]))((,(\*|([1-3]?[0-9])))*([/-][1-3]?[0-9])?)*'
1147
+ day_of_month_aws = r'\*|\?|(\*|([1-3]?[0-9]L?W?))((,(\*|([1-3]?[0-9]L?W?)))*([/-][1-3]?[0-9])?)*'
1148
+ month = r'\*|(\*|(1?[0-9]))((,(\*|(1?[0-9])))*([/-]1?[0-9])?)*'
1149
+ day_of_week_gcp = r'\*|((\*|[0-7]|mon|tue|wed|thu|fri|sat|sun)((,(\*|[0-7]|mon|tue|wed|thu|fri|sat|sun))*([/-]([1-6]|mon|tue|wed|thu|fri|sat|sun))?)*)'
1150
+ day_of_week_aws = r'\*|\?|(((\*|[1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)L?)(([,#](([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)L?))*([/-]([1-6]|MON|TUE|WED|THU|FRI|SAT|SUN))?)*)'
1151
+ year = r'\*|(\*|(2[01][0-9]{2}))((,(\*|(2[01][0-9]{2})))*(-2[01][0-9]{2})?(/\d{1,3})?)*'
1152
+
1153
+
1154
+ if cloud_provider == CSP .GCP :
1155
+ pattern = special + '|' + '((' + minute + r')\s(' + hour + r')\s(' + day_of_month_gcp + r')\s(' + month + r')\s(' + day_of_week_gcp + '))'
1156
+ url = 'https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-schedule-syntax'
1157
+ else :
1158
+ pattern = r'cron\((' + minute + r')\s(' + hour + r')\s(' + day_of_month_aws + r')\s(' + month + r')\s(' + day_of_week_aws + r')\s(' + year + r')\)'
1159
+ url = 'https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html'
1160
+
1161
+ r = re .fullmatch (pattern , val )
1162
+ if r is None :
1163
+ raise ValueError (f'Invalid value of environment variable { ELB_JANITOR_SCHEDULE } "{ val } ". The string must match the regular expression "{ pattern } ". For more information, please see { url } ' )
1164
+
1165
+
1111
1166
class JSONEnumEncoder (json .JSONEncoder ):
1112
1167
"""JSON encoder that handles basic types and Enum"""
1113
1168
def default (self , o ):
@@ -1116,3 +1171,28 @@ def default(self, o):
1116
1171
return o .name
1117
1172
else :
1118
1173
return json .JSONEncoder (self , o )
1174
+
1175
+
1176
+ def get_gcp_project () -> Optional [str ]:
1177
+ """Return current GCP project or None if the property is unset.
1178
+
1179
+ Raises:
1180
+ util.SafeExecError on problems with command line gcloud
1181
+ RuntimeError if gcloud run is successful, but the result is empty"""
1182
+ cmd : str = 'gcloud config get-value project'
1183
+ p = safe_exec (cmd )
1184
+ result : Optional [str ]
1185
+
1186
+ # the result should not be empty, for unset properties gcloud returns the
1187
+ # string: '(unset)' to stderr
1188
+ if not p .stdout and not p .stderr :
1189
+ raise RuntimeError ('Current GCP project could not be established' )
1190
+
1191
+ result = p .stdout .decode ().split ('\n ' )[0 ]
1192
+
1193
+ # return None if project is unset
1194
+ if result == '(unset)' :
1195
+ result = None
1196
+ return result
1197
+
1198
+
0 commit comments