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 src/load/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Release History
===============

2.1.0
++++++
* Add option for `--autostop-engine-users` to set maximum users per engine for AutoStop criteria.
* Update API version to 2025-03-01-preview.

2.0.0
++++++
* Add commands for creating and managing notification rules using CLI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def list_notification_rules(
if test_ids:
test_ids = ",".join(test_ids)
logger.info("Filtering notification rules by test ids: %s", test_ids)
responses = client.list_notification_rule(test_ids=test_ids)
responses = client.list_notification_rules(test_ids=test_ids)
logger.info("Retrieved notification rules: %s", responses)
return [response.as_dict() for response in responses]

Expand Down
12 changes: 10 additions & 2 deletions src/load/azext_load/data_plane/load_test/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def create_test(
autostop=None,
autostop_error_rate=None,
autostop_error_rate_time_window=None,
autostop_maximum_virtual_users_per_engine=None,
regionwise_engines=None,
engine_ref_id_type=None,
engine_ref_ids=None,
Expand All @@ -80,7 +81,10 @@ def create_test(
yaml, yaml_test_body = None, None
app_components, add_defaults_to_app_components, server_metrics = None, None, None
autostop_criteria = create_autostop_criteria_from_args(
autostop=autostop, error_rate=autostop_error_rate, time_window=autostop_error_rate_time_window)
autostop=autostop,
error_rate=autostop_error_rate,
time_window=autostop_error_rate_time_window,
max_vu_per_engine=autostop_maximum_virtual_users_per_engine)
if load_test_config_file is None:
test_type = test_type or infer_test_type_from_test_plan(test_plan)
logger.debug("Inferred test type: %s", test_type)
Expand Down Expand Up @@ -198,6 +202,7 @@ def update_test(
autostop=None,
autostop_error_rate=None,
autostop_error_rate_time_window=None,
autostop_maximum_virtual_users_per_engine=None,
regionwise_engines=None,
engine_ref_id_type=None,
engine_ref_ids=None,
Expand All @@ -215,7 +220,10 @@ def update_test(
yaml, yaml_test_body = None, None
app_components, server_metrics, add_defaults_to_app_components = None, None, None
autostop_criteria = create_autostop_criteria_from_args(
autostop=autostop, error_rate=autostop_error_rate, time_window=autostop_error_rate_time_window)
autostop=autostop,
error_rate=autostop_error_rate,
time_window=autostop_error_rate_time_window,
max_vu_per_engine=autostop_maximum_virtual_users_per_engine)
if load_test_config_file is not None:
yaml = load_yaml(load_test_config_file)
yaml_test_body = convert_yaml_to_test(cmd, yaml)
Expand Down
2 changes: 1 addition & 1 deletion src/load/azext_load/data_plane/load_test/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
az load test create --test-id sample-test-id --load-test-resource sample-alt-resource --resource-group sample-rg --display-name "Sample Name" --subnet-id "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/sample-rg/providers/Microsoft.Network/virtualNetworks/SampleVMVNET/subnets/SampleVMSubnet" --split-csv true
- name: Create a test with custom defined autostop criteria or enable / disable autostop for a test.
text: |
az load test create --test-id sample-test-id --load-test-resource sample-alt-resource --resource-group sample-rg --display-name "Sample Name" --autostop-error-rate 80.5 --autostop-time-window 120
az load test create --test-id sample-test-id --load-test-resource sample-alt-resource --resource-group sample-rg --display-name "Sample Name" --autostop-error-rate 80.5 --autostop-time-window 120 --autostop-engine-users 1000
az load test create --test-id sample-test-id --load-test-resource sample-alt-resource --resource-group sample-rg --display-name "Sample Name" --autostop disable
az load test create --test-id sample-test-id --load-test-resource sample-alt-resource --resource-group sample-rg --display-name "Sample Name" --autostop enable
- name: Create a test with a multi-region load configuration using region names in the format accepted by Azure Resource Manager (ARM). Ensure the specified regions are supported by Azure Load Testing. Multi-region load tests are restricted to public endpoints only.
Expand Down
2 changes: 2 additions & 0 deletions src/load/azext_load/data_plane/load_test/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def load_arguments(self, _):
c.argument("autostop", argtypes.autostop)
c.argument("autostop_error_rate", argtypes.autostop_error_rate)
c.argument("autostop_error_rate_time_window", argtypes.autostop_error_rate_time_window)
c.argument("autostop_maximum_virtual_users_per_engine", argtypes.autostop_maximum_virtual_users_per_engine)
c.argument("regionwise_engines", argtypes.regionwise_engines)
c.argument("engine_ref_id_type", argtypes.engine_ref_id_type)
c.argument("engine_ref_ids", argtypes.engine_ref_ids)
Expand All @@ -62,6 +63,7 @@ def load_arguments(self, _):
c.argument("autostop", argtypes.autostop)
c.argument("autostop_error_rate", argtypes.autostop_error_rate)
c.argument("autostop_error_rate_time_window", argtypes.autostop_error_rate_time_window)
c.argument("autostop_maximum_virtual_users_per_engine", argtypes.autostop_maximum_virtual_users_per_engine)
c.argument("regionwise_engines", argtypes.regionwise_engines)
c.argument("engine_ref_id_type", argtypes.engine_ref_id_type)
c.argument("engine_ref_ids", argtypes.engine_ref_ids)
Expand Down
2 changes: 1 addition & 1 deletion src/load/azext_load/data_plane/load_trigger/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,6 @@ def list_trigger_schedules(
if test_ids:
test_ids = ",".join(test_ids)
logger.info("Schedule trigger states: %s", trigger_states)
response_list = client.list_trigger(test_ids=test_ids, states=trigger_states)
response_list = client.list_triggers(test_ids=test_ids, states=trigger_states)
logger.debug("Fetched list of schedule triggers: %s", response_list)
return [response.as_dict() for response in response_list]
7 changes: 7 additions & 0 deletions src/load/azext_load/data_plane/utils/argtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@
help="Time window during which the error percentage should be evaluated in seconds.",
)

autostop_maximum_virtual_users_per_engine = CLIArgumentType(
options_list=["--autostop-engine-users"],
type=int,
validator=validators.validate_autostop_maximum_virtual_users_per_engine,
help="Maximum number of users per engine at which the test will stop.",
)

regionwise_engines = CLIArgumentType(
options_list=["--regionwise-engines"],
validator=validators.validate_regionwise_engines,
Expand Down
1 change: 1 addition & 0 deletions src/load/azext_load/data_plane/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class LoadTestConfigKeys:
AUTOSTOP = "autoStop"
AUTOSTOP_ERROR_RATE = "errorPercentage"
AUTOSTOP_ERROR_RATE_TIME_WINDOW = "timeWindow"
AUTOSTOP_MAX_VU_PER_ENGINE = "maximumVirtualUsersPerEngine"
FAILURE_CRITERIA = "failureCriteria"
CLIENT_METRICS_PF = "clientMetrics"
SERVER_METRICS_PF = "serverMetrics"
Expand Down
22 changes: 18 additions & 4 deletions src/load/azext_load/data_plane/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,17 @@ def parse_env(envs):
return env_dict


def create_autostop_criteria_from_args(autostop, error_rate, time_window):
if (autostop is None and error_rate is None and time_window is None):
def create_autostop_criteria_from_args(autostop, error_rate, time_window, max_vu_per_engine):
if (autostop is None and error_rate is None and time_window is None and max_vu_per_engine is None):
return None
autostop_criteria = {}
autostop_criteria["autoStopDisabled"] = not autostop if autostop is not None else False
if error_rate is not None:
autostop_criteria["errorRate"] = error_rate
if time_window is not None:
autostop_criteria["errorRateTimeWindowInSeconds"] = time_window
if max_vu_per_engine is not None:
autostop_criteria["maximumVirtualUsersPerEngine"] = max_vu_per_engine
return autostop_criteria


Expand Down Expand Up @@ -591,10 +593,16 @@ def create_or_update_test_with_config(
):
new_body["autoStopCriteria"]["errorRateTimeWindowInSeconds"] = \
body["autoStopCriteria"]["errorRateTimeWindowInSeconds"]
if (
new_body["autoStopCriteria"].get("maximumVirtualUsersPerEngine") is None
and body.get("autoStopCriteria", {}).get("maximumVirtualUsersPerEngine") is not None
):
new_body["autoStopCriteria"]["maximumVirtualUsersPerEngine"] = \
body["autoStopCriteria"]["maximumVirtualUsersPerEngine"]

if (new_body["autoStopCriteria"].get("autoStopDisabled") is True):
logger.warning(
"Auto stop is disabled. Error rate and time window will be ignored. "
"Auto stop is disabled. Error rate, time window and engine users will be ignored. "
"This can lead to incoming charges for an incorrectly configured test."
)

Expand Down Expand Up @@ -749,9 +757,15 @@ def create_or_update_test_without_config(
):
new_body["autoStopCriteria"]["errorRateTimeWindowInSeconds"] = \
body["autoStopCriteria"]["errorRateTimeWindowInSeconds"]
if (
new_body["autoStopCriteria"].get("maximumVirtualUsersPerEngine") is None
and body.get("autoStopCriteria", {}).get("maximumVirtualUsersPerEngine") is not None
):
new_body["autoStopCriteria"]["maximumVirtualUsersPerEngine"] = \
body["autoStopCriteria"]["maximumVirtualUsersPerEngine"]
if (new_body["autoStopCriteria"].get("autoStopDisabled") is True):
logger.warning(
"Auto stop is disabled. Error rate and time window will be ignored. "
"Auto stop is disabled. Error rate, time window and engine users will be ignored. "
"This can lead to incoming charges for an incorrectly configured test."
)
new_body["baselineTestRunId"] = baseline_test_run_id if baseline_test_run_id else body.get("baselineTestRunId")
Expand Down
5 changes: 4 additions & 1 deletion src/load/azext_load/data_plane/utils/utils_yaml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ def yaml_parse_autostop_criteria(data):
}
error_rate = data[LoadTestConfigKeys.AUTOSTOP].get(LoadTestConfigKeys.AUTOSTOP_ERROR_RATE)
time_window = data[LoadTestConfigKeys.AUTOSTOP].get(LoadTestConfigKeys.AUTOSTOP_ERROR_RATE_TIME_WINDOW)
max_vu_per_engine = data[LoadTestConfigKeys.AUTOSTOP].get(LoadTestConfigKeys.AUTOSTOP_MAX_VU_PER_ENGINE)
# pylint: disable-next=protected-access
validators._validate_autostop_criteria_configfile(error_rate, time_window)
validators._validate_autostop_criteria_configfile(error_rate, time_window, max_vu_per_engine)
autostop_criteria = {
"autoStopDisabled": False,
}
if error_rate is not None:
autostop_criteria["errorRate"] = error_rate
if time_window is not None:
autostop_criteria["errorRateTimeWindowInSeconds"] = time_window
if max_vu_per_engine is not None:
autostop_criteria["maximumVirtualUsersPerEngine"] = max_vu_per_engine
return autostop_criteria


Expand Down
32 changes: 25 additions & 7 deletions src/load/azext_load/data_plane/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from azure.cli.core.commands.parameters import get_subscription_locations
from azure.mgmt.core.tools import is_valid_resource_id
from knack.log import get_logger
from azext_load.vendored_sdks.loadtesting.models import NotificationEventType, Status, PFTestResult
from azext_load.vendored_sdks.loadtesting.models import NotificationEventType, TestRunStatus, PassFailTestResult

from . import utils
from .models import (
Expand Down Expand Up @@ -470,14 +470,28 @@ def validate_autostop_error_rate(namespace):
)


def validate_autostop_maximum_virtual_users_per_engine(namespace):
if namespace.autostop_maximum_virtual_users_per_engine is None:
return
if not isinstance(namespace.autostop_maximum_virtual_users_per_engine, int):
raise InvalidArgumentValueError(
f"Invalid autostop-engine-users type: {type(namespace.autostop_maximum_virtual_users_per_engine)}"
)
if namespace.autostop_maximum_virtual_users_per_engine <= 0:
raise InvalidArgumentValueError(
"Autostop maximum users per engine should be greater than 0"
)


def _validate_autostop_disable_configfile(autostop):
if autostop.casefold() not in ["disable"]:
raise InvalidArgumentValueError(
"Invalid value for autoStop. Valid values are 'disable' or an object with errorPercentage and timeWindow"
"Invalid value for autoStop. Valid values are 'disable' or an object with errorPercentage, timeWindow "
"and/or maximumVirtualUsersPerEngine"
)


def _validate_autostop_criteria_configfile(error_rate, time_window):
def _validate_autostop_criteria_configfile(error_rate, time_window, max_vu_per_engine):
if error_rate is not None:
if isinstance(error_rate, float) and (error_rate < 0.0 or error_rate > 100.0):
raise InvalidArgumentValueError(
Expand All @@ -491,6 +505,10 @@ def _validate_autostop_criteria_configfile(error_rate, time_window):
raise InvalidArgumentValueError(
"Invalid value for timeWindow. Value should be an integer greater than or equal to 0"
)
if max_vu_per_engine is not None and (not isinstance(max_vu_per_engine, int) or max_vu_per_engine <= 0):
raise InvalidArgumentValueError(
"Invalid value for maximumVirtualUsersPerEngine. Value should be an integer greater than 0"
)


def validate_regionwise_engines(cmd, namespace):
Expand Down Expand Up @@ -642,20 +660,20 @@ def _validate_notification_event(event: dict):
if "status" in event:
statuses = event["status"].split(",")
for status in statuses:
if status not in [e.value for e in Status]: # Use Status in _enum file
if status not in [e.value for e in TestRunStatus]: # Use Status in _enum file
raise InvalidArgumentValueError(
"Invalid status: {}. Allowed values: {}".format(
event["status"], ", ".join(e.value for e in Status)
event["status"], ", ".join(e.value for e in TestRunStatus)
)
)
event["status"] = statuses
if "result" in event:
results = event["result"].split(",")
for result in results:
if result not in [e.value for e in PFTestResult]: # Use PFResult in _enum file
if result not in [e.value for e in PassFailTestResult]: # Use PFResult in _enum file
raise InvalidArgumentValueError(
"Invalid result: {}. Allowed values: {}".format(
event["result"], ", ".join(e.value for e in PFTestResult)
event["result"], ", ".join(e.value for e in PassFailTestResult)
)
)
event["result"] = results
Expand Down
Loading
Loading