#!/usr/bin/env python3 # # Copyright (C) 2017 Mixed Mode GmbH # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Main ISAR executable. Bitbake itself cannot be executed, instead call this wrapper for # running builds. # import argparse import sys import docker import logging import traceback import os from threading import Thread # Remove capabilities from container increasing # host system savety. CAP_DROP = ['MKNOD'] def get_logger(): logger = logging.getLogger('isar') formatter = logging.Formatter('%(asctime)s-%(name)s-%(levelname)s: %(message)s') logger.setLevel(logging.DEBUG) shandler = logging.StreamHandler() shandler.setLevel(logging.DEBUG) shandler.setFormatter(formatter) logger.addHandler(shandler) return logger # Add function tracing def addlog(func): def wrapper(*args, **kwargs): log.debug('Starting %s """%s"""' % (func.__name__, func.__doc__)) func(*args, **kwargs) log.debug('Finished %s """%s"""' % (func.__name__, func.__doc__)) return wrapper log = get_logger() def docker_log(stream_handler): """ Threaded handler for receiving current container and also image build logs. Print status lines in place instead of writing those logs into next newlines: {"status":"Extracting","progressDetail":{"current":40370176,"total":45129088},"progress":"[============================================\u003e ] 40.37 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":40828928,"total":45129088},"progress":"[=============================================\u003e ] 40.83 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":41287680,"total":45129088},"progress":"[=============================================\u003e ] 41.29 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":41746432,"total":45129088},"progress":"[==============================================\u003e ] 41.75 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":42205184,"total":45129088},"progress":"[==============================================\u003e ] 42.21 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":42663936,"total":45129088},"progress":"[===============================================\u003e ] 42.66 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":43122688,"total":45129088},"progress":"[===============================================\u003e ] 43.12 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":43581440,"total":45129088},"progress":"[================================================\u003e ] 43.58 MB/45.13 MB","id":"3e17c6eae66c"} {"status":"Extracting","progressDetail":{"current":44040192,"total":45129088},"progress":"[================================================\u003e ] 44.04 MB/45.13 MB","id":"3e17c6eae66c"} """ for i in stream_handler: if isinstance(i, dict): # Image build logs # # different keys possible (stream, status) # #i = i['stream'] print(i.strip()) else: # Container logs print(i.decode().strip()) class IsarDocker(): def __init__(self, args): try: self.client = docker.from_env() except: traceback.print_exc() log.error('Cannot connect to the docker socket! Exciting now...') exit(2) # ENOENT self.builddir = os.environ['BUILDDIR'] self.bspdir = os.path.realpath(os.path.join(self.builddir, '..')) self.dockerdir = args.path self.cap_drop = CAP_DROP self.dockerdir self.volume_binds = { self.bspdir : { 'bind' : self.bspdir, 'mode' : 'rw', } } self.hostconfig = self.client.create_host_config(privileged=False, cap_drop=self.cap_drop, binds=self.volume_binds) @addlog def build(self): """ Build the isar docker image. """ os.chdir(self.builddir) self.blogs = self.client.build(path=self.dockerdir, rm=True, tag='isar_image:nanopi', decode=False) thread = Thread(target=docker_log, args=(self.blogs, )) thread.start() thread.join() @addlog def _create(self, cmd): """ Create the container without starting it.""" try: self.container = self.client.create_container(image='isar_image:nanopi', command=cmd, host_config=self.hostconfig) except: traceback.print_exc() log.error('Cannot create container! Exciting now...') exit(1) @addlog def run(self, cmd): """ Run a command in the container. """ # # TODO: Use exec_create instead, so _create is not required anymore. # BUT!!: Is it possible to drop capabilities then? # self._create(cmd) try: self.client.start(container=self.container.get('Id')) self.clogs = self.client.logs(container=self.container.get('Id'), stream=True) # Thread for receiving current container logs thread = Thread(target=docker_log, args=(self.clogs, )) thread.start() self.retcode = self.client.wait(container=self.container.get('Id')) thread.join() if self.retcode != 0: log.warning('Command return non zero status!') except: traceback.print_exc() log.error('Executing docker container! Exciting now...') exit(1) def run_docker(args): log.debug('Running docker subcommand') dc = IsarDocker(args) if args.setup: dc.build() elif args.run: dc.run(args.run) def run_bitbake(args): log.debug('Running bitbake subcommand') dc = IsarDocker(args) builddir = os.path.basename(dc.builddir) log.debug('Args: %s' % args.args) log.debug('Build directory: %s' % builddir) cmd = "bash -c 'cd %s; source setup-environment %s; bitbake %s'" % (dc.bspdir, builddir, args.args) log.debug(cmd) dc.run(cmd) # # Cli # parser = argparse.ArgumentParser(prog='isar') parser.add_argument('--path', type=str, default='./docker', help='Path to the directory holding the dockerfile. Defaults to $BUILDDIR/docker') subparsers = parser.add_subparsers() # create the parser for the docker command docker_parser = subparsers.add_parser('docker', help='docker --help') docker_parser.add_argument('--create', action='store_true', help='Creates the basic docker image where isar builds run.' 'The dockerfile specified by --path is used.') docker_parser.add_argument('--run', type=str, help='Run a command in the docker container.') docker_parser.set_defaults(func=run_docker) # create the parser for the bitbake command bitbake_parser = subparsers.add_parser('bitbake', help='bitbake --help') bitbake_parser.add_argument('--args', type=str, required=True, help='Arguments forwarded to bitbake command.' 'The complete argument string will be appended to the bitbake command.') bitbake_parser.set_defaults(func=run_bitbake) args = parser.parse_args(sys.argv[1:]) args.func(args)