Merge "Use an image object, recorder object and status constants"

This commit is contained in:
Jenkins 2016-06-03 16:17:54 +00:00 committed by Gerrit Code Review
commit 5b170793c0
2 changed files with 197 additions and 143 deletions

View File

@ -80,6 +80,51 @@ class KollaRpmSetupUnknownConfig(Exception):
pass
# Image status constants.
#
# TODO(harlowja): use enum lib in the future??
STATUS_CONNECTION_ERROR = 'connection_error'
STATUS_PUSH_ERROR = 'push_error'
STATUS_ERROR = 'error'
STATUS_PARENT_ERROR = 'parent_error'
STATUS_BUILT = 'built'
STATUS_BUILDING = 'building'
STATUS_UNMATCHED = 'unmatched'
STATUS_MATCHED = 'matched'
STATUS_UNPROCESSED = 'unprocessed'
# All error status constants.
STATUS_ERRORS = (STATUS_CONNECTION_ERROR, STATUS_PUSH_ERROR,
STATUS_ERROR, STATUS_PARENT_ERROR)
class Recorder(object):
"""Recorder/buffer of (unicode) log lines for eventual display."""
def __init__(self):
self._lines = []
def write(self, text=""):
if isinstance(text, six.text_type):
self._lines.append(text)
elif isinstance(text, six.binary_type):
self._lines.append(text.decode('utf8'))
elif isinstance(text, Recorder):
self._lines.extend(text._lines)
else:
self.write(text=str(text))
def clear(self):
self._lines = []
def __iter__(self):
for line in self._lines:
yield line
def __str__(self):
return u"\n".join(self._lines)
def docker_client():
try:
docker_kwargs = docker.utils.kwargs_from_env()
@ -90,6 +135,28 @@ def docker_client():
sys.exit(1)
class Image(object):
def __init__(self, name, canonical_name, path, parent_name='',
status=STATUS_UNPROCESSED, parent=None, source=None):
self.name = name
self.canonical_name = canonical_name
self.path = path
self.status = status
self.parent = parent
self.source = source
self.parent_name = parent_name
self.logs = Recorder()
self.push_logs = Recorder()
self.children = []
self.plugins = []
def __repr__(self):
return ("Image(%s, %s, %s, parent_name=%s,"
" status=%s, parent=%s, source=%s)") % (
self.name, self.canonical_name, self.path,
self.parent_name, self.status, self.parent, self.source)
class PushIntoQueueTask(task.Task):
"""Task that pushes some other task into a queue."""
@ -119,41 +186,41 @@ class PushTask(task.Task):
@property
def name(self):
return 'PushTask(%s)' % self.image['name']
return 'PushTask(%s)' % self.image.name
def run(self):
image = self.image
try:
LOG.debug('%s:Try to push the image', image['name'])
LOG.debug('%s:Try to push the image', image.name)
self.push_image(image)
except requests_exc.ConnectionError:
LOG.exception('%s:Make sure Docker is running and that you'
' have the correct privileges to run Docker'
' (root)', image['name'])
image['status'] = "connection_error"
' (root)', image.name)
image.status = STATUS_CONNECTION_ERROR
except Exception:
LOG.exception('%s:Unknown error when pushing', image['name'])
image['status'] = "push_error"
LOG.exception('%s:Unknown error when pushing', image.name)
image.status = STATUS_PUSH_ERROR
finally:
if "error" not in image['status']:
LOG.info('%s:Pushed successfully', image['name'])
if (image.status not in STATUS_ERRORS
and image.status != STATUS_UNPROCESSED):
LOG.info('%s:Pushed successfully', image.name)
self.success = True
else:
self.success = False
def push_image(self, image):
image['push_logs'] = str()
for response in self.dc.push(image['fullname'],
image.push_logs.clear()
for response in self.dc.push(image.canonical_name,
stream=True,
insecure_registry=True):
stream = json.loads(response)
if 'stream' in stream:
image['push_logs'] = image['logs'] + stream['stream']
image.push_logs.write(image.logs)
image.push_logs.write(stream['stream'])
LOG.info('%s', stream['stream'])
elif 'errorDetail' in stream:
image['status'] = "error"
image.status = STATUS_ERROR
LOG.error(stream['errorDetail']['message'])
@ -171,11 +238,11 @@ class BuildTask(task.Task):
@property
def name(self):
return 'BuildTask(%s)' % self.image['name']
return 'BuildTask(%s)' % self.image.name
def run(self):
self.builder(self.image)
if self.image['status'] == 'built':
if self.image.status == STATUS_BUILT:
self.success = True
@property
@ -189,24 +256,24 @@ class BuildTask(task.Task):
PushTask(self.conf, self.image),
self.push_queue),
])
if self.image['children'] and self.success:
for image in self.image['children']:
if self.image.children and self.success:
for image in self.image.children:
followups.append(BuildTask(self.conf, image, self.push_queue))
return followups
def process_source(self, image, source):
dest_archive = os.path.join(image['path'], source['name'] + '-archive')
dest_archive = os.path.join(image.path, source['name'] + '-archive')
if source.get('type') == 'url':
LOG.debug("%s:Getting archive from %s", image['name'],
LOG.debug("%s:Getting archive from %s", image.name,
source['source'])
try:
r = requests.get(source['source'], timeout=self.conf.timeout)
except requests_exc.Timeout:
LOG.exception('Request timed out while getting archive'
' from %s', source['source'])
image['status'] = "error"
image['logs'] = str()
image.status = STATUS_ERROR
image.logs.clear()
return
if r.status_code == 200:
@ -214,34 +281,34 @@ class BuildTask(task.Task):
f.write(r.content)
else:
LOG.error('%s:Failed to download archive: status_code %s',
image['name'], r.status_code)
image['status'] = "error"
image.name, r.status_code)
image.status = STATUS_ERROR
return
elif source.get('type') == 'git':
clone_dir = '{}-{}'.format(dest_archive,
source['reference'].replace('/', '-'))
try:
LOG.debug("%s:Cloning from %s", image['name'],
LOG.debug("%s:Cloning from %s", image.name,
source['source'])
git.Git().clone(source['source'], clone_dir)
git.Git(clone_dir).checkout(source['reference'])
reference_sha = git.Git(clone_dir).rev_parse('HEAD')
LOG.debug("%s:Git checkout by reference %s (%s)",
image['name'], source['reference'], reference_sha)
image.name, source['reference'], reference_sha)
except Exception as e:
LOG.error("%s:Failed to get source from git", image['name'])
LOG.error("%s:Error:%s", image['name'], str(e))
LOG.error("%s:Failed to get source from git", image.name)
LOG.error("%s:Error:%s", image.name, str(e))
# clean-up clone folder to retry
shutil.rmtree(clone_dir)
image['status'] = "error"
image.status = STATUS_ERROR
return
with tarfile.open(dest_archive, 'w') as tar:
tar.add(clone_dir, arcname=os.path.basename(clone_dir))
elif source.get('type') == 'local':
LOG.debug("%s:Getting local archive from %s", image['name'],
LOG.debug("%s:Getting local archive from %s", image.name,
source['source'])
if os.path.isdir(source['source']):
with tarfile.open(dest_archive, 'w') as tar:
@ -251,9 +318,9 @@ class BuildTask(task.Task):
shutil.copyfile(source['source'], dest_archive)
else:
LOG.error("%s:Wrong source type '%s'", image['name'],
LOG.error("%s:Wrong source type '%s'", image.name,
source.get('type'))
image['status'] = "error"
image.status = STATUS_ERROR
return
# Set time on destination archive to epoch 0
@ -279,31 +346,30 @@ class BuildTask(task.Task):
return buildargs
def builder(self, image):
LOG.debug('%s:Processing', image['name'])
if image['status'] == 'unmatched':
LOG.debug('%s:Processing', image.name)
if image.status == STATUS_UNMATCHED:
return
if (image['parent'] is not None and
image['parent']['status'] in ['error', 'parent_error',
'connection_error']):
if (image.parent is not None and
image.parent.status in STATUS_ERRORS):
LOG.error('%s:Parent image error\'d with message "%s"',
image['name'], image['parent']['status'])
image['status'] = "parent_error"
image.name, image.parent.status)
image.status = STATUS_PARENT_ERROR
return
image['status'] = "building"
LOG.info('%s:Building', image['name'])
image.status = STATUS_BUILDING
LOG.info('%s:Building', image.name)
if 'source' in image and 'source' in image['source']:
self.process_source(image, image['source'])
if image['status'] == "error":
if image.source and 'source' in image.source:
self.process_source(image, image.source)
if image.status in STATUS_ERRORS:
return
plugin_archives = list()
plugins_path = os.path.join(image['path'], 'plugins')
for plugin in image['plugins']:
plugins_path = os.path.join(image.path, 'plugins')
for plugin in image.plugins:
archive_path = self.process_source(image, plugin)
if image['status'] == "error":
if image.status in STATUS_ERRORS:
return
plugin_archives.append(archive_path)
if plugin_archives:
@ -320,19 +386,19 @@ class BuildTask(task.Task):
else:
LOG.error('Failed to create directory %s: %s',
plugins_path, e)
image['status'] = "error"
image.status = STATUS_CONNECTION_ERROR
return
with tarfile.open(os.path.join(image['path'], 'plugins-archive'),
with tarfile.open(os.path.join(image.path, 'plugins-archive'),
'w') as tar:
tar.add(plugins_path, arcname='plugins')
# Pull the latest image for the base distro only
pull = True if image['parent'] is None else False
pull = True if image.parent is None else False
image['logs'] = str()
image.logs.clear()
buildargs = self.update_buildargs()
for response in self.dc.build(path=image['path'],
tag=image['fullname'],
for response in self.dc.build(path=image.path,
tag=image.canonical_name,
nocache=self.nocache,
rm=True,
pull=pull,
@ -341,23 +407,23 @@ class BuildTask(task.Task):
stream = json.loads(response.decode('utf-8'))
if 'stream' in stream:
image['logs'] = image['logs'] + stream['stream']
image.logs.write(stream['stream'])
for line in stream['stream'].split('\n'):
if line:
LOG.info('%s:%s', image['name'], line)
LOG.info('%s:%s', image.name, line)
if 'errorDetail' in stream:
image['status'] = "error"
image.status = STATUS_ERROR
LOG.error('%s:Error\'d with the following message',
image['name'])
image.name)
for line in stream['errorDetail']['message'].split('\n'):
if line:
LOG.error('%s:%s', image['name'], line)
LOG.error('%s:%s', image.name, line)
return
image['status'] = "built"
image.status = STATUS_BUILT
LOG.info('%s:Built', image['name'])
LOG.info('%s:Built', image.name)
class WorkerThread(threading.Thread):
@ -606,31 +672,31 @@ class KollaWorker(object):
if filter_:
patterns = re.compile(r"|".join(filter_).join('()'))
for image in self.images:
if image['status'] == 'matched':
if image.status == STATUS_MATCHED:
continue
if re.search(patterns, image['name']):
image['status'] = 'matched'
while (image['parent'] is not None and
image['parent']['status'] != 'matched'):
image = image['parent']
image['status'] = 'matched'
LOG.debug('%s:Matched regex', image['name'])
if re.search(patterns, image.name):
image.status = STATUS_MATCHED
while (image.parent is not None and
image.parent.status != STATUS_MATCHED):
image = image.parent
image.status = STATUS_MATCHED
LOG.debug('%s:Matched regex', image.name)
else:
image['status'] = 'unmatched'
image.status = STATUS_UNMATCHED
else:
for image in self.images:
image['status'] = 'matched'
image.status = STATUS_MATCHED
def summary(self):
"""Walk the dictionary of images statuses and print results"""
# For debug we print the logs again if the image error'd. This is to
# to help us debug and it will be extra helpful in the gate.
for image in self.images:
if image['status'] == 'error':
LOG.debug("%s:Failed with the following logs", image['name'])
for line in image['logs'].split('\n'):
if image.status in STATUS_ERRORS:
LOG.debug("%s:Failed with the following logs", image.name)
for line in image.logs:
if line:
LOG.debug("%s:%s", image['name'], ''.join(line))
LOG.debug("%s:%s", image.name, line)
self.get_image_statuses()
@ -660,12 +726,12 @@ class KollaWorker(object):
self.image_statuses_good,
self.image_statuses_unmatched)
for image in self.images:
if image['status'] == "built":
self.image_statuses_good[image['name']] = image['status']
elif image['status'] == "unmatched":
self.image_statuses_unmatched[image['name']] = image['status']
if image.status == STATUS_BUILT:
self.image_statuses_good[image.name] = image.status
elif image.status == STATUS_UNMATCHED:
self.image_statuses_unmatched[image.name] = image.status
else:
self.image_statuses_bad[image['name']] = image['status']
self.image_statuses_bad[image.name] = image.status
return (self.image_statuses_bad,
self.image_statuses_good,
self.image_statuses_unmatched)
@ -689,33 +755,26 @@ class KollaWorker(object):
with open(os.path.join(path, 'Dockerfile')) as f:
content = f.read()
image = dict()
image['status'] = "unprocessed"
image['name'] = os.path.basename(path)
image['fullname'] = self.namespace + '/' + self.image_prefix + \
image['name'] + ':' + self.tag
image['path'] = path
image['parent_name'] = content.split(' ')[1].split('\n')[0]
if not image['parent_name'].startswith(self.namespace + '/'):
image['parent'] = None
image['children'] = list()
image['plugins'] = list()
image_name = os.path.basename(path)
canonical_name = (self.namespace + '/' + self.image_prefix +
image_name + ':' + self.tag)
image = Image(image_name, canonical_name, path,
parent_name=content.split(' ')[1].split('\n')[0])
if self.install_type == 'source':
# NOTE(jeffrey4l): register the opts if the section didn't
# register in the kolla/common/config.py file
if image['name'] not in self.conf._groups:
if image.name not in self.conf._groups:
self.conf.register_opts(common_config.get_source_opts(),
image['name'])
image['source'] = process_source_installation(image,
image['name'])
image.name)
image.source = process_source_installation(image, image.name)
for plugin in [match.group(0) for match in
(re.search('{}-plugin-.+'.format(image['name']),
(re.search('{}-plugin-.+'.format(image.name),
section) for section in
self.conf.list_all_sections()) if match]:
self.conf.register_opts(common_config.get_source_opts(),
plugin)
image['plugins'].append(
image.plugins.append(
process_source_installation(image, plugin))
self.images.append(image)
@ -724,25 +783,25 @@ class KollaWorker(object):
dot = graphviz.Digraph(comment='Docker Images Dependency')
dot.body.extend(['rankdir=LR'])
for image in self.images:
if image['status'] not in ['matched']:
if image.status not in [STATUS_MATCHED]:
continue
dot.node(image['name'])
if image['parent'] is not None:
dot.edge(image['parent']['name'], image['name'])
dot.node(image.name)
if image.parent is not None:
dot.edge(image.parent.name, image.name)
with open(to_file, 'w') as f:
f.write(dot.source)
def list_images(self):
for count, image in enumerate(self.images):
print(count + 1, ':', image['name'])
print(count + 1, ':', image.name)
def list_dependencies(self):
match = False
for image in self.images:
if image['status'] in ['matched']:
if image.status in [STATUS_MATCHED]:
match = True
if image['parent'] is None:
if image.parent is None:
base = image
if not match:
print('Nothing matched!')
@ -751,18 +810,17 @@ class KollaWorker(object):
def list_children(images, ancestry):
children = ancestry.values()[0]
for item in images:
if item['status'] not in ['matched']:
if item.status not in [STATUS_MATCHED]:
continue
if not item['children']:
children.append(item['name'])
if not item.children:
children.append(item.name)
else:
newparent = {item['name']: []}
newparent = {item.name: []}
children.append(newparent)
list_children(item['children'], newparent)
list_children(item.children, newparent)
ancestry = {base['name']: []}
list_children(base['children'], ancestry)
ancestry = {base.name: []}
list_children(base.children, ancestry)
pprint.pprint(ancestry)
def find_parents(self):
@ -770,13 +828,13 @@ class KollaWorker(object):
sort_images = dict()
for image in self.images:
sort_images[image['fullname']] = image
sort_images[image.canonical_name] = image
for parent_name, parent in sort_images.items():
for image in sort_images.values():
if image['parent_name'] == parent_name:
parent['children'].append(image)
image['parent'] = parent
if image.parent_name == parent_name:
parent.children.append(image)
image.parent = parent
def build_queue(self, push_queue):
"""Organizes Queue list
@ -791,9 +849,9 @@ class KollaWorker(object):
queue = six.moves.queue.Queue()
for image in self.images:
if image['parent'] is None:
if image.parent is None:
queue.put(BuildTask(self.conf, image, push_queue))
LOG.debug('%s:Added image to queue', image['name'])
LOG.debug('%s:Added image to queue', image.name)
return queue

View File

@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import fixtures
import itertools
import mock
@ -20,24 +21,18 @@ from kolla.cmd import build
from kolla.tests import base
FAKE_IMAGE = {
'name': 'image-base',
'status': 'matched',
'parent': None,
'parent_name': None,
'path': '/fake/path',
'plugins': [],
'fullname': 'image-base:latest',
}
FAKE_IMAGE = build.Image('image-base', 'image-base:latest',
'/fake/path', parent_name=None,
parent=None, status=build.STATUS_MATCHED)
class TasksTest(base.TestCase):
def setUp(self):
super(TasksTest, self).setUp()
self.image = FAKE_IMAGE.copy()
self.image = copy.deepcopy(FAKE_IMAGE)
# NOTE(jeffrey4l): use a real, temporary dir
self.image['path'] = self.useFixture(fixtures.TempDir()).path
self.image.path = self.useFixture(fixtures.TempDir()).path
@mock.patch.dict(os.environ, clear=True)
@mock.patch('docker.Client')
@ -45,7 +40,7 @@ class TasksTest(base.TestCase):
pusher = build.PushTask(self.conf, self.image)
pusher.run()
mock_client().push.assert_called_once_with(
self.image['fullname'], stream=True, insecure_registry=True)
self.image.canonical_name, stream=True, insecure_registry=True)
@mock.patch.dict(os.environ, clear=True)
@mock.patch('docker.Client')
@ -55,7 +50,7 @@ class TasksTest(base.TestCase):
builder.run()
mock_client().build.assert_called_once_with(
path=self.image['path'], tag=self.image['fullname'],
path=self.image.path, tag=self.image.canonical_name,
nocache=False, rm=True, pull=True, forcerm=True,
buildargs=None)
@ -74,7 +69,7 @@ class TasksTest(base.TestCase):
builder.run()
mock_client().build.assert_called_once_with(
path=self.image['path'], tag=self.image['fullname'],
path=self.image.path, tag=self.image.canonical_name,
nocache=False, rm=True, pull=True, forcerm=True,
buildargs=build_args)
@ -92,7 +87,7 @@ class TasksTest(base.TestCase):
builder.run()
mock_client().build.assert_called_once_with(
path=self.image['path'], tag=self.image['fullname'],
path=self.image.path, tag=self.image.canonical_name,
nocache=False, rm=True, pull=True, forcerm=True,
buildargs=build_args)
@ -112,7 +107,7 @@ class TasksTest(base.TestCase):
builder.run()
mock_client().build.assert_called_once_with(
path=self.image['path'], tag=self.image['fullname'],
path=self.image.path, tag=self.image.canonical_name,
nocache=False, rm=True, pull=True, forcerm=True,
buildargs=build_args)
@ -121,7 +116,7 @@ class TasksTest(base.TestCase):
@mock.patch('docker.Client')
@mock.patch('requests.get')
def test_requests_get_timeout(self, mock_get, mock_client):
self.image['source'] = {
self.image.source = {
'source': 'http://fake/source',
'type': 'url',
'name': 'fake-image-base'
@ -129,12 +124,12 @@ class TasksTest(base.TestCase):
push_queue = mock.Mock()
builder = build.BuildTask(self.conf, self.image, push_queue)
mock_get.side_effect = requests.exceptions.Timeout
get_result = builder.process_source(self.image, self.image['source'])
get_result = builder.process_source(self.image, self.image.source)
self.assertIsNone(get_result)
self.assertEqual(self.image['status'], 'error')
self.assertEqual(self.image['logs'], str())
mock_get.assert_called_once_with(self.image['source']['source'],
self.assertEqual(self.image.status, build.STATUS_ERROR)
self.assertEqual(str(self.image.logs), str())
mock_get.assert_called_once_with(self.image.source['source'],
timeout=120)
self.assertFalse(builder.success)
@ -146,8 +141,8 @@ class KollaWorkerTest(base.TestCase):
def setUp(self):
super(KollaWorkerTest, self).setUp()
image = FAKE_IMAGE.copy()
image['status'] = None
image = copy.deepcopy(FAKE_IMAGE)
image.status = None
self.images = [image]
def test_supported_base_type(self):
@ -188,14 +183,15 @@ class KollaWorkerTest(base.TestCase):
'type': 'git'
}
for image in kolla.images:
if image['name'] == 'neutron-server':
self.assertEqual(image['plugins'][0], expected_plugin)
if image.name == 'neutron-server':
self.assertEqual(image.plugins[0], expected_plugin)
break
else:
self.fail('Can not find the expected neutron arista plugin')
def _get_matched_images(self, images):
return [image for image in images if image['status'] == 'matched']
return [image for image in images
if image.status == build.STATUS_MATCHED]
def test_without_profile(self):
kolla = build.KollaWorker(self.conf)