chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,657 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib
|
||||
from typing import Any, Dict
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
import yaml
|
||||
from click.exceptions import ClickException
|
||||
|
||||
from ray._private.test_utils import load_test_config, recursive_fnmatch
|
||||
from ray.autoscaler._private._azure.config import (
|
||||
_configure_key_pair as _azure_configure_key_pair,
|
||||
)
|
||||
from ray.autoscaler._private.gcp import config as gcp_config
|
||||
from ray.autoscaler._private.providers import _NODE_PROVIDERS
|
||||
from ray.autoscaler._private.util import (
|
||||
fill_node_type_min_max_workers,
|
||||
merge_setup_commands,
|
||||
prepare_config,
|
||||
validate_config,
|
||||
)
|
||||
|
||||
RAY_PATH = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
CONFIG_PATHS = recursive_fnmatch(os.path.join(RAY_PATH, "autoscaler"), "*.yaml")
|
||||
|
||||
CONFIG_PATHS += recursive_fnmatch(os.path.join(RAY_PATH, "tune", "examples"), "*.yaml")
|
||||
|
||||
EXPECTED_LOCAL_CONFIG_STR = """
|
||||
cluster_name: minimal-manual
|
||||
provider:
|
||||
head_ip: xxx.yyy
|
||||
type: local
|
||||
worker_ips:
|
||||
- aaa.bbb
|
||||
- ccc.ddd
|
||||
- eee.fff
|
||||
auth:
|
||||
ssh_private_key: ~/.ssh/id_rsa
|
||||
ssh_user: user
|
||||
docker: {}
|
||||
max_workers: 3
|
||||
available_node_types:
|
||||
local.cluster.node:
|
||||
max_workers: 3
|
||||
min_workers: 3
|
||||
node_config: {}
|
||||
resources: {}
|
||||
head_node_type: local.cluster.node
|
||||
head_start_ray_commands:
|
||||
- ray stop
|
||||
- ulimit -c unlimited; ray start --head --port=6379 --autoscaling-config=~/ray_bootstrap_config.yaml
|
||||
worker_start_ray_commands:
|
||||
- ray stop
|
||||
- ray start --address=$RAY_HEAD_IP:6379
|
||||
cluster_synced_files: []
|
||||
idle_timeout_minutes: 5
|
||||
upscaling_speed: 1.0
|
||||
file_mounts: {}
|
||||
file_mounts_sync_continuously: false
|
||||
head_setup_commands: []
|
||||
initialization_commands: []
|
||||
rsync_exclude: []
|
||||
rsync_filter: []
|
||||
setup_commands: []
|
||||
worker_setup_commands: []
|
||||
""" # noqa E501
|
||||
|
||||
|
||||
def fake_fillout_available_node_types_resources(config: Dict[str, Any]) -> None:
|
||||
"""A cheap way to fill out the resources field (the same way a node
|
||||
provider would autodetect them) as far as schema validation is concerned."""
|
||||
available_node_types = config.get("available_node_types", {})
|
||||
for value in available_node_types.values():
|
||||
value["resources"] = value.get("resources", {"filler": 1})
|
||||
|
||||
|
||||
class AutoscalingConfigTest(unittest.TestCase):
|
||||
def testValidateDefaultConfig(self):
|
||||
for config_path in CONFIG_PATHS:
|
||||
try:
|
||||
if os.path.join("aws", "example-multi-node-type.yaml") in config_path:
|
||||
# aws tested in testValidateDefaultConfigAWSMultiNodeTypes.
|
||||
continue
|
||||
if "local" in config_path:
|
||||
# local tested in testValidateLocal
|
||||
continue
|
||||
if "fake_multi_node" in config_path:
|
||||
# not supported with ray up
|
||||
continue
|
||||
if "kuberay" in config_path:
|
||||
# not supported with ray up
|
||||
continue
|
||||
with open(config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
config = prepare_config(config)
|
||||
if config["provider"]["type"] == "aws":
|
||||
fake_fillout_available_node_types_resources(config)
|
||||
validate_config(config)
|
||||
except Exception:
|
||||
logging.exception("")
|
||||
self.fail(f"Config {config_path} did not pass validation test!")
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Fails on Windows.")
|
||||
def testValidateDefaultConfigMinMaxWorkers(self):
|
||||
aws_config_path = os.path.join(
|
||||
RAY_PATH, "autoscaler/aws/example-multi-node-type.yaml"
|
||||
)
|
||||
with open(aws_config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
config = prepare_config(config)
|
||||
for node_type in config["available_node_types"]:
|
||||
config["available_node_types"][node_type]["resources"] = config[
|
||||
"available_node_types"
|
||||
][node_type].get("resources", {})
|
||||
try:
|
||||
validate_config(config)
|
||||
except Exception:
|
||||
self.fail("Config did not pass validation test!")
|
||||
|
||||
config["max_workers"] = 0 # the sum of min_workers is 1.
|
||||
with pytest.raises(ValueError):
|
||||
validate_config(config)
|
||||
|
||||
# make sure edge case of exactly 1 passes too.
|
||||
config["max_workers"] = 1
|
||||
try:
|
||||
validate_config(config)
|
||||
except Exception:
|
||||
self.fail("Config did not pass validation test!")
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Fails on Windows.")
|
||||
def testValidateDefaultConfigAWSMultiNodeTypes(self):
|
||||
aws_config_path = os.path.join(
|
||||
RAY_PATH, "autoscaler/aws/example-multi-node-type.yaml"
|
||||
)
|
||||
with open(aws_config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
new_config = copy.deepcopy(config)
|
||||
# modify it here
|
||||
new_config["available_node_types"] = {
|
||||
"cpu_4_ondemand": new_config["available_node_types"]["cpu_4_ondemand"],
|
||||
"cpu_16_spot": new_config["available_node_types"]["cpu_16_spot"],
|
||||
"gpu_8_ondemand": new_config["available_node_types"]["gpu_8_ondemand"],
|
||||
"neuron_core_inf_1_ondemand": {
|
||||
"node_config": {
|
||||
"InstanceType": "inf2.xlarge",
|
||||
"ImageId": "latest_dlami",
|
||||
},
|
||||
"max_workers": 2,
|
||||
},
|
||||
}
|
||||
orig_new_config = copy.deepcopy(new_config)
|
||||
expected_available_node_types = orig_new_config["available_node_types"]
|
||||
expected_available_node_types["cpu_4_ondemand"]["resources"] = {"CPU": 4}
|
||||
expected_available_node_types["cpu_16_spot"]["resources"] = {
|
||||
"CPU": 16,
|
||||
"memory": 48103633715,
|
||||
"Custom1": 1,
|
||||
"is_spot": 1,
|
||||
}
|
||||
expected_available_node_types["gpu_8_ondemand"]["resources"] = {
|
||||
"CPU": 32,
|
||||
"memory": 183395103539,
|
||||
"GPU": 4,
|
||||
"accelerator_type:V100": 1,
|
||||
}
|
||||
expected_available_node_types["neuron_core_inf_1_ondemand"]["resources"] = {
|
||||
"CPU": 4,
|
||||
"memory": 12025908428,
|
||||
"neuron_cores": 2,
|
||||
"accelerator_type:aws-neuron-core": 1,
|
||||
}
|
||||
expected_available_node_types["cpu_16_spot"]["min_workers"] = 0
|
||||
expected_available_node_types["gpu_8_ondemand"]["min_workers"] = 0
|
||||
expected_available_node_types["neuron_core_inf_1_ondemand"]["min_workers"] = 0
|
||||
|
||||
boto3_dict = {
|
||||
"InstanceTypes": [
|
||||
{
|
||||
"InstanceType": "m4.xlarge",
|
||||
"VCpuInfo": {"DefaultVCpus": 4},
|
||||
"MemoryInfo": {"SizeInMiB": 16384},
|
||||
},
|
||||
{
|
||||
"InstanceType": "m4.4xlarge",
|
||||
"VCpuInfo": {"DefaultVCpus": 16},
|
||||
"MemoryInfo": {"SizeInMiB": 65536},
|
||||
},
|
||||
{
|
||||
"InstanceType": "p3.8xlarge",
|
||||
"VCpuInfo": {"DefaultVCpus": 32},
|
||||
"MemoryInfo": {"SizeInMiB": 249856},
|
||||
"GpuInfo": {"Gpus": [{"Name": "V100", "Count": 4}]},
|
||||
},
|
||||
{
|
||||
"InstanceType": "inf2.xlarge",
|
||||
"VCpuInfo": {"DefaultVCpus": 4},
|
||||
"MemoryInfo": {"SizeInMiB": 16384},
|
||||
"AcceleratorInfo": {
|
||||
"Accelerators": [{"Name": "Inferentia", "Count": 1}]
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
describe_instance_types_mock = Mock()
|
||||
describe_instance_types_mock.describe_instance_types = MagicMock(
|
||||
return_value=boto3_dict
|
||||
)
|
||||
client_cache_mock = MagicMock(return_value=describe_instance_types_mock)
|
||||
with patch.multiple(
|
||||
"ray.autoscaler._private.aws.node_provider",
|
||||
client_cache=client_cache_mock,
|
||||
):
|
||||
new_config = prepare_config(new_config)
|
||||
importer = _NODE_PROVIDERS.get(new_config["provider"]["type"])
|
||||
provider_cls = importer(new_config["provider"])
|
||||
|
||||
try:
|
||||
new_config = provider_cls.fillout_available_node_types_resources(
|
||||
new_config
|
||||
)
|
||||
validate_config(new_config)
|
||||
assert (
|
||||
expected_available_node_types == new_config["available_node_types"]
|
||||
)
|
||||
except Exception:
|
||||
self.fail("Config did not pass multi node types auto fill test!")
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Fails on Windows.")
|
||||
def testValidateLocal(self):
|
||||
"""
|
||||
Tests local node provider config validation for the most common use
|
||||
case of bootstrapping a cluster at a static set of ips.
|
||||
"""
|
||||
local_config_path = os.path.join(
|
||||
RAY_PATH, "autoscaler/local/example-minimal-manual.yaml"
|
||||
)
|
||||
base_config = yaml.safe_load(open(local_config_path).read())
|
||||
base_config["provider"]["head_ip"] = "xxx.yyy"
|
||||
base_config["provider"]["worker_ips"] = ["aaa.bbb", "ccc.ddd", "eee.fff"]
|
||||
base_config["auth"]["ssh_user"] = "user"
|
||||
base_config["auth"]["ssh_private_key"] = "~/.ssh/id_rsa"
|
||||
|
||||
test_prepare_config = copy.deepcopy(base_config)
|
||||
prepared_config = prepare_config(test_prepare_config)
|
||||
try:
|
||||
validate_config(prepared_config)
|
||||
except Exception:
|
||||
self.fail("Failed to validate local/example-minimal-manual.yaml")
|
||||
expected_prepared = yaml.safe_load(EXPECTED_LOCAL_CONFIG_STR)
|
||||
assert prepared_config == expected_prepared
|
||||
|
||||
no_worker_config = copy.deepcopy(base_config)
|
||||
del no_worker_config["provider"]["worker_ips"]
|
||||
with pytest.raises(ClickException):
|
||||
prepare_config(no_worker_config)
|
||||
no_head_config = copy.deepcopy(base_config)
|
||||
del no_head_config["provider"]["head_ip"]
|
||||
with pytest.raises(ClickException):
|
||||
prepare_config(no_head_config)
|
||||
for field in "head_node", "worker_nodes", "available_node_types":
|
||||
faulty_config = copy.deepcopy(base_config)
|
||||
faulty_config[field] = "This field shouldn't be in here."
|
||||
with pytest.raises(ClickException):
|
||||
prepare_config(faulty_config)
|
||||
|
||||
too_many_workers_config = copy.deepcopy(base_config)
|
||||
|
||||
# More workers requested than the three available ips.
|
||||
too_many_workers_config["max_workers"] = 10
|
||||
too_many_workers_config["min_workers"] = 10
|
||||
prepared_config = prepare_config(too_many_workers_config)
|
||||
|
||||
# Check that worker config numbers were clipped to 3.
|
||||
assert prepared_config == expected_prepared
|
||||
|
||||
not_enough_workers_config = copy.deepcopy(base_config)
|
||||
|
||||
# Max workers is less than than the three available ips.
|
||||
# The user is probably has probably made an error. Make sure we log a warning.
|
||||
not_enough_workers_config["max_workers"] = 0
|
||||
not_enough_workers_config["min_workers"] = 0
|
||||
with mock.patch(
|
||||
"ray.autoscaler._private.local.config.cli_logger.warning"
|
||||
) as warning:
|
||||
prepared_config = prepare_config(not_enough_workers_config)
|
||||
warning.assert_called_with(
|
||||
"The value of `max_workers` supplied (0) is less"
|
||||
" than the number of available worker ips (3)."
|
||||
" At most 0 Ray worker nodes will connect to the cluster."
|
||||
)
|
||||
expected_prepared = yaml.safe_load(EXPECTED_LOCAL_CONFIG_STR)
|
||||
# We logged a warning.
|
||||
# However, prepare_config does not repair the strange config setting:
|
||||
expected_prepared["max_workers"] = 0
|
||||
expected_prepared["available_node_types"]["local.cluster.node"][
|
||||
"max_workers"
|
||||
] = 0
|
||||
expected_prepared["available_node_types"]["local.cluster.node"][
|
||||
"min_workers"
|
||||
] = 0
|
||||
assert prepared_config == expected_prepared
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("win"),
|
||||
reason="SSL cert verification fails on Windows CI due to outdated Miniconda CA bundle. Working to update the bundle in https://github.com/ray-project/ray/pull/61545.",
|
||||
)
|
||||
def testValidateNetworkConfigForBackwardsCompatibility(self):
|
||||
web_yaml = (
|
||||
"https://raw.githubusercontent.com/ray-project/ray/"
|
||||
"master/python/ray/autoscaler/aws/example-full.yaml"
|
||||
)
|
||||
response = urllib.request.urlopen(web_yaml, timeout=5)
|
||||
content = response.read()
|
||||
with tempfile.TemporaryFile() as f:
|
||||
f.write(content)
|
||||
f.seek(0)
|
||||
config = yaml.safe_load(f)
|
||||
config = prepare_config(config)
|
||||
try:
|
||||
validate_config(config)
|
||||
except Exception:
|
||||
self.fail("Config did not pass validation test!")
|
||||
|
||||
def _test_invalid_config(self, config_path):
|
||||
with open(os.path.join(RAY_PATH, config_path)) as f:
|
||||
config = yaml.safe_load(f)
|
||||
try:
|
||||
validate_config(config)
|
||||
self.fail("Expected validation to fail for {}".format(config_path))
|
||||
except jsonschema.ValidationError:
|
||||
pass
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "Failing on Windows.")
|
||||
def testInvalidConfig(self):
|
||||
self._test_invalid_config(os.path.join("tests", "additional_property.yaml"))
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "Failing on Windows.")
|
||||
def testValidateCustomSecurityGroupConfig(self):
|
||||
aws_config_path = os.path.join(RAY_PATH, "autoscaler/aws/example-minimal.yaml")
|
||||
with open(aws_config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Test validate security group with custom permissions
|
||||
ip_permissions = [
|
||||
{
|
||||
"FromPort": port,
|
||||
"ToPort": port,
|
||||
"IpProtocol": "TCP",
|
||||
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
||||
}
|
||||
for port in [80, 443, 8265]
|
||||
]
|
||||
config["provider"].update({"security_group": {"IpPermissions": ip_permissions}})
|
||||
config = prepare_config(copy.deepcopy(config))
|
||||
try:
|
||||
validate_config(config)
|
||||
assert (
|
||||
config["provider"]["security_group"]["IpPermissions"] == ip_permissions
|
||||
)
|
||||
except Exception:
|
||||
self.fail("Failed to validate config with security group in bound rules!")
|
||||
|
||||
# Test validate security group with custom name
|
||||
group_name = "test_security_group_name"
|
||||
config["provider"]["security_group"].update({"GroupName": group_name})
|
||||
|
||||
try:
|
||||
validate_config(config)
|
||||
assert config["provider"]["security_group"]["GroupName"] == group_name
|
||||
except Exception:
|
||||
self.fail("Failed to validate config with security group name!")
|
||||
|
||||
def testMaxWorkerDefault(self):
|
||||
# Load config, call prepare config, check that default max_workers
|
||||
# is filled correctly for node types that don't specify it.
|
||||
# Check that max_workers is untouched for node types
|
||||
# that do specify it.
|
||||
config = load_test_config("test_multi_node.yaml")
|
||||
node_types = config["available_node_types"]
|
||||
|
||||
# Max workers initially absent for this node type.
|
||||
assert "max_workers" not in node_types["worker_node_max_unspecified"]
|
||||
# Max workers specified for this node type.
|
||||
assert "max_workers" in node_types["worker_node_max_specified"]
|
||||
|
||||
prepared_config = prepare_config(config)
|
||||
prepared_node_types = prepared_config["available_node_types"]
|
||||
# Max workers unchanged.
|
||||
assert (
|
||||
node_types["worker_node_max_specified"]["max_workers"]
|
||||
== prepared_node_types["worker_node_max_specified"]["max_workers"]
|
||||
== 3
|
||||
)
|
||||
# Max workers auto-filled with specified cluster-wide value of 5.
|
||||
assert (
|
||||
config["max_workers"]
|
||||
== prepared_node_types["worker_node_max_unspecified"]["max_workers"]
|
||||
== 5
|
||||
)
|
||||
|
||||
# Repeat with a config that doesn't specify global max workers.
|
||||
# Default value of 2 should be pulled in for global max workers.
|
||||
config = load_test_config("test_multi_node.yaml")
|
||||
# Delete global max_workers so it can be autofilled with default of 2.
|
||||
del config["max_workers"]
|
||||
node_types = config["available_node_types"]
|
||||
|
||||
# Max workers initially absent for this node type.
|
||||
assert "max_workers" not in node_types["worker_node_max_unspecified"]
|
||||
# Max workers specified for this node type.
|
||||
assert "max_workers" in node_types["worker_node_max_specified"]
|
||||
|
||||
prepared_config = prepare_config(config)
|
||||
prepared_node_types = prepared_config["available_node_types"]
|
||||
# Max workers unchanged.
|
||||
assert (
|
||||
node_types["worker_node_max_specified"]["max_workers"]
|
||||
== prepared_node_types["worker_node_max_specified"]["max_workers"]
|
||||
== 3
|
||||
)
|
||||
# Max workers auto-filled with default cluster-wide value of 2.
|
||||
assert (
|
||||
prepared_config["max_workers"]
|
||||
== prepared_node_types["worker_node_max_unspecified"]["max_workers"]
|
||||
== 2
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Fails on Windows.")
|
||||
def testExampleFull(self):
|
||||
"""
|
||||
Test that example-full yamls are unmodified by prepared_config,
|
||||
except possibly by having setup_commands merged and
|
||||
default per-node max/min workers set.
|
||||
"""
|
||||
providers = ["aws", "gcp", "azure"]
|
||||
for provider in providers:
|
||||
path = os.path.join(RAY_PATH, "autoscaler", provider, "example-full.yaml")
|
||||
config = yaml.safe_load(open(path).read())
|
||||
config_copy = copy.deepcopy(config)
|
||||
merge_setup_commands(config_copy)
|
||||
fill_node_type_min_max_workers(config_copy)
|
||||
assert config_copy == prepare_config(config)
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Fails on Windows.")
|
||||
def testAzureKeyPair(self):
|
||||
azure_config_path = os.path.join(RAY_PATH, "autoscaler/azure/example-full.yaml")
|
||||
azure_config = yaml.safe_load(open(azure_config_path))
|
||||
azure_config["auth"]["ssh_user"] = "default_user"
|
||||
with tempfile.NamedTemporaryFile() as pub_key, tempfile.NamedTemporaryFile() as priv_key: # noqa: E501
|
||||
pub_key.write(b"PUBLICKEY")
|
||||
pub_key.flush()
|
||||
priv_key.write(b"PRIVATEKEY")
|
||||
priv_key.flush()
|
||||
azure_config["auth"]["ssh_private_key"] = priv_key.name
|
||||
azure_config["auth"]["ssh_public_key"] = pub_key.name
|
||||
modified_config = _azure_configure_key_pair(azure_config)
|
||||
for node_type in modified_config["available_node_types"].values():
|
||||
assert (
|
||||
node_type["node_config"]["azure_arm_parameters"]["adminUsername"]
|
||||
== "default_user"
|
||||
)
|
||||
|
||||
assert (
|
||||
node_type["node_config"]["azure_arm_parameters"]["publicKey"]
|
||||
== "PUBLICKEY"
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Fails on Windows.")
|
||||
def testGCPSubnets(self):
|
||||
"""Validates gcp _configure_subnet logic.
|
||||
|
||||
Checks that _configure_subnet fills default networkInterfaces data for
|
||||
each node type that doesn't specify networkInterfaces.
|
||||
|
||||
Checks that _list_subnets is not called if all node types specify
|
||||
networkInterfaces.
|
||||
"""
|
||||
path = os.path.join(RAY_PATH, "autoscaler", "gcp", "example-full.yaml")
|
||||
config = yaml.safe_load(open(path).read())
|
||||
|
||||
config_subnets_configured = copy.deepcopy(config)
|
||||
config_subnets_worker_configured = copy.deepcopy(config)
|
||||
config_subnets_head_configured = copy.deepcopy(config)
|
||||
config_subnets_no_type_configured = copy.deepcopy(config)
|
||||
|
||||
config_subnets_configured["available_node_types"]["ray_head_default"][
|
||||
"node_config"
|
||||
]["networkInterfaces"] = "mock_interfaces"
|
||||
config_subnets_configured["available_node_types"]["ray_worker_small"][
|
||||
"node_config"
|
||||
]["networkInterfaces"] = "mock_interfaces"
|
||||
|
||||
config_subnets_worker_configured["available_node_types"]["ray_worker_small"][
|
||||
"node_config"
|
||||
]["networkInterfaces"] = "mock_interfaces"
|
||||
|
||||
config_subnets_head_configured["available_node_types"]["ray_head_default"][
|
||||
"node_config"
|
||||
]["networkInterfaces"] = "mock_interfaces"
|
||||
|
||||
assert (
|
||||
"networkInterfaces"
|
||||
not in config_subnets_no_type_configured["available_node_types"][
|
||||
"ray_head_default"
|
||||
]["node_config"]
|
||||
)
|
||||
assert (
|
||||
"networkInterfaces"
|
||||
not in config_subnets_no_type_configured["available_node_types"][
|
||||
"ray_worker_small"
|
||||
]["node_config"]
|
||||
)
|
||||
|
||||
# Configure subnets modifies configs in place so we need to copy
|
||||
# the configs for comparison after passing into the method.
|
||||
config_subnets_configured_post = copy.deepcopy(config_subnets_configured)
|
||||
config_subnets_worker_configured_post = copy.deepcopy(
|
||||
config_subnets_worker_configured
|
||||
)
|
||||
config_subnets_head_configured_post = copy.deepcopy(
|
||||
config_subnets_head_configured
|
||||
)
|
||||
config_subnets_no_type_configured_post = copy.deepcopy(
|
||||
config_subnets_no_type_configured
|
||||
)
|
||||
|
||||
# Track number of times list_subnets has been called.
|
||||
list_subnets_counter = 0
|
||||
|
||||
def mock_list_subnets(*args):
|
||||
nonlocal list_subnets_counter
|
||||
list_subnets_counter += 1
|
||||
return [{"selfLink": "link"}]
|
||||
|
||||
# Attempting to patch produces an error in the CI.
|
||||
gcp_config._list_subnets = mock_list_subnets
|
||||
config_subnets_configured_post = gcp_config._configure_subnet(
|
||||
config_subnets_configured, compute="mock_compute"
|
||||
)
|
||||
# List subnets wasn't called
|
||||
assert list_subnets_counter == 0
|
||||
config_subnets_worker_configured_post = gcp_config._configure_subnet(
|
||||
config_subnets_worker_configured, compute="mock_compute"
|
||||
)
|
||||
# List subnets was called
|
||||
assert list_subnets_counter == 1
|
||||
config_subnets_head_configured_post = gcp_config._configure_subnet(
|
||||
config_subnets_head_configured, compute="mock_compute"
|
||||
)
|
||||
# List subnets was called
|
||||
assert list_subnets_counter == 2
|
||||
config_subnets_no_type_configured_post = gcp_config._configure_subnet(
|
||||
config_subnets_no_type_configured, compute="mock_compute"
|
||||
)
|
||||
# List subnets was called
|
||||
assert list_subnets_counter == 3
|
||||
|
||||
# networkInterfaces field generated by configure subnets with mocked
|
||||
# _list_subnets
|
||||
default_interfaces = [
|
||||
{
|
||||
"subnetwork": "link",
|
||||
"accessConfigs": [
|
||||
{
|
||||
"name": "External NAT",
|
||||
"type": "ONE_TO_ONE_NAT",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
# Unchanged
|
||||
assert config_subnets_configured_post == config_subnets_configured
|
||||
assert (
|
||||
config_subnets_configured_post["available_node_types"]["ray_head_default"][
|
||||
"node_config"
|
||||
]["networkInterfaces"]
|
||||
== "mock_interfaces"
|
||||
)
|
||||
assert (
|
||||
config_subnets_configured_post["available_node_types"]["ray_worker_small"][
|
||||
"node_config"
|
||||
]["networkInterfaces"]
|
||||
== "mock_interfaces"
|
||||
)
|
||||
|
||||
# Head subnets filled
|
||||
assert (
|
||||
config_subnets_worker_configured_post["available_node_types"][
|
||||
"ray_worker_small"
|
||||
]["node_config"]["networkInterfaces"]
|
||||
== "mock_interfaces"
|
||||
)
|
||||
assert (
|
||||
config_subnets_worker_configured_post["available_node_types"][
|
||||
"ray_head_default"
|
||||
]["node_config"]["networkInterfaces"]
|
||||
== default_interfaces
|
||||
)
|
||||
|
||||
# Worker subnets filled
|
||||
assert (
|
||||
config_subnets_head_configured_post["available_node_types"][
|
||||
"ray_worker_small"
|
||||
]["node_config"]["networkInterfaces"]
|
||||
== default_interfaces
|
||||
)
|
||||
assert (
|
||||
config_subnets_head_configured_post["available_node_types"][
|
||||
"ray_head_default"
|
||||
]["node_config"]["networkInterfaces"]
|
||||
== "mock_interfaces"
|
||||
)
|
||||
|
||||
# Head and worker subnets filled
|
||||
assert (
|
||||
config_subnets_no_type_configured_post["available_node_types"][
|
||||
"ray_worker_small"
|
||||
]["node_config"]["networkInterfaces"]
|
||||
== default_interfaces
|
||||
)
|
||||
assert (
|
||||
config_subnets_no_type_configured_post["available_node_types"][
|
||||
"ray_head_default"
|
||||
]["node_config"]["networkInterfaces"]
|
||||
== default_interfaces
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Fails on Windows.")
|
||||
def testFaultyResourceValidation(self):
|
||||
"""Checks that schema validation catches invalid node type resource
|
||||
field.
|
||||
|
||||
Demonstrates a fix in https://github.com/ray-project/ray/pull/16691."""
|
||||
path = os.path.join(RAY_PATH, "autoscaler", "aws", "example-full.yaml")
|
||||
config = yaml.safe_load(open(path).read())
|
||||
node_type = config["available_node_types"]["ray.head.default"]
|
||||
# Invalid `resources` field, say user entered `resources: `.
|
||||
node_type["resources"] = None
|
||||
with pytest.raises(jsonschema.exceptions.ValidationError):
|
||||
validate_config(config)
|
||||
# Invalid value in resource dict.
|
||||
node_type["resources"] = {"CPU": "a string is not valid here"}
|
||||
with pytest.raises(jsonschema.exceptions.ValidationError):
|
||||
validate_config(config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-sv", __file__]))
|
||||
Reference in New Issue
Block a user