Add new build script

This new build script is written entirely in python and supports
multithreading to speed up the builds.

Partially-Implements: blueprint build-script
Change-Id: Ia630e5a83951ec37706a9596427024f3b7c10ba7
This commit is contained in:
Sam Yaple 2015-06-29 10:28:15 +00:00
parent 1c5988e642
commit 0e0b7e77ab
2 changed files with 246 additions and 0 deletions

View File

@ -1 +1,2 @@
docker-compose==1.3.0 docker-compose==1.3.0
docker-py>=1.2.0

245
tools/build.py Executable file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python
#TODO(SamYaple): Allow image pushing
#TODO(SamYaple): Single image building w/ optional parent building
#TODO(SamYaple): Build only missing images
#TODO(SamYaple): Execute the source install script that will pull down and create tarball
#TODO(SamYaple): Improve logging instead of printing to stdout
from __future__ import print_function
import argparse
import datetime
import json
import os
import Queue
import shutil
import sys
import tempfile
from threading import Thread
import time
import traceback
import docker
class WorkerThread(Thread):
def __init__(self, queue, cache, rm):
self.queue = queue
self.nocache = not cache
self.forcerm = rm
self.dc = docker.Client()
Thread.__init__(self)
def run(self):
""" Executes tasks until the queue is empty """
while True:
try:
data = self.queue.get(block=False)
self.builder(data)
self.queue.task_done()
except Queue.Empty:
break
except:
traceback.print_exc()
self.queue.task_done()
def builder(self, image):
print('Processing:', image['name'])
image['status'] = "building"
if image['parent'] != None and image['parent']['status'] == "error":
image['status'] = "parent_error"
return
# Pull the latest image for the base distro only
pull = True if image['parent'] == None else False
image['logs'] = str()
for response in self.dc.build(path=image['path'],
tag=image['fullname'],
nocache=self.nocache,
rm=True,
pull=pull,
forcerm=self.forcerm):
stream = json.loads(response)
if 'stream' in stream:
image['logs'] = image['logs'] + stream['stream']
elif 'errorDetail' in stream:
image['status'] = "error"
raise Exception(stream['errorDetail']['message'])
image['status'] = "built"
print(image['logs'], '\nProcessed:', image['name'])
def argParser():
parser = argparse.ArgumentParser(description='Kolla build script')
parser.add_argument('-n','--namespace',
help='Set the Docker namespace name',
type=str,
default='kollaglue')
parser.add_argument('--tag',
help='Set the Docker tag',
type=str,
default='latest')
parser.add_argument('-b','--base',
help='The base distro to use when building',
type=str,
default='centos')
parser.add_argument('-t','--type',
help='The method of the Openstack install',
type=str,
default='binary')
parser.add_argument('-c','--cache',
help='Use Docker cache when building',
type=bool,
default=True)
parser.add_argument('-r','--rm',
help='Remove intermediate containers while building',
type=bool,
default=True)
parser.add_argument('-T','--threads',
help='The number of threads to use while building',
type=int,
default=8)
return vars(parser.parse_args())
class KollaWorker():
def __init__(self, args):
self.kolla_dir = os.path.join(sys.path[0], '..')
self.images_dir = os.path.join(self.kolla_dir, 'docker')
self.namespace = args['namespace']
self.base = args['base']
self.type_ = args['type']
self.tag = args['tag']
self.prefix = self.base + '-' + self.type_ + '-'
def setupWorkingDir(self):
""" Creates a working directory for use while building """
ts = time.time()
ts = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d_%H-%M-%S_')
self.temp_dir = tempfile.mkdtemp(prefix='kolla-' + ts)
self.working_dir = os.path.join(self.temp_dir, 'docker')
shutil.copytree(self.images_dir, self.working_dir)
def findDockerfiles(self):
""" Recursive search for Dockerfiles in the working directory """
self.docker_build_paths = list()
path = os.path.join(self.working_dir, self.base, self.type_)
for root, dirs, names in os.walk(path):
if 'Dockerfile' in names:
self.docker_build_paths.append(root)
def cleanup(self):
""" Remove temp files """
shutil.rmtree(self.temp_dir)
def sortImages(self):
""" Build images dependency tiers """
images_to_process = list(self.images)
self.tiers = list()
while images_to_process:
self.tiers.append(list())
processed_images = list()
for image in images_to_process:
if image['parent'] == None:
self.tiers[-1].append(image)
processed_images.append(image)
if len(self.tiers) > 1:
for parent in self.tiers[-2]:
if image['parent'] == parent['fullname']:
image['parent'] = parent
self.tiers[-1].append(image)
processed_images.append(image)
#TODO(SamYaple): Improve error handling in this section
if not processed_images:
print('Could not find parent image from some images. Aborting', file=sys.stderr)
for image in images_to_process:
print(image['name'], image['parent'], file=sys.stderr)
sys.exit()
# You cannot modify a list while using the list in a for loop as it
# will produce unexpected results by messing up the index so we
# build a seperate list and remove them here instead
for image in processed_images:
images_to_process.remove(image)
def summary(self):
""" Walk the list of images and check for errors """
print("Successfully built images")
print("=========================")
for image in self.images:
if image['status'] == "built":
print(image['name'])
print("\nImages that failed to build")
print("===========================")
for image in self.images:
if image['status'] != "built":
print(image['name'], "\r\t\t\t Failed with status:", image['status'])
def buildImageList(self):
self.images = list()
# Walk all of the Dockerfiles and replace the %%KOLLA%% variables
for path in self.docker_build_paths:
with open(os.path.join(path, 'Dockerfile')) as f:
content = f.read().replace('%%KOLLA_NAMESPACE%%', self.namespace)
content = content.replace('%%KOLLA_PREFIX%%', self.prefix)
content = content.replace('%%KOLLA_TAG%%', self.tag)
with open(os.path.join(path, 'Dockerfile'), 'w') as f:
f.write(content)
image = dict()
image['status'] = "unprocessed"
image['name'] = os.path.basename(path)
image['fullname'] = self.namespace + '/' + self.prefix + \
image['name'] + ':' + self.tag
image['path'] = path
image['parent'] = content.split(' ')[1].split('\n')[0]
if not self.namespace in image['parent']:
image['parent'] = None
self.images.append(image)
def buildQueues(self):
"""
Return a list of Queues that have been organized into a hierarchy
based on dependencies
"""
self.buildImageList()
self.sortImages()
pools = list()
for tier in self.tiers:
pool = Queue.Queue()
for image in tier:
pool.put(image)
pools.append(pool)
return pools
def main():
args = argParser()
kolla = KollaWorker(args)
kolla.setupWorkingDir()
kolla.findDockerfiles()
# Returns a list of Queues for us to loop through
for pool in kolla.buildQueues():
for x in xrange(args['threads']):
WorkerThread(pool, args['cache'], args['rm']).start()
# block until queue is empty
pool.join()
kolla.summary()
kolla.cleanup()
if __name__ == '__main__':
main()