| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+# VERSION: 0.22 |
|
| 1 |
+# DOCKER-VERSION 0.6.3 |
|
| 2 |
+# AUTHOR: Daniel Mizyrycki <daniel@dotcloud.com> |
|
| 3 |
+# DESCRIPTION: Generate docker-ci daily report |
|
| 4 |
+# COMMENTS: The build process is initiated by deployment.py |
|
| 5 |
+ Report configuration is passed through ./credentials.json at |
|
| 6 |
+# deployment time. |
|
| 7 |
+# TO_BUILD: docker build -t report . |
|
| 8 |
+# TO_DEPLOY: docker run report |
|
| 9 |
+ |
|
| 10 |
+from ubuntu:12.04 |
|
| 11 |
+maintainer Daniel Mizyrycki <daniel@dotcloud.com> |
|
| 12 |
+ |
|
| 13 |
+env PYTHONPATH /report |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+# Add report dependencies |
|
| 17 |
+run echo 'deb http://archive.ubuntu.com/ubuntu precise main universe' > \ |
|
| 18 |
+ /etc/apt/sources.list |
|
| 19 |
+run apt-get update; apt-get install -y python2.7 python-pip ssh rsync |
|
| 20 |
+ |
|
| 21 |
+# Set San Francisco timezone |
|
| 22 |
+run echo "America/Los_Angeles" >/etc/timezone |
|
| 23 |
+run dpkg-reconfigure --frontend noninteractive tzdata |
|
| 24 |
+ |
|
| 25 |
+# Add report code and set default container command |
|
| 26 |
+add . /report |
|
| 27 |
+cmd "/report/report.py" |
| 0 | 28 |
new file mode 100755 |
| ... | ... |
@@ -0,0 +1,130 @@ |
| 0 |
+#!/usr/bin/env python |
|
| 1 |
+ |
|
| 2 |
+'''Deploy docker-ci report container on Digital Ocean. |
|
| 3 |
+Usage: |
|
| 4 |
+ export CONFIG_JSON=' |
|
| 5 |
+ { "DROPLET_NAME": "Digital_Ocean_dropplet_name",
|
|
| 6 |
+ "DO_CLIENT_ID": "Digital_Ocean_client_id", |
|
| 7 |
+ "DO_API_KEY": "Digital_Ocean_api_key", |
|
| 8 |
+ "DOCKER_KEY_ID": "Digital_Ocean_ssh_key_id", |
|
| 9 |
+ "DOCKER_CI_KEY_PATH": "docker-ci_private_key_path", |
|
| 10 |
+ "DOCKER_CI_PUB": "$(cat docker-ci_ssh_public_key.pub)", |
|
| 11 |
+ "DOCKER_CI_ADDRESS" "user@docker-ci_fqdn_server", |
|
| 12 |
+ "SMTP_USER": "SMTP_server_user", |
|
| 13 |
+ "SMTP_PWD": "SMTP_server_password", |
|
| 14 |
+ "EMAIL_SENDER": "Buildbot_mailing_sender", |
|
| 15 |
+ "EMAIL_RCP": "Buildbot_mailing_receipient" }' |
|
| 16 |
+ python deployment.py |
|
| 17 |
+''' |
|
| 18 |
+ |
|
| 19 |
+import re, json, requests, base64 |
|
| 20 |
+from fabric import api |
|
| 21 |
+from fabric.api import cd, run, put, sudo |
|
| 22 |
+from os import environ as env |
|
| 23 |
+from time import sleep |
|
| 24 |
+from datetime import datetime |
|
| 25 |
+ |
|
| 26 |
+# Populate environment variables |
|
| 27 |
+CONFIG = json.loads(env['CONFIG_JSON']) |
|
| 28 |
+for key in CONFIG: |
|
| 29 |
+ env[key] = CONFIG[key] |
|
| 30 |
+ |
|
| 31 |
+# Load DOCKER_CI_KEY |
|
| 32 |
+env['DOCKER_CI_KEY'] = open(env['DOCKER_CI_KEY_PATH']).read() |
|
| 33 |
+ |
|
| 34 |
+DROPLET_NAME = env.get('DROPLET_NAME','report')
|
|
| 35 |
+TIMEOUT = 120 # Seconds before timeout droplet creation |
|
| 36 |
+IMAGE_ID = 894856 # Docker on Ubuntu 13.04 |
|
| 37 |
+REGION_ID = 4 # New York 2 |
|
| 38 |
+SIZE_ID = 66 # memory 512MB |
|
| 39 |
+DO_IMAGE_USER = 'root' # Image user on Digital Ocean |
|
| 40 |
+API_URL = 'https://api.digitalocean.com/' |
|
| 41 |
+ |
|
| 42 |
+ |
|
| 43 |
+class digital_ocean(): |
|
| 44 |
+ |
|
| 45 |
+ def __init__(self, key, client): |
|
| 46 |
+ '''Set default API parameters''' |
|
| 47 |
+ self.key = key |
|
| 48 |
+ self.client = client |
|
| 49 |
+ self.api_url = API_URL |
|
| 50 |
+ |
|
| 51 |
+ def api(self, cmd_path, api_arg={}):
|
|
| 52 |
+ '''Make api call''' |
|
| 53 |
+ api_arg.update({'api_key':self.key, 'client_id':self.client})
|
|
| 54 |
+ resp = requests.get(self.api_url + cmd_path, params=api_arg).text |
|
| 55 |
+ resp = json.loads(resp) |
|
| 56 |
+ if resp['status'] != 'OK': |
|
| 57 |
+ raise Exception(resp['error_message']) |
|
| 58 |
+ return resp |
|
| 59 |
+ |
|
| 60 |
+ def droplet_data(self, name): |
|
| 61 |
+ '''Get droplet data''' |
|
| 62 |
+ data = self.api('droplets')
|
|
| 63 |
+ data = [droplet for droplet in data['droplets'] |
|
| 64 |
+ if droplet['name'] == name] |
|
| 65 |
+ return data[0] if data else {}
|
|
| 66 |
+ |
|
| 67 |
+def json_fmt(data): |
|
| 68 |
+ '''Format json output''' |
|
| 69 |
+ return json.dumps(data, sort_keys = True, indent = 2) |
|
| 70 |
+ |
|
| 71 |
+ |
|
| 72 |
+do = digital_ocean(env['DO_API_KEY'], env['DO_CLIENT_ID']) |
|
| 73 |
+ |
|
| 74 |
+# Get DROPLET_NAME data |
|
| 75 |
+data = do.droplet_data(DROPLET_NAME) |
|
| 76 |
+ |
|
| 77 |
+# Stop processing if DROPLET_NAME exists on Digital Ocean |
|
| 78 |
+if data: |
|
| 79 |
+ print ('Droplet: {} already deployed. Not further processing.'
|
|
| 80 |
+ .format(DROPLET_NAME)) |
|
| 81 |
+ exit(1) |
|
| 82 |
+ |
|
| 83 |
+# Create droplet |
|
| 84 |
+do.api('droplets/new', {'name':DROPLET_NAME, 'region_id':REGION_ID,
|
|
| 85 |
+ 'image_id':IMAGE_ID, 'size_id':SIZE_ID, |
|
| 86 |
+ 'ssh_key_ids':[env['DOCKER_KEY_ID']]}) |
|
| 87 |
+ |
|
| 88 |
+# Wait for droplet to be created. |
|
| 89 |
+start_time = datetime.now() |
|
| 90 |
+while (data.get('status','') != 'active' and (
|
|
| 91 |
+ datetime.now()-start_time).seconds < TIMEOUT): |
|
| 92 |
+ data = do.droplet_data(DROPLET_NAME) |
|
| 93 |
+ print data['status'] |
|
| 94 |
+ sleep(3) |
|
| 95 |
+ |
|
| 96 |
+# Wait for the machine to boot |
|
| 97 |
+sleep(15) |
|
| 98 |
+ |
|
| 99 |
+# Get droplet IP |
|
| 100 |
+ip = str(data['ip_address']) |
|
| 101 |
+print 'droplet: {} ip: {}'.format(DROPLET_NAME, ip)
|
|
| 102 |
+ |
|
| 103 |
+api.env.host_string = ip |
|
| 104 |
+api.env.user = DO_IMAGE_USER |
|
| 105 |
+api.env.key_filename = env['DOCKER_CI_KEY_PATH'] |
|
| 106 |
+ |
|
| 107 |
+# Correct timezone |
|
| 108 |
+sudo('echo "America/Los_Angeles" >/etc/timezone')
|
|
| 109 |
+sudo('dpkg-reconfigure --frontend noninteractive tzdata')
|
|
| 110 |
+ |
|
| 111 |
+# Load JSON_CONFIG environment for Dockerfile |
|
| 112 |
+CONFIG_JSON= base64.b64encode( |
|
| 113 |
+ '{{"DOCKER_CI_PUB": "{DOCKER_CI_PUB}",'
|
|
| 114 |
+ ' "DOCKER_CI_KEY": "{DOCKER_CI_KEY}",'
|
|
| 115 |
+ ' "DOCKER_CI_ADDRESS": "{DOCKER_CI_ADDRESS}",'
|
|
| 116 |
+ ' "SMTP_USER": "{SMTP_USER}",'
|
|
| 117 |
+ ' "SMTP_PWD": "{SMTP_PWD}",'
|
|
| 118 |
+ ' "EMAIL_SENDER": "{EMAIL_SENDER}",'
|
|
| 119 |
+ ' "EMAIL_RCP": "{EMAIL_RCP}"}}'.format(**env))
|
|
| 120 |
+ |
|
| 121 |
+run('mkdir -p /data/report')
|
|
| 122 |
+put('./', '/data/report')
|
|
| 123 |
+with cd('/data/report'):
|
|
| 124 |
+ run('chmod 700 report.py')
|
|
| 125 |
+ run('echo "{}" > credentials.json'.format(CONFIG_JSON))
|
|
| 126 |
+ run('docker build -t report .')
|
|
| 127 |
+ run('rm credentials.json')
|
|
| 128 |
+ run("echo -e '30 09 * * * /usr/bin/docker run report\n' |"
|
|
| 129 |
+ " /usr/bin/crontab -") |
| 0 | 130 |
new file mode 100755 |
| ... | ... |
@@ -0,0 +1,145 @@ |
| 0 |
+#!/usr/bin/python |
|
| 1 |
+ |
|
| 2 |
+'''CONFIG_JSON is a json encoded string base64 environment variable. It is used |
|
| 3 |
+to clone docker-ci database, generate docker-ci report and submit it by email. |
|
| 4 |
+CONFIG_JSON data comes from the file /report/credentials.json inserted in this |
|
| 5 |
+container by deployment.py: |
|
| 6 |
+ |
|
| 7 |
+{ "DOCKER_CI_PUB": "$(cat docker-ci_ssh_public_key.pub)",
|
|
| 8 |
+ "DOCKER_CI_KEY": "$(cat docker-ci_ssh_private_key.key)", |
|
| 9 |
+ "DOCKER_CI_ADDRESS": "user@docker-ci_fqdn_server", |
|
| 10 |
+ "SMTP_USER": "SMTP_server_user", |
|
| 11 |
+ "SMTP_PWD": "SMTP_server_password", |
|
| 12 |
+ "EMAIL_SENDER": "Buildbot_mailing_sender", |
|
| 13 |
+ "EMAIL_RCP": "Buildbot_mailing_receipient" } ''' |
|
| 14 |
+ |
|
| 15 |
+import os, re, json, sqlite3, datetime, base64 |
|
| 16 |
+import smtplib |
|
| 17 |
+from datetime import timedelta |
|
| 18 |
+from subprocess import call |
|
| 19 |
+from os import environ as env |
|
| 20 |
+ |
|
| 21 |
+TODAY = datetime.date.today() |
|
| 22 |
+ |
|
| 23 |
+# Load credentials to the environment |
|
| 24 |
+env['CONFIG_JSON'] = base64.b64decode(open('/report/credentials.json').read())
|
|
| 25 |
+ |
|
| 26 |
+# Remove SSH private key as it needs more processing |
|
| 27 |
+CONFIG = json.loads(re.sub(r'("DOCKER_CI_KEY".+?"(.+?)",)','',
|
|
| 28 |
+ env['CONFIG_JSON'], flags=re.DOTALL)) |
|
| 29 |
+ |
|
| 30 |
+# Populate environment variables |
|
| 31 |
+for key in CONFIG: |
|
| 32 |
+ env[key] = CONFIG[key] |
|
| 33 |
+ |
|
| 34 |
+# Load SSH private key |
|
| 35 |
+env['DOCKER_CI_KEY'] = re.sub('^.+"DOCKER_CI_KEY".+?"(.+?)".+','\\1',
|
|
| 36 |
+ env['CONFIG_JSON'],flags=re.DOTALL) |
|
| 37 |
+ |
|
| 38 |
+# Prevent rsync to validate host on first connection to docker-ci |
|
| 39 |
+os.makedirs('/root/.ssh')
|
|
| 40 |
+open('/root/.ssh/id_rsa','w').write(env['DOCKER_CI_KEY'])
|
|
| 41 |
+os.chmod('/root/.ssh/id_rsa',0600)
|
|
| 42 |
+open('/root/.ssh/config','w').write('StrictHostKeyChecking no\n')
|
|
| 43 |
+ |
|
| 44 |
+ |
|
| 45 |
+# Sync buildbot database from docker-ci |
|
| 46 |
+call('rsync {}:/data/buildbot/master/state.sqlite .'.format(
|
|
| 47 |
+ env['DOCKER_CI_ADDRESS']), shell=True) |
|
| 48 |
+ |
|
| 49 |
+class SQL: |
|
| 50 |
+ def __init__(self, database_name): |
|
| 51 |
+ sql = sqlite3.connect(database_name) |
|
| 52 |
+ # Use column names as keys for fetchall rows |
|
| 53 |
+ sql.row_factory = sqlite3.Row |
|
| 54 |
+ sql = sql.cursor() |
|
| 55 |
+ self.sql = sql |
|
| 56 |
+ |
|
| 57 |
+ def query(self,query_statement): |
|
| 58 |
+ return self.sql.execute(query_statement).fetchall() |
|
| 59 |
+ |
|
| 60 |
+sql = SQL("state.sqlite")
|
|
| 61 |
+ |
|
| 62 |
+ |
|
| 63 |
+class Report(): |
|
| 64 |
+ |
|
| 65 |
+ def __init__(self,period='',date=''): |
|
| 66 |
+ self.data = [] |
|
| 67 |
+ self.period = 'date' if not period else period |
|
| 68 |
+ self.date = str(TODAY) if not date else date |
|
| 69 |
+ self.compute() |
|
| 70 |
+ |
|
| 71 |
+ def compute(self): |
|
| 72 |
+ '''Compute report''' |
|
| 73 |
+ if self.period == 'week': |
|
| 74 |
+ self.week_report(self.date) |
|
| 75 |
+ else: |
|
| 76 |
+ self.date_report(self.date) |
|
| 77 |
+ |
|
| 78 |
+ |
|
| 79 |
+ def date_report(self,date): |
|
| 80 |
+ '''Create a date test report''' |
|
| 81 |
+ builds = [] |
|
| 82 |
+ # Get a queryset with all builds from date |
|
| 83 |
+ rows = sql.query('SELECT * FROM builds JOIN buildrequests'
|
|
| 84 |
+ ' WHERE builds.brid=buildrequests.id and' |
|
| 85 |
+ ' date(start_time, "unixepoch", "localtime") = "{0}"'
|
|
| 86 |
+ ' GROUP BY number'.format(date)) |
|
| 87 |
+ build_names = sorted(set([row['buildername'] for row in rows])) |
|
| 88 |
+ # Create a report build line for a given build |
|
| 89 |
+ for build_name in build_names: |
|
| 90 |
+ tried = len([row['buildername'] |
|
| 91 |
+ for row in rows if row['buildername'] == build_name]) |
|
| 92 |
+ fail_tests = [row['buildername'] for row in rows if ( |
|
| 93 |
+ row['buildername'] == build_name and row['results'] != 0)] |
|
| 94 |
+ fail = len(fail_tests) |
|
| 95 |
+ fail_details = '' |
|
| 96 |
+ fail_pct = int(100.0*fail/tried) if tried != 0 else 100 |
|
| 97 |
+ builds.append({'name': build_name, 'tried': tried, 'fail': fail,
|
|
| 98 |
+ 'fail_pct': fail_pct, 'fail_details':fail_details}) |
|
| 99 |
+ if builds: |
|
| 100 |
+ self.data.append({'date': date, 'builds': builds})
|
|
| 101 |
+ |
|
| 102 |
+ |
|
| 103 |
+ def week_report(self,date): |
|
| 104 |
+ '''Add the week's date test reports to report.data''' |
|
| 105 |
+ date = datetime.datetime.strptime(date,'%Y-%m-%d').date() |
|
| 106 |
+ last_monday = date - datetime.timedelta(days=date.weekday()) |
|
| 107 |
+ week_dates = [last_monday + timedelta(days=x) for x in range(7,-1,-1)] |
|
| 108 |
+ for date in week_dates: |
|
| 109 |
+ self.date_report(str(date)) |
|
| 110 |
+ |
|
| 111 |
+ def render_text(self): |
|
| 112 |
+ '''Return rendered report in text format''' |
|
| 113 |
+ retval = '' |
|
| 114 |
+ fail_tests = {}
|
|
| 115 |
+ for builds in self.data: |
|
| 116 |
+ retval += 'Test date: {0}\n'.format(builds['date'],retval)
|
|
| 117 |
+ table = '' |
|
| 118 |
+ for build in builds['builds']: |
|
| 119 |
+ table += ('Build {name:15} Tried: {tried:4} '
|
|
| 120 |
+ ' Failures: {fail:4} ({fail_pct}%)\n'.format(**build))
|
|
| 121 |
+ if build['name'] in fail_tests: |
|
| 122 |
+ fail_tests[build['name']] += build['fail_details'] |
|
| 123 |
+ else: |
|
| 124 |
+ fail_tests[build['name']] = build['fail_details'] |
|
| 125 |
+ retval += '{0}\n'.format(table)
|
|
| 126 |
+ retval += '\n Builds failing' |
|
| 127 |
+ for fail_name in fail_tests: |
|
| 128 |
+ retval += '\n' + fail_name + '\n' |
|
| 129 |
+ for (fail_id,fail_url,rn_tests,nr_errors,log_errors, |
|
| 130 |
+ tracelog_errors) in fail_tests[fail_name]: |
|
| 131 |
+ retval += fail_url + '\n' |
|
| 132 |
+ retval += '\n\n' |
|
| 133 |
+ return retval |
|
| 134 |
+ |
|
| 135 |
+ |
|
| 136 |
+# Send email |
|
| 137 |
+smtp_from = env['EMAIL_SENDER'] |
|
| 138 |
+subject = '[docker-ci] Daily report for {}'.format(str(TODAY))
|
|
| 139 |
+msg = "From: {}\r\nTo: {}\r\nSubject: {}\r\n\r\n".format(
|
|
| 140 |
+ smtp_from, env['EMAIL_RCP'], subject) |
|
| 141 |
+msg = msg + Report('week').render_text()
|
|
| 142 |
+server = smtplib.SMTP_SSL('smtp.mailgun.org')
|
|
| 143 |
+server.login(env['SMTP_USER'], env['SMTP_PWD']) |
|
| 144 |
+server.sendmail(smtp_from, env['EMAIL_RCP'], msg) |