Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/actions/make-integ-config/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ inputs:
oidc_client_secret:
description: 'oidc_client_secret'
required: true
oidc_users_0_password:
description: 'oidc_users_0_password'
required: true
runs:
using: "composite"
steps:
Expand All @@ -35,6 +38,8 @@ runs:
smb_share: /cidata
oidc_client_id: d2298ec0-0596-49d2-9554-840a2fe20603
oidc_client_secret: ${{ inputs.oidc_client_secret }}
oidc_users_0_username: xlabciuser@scalemsdnscalecomputing.onmicrosoft.com
oidc_users_0_password: ${{ inputs.oidc_users_0_password }}
EOF
cat integ_config_vars.yml | jinja2 --strict tests/integration/integration_config.yml.j2 > tests/integration/integration_config.yml
echo "sc_host: ${{ inputs.sc_host }}" >> tests/integration/integration_config.yml
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/integ-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
# dns_config|time_server - NTP cannot be reconfigured if DNS is invalid
# git_issues - slow, do not run on each push. TODO - run them only once a day
# oidc_config - during reconfiguration API returns 500/502 errors for other requests
# utils_login - it uses OIDC user to login
# smtp - email_alert test requires a configured SMTP
# role_cluster_config - role cluster_config reconfigures DNS, SMTP, OIDC. And it is slow.
# version_update_single_node - role would change version, VSNS system cannot be updated.
Expand All @@ -23,7 +24,7 @@ on:
List integration tests to exclude.
Use "*" to exclude all tests.
Use regex like 'node|^git_issue|^dns_config$' to exclude only a subset.
default: "^dns_config$|^cluster_shutdown$|^version_update$|^oidc_config$|^smtp$|^role_cluster_config$|^role_version_update_single_node$"
default: "^dns_config$|^cluster_shutdown$|^version_update$|^oidc_config$|^smtp$|^role_cluster_config$|^role_version_update_single_node$|^utils_login$"
examples_tests_include:
type: string
description: |-
Expand All @@ -32,7 +33,7 @@ on:
default: "iso_info"
env:
INTEG_TESTS_INCLUDE_SCHEDULE: "*"
INTEG_TESTS_EXCLUDE_SCHEDULE: "^dns_config$|^cluster_shutdown$|^version_update$|^oidc_config$|^smtp$|^role_cluster_config$|^role_version_update_single_node$"
INTEG_TESTS_EXCLUDE_SCHEDULE: "^dns_config$|^cluster_shutdown$|^version_update$|^oidc_config$|^smtp$|^role_cluster_config$|^role_version_update_single_node$|^utils_login$"
EXAMPLES_TESTS_INCLUDE_SCHEDULE: "*"
# ansible-test needs special directory structure.
# WORKDIR is a subdir of GITHUB_WORKSPACE
Expand Down Expand Up @@ -73,6 +74,7 @@ jobs:
sc_password_50: ${{ secrets.CI_CONFIG_HC_IP50_SC_PASSWORD }}
smb_password: ${{ secrets.CI_CONFIG_HC_IP50_SMB_PASSWORD }}
oidc_client_secret: ${{ secrets.OIDC_CLIENT_SECRET }}
oidc_users_0_password: ${{ secrets.OIDC_USERS_0_PASSWORD }}
working_directory: ${{ env.WORKDIR }}
- run: ansible-playbook tests/integration/prepare/prepare_iso.yml
- run: ansible-playbook tests/integration/prepare/prepare_vm.yml
Expand Down Expand Up @@ -177,6 +179,7 @@ jobs:
sc_password_50: ${{ secrets.CI_CONFIG_HC_IP50_SC_PASSWORD }}
smb_password: ${{ secrets.CI_CONFIG_HC_IP50_SMB_PASSWORD }}
oidc_client_secret: ${{ secrets.OIDC_CLIENT_SECRET }}
oidc_users_0_password: ${{ secrets.OIDC_USERS_0_PASSWORD }}
working_directory: ${{ env.WORKDIR }}
- run: |
pwd
Expand Down Expand Up @@ -220,6 +223,12 @@ jobs:
test_name: support_tunnel
- sc_host: https://10.5.11.50
test_name: vm_clone__replicated
# test oidc login
- sc_host: https://10.5.11.50
test_name: utils_login
# test inventory plugin with oidc login
- sc_host: https://10.5.11.50
test_name: inventory
exclude:
# The VSNS were not configured with remote replication cluster.
- sc_host: https://10.5.11.200
Expand Down Expand Up @@ -255,6 +264,7 @@ jobs:
sc_password_50: ${{ secrets.CI_CONFIG_HC_IP50_SC_PASSWORD }}
smb_password: ${{ secrets.CI_CONFIG_HC_IP50_SMB_PASSWORD }}
oidc_client_secret: ${{ secrets.OIDC_CLIENT_SECRET }}
oidc_users_0_password: ${{ secrets.OIDC_USERS_0_PASSWORD }}
working_directory: ${{ env.WORKDIR }}
- run: ansible-test integration --local ${{ matrix.test_name }}

Expand Down Expand Up @@ -282,6 +292,7 @@ jobs:
sc_password_50: ${{ secrets.CI_CONFIG_HC_IP50_SC_PASSWORD }}
smb_password: ${{ secrets.CI_CONFIG_HC_IP50_SMB_PASSWORD }}
oidc_client_secret: ${{ secrets.OIDC_CLIENT_SECRET }}
oidc_users_0_password: ${{ secrets.OIDC_USERS_0_PASSWORD }}
working_directory: ${{ env.WORKDIR }}
- run: ansible-galaxy collection install community.general
- run: ansible-playbook tests/integration/cleanup/ci_replica_cleanup.yml
Expand All @@ -306,6 +317,7 @@ jobs:
sc_password_50: ${{ secrets.CI_CONFIG_HC_IP50_SC_PASSWORD }}
smb_password: ${{ secrets.CI_CONFIG_HC_IP50_SMB_PASSWORD }}
oidc_client_secret: ${{ secrets.OIDC_CLIENT_SECRET }}
oidc_users_0_password: ${{ secrets.OIDC_USERS_0_PASSWORD }}
working_directory: ${{ env.WORKDIR }}
- run: |
cd tests/integration/cleanup && ./smb_cleanup.sh \
Expand Down
10 changes: 10 additions & 0 deletions plugins/doc_fragments/cluster_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,14 @@ class ModuleDocFragment(object):
variable will be used.
required: false
type: float
auth_method:
description:
- Select login method.
If not set, the value of the C(SC_AUTH_METHOD) environment
variable will be used.
- Value I(local) - username/password is verified by the HyperCore server (the local users).
- Value I(oidc) - username/password is verified by the configured OIDC provider.
default: local
choices: [local, oidc]
type: str
"""
3 changes: 2 additions & 1 deletion plugins/inventory/hypercore.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ def parse(self, inventory, loader, path, cache=False):
username = os.getenv("SC_USERNAME")
password = os.getenv("SC_PASSWORD")
timeout = os.getenv("SC_TIMEOUT")
auth_method = os.getenv("SC_AUTH_METHOD")
if timeout:
try:
timeout = float(timeout)
Expand All @@ -234,7 +235,7 @@ def parse(self, inventory, loader, path, cache=False):
raise errors.ScaleComputingError(
"Missing one or more parameters: sc_host, sc_username, sc_password."
)
client = Client(host, username, password, timeout)
client = Client(host, username, password, timeout, auth_method)
rest_client = RestClient(client)
vms = rest_client.list_records("/rest/v1/VirDomain")

Expand Down
7 changes: 7 additions & 0 deletions plugins/module_utils/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ansible.module_utils.basic import env_fallback
from typing import Any
from ..module_utils.client import AuthMethod

# TODO - env from /etc/environment is loaded
# But when env is set in bash session, env seems to be lost on ssh connection to localhost.
Expand Down Expand Up @@ -42,6 +43,12 @@
required=False,
fallback=(env_fallback, ["SC_TIMEOUT"]),
),
auth_method=dict(
type="str",
default=AuthMethod.local,
choices=[am.value for am in AuthMethod],
fallback=(env_fallback, ["SC_AUTH_METHOD"]),
),
),
required_together=[("username", "password")],
),
Expand Down
44 changes: 35 additions & 9 deletions plugins/module_utils/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
import ssl
from typing import Any, Optional, Union
from io import BufferedReader
import enum

from ansible.module_utils.urls import Request, basic_auth_header
from ansible.module_utils.urls import Request

from .errors import (
AuthError,
Expand All @@ -29,6 +30,11 @@
DEFAULT_HEADERS = dict(Accept="application/json")


class AuthMethod(str, enum.Enum):
local = "local"
oidc = "oidc"


class Response:
# I have felling (but I'm not sure) we will always use
# "Response(raw_resp.status, raw_resp.read(), raw_resp.headers)"
Expand Down Expand Up @@ -64,6 +70,7 @@ def __init__(
username: str,
password: str,
timeout: float,
auth_method: str,
):
if not (host or "").startswith(("https://", "http://")):
raise ScaleComputingError(
Expand All @@ -75,8 +82,9 @@ def __init__(
self.username = username
self.password = password
self.timeout = timeout
self.auth_method = auth_method

self._auth_header: Optional[dict[str, bytes]] = None
self._auth_header: Optional[dict[str, str]] = None
self._client = Request()

@classmethod
Expand All @@ -86,19 +94,38 @@ def get_client(cls, cluster_instance: TypedClusterInstance) -> Client:
cluster_instance["username"],
cluster_instance["password"],
cluster_instance["timeout"],
cluster_instance["auth_method"],
)

@property
def auth_header(self) -> dict[str, bytes]:
def auth_header(self) -> dict[str, str]:
if not self._auth_header:
self._auth_header = self._login()
return self._auth_header

def _login(self) -> dict[str, bytes]:
def _login(self) -> dict[str, str]:
return self._login_username_password()

def _login_username_password(self) -> dict[str, bytes]:
return dict(Authorization=basic_auth_header(self.username, self.password))
def _login_username_password(self) -> dict[str, str]:
headers = {
"Accept": "application/json",
"Content-type": "application/json",
}
use_oidc = self.auth_method == AuthMethod.oidc.value
resp = self._request(
"POST",
f"{self.host}/rest/v1/login",
data=json.dumps(
dict(
username=self.username,
password=self.password,
useOIDC=use_oidc,
)
),
headers=headers,
timeout=self.timeout,
)
return dict(Cookie=f"sessionID={resp.json['sessionID']}")

def _request(
self,
Expand All @@ -108,9 +135,8 @@ def _request(
headers: Optional[dict[Any, Any]] = None,
timeout: Optional[float] = None,
) -> Response:
if (
timeout is None
): # If timeout from request is not specifically provided, take it from the Client.
if timeout is None:
# If timeout from request is not specifically provided, take it from the Client.
timeout = self.timeout
try:
raw_resp = self._client.open(
Expand Down
1 change: 1 addition & 0 deletions plugins/module_utils/typed_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class TypedClusterInstance(TypedDict):
username: str
password: str
timeout: float
auth_method: str


# Registration to ansible return dict.
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/integration_config.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ sc_config:
shared_secret_default: "{{ oidc_client_secret }}"
config_url_default: https://login.microsoftonline.com/76d4c62a-a9ca-4dc2-9187-e2cc4d9abe7f/v2.0/.well-known/openid-configuration
scopes: "openid+profile"
# Users that can login when config_url_default is configured as OIDC provider
users:
- username: "{{ oidc_users_0_username }}"
password: "{{ oidc_users_0_password }}"
client_id_test: ci-client-id
shared_secret_test: ci-client-secret
config_url_test: https://login.microsoftonline.com/76d4c62a-a9ca-4dc2-9187-e2cc4d9abe7f/v2.0/.well-known/openid-configuration
Expand Down
31 changes: 29 additions & 2 deletions tests/integration/targets/inventory/runme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ print("export SC_HOST='{}'".format(sc_host))
print("export SC_TIMEOUT='{}'".format(sc_timeout))
print("export SC_USERNAME='{}'".format(data["sc_config"][sc_host]["sc_username"]))
print("export SC_PASSWORD='{}'".format(data["sc_config"][sc_host]["sc_password"]))
# SC_AUTH_METHOD==local by default, leave it unset
EOF
)"

Expand Down Expand Up @@ -45,8 +46,34 @@ ansible-playbook -i localhost, -i hypercore_inventory_ansible_enable.yml run_ans
ansible-playbook -i localhost, -i hypercore_inventory_ansible_disable.yml run_ansible_disable_tests.yml
ansible-playbook -i localhost, -i hypercore_inventory_ansible_both_true.yml run_ansible_both_true_tests.yml

unset SC_TIMEOUT # do one test without SC_TIMEOUT

# do one test without SC_TIMEOUT
unset SC_TIMEOUT
ansible-playbook -i localhost, -i hypercore_inventory_ansible_both_false.yml run_ansible_both_false_tests.yml
# test with SC_AUTH_METHOD being set to "local"
export SC_AUTH_METHOD=local
ansible-playbook -i localhost, -i hypercore_inventory_ansible_both_false.yml run_ansible_both_false_tests.yml

# test with OIDC user
if [[ "$SC_HOST" == "https://10.5.11.50" ]]
then
# We can do this only if OIDC login is configured.
echo "Testing inventory plugin with OIDC user."
eval "$(cat <<EOF | python
import yaml
with open("$vars_file") as fd:
data = yaml.safe_load(fd)
sc_host=data["sc_host"]
sc_timeout=data["sc_timeout"]
print("export SC_HOST='{}'".format(sc_host))
print("export SC_TIMEOUT='{}'".format(sc_timeout))
print("export SC_USERNAME='{}'".format(data["sc_config"][sc_host]["oidc"]["users"][0]["username"]))
print("export SC_PASSWORD='{}'".format(data["sc_config"][sc_host]["oidc"]["users"][0]["password"]))
print("export SC_AUTH_METHOD=oidc")
EOF
)"
ansible-playbook -i localhost, -i hypercore_inventory_ansible_both_false.yml run_ansible_both_false_tests.yml
else
echo "OIDC is not configured on host $SC_HOST, inventory plugin was not tested with OIDC user."
fi

ansible-playbook cleanup.yml
75 changes: 75 additions & 0 deletions tests/integration/targets/utils_login/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
# Test modules are able to login to Hypercore.
# The local or oidc user accounts are supported.

# ===================================================================
# local user, from environ
- environment:
SC_HOST: "{{ sc_host }}"
SC_USERNAME: "{{ sc_config[sc_host].sc_username }}"
SC_PASSWORD: "{{ sc_config[sc_host].sc_password }}"
SC_TIMEOUT: "{{ sc_timeout }}"
SC_AUTH_METHOD: local

block:
- name: Get HC3 ping - local user, from environ
scale_computing.hypercore.api:
action: get
endpoint: /rest/v1/ping
register: ping_result
- &assert_ping_result
name: Check ping_result
ansible.builtin.assert:
that:
- ping_result is not changed
- ping_result.record.0 == "status"

# ===================================================================
# local user, from cluster_instance variable
- vars:
cluster_instance:
host: "{{ sc_host }}"
username: "{{ sc_config[sc_host].sc_username }}"
password: "{{ sc_config[sc_host].sc_password }}"
auth_method: local
block:
- name: Get HC3 ping - local user, from cluster_instance variable
scale_computing.hypercore.api:
action: get
endpoint: /rest/v1/ping
cluster_instance: "{{ cluster_instance }}"
register: ping_result
- *assert_ping_result

# ===================================================================
# OIDC user, from environ
- environment:
SC_HOST: "{{ sc_host }}"
SC_USERNAME: "{{ sc_config[sc_host].oidc.users[0].username }}"
SC_PASSWORD: "{{ sc_config[sc_host].oidc.users[0].password }}"
SC_TIMEOUT: "{{ sc_timeout }}"
SC_AUTH_METHOD: oidc
block:
- name: Get HC3 ping - OIDC user, from environ
scale_computing.hypercore.api:
action: get
endpoint: /rest/v1/ping
register: ping_result
- *assert_ping_result

# ===================================================================
# OIDC user, from cluster_instance variable
- vars:
cluster_instance: &cluster_instance_local
host: "{{ sc_host }}"
username: "{{ sc_config[sc_host].oidc.users[0].username }}"
password: "{{ sc_config[sc_host].oidc.users[0].password }}"
auth_method: oidc
block:
- name: Get HC3 ping - OIDC user, from cluster_instance variable
scale_computing.hypercore.api:
action: get
endpoint: /rest/v1/ping
cluster_instance: "{{ cluster_instance }}"
register: ping_result
- *assert_ping_result
Loading