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 | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 16 | from autotest_lib.client.common_lib import utils |
| 17 | |
| 18 | # basic getter/setter calls |
| 19 | # TODO: deprecate the basic calls and reimplement them in the REST framework |
| 20 | |
| 21 | def get_plan(id): |
| 22 | return afe_rpc_utils.prepare_for_serialization( |
| 23 | models.Plan.smart_get(id).get_object_dict()) |
| 24 | |
| 25 | |
| 26 | def modify_plan(id, **data): |
| 27 | models.Plan.smart_get(id).update_object(data) |
| 28 | |
| 29 | |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 30 | def modify_test_run(id, **data): |
| 31 | models.TestRun.objects.get(id=id).update_object(data) |
| 32 | |
| 33 | |
| 34 | def modify_host(id, **data): |
| 35 | models.Host.objects.get(id=id).update_object(data) |
| 36 | |
| 37 | |
| 38 | def get_test_config(id): |
| 39 | return afe_rpc_utils.prepare_rows_as_nested_dicts( |
| 40 | models.TestConfig.objects.filter(id=id), ('control_file',))[0] |
| 41 | |
| 42 | |
| 43 | def add_job(plan_id, test_config_id, afe_job_id): |
| 44 | models.Job.objects.create( |
| 45 | plan=models.Plan.objects.get(id=plan_id), |
| 46 | test_config=models.TestConfig.objects.get(id=test_config_id), |
| 47 | afe_job=afe_models.Job.objects.get(id=afe_job_id)) |
| 48 | |
| 49 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 50 | # more advanced calls |
| 51 | |
| 52 | def submit_plan(name, hosts, host_labels, tests, |
| 53 | support=None, label_override=None): |
| 54 | """ |
| 55 | Submits a plan to the Test Planner |
| 56 | |
| 57 | @param name: the name of the plan |
| 58 | @param hosts: a list of hostnames |
| 59 | @param host_labels: a list of host labels. The hosts under test will update |
| 60 | to reflect changes in the label |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 61 | @param tests: an ordered list of dictionaries: |
| 62 | alias: an alias for the test |
| 63 | control_file: the test control file |
| 64 | is_server: True if is a server-side control file |
| 65 | estimated_runtime: estimated number of hours this test |
| 66 | will run |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame^] | 67 | @param support: the global support script |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 68 | @param label_override: label to prepend to all AFE jobs for this test plan. |
| 69 | Defaults to the plan name. |
| 70 | """ |
| 71 | host_objects = [] |
| 72 | label_objects = [] |
| 73 | |
| 74 | for host in hosts or []: |
| 75 | try: |
| 76 | host_objects.append( |
| 77 | afe_models.Host.valid_objects.get(hostname=host)) |
| 78 | except afe_models.Host.DoesNotExist: |
| 79 | raise model_logic.ValidationError( |
| 80 | {'hosts': 'host %s does not exist' % host}) |
| 81 | |
| 82 | for label in host_labels or []: |
| 83 | try: |
| 84 | label_objects.append(afe_models.Label.valid_objects.get(name=label)) |
| 85 | except afe_models.Label.DoesNotExist: |
| 86 | raise model_logic.ValidationError( |
| 87 | {'host_labels': 'host label %s does not exist' % label}) |
| 88 | |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 89 | aliases_seen = set() |
| 90 | test_required_fields = ( |
| 91 | 'alias', 'control_file', 'is_server', 'estimated_runtime') |
| 92 | for test in tests: |
| 93 | for field in test_required_fields: |
| 94 | if field not in test: |
| 95 | raise model_logic.ValidationError( |
| 96 | {'tests': 'field %s is required' % field}) |
| 97 | |
| 98 | alias = test['alias'] |
| 99 | if alias in aliases_seen: |
| 100 | raise model_logic.Validationerror( |
| 101 | {'tests': 'alias %s occurs more than once' % alias}) |
| 102 | aliases_seen.add(alias) |
| 103 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 104 | plan, created = models.Plan.objects.get_or_create(name=name) |
| 105 | if not created: |
| 106 | raise model_logic.ValidationError( |
| 107 | {'name': 'Plan name %s already exists' % name}) |
| 108 | |
| 109 | try: |
| 110 | label = rpc_utils.create_plan_label(plan) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 111 | try: |
| 112 | for i, test in enumerate(tests): |
| 113 | control, _ = models.ControlFile.objects.get_or_create( |
| 114 | contents=test['control_file']) |
| 115 | models.TestConfig.objects.create( |
| 116 | plan=plan, alias=test['alias'], control_file=control, |
| 117 | is_server=test['is_server'], execution_order=i, |
| 118 | estimated_runtime=test['estimated_runtime']) |
| 119 | |
| 120 | plan.label_override = label_override |
| 121 | plan.support = support or '' |
| 122 | plan.save() |
| 123 | |
| 124 | plan.owners.add(afe_models.User.current_user()) |
| 125 | |
| 126 | for host in host_objects: |
| 127 | planner_host = models.Host.objects.create(plan=plan, host=host) |
| 128 | |
| 129 | plan.host_labels.add(*label_objects) |
| 130 | |
| 131 | rpc_utils.start_plan(plan, label) |
| 132 | |
| 133 | return plan.id |
| 134 | except: |
| 135 | label.delete() |
| 136 | raise |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 137 | except: |
| 138 | plan.delete() |
| 139 | raise |
| 140 | |
jamesren | c394022 | 2010-02-19 21:57:37 +0000 | [diff] [blame] | 141 | |
| 142 | def get_hosts(plan_id): |
| 143 | """ |
| 144 | Gets the hostnames of all the hosts in this test plan. |
| 145 | |
| 146 | Resolves host labels in the plan. |
| 147 | """ |
| 148 | plan = models.Plan.smart_get(plan_id) |
| 149 | |
| 150 | hosts = set(plan.hosts.all().values_list('hostname', flat=True)) |
| 151 | for label in plan.host_labels.all(): |
| 152 | hosts.update(label.host_set.all().values_list('hostname', flat=True)) |
| 153 | |
| 154 | return afe_rpc_utils.prepare_for_serialization(hosts) |
| 155 | |
| 156 | |
| 157 | def get_atomic_group_control_file(): |
| 158 | """ |
| 159 | Gets the control file to apply the atomic group for a set of machines |
| 160 | """ |
| 161 | return rpc_utils.lazy_load(os.path.join(os.path.dirname(__file__), |
| 162 | 'set_atomic_group_control.srv')) |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 163 | |
| 164 | |
| 165 | def get_next_test_configs(plan_id): |
| 166 | """ |
| 167 | Gets information about the next planner test configs that need to be run |
| 168 | |
| 169 | @param plan_id: the ID or name of the test plan |
| 170 | @return a dictionary: |
| 171 | complete: True or False, shows test plan completion |
| 172 | next_configs: a list of dictionaries: |
| 173 | host: ID of the host |
| 174 | next_test_config_id: ID of the next Planner test to run |
| 175 | """ |
| 176 | plan = models.Plan.smart_get(plan_id) |
| 177 | |
| 178 | result = {'next_configs': []} |
| 179 | |
| 180 | rpc_utils.update_hosts_table(plan) |
| 181 | for host in models.Host.objects.filter(plan=plan): |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame^] | 182 | next_test_config = rpc_utils.compute_next_test_config(plan, host) |
| 183 | if next_test_config: |
| 184 | config = {'next_test_config_id': next_test_config.id, |
| 185 | 'next_test_config_alias': next_test_config.alias, |
jamesren | 3e9f609 | 2010-03-11 21:32:10 +0000 | [diff] [blame] | 186 | 'host': host.host.hostname} |
| 187 | result['next_configs'].append(config) |
| 188 | |
| 189 | rpc_utils.check_for_completion(plan) |
| 190 | result['complete'] = plan.complete |
| 191 | |
| 192 | return result |
| 193 | |
| 194 | |
| 195 | def update_test_runs(plan_id): |
| 196 | """ |
| 197 | Add all applicable TKO jobs to the Planner DB tables |
| 198 | |
| 199 | Looks for tests in the TKO tables that were started as a part of the test |
| 200 | plan, and add them to the Planner tables. |
| 201 | |
| 202 | Also updates the status of the test run if the underlying TKO test move from |
| 203 | an active status to a completed status. |
| 204 | |
| 205 | @return a list of dictionaries: |
| 206 | status: the status of the new (or updated) test run |
| 207 | tko_test_idx: the ID of the TKO test added |
| 208 | hostname: the host added |
| 209 | """ |
| 210 | plan = models.Plan.objects.get(id=plan_id) |
| 211 | updated = [] |
| 212 | |
| 213 | for planner_job in plan.job_set.all(): |
| 214 | known_statuses = dict((test_run.tko_test.test_idx, test_run.status) |
| 215 | for test_run in planner_job.testrun_set.all()) |
| 216 | tko_tests_for_job = tko_models.Test.objects.filter( |
| 217 | job__afe_job_id=planner_job.afe_job.id) |
| 218 | |
| 219 | for tko_test in tko_tests_for_job: |
| 220 | status = rpc_utils.compute_test_run_status(tko_test.status.word) |
| 221 | needs_update = (tko_test.test_idx not in known_statuses or |
| 222 | status != known_statuses[tko_test.test_idx]) |
| 223 | if needs_update: |
| 224 | hostnames = tko_test.machine.hostname.split(',') |
| 225 | for hostname in hostnames: |
| 226 | rpc_utils.add_test_run( |
| 227 | plan, planner_job, tko_test, hostname, status) |
| 228 | updated.append({'status': status, |
| 229 | 'tko_test_idx': tko_test.test_idx, |
| 230 | 'hostname': hostname}) |
| 231 | |
| 232 | return updated |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 233 | |
| 234 | |
| 235 | def get_failures(plan_id): |
| 236 | """ |
| 237 | Gets a list of the untriaged failures associated with this plan |
| 238 | |
| 239 | @return a list of dictionaries: |
| 240 | id: the failure ID, for passing back to triage the failure |
| 241 | group: the group for the failure. Normally the same as the |
| 242 | reason, but can be different for custom queries |
| 243 | machine: the failed machine |
| 244 | blocked: True if the failure caused the machine to block |
| 245 | test_name: Concatenation of the Planner alias and the TKO test |
| 246 | name for the failed test |
| 247 | reason: test failure reason |
| 248 | seen: True if the failure is marked as "seen" |
| 249 | """ |
| 250 | plan = models.Plan.smart_get(plan_id) |
| 251 | result = {} |
| 252 | |
| 253 | failures = plan.testrun_set.filter( |
| 254 | finalized=True, triaged=False, |
| 255 | status=model_attributes.TestRunStatus.FAILED) |
| 256 | failures = failures.select_related('test_job__test', 'host__host', |
| 257 | 'tko_test') |
| 258 | for failure in failures: |
| 259 | test_name = '%s:%s' % ( |
| 260 | failure.test_job.test_config.alias, failure.tko_test.test) |
| 261 | |
| 262 | group_failures = result.setdefault(failure.tko_test.reason, []) |
| 263 | failure_dict = {'id': failure.id, |
| 264 | 'machine': failure.host.host.hostname, |
| 265 | 'blocked': bool(failure.host.blocked), |
| 266 | 'test_name': test_name, |
| 267 | 'reason': failure.tko_test.reason, |
| 268 | 'seen': bool(failure.seen)} |
| 269 | group_failures.append(failure_dict) |
| 270 | |
| 271 | return result |
| 272 | |
| 273 | |
jamesren | dbeebf8 | 2010-04-08 22:58:26 +0000 | [diff] [blame^] | 274 | def get_test_runs(**filter_data): |
| 275 | """ |
| 276 | Gets a list of test runs that match the filter data. |
| 277 | |
| 278 | Returns a list of expanded TestRun object dictionaries. Specifically, the |
| 279 | "host" and "test_job" fields are expanded. Additionally, the "test_config" |
| 280 | field of the "test_job" expansion is also expanded. |
| 281 | """ |
| 282 | result = [] |
| 283 | for test_run in models.TestRun.objects.filter(**filter_data): |
| 284 | test_run_dict = test_run.get_object_dict() |
| 285 | test_run_dict['host'] = test_run.host.get_object_dict() |
| 286 | test_run_dict['test_job'] = test_run.test_job.get_object_dict() |
| 287 | test_run_dict['test_job']['test_config'] = ( |
| 288 | test_run.test_job.test_config.get_object_dict()) |
| 289 | result.append(test_run_dict) |
| 290 | return result |
| 291 | |
| 292 | |
| 293 | def skip_test(test_config_id, hostname): |
| 294 | """ |
| 295 | Marks a test config as "skipped" for a given host |
| 296 | """ |
| 297 | config = models.TestConfig.objects.get(id=test_config_id) |
| 298 | config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname)) |
| 299 | |
| 300 | |
jamesren | b852bce | 2010-04-07 20:36:13 +0000 | [diff] [blame] | 301 | def get_static_data(): |
| 302 | result = {'motd': afe_rpc_utils.get_motd()} |
| 303 | return result |