Make task format v2 more user-friendly

- move scenario name and args into separate section
- rename section 'context' to 'contexts'

Change-Id: Ib683dfb81610030b4fccb119f2fb1a6356d1dce1
This commit is contained in:
Andrey Kurilin 2017-08-17 21:09:30 +03:00
parent 9dc5d9a554
commit e12e5b184f
4 changed files with 334 additions and 336 deletions

View File

@ -13,41 +13,41 @@
title: Test main Cinder actions title: Test main Cinder actions
workloads: workloads:
- -
name: CinderVolumes.create_volume scenario:
args: CinderVolumes.create_volume:
size: 1 size: 1
runner: runner:
type: "constant" constant:
times: 2 times: 2
concurrency: 2 concurrency: 2
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: CinderVolumes.create_volume scenario:
args: CinderVolumes.create_volume:
size: 1 size: 1
image: image:
name: {{image_name}} name: {{image_name}}
runner: runner:
type: "constant" constant:
times: 1 times: 1
concurrency: 1 concurrency: 1
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: CinderVolumes.create_snapshot_and_attach_volume scenario:
args: CinderVolumes.create_snapshot_and_attach_volume:
volume_type: "lvmdriver-1" volume_type: "lvmdriver-1"
size: size:
min: 1 min: 1
max: 1 max: 1
runner: runner:
type: "constant" constant:
times: 2 times: 2
concurrency: 2 concurrency: 2
context: contexts:
servers: servers:
image: image:
name: {{image_name}} name: {{image_name}}
@ -61,17 +61,17 @@
title: Test main Nova actions title: Test main Nova actions
workloads: workloads:
- -
name: NovaServers.boot_and_list_server scenario:
args: NovaServers.boot_and_list_server:
flavor: flavor:
name: {{flavor_name}} name: {{flavor_name}}
image: image:
name: {{image_name}} name: {{image_name}}
detailed: True detailed: True
runner: runner:
type: "constant" constant:
times: 2 times: 2
concurrency: 2 concurrency: 2
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
@ -79,15 +79,15 @@
title: Test main Glance actions title: Test main Glance actions
workloads: workloads:
- -
name: GlanceImages.create_and_delete_image scenario:
args: GlanceImages.create_and_delete_image:
image_location: "{{ cirros_image_url }}" image_location: "{{ cirros_image_url }}"
container_format: "bare" container_format: "bare"
disk_format: "qcow2" disk_format: "qcow2"
runner: runner:
type: "constant" constant:
times: 1 times: 1
concurrency: 1 concurrency: 1
sla: sla:
failure_rate: failure_rate:
max: 100 max: 100
@ -95,37 +95,37 @@
title: Test main Neutron actions title: Test main Neutron actions
workloads: workloads:
- -
name: NeutronNetworks.create_and_list_networks scenario:
args: NeutronNetworks.create_and_list_networks:
network_create_args: network_create_args:
runner: runner:
type: "constant" constant:
times: 2 times: 2
concurrency: 2 concurrency: 2
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: NeutronNetworks.create_and_list_subnets scenario:
args: NeutronNetworks.create_and_list_subnets:
subnet_cidr_start: "1.1.0.0/30" subnet_cidr_start: "1.1.0.0/30"
subnets_per_network: 2 subnets_per_network: 2
runner: runner:
type: "constant" constant:
times: 2 times: 2
concurrency: 2 concurrency: 2
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: NeutronNetworks.create_and_list_floating_ips scenario:
args: NeutronNetworks.create_and_list_floating_ips:
floating_network: "public" floating_network: "public"
floating_ip_args: {} floating_ip_args: {}
runner: runner:
type: "constant" constant:
times: 2 times: 2
concurrency: 2 concurrency: 2
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0

View File

@ -8,14 +8,14 @@
title: Test SLA plugins title: Test SLA plugins
workloads: workloads:
- -
name: Dummy.dummy
description: "Check SLA" description: "Check SLA"
args: scenario:
sleep: 0.25 Dummy.dummy:
sleep: 0.25
runner: runner:
type: "constant" constant:
times: 20 times: 20
concurrency: 5 concurrency: 5
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
@ -28,31 +28,31 @@
performance_degradation: performance_degradation:
max_degradation: 50 max_degradation: 50
- -
name: Dummy.failure
description: Check failure_rate SLA plugin description: Check failure_rate SLA plugin
args: scenario:
sleep: 0.2 Dummy.failure:
from_iteration: 5 sleep: 0.2
to_iteration: 15 from_iteration: 5
each: 2 to_iteration: 15
each: 2
runner: runner:
type: "constant" constant:
times: 20 times: 20
concurrency: 5 concurrency: 5
sla: sla:
failure_rate: failure_rate:
min: 25 min: 25
max: 25 max: 25
- -
name: Dummy.dummy_timed_atomic_actions
description: Check max_avg_duration_per_atomic SLA plugin description: Check max_avg_duration_per_atomic SLA plugin
args: scenario:
number_of_actions: 5 Dummy.dummy_timed_atomic_actions:
sleep_factor: 1 number_of_actions: 5
sleep_factor: 1
runner: runner:
type: "constant" constant:
times: 3 times: 3
concurrency: 3 concurrency: 3
sla: sla:
max_avg_duration_per_atomic: max_avg_duration_per_atomic:
action_0: 1.0 action_0: 1.0
@ -65,40 +65,40 @@
title: Test constant runner title: Test constant runner
workloads: workloads:
- -
name: Dummy.dummy
description: "Check 'constant' runner." description: "Check 'constant' runner."
args: scenario:
sleep: 0.25 Dummy.dummy:
sleep: 0.25
runner: runner:
type: "constant" constant:
times: 8 times: 8
concurrency: 4 concurrency: 4
max_cpu_count: 2 max_cpu_count: 2
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy scenario:
args: Dummy.dummy:
sleep: 0 sleep: 0
runner: runner:
type: "constant" constant:
times: 4500 times: 4500
concurrency: 20 concurrency: 20
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: > description: >
Check the ability of constant runner to terminate scenario by timeout. Check the ability of constant runner to terminate scenario by timeout.
args: scenario:
sleep: 30 Dummy.dummy:
sleep: 30
runner: runner:
type: "constant" constant:
times: 2 times: 2
concurrency: 2 concurrency: 2
timeout: 1 timeout: 1
sla: sla:
failure_rate: failure_rate:
min: 100 min: 100
@ -107,14 +107,14 @@
title: Test constant_for_duration runner title: Test constant_for_duration runner
workloads: workloads:
- -
name: Dummy.dummy
description: "Check 'constant_for_duration' runner." description: "Check 'constant_for_duration' runner."
args: scenario:
sleep: 0.1 Dummy.dummy:
sleep: 0.1
runner: runner:
type: "constant_for_duration" constant_for_duration:
duration: 5 duration: 5
concurrency: 5 concurrency: 5
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
@ -123,104 +123,104 @@
title: Test rps runner title: Test rps runner
workloads: workloads:
- -
name: Dummy.dummy
description: "Check 'rps' runner." description: "Check 'rps' runner."
args: scenario:
sleep: 0.001 Dummy.dummy:
sleep: 0.001
runner: runner:
type: "rps" rps:
times: 2000 times: 2000
rps: 200 rps: 200
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: > description: >
Check 'rps' runner with float value of requests per second. Check 'rps' runner with float value of requests per second.
args: scenario:
sleep: 0.1 Dummy.dummy:
sleep: 0.1
runner: runner:
type: "rps" rps:
times: 5 times: 5
rps: 0.5 rps: 0.5
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: > description: >
Check 'rps' runner with float value of requests per second. Check 'rps' runner with float value of requests per second.
args: scenario:
sleep: 0.1 Dummy.dummy:
sleep: 0.1
runner: runner:
type: "rps" rps:
times: 5 times: 5
rps: 0.2 rps: 0.2
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: > description: >
Check 'max_concurrency' and 'max_cpu_count' properties of 'rps' runner. Check 'max_concurrency' and 'max_cpu_count' properties of 'rps' runner.
args: scenario:
sleep: 0.001 Dummy.dummy:
sleep: 0.001
runner: runner:
type: "rps"
times: 200
rps: 20
max_concurrency: 10
max_cpu_count: 3
sla:
failure_rate:
max: 0
-
name: Dummy.dummy
description: "Check 'rps' with start, end, step arguments"
args:
sleep: 0.25
runner:
type: "rps"
times: 55
rps: rps:
start: 1 times: 200
end: 10 rps: 20
step: 1 max_concurrency: 10
max_concurrency: 10 max_cpu_count: 3
max_cpu_count: 3
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: "Check 'rps' with start, end, step arguments" description: "Check 'rps' with start, end, step arguments"
args: scenario:
sleep: 0.5 Dummy.dummy:
sleep: 0.25
runner: runner:
type: "rps"
times: 55
rps: rps:
start: 1 times: 55
end: 10 rps:
step: 1 start: 1
duration: 2 end: 10
max_concurrency: 10 step: 1
max_cpu_count: 3 max_concurrency: 10
max_cpu_count: 3
sla:
failure_rate:
max: 0
-
description: "Check 'rps' with start, end, step arguments"
scenario:
Dummy.dummy:
sleep: 0.5
runner:
rps:
times: 55
rps:
start: 1
end: 10
step: 1
duration: 2
max_concurrency: 10
max_cpu_count: 3
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: > description: >
Check the ability of rps runner to terminate scenario by timeout. Check the ability of rps runner to terminate scenario by timeout.
args: scenario:
sleep: 30 Dummy.dummy:
sleep: 30
runner: runner:
type: "rps" rps:
times: 1 times: 1
rps: 1 rps: 1
timeout: 1 timeout: 1
sla: sla:
failure_rate: failure_rate:
min: 100 min: 100
@ -229,13 +229,13 @@
title: Test serial runner title: Test serial runner
workloads: workloads:
- -
name: Dummy.dummy
description: "Check 'serial' runner." description: "Check 'serial' runner."
args: scenario:
sleep: 0.1 Dummy.dummy:
sleep: 0.1
runner: runner:
type: "serial" serial:
times: 20 times: 20
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
@ -244,14 +244,14 @@
title: Test Hook and Trigger plugins title: Test Hook and Trigger plugins
workloads: workloads:
- -
name: Dummy.dummy
description: "Check sys_call hook." description: "Check sys_call hook."
args: scenario:
sleep: 0.75 Dummy.dummy:
sleep: 0.75
runner: runner:
type: "constant" constant:
times: 20 times: 20
concurrency: 2 concurrency: 2
hooks: hooks:
- name: sys_call - name: sys_call
description: Run script description: Run script
@ -281,14 +281,14 @@
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: "Check periodic trigger with iteration unit." description: "Check periodic trigger with iteration unit."
args: scenario:
sleep: 0.25 Dummy.dummy:
sleep: 0.25
runner: runner:
type: "constant" constant:
times: 10 times: 10
concurrency: 2 concurrency: 2
hooks: hooks:
- name: sys_call - name: sys_call
description: test hook description: test hook
@ -304,14 +304,13 @@
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: "Check event trigger args." description: "Check event trigger args."
args: scenario:
sleep: 1 Dummy.dummy:
sleep: 1
runner: runner:
type: "constant" serial:
times: 10 times: 10
concurrency: 1
hooks: hooks:
- name: sys_call - name: sys_call
description: Get system name description: Get system name
@ -325,14 +324,13 @@
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy
description: "Check periodic trigger with time unit." description: "Check periodic trigger with time unit."
args: scenario:
sleep: 1 Dummy.dummy:
sleep: 1
runner: runner:
type: "constant" serial:
times: 10 times: 10
concurrency: 1
hooks: hooks:
- name: sys_call - name: sys_call
description: test hook description: test hook
@ -349,66 +347,67 @@
title: Test Dummy scenarios title: Test Dummy scenarios
workloads: workloads:
- -
name: Dummy.dummy_exception scenario:
args: Dummy.dummy_exception:
size_of_message: 5 size_of_message: 5
runner: runner:
type: "constant" constant:
times: 20 times: 20
concurrency: 5 concurrency: 5
- -
name: Dummy.dummy_exception_probability scenario:
args: Dummy.dummy_exception_probability:
exception_probability: 0.05 exception_probability: 0.05
runner: runner:
type: "constant" serial:
times: 2042 times: 2042
concurrency: 1
- -
name: Dummy.dummy_exception_probability scenario:
args: Dummy.dummy_exception_probability:
exception_probability: 0.5 exception_probability: 0.5
runner: runner:
type: "constant" serial:
times: 100 times: 100
concurrency: 1
sla: sla:
failure_rate: failure_rate:
min: 20 min: 20
max: 80 max: 80
- -
name: Dummy.dummy_output scenario:
Dummy.dummy_output: {}
runner: runner:
type: "constant" constant:
times: 20 times: 20
concurrency: 10 concurrency: 10
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
- -
name: Dummy.dummy_random_fail_in_atomic scenario:
args: Dummy.dummy_random_fail_in_atomic:
exception_probability: 0.5 exception_probability: 0.5
runner: runner:
type: "constant" constant:
times: 50 times: 50
concurrency: 10 concurrency: 10
- -
name: Dummy.dummy_random_action scenario:
Dummy.dummy_random_action: {}
runner: runner:
type: "constant" constant:
times: 10 times: 10
concurrency: 5 concurrency: 5
- -
title: Test function based scenario title: Test function based scenario
workloads: workloads:
- -
name: FakePlugin.testplugin scenario:
FakePlugin.testplugin: {}
runner: runner:
type: "constant" constant:
times: 4 times: 4
concurrency: 4 concurrency: 4
sla: sla:
failure_rate: failure_rate:
max: 0 max: 0
@ -417,39 +416,39 @@
title: Profile generate_random_name method title: Profile generate_random_name method
workloads: workloads:
- -
name: RallyProfile.generate_names_in_atomic scenario:
args: RallyProfile.generate_names_in_atomic:
number_of_names: 100 number_of_names: 100
runner: runner:
type: "constant" constant:
times: 1000 times: 1000
concurrency: 10 concurrency: 10
sla: sla:
max_avg_duration_per_atomic: max_avg_duration_per_atomic:
generate_100_names: 0.015 generate_100_names: 0.015
failure_rate: failure_rate:
max: 0 max: 0
- -
name: RallyProfile.generate_names_in_atomic scenario:
args: RallyProfile.generate_names_in_atomic:
number_of_names: 1000 number_of_names: 1000
runner: runner:
type: "constant" constant:
times: 500 times: 500
concurrency: 10 concurrency: 10
sla: sla:
max_avg_duration_per_atomic: max_avg_duration_per_atomic:
generate_1000_names: 0.1 generate_1000_names: 0.1
failure_rate: failure_rate:
max: 0 max: 0
- -
name: RallyProfile.generate_names_in_atomic scenario:
args: RallyProfile.generate_names_in_atomic:
number_of_names: 10000 number_of_names: 10000
runner: runner:
type: "constant" constant:
times: 200 times: 200
concurrency: 10 concurrency: 10
sla: sla:
max_avg_duration_per_atomic: max_avg_duration_per_atomic:
generate_10000_names: 1 generate_10000_names: 1
@ -460,26 +459,26 @@
title: Profile atomic actions title: Profile atomic actions
workloads: workloads:
- -
name: RallyProfile.calculate_atomic scenario:
args: RallyProfile.calculate_atomic:
number_of_atomics: 100 number_of_atomics: 100
runner: runner:
type: "constant" constant:
times: 300 times: 300
concurrency: 10 concurrency: 10
sla: sla:
max_avg_duration_per_atomic: max_avg_duration_per_atomic:
calculate_100_atomics: 0.04 calculate_100_atomics: 0.04
failure_rate: failure_rate:
max: 0 max: 0
- -
name: RallyProfile.calculate_atomic scenario:
args: RallyProfile.calculate_atomic:
number_of_atomics: 500 number_of_atomics: 500
runner: runner:
type: "constant" constant:
times: 100 times: 100
concurrency: 10 concurrency: 10
sla: sla:
max_avg_duration_per_atomic: max_avg_duration_per_atomic:
calculate_500_atomics: 0.5 calculate_500_atomics: 0.5

View File

@ -638,27 +638,20 @@ class TaskConfig(object):
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"name": {"type": "string"}, "scenario": {
"$ref": "#/definitions/singleEntity"},
"description": {"type": "string"}, "description": {"type": "string"},
"args": {"type": "object"},
"runner": { "runner": {
"type": "object", "$ref": "#/definitions/singleEntity"},
"properties": {
"type": {"type": "string"}
},
"required": ["type"]
},
"sla": {"type": "object"}, "sla": {"type": "object"},
"hooks": { "hooks": {
"type": "array", "type": "array",
"items": HOOK_CONFIG, "items": HOOK_CONFIG,
}, },
"context": {"type": "object"} "contexts": {"type": "object"}
}, },
"additionalProperties": False, "additionalProperties": False,
"required": ["name", "runner"] "required": ["scenario", "runner"]
} }
} }
}, },
@ -668,7 +661,17 @@ class TaskConfig(object):
} }
}, },
"additionalProperties": False, "additionalProperties": False,
"required": ["title", "subtasks"] "required": ["title", "subtasks"],
"definitions": {
"singleEntity": {
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"patternProperties": {
".*": {"type": "object"}
}
}
}
} }
CONFIG_SCHEMAS = {1: CONFIG_SCHEMA_V1, 2: CONFIG_SCHEMA_V2} CONFIG_SCHEMAS = {1: CONFIG_SCHEMA_V1, 2: CONFIG_SCHEMA_V2}
@ -676,13 +679,15 @@ class TaskConfig(object):
def __init__(self, config): def __init__(self, config):
"""TaskConfig constructor. """TaskConfig constructor.
Validates and represents different versions of task configuration in
unified form.
:param config: Dict with configuration of specified task :param config: Dict with configuration of specified task
:raises Exception: in case of validation error. (This gets reraised as
InvalidTaskException. if we raise it here as InvalidTaskException,
then "Task config is invalid: " gets prepended to the message twice
""" """
if config is None: if config is None:
# NOTE(stpierre): This gets reraised as
# InvalidTaskException. if we raise it here as
# InvalidTaskException, then "Task config is invalid: "
# gets prepended to the message twice.
raise Exception(_("Input task is empty")) raise Exception(_("Input task is empty"))
self.version = self._get_version(config) self.version = self._get_version(config)
@ -708,7 +713,13 @@ class TaskConfig(object):
workloads = [] workloads = []
for position, wconf in enumerate(sconf["workloads"]): for position, wconf in enumerate(sconf["workloads"]):
# fill all missed properties of a Workload # fill all missed properties of a Workload
wconf["name"], wconf["args"] = list(
wconf["scenario"].items())[0]
del wconf["scenario"]
wconf["position"] = position wconf["position"] = position
if not wconf.get("description", ""): if not wconf.get("description", ""):
try: try:
wconf["description"] = scenario.Scenario.get( wconf["description"] = scenario.Scenario.get(
@ -718,12 +729,16 @@ class TaskConfig(object):
# let's fail an issue with loading plugin at a # let's fail an issue with loading plugin at a
# validation step # validation step
pass pass
wconf.setdefault("args", {})
wconf.setdefault("context", {}) wconf["context"] = wconf.pop("contexts", {})
wconf.setdefault("runner", {})
runner_type, runner_cfg = list(
wconf["runner"].items())[0]
runner_cfg["type"] = runner_type
wconf["runner"] = runner_cfg
wconf.setdefault("sla", {}) wconf.setdefault("sla", {})
wconf.setdefault("hooks", []) wconf.setdefault("hooks", [])
# store hooks in the format which we have in db
wconf["hooks"] = [{"config": h} for h in wconf["hooks"]] wconf["hooks"] = [{"config": h} for h in wconf["hooks"]]
workloads.append(wconf) workloads.append(wconf)
sconf["workloads"] = workloads sconf["workloads"] = workloads
@ -755,7 +770,13 @@ class TaskConfig(object):
for name, v1_workloads in config.items(): for name, v1_workloads in config.items():
for v1_workload in v1_workloads: for v1_workload in v1_workloads:
v2_workload = copy.deepcopy(v1_workload) v2_workload = copy.deepcopy(v1_workload)
v2_workload["name"] = name v2_workload["scenario"] = {name: v2_workload.pop("args", {})}
v2_workload["sla"] = v2_workload.pop("sla", {})
v2_workload["contexts"] = v2_workload.pop("context", {})
if "runner" in v2_workload:
runner_type = v2_workload["runner"].pop("type")
v2_workload["runner"] = {
runner_type: v2_workload["runner"]}
subtasks.append({"title": name, "workloads": [v2_workload]}) subtasks.append({"title": name, "workloads": [v2_workload]})
return {"title": "Task (adopted from task format v1)", return {"title": "Task (adopted from task format v1)",
"subtasks": subtasks} "subtasks": subtasks}

View File

@ -1020,53 +1020,31 @@ class TaskTestCase(test.TestCase):
self.assertRaises(exceptions.InvalidTaskException, engine.TaskConfig, self.assertRaises(exceptions.InvalidTaskException, engine.TaskConfig,
mock.MagicMock) mock.MagicMock)
@mock.patch("rally.task.engine.TaskConfig._get_version") def test__adopt_task_format_v1(self):
@mock.patch("rally.task.engine.TaskConfig._validate_json")
def test__adopt_task_format_v1( # mock all redundant checks :)
self, mock_task_config__validate_json, class TaskConfig(engine.TaskConfig):
mock_task_config__get_version): def __init__(self):
mock_task_config__get_version.return_value = 1 pass
config = collections.OrderedDict() config = collections.OrderedDict()
config["a.task"] = [{"s": 1}, {"s": 2}] config["a.task"] = [{"s": 1, "context": {"foo": "bar"}}, {"s": 2}]
config["b.task"] = [{"s": 3}] config["b.task"] = [{"s": 3, "sla": {"key": "value"}}]
self.assertEqual([ self.assertEqual(
{"title": "a.task", {"title": "Task (adopted from task format v1)",
"context": {}, "subtasks": [{"title": "a.task",
"description": None, "workloads": [{"s": 1,
"group": None, "scenario": {"a.task": {}},
"tags": [], "sla": {},
"workloads": [{"s": 1, "contexts": {"foo": "bar"}}]},
"name": "a.task", {"title": "a.task",
"args": {}, "workloads": [{"s": 2,
"context": {}, "scenario": {"a.task": {}},
"runner": {}, "sla": {},
"sla": {}, "contexts": {}}]},
"hooks": [], {"title": "b.task",
"position": 0}]}, "workloads": [{"s": 3,
{"title": "a.task", "scenario": {"b.task": {}},
"context": {}, "sla": {"key": "value"},
"description": None, "contexts": {}}]}]},
"group": None, TaskConfig._adopt_task_format_v1(config))
"tags": [],
"workloads": [{"s": 2,
"name": "a.task",
"args": {},
"context": {},
"runner": {},
"sla": {},
"hooks": [],
"position": 0}]},
{"title": "b.task",
"context": {},
"description": None,
"group": None,
"tags": [],
"workloads": [{"s": 3,
"name": "b.task",
"args": {},
"context": {},
"runner": {},
"sla": {},
"hooks": [],
"position": 0}]}
], engine.TaskConfig(config).subtasks)