showard | 26b7ec7 | 2009-12-21 22:43:57 +0000 | [diff] [blame] | 1 | """\ |
| 2 | Functions to expose over the RPC interface. |
| 3 | """ |
| 4 | |
| 5 | __author__ = 'jamesren@google.com (James Ren)' |
| 6 | |
| 7 | |
jamesren | 9a6f5f6 | 2010-05-05 22:55:54 +0000 | [diff] [blame^] | 8 | import os, re |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 9 | import common |
| 10 | from django.db import models as django_models |
| 11 | from autotest_lib.frontend import thread_local |
| 12 | from autotest_lib.frontend.afe import model_logic, models as afe_models |
| 13 | from autotest_lib.frontend.afe import rpc_utils as afe_rpc_utils |
jamesren | 9a6f5f6 | 2010-05-05 22:55:54 +0000 | [diff] [blame^] | 14 | from autotest_lib.frontend.afe import rpc_interface as afe_rpc_interface |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 15 | from autotest_lib.frontend.tko import models as tko_models |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 16 | from autotest_lib.frontend.planner import models, rpc_utils, model_attributes |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 17 | from autotest_lib.frontend.planner import failure_actions |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 18 | from autotest_lib.client.common_lib import utils |
| 19 | |
| 20 | # basic getter/setter calls |
| 21 | # TODO: deprecate the basic calls and reimplement them in the REST framework |
| 22 | |
| 23 | def get_plan(id): |
| 24 | return afe_rpc_utils.prepare_for_serialization( |
| 25 | models.Plan.smart_get(id).get_object_dict()) |
| 26 | |
| 27 | |
| 28 | def modify_plan(id, **data): |
| 29 | models.Plan.smart_get(id).update_object(data) |
| 30 | |
| 31 | |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 32 | def modify_test_run(id, **data): |
| 33 | models.TestRun.objects.get(id=id).update_object(data) |
| 34 | |
| 35 | |
| 36 | def modify_host(id, **data): |
| 37 | models.Host.objects.get(id=id).update_object(data) |
| 38 | |
| 39 | |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 40 | def add_job(plan_id, test_config_id, afe_job_id): |
| 41 | models.Job.objects.create( |
| 42 | plan=models.Plan.objects.get(id=plan_id), |
| 43 | test_config=models.TestConfig.objects.get(id=test_config_id), |
| 44 | afe_job=afe_models.Job.objects.get(id=afe_job_id)) |
| 45 | |
| 46 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 47 | # more advanced calls |
| 48 | |
jamesren | 9a6f5f6 | 2010-05-05 22:55:54 +0000 | [diff] [blame^] | 49 | def submit_plan(name, hosts, host_labels, tests, support=None, |
| 50 | label_override=None, additional_parameters=None): |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 51 | """ |
| 52 | Submits a plan to the Test Planner |
| 53 | |
| 54 | @param name: the name of the plan |
| 55 | @param hosts: a list of hostnames |
| 56 | @param host_labels: a list of host labels. The hosts under test will update |
| 57 | to reflect changes in the label |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 58 | @param tests: an ordered list of dictionaries: |
| 59 | alias: an alias for the test |
| 60 | control_file: the test control file |
| 61 | is_server: True if is a server-side control file |
| 62 | estimated_runtime: estimated number of hours this test |
| 63 | will run |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame] | 64 | @param support: the global support script |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 65 | @param label_override: label to prepend to all AFE jobs for this test plan. |
| 66 | Defaults to the plan name. |
jamesren | 9a6f5f6 | 2010-05-05 22:55:54 +0000 | [diff] [blame^] | 67 | @param additional_parameters: A mapping of AdditionalParameters to apply to |
| 68 | this test plan, as an ordered list. Each item |
| 69 | of the list is a dictionary: |
| 70 | hostname_regex: A regular expression; the |
| 71 | additional parameter in the |
| 72 | value will be applied if the |
| 73 | hostname matches this regex |
| 74 | param_type: The type of additional parameter |
| 75 | param_values: A dictionary of key=value pairs |
| 76 | for this parameter |
| 77 | example: |
| 78 | [{'hostname_regex': 'host[0-9]', |
| 79 | 'param_type': 'Verify', |
| 80 | 'param_values': {'key1': 'value1', |
| 81 | 'key2': 'value2'}}, |
| 82 | {'hostname_regex': '.*', |
| 83 | 'param_type': 'Verify', |
| 84 | 'param_values': {'key': 'value'}}] |
| 85 | |
| 86 | Currently, the only (non-site-specific) |
| 87 | param_type available is 'Verify'. Setting |
| 88 | these parameters allows the user to specify |
| 89 | arguments to the |
| 90 | job.run_test('verify_test', ...) line at the |
| 91 | beginning of the wrapped control file for each |
| 92 | test |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 93 | """ |
| 94 | host_objects = [] |
| 95 | label_objects = [] |
| 96 | |
| 97 | for host in hosts or []: |
| 98 | try: |
| 99 | host_objects.append( |
| 100 | afe_models.Host.valid_objects.get(hostname=host)) |
| 101 | except afe_models.Host.DoesNotExist: |
| 102 | raise model_logic.ValidationError( |
| 103 | {'hosts': 'host %s does not exist' % host}) |
| 104 | |
| 105 | for label in host_labels or []: |
| 106 | try: |
| 107 | label_objects.append(afe_models.Label.valid_objects.get(name=label)) |
| 108 | except afe_models.Label.DoesNotExist: |
| 109 | raise model_logic.ValidationError( |
| 110 | {'host_labels': 'host label %s does not exist' % label}) |
| 111 | |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 112 | aliases_seen = set() |
| 113 | test_required_fields = ( |
| 114 | 'alias', 'control_file', 'is_server', 'estimated_runtime') |
| 115 | for test in tests: |
| 116 | for field in test_required_fields: |
| 117 | if field not in test: |
| 118 | raise model_logic.ValidationError( |
| 119 | {'tests': 'field %s is required' % field}) |
| 120 | |
| 121 | alias = test['alias'] |
| 122 | if alias in aliases_seen: |
| 123 | raise model_logic.Validationerror( |
| 124 | {'tests': 'alias %s occurs more than once' % alias}) |
| 125 | aliases_seen.add(alias) |
| 126 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 127 | plan, created = models.Plan.objects.get_or_create(name=name) |
| 128 | if not created: |
| 129 | raise model_logic.ValidationError( |
| 130 | {'name': 'Plan name %s already exists' % name}) |
| 131 | |
| 132 | try: |
jamesren | 9a6f5f6 | 2010-05-05 22:55:54 +0000 | [diff] [blame^] | 133 | rpc_utils.set_additional_parameters(plan, additional_parameters) |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 134 | label = rpc_utils.create_plan_label(plan) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 135 | try: |
| 136 | for i, test in enumerate(tests): |
| 137 | control, _ = models.ControlFile.objects.get_or_create( |
| 138 | contents=test['control_file']) |
| 139 | models.TestConfig.objects.create( |
| 140 | plan=plan, alias=test['alias'], control_file=control, |
| 141 | is_server=test['is_server'], execution_order=i, |
| 142 | estimated_runtime=test['estimated_runtime']) |
| 143 | |
| 144 | plan.label_override = label_override |
| 145 | plan.support = support or '' |
| 146 | plan.save() |
| 147 | |
| 148 | plan.owners.add(afe_models.User.current_user()) |
| 149 | |
| 150 | for host in host_objects: |
| 151 | planner_host = models.Host.objects.create(plan=plan, host=host) |
| 152 | |
| 153 | plan.host_labels.add(*label_objects) |
| 154 | |
| 155 | rpc_utils.start_plan(plan, label) |
| 156 | |
| 157 | return plan.id |
| 158 | except: |
| 159 | label.delete() |
| 160 | raise |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 161 | except: |
| 162 | plan.delete() |
| 163 | raise |
| 164 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 165 | |
| 166 | def get_hosts(plan_id): |
| 167 | """ |
| 168 | Gets the hostnames of all the hosts in this test plan. |
| 169 | |
| 170 | Resolves host labels in the plan. |
| 171 | """ |
| 172 | plan = models.Plan.smart_get(plan_id) |
| 173 | |
| 174 | hosts = set(plan.hosts.all().values_list('hostname', flat=True)) |
| 175 | for label in plan.host_labels.all(): |
| 176 | hosts.update(label.host_set.all().values_list('hostname', flat=True)) |
| 177 | |
| 178 | return afe_rpc_utils.prepare_for_serialization(hosts) |
| 179 | |
| 180 | |
| 181 | def get_atomic_group_control_file(): |
| 182 | """ |
| 183 | Gets the control file to apply the atomic group for a set of machines |
| 184 | """ |
| 185 | return rpc_utils.lazy_load(os.path.join(os.path.dirname(__file__), |
| 186 | 'set_atomic_group_control.srv')) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 187 | |
| 188 | |
| 189 | def get_next_test_configs(plan_id): |
| 190 | """ |
| 191 | Gets information about the next planner test configs that need to be run |
| 192 | |
| 193 | @param plan_id: the ID or name of the test plan |
| 194 | @return a dictionary: |
| 195 | complete: True or False, shows test plan completion |
| 196 | next_configs: a list of dictionaries: |
| 197 | host: ID of the host |
| 198 | next_test_config_id: ID of the next Planner test to run |
| 199 | """ |
| 200 | plan = models.Plan.smart_get(plan_id) |
| 201 | |
| 202 | result = {'next_configs': []} |
| 203 | |
| 204 | rpc_utils.update_hosts_table(plan) |
| 205 | for host in models.Host.objects.filter(plan=plan): |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame] | 206 | next_test_config = rpc_utils.compute_next_test_config(plan, host) |
| 207 | if next_test_config: |
| 208 | config = {'next_test_config_id': next_test_config.id, |
| 209 | 'next_test_config_alias': next_test_config.alias, |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 210 | 'host': host.host.hostname} |
| 211 | result['next_configs'].append(config) |
| 212 | |
| 213 | rpc_utils.check_for_completion(plan) |
| 214 | result['complete'] = plan.complete |
| 215 | |
| 216 | return result |
| 217 | |
| 218 | |
| 219 | def update_test_runs(plan_id): |
| 220 | """ |
| 221 | Add all applicable TKO jobs to the Planner DB tables |
| 222 | |
| 223 | Looks for tests in the TKO tables that were started as a part of the test |
| 224 | plan, and add them to the Planner tables. |
| 225 | |
| 226 | Also updates the status of the test run if the underlying TKO test move from |
| 227 | an active status to a completed status. |
| 228 | |
| 229 | @return a list of dictionaries: |
| 230 | status: the status of the new (or updated) test run |
| 231 | tko_test_idx: the ID of the TKO test added |
| 232 | hostname: the host added |
| 233 | """ |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 234 | plan = models.Plan.smart_get(plan_id) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 235 | updated = [] |
| 236 | |
| 237 | for planner_job in plan.job_set.all(): |
| 238 | known_statuses = dict((test_run.tko_test.test_idx, test_run.status) |
| 239 | for test_run in planner_job.testrun_set.all()) |
| 240 | tko_tests_for_job = tko_models.Test.objects.filter( |
| 241 | job__afe_job_id=planner_job.afe_job.id) |
| 242 | |
| 243 | for tko_test in tko_tests_for_job: |
| 244 | status = rpc_utils.compute_test_run_status(tko_test.status.word) |
| 245 | needs_update = (tko_test.test_idx not in known_statuses or |
| 246 | status != known_statuses[tko_test.test_idx]) |
| 247 | if needs_update: |
| 248 | hostnames = tko_test.machine.hostname.split(',') |
| 249 | for hostname in hostnames: |
| 250 | rpc_utils.add_test_run( |
| 251 | plan, planner_job, tko_test, hostname, status) |
| 252 | updated.append({'status': status, |
| 253 | 'tko_test_idx': tko_test.test_idx, |
| 254 | 'hostname': hostname}) |
| 255 | |
| 256 | return updated |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 257 | |
| 258 | |
| 259 | def get_failures(plan_id): |
| 260 | """ |
| 261 | Gets a list of the untriaged failures associated with this plan |
| 262 | |
| 263 | @return a list of dictionaries: |
| 264 | id: the failure ID, for passing back to triage the failure |
| 265 | group: the group for the failure. Normally the same as the |
| 266 | reason, but can be different for custom queries |
| 267 | machine: the failed machine |
| 268 | blocked: True if the failure caused the machine to block |
| 269 | test_name: Concatenation of the Planner alias and the TKO test |
| 270 | name for the failed test |
| 271 | reason: test failure reason |
| 272 | seen: True if the failure is marked as "seen" |
| 273 | """ |
| 274 | plan = models.Plan.smart_get(plan_id) |
| 275 | result = {} |
| 276 | |
| 277 | failures = plan.testrun_set.filter( |
| 278 | finalized=True, triaged=False, |
| 279 | status=model_attributes.TestRunStatus.FAILED) |
jamesren | 6275824 | 2010-04-28 18:08:25 +0000 | [diff] [blame] | 280 | failures = failures.order_by('seen').select_related('test_job__test', |
| 281 | 'host__host', |
| 282 | 'tko_test') |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 283 | for failure in failures: |
jamesren | 6275824 | 2010-04-28 18:08:25 +0000 | [diff] [blame] | 284 | test_name = '%s: %s' % ( |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 285 | failure.test_job.test_config.alias, failure.tko_test.test) |
| 286 | |
| 287 | group_failures = result.setdefault(failure.tko_test.reason, []) |
| 288 | failure_dict = {'id': failure.id, |
| 289 | 'machine': failure.host.host.hostname, |
| 290 | 'blocked': bool(failure.host.blocked), |
| 291 | 'test_name': test_name, |
| 292 | 'reason': failure.tko_test.reason, |
| 293 | 'seen': bool(failure.seen)} |
| 294 | group_failures.append(failure_dict) |
| 295 | |
| 296 | return result |
| 297 | |
| 298 | |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame] | 299 | def get_test_runs(**filter_data): |
| 300 | """ |
| 301 | Gets a list of test runs that match the filter data. |
| 302 | |
| 303 | Returns a list of expanded TestRun object dictionaries. Specifically, the |
| 304 | "host" and "test_job" fields are expanded. Additionally, the "test_config" |
| 305 | field of the "test_job" expansion is also expanded. |
| 306 | """ |
| 307 | result = [] |
| 308 | for test_run in models.TestRun.objects.filter(**filter_data): |
| 309 | test_run_dict = test_run.get_object_dict() |
| 310 | test_run_dict['host'] = test_run.host.get_object_dict() |
| 311 | test_run_dict['test_job'] = test_run.test_job.get_object_dict() |
| 312 | test_run_dict['test_job']['test_config'] = ( |
| 313 | test_run.test_job.test_config.get_object_dict()) |
| 314 | result.append(test_run_dict) |
| 315 | return result |
| 316 | |
| 317 | |
| 318 | def skip_test(test_config_id, hostname): |
| 319 | """ |
| 320 | Marks a test config as "skipped" for a given host |
| 321 | """ |
| 322 | config = models.TestConfig.objects.get(id=test_config_id) |
| 323 | config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname)) |
| 324 | |
| 325 | |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 326 | def mark_failures_as_seen(failure_ids): |
| 327 | """ |
| 328 | Marks a set of failures as 'seen' |
| 329 | |
| 330 | @param failure_ids: A list of failure IDs, as returned by get_failures(), to |
| 331 | mark as seen |
| 332 | """ |
| 333 | models.TestRun.objects.filter(id__in=failure_ids).update(seen=True) |
| 334 | |
| 335 | |
jamesren | 6275824 | 2010-04-28 18:08:25 +0000 | [diff] [blame] | 336 | def process_failures(failure_ids, host_action, test_action, labels=(), |
| 337 | keyvals=None, bugs=(), reason=None, invalidate=False): |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 338 | """ |
| 339 | Triage a failure |
| 340 | |
| 341 | @param failure_id: The failure ID, as returned by get_failures() |
| 342 | @param host_action: One of 'Block', 'Unblock', 'Reinstall' |
| 343 | @param test_action: One of 'Skip', 'Rerun' |
| 344 | |
| 345 | @param labels: Test labels to apply, by name |
| 346 | @param keyvals: Dictionary of job keyvals to add (or replace) |
| 347 | @param bugs: List of bug IDs to associate with this failure |
| 348 | @param reason: An override for the test failure reason |
| 349 | @param invalidate: True if failure should be invalidated for the purposes of |
| 350 | reporting. Defaults to False. |
| 351 | """ |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 352 | host_choices = failure_actions.HostAction.values |
| 353 | test_choices = failure_actions.TestAction.values |
| 354 | if host_action not in host_choices: |
| 355 | raise model_logic.ValidationError( |
| 356 | {'host_action': ('host action %s not valid; must be one of %s' |
| 357 | % (host_action, ', '.join(host_choices)))}) |
| 358 | if test_action not in test_choices: |
| 359 | raise model_logic.ValidationError( |
| 360 | {'test_action': ('test action %s not valid; must be one of %s' |
| 361 | % (test_action, ', '.join(test_choices)))}) |
| 362 | |
jamesren | 6275824 | 2010-04-28 18:08:25 +0000 | [diff] [blame] | 363 | for failure_id in failure_ids: |
| 364 | rpc_utils.process_failure( |
| 365 | failure_id=failure_id, host_action=host_action, |
| 366 | test_action=test_action, labels=labels, keyvals=keyvals, |
| 367 | bugs=bugs, reason=reason, invalidate=invalidate) |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 368 | |
| 369 | |
jamesren | 9a6f5f6 | 2010-05-05 22:55:54 +0000 | [diff] [blame^] | 370 | def generate_test_config(alias, afe_test_name=None, |
| 371 | estimated_runtime=0, **kwargs): |
| 372 | """ |
| 373 | Creates and returns a test config suitable for passing into submit_plan() |
| 374 | |
| 375 | Also accepts optional parameters to pass directly in to the AFE RPC |
| 376 | interface's generate_control_file() method. |
| 377 | |
| 378 | @param alias: The alias for the test |
| 379 | @param afe_test_name: The name of the test, as shown on AFE |
| 380 | @param estimated_runtime: Estimated number of hours this test is expected to |
| 381 | run. For reporting purposes. |
| 382 | """ |
| 383 | if afe_test_name is None: |
| 384 | afe_test_name = alias |
| 385 | alias = alias.replace(' ', '_') |
| 386 | |
| 387 | control = afe_rpc_interface.generate_control_file(tests=[afe_test_name], |
| 388 | **kwargs) |
| 389 | |
| 390 | return {'alias': alias, |
| 391 | 'control_file': control['control_file'], |
| 392 | 'is_server': control['is_server'], |
| 393 | 'estimated_runtime': estimated_runtime} |
| 394 | |
| 395 | |
| 396 | def get_wrapped_test_config(id, hostname, run_verify): |
| 397 | """ |
| 398 | Gets the TestConfig object identified by the ID |
| 399 | |
| 400 | Returns the object dict of the TestConfig, plus an additional |
| 401 | 'wrapped_control_file' value, which includes the pre-processing that the |
| 402 | ControlParameters specify. |
| 403 | |
| 404 | @param hostname: Hostname of the machine this test config will run on |
| 405 | @param run_verify: Set to True or False to override the default behavior |
| 406 | (which is to run the verify test unless the skip_verify |
| 407 | ControlParameter is set) |
| 408 | """ |
| 409 | test_config = models.TestConfig.objects.get(id=id) |
| 410 | object_dict = test_config.get_object_dict() |
| 411 | object_dict['control_file'] = test_config.control_file.get_object_dict() |
| 412 | object_dict['wrapped_control_file'] = rpc_utils.wrap_control_file( |
| 413 | plan=test_config.plan, hostname=hostname, |
| 414 | run_verify=run_verify, test_config=test_config) |
| 415 | |
| 416 | return object_dict |
| 417 | |
| 418 | |
| 419 | def generate_additional_parameters(hostname_regex, param_type, param_values): |
| 420 | """ |
| 421 | Generates an AdditionalParamter dictionary, for passing in to submit_plan() |
| 422 | |
| 423 | Returns a dictionary. To use in submit_job(), put this dictionary into a |
| 424 | list (possibly with other additional_parameters dictionaries) |
| 425 | |
| 426 | @param hostname_regex: The hostname regular expression to match |
| 427 | @param param_type: One of get_static_data()['additional_parameter_types'] |
| 428 | @param param_values: Dictionary of key=value pairs for this parameter |
| 429 | """ |
| 430 | try: |
| 431 | re.compile(hostname_regex) |
| 432 | except Exception: |
| 433 | raise model_logic.ValidationError( |
| 434 | {'hostname_regex': '%s is not a valid regex' % hostname_regex}) |
| 435 | |
| 436 | if param_type not in model_attributes.AdditionalParameterType.values: |
| 437 | raise model_logic.ValidationError( |
| 438 | {'param_type': '%s is not a valid parameter type' % param_type}) |
| 439 | |
| 440 | if type(param_values) is not dict: |
| 441 | raise model_logic.ValidationError( |
| 442 | {'param_values': '%s is not a dictionary' % repr(param_values)}) |
| 443 | |
| 444 | return {'hostname_regex': hostname_regex, |
| 445 | 'param_type': param_type, |
| 446 | 'param_values': param_values} |
| 447 | |
| 448 | |
jamesren | 1602f3b | 2010-04-09 20:46:29 +0000 | [diff] [blame] | 449 | def get_motd(): |
| 450 | return afe_rpc_utils.get_motd() |
| 451 | |
| 452 | |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 453 | def get_static_data(): |
jamesren | 1602f3b | 2010-04-09 20:46:29 +0000 | [diff] [blame] | 454 | result = {'motd': get_motd(), |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 455 | 'host_actions': sorted(failure_actions.HostAction.values), |
jamesren | 9a6f5f6 | 2010-05-05 22:55:54 +0000 | [diff] [blame^] | 456 | 'test_actions': sorted(failure_actions.TestAction.values), |
| 457 | 'additional_parameter_types': |
| 458 | sorted(model_attributes.AdditionalParameterType.values)} |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 459 | return result |