diff --git a/doc/changelog.d/3205.added.md b/doc/changelog.d/3205.added.md new file mode 100644 index 0000000000..44cf5dc554 --- /dev/null +++ b/doc/changelog.d/3205.added.md @@ -0,0 +1 @@ +feat: Detaching logging from main logic \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f5cff755ce..2226a16a50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,11 @@ doc = [ "vtk==9.3.0", ] +hps =[ + "ansys-hps-client==0.8.0", + "keyring==25.2.1", +] + [tool.flit.module] name = "ansys.mapdl.core" diff --git a/src/ansys/mapdl/core/cli/__init__.py b/src/ansys/mapdl/core/cli/__init__.py index b2936f4558..f0e70da863 100644 --- a/src/ansys/mapdl/core/cli/__init__.py +++ b/src/ansys/mapdl/core/cli/__init__.py @@ -29,6 +29,13 @@ _HAS_CLICK = False +try: + from ansys.hps.client import Client + + _HAS_HPS = True +except ModuleNotFoundError: + _HAS_HPS = False + if _HAS_CLICK: ################################### # PyMAPDL CLI @@ -39,7 +46,6 @@ def main(ctx): pass from ansys.mapdl.core.cli.convert import convert - from ansys.mapdl.core.cli.hpc import submit from ansys.mapdl.core.cli.list_instances import list_instances from ansys.mapdl.core.cli.start import start from ansys.mapdl.core.cli.stop import stop @@ -50,10 +56,18 @@ def main(ctx): main.add_command(list_instances, name="list") # HPC commands - # pymapdl hpc submit - # pymapdl hpc list - # pymapdl hpc stop - main.add_command(submit) + # pymapdl (hpc) login + # pymapdl (hpc) submit + # pymapdl (hpc) list #To be implemented + # pymapdl (hpc) stop #To be implemented + + if _HAS_HPS: + from ansys.mapdl.core.cli.hpc import submit + from ansys.mapdl.core.cli.login import login, logout + + main.add_command(login) + main.add_command(submit) + main.add_command(logout) def old_pymapdl_convert_script_entry_point(): print( diff --git a/src/ansys/mapdl/core/cli/convert.py b/src/ansys/mapdl/core/cli/convert.py index 61224e49e2..4e6c8c09bf 100644 --- a/src/ansys/mapdl/core/cli/convert.py +++ b/src/ansys/mapdl/core/cli/convert.py @@ -212,6 +212,9 @@ def convert( ) else: + if not filename_in: + raise ValueError("A file path must be provided.") + convert_script( filename_in, filename_out, diff --git a/src/ansys/mapdl/core/cli/hpc.py b/src/ansys/mapdl/core/cli/hpc.py index 0d79ed4ddb..a3b765dc58 100644 --- a/src/ansys/mapdl/core/cli/hpc.py +++ b/src/ansys/mapdl/core/cli/hpc.py @@ -28,12 +28,10 @@ import click -from ansys.mapdl.core.cli import main - logger = logging.getLogger() -@main.command( +@click.command( short_help="Submit jobs to an HPC cluster using PyHPS.", help=""" Submit jobs to an HPC cluster using PyHPS. @@ -55,16 +53,35 @@ "--url", default=None, type=str, - help="""URL where the HPS cluster is deployed. For example: "https://myserver:3000/hps" """, + help="""URL where the HPS cluster is deployed. For example: "https://myserver:3000/hps". +If it is not input, there is a chain of places where PyMAPDL looks for an URL. +First, it checks if the URL is given in the file specified by the argument ``--config_file``. +If that file does not have an URL or does not exist, then it checks the default user credentials stored with ``pymapdl login --default`` CLI command. +If no URL is found, an exception is raised.""", ) @click.option( - "--user", default=None, type=str, help="Username for logging into the HPC cluster." + "--user", + default=None, + type=str, + help="""Username for logging into the HPC cluster. +If it is not input, there is a chain of places where PyMAPDL looks for an username. +First, it checks if the username is given in the file specified by the argument ``--config_file``. +If that file does not have an username or does not exist, then it checks the username configured using ``pymapdl login`` CLI command, for the given HPS cluster URL. +If there is no user credential stored for that HPS cluster URL, then it checks the default user credentials stored with ``pymapdl login --default`` CLI command. +If no user is found, an exception is raised. +""", ) @click.option( "--password", default=None, type=str, - help="Password for logging into the HPC cluster.", + help="""Password for logging into the HPC cluster. +If it is not input, there is a chain of places where PyMAPDL looks for a password. +First, it checks if the password is given in the file specified by the argument ``--config_file``. +If that file does not have a password or does not exist, then it checks the password configured using ``pymapdl login`` CLI command, for the given HPS cluster URL. +If there is no user credential stored for that HPS cluster URL, then it checks the default user credentials stored with ``pymapdl login --default`` CLI command. +If no password is found, an exception is raised. +""", ) @click.option( "--python", @@ -235,7 +252,7 @@ def submit( mode: Optional[Union["python", "shell", "apdl"]] = None, to_json: Optional[bool] = False, ): - import json + from ansys.mapdl.core.hpc.login import get_default_url, get_token_access if to_json: import json @@ -254,11 +271,25 @@ def submit( if config_file is None: config_file = os.path.join(os.getcwd(), "hps_config.json") + if not os.path.exists(config_file): + config_file = None logger.debug(f"Using default HPS configuration file: {config_file}") - url = get_value_from_json_or_default(url, config_file, "url", None) - user = get_value_from_json_or_default(user, config_file, "user", None) - password = get_value_from_json_or_default(password, config_file, "password", None) + # Getting cluster login configuration from CLI or file + url = get_value_from_json_or_default( + url, config_file, "url", None, raise_if_none=False + ) + url = url or get_default_url() # using default URL stored. + + # allow retrieving user from the configuration + user = get_value_from_json_or_default( + user, config_file, "user", raise_if_none=False + ) + + # Getting access token + token = get_token_access(url, user, password) + + # Getting other configuration from CLI or file python = get_value_from_json_or_default(python, config_file, "python", 3) name = get_value_from_json_or_default(name, config_file, "name", "My PyMAPDL job") @@ -276,8 +307,7 @@ def submit( job = PyMAPDLJobSubmission( url=url, - user=user, - password=password, + token=token, main_file=main_file, mode=mode, inputs=inputs, diff --git a/src/ansys/mapdl/core/cli/list_instances.py b/src/ansys/mapdl/core/cli/list_instances.py index a98c72ec89..f9ebf4ab25 100644 --- a/src/ansys/mapdl/core/cli/list_instances.py +++ b/src/ansys/mapdl/core/cli/list_instances.py @@ -28,6 +28,7 @@ @main.command( short_help="List MAPDL running instances.", help="""This command list MAPDL instances""", + name="list", ) @click.option( "--instances", diff --git a/src/ansys/mapdl/core/cli/login.py b/src/ansys/mapdl/core/cli/login.py new file mode 100644 index 0000000000..4e9e1e9fc7 --- /dev/null +++ b/src/ansys/mapdl/core/cli/login.py @@ -0,0 +1,284 @@ +# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Login into a PyHPS cluster""" +from getpass import getpass +import logging +from typing import Optional + +import click + +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger() + + +@click.command( + name="login", + short_help="Login into an HPS cluster.", + help="""Login into an HPS cluster. + +It does store credentials (cluster url, password and username) in the OS credential manager. +If you want to change any credential, just issue the command again with the new values. + +Examples +-------- + +Prompt the values for user, password and HPC cluster URL: + +$ pymapdl login +Username: myuser +Password: mypassword +HPS cluster URL: https://123.456.789.1:3000/hps +Stored credentials: + User : 'user' + Cluster URL : 'https://123.456.789.1:3000/hps' + +Use the CLI arguments to supply the values + +$ pymapdl login --user myuser --password mypassword --url "https://123.456.789.1:3000/hps" +Stored credentials: + User : 'user' + Cluster URL : 'https://123.456.789.1:3000/hps' + +Set the defaults user, password and URL. They will be used when one of them is +missing. + +$ pymapdl login --default --user myuser --password mypassword --url "https://123.456.789.1:3000/hps" +Stored default credentials. + +It is possible to input some arguments using the CLI arguments, and other using +the prompt: + +$ pymapdl login --user myuser --url "https://123.456.789.1:3000/hps" +Password: mypassword +Stored credentials: + User : 'user' + Cluster URL : 'https://123.456.789.1:3000/hps' + +""", +) +@click.option("--user", default=None, type=str, help="The username to login.") +@click.option("--password", default=None, type=str, help="The password to login.") +@click.option( + "--url", + default=None, + type=str, + help="The HPS cluster URL. For instance 'https://123.456.789.1:3000/hps'.", +) +@click.option( + "--default", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Set the default user, password and URL. These credentials are not tested against any HPC.""", +) +@click.option( + "--test_token", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Test if the token is valid. This argument is ignored if '--default' argument is ``True``.""", +) +@click.option( + "--quiet", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Suppress all console printout.""", +) +@click.option( + "--debug", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Activate debugging printout. It might show the input password!""", +) +def login( + user: Optional[str] = None, + password: Optional[str] = None, + url: Optional[str] = None, + default: bool = False, + test_token: bool = False, + quiet: bool = False, + debug: bool = False, +): + from ansys.mapdl.core.hpc.login import login_in_cluster, store_credentials + + if debug: + logger.setLevel(logging.DEBUG) + + if quiet: + import urllib3 + + urllib3.disable_warnings() + + logger.debug("Storing non-default credentials.") + if not user: + user = click.prompt("Username") + + if not user: + raise ValueError("No user was provided.") + + if not password: + password = getpass("Password: ") + if not password: + raise ValueError("No password was provided.") + + if not default and not url: + url = click.prompt("HPS cluster URL") + if not url: + raise ValueError("No password was provided.") + + token = login_in_cluster(user, password, url) + logger.debug(f"Login successful") + + if test_token: + logger.debug("Testing token") + from requests import ConnectionError + + from ansys.mapdl.core.hpc.login import token_is_valid + + if not token_is_valid(url, token): + raise ConnectionError("The retrieved token is not valid.") + else: + if not quiet: + click.echo("Token has been verified with the HPC cluster.") + + logger.info(f"Stored credentials: {user}, {password}, {url}") + store_credentials(user, password, url, default=default) + + if not quiet: + if default: + click.echo("Stored default credentials.") + else: + click.echo( + f"Stored credentials:\n User : '{user}'\n Cluster URL : '{url}'" + ) + + +@click.command( + short_help="Logout from an HPS cluster.", + help="""Logout from an HPS cluster. + +It deletes credentials stored on the system. + +Examples +-------- + +Delete the credentials associated to an specific URL + +$ pymapdl logout --url "https://123.456.789.1:3000/hps" +The HPS cluster 'https://123.456.789.1:3000/hps' credentials have been deleted. + +Delete the default credentials. + +$ pymapdl logout --default +The default credentials have been deleted. + +Notes +----- +- If the credentials do not exist, the CLI notifies and exits cleanly. + No exception is raised. If you want to raise an exception (exit 1), then pass + the argument ``--strict``. +""", +) +@click.option( + "--url", + default=None, + type=str, + help="The HPS cluster URL. For instance 'https://10.231.106.1:3000/hps'.", +) +@click.option( + "--default", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Deletes the default login configuration.""", +) +@click.option( + "--quiet", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Suppress all console printout.""", +) +@click.option( + "--debug", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Activate debugging printout.""", +) +@click.option( + "--strict", + default=False, + type=bool, + is_flag=False, + flag_value=True, + help="""Raise an issue if the credentials do not exist.""", +) +def logout(url, default, quiet, debug, strict): + + # TODO: keyrings library seems to not being able to list the credentials + # under a service name. We might need to keep track of those in a file or + # something. + + import keyring + + from ansys.mapdl.core.hpc.login import delete_credentials + + if debug: + logger.setLevel(logging.DEBUG) + + if not url and not default: + raise ValueError("An URL needs to be used.") + + if url and default: + raise ValueError("The argument '--default' cannot be used with an URL.") + + if default: + logger.debug("Deleting credentials for the default profile.") + url = None + + try: + delete_credentials(url) + success_message = "The {0} credentials have been deleted.".format( + "default" if default else f"HPS cluster '{url}'" + ) + except keyring.errors.PasswordDeleteError: + success_message = "The {0} credentials do not exist.".format( + "default" if default else f"HPS cluster '{url}'" + ) + if strict: + raise keyring.errors.PasswordDeleteError(success_message) + + if not quiet: + click.echo(success_message) diff --git a/src/ansys/mapdl/core/hpc/login.py b/src/ansys/mapdl/core/hpc/login.py new file mode 100644 index 0000000000..ccee2a5ab3 --- /dev/null +++ b/src/ansys/mapdl/core/hpc/login.py @@ -0,0 +1,348 @@ +# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import logging +import time +from typing import Optional + +try: + from ansys.hps.client import AuthApi, Client + from ansys.hps.client.authenticate import authenticate + import keyring + from requests import ConnectionError +except ModuleNotFoundError: + raise ModuleNotFoundError( + """Some of the dependencies required for login into an HPS cluster are not installed. +Please install them using "pip install 'ansys-mapdl-core[hps]".""" + ) + +DEFAULT_IDENTIFIER = "defaultconfig" +SERVICE_NAME = "pymapdl-pyhps" +EXPIRATION_TIME = 4 * 24 * 60 # 2 days in minutes + +logger = logging.getLogger() + + +def get_password(*args, **kwargs): + logger.debug(f"Getting password from service '{args[0]}' and key '{args[1]}'") + return keyring.get_password(*args, **kwargs) + + +def set_password(*args, **kwargs): + logger.debug(f"Setting password from service '{args[0]}' and key '{args[1]}'") + return keyring.set_password(*args, **kwargs) + + +def login_in_cluster(user, password, url): + """ + Authenticate with the server and return the access token. + + Parameters + ---------- + user : str + Username. + password : str + Password. + url : str + URL. + + Returns + ------- + str + Access token. + """ + logger.debug(f"Authenticating on cluster '{url}' using user '{user}'.") + access_token = authenticate( + url=url, username=user, password=password, scope="openid", verify=False + )["access_token"] + return access_token + + +def store_credentials( + user: str = None, + password: str = None, + url: str = None, + default=False, + expiration_time: float = EXPIRATION_TIME, +): + """ + Store user credentials and the current timestamp in the keyring. + + If ``default`` argument is ``True``, you can store a default password, + default user, or/and default URL. + + Parameters + ---------- + user : str, optional + Username. + password : str, optional + Password + url : str, optional + URL of the HPS cluster + + """ + if default: + identifier = DEFAULT_IDENTIFIER + else: + identifier = url + logger.debug(f"Using identifier: '{identifier}'") + + if not default and (not url or not user or not password): + raise ValueError( + "To store non-default credentials, an URL, an user and a password are needed." + ) + + if url: + set_password(SERVICE_NAME, f"{identifier}_url", url) + if user: + set_password(SERVICE_NAME, f"{identifier}_user", user) + if password: + set_password(SERVICE_NAME, f"{identifier}_password", password) + if expiration_time: + set_password( + SERVICE_NAME, f"{identifier}_expiration_time", str(expiration_time) + ) + + set_password(SERVICE_NAME, f"{identifier}_timestamp", str(time.time())) + + +def get_stored_credentials(identifier: str): + """ + Retrieve stored credentials, timestamp and expiration time from the keyring. + + Parameters + ---------- + identifier: str + Identifier for the credentials + + Returns + ------- + tuple + (user, password, timestamp) or (None, None, None) if not found + """ + logger.debug(f"Retrieving info for '{identifier}'") + url = get_password(SERVICE_NAME, f"{identifier}_url") + user = get_password(SERVICE_NAME, f"{identifier}_user") + password = get_password(SERVICE_NAME, f"{identifier}_password") + timestamp = get_password(SERVICE_NAME, f"{identifier}_timestamp") + expiration_time = get_password(SERVICE_NAME, f"{identifier}_expiration_time") + + if timestamp: + timestamp = float(timestamp) + if expiration_time: + expiration_time = float(expiration_time) + + logger.debug( + f"Retrieved info for '{identifier}': {url}, {user}, {password}, {timestamp}, {expiration_time} " + ) + return url, user, password, timestamp, expiration_time + + +def credentials_expired(timestamp: float, expiration_time: float = EXPIRATION_TIME): + """ + Check if the stored credentials have expired. + + Parameters + ---------- + timestamp, float + Timestamp of when the credentials were stored + + expiration_time float + Amount of time before the credentials expires. + + Returns + ------- + bool + True if credentials have expired, False otherwise + """ + return time.time() - timestamp > expiration_time * 60 + + +def delete_credentials(identifier: Optional[str] = None): + """ + Delete stored credentials. + + Parameters + ---------- + identifier: str, Optional + Identifier for the credentials. If it is ``None``, the + default credentials are deleted. + + Returns + ------- + None + """ + if not identifier: + identifier = DEFAULT_IDENTIFIER + + logger.debug(f"Deleting credentials for identifier: {identifier}") + + keyring.delete_password(SERVICE_NAME, f"{identifier}_url") + keyring.delete_password(SERVICE_NAME, f"{identifier}_user") + keyring.delete_password(SERVICE_NAME, f"{identifier}_password") + keyring.delete_password(SERVICE_NAME, f"{identifier}_timestamp") + keyring.delete_password(SERVICE_NAME, f"{identifier}_expiration_time") + + +def token_is_valid(url, token): + """Check if a token is valid. + + The validation is performed by requesting the number of users to the HPS cluster. + + Parameters + ---------- + url : str + HPS cluster URL. + token : str + Authentication token. + + Returns + ------- + bool + Whether the token is valid or not. + """ + client = Client(url=url, access_token=token, verify=False) + auth_api = AuthApi(client) + + try: + auth_api.get_users() + return True + except ConnectionError: + return False + except Exception as e: + raise e + + +def get_token_access(url: str = None, user: str = None, password: str = None): + """ + Access an HPS cluster by logging in with the provided or stored credentials. + + This function attempts to log in to a cluster using the provided URL, username, + and password. + If any of these parameters are not provided, it attempts to retrieve stored + credentials associated with the given URL. + If no URL is provided, then it retrieves the default credentials. + + If the credentials are expired or not found, appropriate errors are raised. + + Parameters + ---------- + url : str, optional + The URL of the cluster to log in to. If not provided, a stored URL + associated with the default or given identifier is used. + user : str, optional + The username for logging in. If not provided, a stored username associated + with the default or given identifier is used. + password : str, optional + The password for logging in. If not provided, a stored password associated + with the default or given identifier is used. + + Returns + ------- + str + It returns the authentication token for the session. + + Raises + ------ + ConnectionError + If there are no stored credentials for the given identifier, or if the stored credentials + are expired. + ValueError + If a URL, username, or password is not provided and cannot be found in the stored + credentials. + + Notes + ----- + - If credentials are expired, they are deleted from storage. + - The credentials can be stored using ``pymapdl login`` CLI. + + Examples + -------- + Using url, user and password: + + >>> get_token_access(url='https://cluster.example.com', user='admin', password='securepass') + 'eyJhbGciOiJSUzI1NiIsI...' + + Using the stored credential for that URL. If those credentials do not exists, + the default credentials are used. + + >>> get_token_access(url='https://cluster.example.com') + 'bGciOiJSeyJhUzI1NiIsI...' + + Login using the default stored credentials: + >>> get_token_access() + 'iJSeyJhUzI1bGciONiIsI...' + """ + if not url or not user or not password: + if not url: + identifier = DEFAULT_IDENTIFIER + else: + identifier = url + + ( + url_default, + user_default, + password_default, + timestamp_default, + expiration_default, + ) = get_stored_credentials(identifier=identifier) + + if not url_default or not user_default or not password_default: + raise ConnectionError( + f"There are no credentials stored for '{identifier}'." + ) + + if credentials_expired(timestamp_default, expiration_time=expiration_default): + delete_credentials(identifier) + + raise ConnectionError(f"The stored '{identifier}' credentials are expired.") + + if not url: + if url_default: + url = url_default + else: + raise ValueError( + f"No 'URL' is given nor stored for '{identifier}'. You must input one." + ) + + if not user: + if user_default: + user = user_default + else: + raise ValueError( + f"No 'user' is given nor stored for '{identifier}'. You must input one." + ) + + if not password: + if password_default: + password = password_default + else: + raise ValueError( + f"No 'password' is given nor stored for '{identifier}'. You must input one." + ) + + return login_in_cluster(user=user, password=password, url=url) + + +def get_default_url(): + """Return the default credentials URL""" + return get_password(SERVICE_NAME, f"{DEFAULT_IDENTIFIER}_url") diff --git a/src/ansys/mapdl/core/hpc/pyhps.py b/src/ansys/mapdl/core/hpc/pyhps.py index 6cf0de8787..457a83950d 100644 --- a/src/ansys/mapdl/core/hpc/pyhps.py +++ b/src/ansys/mapdl/core/hpc/pyhps.py @@ -28,34 +28,45 @@ from typing import Any, Optional, Union from warnings import warn -from ansys.hps.client import Client -from ansys.hps.client.jms import ( - File, - FloatParameterDefinition, - HpcResources, - JmsApi, - Job, - JobDefinition, - ParameterMapping, - Project, - ProjectApi, - ResourceRequirements, - Software, - StringParameterDefinition, - TaskDefinition, -) +try: + from ansys.hps.client import Client + from ansys.hps.client.jms import ( + File, + FloatParameterDefinition, + HpcResources, + JmsApi, + Job, + JobDefinition, + ParameterMapping, + Project, + ProjectApi, + ResourceRequirements, + Software, + StringParameterDefinition, + TaskDefinition, + ) + +except ModuleNotFoundError: + raise ModuleNotFoundError( + """Some of the dependencies required for submit jobs into an HPS cluster are not installed. +Please install them using "pip install 'ansys-mapdl-core[hps]".""" + ) logger = logging.getLogger() def get_value_from_json_or_default( - arg: str, json_file: str, key: str, default_value: Optional[Union[str, Any]] = None + arg: str, + json_file: Optional[str], + key: str, + default_value: Optional[Union[str, Any]] = None, + raise_if_none: Optional[bool] = True, ): if arg is not None: logger.debug(f"Using '{arg}' for {key}") return arg - if os.path.exists(json_file): + if json_file and os.path.exists(json_file): if os.path.getsize(json_file) > 0: with open(json_file, "r") as fid: config = json.load(fid) @@ -64,7 +75,7 @@ def get_value_from_json_or_default( logger.debug(f"Using '{config[key]}' for {key}") return config[key] - if default_value is None: + if default_value is None and raise_if_none: raise ValueError( f"The argument {arg} is not given through the CLI or config file." ) @@ -106,10 +117,11 @@ class JobSubmission: def __init__( self, - url, - user, - password, main_file, + url, + user: Optional[str] = None, + password: Optional[str] = None, + token: Optional[str] = None, mode: Optional[str] = None, inputs: Optional[Union[list[str]]] = None, outputs: Optional[Union[list[str]]] = None, @@ -125,9 +137,15 @@ def __init__( max_execution_time: Optional[int] = None, name: Optional[str] = None, ): + + if not token and (not user or not password): + raise ValueError("An access token or an user-password pair must be used.") + self._url = url self._user = user self._password = password + self._token = token + self._main_file = self._validate_main_file(main_file) self._mode = self._validate_mode(mode) @@ -622,7 +640,6 @@ def _create_task(self, file_input_ids, file_output_ids): executable=os.path.basename(executable) ) - print(f"Using executable: '{execution_command}'") logger.debug(f"Using executable: '{execution_command}'") # Process step @@ -631,6 +648,7 @@ def _create_task(self, file_input_ids, file_output_ids): TaskDefinition( execution_command=execution_command, resource_requirements=ResourceRequirements( + platform="linux", num_cores=self.num_cores, memory=self.memory * 1024 * 1024, disk_space=self.disk_space * 1024 * 1024, @@ -816,7 +834,8 @@ def _add_files(self): self.output_files.append(self._output_parms_file) if self.mode == "python": - self.input_files.append(self.requirements_file) + if self._requirements_file is not False: + self.input_files.append(self.requirements_file) self.input_files.append(self.shell_file) if self.mode == "python" and (self.inputs or self.outputs): @@ -831,14 +850,18 @@ def _add_files(self): def _prepare_shell_file(self): content = f""" echo "Starting" +""" + if self._requirements_file: + content += f""" # Start venv python{self.python} -m venv .venv source .venv/bin/activate # Install requirements pip install -r {os.path.basename(self.requirements_file)} - +""" + content += f""" # Run script python {self._executed_pyscript} """ @@ -847,6 +870,10 @@ def _prepare_shell_file(self): logger.debug(f"Shell file in: {self.shell_file}") def _prepare_requirements_file(self): + + if self._requirements_file is False: + return + import pkg_resources content = "\n".join( @@ -938,8 +965,17 @@ def _load_results(self): self._output_values.append(each_job.values) def _connect_client(self): + if not self._token: + logger.debug("Getting a valid token") + from ansys.mapdl.core.hpc.login import get_token_access + + self._token = get_token_access( + url=self.url, user=self.user, password=self.password + ) + + logger.debug("Using a token to authenticate the user.") self._client: Client = Client( - url=self.url, username=self.user, password=self.password, verify=False + url=self._url, access_token=self._token, verify=False ) def close_client(self):