psr-factory 4.0.27__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- psr/apps/__init__.py +7 -0
- psr/apps/apps.py +232 -0
- psr/apps/version.py +5 -0
- psr/cloud/__init__.py +7 -0
- psr/cloud/cloud.py +1079 -0
- psr/cloud/data.py +105 -0
- psr/cloud/desktop.py +82 -0
- psr/cloud/log.py +40 -0
- psr/cloud/status.py +81 -0
- psr/cloud/tempfile.py +117 -0
- psr/cloud/version.py +5 -0
- psr/cloud/xml.py +55 -0
- psr/factory/__init__.py +7 -0
- psr/factory/api.py +1950 -0
- psr/factory/factory.dll +0 -0
- psr/factory/factory.pmd +6830 -0
- psr/factory/factory.pmk +18299 -0
- psr/factory/factorylib.py +308 -0
- psr/factory/libcurl-x64.dll +0 -0
- psr/factory/py.typed +0 -0
- psr/psrfcommon/__init__.py +6 -0
- psr/psrfcommon/psrfcommon.py +54 -0
- psr/psrfcommon/tempfile.py +118 -0
- psr/runner/__init__.py +7 -0
- psr/runner/runner.py +629 -0
- psr/runner/version.py +5 -0
- psr_factory-4.0.27.dist-info/METADATA +77 -0
- psr_factory-4.0.27.dist-info/RECORD +31 -0
- psr_factory-4.0.27.dist-info/WHEEL +5 -0
- psr_factory-4.0.27.dist-info/licenses/LICENSE.txt +21 -0
- psr_factory-4.0.27.dist-info/top_level.txt +1 -0
psr/cloud/cloud.py
ADDED
@@ -0,0 +1,1079 @@
|
|
1
|
+
# PSR Cloud. Copyright (C) PSR, Inc - All Rights Reserved
|
2
|
+
# Unauthorized copying of this file, via any medium is strictly prohibited
|
3
|
+
# Proprietary and confidential
|
4
|
+
|
5
|
+
import copy
|
6
|
+
import functools
|
7
|
+
import hashlib
|
8
|
+
import logging
|
9
|
+
import os
|
10
|
+
import re
|
11
|
+
import subprocess
|
12
|
+
import sys
|
13
|
+
import warnings
|
14
|
+
import xml.etree.ElementTree as ET
|
15
|
+
from datetime import datetime, timedelta
|
16
|
+
from pathlib import Path
|
17
|
+
from time import sleep, time
|
18
|
+
from typing import List, Optional, Union
|
19
|
+
|
20
|
+
import pefile
|
21
|
+
import zeep
|
22
|
+
from filelock import FileLock
|
23
|
+
|
24
|
+
from .data import Case, CloudError, CloudInputError
|
25
|
+
from .desktop import import_case
|
26
|
+
from .log import enable_log_timestamp, get_logger
|
27
|
+
from .status import FAULTY_TERMINATION_STATUS, STATUS_MAP_TEXT, ExecutionStatus
|
28
|
+
from .tempfile import CreateTempFile
|
29
|
+
from .version import __version__
|
30
|
+
from .xml import create_case_xml
|
31
|
+
|
32
|
+
INTERFACE_VERSION = "PyCloud " + __version__ + ", binding for " + sys.version
|
33
|
+
|
34
|
+
|
35
|
+
def thread_safe():
|
36
|
+
"""
|
37
|
+
Decorator to make a function thread-safe using filelock.
|
38
|
+
:param lock_file: Path to the lock file. If None, it will be automatically generated.
|
39
|
+
"""
|
40
|
+
|
41
|
+
def decorator(func):
|
42
|
+
@functools.wraps(func)
|
43
|
+
def wrapper(*args, **kwargs):
|
44
|
+
with FileLock("pycloud.lock"):
|
45
|
+
return func(*args, **kwargs)
|
46
|
+
|
47
|
+
return wrapper
|
48
|
+
|
49
|
+
return decorator
|
50
|
+
|
51
|
+
|
52
|
+
def _md5sum(value: str) -> str:
|
53
|
+
return hashlib.md5(value.encode("utf-8")).hexdigest() # nosec
|
54
|
+
|
55
|
+
|
56
|
+
def hash_password(password: str) -> str:
|
57
|
+
return _md5sum(password).upper()
|
58
|
+
|
59
|
+
|
60
|
+
def _check_for_errors(
|
61
|
+
xml: ET.ElementTree, logger: Optional[logging.Logger] = None
|
62
|
+
) -> None:
|
63
|
+
error = xml.find("./Parametro[@nome='erro']")
|
64
|
+
if error is not None:
|
65
|
+
if logger is not None:
|
66
|
+
logger.error(error.text)
|
67
|
+
raise CloudError(error.text)
|
68
|
+
|
69
|
+
|
70
|
+
def _hide_password(params: str) -> str:
|
71
|
+
pattern = r'(<Parametro nome="senha"[^>]*>)(.*?)(</Parametro>)'
|
72
|
+
result = re.sub(pattern, r"\1********\3", params)
|
73
|
+
return result
|
74
|
+
|
75
|
+
|
76
|
+
def _xml_to_str(xml_content: ET.ElementTree) -> str:
|
77
|
+
# Remove <Parametro nome="senha" ...> tag, if found in the xml_content
|
78
|
+
xml_str = ET.tostring(
|
79
|
+
xml_content.getroot(), encoding="utf-8", method="xml"
|
80
|
+
).decode()
|
81
|
+
return _hide_password(xml_str)
|
82
|
+
|
83
|
+
|
84
|
+
def _handle_relative_path(path: str) -> str:
|
85
|
+
if not os.path.isabs(path):
|
86
|
+
return os.path.abspath(path)
|
87
|
+
return path
|
88
|
+
|
89
|
+
|
90
|
+
_PSRCLOUD_PATH = r"C:\PSR\PSRCloud"
|
91
|
+
|
92
|
+
_CONSOLE_REL_PARENT_PATH = r"Oper\Console"
|
93
|
+
|
94
|
+
_CONSOLE_APP = r"FakeConsole.exe"
|
95
|
+
|
96
|
+
_ALLOWED_PROGRAMS = ["SDDP", "OPTGEN", "PSRIO", "GRAF"]
|
97
|
+
|
98
|
+
if os.name == "nt":
|
99
|
+
_PSRCLOUD_CREDENTIALS_PATH = os.path.expandvars(
|
100
|
+
os.path.join("%appdata%", "PSR", "PSRCloud", "EPSRConfig.xml")
|
101
|
+
)
|
102
|
+
else:
|
103
|
+
_PSRCLOUD_CREDENTIALS_PATH = ""
|
104
|
+
|
105
|
+
_PSRCLOUD_USER_ENV_VAR = "PSR_CLOUD_USER"
|
106
|
+
_PSRCLOUD_PASSWORD_HASH_ENV_VAR = "PSR_CLOUD_PASSWORD_HASH" # nosec
|
107
|
+
_PSRCLOUD_CONSOLE_ENV_VAR = "PSR_CLOUD_CONSOLE_PATH"
|
108
|
+
|
109
|
+
_auth_error_message = f"Please set {_PSRCLOUD_USER_ENV_VAR} and {_PSRCLOUD_PASSWORD_HASH_ENV_VAR} environment variables."
|
110
|
+
|
111
|
+
# FIXME uninspired name
|
112
|
+
_DEFAULT_GET_CASES_SINCE_DAYS = 7
|
113
|
+
|
114
|
+
|
115
|
+
_DEFAULT_CLUSTER = {
|
116
|
+
"name": "PSR-US",
|
117
|
+
"pretty_name": "External",
|
118
|
+
"url": "https://psrcloud.psr-inc.com/CamadaGerenciadoraServicoWeb/DespachanteWS.asmx",
|
119
|
+
}
|
120
|
+
|
121
|
+
|
122
|
+
class Client:
|
123
|
+
def __init__(self, **kwargs) -> None:
|
124
|
+
self.cwd = Path.cwd()
|
125
|
+
|
126
|
+
# Caches (avoiding multiple soap requests)
|
127
|
+
self._cloud_version_xml_cache = None
|
128
|
+
self._cloud_clusters_xml_cache = None
|
129
|
+
self._instance_type_map = None
|
130
|
+
|
131
|
+
# Options
|
132
|
+
self._selected_cluster = kwargs.get("cluster", _DEFAULT_CLUSTER["pretty_name"])
|
133
|
+
self._import_desktop = kwargs.get("import_desktop", True)
|
134
|
+
self._debug_mode = kwargs.get("debug", False)
|
135
|
+
self._dry_run = kwargs.get("dry_run", False)
|
136
|
+
self._timeout = kwargs.get("timeout", None)
|
137
|
+
|
138
|
+
# Client version
|
139
|
+
self.application_version = kwargs.get("application_version", None)
|
140
|
+
|
141
|
+
# Logging setup
|
142
|
+
self._quiet = kwargs.get("quiet", False)
|
143
|
+
self._verbose = kwargs.get("verbose", False)
|
144
|
+
if self._debug_mode:
|
145
|
+
self._quiet = False
|
146
|
+
self._verbose = True
|
147
|
+
log_id = id(self)
|
148
|
+
self._logger = get_logger(
|
149
|
+
log_id, quiet=self._quiet, debug_mode=self._debug_mode
|
150
|
+
)
|
151
|
+
|
152
|
+
self._logger.info(f"Client uid {log_id} initialized.")
|
153
|
+
|
154
|
+
self._console_path_setup(**kwargs)
|
155
|
+
|
156
|
+
self._credentials_setup(**kwargs)
|
157
|
+
|
158
|
+
self._cluster_setup(self._selected_cluster)
|
159
|
+
|
160
|
+
def _console_path_setup(self, **kwargs) -> None:
|
161
|
+
# For common users - provide PSR Cloud install path
|
162
|
+
if "psrcloud_path" in kwargs:
|
163
|
+
psrcloud_path = Path(kwargs["psrcloud_path"])
|
164
|
+
self._console_path = psrcloud_path / _CONSOLE_REL_PARENT_PATH / _CONSOLE_APP
|
165
|
+
if not os.path.exists(self._console_path):
|
166
|
+
err_msg = (
|
167
|
+
f"PSR Cloud application not found at {self._console_path} "
|
168
|
+
f"Make sure the path is correct and PSR Cloud is installed."
|
169
|
+
)
|
170
|
+
self._logger.error(err_msg)
|
171
|
+
self._logger.info("Provided psrcloud_path: " + str(psrcloud_path))
|
172
|
+
raise CloudError(err_msg)
|
173
|
+
# For advanced users or tests - provide full FakeConsole.exe path.
|
174
|
+
elif "fakeconsole_path" in kwargs:
|
175
|
+
self._console_path = Path(kwargs["fakeconsole_path"])
|
176
|
+
if not os.path.exists(self._console_path):
|
177
|
+
err_msg = (
|
178
|
+
f"PSR Cloud application not found at {self._console_path} "
|
179
|
+
f"Make sure the path is correct and PSR Cloud is installed."
|
180
|
+
)
|
181
|
+
self._logger.error(err_msg)
|
182
|
+
self._logger.info(
|
183
|
+
"Provided fakeconsole_path: " + str(self._console_path)
|
184
|
+
)
|
185
|
+
raise CloudError(err_msg)
|
186
|
+
# For advanced users or tests - provide PSR Cloud console path as environment variable.
|
187
|
+
elif _PSRCLOUD_CONSOLE_ENV_VAR in os.environ:
|
188
|
+
self._console_path = Path(os.environ[_PSRCLOUD_CONSOLE_ENV_VAR]).resolve()
|
189
|
+
if not os.path.exists(self._console_path):
|
190
|
+
err_msg = (
|
191
|
+
f"PSR Cloud application not found at {self._console_path} "
|
192
|
+
f"Make sure the path is correct and PSR Cloud is installed."
|
193
|
+
)
|
194
|
+
self._logger.error(err_msg)
|
195
|
+
self._logger.info("Provided console path: " + str(self._console_path))
|
196
|
+
raise CloudError(err_msg)
|
197
|
+
else:
|
198
|
+
self._console_path = (
|
199
|
+
Path(_PSRCLOUD_PATH) / _CONSOLE_REL_PARENT_PATH / _CONSOLE_APP
|
200
|
+
)
|
201
|
+
if not os.path.exists(self._console_path):
|
202
|
+
err_msg = (
|
203
|
+
f"PSR Cloud application not found at {self._console_path} "
|
204
|
+
f"Make sure the path is correct and PSR Cloud is installed."
|
205
|
+
)
|
206
|
+
self._logger.error(err_msg)
|
207
|
+
self._logger.info("Using default console path.")
|
208
|
+
raise CloudError(err_msg)
|
209
|
+
|
210
|
+
self._logger.info(f"PSR Cloud console path: {self._console_path}")
|
211
|
+
self._logger.info(f"PSR Cloud console version: {self._get_console_version()}")
|
212
|
+
|
213
|
+
def _credentials_setup(self, **kwargs) -> None:
|
214
|
+
self.username = kwargs.get("username", None)
|
215
|
+
self.__password = None
|
216
|
+
if self.username is not None:
|
217
|
+
self.username = kwargs["username"]
|
218
|
+
self.__password = hash_password(kwargs["password"])
|
219
|
+
self._logger.info(
|
220
|
+
"Using provided credentials from PSR Cloud console arguments."
|
221
|
+
)
|
222
|
+
self._logger.warning(
|
223
|
+
"For security reasons, it is highly recommended to use environment variables to store your credentials.\n"
|
224
|
+
+ f"({_PSRCLOUD_USER_ENV_VAR}, {_PSRCLOUD_PASSWORD_HASH_ENV_VAR})"
|
225
|
+
)
|
226
|
+
else:
|
227
|
+
if (
|
228
|
+
_PSRCLOUD_USER_ENV_VAR in os.environ
|
229
|
+
and _PSRCLOUD_PASSWORD_HASH_ENV_VAR in os.environ
|
230
|
+
):
|
231
|
+
self.username = os.environ[_PSRCLOUD_USER_ENV_VAR]
|
232
|
+
self.__password = os.environ[_PSRCLOUD_PASSWORD_HASH_ENV_VAR].upper()
|
233
|
+
self._logger.info("Using credentials from environment variables")
|
234
|
+
elif os.path.exists(_PSRCLOUD_CREDENTIALS_PATH):
|
235
|
+
self._logger.info(
|
236
|
+
"Environment variables for Cloud credentials not found"
|
237
|
+
)
|
238
|
+
xml = ET.parse(
|
239
|
+
_PSRCLOUD_CREDENTIALS_PATH, parser=ET.XMLParser(encoding="utf-16")
|
240
|
+
)
|
241
|
+
root = xml.getroot()
|
242
|
+
username = None
|
243
|
+
_password = None
|
244
|
+
for elem in root.iter("Aplicacao"):
|
245
|
+
username = elem.attrib.get("SrvUsuario")
|
246
|
+
_password = elem.attrib.get("SrvSenha")
|
247
|
+
break
|
248
|
+
if username is None or _password is None:
|
249
|
+
err_msg = "Credentials not provided. " + _auth_error_message
|
250
|
+
self._logger.info(
|
251
|
+
"Loading credentials from file: " + _PSRCLOUD_CREDENTIALS_PATH
|
252
|
+
)
|
253
|
+
self._logger.error(err_msg)
|
254
|
+
raise CloudInputError(err_msg)
|
255
|
+
self.username = username
|
256
|
+
self.__password = _password
|
257
|
+
self._logger.info("Using credentials from PSR Cloud Desktop cache")
|
258
|
+
else:
|
259
|
+
err_msg = "Username and password not provided." + _auth_error_message
|
260
|
+
self._logger.info(
|
261
|
+
"Trying to get credentials from environment variables."
|
262
|
+
)
|
263
|
+
self._logger.error(err_msg)
|
264
|
+
raise CloudInputError(err_msg)
|
265
|
+
self._logger.info(f"Logged as {self.username}")
|
266
|
+
|
267
|
+
def _cluster_setup(self, cluster_str: str) -> None:
|
268
|
+
"""
|
269
|
+
Get cluster object by name.
|
270
|
+
If the cluster is the default one, select it directly. If not, check using default cluster to get
|
271
|
+
the available clusters for this user and select the one that matches the provided name.
|
272
|
+
"""
|
273
|
+
|
274
|
+
if (
|
275
|
+
_DEFAULT_CLUSTER["name"].upper() == cluster_str.upper()
|
276
|
+
or _DEFAULT_CLUSTER["pretty_name"].capitalize() == cluster_str.capitalize()
|
277
|
+
):
|
278
|
+
self.cluster = _DEFAULT_CLUSTER
|
279
|
+
else:
|
280
|
+
self.cluster = None
|
281
|
+
clusters = self._get_clusters_by_user()
|
282
|
+
for cluster in clusters:
|
283
|
+
if (
|
284
|
+
cluster["name"].upper() == cluster_str.upper()
|
285
|
+
or cluster["pretty_name"].capitalize() == cluster_str.capitalize()
|
286
|
+
):
|
287
|
+
self.cluster = cluster
|
288
|
+
|
289
|
+
if self.cluster is not None:
|
290
|
+
self._logger.info(
|
291
|
+
f"Running on Cluster {self.cluster['name']} ({self.cluster['pretty_name']})"
|
292
|
+
)
|
293
|
+
else:
|
294
|
+
raise CloudInputError(f"Cluster {cluster_str} not found")
|
295
|
+
|
296
|
+
def set_cluster(self, cluster_str: str) -> None:
|
297
|
+
self._cluster_setup(cluster_str)
|
298
|
+
# Clear caches
|
299
|
+
self._cloud_version_xml_cache = None
|
300
|
+
self._cloud_clusters_xml_cache = None
|
301
|
+
self._instance_type_map = None
|
302
|
+
|
303
|
+
def _get_console_path(self) -> Path:
|
304
|
+
return self._console_path
|
305
|
+
|
306
|
+
def _get_console_parent_path(self) -> Path:
|
307
|
+
return self._console_path.parent
|
308
|
+
|
309
|
+
def _get_console_version(self) -> str:
|
310
|
+
console_path = self._get_console_path()
|
311
|
+
pe = pefile.PE(console_path)
|
312
|
+
for file_info in getattr(pe, "FileInfo", []):
|
313
|
+
for entry in file_info:
|
314
|
+
for st in getattr(entry, "StringTable", []):
|
315
|
+
product_version = st.entries.get(b"ProductVersion")
|
316
|
+
if product_version:
|
317
|
+
return product_version.decode()
|
318
|
+
|
319
|
+
@staticmethod
|
320
|
+
def _check_xml(xml_content: str) -> None:
|
321
|
+
try:
|
322
|
+
ET.fromstring(xml_content)
|
323
|
+
except ET.ParseError:
|
324
|
+
_hide_password(xml_content)
|
325
|
+
raise CloudInputError(
|
326
|
+
f"Invalid XML content.\n"
|
327
|
+
f"Contact PSR support at psrcloud@psr-inc.com with following data:\n\n{xml_content}\n\n"
|
328
|
+
)
|
329
|
+
|
330
|
+
def _get_clusters_by_user(self) -> list:
|
331
|
+
try:
|
332
|
+
previous_cluster = self.cluster
|
333
|
+
self.cluster = _DEFAULT_CLUSTER
|
334
|
+
xml = self._make_soap_request("listarCluster", "listaCluster")
|
335
|
+
|
336
|
+
clusters = []
|
337
|
+
for cluster in xml.findall("Cluster"):
|
338
|
+
nome = cluster.attrib.get("nome")
|
339
|
+
url = cluster.attrib.get("urlServico") + "/DespachanteWS.asmx"
|
340
|
+
pretty_name = cluster.attrib.get("legenda", nome)
|
341
|
+
clusters.append({"name": nome, "pretty_name": pretty_name, "url": url})
|
342
|
+
|
343
|
+
self.cluster = previous_cluster
|
344
|
+
except Exception as e:
|
345
|
+
self.cluster = previous_cluster
|
346
|
+
raise e
|
347
|
+
return clusters
|
348
|
+
|
349
|
+
def get_clusters(self) -> List[str]:
|
350
|
+
clusters = self._get_clusters_by_user()
|
351
|
+
return [cluster["pretty_name"] for cluster in clusters]
|
352
|
+
|
353
|
+
def _run_console(self, xml_content: str) -> None:
|
354
|
+
self._check_xml(xml_content)
|
355
|
+
delete_xml = not self._debug_mode
|
356
|
+
with CreateTempFile(
|
357
|
+
str(self.cwd), "psr_cloud_", xml_content, delete_xml
|
358
|
+
) as xml_file:
|
359
|
+
xml_file.close()
|
360
|
+
command = [self._get_console_path(), xml_file.name]
|
361
|
+
command_str = " ".join(map(str, command))
|
362
|
+
self._logger.debug(f"Running console command {command_str}")
|
363
|
+
quiet_goes_to_log = subprocess.PIPE if self._debug_mode else None
|
364
|
+
if self._verbose:
|
365
|
+
proc_stdout = subprocess.PIPE
|
366
|
+
proc_stderr = subprocess.PIPE
|
367
|
+
else:
|
368
|
+
if self._quiet:
|
369
|
+
proc_stdout = quiet_goes_to_log
|
370
|
+
proc_stderr = quiet_goes_to_log
|
371
|
+
else:
|
372
|
+
proc_stdout = subprocess.PIPE
|
373
|
+
proc_stderr = None
|
374
|
+
try:
|
375
|
+
process = subprocess.Popen(
|
376
|
+
command, stdout=proc_stdout, stderr=proc_stderr, shell=False
|
377
|
+
)
|
378
|
+
enable_log_timestamp(self._logger, False)
|
379
|
+
if proc_stdout is not None:
|
380
|
+
with process.stdout:
|
381
|
+
for line in iter(process.stdout.readline, b""):
|
382
|
+
if self._verbose:
|
383
|
+
self._logger.info(line.decode().strip())
|
384
|
+
else:
|
385
|
+
self._logger.debug(line.decode().strip())
|
386
|
+
if proc_stderr is not None:
|
387
|
+
with process.stderr:
|
388
|
+
for line in iter(process.stderr.readline, b""):
|
389
|
+
self._logger.error(line.decode().strip())
|
390
|
+
enable_log_timestamp(self._logger, True)
|
391
|
+
result = process.wait(timeout=self._timeout)
|
392
|
+
|
393
|
+
if result != 0:
|
394
|
+
err_msg = (
|
395
|
+
f"PSR Cloud console command failed with return code {result}"
|
396
|
+
)
|
397
|
+
self._logger.error(err_msg)
|
398
|
+
raise CloudError(err_msg)
|
399
|
+
except subprocess.CalledProcessError as e:
|
400
|
+
err_msg = f"PSR Cloud console command failed with exception: {str(e)}"
|
401
|
+
self._logger.error(err_msg)
|
402
|
+
raise CloudError(err_msg)
|
403
|
+
|
404
|
+
def _validate_case(self, case: "Case") -> "Case":
|
405
|
+
if not case.program:
|
406
|
+
raise CloudInputError("Program not provided")
|
407
|
+
elif case.program not in self.get_programs():
|
408
|
+
raise CloudInputError(
|
409
|
+
f"Program {case.program} not found. Available programs are: {', '.join(self.get_programs())}"
|
410
|
+
)
|
411
|
+
|
412
|
+
if not case.memory_per_process_ratio:
|
413
|
+
raise CloudInputError("Memory per process ratio not provided")
|
414
|
+
elif case.memory_per_process_ratio not in self.get_memory_per_process_ratios():
|
415
|
+
raise CloudInputError(
|
416
|
+
f"Memory per process ratio {case.memory_per_process_ratio} not found. Available ratios are: {', '.join(self.get_memory_per_process_ratios())}"
|
417
|
+
)
|
418
|
+
|
419
|
+
if case.number_of_processes < 1 or case.number_of_processes > 512:
|
420
|
+
raise CloudInputError("Number of processes must be between 1 and 512")
|
421
|
+
|
422
|
+
if case.data_path and not Path(case.data_path).exists():
|
423
|
+
raise CloudInputError("Data path does not exist")
|
424
|
+
|
425
|
+
if case.parent_case_id is None:
|
426
|
+
case.parent_case_id = 0
|
427
|
+
|
428
|
+
def validate_selection(
|
429
|
+
selection, available_options, selection_name, program_name
|
430
|
+
):
|
431
|
+
if selection is None:
|
432
|
+
raise CloudInputError(
|
433
|
+
f"{selection_name} of program {program_name} not provided"
|
434
|
+
)
|
435
|
+
elif isinstance(selection, str):
|
436
|
+
if selection not in available_options.values():
|
437
|
+
raise CloudInputError(
|
438
|
+
f"{selection_name} {selection} of program {program_name} not found. Available {selection_name.lower()}s are: {', '.join(available_options.values())}"
|
439
|
+
)
|
440
|
+
return next(
|
441
|
+
key
|
442
|
+
for key, value in available_options.items()
|
443
|
+
if value == selection
|
444
|
+
)
|
445
|
+
elif selection not in available_options:
|
446
|
+
raise CloudInputError(
|
447
|
+
f"{selection_name} id {selection} of program {program_name} not found. Available {selection_name.lower()} ids are: {', '.join(map(str,available_options.keys()))}"
|
448
|
+
)
|
449
|
+
return selection
|
450
|
+
|
451
|
+
program_versions = self.get_program_versions(case.program)
|
452
|
+
case.program_version = validate_selection(
|
453
|
+
case.program_version, program_versions, "Version", case.program
|
454
|
+
)
|
455
|
+
|
456
|
+
execution_types = self.get_execution_types(case.program, case.program_version)
|
457
|
+
case.execution_type = validate_selection(
|
458
|
+
case.execution_type, execution_types, "Execution type", case.program
|
459
|
+
)
|
460
|
+
|
461
|
+
instance_type_map = self._get_instance_type_map()
|
462
|
+
if all(value[1] == False for value in instance_type_map.values()):
|
463
|
+
is_spot_disabled = True
|
464
|
+
else:
|
465
|
+
is_spot_disabled = False
|
466
|
+
|
467
|
+
if case.price_optimized == True and is_spot_disabled == True:
|
468
|
+
raise CloudError("Price Optimized is temporarily unavailable.")
|
469
|
+
|
470
|
+
repository_durations = self.get_repository_durations()
|
471
|
+
case.repository_duration = validate_selection(
|
472
|
+
case.repository_duration,
|
473
|
+
repository_durations,
|
474
|
+
"Repository duration",
|
475
|
+
case.program,
|
476
|
+
)
|
477
|
+
|
478
|
+
if case.budget:
|
479
|
+
budgets = self.get_budgets()
|
480
|
+
match_list = _budget_matches_list(case.budget, budgets)
|
481
|
+
if len(match_list) == 0:
|
482
|
+
raise CloudInputError(
|
483
|
+
f'Budget "{case.budget}" not found. Get a list of available budgets using Client().get_budgets().'
|
484
|
+
)
|
485
|
+
elif len(match_list) > 1:
|
486
|
+
raise CloudInputError(
|
487
|
+
f'Multiple budgets found for "{case.budget}". Please use the budget id instead of the name.\n'
|
488
|
+
"\n".join([f' - "{budget}"' for budget in match_list])
|
489
|
+
)
|
490
|
+
else:
|
491
|
+
# Replace partial with complete budget name
|
492
|
+
case.budget = match_list[0]
|
493
|
+
|
494
|
+
return case
|
495
|
+
|
496
|
+
def _pre_process_graph(self, path: str, case_id: int) -> None:
|
497
|
+
# This method is only used for testing the graf cloud execution.
|
498
|
+
# Error handling is already done on the tests module.
|
499
|
+
parameters = {
|
500
|
+
"urlServico": self.cluster["url"],
|
501
|
+
"usuario": self.username,
|
502
|
+
"senha": self.__password,
|
503
|
+
"idioma": "3",
|
504
|
+
"modelo": "Graf",
|
505
|
+
"comando": "PreProcessamento",
|
506
|
+
"cluster": self.cluster["name"],
|
507
|
+
"repositorioId": str(case_id),
|
508
|
+
"diretorioDestino": path,
|
509
|
+
"tipoExecucao": "1",
|
510
|
+
}
|
511
|
+
|
512
|
+
xml_content = create_case_xml(parameters)
|
513
|
+
self._run_console(xml_content)
|
514
|
+
|
515
|
+
def _check_until_status(
|
516
|
+
self, case_id: int, requested_status: "ExecutionStatus", timeout: int = 60 * 60
|
517
|
+
) -> bool:
|
518
|
+
"""
|
519
|
+
Check the status of a case until the requested status is reached or timeout occurs.
|
520
|
+
|
521
|
+
:param case_id: The ID of the case to check.
|
522
|
+
:param requested_status: The status to wait for.
|
523
|
+
:param timeout: The maximum time to wait in seconds (default is 3600 seconds or 1 hour).
|
524
|
+
:return: True if the requested status is reached, False if timeout occurs.
|
525
|
+
"""
|
526
|
+
status = None
|
527
|
+
start_time = time()
|
528
|
+
original_quiet_flag = self._quiet
|
529
|
+
original_verbose_flag = self._verbose
|
530
|
+
original_debug_flag = self._debug_mode
|
531
|
+
self._quiet, self._verbose, self._debug_mode = True, False, False
|
532
|
+
try:
|
533
|
+
while status not in FAULTY_TERMINATION_STATUS + [
|
534
|
+
ExecutionStatus.SUCCESS,
|
535
|
+
requested_status,
|
536
|
+
]:
|
537
|
+
if time() - start_time > timeout:
|
538
|
+
self._logger.error(
|
539
|
+
f"Timeout reached while waiting for status {requested_status}"
|
540
|
+
)
|
541
|
+
return False
|
542
|
+
status, _ = self.get_status(case_id)
|
543
|
+
sleep(20)
|
544
|
+
finally:
|
545
|
+
self._quiet = original_quiet_flag
|
546
|
+
self._verbose = original_verbose_flag
|
547
|
+
self._debug_mode = original_debug_flag
|
548
|
+
|
549
|
+
return status == requested_status
|
550
|
+
|
551
|
+
def _clean_folder(self, folder):
|
552
|
+
for root, dirs, files in os.walk(folder, topdown=False):
|
553
|
+
for file in files:
|
554
|
+
file_path = os.path.join(root, file)
|
555
|
+
os.remove(file_path)
|
556
|
+
|
557
|
+
for dir in dirs:
|
558
|
+
dir_path = os.path.join(root, dir)
|
559
|
+
os.rmdir(dir_path)
|
560
|
+
|
561
|
+
@thread_safe()
|
562
|
+
def run_case(self, case: "Case", **kwargs) -> int:
|
563
|
+
self._validate_case(case)
|
564
|
+
instance_type_map = self._get_instance_type_map()
|
565
|
+
instance_type_id = next(
|
566
|
+
key
|
567
|
+
for key, value in instance_type_map.items()
|
568
|
+
if value[0] == case.memory_per_process_ratio
|
569
|
+
and value[1] == case.price_optimized
|
570
|
+
)
|
571
|
+
case.data_path = _handle_relative_path(case.data_path)
|
572
|
+
|
573
|
+
if case.program == "GRAF":
|
574
|
+
wait = True
|
575
|
+
else:
|
576
|
+
wait = kwargs.get("wait", False)
|
577
|
+
|
578
|
+
if self.application_version:
|
579
|
+
interface_version = self.application_version + " - " + INTERFACE_VERSION
|
580
|
+
else:
|
581
|
+
interface_version = INTERFACE_VERSION
|
582
|
+
parameters = {
|
583
|
+
"urlServico": self.cluster["url"],
|
584
|
+
"usuario": self.username,
|
585
|
+
"senha": self.__password,
|
586
|
+
"idioma": "3",
|
587
|
+
"modelo": case.program,
|
588
|
+
"comando": "executar",
|
589
|
+
"cluster": self.cluster["name"],
|
590
|
+
"diretorioDados": case.data_path,
|
591
|
+
"origemDados": "LOCAL",
|
592
|
+
"s3Dados": "",
|
593
|
+
"nproc": case.number_of_processes,
|
594
|
+
"repositorioId": "0",
|
595
|
+
"repositorioPai": case.parent_case_id,
|
596
|
+
"instanciaTipo": instance_type_id,
|
597
|
+
"validacaoModelo": "True",
|
598
|
+
"validacaoUsuario": "False",
|
599
|
+
"idVersao": case.program_version,
|
600
|
+
"pathModelo": "C:\\PSR",
|
601
|
+
"idTipoExecucao": case.execution_type,
|
602
|
+
"nomeCaso": case.name,
|
603
|
+
"tipoExecucao": str(int(not wait)),
|
604
|
+
"deveAgendar": "False",
|
605
|
+
"userTag": "(Untagged)",
|
606
|
+
"lifecycle": case.repository_duration,
|
607
|
+
"versaoInterface": interface_version,
|
608
|
+
}
|
609
|
+
|
610
|
+
if case.budget:
|
611
|
+
parameters["budget"] = case.budget
|
612
|
+
if case.upload_only is not None:
|
613
|
+
parameters["saveInCloud"] = case.upload_only
|
614
|
+
|
615
|
+
xml_content = create_case_xml(parameters)
|
616
|
+
|
617
|
+
if self._dry_run:
|
618
|
+
return xml_content
|
619
|
+
|
620
|
+
self._run_console(xml_content)
|
621
|
+
xml = ET.parse(
|
622
|
+
f"{self._get_console_parent_path()}\\fake{case.program}_async.xml"
|
623
|
+
)
|
624
|
+
_check_for_errors(xml, self._logger)
|
625
|
+
id_parameter = xml.find("./Parametro[@nome='repositorioId']")
|
626
|
+
if id_parameter is None:
|
627
|
+
xml_str = _xml_to_str(xml)
|
628
|
+
raise CloudError(
|
629
|
+
f"Case id not found on returned XML response.\n"
|
630
|
+
f"Contact PSR support at psrcloud@psr-inc.com with following data:\n\n{xml_str}\n\n"
|
631
|
+
)
|
632
|
+
|
633
|
+
case_id = int(id_parameter.text)
|
634
|
+
if not wait:
|
635
|
+
self._logger.info(f"Case {case.name} started with id {case_id}")
|
636
|
+
|
637
|
+
if self._import_desktop and case.program != "GRAF":
|
638
|
+
try:
|
639
|
+
case_copy = copy.deepcopy(case)
|
640
|
+
case_copy.id = case_id
|
641
|
+
replace_case_str_values(self, case_copy)
|
642
|
+
import_case(case_copy, self.cluster["name"], instance_type_id)
|
643
|
+
except Exception as e:
|
644
|
+
msg = f"Failed to import case {case.name} to desktop:\n{str(e)}"
|
645
|
+
self._logger.error(msg)
|
646
|
+
warnings.warn(msg)
|
647
|
+
|
648
|
+
return case_id
|
649
|
+
|
650
|
+
def get_status(self, case_id: int) -> tuple["ExecutionStatus", str]:
|
651
|
+
# case = self.get_case(case_id)
|
652
|
+
delete_xml = not self._debug_mode
|
653
|
+
xml_content = ""
|
654
|
+
with CreateTempFile(
|
655
|
+
str(self.cwd), "psr_cloud_status_", xml_content, delete_xml
|
656
|
+
) as xml_file:
|
657
|
+
status_xml_path = os.path.abspath(xml_file.name)
|
658
|
+
|
659
|
+
parameters = {
|
660
|
+
"urlServico": self.cluster["url"],
|
661
|
+
"usuario": self.username,
|
662
|
+
"senha": self.__password,
|
663
|
+
"idioma": "3",
|
664
|
+
"idFila": str(case_id),
|
665
|
+
"modelo": "SDDP",
|
666
|
+
"comando": "obterstatusresultados",
|
667
|
+
"arquivoSaida": status_xml_path,
|
668
|
+
}
|
669
|
+
run_xml_content = create_case_xml(parameters)
|
670
|
+
|
671
|
+
self._run_console(run_xml_content)
|
672
|
+
xml = ET.parse(status_xml_path)
|
673
|
+
parameter_status = xml.find("./Parametro[@nome='statusExecucao']")
|
674
|
+
if parameter_status is None:
|
675
|
+
xml_str = _xml_to_str(xml)
|
676
|
+
raise CloudError(
|
677
|
+
f"Status not found on returned XML response.\n"
|
678
|
+
f"Contact PSR support at psrcloud@psr-inc.com with following data:\n\n{xml_str}\n\n"
|
679
|
+
)
|
680
|
+
try:
|
681
|
+
status = ExecutionStatus(int(parameter_status.text))
|
682
|
+
except CloudError:
|
683
|
+
xml_str = _xml_to_str(xml)
|
684
|
+
raise CloudError(
|
685
|
+
f"Unrecognized status on returned XML response.\n"
|
686
|
+
f"Contact PSR support at psrcloud@psr-inc.com with following data:\n\n{xml_str}\n\n"
|
687
|
+
)
|
688
|
+
|
689
|
+
self._logger.info(f"Status: {STATUS_MAP_TEXT[status]}")
|
690
|
+
return status, STATUS_MAP_TEXT[status]
|
691
|
+
|
692
|
+
def list_download_files(self, case_id: int) -> List[str]:
|
693
|
+
xml_files = self._make_soap_request(
|
694
|
+
"prepararListaArquivosRemotaDownload",
|
695
|
+
"listaArquivoRemota",
|
696
|
+
additional_arguments={
|
697
|
+
"cluster": self.cluster["name"],
|
698
|
+
"filtro": "(.*)",
|
699
|
+
"diretorioRemoto": str(case_id),
|
700
|
+
},
|
701
|
+
)
|
702
|
+
|
703
|
+
files = []
|
704
|
+
|
705
|
+
for file in xml_files.findall("Arquivo"):
|
706
|
+
file_info = {
|
707
|
+
"name": file.attrib.get("nome"),
|
708
|
+
"filesize": file.attrib.get("filesize"),
|
709
|
+
"filedate": file.attrib.get("filedate"),
|
710
|
+
}
|
711
|
+
files.append(file_info)
|
712
|
+
|
713
|
+
return files
|
714
|
+
|
715
|
+
def download_results(
|
716
|
+
self,
|
717
|
+
case_id: int,
|
718
|
+
output_path: Union[str, Path],
|
719
|
+
files: Optional[List[str]] = None,
|
720
|
+
extensions: Optional[List[str]] = None,
|
721
|
+
) -> None:
|
722
|
+
filter = ".*("
|
723
|
+
|
724
|
+
if not extensions and not files:
|
725
|
+
extensions = ["csv", "log", "hdr", "bin", "out", "ok"]
|
726
|
+
|
727
|
+
filter_elements = []
|
728
|
+
|
729
|
+
if extensions:
|
730
|
+
Client._validate_extensions(extensions)
|
731
|
+
filter_elements.extend([f".*.{ext}$" for ext in extensions])
|
732
|
+
|
733
|
+
if files:
|
734
|
+
filter_elements.extend(files)
|
735
|
+
|
736
|
+
filter += "|".join(filter_elements)
|
737
|
+
filter += ")$"
|
738
|
+
|
739
|
+
self._logger.info("Download filter: " + filter)
|
740
|
+
case = self.get_case(case_id)
|
741
|
+
output_path = _handle_relative_path(output_path)
|
742
|
+
parameters = {
|
743
|
+
"urlServico": self.cluster["url"],
|
744
|
+
"usuario": self.username,
|
745
|
+
"senha": self.__password,
|
746
|
+
"idioma": "3",
|
747
|
+
"_cluster": self.cluster["name"],
|
748
|
+
"modelo": case.program,
|
749
|
+
"comando": "download",
|
750
|
+
"diretorioDestino": output_path,
|
751
|
+
"repositorioId": str(case_id),
|
752
|
+
"filtroDownloadPorMascara": filter,
|
753
|
+
}
|
754
|
+
|
755
|
+
xml_content = create_case_xml(parameters)
|
756
|
+
self._run_console(xml_content)
|
757
|
+
self._logger.info(f"Results downloaded to {output_path}")
|
758
|
+
|
759
|
+
def cancel_case(self, case_id: int, wait: bool = False) -> bool:
|
760
|
+
parameters = {
|
761
|
+
"urlServico": self.cluster["url"],
|
762
|
+
"usuario": self.username,
|
763
|
+
"senha": self.__password,
|
764
|
+
"idioma": "3",
|
765
|
+
"modelo": "SDDP",
|
766
|
+
"comando": "cancelarfila",
|
767
|
+
"cancelamentoForcado": "False",
|
768
|
+
"idFila": str(case_id),
|
769
|
+
}
|
770
|
+
|
771
|
+
xml_content = create_case_xml(parameters)
|
772
|
+
self._run_console(xml_content)
|
773
|
+
self._logger.info(f"Request to cancel case {case_id} was sent")
|
774
|
+
|
775
|
+
if wait:
|
776
|
+
self._logger.info(f"Waiting for case {case_id} to be canceled")
|
777
|
+
if self._check_until_status(
|
778
|
+
case_id, ExecutionStatus.CANCELLED, timeout=60 * 10
|
779
|
+
):
|
780
|
+
self._logger.info(f"Case {case_id} was successfully canceled")
|
781
|
+
return True
|
782
|
+
else:
|
783
|
+
self._logger.error(f"Failed to cancel case {case_id}")
|
784
|
+
return False
|
785
|
+
else:
|
786
|
+
return True
|
787
|
+
|
788
|
+
def _cases_from_xml(self, xml: ET.Element) -> List["Case"]:
|
789
|
+
instance_type_map = self._get_instance_type_map()
|
790
|
+
cases = []
|
791
|
+
for fila in xml.findall("Fila"):
|
792
|
+
try:
|
793
|
+
case = Case(
|
794
|
+
name=fila.attrib.get("nomeCaso"),
|
795
|
+
data_path=None,
|
796
|
+
program=fila.attrib.get("programa"),
|
797
|
+
program_version=int(fila.attrib.get("idVersao")),
|
798
|
+
execution_type=int(fila.attrib.get("idTipoExecucao")),
|
799
|
+
price_optimized=bool(fila.attrib.get("flagSpot")),
|
800
|
+
number_of_processes=int(fila.attrib.get("numeroProcesso")),
|
801
|
+
id=int(fila.attrib.get("repositorioId")),
|
802
|
+
user=fila.attrib.get("usuario"),
|
803
|
+
execution_date=datetime.strptime(
|
804
|
+
fila.attrib.get("dataInicio"), "%d/%m/%Y %H:%M"
|
805
|
+
),
|
806
|
+
parent_case_id=int(fila.attrib.get("repositorioPai"))
|
807
|
+
if fila.attrib.get("repositorioPai")
|
808
|
+
else 0,
|
809
|
+
memory_per_process_ratio=(
|
810
|
+
instance_type_map[int(fila.attrib.get("instanciaTipo"))][0]
|
811
|
+
if fila.attrib.get("instanciaTipo") in instance_type_map
|
812
|
+
else min([value[0] for value in instance_type_map.values()])
|
813
|
+
),
|
814
|
+
repository_duration=int(fila.attrib.get("duracaoRepositorio")),
|
815
|
+
budget=fila.attrib.get("budget"),
|
816
|
+
)
|
817
|
+
cases.append(case)
|
818
|
+
except (TypeError, ValueError):
|
819
|
+
pass
|
820
|
+
# case_id = fila.attrib.get("repositorioId")
|
821
|
+
# Optionally, log the error or handle it as needed
|
822
|
+
# self._logger.error(f"Error processing case with ID {case_id}: {e}")
|
823
|
+
|
824
|
+
# sort cases by execution date desc
|
825
|
+
cases.sort(key=lambda x: x.execution_date, reverse=True)
|
826
|
+
return cases
|
827
|
+
|
828
|
+
def get_all_cases_since(
|
829
|
+
self, since: Union[int, datetime] = _DEFAULT_GET_CASES_SINCE_DAYS
|
830
|
+
) -> List["Case"]:
|
831
|
+
if isinstance(since, int):
|
832
|
+
initial_date = datetime.now() - timedelta(days=since)
|
833
|
+
initial_date_iso = initial_date.isoformat().replace("T", " ")[:-7]
|
834
|
+
else:
|
835
|
+
initial_date_iso = since.strftime("%Y-%m-%d %H:%M:%S")
|
836
|
+
|
837
|
+
xml = self._make_soap_request(
|
838
|
+
"listarFila",
|
839
|
+
"dados",
|
840
|
+
additional_arguments={"dataInicial": initial_date_iso},
|
841
|
+
)
|
842
|
+
|
843
|
+
return self._cases_from_xml(xml)
|
844
|
+
|
845
|
+
def get_case(self, case_id: int) -> "Case":
|
846
|
+
cases = self.get_cases([case_id])
|
847
|
+
if cases and len(cases) > 0:
|
848
|
+
return cases[0]
|
849
|
+
raise CloudInputError(f"Case {case_id} not found")
|
850
|
+
|
851
|
+
def get_cases(self, case_ids: List[int]) -> List["Case"]:
|
852
|
+
case_ids_str = ",".join(map(str, case_ids))
|
853
|
+
xml = self._make_soap_request(
|
854
|
+
"listarFila",
|
855
|
+
"dados",
|
856
|
+
additional_arguments={"listaRepositorio": case_ids_str},
|
857
|
+
)
|
858
|
+
return self._cases_from_xml(xml)
|
859
|
+
|
860
|
+
def get_budgets(self) -> list:
|
861
|
+
xml = self._make_soap_request(
|
862
|
+
"listarCluster",
|
863
|
+
"listaCluster",
|
864
|
+
)
|
865
|
+
|
866
|
+
budgets = []
|
867
|
+
for cluster in xml.findall("Cluster"):
|
868
|
+
if cluster.attrib.get("nome").lower() == self.cluster["name"].lower():
|
869
|
+
collection = cluster.findall("ColecaoBudget")[0]
|
870
|
+
budgets = [
|
871
|
+
budget.attrib.get("nome") for budget in collection.findall("Budget")
|
872
|
+
]
|
873
|
+
break
|
874
|
+
budgets.sort()
|
875
|
+
return budgets
|
876
|
+
|
877
|
+
def _make_soap_request(self, service: str, name: str = "", **kwargs) -> ET.Element:
|
878
|
+
portal_ws = zeep.Client(self.cluster["url"] + "?WSDL")
|
879
|
+
section = str(id(self))
|
880
|
+
password_md5 = _md5sum(self.username + self.__password + section).upper()
|
881
|
+
password_md5 = (
|
882
|
+
password_md5
|
883
|
+
if self.cluster["name"] == "PSR-US"
|
884
|
+
or self.cluster["name"] == "PSR-HOTFIX"
|
885
|
+
or self.cluster["name"] == "PSR-US_OHIO"
|
886
|
+
else self.__password.upper()
|
887
|
+
)
|
888
|
+
additional_arguments = kwargs.get("additional_arguments", None)
|
889
|
+
parameters = {
|
890
|
+
"sessao_id": section,
|
891
|
+
"tipo_autenticacao": "portal"
|
892
|
+
if self.cluster["name"] == "PSR-US"
|
893
|
+
or self.cluster["name"] == "PSR-HOTFIX"
|
894
|
+
or self.cluster["name"] == "PSR-US_OHIO"
|
895
|
+
else "bcrypt",
|
896
|
+
"idioma": "3",
|
897
|
+
}
|
898
|
+
if additional_arguments:
|
899
|
+
parameters.update(additional_arguments)
|
900
|
+
|
901
|
+
# FIXME make additional arguments work as a dictionary to work with this code
|
902
|
+
xml_input = create_case_xml(parameters)
|
903
|
+
|
904
|
+
try:
|
905
|
+
xml_output_str = portal_ws.service.despacharServico(
|
906
|
+
service, self.username, password_md5, xml_input
|
907
|
+
)
|
908
|
+
except zeep.exceptions.Fault as e:
|
909
|
+
# Log the full exception details
|
910
|
+
self._logger.error(f"Zeep Fault: {str(e)}")
|
911
|
+
raise CloudError(
|
912
|
+
"Failed to connect to PSR Cloud service. Contact PSR support: psrcloud@psr-inc.com"
|
913
|
+
)
|
914
|
+
# Remove control characters - this is a thing
|
915
|
+
xml_output_str = xml_output_str.replace("&#x1F;", "")
|
916
|
+
|
917
|
+
xml_output = ET.fromstring(xml_output_str)
|
918
|
+
|
919
|
+
if name:
|
920
|
+
for child in xml_output:
|
921
|
+
if child.attrib.get("nome") == name:
|
922
|
+
xml_output = ET.fromstring(child.text)
|
923
|
+
break
|
924
|
+
else:
|
925
|
+
raise ValueError(
|
926
|
+
f"Invalid XML response from PSR Cloud: {xml_output_str}. Please contact PSR support at psrcloud@psr-inc.com"
|
927
|
+
)
|
928
|
+
return xml_output
|
929
|
+
|
930
|
+
def _get_cloud_versions_xml(self) -> ET.Element:
|
931
|
+
if self._cloud_version_xml_cache is not None:
|
932
|
+
return self._cloud_version_xml_cache
|
933
|
+
self._cloud_version_xml_cache = self._make_soap_request("obterVersoes", "dados")
|
934
|
+
return self._cloud_version_xml_cache
|
935
|
+
|
936
|
+
def _get_cloud_clusters_xml(self) -> ET.Element:
|
937
|
+
if self._cloud_clusters_xml_cache is not None:
|
938
|
+
return self._cloud_clusters_xml_cache
|
939
|
+
self._cloud_clusters_xml_cache = self._make_soap_request(
|
940
|
+
"listarCluster", "listaCluster"
|
941
|
+
)
|
942
|
+
return self._cloud_clusters_xml_cache
|
943
|
+
|
944
|
+
def get_programs(self) -> List[str]:
|
945
|
+
xml = self._get_cloud_versions_xml()
|
946
|
+
programs = [model.attrib["nome"] for model in xml]
|
947
|
+
return [program for program in programs if program in _ALLOWED_PROGRAMS]
|
948
|
+
|
949
|
+
def get_program_versions(self, program: str) -> dict[int, str]:
|
950
|
+
if not isinstance(program, str):
|
951
|
+
raise CloudInputError("Program must be a string")
|
952
|
+
elif program not in self.get_programs():
|
953
|
+
raise CloudInputError(
|
954
|
+
f"Program {program} not found. Available programs: {', '.join(self.get_programs())}"
|
955
|
+
)
|
956
|
+
xml = self._get_cloud_versions_xml()
|
957
|
+
versions = {}
|
958
|
+
|
959
|
+
for model in xml:
|
960
|
+
if model.attrib["nome"] == program:
|
961
|
+
for version_child in model.findall(".//Versao"):
|
962
|
+
version_id = int(version_child.attrib["id"])
|
963
|
+
version_name = version_child.attrib["versao"]
|
964
|
+
versions[version_id] = version_name
|
965
|
+
|
966
|
+
return versions
|
967
|
+
|
968
|
+
def get_execution_types(
|
969
|
+
self, program: str, version: Union[str, int]
|
970
|
+
) -> dict[int, str]:
|
971
|
+
if not isinstance(program, str):
|
972
|
+
raise CloudInputError("Program must be a string")
|
973
|
+
elif program not in self.get_programs():
|
974
|
+
raise CloudInputError(
|
975
|
+
f"Program {program} not found. Available programs: {', '.join(self.get_programs())}"
|
976
|
+
)
|
977
|
+
if isinstance(version, int):
|
978
|
+
if version not in self.get_program_versions(program):
|
979
|
+
raise CloudInputError(
|
980
|
+
f"Version id {version} of program {program} not found. Available version ids: {', '.join(map(str, list(self.get_program_versions(program).keys())))}"
|
981
|
+
)
|
982
|
+
version = next(
|
983
|
+
v for k, v in self.get_program_versions(program).items() if k == version
|
984
|
+
)
|
985
|
+
elif version not in self.get_program_versions(program).values():
|
986
|
+
raise CloudInputError(
|
987
|
+
f"Version {version} of program {program} not found. Available versions: {', '.join(self.get_program_versions(program).values())}"
|
988
|
+
)
|
989
|
+
xml = self._get_cloud_versions_xml()
|
990
|
+
return {
|
991
|
+
int(execution_type.attrib["id"]): execution_type.attrib["nome"]
|
992
|
+
for program_child in xml
|
993
|
+
if program_child.attrib["nome"] == program
|
994
|
+
for version_child in program_child[0][0][0]
|
995
|
+
if version_child.attrib["versao"] == version
|
996
|
+
for execution_type in version_child[0]
|
997
|
+
}
|
998
|
+
|
999
|
+
def get_memory_per_process_ratios(self) -> List[str]:
|
1000
|
+
xml = self._get_cloud_clusters_xml()
|
1001
|
+
return sorted(
|
1002
|
+
list(
|
1003
|
+
{
|
1004
|
+
f"{instance_type.attrib['memoriaPorCore']}:1"
|
1005
|
+
for cluster in xml
|
1006
|
+
if cluster.attrib["nome"] == self.cluster["name"]
|
1007
|
+
for colection in cluster
|
1008
|
+
if colection.tag == "ColecaoInstanciaTipo"
|
1009
|
+
for instance_type in colection
|
1010
|
+
}
|
1011
|
+
)
|
1012
|
+
)
|
1013
|
+
|
1014
|
+
def get_repository_durations(self) -> dict[int, str]:
|
1015
|
+
if self.cluster == "PSR-US":
|
1016
|
+
return {
|
1017
|
+
2: "Normal (1 month)",
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
else:
|
1021
|
+
return {
|
1022
|
+
1: "Short (1 week)",
|
1023
|
+
2: "Normal (1 month)",
|
1024
|
+
3: "Extended (6 months)",
|
1025
|
+
4: "Long (2 years)",
|
1026
|
+
}
|
1027
|
+
|
1028
|
+
def _get_instance_type_map(self) -> dict[int, tuple[str, bool]]:
|
1029
|
+
if self._instance_type_map is not None:
|
1030
|
+
return self._instance_type_map
|
1031
|
+
xml = self._get_cloud_clusters_xml()
|
1032
|
+
self._instance_type_map = {
|
1033
|
+
int(instance_type.attrib["id"]): (
|
1034
|
+
f'{instance_type.attrib["memoriaPorCore"]}:1',
|
1035
|
+
"Price Optimized" in instance_type.attrib["descricao"],
|
1036
|
+
)
|
1037
|
+
for cluster in xml
|
1038
|
+
if cluster.attrib["nome"] == self.cluster["name"]
|
1039
|
+
for collection in cluster
|
1040
|
+
if collection.tag == "ColecaoInstanciaTipo"
|
1041
|
+
for instance_type in collection
|
1042
|
+
}
|
1043
|
+
return self._instance_type_map
|
1044
|
+
|
1045
|
+
@staticmethod
|
1046
|
+
def _validate_extensions(extensions: List[str]):
|
1047
|
+
for ext in extensions:
|
1048
|
+
if not ext.isalnum():
|
1049
|
+
raise CloudInputError(
|
1050
|
+
f"Invalid extension '{ext}' detected. Extensions must be alphanumeric."
|
1051
|
+
)
|
1052
|
+
|
1053
|
+
|
1054
|
+
def _budget_matches_list(budget_part: str, all_budgets: List[str]) -> List[str]:
|
1055
|
+
"""Tests if a part of a budget name is in the list all_budgets and returns a list of matches."""
|
1056
|
+
lowered_budget_part = budget_part.lower()
|
1057
|
+
return [budget for budget in all_budgets if lowered_budget_part in budget.lower()]
|
1058
|
+
|
1059
|
+
|
1060
|
+
def replace_case_str_values(client: Client, case: Case) -> Case:
|
1061
|
+
"""Create a new case object using internal integer IDs instead of string values."""
|
1062
|
+
# Model Version
|
1063
|
+
if isinstance(case.program_version, str):
|
1064
|
+
program_versions = client.get_program_versions(case.program)
|
1065
|
+
case.program_version = next(
|
1066
|
+
key
|
1067
|
+
for key, value in program_versions.items()
|
1068
|
+
if value == case.program_version
|
1069
|
+
)
|
1070
|
+
|
1071
|
+
# Execution Type
|
1072
|
+
if isinstance(case.execution_type, str):
|
1073
|
+
execution_types = client.get_execution_types(case.program, case.program_version)
|
1074
|
+
case.execution_type = next(
|
1075
|
+
key
|
1076
|
+
for key, value in execution_types.items()
|
1077
|
+
if value == case.execution_type
|
1078
|
+
)
|
1079
|
+
return case
|