Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying the ssh key for the tests and loosen fallbacks to default ssh key #6563

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/ci-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ jobs:
AIIDA_WARN_v3: 1
# Python 3.12 has a performance regression when running with code coverage
# so run code coverage only for python 3.9.
run: pytest -v tests -m 'not nightly' ${{ matrix.python-version == '3.9' && '--cov aiida' || '' }}
run: |
# this env needs to set in run and not env, because we need to access $HOME
export AIIDA_PYTEST_SSH_KEY=$HOME/.ssh/id_rsa_aiida_pytest
pytest -v tests -m 'not nightly' ${{ matrix.python-version == '3.9' && '--cov aiida' || '' }}

- name: Upload coverage report
if: matrix.python-version == 3.9 && github.repository == 'aiidateam/aiida-core'
Expand Down Expand Up @@ -139,8 +142,10 @@ jobs:
- name: Run test suite
env:
AIIDA_WARN_v3: 0
run: pytest -m 'presto'

run: |
# this env needs to set in run and not env, because we need to access $HOME
export AIIDA_PYTEST_SSH_KEY=$HOME/.ssh/id_rsa_aiida_pytest
pytest -m 'presto'

verdi:

Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/setup_ssh.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
#!/usr/bin/env bash
# Sets up ssh keys to allow a ssh connection to localhost. This is needed
# because localhost is used as remote address to run the tests locally.
set -ev

ssh-keygen -q -t rsa -b 4096 -N "" -f "${HOME}/.ssh/id_rsa"
ssh-keygen -y -f "${HOME}/.ssh/id_rsa" >> "${HOME}/.ssh/authorized_keys"
mkdir -p ${HOME}/.ssh
ssh-keygen -q -t rsa -b 4096 -N "" -f "${HOME}/.ssh/id_rsa_aiida_pytest"
ssh-keygen -y -f "${HOME}/.ssh/id_rsa_aiida_pytest" >> "${HOME}/.ssh/authorized_keys"
ssh-keyscan -H localhost >> "${HOME}/.ssh/known_hosts"
# to test core.ssh_auto transport plugin we need to append this to the config
cat <<EOT >> ${HOME}/.ssh/config
Host localhost
IdentityFile ${HOME}/.ssh/id_rsa_aiida_pytest
EOT


# The permissions on the GitHub runner are 777 which will cause SSH to refuse the keys and cause authentication to fail
chmod 755 "${HOME}"
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -430,12 +430,23 @@ deps =
py310: -rrequirements/requirements-py-3.10.txt
py311: -rrequirements/requirements-py-3.11.txt
py312: -rrequirements/requirements-py-3.12.txt
setenv =
AIIDA_PYTEST_SSH_KEY = $HOME/.ssh/id_rsa_aiida_pytest

[testenv:py{39,310,311,312}-presto]
passenv =
PYTHONASYNCIODEBUG
setenv =
AIIDA_WARN_v3 =
AIIDA_PYTEST_SSH_KEY = $HOME/.ssh/id_rsa_aiida_pytest
commands = pytest -m 'presto' {posargs}

[testenv:py{39,310,311,312}]
passenv =
PYTHONASYNCIODEBUG
setenv =
AIIDA_WARN_v3 =
AIIDA_PYTEST_SSH_KEY = $HOME/.ssh/id_rsa_aiida_pytest
commands = pytest {posargs}

[testenv:py{39,310,311,312}-verdi]
Expand Down
66 changes: 36 additions & 30 deletions src/aiida/manage/tests/pytest_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,44 +504,50 @@

@pytest.fixture(scope='session')
def ssh_key(tmp_path_factory) -> t.Generator[pathlib.Path, None, None]:
"""Generate a temporary SSH key pair for the test session and return the filepath of the private key.
"""Returns a SSH key for the test session.

If the environment variable ``AIIDA_PYTEST_SSH_KEY`` is set we take the key from this path otherwise we generate a
temporary SSH key pair for the test session and return the filepath of the private key.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
temporary SSH key pair for the test session and return the filepath of the private key.
temporary SSH key pair for the test session and return the filepath of the private key. The temporary key cannot be used to connect to localhost as it is not authorized, therefore it is required that the default ssh key `~/.ssh/id_rsa`is authorized to connect to localhost to automatically fallback.


The filepath of the public key is the same as the private key, but it adds the ``.pub`` file extension.
"""
from cryptography.hazmat.backends import default_backend as crypto_default_backend
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa

key = rsa.generate_private_key(
backend=crypto_default_backend(),
public_exponent=65537,
key_size=2048,
)
if (ssh_key_path := os.environ.get('AIIDA_PYTEST_SSH_KEY')) is not None:
yield pathlib.Path(ssh_key_path)

Check warning on line 515 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L514-L515

Added lines #L514 - L515 were not covered by tests
else:
from cryptography.hazmat.backends import default_backend as crypto_default_backend
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa

Check warning on line 519 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L517-L519

Added lines #L517 - L519 were not covered by tests

key = rsa.generate_private_key(

Check warning on line 521 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L521

Added line #L521 was not covered by tests
backend=crypto_default_backend(),
public_exponent=65537,
key_size=2048,
)

private_key = key.private_bytes(
crypto_serialization.Encoding.PEM,
crypto_serialization.PrivateFormat.PKCS8,
crypto_serialization.NoEncryption(),
)
private_key = key.private_bytes(

Check warning on line 527 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L527

Added line #L527 was not covered by tests
crypto_serialization.Encoding.PEM,
crypto_serialization.PrivateFormat.PKCS8,
crypto_serialization.NoEncryption(),
)

public_key = key.public_key().public_bytes(
crypto_serialization.Encoding.OpenSSH,
crypto_serialization.PublicFormat.OpenSSH,
)
public_key = key.public_key().public_bytes(

Check warning on line 533 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L533

Added line #L533 was not covered by tests
crypto_serialization.Encoding.OpenSSH,
crypto_serialization.PublicFormat.OpenSSH,
)

dirpath = tmp_path_factory.mktemp('keys')
filename = uuid.uuid4().hex
filepath_private_key = dirpath / filename
filepath_public_key = dirpath / f'{filename}.pub'
dirpath = tmp_path_factory.mktemp('keys')
filename = uuid.uuid4().hex
filepath_private_key = dirpath / filename
filepath_public_key = dirpath / f'{filename}.pub'

Check warning on line 541 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L538-L541

Added lines #L538 - L541 were not covered by tests

filepath_private_key.write_bytes(private_key)
filepath_public_key.write_bytes(public_key)
filepath_private_key.write_bytes(private_key)
filepath_public_key.write_bytes(public_key)

Check warning on line 544 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L543-L544

Added lines #L543 - L544 were not covered by tests

try:
yield filepath_private_key
finally:
filepath_private_key.unlink(missing_ok=True)
filepath_public_key.unlink(missing_ok=True)
try:
yield filepath_private_key

Check warning on line 547 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L546-L547

Added lines #L546 - L547 were not covered by tests
finally:
filepath_private_key.unlink(missing_ok=True)
filepath_public_key.unlink(missing_ok=True)

Check warning on line 550 in src/aiida/manage/tests/pytest_fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/manage/tests/pytest_fixtures.py#L549-L550

Added lines #L549 - L550 were not covered by tests


@pytest.fixture
Expand Down
83 changes: 46 additions & 37 deletions src/aiida/tools/pytest_fixtures/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,57 @@

@pytest.fixture(scope='session')
def ssh_key(tmp_path_factory) -> t.Generator[pathlib.Path, None, None]:
"""Generate a temporary SSH key pair for the test session and return the filepath of the private key.
"""Returns a SSH key for the test session.

If the environment variable ``AIIDA_PYTEST_SSH_KEY`` is set we take the key
from this path otherwise we generate a temporary SSH key pair for the test
session and return the filepath of the private key.

The filepath of the public key is the same as the private key, but it adds the ``.pub`` file extension.

:returns: The filepath of the generated private key.
"""
from uuid import uuid4

from cryptography.hazmat.backends import default_backend as crypto_default_backend
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa

key = rsa.generate_private_key(
backend=crypto_default_backend(),
public_exponent=65537,
key_size=2048,
)

private_key = key.private_bytes(
crypto_serialization.Encoding.PEM,
crypto_serialization.PrivateFormat.PKCS8,
crypto_serialization.NoEncryption(),
)

public_key = key.public_key().public_bytes(
crypto_serialization.Encoding.OpenSSH,
crypto_serialization.PublicFormat.OpenSSH,
)

dirpath = tmp_path_factory.mktemp('keys')
filename = uuid4().hex
filepath_private_key = dirpath / filename
filepath_public_key = dirpath / f'{filename}.pub'

filepath_private_key.write_bytes(private_key)
filepath_public_key.write_bytes(public_key)

try:
yield filepath_private_key
finally:
filepath_private_key.unlink(missing_ok=True)
filepath_public_key.unlink(missing_ok=True)
import os

if (ssh_key_path := os.environ.get('AIIDA_PYTEST_SSH_KEY')) is not None:
yield pathlib.Path(ssh_key_path)
else:
from uuid import uuid4

Check warning on line 31 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L31

Added line #L31 was not covered by tests

from cryptography.hazmat.backends import default_backend as crypto_default_backend
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa

Check warning on line 35 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L33-L35

Added lines #L33 - L35 were not covered by tests

key = rsa.generate_private_key(

Check warning on line 37 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L37

Added line #L37 was not covered by tests
backend=crypto_default_backend(),
public_exponent=65537,
key_size=2048,
)

private_key = key.private_bytes(

Check warning on line 43 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L43

Added line #L43 was not covered by tests
crypto_serialization.Encoding.PEM,
crypto_serialization.PrivateFormat.PKCS8,
crypto_serialization.NoEncryption(),
)

public_key = key.public_key().public_bytes(

Check warning on line 49 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L49

Added line #L49 was not covered by tests
crypto_serialization.Encoding.OpenSSH,
crypto_serialization.PublicFormat.OpenSSH,
)

dirpath = tmp_path_factory.mktemp('keys')
filename = uuid4().hex
filepath_private_key = dirpath / filename
filepath_public_key = dirpath / f'{filename}.pub'

Check warning on line 57 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L54-L57

Added lines #L54 - L57 were not covered by tests

filepath_private_key.write_bytes(private_key)
filepath_public_key.write_bytes(public_key)

Check warning on line 60 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L59-L60

Added lines #L59 - L60 were not covered by tests

try:
yield filepath_private_key

Check warning on line 63 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L62-L63

Added lines #L62 - L63 were not covered by tests
finally:
filepath_private_key.unlink(missing_ok=True)
filepath_public_key.unlink(missing_ok=True)

Check warning on line 66 in src/aiida/tools/pytest_fixtures/orm.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/tools/pytest_fixtures/orm.py#L65-L66

Added lines #L65 - L66 were not covered by tests


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions tests/cmdline/commands/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ def test_code_test(run_cli_command):


@pytest.fixture
def command_options(request, aiida_localhost, tmp_path):
def command_options(request, aiida_localhost, tmp_path, bash_path):
"""Return tuple of list of options and entry point."""
options = [request.param, '-n', '--label', str(uuid.uuid4())]

Expand All @@ -550,7 +550,7 @@ def command_options(request, aiida_localhost, tmp_path):
'--computer',
str(aiida_localhost.pk),
'--filepath-executable',
'/usr/bin/bash',
str(bash_path.absolute()),
'--engine-command',
engine_command,
'--image-name',
Expand Down
13 changes: 0 additions & 13 deletions tests/common/test_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,6 @@ def test_now():
assert from_tz >= ref - dt


def test_make_aware():
"""Test the :func:`aiida.common.timezone.make_aware` function.

This should make a naive datetime object aware using the timezone of the operating system.
"""
system_tzinfo = datetime.now(timezone.utc).astimezone() # This is how to get the timezone of the OS.
naive = datetime(1970, 1, 1)
aware = make_aware(naive)
assert is_aware(aware)
assert aware.tzinfo.tzname(aware) == system_tzinfo.tzname()
assert aware.tzinfo.utcoffset(aware) == system_tzinfo.utcoffset()


def test_make_aware_already_aware():
"""Test the :func:`aiida.common.timezone.make_aware` function for an already aware datetime.

Expand Down
15 changes: 15 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import dataclasses
import os
import pathlib
import subprocess
import types
import typing as t
import warnings
Expand Down Expand Up @@ -860,3 +861,17 @@ def factory(dirpath: pathlib.Path, read_bytes=True) -> dict:
return serialized

return factory


@pytest.fixture(scope='session')
def bash_path() -> Path:
run_process = subprocess.run(['which', 'bash'], capture_output=True, check=True)
path = run_process.stdout.decode('utf-8').strip()
return Path(path)


@pytest.fixture(scope='session')
def cat_path() -> Path:
run_process = subprocess.run(['which', 'cat'], capture_output=True, check=True)
path = run_process.stdout.decode('utf-8').strip()
return Path(path)
Loading
Loading