Skip to content

Commit 403c217

Browse files
committed
verdi computer setup-many
1 parent 5202d67 commit 403c217

File tree

3 files changed

+115
-204
lines changed

3 files changed

+115
-204
lines changed

docs/source/reference/command_line.rst

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,18 @@ Below is a list with all available subcommands.
100100
--help Show this message and exit.
101101
102102
Commands:
103-
configure Configure the transport for a computer and user.
104-
delete Delete a computer.
105-
disable Disable the computer for the given user.
106-
duplicate Duplicate a computer allowing to change some parameters.
107-
enable Enable the computer for the given user.
108-
export Export the setup or configuration of a computer.
109-
list List all available computers.
110-
relabel Relabel a computer.
111-
setup Create a new computer.
112-
show Show detailed information for a computer.
113-
test Test the connection to a computer.
103+
configure Configure the transport for a computer and user.
104+
delete Delete a computer.
105+
disable Disable the computer for the given user.
106+
duplicate Duplicate a computer allowing to change some parameters.
107+
enable Enable the computer for the given user.
108+
export Export the setup or configuration of a computer.
109+
list List all available computers.
110+
relabel Relabel a computer.
111+
setup Create a new computer.
112+
setup-many Create multiple computers from YAML configuration files.
113+
show Show detailed information for a computer.
114+
test Test the connection to a computer.
114115
115116
116117
.. _reference:command-line:verdi-config:

src/aiida/cmdline/commands/cmd_computer.py

Lines changed: 95 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,39 @@ def computer_setup(ctx, non_interactive, **kwargs):
321321
echo.echo_report(f' verdi -p {profile.name} computer configure {computer.transport_type} {computer.label}')
322322

323323

324+
@verdi_computer.command('setup-many')
325+
@click.argument('config_files', nargs=-1, required=True, type=click.Path(exists=True, path_type=pathlib.Path))
326+
@with_dbenv()
327+
def computer_setup_many(config_files):
328+
"""Create multiple computers from YAML configuration files."""
329+
import yaml
330+
331+
from aiida.common.exceptions import IntegrityError
332+
from aiida.orm.utils.builders.computer import ComputerBuilder
333+
334+
for config_path in config_files:
335+
try:
336+
with open(config_path, 'r', encoding='utf-8') as f:
337+
config_data = yaml.safe_load(f)
338+
339+
computer_builder = ComputerBuilder(**config_data)
340+
computer = computer_builder.new()
341+
computer.store()
342+
343+
echo.echo_success(f'Computer<{computer.pk}> {computer.label} created')
344+
except IntegrityError as e:
345+
if 'UNIQUE constraint failed: db_dbcomputer.label' in str(e):
346+
msg = (
347+
f'Error processing {config_path}: Computer with label "{config_data.get("label", "unknown")}"'
348+
'already exists'
349+
)
350+
echo.echo_error(msg)
351+
else:
352+
echo.echo_error(f'Error processing {config_path}: Database integrity error - {e}')
353+
except Exception as e:
354+
echo.echo_error(f'Error processing {config_path}: {e}')
355+
356+
324357
@verdi_computer.command('duplicate')
325358
@arguments.COMPUTER(callback=set_computer_builder)
326359
@options_computer.LABEL(contextual_default=partial(get_parameter_default, 'label'))
@@ -744,222 +777,91 @@ def computer_export():
744777

745778

746779
@computer_export.command('setup')
747-
@click.argument('computers_and_output', nargs=-1, required=False)
748-
@click.option('-a', '--all', 'export_all', is_flag=True, help='Export all computers.')
780+
@arguments.COMPUTER()
781+
@arguments.OUTPUT_FILE(type=click.Path(exists=False, path_type=pathlib.Path), required=False)
749782
@options.OVERWRITE()
750783
@options.SORT()
751784
@with_dbenv()
752-
def computer_export_setup(computers_and_output, export_all, overwrite, sort):
753-
"""Export computer setup(s) to YAML file(s).
754-
755-
Usage:
756-
- verdi computer export setup COMPUTER_ID [COMPUTER_ID ...] [OUTPUT_FILE]
757-
- verdi computer export setup --all
758-
759-
If no output file is given, default names are created based on the computer labels.
760-
Custom output filename can only be provided when exporting a single computer.
761-
"""
785+
def computer_export_setup(computer, output_file, overwrite, sort):
786+
"""Export computer setup to a YAML file."""
762787
import yaml
763788

764-
from aiida import orm
765-
from aiida.common import NotExistent
766-
from aiida.orm import load_computer
767-
768-
# Handle --all option
769-
if export_all:
770-
if computers_and_output:
771-
echo.echo_critical('Cannot specify both --all and individual computers.')
772-
773-
# Get all computers
774-
query = orm.QueryBuilder()
775-
query.append(orm.Computer)
776-
computers = [computer for [computer] in query.all()]
777-
if not computers:
778-
echo.echo_report('No computers found in the database.')
779-
return
780-
output_file = None
781-
else:
782-
# Parse computers and potential output file
783-
if not computers_and_output:
784-
echo.echo_critical('Must specify either --all or individual computer identifiers.')
785-
786-
computers = []
787-
output_file = None
788-
789-
# Try to parse all arguments as computers first
790-
for i, arg in enumerate(computers_and_output):
791-
try:
792-
computer = load_computer(arg)
793-
computers.append(computer)
794-
except NotExistent:
795-
# This argument is not a valid computer identifier
796-
if i == len(computers_and_output) - 1:
797-
# Last argument and not a computer - treat as output file
798-
output_file = pathlib.Path(arg)
799-
break
800-
else:
801-
# Not last argument and not a computer - error
802-
echo.echo_critical(f'Invalid computer identifier: {arg}')
803-
804-
if not computers:
805-
echo.echo_critical('No valid computer identifiers provided.')
806-
807-
# Validate output file usage
808-
if output_file and len(computers) > 1:
809-
msg = 'Custom output filename can only be provided if a single computer is being exported.'
810-
raise click.BadParameter(msg)
811-
812-
# Export each computer
813-
for computer in computers:
814-
computer_setup = {
815-
'label': computer.label,
816-
'hostname': computer.hostname,
817-
'description': computer.description,
818-
'transport': computer.transport_type,
819-
'scheduler': computer.scheduler_type,
820-
'shebang': computer.get_shebang(),
821-
'work_dir': computer.get_workdir(),
822-
'mpirun_command': ' '.join(computer.get_mpirun_command()),
823-
'mpiprocs_per_machine': computer.get_default_mpiprocs_per_machine(),
824-
'default_memory_per_machine': computer.get_default_memory_per_machine(),
825-
'use_double_quotes': computer.get_use_double_quotes(),
826-
'prepend_text': computer.get_prepend_text(),
827-
'append_text': computer.get_append_text(),
828-
}
829-
830-
# Determine the output file for this specific computer
831-
if output_file is None:
832-
current_output_file = pathlib.Path(f'{computer.label}-setup.yaml')
833-
else:
834-
current_output_file = output_file
789+
computer_setup = {
790+
'label': computer.label,
791+
'hostname': computer.hostname,
792+
'description': computer.description,
793+
'transport': computer.transport_type,
794+
'scheduler': computer.scheduler_type,
795+
'shebang': computer.get_shebang(),
796+
'work_dir': computer.get_workdir(),
797+
'mpirun_command': ' '.join(computer.get_mpirun_command()),
798+
'mpiprocs_per_machine': computer.get_default_mpiprocs_per_machine(),
799+
'default_memory_per_machine': computer.get_default_memory_per_machine(),
800+
'use_double_quotes': computer.get_use_double_quotes(),
801+
'prepend_text': computer.get_prepend_text(),
802+
'append_text': computer.get_append_text(),
803+
}
835804

836-
try:
837-
validate_output_filename(output_file=current_output_file, overwrite=overwrite)
838-
except (FileExistsError, IsADirectoryError) as exception:
839-
raise click.BadParameter(str(exception), param_hint='OUTPUT_FILE') from exception
805+
if output_file is None:
806+
output_file = pathlib.Path(f'{computer.label}-setup.yaml')
807+
try:
808+
validate_output_filename(output_file=output_file, overwrite=overwrite)
809+
except (FileExistsError, IsADirectoryError) as exception:
810+
raise click.BadParameter(str(exception), param_hint='OUTPUT_FILE') from exception
840811

841-
try:
842-
current_output_file.write_text(yaml.dump(computer_setup, sort_keys=sort), 'utf-8')
843-
except Exception as e:
844-
error_traceback = traceback.format_exc()
845-
echo.CMDLINE_LOGGER.debug(error_traceback)
846-
echo.echo_critical(
847-
f'Unexpected error while exporting setup for Computer<{computer.pk}> {computer.label}:\n ({e!s}).'
848-
)
849-
else:
850-
echo.echo_success(
851-
f"Computer<{computer.pk}> {computer.label} setup exported to file '{current_output_file}'."
852-
)
812+
try:
813+
output_file.write_text(yaml.dump(computer_setup, sort_keys=sort), 'utf-8')
814+
except Exception as e:
815+
error_traceback = traceback.format_exc()
816+
echo.CMDLINE_LOGGER.debug(error_traceback)
817+
echo.echo_critical(
818+
f'Unexpected error while exporting setup for Computer<{computer.pk}> {computer.label}:\n ({e!s}).'
819+
)
820+
else:
821+
echo.echo_success(f"Computer<{computer.pk}> {computer.label} setup exported to file '{output_file}'.")
853822

854823

855824
@computer_export.command('config')
856-
@click.argument('computers_and_output', nargs=-1, required=False)
857-
@click.option('-a', '--all', 'export_all', is_flag=True, help='Export all computers.')
825+
@arguments.COMPUTER()
826+
@arguments.OUTPUT_FILE(type=click.Path(exists=False, path_type=pathlib.Path), required=False)
858827
@options.USER(
859828
help='Email address of the AiiDA user from whom to export this computer (if different from default user).'
860829
)
861830
@options.OVERWRITE()
862831
@options.SORT()
863832
@with_dbenv()
864-
def computer_export_config(computers_and_output, export_all, user, overwrite, sort):
865-
"""Export computer transport configuration(s) for a user to YAML file(s).
866-
867-
Usage:
868-
- verdi computer export config COMPUTER_ID [COMPUTER_ID ...] [OUTPUT_FILE]
869-
- verdi computer export config --all
870-
871-
If no output file is given, default names are created based on the computer labels.
872-
Custom output filename can only be provided when exporting a single computer.
873-
"""
833+
def computer_export_config(computer, output_file, user, overwrite, sort):
834+
"""Export computer transport configuration for a user to a YAML file."""
874835
import yaml
875836

876-
from aiida import orm
877-
from aiida.common import NotExistent
878-
from aiida.orm import load_computer
879-
880-
# Handle --all option
881-
if export_all:
882-
if computers_and_output:
883-
echo.echo_critical('Cannot specify both --all and individual computers.')
884-
885-
# Get all configured computers
886-
query = orm.QueryBuilder()
887-
query.append(orm.Computer)
888-
all_computers = [computer for [computer] in query.all()]
889-
computers = [comp for comp in all_computers if comp.is_configured]
890-
if not computers:
891-
echo.echo_report('No configured computers found in the database.')
892-
return
893-
output_file = None
837+
if not computer.is_configured:
838+
echo.echo_critical(
839+
f'Computer<{computer.pk}> {computer.label} configuration cannot be exported,'
840+
' because computer has not been configured yet.'
841+
)
894842
else:
895-
# Parse computers and potential output file
896-
if not computers_and_output:
897-
echo.echo_critical('Must specify either --all or individual computer identifiers.')
898-
899-
computers = []
900-
output_file = None
901-
902-
# Try to parse all arguments as computers first
903-
for i, arg in enumerate(computers_and_output):
904-
try:
905-
computer = load_computer(arg)
906-
computers.append(computer)
907-
except NotExistent:
908-
# This argument is not a valid computer identifier
909-
if i == len(computers_and_output) - 1:
910-
# Last argument and not a computer - treat as output file
911-
output_file = pathlib.Path(arg)
912-
break
913-
else:
914-
# Not last argument and not a computer - error
915-
echo.echo_critical(f'Invalid computer identifier: {arg}')
916-
917-
if not computers:
918-
echo.echo_critical('No valid computer identifiers provided.')
919-
920-
# Validate output file usage
921-
if output_file and len(computers) > 1:
922-
msg = 'Custom output filename can only be provided if a single computer is being exported.'
923-
raise click.BadParameter(msg)
924-
925-
# Export each computer
926-
for computer in computers:
927-
if not computer.is_configured:
928-
echo.echo_warning(
929-
f'Computer<{computer.pk}> {computer.label} configuration cannot be exported,'
930-
' because computer has not been configured yet. Skipping.'
931-
)
932-
continue
933-
934-
# Determine the output file for this specific computer
935843
if output_file is None:
936-
current_output_file = pathlib.Path(f'{computer.label}-config.yaml')
937-
else:
938-
current_output_file = output_file
939-
844+
output_file = pathlib.Path(f'{computer.label}-config.yaml')
940845
try:
941-
validate_output_filename(output_file=current_output_file, overwrite=overwrite)
846+
validate_output_filename(output_file=output_file, overwrite=overwrite)
942847
except (FileExistsError, IsADirectoryError) as exception:
943848
raise click.BadParameter(str(exception), param_hint='OUTPUT_FILE') from exception
944849

945-
try:
946-
computer_configuration = computer.get_configuration(user)
947-
current_output_file.write_text(yaml.dump(computer_configuration, sort_keys=sort), 'utf-8')
948-
949-
except Exception as exception:
950-
error_traceback = traceback.format_exc()
951-
echo.CMDLINE_LOGGER.debug(error_traceback)
952-
if user is None:
953-
echo.echo_critical(
954-
'Unexpected error while exporting configuration for '
955-
f'Computer<{computer.pk}> {computer.label}: {exception!s}.'
956-
)
957-
else:
958-
echo.echo_critical(
959-
f'Unexpected error while exporting configuration for Computer<{computer.pk}> {computer.label}'
960-
f' and User<{user.pk}> {user.email}: {exception!s}.'
961-
)
850+
try:
851+
computer_configuration = computer.get_configuration(user)
852+
output_file.write_text(yaml.dump(computer_configuration, sort_keys=sort), 'utf-8')
853+
854+
except Exception as exception:
855+
error_traceback = traceback.format_exc()
856+
echo.CMDLINE_LOGGER.debug(error_traceback)
857+
if user is None:
858+
echo.echo_critical(
859+
f'Unexpected error while exporting configuration for Computer<{computer.pk}> {computer.label}: {exception!s}.' # noqa: E501
860+
)
962861
else:
963-
echo.echo_success(
964-
f'Computer<{computer.pk}> {computer.label} configuration exported to file `{current_output_file}`.'
862+
echo.echo_critical(
863+
f'Unexpected error while exporting configuration for Computer<{computer.pk}> {computer.label}'
864+
f' and User<{user.pk}> {user.email}: {exception!s}.'
965865
)
866+
else:
867+
echo.echo_success(f'Computer<{computer.pk}> {computer.label} configuration exported to file `{output_file}`.')

src/aiida/cmdline/params/options/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
'COMPUTER',
4848
'COMPUTERS',
4949
'CONFIG_FILE',
50+
'CONFIG_FILES',
5051
'DATA',
5152
'DATUM',
5253
'DB_BACKEND',
@@ -735,6 +736,13 @@ def set_log_level(ctx: click.Context, _param: click.Parameter, value: t.Any) ->
735736
help='Load option values from configuration file in yaml format (local path or URL).',
736737
)
737738

739+
CONFIG_FILES = click.option(
740+
'--config-files',
741+
multiple=True,
742+
type=click.Path(exists=True, path_type=pathlib.Path),
743+
help='Load computer setup from multiple configuration files in YAML format. Can be specified multiple times.',
744+
)
745+
738746
IDENTIFIER = OverridableOption(
739747
'-i',
740748
'--identifier',

0 commit comments

Comments
 (0)