def on_buildbot_event(data, message, dry_run, stage=False): """Act upon buildbot events.""" # Pulse gives us a job_id and a job_guid, we need request_id. LOG.info( "%s action requested by %s on repo_name %s with job_id: %s" % (data["action"], data["requester"], data["project"], data["job_id"]) ) # Cleaning mozci caches buildjson.BUILDS_CACHE = {} query_jobs.JOBS_CACHE = {} if stage: treeherder_client = TreeherderClient(host="treeherder.allizom.org") else: treeherder_client = TreeherderClient() repo_name = data["project"] job_id = data["job_id"] result = treeherder_client.get_jobs(repo_name, id=job_id) # If result not found, ignore if not result: LOG.info("We could not find any result for repo_name: %s and " "job_id: %s" % (repo_name, job_id)) message.ack() return result = result[0] buildername = result["ref_data_name"] resultset_id = result["result_set_id"] result_sets = treeherder_client.get_resultsets(repo_name, id=resultset_id) revision = result_sets[0]["revision"] action = data["action"] status = None buildername = filter_invalid_builders(buildername) # Treeherder can send us invalid builder names # https://bugzilla.mozilla.org/show_bug.cgi?id=1242038 if buildername is None: status = "Builder %s was invalid." % buildername[0] # Backfill action elif action == "backfill": manual_backfill(revision, buildername, max_revisions=get_maxRevisions(buildername), dry_run=dry_run) if not dry_run: status = "Backfill request sent" else: status = "Dry-run mode, nothing was backfilled" # Send a pulse message showing what we did message_sender = MessageHandler() pulse_message = {"job_id": job_id, "action": action, "requester": data["requester"], "status": status} routing_key = "{}.{}".format(repo_name, action) try: message_sender.publish_message(pulse_message, routing_key) except: LOG.warning("Failed to publish message over pulse stream.") if not dry_run: # We need to ack the message to remove it from our queue message.ack()
def on_event(data, message, dry_run, treeherder_server_url, acknowledge, **kwargs): """Act upon Treeherder job events. Return if the outcome was successful or not """ LOG.info('Acknowledge value: {}'.format(acknowledge)) exit_code = 0 # SUCCESS if ignored(data): if acknowledge: # We need to ack the message to remove it from our queue LOG.info('Message acknowledged') message.ack() return exit_code # Cleaning mozci caches buildjson.BUILDS_CACHE = {} query_jobs.JOBS_CACHE = {} treeherder_client = TreeherderClient(server_url=treeherder_server_url) action = data['action'].capitalize() job_id = data['job_id'] repo_name = data['project'] status = None # We want to know the status of the job we're processing try: job_info = treeherder_client.get_jobs(repo_name, id=job_id)[0] except IndexError: LOG.info("We could not find any job_info for repo_name: %s and " "job_id: %s" % (repo_name, job_id)) return exit_code buildername = job_info["ref_data_name"] # We want to know the revision associated for this job result_sets = treeherder_client.get_resultsets( repo_name, id=job_info["result_set_id"]) revision = result_sets[0]["revision"] link_to_job = '{}/#/jobs?repo={}&revision={}&selectedJob={}'.format( treeherder_server_url, repo_name, revision, job_id) LOG.info("{} action requested by {} for '{}'".format( action, data['requester'], buildername, )) LOG.info('Request for {}'.format(link_to_job)) buildername = filter_invalid_builders(buildername) if buildername is None: LOG.info('Treeherder can send us invalid builder names.') LOG.info('See https://bugzilla.mozilla.org/show_bug.cgi?id=1242038.') LOG.warning('Requested job name "%s" is invalid.' % job_info['ref_data_name']) exit_code = -1 # FAILURE # There are various actions that can be taken on a job, however, we currently # only process the backfill one elif action == "Backfill": exit_code = manual_backfill( revision=revision, buildername=buildername, dry_run=dry_run, ) if not dry_run: status = 'Backfill request sent' else: status = 'Dry-run mode, nothing was backfilled.' LOG.debug(status) else: LOG.error('We were not aware of the "{}" action. Please file an issue'. format(action)) exit_code = -1 # FAILURE if acknowledge: # We need to ack the message to remove it from our queue LOG.info('Message acknowledged') message.ack() return exit_code
def on_event(data, message, dry_run, treeherder_server_url, acknowledge, **kwargs): """Act upon Treeherder job events. Return if the outcome was successful or not """ exit_code = 0 # SUCCESS if ignored(data): if acknowledge: # We need to ack the message to remove it from our queue message.ack() return exit_code # Cleaning mozci caches buildjson.BUILDS_CACHE = {} query_jobs.JOBS_CACHE = {} treeherder_client = TreeherderClient(server_url=treeherder_server_url) action = data["action"].capitalize() job_id = data["job_id"] repo_name = data["project"] status = None # We want to know the status of the job we're processing try: job_info = treeherder_client.get_jobs(repo_name, id=job_id)[0] except IndexError: LOG.info("We could not find any job_info for repo_name: %s and " "job_id: %s" % (repo_name, job_id)) return exit_code buildername = job_info["ref_data_name"] # We want to know the revision associated for this job result_sets = treeherder_client.get_resultsets(repo_name, id=job_info["result_set_id"]) revision = result_sets[0]["revision"] link_to_job = "{}/#/jobs?repo={}&revision={}&selectedJob={}".format( treeherder_server_url, repo_name, revision, job_id ) LOG.info("{} action requested by {} for '{}'".format(action, data["requester"], buildername)) LOG.info("Request for {}".format(link_to_job)) buildername = filter_invalid_builders(buildername) if buildername is None: LOG.info("Treeherder can send us invalid builder names.") LOG.info("See https://bugzilla.mozilla.org/show_bug.cgi?id=1242038.") LOG.warning('Requested job name "%s" is invalid.' % job_info["ref_data_name"]) exit_code = -1 # FAILURE # There are various actions that can be taken on a job, however, we currently # only process the backfill one elif action == "Backfill": exit_code = manual_backfill(revision=revision, buildername=buildername, dry_run=dry_run) if not dry_run: status = "Backfill request sent" else: status = "Dry-run mode, nothing was backfilled." LOG.debug(status) else: LOG.error('We were not aware of the "{}" action. Please file an issue'.format(action)) exit_code = -1 # FAILURE if acknowledge: # We need to ack the message to remove it from our queue message.ack() return exit_code
def main(): options = parse_args() if options.debug: LOG = setup_logging(logging.DEBUG) else: LOG = setup_logging(logging.INFO) validate_options(options) if not options.dry_run and not valid_credentials(): sys.exit(-1) # Setting the QUERY_SOURCE global variable in mozci.py set_query_source(options.query_source) if options.buildernames: options.buildernames = sanitize_buildernames(options.buildernames) repo_url = query_repo_url_from_buildername(options.buildernames[0]) if not options.repo_name: repo_name = query_repo_name_from_buildername(options.buildernames[0]) else: repo_name = options.repo_name repo_url = query_repo_url(repo_name) if options.rev == 'tip': revision = query_repo_tip(repo_url).changesets[0].node LOG.info("The tip of %s is %s", repo_name, revision) else: revision = query_push_by_revision(repo_url, options.rev, return_revision_list=True) # Schedule jobs through TaskCluster if --taskcluster option has been set to true if options.taskcluster: mgr = TaskClusterBuildbotManager() else: mgr = BuildAPIManager() trigger_build_if_missing = options.trigger_build_if_missing if repo_name == 'try': trigger_build_if_missing = False # Mode 0: Backfill if options.backfill: manual_backfill(revision, options.buildernames[0], dry_run=options.dry_run) return # Mode 1: Trigger coalesced jobs if options.coalesced: query_api = BuildApi() request_ids = query_api.find_all_jobs_by_status(repo_name, revision, COALESCED) if len(request_ids) == 0: LOG.info('We did not find any coalesced job') for request_id in request_ids: make_retrigger_request(repo_name=repo_name, request_id=request_id, auth=get_credentials(), dry_run=options.dry_run) return # Mode #2: Fill-in a revision or trigger_test_jobs_only if options.fill_revision or options.trigger_tests_only: mgr.trigger_missing_jobs_for_revision( repo_name=repo_name, revision=revision, dry_run=options.dry_run, trigger_build_if_missing=not options.trigger_tests_only ) return # Mode #3: Trigger jobs based on revision list modifiers if not (options.includes or options.exclude or options.failed_jobs): job_names = options.buildernames # Mode 4 - Schedule every builder matching --includes and does not match --exclude. elif options.includes or options.exclude: filters_in = options.includes.split(',') + [repo_name] filters_out = [] if options.exclude: filters_out = options.exclude.split(',') job_names = filter_buildernames( buildernames=query_builders(repo_name=repo_name), include=filters_in, exclude=filters_out ) if len(job_names) == 0: LOG.info("0 jobs match these filters. please try again.") return if options.existing_only: # We query all successful jobs for a given revision and filter # them by include/exclude filters. trigger_build_if_missing = False successful_jobs = TreeherderApi().find_all_jobs_by_status( repo_name=repo_name, revision=revision, status=SUCCESS) # We will filter out all the existing job from those successful job we have. job_names = [buildername for buildername in successful_jobs if buildername in job_names] cont = raw_input("The ones which have existing builds out of %i jobs will be triggered,\ do you wish to continue? y/n/d (d=show details) " % len(job_names)) else: cont = raw_input("%i jobs will be triggered, do you wish to continue? \ y/n/d (d=show details) " % len(job_names)) if cont.lower() == 'd': LOG.info("The following jobs will be triggered: \n %s" % '\n'.join(job_names)) cont = raw_input("Do you wish to continue? y/n ") if cont.lower() != 'y': exit(1) # Mode 5: Use --failed-jobs to trigger jobs for particular revision elif options.failed_jobs: job_names = TreeherderApi().find_all_jobs_by_status( repo_name=repo_name, revision=revision, status=WARNING) for buildername in job_names: revlist = determine_revlist( repo_url=repo_url, buildername=buildername, rev=revision, back_revisions=options.back_revisions, delta=options.delta, from_rev=options.from_rev, backfill=options.backfill, skips=options.skips, max_revisions=options.max_revisions) _print_treeherder_link( revlist=revlist, repo_name=repo_name, buildername=buildername, revision=revision, log=LOG, includes=options.includes, exclude=options.exclude) try: mgr.trigger_range( buildername=buildername, repo_name=repo_name, revisions=revlist, times=options.times, dry_run=options.dry_run, files=options.files, trigger_build_if_missing=trigger_build_if_missing ) except Exception, e: LOG.exception(e) exit(1)
def on_event(data, message, dry_run, treeherder_server_url, **kwargs): """Act upon Treeherder job events. Return if the outcome was successful or not """ exit_code = 0 # SUCCESS if ignored(data): return exit_code # Cleaning mozci caches buildjson.BUILDS_CACHE = {} query_jobs.JOBS_CACHE = {} treeherder_client = TreeherderClient(server_url=treeherder_server_url) action = data['action'].capitalize() job_id = data['job_id'] repo_name = data['project'] status = None # We want to know the status of the job we're processing try: job_info = treeherder_client.get_jobs(repo_name, id=job_id)[0] except IndexError: LOG.info("We could not find any job_info for repo_name: %s and " "job_id: %s" % (repo_name, job_id)) return exit_code # We want to know the revision associated for this job result_set = treeherder_client.get_resultsets( repo_name, id=job_info["result_set_id"])[0] revision = result_set["revision"] link_to_job = '{}/#/jobs?repo={}&revision={}&selectedJob={}'.format( treeherder_server_url, repo_name, revision, job_id) # There are various actions that can be taken on a job, however, we currently # only process the backfill one if action == "Backfill": if job_info["build_system_type"] == "taskcluster": jobs = [] jobs_per_call = 250 offset = 0 while True: results = treeherder_client.get_jobs( repo_name, push_id=job_info["result_set_id"], count=jobs_per_call, offset=offset) jobs += results if (len(results) < jobs_per_call): break offset += jobs_per_call decision = [ t for t in jobs if t["job_type_name"] == "Gecko Decision Task" ][0] details = treeherder_client.get_job_details( job_guid=decision["job_guid"]) inspect = [ detail["url"] for detail in details if detail["value"] == "Inspect Task" ][0] # Pull out the taskId from the URL e.g. # oN1NErz_Rf2DZJ1hi7YVfA from <tc_tools_site>/task-inspector/#oN1NErz_Rf2DZJ1hi7YVfA/ decision_id = inspect.partition("#")[-1].rpartition("/")[0] mgr = TaskClusterManager(dry_run=dry_run) mgr.schedule_action_task(decision_id=decision_id, action="backfill", action_args={ "project": repo_name, "job": job_info["id"] }) else: buildername = job_info["ref_data_name"] LOG.info("{} action requested by {} for '{}'".format( action, data['requester'], buildername, )) LOG.info('Request for {}'.format(link_to_job)) buildername = filter_invalid_builders(buildername) if buildername is None: LOG.info('Treeherder can send us invalid builder names.') LOG.info( 'See https://bugzilla.mozilla.org/show_bug.cgi?id=1242038.' ) LOG.warning('Requested job name "%s" is invalid.' % job_info['ref_data_name']) exit_code = -1 # FAILURE else: exit_code = manual_backfill( revision=revision, buildername=buildername, dry_run=dry_run, ) if not dry_run: status = 'Backfill request sent' else: status = 'Dry-run mode, nothing was backfilled.' LOG.debug(status) else: LOG.error('We were not aware of the "{}" action. Please file an issue'. format(action)) exit_code = -1 # FAILURE return exit_code
def main(): options = parse_args() if options.debug: LOG = setup_logging(logging.DEBUG) else: LOG = setup_logging(logging.INFO) if options.action == 'trigger-all-talos': trigger_all_talos_jobs(options.repo_name, options.rev, options.times, dry_run=options.dry_run) sys.exit(0) validate_options(options) if not options.dry_run and not valid_credentials(): sys.exit(-1) # Setting the QUERY_SOURCE global variable in mozci.py set_query_source(options.query_source) if options.buildernames: options.buildernames = sanitize_buildernames(options.buildernames) repo_url = query_repo_url_from_buildername(options.buildernames[0]) if not options.repo_name: repo_name = query_repo_name_from_buildername(options.buildernames[0]) else: repo_name = options.repo_name repo_url = query_repo_url(repo_name) if options.rev == 'tip': revision = query_repo_tip(repo_url).changesets[0].node LOG.info("The tip of %s is %s", repo_name, revision) else: revision = query_push_by_revision(repo_url, options.rev, return_revision_list=True) # Schedule jobs through TaskCluster if --taskcluster option has been set to true if options.taskcluster: mgr = TaskClusterBuildbotManager(web_auth=True) else: mgr = BuildAPIManager() trigger_build_if_missing = options.trigger_build_if_missing if repo_name == 'try': trigger_build_if_missing = False # Mode 0: Backfill if options.backfill: manual_backfill(revision, options.buildernames[0], dry_run=options.dry_run) return # Mode 1: Trigger coalesced jobs if options.coalesced: query_api = BuildApi() request_ids = query_api.find_all_jobs_by_status( repo_name, revision, COALESCED) if len(request_ids) == 0: LOG.info('We did not find any coalesced job') for request_id in request_ids: make_retrigger_request(repo_name=repo_name, request_id=request_id, auth=get_credentials(), dry_run=options.dry_run) return # Mode #2: Fill-in a revision or trigger_test_jobs_only if options.fill_revision or options.trigger_tests_only: mgr.trigger_missing_jobs_for_revision( repo_name=repo_name, revision=revision, dry_run=options.dry_run, trigger_build_if_missing=not options.trigger_tests_only) return # Mode #3: Trigger jobs based on revision list modifiers if not (options.includes or options.exclude or options.failed_jobs or options.trigger_talos_for_build): job_names = options.buildernames # Mode 4 - Schedule every builder matching --includes and does not match --exclude. elif options.includes or options.exclude: _includes_excludes(options) # Mode 5: Use --failed-jobs to trigger jobs for particular revision elif options.failed_jobs: job_names = TreeherderApi().find_all_jobs_by_status( repo_name=repo_name, revision=revision, status=WARNING) elif options.trigger_talos_for_build: trigger_talos_jobs_for_build( buildername=options.buildernames[0], revision=revision, times=2, dry_run=options.dry_run, ) exit(0) for buildername in job_names: revlist = determine_revlist(repo_url=repo_url, buildername=buildername, rev=revision, back_revisions=options.back_revisions, delta=options.delta, from_rev=options.from_rev, backfill=options.backfill, skips=options.skips, max_revisions=options.max_revisions) _print_treeherder_link(revlist=revlist, repo_name=repo_name, buildername=buildername, revision=revision, log=LOG, includes=options.includes, exclude=options.exclude) try: mgr.trigger_range( buildername=buildername, repo_name=repo_name, revisions=revlist, times=options.times, dry_run=options.dry_run, files=options.files, trigger_build_if_missing=trigger_build_if_missing) except Exception as e: LOG.exception(e) exit(1)
def main(): options = parse_args() if options.debug: LOG = setup_logging(logging.DEBUG) else: LOG = setup_logging(logging.INFO) if options.action == 'trigger-all-talos': trigger_all_talos_jobs(options.repo_name, options.rev, options.times, dry_run=options.dry_run) sys.exit(0) validate_options(options) if not options.dry_run and not valid_credentials(): sys.exit(-1) # Setting the QUERY_SOURCE global variable in mozci.py set_query_source(options.query_source) if options.buildernames: options.buildernames = sanitize_buildernames(options.buildernames) repo_url = query_repo_url_from_buildername(options.buildernames[0]) if not options.repo_name: repo_name = query_repo_name_from_buildername(options.buildernames[0]) else: repo_name = options.repo_name repo_url = query_repo_url(repo_name) if options.rev == 'tip': revision = query_repo_tip(repo_url).changesets[0].node LOG.info("The tip of %s is %s", repo_name, revision) else: revision = query_push_by_revision(repo_url, options.rev, return_revision_list=True) # Schedule jobs through TaskCluster if --taskcluster option has been set to true if options.taskcluster: mgr = TaskClusterBuildbotManager(web_auth=True) else: mgr = BuildAPIManager() trigger_build_if_missing = options.trigger_build_if_missing if repo_name == 'try': trigger_build_if_missing = False # Mode 0: Backfill if options.backfill: manual_backfill(revision, options.buildernames[0], dry_run=options.dry_run) return # Mode 1: Trigger coalesced jobs if options.coalesced: query_api = BuildApi() request_ids = query_api.find_all_jobs_by_status(repo_name, revision, COALESCED) if len(request_ids) == 0: LOG.info('We did not find any coalesced job') for request_id in request_ids: make_retrigger_request(repo_name=repo_name, request_id=request_id, auth=get_credentials(), dry_run=options.dry_run) return # Mode #2: Fill-in a revision or trigger_test_jobs_only if options.fill_revision or options.trigger_tests_only: mgr.trigger_missing_jobs_for_revision( repo_name=repo_name, revision=revision, dry_run=options.dry_run, trigger_build_if_missing=not options.trigger_tests_only ) return # Mode #3: Trigger jobs based on revision list modifiers if not (options.includes or options.exclude or options.failed_jobs or options.trigger_talos_for_build): job_names = options.buildernames # Mode 4 - Schedule every builder matching --includes and does not match --exclude. elif options.includes or options.exclude: _includes_excludes(options) # Mode 5: Use --failed-jobs to trigger jobs for particular revision elif options.failed_jobs: job_names = TreeherderApi().find_all_jobs_by_status( repo_name=repo_name, revision=revision, status=WARNING) elif options.trigger_talos_for_build: trigger_talos_jobs_for_build( buildername=options.buildernames[0], revision=revision, times=2, dry_run=options.dry_run, ) exit(0) for buildername in job_names: revlist = determine_revlist( repo_url=repo_url, buildername=buildername, rev=revision, back_revisions=options.back_revisions, delta=options.delta, from_rev=options.from_rev, backfill=options.backfill, skips=options.skips, max_revisions=options.max_revisions) _print_treeherder_link( revlist=revlist, repo_name=repo_name, buildername=buildername, revision=revision, log=LOG, includes=options.includes, exclude=options.exclude) try: mgr.trigger_range( buildername=buildername, repo_name=repo_name, revisions=revlist, times=options.times, dry_run=options.dry_run, files=options.files, trigger_build_if_missing=trigger_build_if_missing ) except Exception as e: LOG.exception(e) exit(1)
def main(): options = parse_args() if options.debug: LOG = setup_logging(logging.DEBUG) else: LOG = setup_logging(logging.INFO) if options.action == 'trigger-all-talos': trigger_all_talos_jobs(options.repo_name, options.rev, options.times, dry_run=options.dry_run) sys.exit(0) validate_options(options) if not options.dry_run and not valid_credentials(): sys.exit(-1) # Setting the QUERY_SOURCE global variable in mozci.py set_query_source(options.query_source) if options.buildernames: options.buildernames = sanitize_buildernames(options.buildernames) repo_url = query_repo_url_from_buildername(options.buildernames[0]) if not options.repo_name: repo_name = query_repo_name_from_buildername(options.buildernames[0]) else: repo_name = options.repo_name repo_url = query_repo_url(repo_name) if options.rev == 'tip': revision = query_repo_tip(repo_url).changesets[0].node LOG.info("The tip of %s is %s", repo_name, revision) else: revision = query_push_by_revision(repo_url, options.rev, return_revision_list=True) # Schedule jobs through TaskCluster if --taskcluster option has been set to true if options.taskcluster: mgr = TaskClusterBuildbotManager() else: mgr = BuildAPIManager() trigger_build_if_missing = options.trigger_build_if_missing if repo_name == 'try': trigger_build_if_missing = False # Mode 0: Backfill if options.backfill: manual_backfill(revision, options.buildernames[0], dry_run=options.dry_run) return # Mode 1: Trigger coalesced jobs if options.coalesced: query_api = BuildApi() request_ids = query_api.find_all_jobs_by_status(repo_name, revision, COALESCED) if len(request_ids) == 0: LOG.info('We did not find any coalesced job') for request_id in request_ids: make_retrigger_request(repo_name=repo_name, request_id=request_id, auth=get_credentials(), dry_run=options.dry_run) return # Mode #2: Fill-in a revision or trigger_test_jobs_only if options.fill_revision or options.trigger_tests_only: mgr.trigger_missing_jobs_for_revision( repo_name=repo_name, revision=revision, dry_run=options.dry_run, trigger_build_if_missing=not options.trigger_tests_only ) return # Mode #3: Trigger jobs based on revision list modifiers if not (options.includes or options.exclude or options.failed_jobs or options.trigger_talos_for_build): job_names = options.buildernames # Mode 4 - Schedule every builder matching --includes and does not match --exclude. elif options.includes or options.exclude: filters_in = options.includes.split(',') + [repo_name] filters_out = [] if options.exclude: filters_out = options.exclude.split(',') job_names = filter_buildernames( buildernames=query_builders(repo_name=repo_name), include=filters_in, exclude=filters_out ) if len(job_names) == 0: LOG.info("0 jobs match these filters. please try again.") return if options.existing_only: # We query all successful jobs for a given revision and filter # them by include/exclude filters. trigger_build_if_missing = False successful_jobs = TreeherderApi().find_all_jobs_by_status( repo_name=repo_name, revision=revision, status=SUCCESS) # We will filter out all the existing job from those successful job we have. job_names = [buildername for buildername in successful_jobs if buildername in job_names] cont = raw_input("The ones which have existing builds out of %i jobs will be triggered,\ do you wish to continue? y/n/d (d=show details) " % len(job_names)) else: cont = raw_input("%i jobs will be triggered, do you wish to continue? \ y/n/d (d=show details) " % len(job_names)) if cont.lower() == 'd': LOG.info("The following jobs will be triggered: \n %s" % '\n'.join(job_names)) cont = raw_input("Do you wish to continue? y/n ") if cont.lower() != 'y': exit(1) # Mode 5: Use --failed-jobs to trigger jobs for particular revision elif options.failed_jobs: job_names = TreeherderApi().find_all_jobs_by_status( repo_name=repo_name, revision=revision, status=WARNING) elif options.trigger_talos_for_build: trigger_talos_jobs_for_build( buildername=options.buildernames[0], revision=revision, times=2, dry_run=options.dry_run, ) exit(0) for buildername in job_names: revlist = determine_revlist( repo_url=repo_url, buildername=buildername, rev=revision, back_revisions=options.back_revisions, delta=options.delta, from_rev=options.from_rev, backfill=options.backfill, skips=options.skips, max_revisions=options.max_revisions) _print_treeherder_link( revlist=revlist, repo_name=repo_name, buildername=buildername, revision=revision, log=LOG, includes=options.includes, exclude=options.exclude) try: mgr.trigger_range( buildername=buildername, repo_name=repo_name, revisions=revlist, times=options.times, dry_run=options.dry_run, files=options.files, trigger_build_if_missing=trigger_build_if_missing ) except Exception, e: LOG.exception(e) exit(1)