Add generate-zuul-manifest role
Also, pin gitpython since it's gone py3-only. Change-Id: I0d626945908ec4df785aea793f6243c6fdfbdb14
This commit is contained in:
parent
9efbcdf180
commit
a74ff55816
@ -5,6 +5,7 @@ Log Roles
|
|||||||
.. zuul:autorole:: ara-report
|
.. zuul:autorole:: ara-report
|
||||||
.. zuul:autorole:: ensure-output-dirs
|
.. zuul:autorole:: ensure-output-dirs
|
||||||
.. zuul:autorole:: fetch-output
|
.. zuul:autorole:: fetch-output
|
||||||
|
.. zuul:autorole:: generate-zuul-manifest
|
||||||
.. zuul:autorole:: htmlify-logs
|
.. zuul:autorole:: htmlify-logs
|
||||||
.. zuul:autorole:: merge-output-to-logs
|
.. zuul:autorole:: merge-output-to-logs
|
||||||
.. zuul:autorole:: publish-artifacts-to-fileserver
|
.. zuul:autorole:: publish-artifacts-to-fileserver
|
||||||
|
28
roles/generate-zuul-manifest/README.rst
Normal file
28
roles/generate-zuul-manifest/README.rst
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Generate a Zuul manifest file for log uploading
|
||||||
|
|
||||||
|
This generates a manifest file in preparation for uploading along
|
||||||
|
with logs. The Zuul web interface can fetch this file in order to
|
||||||
|
display logs from a build.
|
||||||
|
|
||||||
|
|
||||||
|
**Role Variables**
|
||||||
|
|
||||||
|
.. zuul:rolevar:: generate_zuul_manifest_root
|
||||||
|
:default: {{ zuul.executor.log_dir }}
|
||||||
|
|
||||||
|
The root directory to index.
|
||||||
|
|
||||||
|
.. zuul:rolevar:: generate_zuul_manifest_filename
|
||||||
|
:default: zuul-manifest.json
|
||||||
|
|
||||||
|
The name of the manifest file.
|
||||||
|
|
||||||
|
.. zuul:rolevar:: generate_zuul_manifest_output
|
||||||
|
:default: {{ zuul.executor.log_dir }}/{{ generate_zuul_manifest_filename }}
|
||||||
|
|
||||||
|
The path to the output manifest file.
|
||||||
|
|
||||||
|
.. zuul:rolevar:: generate_zuul_manifest_type
|
||||||
|
:default: zuul_manifest
|
||||||
|
|
||||||
|
The artifact type to return to Zuul.
|
0
roles/generate-zuul-manifest/__init__.py
Normal file
0
roles/generate-zuul-manifest/__init__.py
Normal file
4
roles/generate-zuul-manifest/defaults/main.yaml
Normal file
4
roles/generate-zuul-manifest/defaults/main.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
generate_zuul_manifest_root: "{{ zuul.executor.log_dir }}"
|
||||||
|
generate_zuul_manifest_filename: "zuul-manifest.json"
|
||||||
|
generate_zuul_manifest_output: "{{ zuul.executor.log_dir }}/{{ generate_zuul_manifest_filename }}"
|
||||||
|
generate_zuul_manifest_type: "zuul_manifest"
|
0
roles/generate-zuul-manifest/library/__init__.py
Normal file
0
roles/generate-zuul-manifest/library/__init__.py
Normal file
124
roles/generate-zuul-manifest/library/generate_manifest.py
Normal file
124
roles/generate-zuul-manifest/library/generate_manifest.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright 2019 Red Hat, Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
|
||||||
|
mimetypes.init()
|
||||||
|
|
||||||
|
|
||||||
|
def path_in_tree(root, path):
|
||||||
|
full_path = os.path.realpath(os.path.abspath(
|
||||||
|
os.path.expanduser(path)))
|
||||||
|
if not full_path.startswith(root):
|
||||||
|
logging.debug("Skipping path outside root: %s" % (path,))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def walk(root, original_root=None):
|
||||||
|
if original_root is None:
|
||||||
|
original_root = root
|
||||||
|
logging.debug("Walk: %s", root)
|
||||||
|
data = []
|
||||||
|
dirs = []
|
||||||
|
files = []
|
||||||
|
for e in os.listdir(root):
|
||||||
|
if os.path.isdir(os.path.join(root, e)):
|
||||||
|
if not os.path.islink(os.path.join(root, e)):
|
||||||
|
dirs.append(e)
|
||||||
|
else:
|
||||||
|
files.append(e)
|
||||||
|
for d in sorted(dirs):
|
||||||
|
logging.debug("Directory: %s", d)
|
||||||
|
path = os.path.join(root, d)
|
||||||
|
if not path_in_tree(original_root, path):
|
||||||
|
continue
|
||||||
|
data.append(dict(name=d,
|
||||||
|
mimetype='application/directory',
|
||||||
|
encoding=None,
|
||||||
|
children=walk(os.path.join(root, d), original_root)))
|
||||||
|
for f in sorted(files):
|
||||||
|
logging.debug("File: %s", f)
|
||||||
|
path = os.path.join(root, f)
|
||||||
|
if not path_in_tree(original_root, path):
|
||||||
|
continue
|
||||||
|
mime_guess, encoding = mimetypes.guess_type(path)
|
||||||
|
if not mime_guess:
|
||||||
|
mime_guess = 'text/plain'
|
||||||
|
st = os.stat(path)
|
||||||
|
last_modified = st[stat.ST_MTIME]
|
||||||
|
size = st[stat.ST_SIZE]
|
||||||
|
data.append(dict(name=f,
|
||||||
|
mimetype=mime_guess,
|
||||||
|
encoding=encoding,
|
||||||
|
last_modified=last_modified,
|
||||||
|
size=size))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def run(root_path, output):
|
||||||
|
data = walk(root_path, root_path)
|
||||||
|
with open(output, 'w') as f:
|
||||||
|
f.write(json.dumps({'tree': data}))
|
||||||
|
|
||||||
|
|
||||||
|
def ansible_main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
root=dict(type='path'),
|
||||||
|
output=dict(type='path'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
p = module.params
|
||||||
|
run(p.get('root'), p.get('output'))
|
||||||
|
|
||||||
|
module.exit_json(changed=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate a Zuul file manifest"
|
||||||
|
)
|
||||||
|
parser.add_argument('--verbose', action='store_true',
|
||||||
|
help='show debug information')
|
||||||
|
parser.add_argument('root',
|
||||||
|
help='Root of upload directory')
|
||||||
|
parser.add_argument('output',
|
||||||
|
help='Output file path')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
run(args.root, args.output)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
cli_main()
|
||||||
|
else:
|
||||||
|
ansible_main()
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"test": "foo"}
|
Binary file not shown.
@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 52 B |
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"test": "foo"}
|
128
roles/generate-zuul-manifest/library/test_generate_manifest.py
Normal file
128
roles/generate-zuul-manifest/library/test_generate_manifest.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Copyright (C) 2019 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
#
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# Make coding more python3-ish
|
||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import os
|
||||||
|
import testtools
|
||||||
|
import fixtures
|
||||||
|
|
||||||
|
from .generate_manifest import walk
|
||||||
|
|
||||||
|
|
||||||
|
FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
|
||||||
|
'test-fixtures')
|
||||||
|
|
||||||
|
|
||||||
|
class SymlinkFixture(fixtures.Fixture):
|
||||||
|
links = [
|
||||||
|
('bad_symlink', '/etc'),
|
||||||
|
('bad_symlink_file', '/etc/issue'),
|
||||||
|
('good_symlink', 'controller'),
|
||||||
|
('recursive_symlink', '.'),
|
||||||
|
('symlink_file', 'job-output.json'),
|
||||||
|
('symlink_loop_a', 'symlink_loop'),
|
||||||
|
('symlink_loop/symlink_loop_b', '..'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _setUp(self):
|
||||||
|
self._cleanup()
|
||||||
|
for (src, target) in self.links:
|
||||||
|
path = os.path.join(FIXTURE_DIR, 'links', src)
|
||||||
|
os.symlink(target, path)
|
||||||
|
self.addCleanup(self._cleanup)
|
||||||
|
|
||||||
|
def _cleanup(self):
|
||||||
|
for (src, target) in self.links:
|
||||||
|
path = os.path.join(FIXTURE_DIR, 'links', src)
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileList(testtools.TestCase):
|
||||||
|
|
||||||
|
def flatten(self, result, out=None, path=''):
|
||||||
|
if out is None:
|
||||||
|
out = []
|
||||||
|
dirs = []
|
||||||
|
for x in result:
|
||||||
|
x['_relative_path'] = os.path.join(path, x['name'])
|
||||||
|
out.append(x)
|
||||||
|
if 'children' in x:
|
||||||
|
dirs.append(x)
|
||||||
|
for x in dirs:
|
||||||
|
self.flatten(x['children'], out, x['_relative_path'])
|
||||||
|
x.pop('children')
|
||||||
|
return out
|
||||||
|
|
||||||
|
def assert_files(self, root, result, files):
|
||||||
|
self.assertEqual(len(result), len(files))
|
||||||
|
for expected, received in zip(files, result):
|
||||||
|
self.assertEqual(expected[0], received['_relative_path'])
|
||||||
|
if expected[0] and expected[0][-1] == '/':
|
||||||
|
efilename = os.path.split(
|
||||||
|
os.path.dirname(expected[0]))[1] + '/'
|
||||||
|
else:
|
||||||
|
efilename = os.path.split(expected[0])[1]
|
||||||
|
self.assertEqual(efilename, received['name'])
|
||||||
|
full_path = os.path.join(root, received['_relative_path'])
|
||||||
|
if received['mimetype'] == 'application/directory':
|
||||||
|
self.assertTrue(os.path.isdir(full_path))
|
||||||
|
else:
|
||||||
|
self.assertTrue(os.path.isfile(full_path))
|
||||||
|
self.assertEqual(expected[1], received['mimetype'])
|
||||||
|
self.assertEqual(expected[2], received['encoding'])
|
||||||
|
|
||||||
|
def find_file(self, file_list, path):
|
||||||
|
for f in file_list:
|
||||||
|
if f.relative_path == path:
|
||||||
|
return f
|
||||||
|
|
||||||
|
def test_single_dir(self):
|
||||||
|
'''Test a single directory with a trailing slash'''
|
||||||
|
|
||||||
|
root = os.path.join(FIXTURE_DIR, 'logs')
|
||||||
|
fl = walk(root)
|
||||||
|
self.assert_files(root, self.flatten(fl), [
|
||||||
|
('controller', 'application/directory', None),
|
||||||
|
('zuul-info', 'application/directory', None),
|
||||||
|
('job-output.json', 'application/json', None),
|
||||||
|
('controller/subdir', 'application/directory', None),
|
||||||
|
('controller/compressed.gz', 'text/plain', 'gzip'),
|
||||||
|
('controller/cpu-load.svg', 'image/svg+xml', None),
|
||||||
|
('controller/journal.xz', 'text/plain', 'xz'),
|
||||||
|
('controller/service_log.txt', 'text/plain', None),
|
||||||
|
('controller/syslog', 'text/plain', None),
|
||||||
|
('controller/subdir/subdir.txt', 'text/plain', None),
|
||||||
|
('zuul-info/inventory.yaml', 'text/plain', None),
|
||||||
|
('zuul-info/zuul-info.controller.txt', 'text/plain', None),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_symlinks(self):
|
||||||
|
'''Test symlinks'''
|
||||||
|
self.useFixture(SymlinkFixture())
|
||||||
|
root = os.path.join(FIXTURE_DIR, 'links')
|
||||||
|
fl = walk(root)
|
||||||
|
self.assert_files(root, self.flatten(fl), [
|
||||||
|
('controller', 'application/directory', None),
|
||||||
|
('symlink_loop', 'application/directory', None),
|
||||||
|
('job-output.json', 'application/json', None),
|
||||||
|
('symlink_file', 'text/plain', None),
|
||||||
|
('controller/service_log.txt', 'text/plain', None),
|
||||||
|
('symlink_loop/placeholder', 'text/plain', None),
|
||||||
|
])
|
14
roles/generate-zuul-manifest/tasks/main.yaml
Normal file
14
roles/generate-zuul-manifest/tasks/main.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
- name: Generate Zuul manifest
|
||||||
|
generate_manifest:
|
||||||
|
root: "{{ generate_zuul_manifest_root }}"
|
||||||
|
output: "{{ generate_zuul_manifest_output }}"
|
||||||
|
|
||||||
|
- name: Return Zuul manifest URL to Zuul
|
||||||
|
zuul_return:
|
||||||
|
data:
|
||||||
|
zuul:
|
||||||
|
artifacts:
|
||||||
|
- name: Manifest
|
||||||
|
url: "{{ generate_zuul_manifest_filename }}"
|
||||||
|
metadata:
|
||||||
|
type: "{{ generate_zuul_manifest_type }}"
|
60
test-playbooks/generate-zuul-manifest.yaml
Normal file
60
test-playbooks/generate-zuul-manifest.yaml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
- name: Run tests for the generate-zuul-manifest role
|
||||||
|
hosts: all
|
||||||
|
pre_tasks:
|
||||||
|
- name: Create test directories
|
||||||
|
file:
|
||||||
|
path: "{{ ansible_user_dir }}/{{ item }}"
|
||||||
|
state: directory
|
||||||
|
loop:
|
||||||
|
- tests
|
||||||
|
- tests/logs
|
||||||
|
|
||||||
|
- name: Create tests files
|
||||||
|
copy:
|
||||||
|
dest: "{{ ansible_user_dir }}/{{ item }}"
|
||||||
|
content: ""
|
||||||
|
loop:
|
||||||
|
- tests/index.txt
|
||||||
|
- tests/logs/file.txt
|
||||||
|
- tests/logs/file.png
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: generate-zuul-manifest
|
||||||
|
generate_zuul_manifest_root: "{{ ansible_user_dir }}/tests"
|
||||||
|
generate_zuul_manifest_filename: "test-manifest.json"
|
||||||
|
generate_zuul_manifest_output: "{{ ansible_user_dir }}/tests/{{ generate_zuul_manifest_filename }}"
|
||||||
|
generate_zuul_manifest_type: "test_zuul_manifest"
|
||||||
|
|
||||||
|
post_tasks:
|
||||||
|
- name: Fetch output
|
||||||
|
fetch:
|
||||||
|
src: "{{ ansible_user_dir }}/tests/test-manifest.json"
|
||||||
|
flat: true
|
||||||
|
dest: "{{ zuul.executor.log_root }}/"
|
||||||
|
|
||||||
|
- name: Load output
|
||||||
|
include_vars:
|
||||||
|
file: "{{ zuul.executor.log_root }}/test-manifest.json"
|
||||||
|
name: manifest
|
||||||
|
|
||||||
|
- name: Check output
|
||||||
|
vars:
|
||||||
|
got: "{{ manifest['tree'] }}"
|
||||||
|
exp:
|
||||||
|
- name: logs
|
||||||
|
mimetype: application/directory
|
||||||
|
children:
|
||||||
|
- name: file.png
|
||||||
|
mimetype: image/png
|
||||||
|
- name: file.txt
|
||||||
|
mimetype: text/plain
|
||||||
|
- name: index.txt
|
||||||
|
mimetype: text/plain
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- got[0]['name'] == exp[0]['name']
|
||||||
|
- got[0]['mimetype'] == exp[0]['mimetype']
|
||||||
|
- got[0]['children'][0]['name'] == exp[0]['children'][0]['name']
|
||||||
|
- got[0]['children'][0]['mimetype'] == exp[0]['children'][0]['mimetype']
|
||||||
|
- got[0]['children'][1]['name'] == exp[0]['children'][1]['name']
|
||||||
|
- got[0]['children'][1]['mimetype'] == exp[0]['children'][1]['mimetype']
|
@ -2,6 +2,7 @@
|
|||||||
# of appearance. Changing the order has an impact on the overall integration
|
# of appearance. Changing the order has an impact on the overall integration
|
||||||
# process, which may cause wedges in the gate later.
|
# process, which may cause wedges in the gate later.
|
||||||
flake8
|
flake8
|
||||||
|
GitPython>=2.1.8,<2.1.12
|
||||||
zuul
|
zuul
|
||||||
|
|
||||||
# We need to pin the ansible version directly here; per the
|
# We need to pin the ansible version directly here; per the
|
||||||
|
@ -413,6 +413,14 @@
|
|||||||
nodes:
|
nodes:
|
||||||
- secondary
|
- secondary
|
||||||
|
|
||||||
|
- job:
|
||||||
|
name: zuul-jobs-test-generate-zuul-manifest
|
||||||
|
description: Test the generate-zuul-manifest role
|
||||||
|
run: test-playbooks/generate-zuul-manifest.yaml
|
||||||
|
files:
|
||||||
|
- ^roles/generate-zuul-manifest/.*
|
||||||
|
- ^test-playbooks/generate-zuul-manifest.yaml
|
||||||
|
|
||||||
- job:
|
- job:
|
||||||
name: zuul-jobs-test-upload-git-mirror
|
name: zuul-jobs-test-upload-git-mirror
|
||||||
description: Test the upload-git-mirror role
|
description: Test the upload-git-mirror role
|
||||||
@ -446,6 +454,7 @@
|
|||||||
- zuul-jobs-test-multinode-roles-ubuntu-bionic
|
- zuul-jobs-test-multinode-roles-ubuntu-bionic
|
||||||
- zuul-jobs-test-multinode-roles-ubuntu-trusty
|
- zuul-jobs-test-multinode-roles-ubuntu-trusty
|
||||||
- zuul-jobs-test-multinode-roles-ubuntu-xenial
|
- zuul-jobs-test-multinode-roles-ubuntu-xenial
|
||||||
|
- zuul-jobs-test-generate-zuul-manifest
|
||||||
- zuul-jobs-test-upload-git-mirror
|
- zuul-jobs-test-upload-git-mirror
|
||||||
gate:
|
gate:
|
||||||
jobs: *id001
|
jobs: *id001
|
||||||
|
Loading…
Reference in New Issue
Block a user