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 | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 8 | import os |
| 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 | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 14 | from autotest_lib.frontend.tko import models as tko_models |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 15 | from autotest_lib.frontend.planner import models, rpc_utils, model_attributes |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 16 | from autotest_lib.frontend.planner import failure_actions |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 17 | from autotest_lib.client.common_lib import utils |
| 18 | |
| 19 | # basic getter/setter calls |
| 20 | # TODO: deprecate the basic calls and reimplement them in the REST framework |
| 21 | |
| 22 | def get_plan(id): |
| 23 | return afe_rpc_utils.prepare_for_serialization( |
| 24 | models.Plan.smart_get(id).get_object_dict()) |
| 25 | |
| 26 | |
| 27 | def modify_plan(id, **data): |
| 28 | models.Plan.smart_get(id).update_object(data) |
| 29 | |
| 30 | |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 31 | def modify_test_run(id, **data): |
| 32 | models.TestRun.objects.get(id=id).update_object(data) |
| 33 | |
| 34 | |
| 35 | def modify_host(id, **data): |
| 36 | models.Host.objects.get(id=id).update_object(data) |
| 37 | |
| 38 | |
| 39 | def get_test_config(id): |
| 40 | return afe_rpc_utils.prepare_rows_as_nested_dicts( |
| 41 | models.TestConfig.objects.filter(id=id), ('control_file',))[0] |
| 42 | |
| 43 | |
| 44 | def add_job(plan_id, test_config_id, afe_job_id): |
| 45 | models.Job.objects.create( |
| 46 | plan=models.Plan.objects.get(id=plan_id), |
| 47 | test_config=models.TestConfig.objects.get(id=test_config_id), |
| 48 | afe_job=afe_models.Job.objects.get(id=afe_job_id)) |
| 49 | |
| 50 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 51 | # more advanced calls |
| 52 | |
| 53 | def submit_plan(name, hosts, host_labels, tests, |
| 54 | support=None, label_override=None): |
| 55 | """ |
| 56 | Submits a plan to the Test Planner |
| 57 | |
| 58 | @param name: the name of the plan |
| 59 | @param hosts: a list of hostnames |
| 60 | @param host_labels: a list of host labels. The hosts under test will update |
| 61 | to reflect changes in the label |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 62 | @param tests: an ordered list of dictionaries: |
| 63 | alias: an alias for the test |
| 64 | control_file: the test control file |
| 65 | is_server: True if is a server-side control file |
| 66 | estimated_runtime: estimated number of hours this test |
| 67 | will run |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame] | 68 | @param support: the global support script |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 69 | @param label_override: label to prepend to all AFE jobs for this test plan. |
| 70 | Defaults to the plan name. |
| 71 | """ |
| 72 | host_objects = [] |
| 73 | label_objects = [] |
| 74 | |
| 75 | for host in hosts or []: |
| 76 | try: |
| 77 | host_objects.append( |
| 78 | afe_models.Host.valid_objects.get(hostname=host)) |
| 79 | except afe_models.Host.DoesNotExist: |
| 80 | raise model_logic.ValidationError( |
| 81 | {'hosts': 'host %s does not exist' % host}) |
| 82 | |
| 83 | for label in host_labels or []: |
| 84 | try: |
| 85 | label_objects.append(afe_models.Label.valid_objects.get(name=label)) |
| 86 | except afe_models.Label.DoesNotExist: |
| 87 | raise model_logic.ValidationError( |
| 88 | {'host_labels': 'host label %s does not exist' % label}) |
| 89 | |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 90 | aliases_seen = set() |
| 91 | test_required_fields = ( |
| 92 | 'alias', 'control_file', 'is_server', 'estimated_runtime') |
| 93 | for test in tests: |
| 94 | for field in test_required_fields: |
| 95 | if field not in test: |
| 96 | raise model_logic.ValidationError( |
| 97 | {'tests': 'field %s is required' % field}) |
| 98 | |
| 99 | alias = test['alias'] |
| 100 | if alias in aliases_seen: |
| 101 | raise model_logic.Validationerror( |
| 102 | {'tests': 'alias %s occurs more than once' % alias}) |
| 103 | aliases_seen.add(alias) |
| 104 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 105 | plan, created = models.Plan.objects.get_or_create(name=name) |
| 106 | if not created: |
| 107 | raise model_logic.ValidationError( |
| 108 | {'name': 'Plan name %s already exists' % name}) |
| 109 | |
| 110 | try: |
| 111 | label = rpc_utils.create_plan_label(plan) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 112 | try: |
| 113 | for i, test in enumerate(tests): |
| 114 | control, _ = models.ControlFile.objects.get_or_create( |
| 115 | contents=test['control_file']) |
| 116 | models.TestConfig.objects.create( |
| 117 | plan=plan, alias=test['alias'], control_file=control, |
| 118 | is_server=test['is_server'], execution_order=i, |
| 119 | estimated_runtime=test['estimated_runtime']) |
| 120 | |
| 121 | plan.label_override = label_override |
| 122 | plan.support = support or '' |
| 123 | plan.save() |
| 124 | |
| 125 | plan.owners.add(afe_models.User.current_user()) |
| 126 | |
| 127 | for host in host_objects: |
| 128 | planner_host = models.Host.objects.create(plan=plan, host=host) |
| 129 | |
| 130 | plan.host_labels.add(*label_objects) |
| 131 | |
| 132 | rpc_utils.start_plan(plan, label) |
| 133 | |
| 134 | return plan.id |
| 135 | except: |
| 136 | label.delete() |
| 137 | raise |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 138 | except: |
| 139 | plan.delete() |
| 140 | raise |
| 141 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 142 | |
| 143 | def get_hosts(plan_id): |
| 144 | """ |
| 145 | Gets the hostnames of all the hosts in this test plan. |
| 146 | |
| 147 | Resolves host labels in the plan. |
| 148 | """ |
| 149 | plan = models.Plan.smart_get(plan_id) |
| 150 | |
| 151 | hosts = set(plan.hosts.all().values_list('hostname', flat=True)) |
| 152 | for label in plan.host_labels.all(): |
| 153 | hosts.update(label.host_set.all().values_list('hostname', flat=True)) |
| 154 | |
| 155 | return afe_rpc_utils.prepare_for_serialization(hosts) |
| 156 | |
| 157 | |
| 158 | def get_atomic_group_control_file(): |
| 159 | """ |
| 160 | Gets the control file to apply the atomic group for a set of machines |
| 161 | """ |
| 162 | return rpc_utils.lazy_load(os.path.join(os.path.dirname(__file__), |
| 163 | 'set_atomic_group_control.srv')) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 164 | |
| 165 | |
| 166 | def get_next_test_configs(plan_id): |
| 167 | """ |
| 168 | Gets information about the next planner test configs that need to be run |
| 169 | |
| 170 | @param plan_id: the ID or name of the test plan |
| 171 | @return a dictionary: |
| 172 | complete: True or False, shows test plan completion |
| 173 | next_configs: a list of dictionaries: |
| 174 | host: ID of the host |
| 175 | next_test_config_id: ID of the next Planner test to run |
| 176 | """ |
| 177 | plan = models.Plan.smart_get(plan_id) |
| 178 | |
| 179 | result = {'next_configs': []} |
| 180 | |
| 181 | rpc_utils.update_hosts_table(plan) |
| 182 | for host in models.Host.objects.filter(plan=plan): |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame] | 183 | next_test_config = rpc_utils.compute_next_test_config(plan, host) |
| 184 | if next_test_config: |
| 185 | config = {'next_test_config_id': next_test_config.id, |
| 186 | 'next_test_config_alias': next_test_config.alias, |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 187 | 'host': host.host.hostname} |
| 188 | result['next_configs'].append(config) |
| 189 | |
| 190 | rpc_utils.check_for_completion(plan) |
| 191 | result['complete'] = plan.complete |
| 192 | |
| 193 | return result |
| 194 | |
| 195 | |
| 196 | def update_test_runs(plan_id): |
| 197 | """ |
| 198 | Add all applicable TKO jobs to the Planner DB tables |
| 199 | |
| 200 | Looks for tests in the TKO tables that were started as a part of the test |
| 201 | plan, and add them to the Planner tables. |
| 202 | |
| 203 | Also updates the status of the test run if the underlying TKO test move from |
| 204 | an active status to a completed status. |
| 205 | |
| 206 | @return a list of dictionaries: |
| 207 | status: the status of the new (or updated) test run |
| 208 | tko_test_idx: the ID of the TKO test added |
| 209 | hostname: the host added |
| 210 | """ |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 211 | plan = models.Plan.smart_get(plan_id) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 212 | updated = [] |
| 213 | |
| 214 | for planner_job in plan.job_set.all(): |
| 215 | known_statuses = dict((test_run.tko_test.test_idx, test_run.status) |
| 216 | for test_run in planner_job.testrun_set.all()) |
| 217 | tko_tests_for_job = tko_models.Test.objects.filter( |
| 218 | job__afe_job_id=planner_job.afe_job.id) |
| 219 | |
| 220 | for tko_test in tko_tests_for_job: |
| 221 | status = rpc_utils.compute_test_run_status(tko_test.status.word) |
| 222 | needs_update = (tko_test.test_idx not in known_statuses or |
| 223 | status != known_statuses[tko_test.test_idx]) |
| 224 | if needs_update: |
| 225 | hostnames = tko_test.machine.hostname.split(',') |
| 226 | for hostname in hostnames: |
| 227 | rpc_utils.add_test_run( |
| 228 | plan, planner_job, tko_test, hostname, status) |
| 229 | updated.append({'status': status, |
| 230 | 'tko_test_idx': tko_test.test_idx, |
| 231 | 'hostname': hostname}) |
| 232 | |
| 233 | return updated |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 234 | |
| 235 | |
| 236 | def get_failures(plan_id): |
| 237 | """ |
| 238 | Gets a list of the untriaged failures associated with this plan |
| 239 | |
| 240 | @return a list of dictionaries: |
| 241 | id: the failure ID, for passing back to triage the failure |
| 242 | group: the group for the failure. Normally the same as the |
| 243 | reason, but can be different for custom queries |
| 244 | machine: the failed machine |
| 245 | blocked: True if the failure caused the machine to block |
| 246 | test_name: Concatenation of the Planner alias and the TKO test |
| 247 | name for the failed test |
| 248 | reason: test failure reason |
| 249 | seen: True if the failure is marked as "seen" |
| 250 | """ |
| 251 | plan = models.Plan.smart_get(plan_id) |
| 252 | result = {} |
| 253 | |
| 254 | failures = plan.testrun_set.filter( |
| 255 | finalized=True, triaged=False, |
| 256 | status=model_attributes.TestRunStatus.FAILED) |
| 257 | failures = failures.select_related('test_job__test', 'host__host', |
| 258 | 'tko_test') |
| 259 | for failure in failures: |
| 260 | test_name = '%s:%s' % ( |
| 261 | failure.test_job.test_config.alias, failure.tko_test.test) |
| 262 | |
| 263 | group_failures = result.setdefault(failure.tko_test.reason, []) |
| 264 | failure_dict = {'id': failure.id, |
| 265 | 'machine': failure.host.host.hostname, |
| 266 | 'blocked': bool(failure.host.blocked), |
| 267 | 'test_name': test_name, |
| 268 | 'reason': failure.tko_test.reason, |
| 269 | 'seen': bool(failure.seen)} |
| 270 | group_failures.append(failure_dict) |
| 271 | |
| 272 | return result |
| 273 | |
| 274 | |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame] | 275 | def get_test_runs(**filter_data): |
| 276 | """ |
| 277 | Gets a list of test runs that match the filter data. |
| 278 | |
| 279 | Returns a list of expanded TestRun object dictionaries. Specifically, the |
| 280 | "host" and "test_job" fields are expanded. Additionally, the "test_config" |
| 281 | field of the "test_job" expansion is also expanded. |
| 282 | """ |
| 283 | result = [] |
| 284 | for test_run in models.TestRun.objects.filter(**filter_data): |
| 285 | test_run_dict = test_run.get_object_dict() |
| 286 | test_run_dict['host'] = test_run.host.get_object_dict() |
| 287 | test_run_dict['test_job'] = test_run.test_job.get_object_dict() |
| 288 | test_run_dict['test_job']['test_config'] = ( |
| 289 | test_run.test_job.test_config.get_object_dict()) |
| 290 | result.append(test_run_dict) |
| 291 | return result |
| 292 | |
| 293 | |
| 294 | def skip_test(test_config_id, hostname): |
| 295 | """ |
| 296 | Marks a test config as "skipped" for a given host |
| 297 | """ |
| 298 | config = models.TestConfig.objects.get(id=test_config_id) |
| 299 | config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname)) |
| 300 | |
| 301 | |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 302 | def mark_failures_as_seen(failure_ids): |
| 303 | """ |
| 304 | Marks a set of failures as 'seen' |
| 305 | |
| 306 | @param failure_ids: A list of failure IDs, as returned by get_failures(), to |
| 307 | mark as seen |
| 308 | """ |
| 309 | models.TestRun.objects.filter(id__in=failure_ids).update(seen=True) |
| 310 | |
| 311 | |
| 312 | def process_failure(failure_id, host_action, test_action, labels=(), |
| 313 | keyvals=None, bugs=(), reason=None, invalidate=False): |
| 314 | """ |
| 315 | Triage a failure |
| 316 | |
| 317 | @param failure_id: The failure ID, as returned by get_failures() |
| 318 | @param host_action: One of 'Block', 'Unblock', 'Reinstall' |
| 319 | @param test_action: One of 'Skip', 'Rerun' |
| 320 | |
| 321 | @param labels: Test labels to apply, by name |
| 322 | @param keyvals: Dictionary of job keyvals to add (or replace) |
| 323 | @param bugs: List of bug IDs to associate with this failure |
| 324 | @param reason: An override for the test failure reason |
| 325 | @param invalidate: True if failure should be invalidated for the purposes of |
| 326 | reporting. Defaults to False. |
| 327 | """ |
| 328 | if keyvals is None: |
| 329 | keyvals = {} |
| 330 | |
| 331 | host_choices = failure_actions.HostAction.values |
| 332 | test_choices = failure_actions.TestAction.values |
| 333 | if host_action not in host_choices: |
| 334 | raise model_logic.ValidationError( |
| 335 | {'host_action': ('host action %s not valid; must be one of %s' |
| 336 | % (host_action, ', '.join(host_choices)))}) |
| 337 | if test_action not in test_choices: |
| 338 | raise model_logic.ValidationError( |
| 339 | {'test_action': ('test action %s not valid; must be one of %s' |
| 340 | % (test_action, ', '.join(test_choices)))}) |
| 341 | |
| 342 | failure = models.TestRun.objects.get(id=failure_id) |
| 343 | |
| 344 | rpc_utils.process_host_action(failure.host, host_action) |
| 345 | rpc_utils.process_test_action(failure.test_job, test_action) |
| 346 | |
| 347 | # Add the test labels |
| 348 | for label in labels: |
| 349 | tko_test_label, _ = ( |
| 350 | tko_models.TestLabel.objects.get_or_create(name=label)) |
| 351 | failure.tko_test.testlabel_set.add(tko_test_label) |
| 352 | |
| 353 | # Set the job keyvals |
| 354 | for key, value in keyvals.iteritems(): |
| 355 | keyval, created = tko_models.JobKeyval.objects.get_or_create( |
| 356 | job=failure.tko_test.job, key=key) |
| 357 | if not created: |
| 358 | tko_models.JobKeyval.objects.create(job=failure.tko_test.job, |
| 359 | key='original_' + key, |
| 360 | value=keyval.value) |
| 361 | keyval.value = value |
| 362 | keyval.save() |
| 363 | |
| 364 | # Add the bugs |
| 365 | for bug_id in bugs: |
| 366 | bug, _ = models.Bug.objects.get_or_create(external_uid=bug_id) |
| 367 | failure.bugs.add(bug) |
| 368 | |
| 369 | # Set the failure reason |
| 370 | if reason is not None: |
| 371 | tko_models.TestAttribute.objects.create(test=failure.tko_test, |
| 372 | attribute='original_reason', |
| 373 | value=failure.tko_test.reason) |
| 374 | failure.tko_test.reason = reason |
| 375 | failure.tko_test.save() |
| 376 | |
| 377 | # Set 'invalidated', 'seen', and 'triaged' |
| 378 | failure.invalidated = invalidate |
| 379 | failure.seen = True |
| 380 | failure.triaged = True |
| 381 | failure.save() |
| 382 | |
| 383 | |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 384 | def get_static_data(): |
jamesren | 4be631f | 2010-04-08 23:01:22 +0000 | [diff] [blame] | 385 | result = {'motd': afe_rpc_utils.get_motd(), |
| 386 | 'host_actions': sorted(failure_actions.HostAction.values), |
| 387 | 'test_actions': sorted(failure_actions.TestAction.values)} |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 388 | return result |