Initialize config repository
diff --git a/roles/fetch-output-openshift/defaults/main.yaml b/roles/fetch-output-openshift/defaults/main.yaml
new file mode 100644
index 0000000..b040970
--- /dev/null
+++ b/roles/fetch-output-openshift/defaults/main.yaml
@@ -0,0 +1,2 @@
+openshift_pods: "{{ zuul.resources }}"
+zuul_output_dir: "{{ ansible_user_dir }}/zuul-output"
diff --git a/roles/fetch-output-openshift/tasks/main.yaml b/roles/fetch-output-openshift/tasks/main.yaml
new file mode 100644
index 0000000..28ba3b3
--- /dev/null
+++ b/roles/fetch-output-openshift/tasks/main.yaml
@@ -0,0 +1,29 @@
+- name: Set log path for multiple nodes
+ set_fact:
+ log_path: "{{ zuul.executor.log_root }}/{{ inventory_hostname }}"
+ when: groups['all'] | length > 1
+
+- name: Set log path for single node
+ set_fact:
+ log_path: "{{ zuul.executor.log_root }}"
+ when: log_path is not defined
+
+- name: Ensure local output dirs
+ delegate_to: localhost
+ file:
+ path: "{{ item }}"
+ state: directory
+ with_items:
+ - "{{ log_path }}"
+ - "{{ log_path }}/npm"
+ - "{{ zuul.executor.work_root }}/artifacts"
+ - "{{ zuul.executor.work_root }}/docs"
+
+- include_tasks: rsync.yaml
+ when: item.1.pod is defined
+ loop: "{{ openshift_pods.items()|list }}"
+ run_once: true
+
+- name: Remove empty directory
+ command: find "{{ zuul.executor.work_root }}" -empty -type d -delete
+ delegate_to: localhost
diff --git a/roles/fetch-output-openshift/tasks/rsync.yaml b/roles/fetch-output-openshift/tasks/rsync.yaml
new file mode 100644
index 0000000..98643bc
--- /dev/null
+++ b/roles/fetch-output-openshift/tasks/rsync.yaml
@@ -0,0 +1,22 @@
+---
+- name: Copy zuul-output from the pod to the executor
+ command: >
+ oc --context "{{ item.1.context }}"
+ --namespace "{{ item.1.namespace }}"
+ rsync -q --progress=false
+ {{ item.1.pod }}:{{ output.src }}/
+ {{ output.dst }}/
+ no_log: true
+ delegate_to: localhost
+ loop:
+ - src: "{{ zuul_output_dir }}/logs"
+ dst: "{{ log_path }}"
+# This need: https://review.opendev.org/#/c/681748/10/roles/ensure-output-dirs/tasks/main.yaml
+# - src: "{{ zuul_output_dir }}/npm"
+# dst: "{{ log_path }}/npm"
+ - src: "{{ zuul_output_dir }}/artifacts"
+ dst: "{{ zuul.executor.work_root }}/artifacts"
+ - src: "{{ zuul_output_dir }}/docs"
+ dst: "{{ zuul.executor.work_root }}/docs"
+ loop_control:
+ loop_var: output
diff --git a/roles/prepare-workspace-openshift/README.rst b/roles/prepare-workspace-openshift/README.rst
new file mode 100644
index 0000000..caa5163
--- /dev/null
+++ b/roles/prepare-workspace-openshift/README.rst
@@ -0,0 +1,15 @@
+Prepare remote workspaces
+
+This role can be used instead of the `prepare-workspace` role when the
+synchronize module doesn't work with kubectl connection. It copies the
+prepared source repos to the pods' cwd using the `oc rsync` command.
+
+This role is intended to run once before any other role in a Zuul job.
+This role requires the origin-clients to be installed.
+
+**Role Variables**
+
+.. zuul:rolevar:: openshift_pods
+ :default: {{ zuul.resources }}
+
+ The dictionary of pod name, pod information to copy the sources to.
diff --git a/roles/prepare-workspace-openshift/defaults/main.yaml b/roles/prepare-workspace-openshift/defaults/main.yaml
new file mode 100644
index 0000000..fa94895
--- /dev/null
+++ b/roles/prepare-workspace-openshift/defaults/main.yaml
@@ -0,0 +1 @@
+openshift_pods: "{{ zuul.resources }}"
diff --git a/roles/prepare-workspace-openshift/tasks/main.yaml b/roles/prepare-workspace-openshift/tasks/main.yaml
new file mode 100644
index 0000000..0d6d50b
--- /dev/null
+++ b/roles/prepare-workspace-openshift/tasks/main.yaml
@@ -0,0 +1,4 @@
+---
+- include_tasks: rsync.yaml
+ when: item.1.pod is defined
+ loop: "{{ openshift_pods.items()|list }}"
diff --git a/roles/prepare-workspace-openshift/tasks/rsync.yaml b/roles/prepare-workspace-openshift/tasks/rsync.yaml
new file mode 100644
index 0000000..c90c4ed
--- /dev/null
+++ b/roles/prepare-workspace-openshift/tasks/rsync.yaml
@@ -0,0 +1,17 @@
+---
+- name: Create src directory
+ command: >
+ oc --context "{{ item.1.context }}"
+ --namespace "{{ item.1.namespace }}"
+ exec {{ item.1.pod }} mkdir src
+ delegate_to: localhost
+
+- name: Copy src repos to the pod
+ command: >
+ oc --context "{{ item.1.context }}"
+ --namespace "{{ item.1.namespace }}"
+ rsync -q --progress=false
+ {{ zuul.executor.src_root }}/
+ {{ item.1.pod }}:src/
+ no_log: true
+ delegate_to: localhost
diff --git a/roles/remove-zuul-sshkey/README.rst b/roles/remove-zuul-sshkey/README.rst
new file mode 100644
index 0000000..2c2d3d2
--- /dev/null
+++ b/roles/remove-zuul-sshkey/README.rst
@@ -0,0 +1,4 @@
+Remove the zuul ssh key
+
+This role is intended to be run on the Zuul Executor at the start of
+every job to prevent access to public Zuul ssh connection.
diff --git a/roles/remove-zuul-sshkey/library/sshagent_remove_keys.py b/roles/remove-zuul-sshkey/library/sshagent_remove_keys.py
new file mode 100644
index 0000000..b4f6ea6
--- /dev/null
+++ b/roles/remove-zuul-sshkey/library/sshagent_remove_keys.py
@@ -0,0 +1,126 @@
+# Copyright 2018 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 os
+import socket
+import struct
+import sys
+import re
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+SSH_AGENT_FAILURE = 5
+SSH_AGENT_SUCCESS = 6
+SSH_AGENT_IDENTITIES_ANSWER = 12
+
+SSH_AGENTC_REQUEST_IDENTITIES = 11
+SSH_AGENTC_REMOVE_IDENTITY = 18
+
+
+def unpack_string(data):
+ (l,) = struct.unpack('!i', data[:4])
+ d = data[4:4 + l]
+ return (d, data[4 + l:])
+
+
+def pack_string(data):
+ ret = struct.pack('!i', len(data))
+ return ret + data
+
+
+class Agent(object):
+ def __init__(self):
+ path = os.environ['SSH_AUTH_SOCK']
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sock.connect(path)
+
+ def send(self, message_type, contents):
+ payload = struct.pack('!ib', len(contents) + 1, message_type)
+ payload += bytearray(contents)
+ self.sock.send(payload)
+
+ def recv(self):
+ buf = b''
+ while len(buf) < 5:
+ buf += self.sock.recv(1)
+ message_len, message_type = struct.unpack('!ib', buf[:5])
+ buf = buf[5:]
+ while len(buf) < message_len - 1:
+ buf += self.sock.recv(1)
+ return message_type, buf
+
+ def list(self):
+ self.send(SSH_AGENTC_REQUEST_IDENTITIES, b'')
+ mtype, data = self.recv()
+ if mtype != SSH_AGENT_IDENTITIES_ANSWER:
+ raise Exception("Invalid response to list")
+ (nkeys,) = struct.unpack('!i', data[:4])
+ data = data[4:]
+ keys = []
+ for i in range(nkeys):
+ blob, data = unpack_string(data)
+ comment, data = unpack_string(data)
+ keys.append((blob, comment))
+ return keys
+
+ def remove(self, blob):
+ self.send(SSH_AGENTC_REMOVE_IDENTITY, pack_string(blob))
+ mtype, data = self.recv()
+ if mtype != SSH_AGENT_SUCCESS:
+ raise Exception("Key was not removed")
+
+
+def run(remove):
+ a = Agent()
+ keys = a.list()
+ removed = []
+ to_remove = re.compile(remove)
+ for blob, comment in keys:
+ if not to_remove.match(comment.decode('utf8')):
+ continue
+ a.remove(blob)
+ removed.append(comment)
+ return removed
+
+
+def ansible_main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ remove=dict(required=True, type='str')))
+
+ removed = run(module.params.get('remove'))
+
+ module.exit_json(changed=(removed != []),
+ removed=removed)
+
+
+def cli_main():
+ parser = argparse.ArgumentParser(
+ description="Remove ssh keys from agent"
+ )
+ parser.add_argument('remove', nargs='+',
+ help='regex matching comments of keys to remove')
+ args = parser.parse_args()
+
+ removed = run(args.remove)
+ print(removed)
+
+
+if __name__ == '__main__':
+ if sys.stdin.isatty():
+ cli_main()
+ else:
+ ansible_main()
diff --git a/roles/remove-zuul-sshkey/tasks/main.yaml b/roles/remove-zuul-sshkey/tasks/main.yaml
new file mode 100644
index 0000000..e417f58
--- /dev/null
+++ b/roles/remove-zuul-sshkey/tasks/main.yaml
@@ -0,0 +1,8 @@
+---
+- name: Remove master key from local agent
+ # The master key has a filename, all others (e.g., per-project keys)
+ # have "(stdin)" as a comment.
+ sshagent_remove_keys:
+ remove: '^(?!\(stdin\)).*'
+ delegate_to: localhost
+ run_once: true
diff --git a/roles/wait-for-changes-ahead/library/wait_for_changes_ahead.py b/roles/wait-for-changes-ahead/library/wait_for_changes_ahead.py
new file mode 100755
index 0000000..33944d6
--- /dev/null
+++ b/roles/wait-for-changes-ahead/library/wait_for_changes_ahead.py
@@ -0,0 +1,107 @@
+#!/bin/env python3
+
+# Copyright (c) 2018 Red Hat
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software 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 software. If not, see <http://www.gnu.org/licenses/>.
+from __future__ import absolute_import, division, print_function
+import traceback
+import json
+import time
+from six.moves import urllib
+from ansible.module_utils.basic import AnsibleModule
+
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: wait_for_changes_ahead
+short_description: Wait for zuul queue
+author: Tristan de Cacqueray (@tristanC)
+description:
+ - Wait for zuul queue ahead to SUCCEED
+requirements:
+ - "python >= 3.5"
+options:
+ zuul_web_url:
+ description:
+ - The zuul web url to query change status
+ required: true
+ type: str
+ zuul_change:
+ description:
+ - The change nr, patchset nr
+ required: true
+ type: str
+ wait_timeout:
+ description:
+ - The maximum waiting time
+ default: 7200
+ type: int
+'''
+
+log = list()
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ zuul_status_url=dict(required=True, type='str'),
+ zuul_change=dict(required=True, type='str'),
+ wait_timeout=dict(type='int'),
+ )
+ )
+ zuul_status_url = module.params['zuul_status_url']
+ zuul_change = module.params['zuul_change']
+ wait_timeout = module.params.get('wait_timeout', 120)
+ if not wait_timeout:
+ wait_timeout = 120
+ wait_timeout = int(wait_timeout) * 60
+
+ if False:
+ module.exit_json(changed=False, msg="noop")
+ try:
+ start_time = time.monotonic()
+ while True:
+ req = urllib.request.urlopen(
+ zuul_status_url + "/change/%s" % zuul_change)
+ changes = json.loads(req.read().decode('utf-8'))
+
+ if not changes:
+ module.fail_json(msg="Unknown change", log="\n".join(log))
+
+ found = None
+ for change in changes:
+ if change["live"] is True:
+ found = change
+ break
+
+ if found and not change["item_ahead"]:
+ break
+
+ if time.monotonic() - start_time > wait_timeout:
+ module.fail_json(msg="Timeout", log="\n".join(log))
+
+ time.sleep(30)
+ except Exception as e:
+ tb = traceback.format_exc()
+ log.append(str(e))
+ log.append(tb)
+ module.fail_json(msg=str(e), log="\n".join(log))
+ finally:
+ log_text = "\n".join(log)
+ module.exit_json(changed=False, msg=log_text)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/wait-for-changes-ahead/tasks/main.yaml b/roles/wait-for-changes-ahead/tasks/main.yaml
new file mode 100644
index 0000000..f0c1aae
--- /dev/null
+++ b/roles/wait-for-changes-ahead/tasks/main.yaml
@@ -0,0 +1,6 @@
+---
+- name: Wait for changes ahead
+ wait_for_changes_ahead:
+ zuul_status_url: "{{ zuul_web_url }}/api/tenant/{{ zuul.tenant }}/status"
+ zuul_change: "{{ zuul.change }},{{ zuul.patchset }}"
+ wait_timeout: "{{ wait_timeout|default(120) }}"