diff options
Diffstat (limited to 'frontend/planner/rpc_interface.py')
| -rw-r--r-- | frontend/planner/rpc_interface.py | 623 | 
1 files changed, 0 insertions, 623 deletions
diff --git a/frontend/planner/rpc_interface.py b/frontend/planner/rpc_interface.py deleted file mode 100644 index b97c3d23..00000000 --- a/frontend/planner/rpc_interface.py +++ /dev/null @@ -1,623 +0,0 @@ -"""\ -Functions to expose over the RPC interface. -""" - -__author__ = 'jamesren@google.com (James Ren)' - - -import os, re -import common -from django.db import models as django_models -from autotest_lib.frontend import thread_local -from autotest_lib.frontend.afe import model_logic, models as afe_models -from autotest_lib.frontend.afe import rpc_utils as afe_rpc_utils -from autotest_lib.frontend.afe import rpc_interface as afe_rpc_interface -from autotest_lib.frontend.tko import models as tko_models -from autotest_lib.frontend.planner import models, rpc_utils, model_attributes -from autotest_lib.frontend.planner import failure_actions -from autotest_lib.client.common_lib import utils - -# basic getter/setter calls -# TODO: deprecate the basic calls and reimplement them in the REST framework - -def get_plan(id): -    return afe_rpc_utils.prepare_for_serialization( -            models.Plan.smart_get(id).get_object_dict()) - - -def modify_plan(id, **data): -    models.Plan.smart_get(id).update_object(data) - - -def modify_test_run(id, **data): -    models.TestRun.objects.get(id=id).update_object(data) - - -def modify_host(id, **data): -    models.Host.objects.get(id=id).update_object(data) - - -def add_job(plan_id, test_config_id, afe_job_id): -    models.Job.objects.create( -            plan=models.Plan.objects.get(id=plan_id), -            test_config=models.TestConfig.objects.get(id=test_config_id), -            afe_job=afe_models.Job.objects.get(id=afe_job_id)) - - -# more advanced calls - -def submit_plan(name, hosts, host_labels, tests, support=None, -                label_override=None, additional_parameters=None): -    """ -    Submits a plan to the Test Planner - -    @param name: the name of the plan -    @param hosts: a list of hostnames -    @param host_labels: a list of host labels. The hosts under test will update -                        to reflect changes in the label -    @param tests: an ordered list of dictionaries: -                      alias: an alias for the test -                      control_file: the test control file -                      is_server: True if is a server-side control file -                      estimated_runtime: estimated number of hours this test -                                         will run -    @param support: the global support script -    @param label_override: label to prepend to all AFE jobs for this test plan. -                           Defaults to the plan name. -    @param additional_parameters: A mapping of AdditionalParameters to apply to -                                  this test plan, as an ordered list. Each item -                                  of the list is a dictionary: -                                  hostname_regex: A regular expression; the -                                                  additional parameter in the -                                                  value will be applied if the -                                                  hostname matches this regex -                                  param_type: The type of additional parameter -                                  param_values: A dictionary of key=value pairs -                                                for this parameter -               example: -               [{'hostname_regex': 'host[0-9]', -                 'param_type': 'Verify', -                 'param_values': {'key1': 'value1', -                                  'key2': 'value2'}}, -                {'hostname_regex': '.*', -                 'param_type': 'Verify', -                 'param_values': {'key': 'value'}}] - -                                  Currently, the only (non-site-specific) -                                  param_type available is 'Verify'. Setting -                                  these parameters allows the user to specify -                                  arguments to the -                                  job.run_test('verify_test', ...) line at the -                                  beginning of the wrapped control file for each -                                  test -    """ -    host_objects = [] -    label_objects = [] - -    for host in hosts or []: -        try: -            host_objects.append( -                    afe_models.Host.valid_objects.get(hostname=host)) -        except afe_models.Host.DoesNotExist: -            raise model_logic.ValidationError( -                    {'hosts': 'host %s does not exist' % host}) - -    for label in host_labels or []: -        try: -            label_objects.append(afe_models.Label.valid_objects.get(name=label)) -        except afe_models.Label.DoesNotExist: -            raise model_logic.ValidationError( -                    {'host_labels': 'host label %s does not exist' % label}) - -    aliases_seen = set() -    test_required_fields = ( -            'alias', 'control_file', 'is_server', 'estimated_runtime') -    for test in tests: -        for field in test_required_fields: -            if field not in test: -                raise model_logic.ValidationError( -                        {'tests': 'field %s is required' % field}) - -        alias = test['alias'] -        if alias in aliases_seen: -            raise model_logic.Validationerror( -                    {'tests': 'alias %s occurs more than once' % alias}) -        aliases_seen.add(alias) - -    plan, created = models.Plan.objects.get_or_create(name=name) -    if not created: -        raise model_logic.ValidationError( -                {'name': 'Plan name %s already exists' % name}) - -    try: -        rpc_utils.set_additional_parameters(plan, additional_parameters) -        label = rpc_utils.create_plan_label(plan) -        try: -            for i, test in enumerate(tests): -                control, _ = models.ControlFile.objects.get_or_create( -                        contents=test['control_file']) -                models.TestConfig.objects.create( -                        plan=plan, alias=test['alias'], control_file=control, -                        is_server=test['is_server'], execution_order=i, -                        estimated_runtime=test['estimated_runtime']) - -            plan.label_override = label_override -            plan.support = support or '' -            plan.save() - -            plan.owners.add(afe_models.User.current_user()) - -            for host in host_objects: -                planner_host = models.Host.objects.create(plan=plan, host=host) - -            plan.host_labels.add(*label_objects) - -            rpc_utils.start_plan(plan, label) - -            return plan.id -        except: -            label.delete() -            raise -    except: -        plan.delete() -        raise - - -def get_hosts(plan_id): -    """ -    Gets the hostnames of all the hosts in this test plan. - -    Resolves host labels in the plan. -    """ -    plan = models.Plan.smart_get(plan_id) - -    hosts = set(plan.hosts.all().values_list('hostname', flat=True)) -    for label in plan.host_labels.all(): -        hosts.update(label.host_set.all().values_list('hostname', flat=True)) - -    return afe_rpc_utils.prepare_for_serialization(hosts) - - -def get_atomic_group_control_file(): -    """ -    Gets the control file to apply the atomic group for a set of machines -    """ -    return rpc_utils.lazy_load(os.path.join(os.path.dirname(__file__), -                                            'set_atomic_group_control.srv')) - - -def get_next_test_configs(plan_id): -    """ -    Gets information about the next planner test configs that need to be run - -    @param plan_id: the ID or name of the test plan -    @return a dictionary: -                complete: True or False, shows test plan completion -                next_configs: a list of dictionaries: -                    host: ID of the host -                    next_test_config_id: ID of the next Planner test to run -    """ -    plan = models.Plan.smart_get(plan_id) - -    result = {'next_configs': []} - -    rpc_utils.update_hosts_table(plan) -    for host in models.Host.objects.filter(plan=plan): -        next_test_config = rpc_utils.compute_next_test_config(plan, host) -        if next_test_config: -            config = {'next_test_config_id': next_test_config.id, -                      'next_test_config_alias': next_test_config.alias, -                      'host': host.host.hostname} -            result['next_configs'].append(config) - -    rpc_utils.check_for_completion(plan) -    result['complete'] = plan.complete - -    return result - - -def update_test_runs(plan_id): -    """ -    Add all applicable TKO jobs to the Planner DB tables - -    Looks for tests in the TKO tables that were started as a part of the test -    plan, and add them to the Planner tables. - -    Also updates the status of the test run if the underlying TKO test move from -    an active status to a completed status. - -    @return a list of dictionaries: -                status: the status of the new (or updated) test run -                tko_test_idx: the ID of the TKO test added -                hostname: the host added -    """ -    plan = models.Plan.smart_get(plan_id) -    updated = [] - -    for planner_job in plan.job_set.all(): -        known_statuses = dict((test_run.tko_test.test_idx, test_run.status) -                              for test_run in planner_job.testrun_set.all()) -        tko_tests_for_job = tko_models.Test.objects.filter( -                job__afe_job_id=planner_job.afe_job.id) - -        for tko_test in tko_tests_for_job: -            status = rpc_utils.compute_test_run_status(tko_test.status.word) -            needs_update = (tko_test.test_idx not in known_statuses or -                            status != known_statuses[tko_test.test_idx]) -            if needs_update: -                hostnames = tko_test.machine.hostname.split(',') -                for hostname in hostnames: -                    rpc_utils.add_test_run( -                            plan, planner_job, tko_test, hostname, status) -                    updated.append({'status': status, -                                    'tko_test_idx': tko_test.test_idx, -                                    'hostname': hostname}) - -    return updated - - -def get_failures(plan_id): -    """ -    Gets a list of the untriaged failures associated with this plan - -    @return a list of dictionaries: -                id: the failure ID, for passing back to triage the failure -                group: the group for the failure. Normally the same as the -                       reason, but can be different for custom queries -                machine: the failed machine -                blocked: True if the failure caused the machine to block -                test_name: Concatenation of the Planner alias and the TKO test -                           name for the failed test -                reason: test failure reason -                seen: True if the failure is marked as "seen" -    """ -    plan = models.Plan.smart_get(plan_id) -    result = {} - -    failures = plan.testrun_set.filter( -            finalized=True, triaged=False, -            status=model_attributes.TestRunStatus.FAILED) -    failures = failures.order_by('seen').select_related('test_job__test', -                                                        'host__host', -                                                        'tko_test') -    for failure in failures: -        test_name = '%s: %s' % ( -                failure.test_job.test_config.alias, failure.tko_test.test) - -        group_failures = result.setdefault(failure.tko_test.reason, []) -        failure_dict = {'id': failure.id, -                        'machine': failure.host.host.hostname, -                        'blocked': bool(failure.host.blocked), -                        'test_name': test_name, -                        'reason': failure.tko_test.reason, -                        'seen': bool(failure.seen)} -        group_failures.append(failure_dict) - -    return result - - -def get_test_runs(**filter_data): -    """ -    Gets a list of test runs that match the filter data. - -    Returns a list of expanded TestRun object dictionaries. Specifically, the -    "host" and "test_job" fields are expanded. Additionally, the "test_config" -    field of the "test_job" expansion is also expanded. -    """ -    result = [] -    for test_run in models.TestRun.objects.filter(**filter_data): -        test_run_dict = test_run.get_object_dict() -        test_run_dict['host'] = test_run.host.get_object_dict() -        test_run_dict['test_job'] = test_run.test_job.get_object_dict() -        test_run_dict['test_job']['test_config'] = ( -                test_run.test_job.test_config.get_object_dict()) -        result.append(test_run_dict) -    return result - - -def skip_test(test_config_id, hostname): -    """ -    Marks a test config as "skipped" for a given host -    """ -    config = models.TestConfig.objects.get(id=test_config_id) -    config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname)) - - -def mark_failures_as_seen(failure_ids): -    """ -    Marks a set of failures as 'seen' - -    @param failure_ids: A list of failure IDs, as returned by get_failures(), to -                        mark as seen -    """ -    models.TestRun.objects.filter(id__in=failure_ids).update(seen=True) - - -def process_failures(failure_ids, host_action, test_action, labels=(), -                     keyvals=None, bugs=(), reason=None, invalidate=False): -    """ -    Triage a failure - -    @param failure_id: The failure ID, as returned by get_failures() -    @param host_action: One of 'Block', 'Unblock', 'Reinstall' -    @param test_action: One of 'Skip', 'Rerun' - -    @param labels: Test labels to apply, by name -    @param keyvals: Dictionary of job keyvals to add (or replace) -    @param bugs: List of bug IDs to associate with this failure -    @param reason: An override for the test failure reason -    @param invalidate: True if failure should be invalidated for the purposes of -                       reporting. Defaults to False. -    """ -    host_choices = failure_actions.HostAction.values -    test_choices = failure_actions.TestAction.values -    if host_action not in host_choices: -        raise model_logic.ValidationError( -                {'host_action': ('host action %s not valid; must be one of %s' -                                 % (host_action, ', '.join(host_choices)))}) -    if test_action not in test_choices: -        raise model_logic.ValidationError( -                {'test_action': ('test action %s not valid; must be one of %s' -                                 % (test_action, ', '.join(test_choices)))}) - -    for failure_id in failure_ids: -        rpc_utils.process_failure( -                failure_id=failure_id, host_action=host_action, -                test_action=test_action, labels=labels, keyvals=keyvals, -                bugs=bugs, reason=reason, invalidate=invalidate) - - -def get_machine_view_data(plan_id): -    """ -    Gets the data required for the web frontend Machine View. - -    @param plan_id: The ID of the test plan -    @return An array. Each element is a dictionary: -                    machine: The name of the machine -                    status: The machine's status (one of -                            model_attributes.HostStatus) -                    bug_ids: List of the IDs for the bugs filed -                    tests_run: An array of dictionaries: -                            test_name: The TKO name of the test -                            success: True if the test passed -    """ -    plan = models.Plan.smart_get(plan_id) -    result = [] -    for host in plan.host_set.all(): -        tests_run = [] - -        machine = host.host.hostname -        host_status = host.status() -        bug_ids = set() - -        testruns = plan.testrun_set.filter(host=host, invalidated=False, -                                           finalized=True) -        for testrun in testruns: -            test_name = testrun.tko_test.test -            test_status = testrun.tko_test.status.word -            testrun_bug_ids = testrun.bugs.all().values_list( -                    'external_uid', flat=True) - -            tests_run.append({'test_name': test_name, -                              'status': test_status}) -            bug_ids.update(testrun_bug_ids) - -        result.append({'machine': machine, -                       'status': host_status, -                       'tests_run': tests_run, -                       'bug_ids': list(bug_ids)}) -    return result - - -def generate_test_config(alias, afe_test_name=None, -                         estimated_runtime=0, **kwargs): -    """ -    Creates and returns a test config suitable for passing into submit_plan() - -    Also accepts optional parameters to pass directly in to the AFE RPC -    interface's generate_control_file() method. - -    @param alias: The alias for the test -    @param afe_test_name: The name of the test, as shown on AFE -    @param estimated_runtime: Estimated number of hours this test is expected to -                              run. For reporting purposes. -    """ -    if afe_test_name is None: -        afe_test_name = alias -    alias = alias.replace(' ', '_') - -    control = afe_rpc_interface.generate_control_file(tests=[afe_test_name], -                                                      **kwargs) - -    return {'alias': alias, -            'control_file': control['control_file'], -            'is_server': control['is_server'], -            'estimated_runtime': estimated_runtime} - - -def get_wrapped_test_config(id, hostname, run_verify): -    """ -    Gets the TestConfig object identified by the ID - -    Returns the object dict of the TestConfig, plus an additional -    'wrapped_control_file' value, which includes the pre-processing that the -    ControlParameters specify. - -    @param hostname: Hostname of the machine this test config will run on -    @param run_verify: Set to True or False to override the default behavior -                       (which is to run the verify test unless the skip_verify -                       ControlParameter is set) -    """ -    test_config = models.TestConfig.objects.get(id=id) -    object_dict = test_config.get_object_dict() -    object_dict['control_file'] = test_config.control_file.get_object_dict() -    object_dict['wrapped_control_file'] = rpc_utils.wrap_control_file( -            plan=test_config.plan, hostname=hostname, -            run_verify=run_verify, test_config=test_config) - -    return object_dict - - -def generate_additional_parameters(hostname_regex, param_type, param_values): -    """ -    Generates an AdditionalParamter dictionary, for passing in to submit_plan() - -    Returns a dictionary. To use in submit_job(), put this dictionary into a -    list (possibly with other additional_parameters dictionaries) - -    @param hostname_regex: The hostname regular expression to match -    @param param_type: One of get_static_data()['additional_parameter_types'] -    @param param_values: Dictionary of key=value pairs for this parameter -    """ -    try: -        re.compile(hostname_regex) -    except Exception: -        raise model_logic.ValidationError( -                {'hostname_regex': '%s is not a valid regex' % hostname_regex}) - -    if param_type not in model_attributes.AdditionalParameterType.values: -        raise model_logic.ValidationError( -                {'param_type': '%s is not a valid parameter type' % param_type}) - -    if type(param_values) is not dict: -        raise model_logic.ValidationError( -                {'param_values': '%s is not a dictionary' % repr(param_values)}) - -    return {'hostname_regex': hostname_regex, -            'param_type': param_type, -            'param_values': param_values} - - -def get_overview_data(plan_ids): -    """ -    Gets the data for the Overview tab - -    @param plan_ids: A list of the plans, by id or name -    @return A dictionary - keys are plan names, values are dictionaries of data: -                machines: A list of dictionaries: -                hostname: The machine's hostname -                status: The host's status -                passed: True if the machine passed the test plan. A 'pass' means -                        that, for every test configuration in the plan, the -                        machine had at least one AFE job with no failed tests. -                        'passed' could also be None, meaning that this host is -                        still running tests. -                bugs: A list of the bugs filed -                test_configs: A list of dictionaries, each representing a test -                              config: -                    complete: Number of hosts that have completed this test -                              config -                    estimated_runtime: Number of hours this test config is -                                       expected to run on each host -    """ -    plans = models.Plan.smart_get_bulk(plan_ids) -    result = {} - -    for plan in plans: -        machines = [] -        for host in plan.host_set.all(): -            pass_status = rpc_utils.compute_test_config_status(host) -            if pass_status == rpc_utils.ComputeTestConfigStatusResult.PASS: -                passed = True -            elif pass_status == rpc_utils.ComputeTestConfigStatusResult.FAIL: -                passed = False -            else: -                passed = None -            machines.append({'hostname': host.host.hostname, -                             'status': host.status(), -                             'passed': passed}) - -        bugs = set() -        for testrun in plan.testrun_set.all(): -            bugs.update(testrun.bugs.values_list('external_uid', flat=True)) - -        test_configs = [] -        for test_config in plan.testconfig_set.all(): -            complete_jobs = test_config.job_set.filter( -                    afe_job__hostqueueentry__complete=True) -            complete_afe_jobs = afe_models.Job.objects.filter( -                    id__in=complete_jobs.values_list('afe_job', flat=True)) - -            complete_hosts = afe_models.Host.objects.filter( -                    hostqueueentry__job__in=complete_afe_jobs) -            complete_hosts |= test_config.skipped_hosts.all() - -            test_configs.append( -                    {'complete': complete_hosts.distinct().count(), -                     'estimated_runtime': test_config.estimated_runtime}) - -        plan_data = {'machines': machines, -                     'bugs': list(bugs), -                     'test_configs': test_configs} -        result[plan.name] = plan_data - -    return result - - -def get_test_view_data(plan_id): -    """ -    Gets the data for the Test View tab - -    @param plan_id: The name or ID of the test plan -    @return A dictionary - Keys are test config aliases, values are dictionaries -                           of data: -                total_machines: Total number of machines scheduled for this test -                                config. Excludes machines that are set to skip -                                this config. -                machine_status: A dictionary: -                    key: The hostname -                    value: The status of the machine: one of 'Scheduled', -                           'Running', 'Pass', or 'Fail' -                total_runs: Total number of runs of this test config. Includes -                            repeated runs (from triage re-run) -                total_passes: Number of runs that resulted in a 'pass', meaning -                              that none of the tests in the test config had any -                              status other than GOOD. -                bugs: List of bugs that were filed under this test config -    """ -    plan = models.Plan.smart_get(plan_id) -    result = {} -    for test_config in plan.testconfig_set.all(): -        skipped_host_ids = test_config.skipped_hosts.values_list('id', -                                                                 flat=True) -        hosts = plan.host_set.exclude(host__id__in=skipped_host_ids) -        total_machines = hosts.count() - -        machine_status = {} -        for host in hosts: -            machine_status[host.host.hostname] = ( -                    rpc_utils.compute_test_config_status(host, test_config)) - -        planner_jobs = test_config.job_set.all() -        total_runs = planner_jobs.count() -        total_passes = 0 -        for planner_job in planner_jobs: -            if planner_job.all_tests_passed(): -                total_passes += 1 - -        test_runs = plan.testrun_set.filter( -                test_job__in=test_config.job_set.all()) -        bugs = set() -        for test_run in test_runs: -            bugs.update(test_run.bugs.values_list('external_uid', flat=True)) - -        result[test_config.alias] = {'total_machines': total_machines, -                                     'machine_status': machine_status, -                                     'total_runs': total_runs, -                                     'total_passes': total_passes, -                                     'bugs': list(bugs)} -    return result - - -def get_motd(): -    return afe_rpc_utils.get_motd() - - -def get_static_data(): -    result = {'motd': get_motd(), -              'host_actions': sorted(failure_actions.HostAction.values), -              'test_actions': sorted(failure_actions.TestAction.values), -              'additional_parameter_types': -                      sorted(model_attributes.AdditionalParameterType.values), -              'host_statuses': sorted(model_attributes.HostStatus.values)} -    return result  | 
