Ejemplo n.º 1
0
def run_report(config, blockers, server, header, test, save):

    # fetch all relevant jobs
    jobs = get_jenkins_jobs(server, config['job_search_fields'])

    # log and exit if no jobs found - no reason to send empty report
    num_jobs_fetched = len(jobs)
    if num_jobs_fetched == 0:
        print("No jobs found with given search field. Exiting...")
        return None

    # Get set from the list of all bugs in all jobs
    all_bugs_set = get_bugs_set(blockers) if blockers else {}

    # Create dictionary the set of all bugs with bug id as key and name and link as value
    all_bugs_dict = get_bugs_dict(all_bugs_set, config)

    # Get set from the list of all jira-tickets in all jobs
    all_tickets_set = get_jira_set(blockers) if blockers else {}

    # Create dictionary from the set of all jira tickets with ticket id as key and name and link as value
    all_jira_dict = get_jira_dict(all_tickets_set, config)

    # iterate through all relevant jobs and build report rows
    num_success = 0
    num_unstable = 0
    num_failure = 0
    num_missing = 0
    num_aborted = 0
    num_error = 0
    rows = []
    all_bugs = []
    all_tickets = []
    for job in jobs:

        # get name and osp version from job object
        job_name = job['name']
        osp_version = get_osp_version(job_name)

        # skip if no OSP version could be found
        if osp_version is None:
            print(
                'No OSP version could be found in job {}. Skipping...'.format(
                    job_name))
            continue

        # get job info from jenkins API
        jenkins_api_info = get_jenkins_job_info(server, job_name)

        # if jeeves was unable to collect any good jenkins api info, skip job
        if jenkins_api_info:

            # take action based on last completed build result
            if jenkins_api_info['lcb_result'] in [
                    "SUCCESS", "NO_KNOWN_BUILDS", "ABORTED"
            ]:
                if jenkins_api_info['lcb_result'] == "SUCCESS":
                    num_success += 1
                elif jenkins_api_info['lcb_result'] == "NO_KNOWN_BUILDS":
                    num_missing += 1
                else:
                    num_aborted += 1

                bugs = [{'bug_name': 'N/A', 'bug_url': None}]
                tickets = [{'ticket_name': 'N/A', 'ticket_url': None}]
                other = [{'other_name': 'N/A', 'other_url': None}]

            elif jenkins_api_info['lcb_result'] in ["UNSTABLE", "FAILURE"]:
                if jenkins_api_info['lcb_result'] == "UNSTABLE":
                    num_unstable += 1
                else:
                    num_failure += 1

                # get all related bugs to job
                try:
                    bug_ids = blockers[job_name]['bz']
                    all_bugs.extend(bug_ids)
                    bugs = list(map(all_bugs_dict.get, bug_ids))
                except:
                    bugs = [{
                        'bug_name': "Could not find relevant bug",
                        'bug_url': None
                    }]

                # get all related tickets to job
                try:
                    ticket_ids = blockers[job_name]['jira']
                    all_tickets.extend(ticket_ids)
                    tickets = list(map(all_jira_dict.get, ticket_ids))
                except:
                    tickets = [{
                        'ticket_name': "Could not find relevant ticket",
                        'ticket_url': None
                    }]

                # get any "other" artifact for job
                try:
                    other = get_other_blockers(blockers, job_name)
                except:
                    other = [{'other_name': 'N/A', 'other_url': None}]

            else:
                print(
                    "job {} had lcb_result {}: reporting as error job".format(
                        job_name, jenkins_api_info['lcb_result']))
                jenkins_api_info['lcb_result'] = "ERROR"
                num_error += 1
                bugs = [{'bug_name': 'N/A', 'bug_url': None}]
                tickets = [{'ticket_name': 'N/A', 'ticket_url': None}]
                other = [{'other_name': 'N/A', 'other_url': None}]

            # build row
            row = {
                'osp_version': osp_version,
                'job_name': job_name,
                'job_url': jenkins_api_info['job_url'],
                'lcb_num': jenkins_api_info['lcb_num'],
                'lcb_url': jenkins_api_info['lcb_url'],
                'compose': jenkins_api_info['compose'],
                'lcb_result': jenkins_api_info['lcb_result'],
                'bugs': bugs,
                'tickets': tickets,
                'other': other
            }

            # append row to rows
            rows.append(row)

    # sort rows by descending OSP version
    rows = sorted(rows, key=lambda row: row['osp_version'], reverse=True)

    # initialize summary
    summary = {}

    # job result metrics
    num_jobs = len(rows)
    summary['total_success'] = "Total SUCCESS:  {}/{} = {}%".format(
        num_success, num_jobs, percent(num_success, num_jobs))
    summary['total_unstable'] = "Total UNSTABLE: {}/{} = {}%".format(
        num_unstable, num_jobs, percent(num_unstable, num_jobs))
    summary['total_failure'] = "Total FAILURE:  {}/{} = {}%".format(
        num_failure, num_jobs, percent(num_failure, num_jobs))

    # bug metrics
    all_bugs = [bug_id for bug_id in all_bugs if bug_id != 0]
    if len(all_bugs) == 0:
        summary['total_bugs'] = "Blocker Bugs: 0 total"
    else:
        unique_bugs = set(all_bugs)
        summary['total_bugs'] = "Blocker Bugs: {} total, {} unique".format(
            len(all_bugs), len(unique_bugs))

    # ticket metrics
    all_tickets = [ticket_id for ticket_id in all_tickets if ticket_id != 0]
    if len(all_tickets) == 0:
        summary['total_tickets'] = "Blocker Tickets: 0 total"
    else:
        unique_tickets = set(all_tickets)
        summary[
            'total_tickets'] = "Blocker Tickets: {} total, {} unique".format(
                len(all_tickets), len(unique_tickets))

    # include missing report if needed
    if num_missing > 0:
        summary[
            'total_missing'] = "Total NO_KNOWN_BUILDS:  {}/{} = {}%".format(
                num_missing, num_jobs, percent(num_missing, num_jobs))
    else:
        summary['total_missing'] = False

    # include abort report if needed
    if num_aborted > 0:
        summary['total_aborted'] = "Total ABORTED:  {}/{} = {}%".format(
            num_aborted, num_jobs, percent(num_aborted, num_jobs))
    else:
        summary['total_aborted'] = False

    # include error report if needed
    if num_error > 0:
        summary['total_error'] = "Total ERROR:  {}/{} = {}%".format(
            num_error, num_jobs, percent(num_error, num_jobs))
    else:
        summary['total_error'] = False

    # initialize jinja2 vars
    loader = jinja2.FileSystemLoader('./report_template.html')
    env = jinja2.Environment(loader=loader)
    template = env.get_template('')

    # generate HTML report
    htmlcode = template.render(header=header, rows=rows, summary=summary)

    # parse list of email addresses
    if test:
        recipients = config['email_to_test'].split(',')
    else:
        recipients = config['email_to'].split(',')

    # construct email
    msg = MIMEMultipart()
    msg['From'] = header['user_email_address']
    msg['Subject'] = config['email_subject']
    msg['To'] = ", ".join(recipients)
    msg.attach(MIMEText(htmlcode, 'html'))

    # create SMTP session - if jeeves is unable to do so an HTML file will be generated
    try:
        with SMTP(config['smtp_host']) as smtp:

            # start TLS for security
            smtp.starttls()

            # use ehlo or helo if needed
            smtp.ehlo_or_helo_if_needed()

            # send email to all addresses
            response = smtp.sendmail(msg['From'], recipients, msg.as_string())

            # log success if all recipients recieved report, otherwise raise exception
            if response == {}:
                print(
                    "Report successfully accepted by mail server for delivery")
            else:
                raise Exception(
                    "Mail server cannot deliver report to following recipients: {}"
                    .format(response))

    except Exception as e:
        print("Error sending email report: {}\nHTML file generated".format(e))
        generate_html_file(htmlcode)

    else:
        if save:
            generate_html_file(htmlcode)
Ejemplo n.º 2
0
def run_remind(config, blockers, server, header):

    # get list of all owners in blocker file
    owner_list = []
    for job in blockers:
        owners = blockers[job].get('owners', False)
        if not owners:
            continue
        owner_list.extend(owners)

    # exit if no owners are found for any jobs in blockers file
    if owner_list == []:
        print("No owners found in blocker file")
        return None

    # fetch optional config options, return None if not present
    fpn = config.get('filter_param_name', None)
    fpv = config.get('filter_param_value', None)

    # find each job with no blockers including the owner and send email with agg'd list
    owner_set = set(owner_list)
    for owner in owner_set:
        rows = []
        for job_name in blockers:
            owners = blockers[job_name].get('owners', [])
            osp_version = get_osp_version(job_name)

            # skip if current owner is not owner of this job
            if owner not in owners:
                continue

            # get job info from jenkins API - will return False if an unmanageable error occured
            jenkins_api_info = get_jenkins_job_info(server,
                                                    job_name,
                                                    filter_param_name=fpn,
                                                    filter_param_value=fpv)

            # if jeeves was unable to collect any good jenkins API info, skip job
            if jenkins_api_info:

                # only care about jobs without SUCCESS status
                if jenkins_api_info['lcb_result'] != "SUCCESS":

                    # get all related bugs to job
                    try:
                        bug_ids = blockers[job_name]['bz']
                        if 0 in bug_ids:
                            bug_ids.remove(0)
                        bugs_dict = get_bugs_dict(bug_ids, config)
                        bugs = list(map(bugs_dict.get, bug_ids))
                    except Exception as e:
                        print("Error fetching bugs for job {}: {}".format(
                            job_name, e))
                        bugs = []

                    # get all related tickets to job
                    try:
                        ticket_ids = blockers[job_name]['jira']
                        if 0 in ticket_ids:
                            ticket_ids.remove(0)
                        tickets_dict = get_jira_dict(ticket_ids, config)
                        tickets = list(map(tickets_dict.get, ticket_ids))
                    except Exception as e:
                        print("Error fetching ticket for job {}: {}".format(
                            job_name, e))
                        tickets = []

                    # get any "other" artifact for job
                    try:
                        other = get_other_blockers(blockers, job_name)
                    except Exception as e:
                        print("Error fetching other blockers for job {}: {}".
                              format(job_name, e))
                        other = []

                    # check if row contains any valid blockers for reporting
                    blocker_bool = True
                    if (len(bugs) == 0) and (len(tickets) == 0) and (len(other)
                                                                     == 0):
                        blocker_bool = False

                    # build row
                    row = {
                        'osp_version':
                        osp_version,
                        'job_name':
                        job_name,
                        'build_days_ago':
                        jenkins_api_info['build_days_ago'],
                        'job_url':
                        jenkins_api_info['job_url'],
                        'lcb_num':
                        jenkins_api_info['lcb_num'],
                        'lcb_url':
                        jenkins_api_info['lcb_url'],
                        'compose':
                        jenkins_api_info['compose'],
                        'lcb_result':
                        jenkins_api_info['lcb_result'],
                        'blocker_bool':
                        blocker_bool,
                        'bugs':
                        bugs,
                        'tickets':
                        tickets,
                        'other':
                        other,
                        'tempest_tests_failed':
                        jenkins_api_info['tempest_tests_failed'],
                        'tempest_tests_url':
                        jenkins_api_info['job_url'] +
                        str(jenkins_api_info['lcb_num']) + '/testReport'
                    }

                    # append row to rows
                    rows.append(row)

        # if no rows were generated, owner has all passing jobs
        if rows != []:

            # sort rows by descending OSP version
            rows = sorted(rows,
                          key=lambda row: row['osp_version'],
                          reverse=True)

            # initialize jinja2 vars
            loader = jinja2.FileSystemLoader('./templates')
            env = jinja2.Environment(loader=loader)
            template = env.get_template('remind_template.html')

            # generate HTML report
            htmlcode = template.render(header=header, rows=rows)

            # construct email
            msg = MIMEMultipart()
            msg['From'] = header['user_email_address']
            msg['Subject'] = "Jeeves Reminder for {}".format(owner)
            msg['To'] = owner
            msg.attach(MIMEText(htmlcode, 'html'))

            # create SMTP session - if jeeves is unable to do so an HTML file will be generated
            try:
                with SMTP(config['smtp_host']) as smtp:

                    # start TLS for security
                    smtp.starttls()

                    # use ehlo or helo if needed
                    smtp.ehlo_or_helo_if_needed()

                    # send email to all addresses
                    response = smtp.sendmail(msg['From'], msg['To'],
                                             msg.as_string())

                    # log success if all recipients recieved reminder, otherwise raise exception
                    if response == {}:
                        print(
                            "Reminder successfully accepted by mail server for delivery"
                        )
                    else:
                        raise Exception(
                            "Mail server cannot deliver reminder to following recipients: {}"
                            .format(response))

            except Exception as e:
                print("Error sending email reminder: {}\nHTML file generated".
                      format(e))
                generate_html_file(htmlcode, remind=True)

        else:
            print("Owner {} has all passing jobs!".format(owner))
Ejemplo n.º 3
0
def run_remind(config, blockers, server, header):

    # get list of all owners in blocker file
    owner_list = []
    for job in blockers:
        owners = blockers[job].get('owners', False)
        if not owners:
            continue
        owner_list.extend(owners)

    # exit if no owners are found for any jobs in blockers file
    if owner_list == []:
        print("No owners found in blocker file")
        return None

    # find each job with no blockers including the owner and send email with agg'd list
    owner_set = set(owner_list)
    for owner in owner_set:
        rows = []
        for job_name in blockers:
            owners = blockers[job_name].get('owners', [])
            osp_version = get_osp_version(job_name)

            # skip if current owner is not owner of this job
            if owner not in owners:
                continue

            # get job info from jenkins API - will return False if an unmanageable error occured
            jenkins_api_info = get_jenkins_job_info(server, job_name)

            # if jeeves was unable to collect any good jenkins API info, skip job
            if jenkins_api_info:

                # only care about jobs with UNSTABLE or FAILURE status
                if jenkins_api_info[
                        'lcb_result'] == "UNSTABLE" or jenkins_api_info[
                            'lcb_result'] == "FAILURE":

                    # get all related bugs to job
                    try:
                        bug_ids = blockers[job_name]['bz']
                        bugs_dict = get_bugs_dict(bug_ids, config)
                        bugs = list(map(bugs_dict.get, bug_ids))
                    except:
                        bugs = [{
                            'bug_name': "Could not find relevant bug",
                            'bug_url': None
                        }]

                    # get all related tickets to job
                    try:
                        ticket_ids = blockers[job_name]['jira']
                        tickets_dict = get_jira_dict(ticket_ids, config)
                        tickets = list(map(tickets_dict.get, ticket_ids))
                    except:
                        tickets = [{
                            'ticket_name': "Could not find relevant ticket",
                            'ticket_url': None
                        }]

                    # get any "other" artifact for job
                    try:
                        other = get_other_blockers(blockers, job_name)
                    except:
                        other = [{'other_name': 'N/A', 'other_url': None}]

                    # build row
                    row = {
                        'osp_version': osp_version,
                        'job_name': job_name,
                        'job_url': jenkins_api_info['job_url'],
                        'lcb_num': jenkins_api_info['lcb_num'],
                        'lcb_url': jenkins_api_info['lcb_url'],
                        'compose': jenkins_api_info['compose'],
                        'lcb_result': jenkins_api_info['lcb_result'],
                        'bugs': bugs,
                        'tickets': tickets,
                        'other': other
                    }

                    # append row to rows
                    rows.append(row)

        # if no rows were generated, owner has no jobs that were UNSTABLE or FAILED
        if rows != []:

            # initialize jinja2 vars
            loader = jinja2.FileSystemLoader('./templates')
            env = jinja2.Environment(loader=loader)
            template = env.get_template('remind_template.html')

            # generate HTML report
            htmlcode = template.render(header=header, rows=rows)

            # construct email
            msg = MIMEMultipart()
            msg['From'] = header['user_email_address']
            msg['Subject'] = "Jeeves Reminder for {}".format(owner)
            msg['To'] = owner
            msg.attach(MIMEText(htmlcode, 'html'))

            # create SMTP session - if jeeves is unable to do so an HTML file will be generated
            try:
                with SMTP(config['smtp_host']) as smtp:

                    # start TLS for security
                    smtp.starttls()

                    # use ehlo or helo if needed
                    smtp.ehlo_or_helo_if_needed()

                    # send email to all addresses
                    response = smtp.sendmail(msg['From'], msg['To'],
                                             msg.as_string())

                    # log success if all recipients recieved reminder, otherwise raise exception
                    if response == {}:
                        print(
                            "Reminder successfully accepted by mail server for delivery"
                        )
                    else:
                        raise Exception(
                            "Mail server cannot deliver reminder to following recipients: {}"
                            .format(response))

            except Exception as e:
                print("Error sending email reminder: {}\nHTML file generated".
                      format(e))
                generate_html_file(htmlcode, remind=True)

        else:
            print("Owner {} has no UNSTABLE or FAILED jobs!".format(owner))
Ejemplo n.º 4
0
def run_report(config, blockers, server, header, test_email, no_email, template_file):

	# fetch all relevant jobs
	jobs = get_jenkins_jobs(server, config['job_search_fields'])

	# log and exit if no jobs found - no reason to send empty report
	num_jobs_fetched = len(jobs)
	if num_jobs_fetched == 0:
		print("No jobs found with given search field. Exiting...")
		return None

	# Get set from the list of all bugs in all jobs
	all_bugs_set = get_bugs_set(blockers) if blockers else {}

	# Create dictionary the set of all bugs with bug id as key and name and link as value
	all_bugs_dict = get_bugs_dict(all_bugs_set, config)

	# Get set from the list of all jira-tickets in all jobs
	all_tickets_set = get_jira_set(blockers) if blockers else {}

	# Create dictionary from the set of all jira tickets with ticket id as key and name and link as value
	all_tickets_dict = get_jira_dict(all_tickets_set, config)

	# fetch optional config options, return None if not present
	fpn = config.get('filter_param_name', None)
	fpv = config.get('filter_param_value', None)

	# iterate through all relevant jobs and build report rows
	num_success = 0
	num_unstable = 0
	num_failure = 0
	num_missing = 0
	num_aborted = 0
	num_error = 0
	rows = []
	all_bugs = []
	all_tickets = []
	for job in jobs:

		# get name and osp version from job object
		job_name = job['name']
		osp_version = get_osp_version(job_name)

		# skip if no OSP version could be found
		if osp_version is None:
			print('No OSP version could be found in job {}. Skipping...'.format(job_name))
			continue

		# get job info from jenkins API - will return False if an unmanageable error occured
		jenkins_api_info = get_jenkins_job_info(server, job_name, filter_param_name=fpn, filter_param_value=fpv)

		# if jeeves was unable to collect any good jenkins api info, skip job
		if jenkins_api_info:

			# take action based on last completed build result
			if jenkins_api_info['lcb_result'] == "SUCCESS":
				num_success += 1
				bugs = []
				tickets = []
				other = []

			elif jenkins_api_info['lcb_result'] in ["UNSTABLE", "FAILURE", "ABORTED", "NO_KNOWN_BUILDS"]:
				if jenkins_api_info['lcb_result'] == "UNSTABLE":
					num_unstable += 1
				elif jenkins_api_info['lcb_result'] == "FAILURE":
					num_failure += 1
				elif jenkins_api_info['lcb_result'] == "ABORTED":
					num_aborted += 1
				else:
					num_missing += 1

				# get all related bugs to job
				try:
					bug_ids = blockers[job_name]['bz']
					if 0 in bug_ids:
						bug_ids.remove(0)
					all_bugs.extend(bug_ids)
					bugs = list(map(all_bugs_dict.get, bug_ids))
				except Exception as e:
					print("Error fetching bugs for job {}: {}".format(job_name, e))
					bugs = []

				# get all related tickets to job
				try:
					ticket_ids = blockers[job_name]['jira']
					if 0 in ticket_ids:
						ticket_ids.remove(0)
					all_tickets.extend(ticket_ids)
					tickets = list(map(all_tickets_dict.get, ticket_ids))
				except Exception as e:
					print("Error fetching tickets for job {}: {}".format(job_name, e))
					tickets = []

				# get any "other" artifact for job
				try:
					other = get_other_blockers(blockers, job_name)
				except Exception as e:
					print("Error fetching other blockers for job {}: {}".format(job_name, e))
					other = []

			else:
				print("job {} had lcb_result {}: reporting as error job".format(job_name, jenkins_api_info['lcb_result']))
				jenkins_api_info['lcb_result'] = "ERROR"
				num_error += 1
				bugs = []
				tickets = []
				other = []

			# check if row contains any valid blockers for reporting
			blocker_bool = True
			if (len(bugs) == 0) and (len(tickets) == 0) and (len(other) == 0):
				blocker_bool = False

			# build row
			row = {
				'osp_version': osp_version,
				'job_name': job_name,
				'build_days_ago': jenkins_api_info['build_days_ago'],
				'job_url': jenkins_api_info['job_url'],
				'lcb_num': jenkins_api_info['lcb_num'],
				'lcb_url': jenkins_api_info['lcb_url'],
				'compose': jenkins_api_info['compose'],
				'lcb_result': jenkins_api_info['lcb_result'],
				'blocker_bool': blocker_bool,
				'bugs': bugs,
				'tickets': tickets,
				'other': other
			}

			# append row to rows
			rows.append(row)

	# sort rows by descending OSP version
	rows = sorted(rows, key=lambda row: row['osp_version'], reverse=True)

	# log and exit if no rows built - otherwise program will crash on summary generation
	num_jobs = len(rows)
	if num_jobs == 0:
		print("No rows could be built with data for any of the jobs found with given search field. Exiting...")
		return None

	# initialize summary
	summary = {}

	# job result metrics
	summary['total_success'] = "Total SUCCESS:  {}/{} = {}%".format(num_success, num_jobs, percent(num_success, num_jobs))
	summary['total_unstable'] = "Total UNSTABLE: {}/{} = {}%".format(num_unstable, num_jobs, percent(num_unstable, num_jobs))
	summary['total_failure'] = "Total FAILURE:  {}/{} = {}%".format(num_failure, num_jobs, percent(num_failure, num_jobs))

	# Map color codes with job count and type
	jobs_dict = {
		'#3465a4': (num_success, 'Success'),
		'#515151': (num_aborted, 'Aborted'),
		'#ef2929': (num_failure, 'Failure'),
		'#704426': (num_error, 'Error'),
		'#ffb738': (num_unstable, 'Unstable'),
		'#bbbbbb': (num_missing, 'Missing')
	}

	# Filter only available jobs
	bg_color_list, data_list, labels_list = [], [], []
	for key, (job_count, job_label) in jobs_dict.items():
		if job_count != 0:
			bg_color_list.append(key)
			data_list.append(job_count)
			labels_list.append(job_label)

	# create chart config
	chart_config = {
		'type': 'doughnut',
		'data': {
			'labels': labels_list,
			'datasets': [{
				'backgroundColor': bg_color_list,
				'data': data_list
			}]
		},
		'options': {
			'plugins': {
				'datalabels': {
					'display': 'true',
					'align': 'middle',
					'backgroundColor': '#fff',
					'borderRadius': 20,
					'font': {
						'weight': 'bold',
					}
				},
				'doughnutlabel': {
					'labels': [{
						'text': num_jobs,
						'font': {
							'size': 20,
						}
					}, {
						'text': 'Total Jobs',
						'font': {
							'size': 15,
						}
					}]
				}
			}
		}
	}
	encoded_config = quote(json.dumps(chart_config))
	summary['chart_url'] = f'https://quickchart.io/chart?c={encoded_config}'

	# bug metrics
	all_bugs = [bug_id for bug_id in all_bugs if bug_id != 0]
	if len(all_bugs) == 0:
		summary['total_bugs'] = "Blocker Bugs: 0 total"
	else:
		unique_bugs = set(all_bugs)
		summary['total_bugs'] = "Blocker Bugs: {} total, {} unique".format(len(all_bugs), len(unique_bugs))

	# ticket metrics
	all_tickets = [ticket_id for ticket_id in all_tickets if ticket_id != 0]
	if len(all_tickets) == 0:
		summary['total_tickets'] = "Blocker Tickets: 0 total"
	else:
		unique_tickets = set(all_tickets)
		summary['total_tickets'] = "Blocker Tickets: {} total, {} unique".format(len(all_tickets), len(unique_tickets))

	# include abort report if needed
	if num_aborted > 0:
		summary['total_aborted'] = "Total ABORTED:  {}/{} = {}%".format(num_aborted, num_jobs, percent(num_aborted, num_jobs))
	else:
		summary['total_aborted'] = False

	# include missing report if needed
	if num_missing > 0:
		summary['total_missing'] = "Total NO_KNOWN_BUILDS:  {}/{} = {}%".format(num_missing, num_jobs, percent(num_missing, num_jobs))
	else:
		summary['total_missing'] = False

	# include error report if needed
	if num_error > 0:
		summary['total_error'] = "Total ERROR:  {}/{} = {}%".format(num_error, num_jobs, percent(num_error, num_jobs))
	else:
		summary['total_error'] = False

	# initialize jinja2 vars
	loader = jinja2.FileSystemLoader('./templates')
	env = jinja2.Environment(loader=loader)
	try:
		template = env.get_template(template_file)
	except Exception as e:
		print("Error loading template file: {}\n{}".format(template_file, e))
		sys.exit(1)

	# generate HTML report
	htmlcode = template.render(
		header=header,
		rows=rows,
		summary=summary
	)

	# save HTML report to file if not test run
	if not test_email:
		filename = generate_html_file(htmlcode)
		print('HTML file generated as {}'.format(filename))

	# if "no email" flag has been passed, do not execute this block
	if not no_email:
		try:

			# parse list of email addresses
			if test_email:
				recipients = config['email_to_test'].split(',')
			else:
				recipients = config['email_to'].split(',')

			# construct email
			msg = MIMEMultipart()
			msg['From'] = header['user_email_address']
			msg['Subject'] = config['email_subject']
			msg['To'] = ", ".join(recipients)
			msg.attach(MIMEText(htmlcode, 'html'))

			# create SMTP session
			with SMTP(config['smtp_host']) as smtp:

				# start TLS for security
				smtp.starttls()

				# use ehlo or helo if needed
				smtp.ehlo_or_helo_if_needed()

				# send email to all addresses
				response = smtp.sendmail(msg['From'], recipients, msg.as_string())

				# log success if all recipients recieved report, otherwise raise exception
				if response == {}:
					print("Report successfully accepted by mail server for delivery")
				else:
					raise Exception("Mail server cannot deliver report to following recipients: {}".format(response))

		except Exception as e:
			print('Error sending email report: {}\nSee HTML file saved in "archive" folder'.format(e))