Add wheel builds

Sem-Ver: feature
Change-Id: I2e9799c142669ee58973779c07c574f9c2854203
diff --git a/doc/source/roles/build_openstack_requirements/index.rst b/doc/source/roles/build_openstack_requirements/index.rst
new file mode 100644
index 0000000..bcdfa7e
--- /dev/null
+++ b/doc/source/roles/build_openstack_requirements/index.rst
@@ -0,0 +1,10 @@
+.. Copyright (C) 2022 VEXXHOST, Inc.
+.. SPDX-License-Identifier: Apache-2.0
+
+``build_openstack_requirements``
+================================
+
+.. toctree::
+   :maxdepth: 2
+
+   defaults/main
\ No newline at end of file
diff --git a/playbooks/build_wheels.yml b/playbooks/build_wheels.yml
new file mode 100644
index 0000000..e5977a6
--- /dev/null
+++ b/playbooks/build_wheels.yml
@@ -0,0 +1,18 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- name: Build wheels for OpenStack requirements
+  hosts: all
+  roles:
+    - build_openstack_requirements
\ No newline at end of file
diff --git a/releasenotes/notes/add-wheel-builds-e731c5a64f98964b.yaml b/releasenotes/notes/add-wheel-builds-e731c5a64f98964b.yaml
new file mode 100644
index 0000000..5d11c4f
--- /dev/null
+++ b/releasenotes/notes/add-wheel-builds-e731c5a64f98964b.yaml
@@ -0,0 +1,3 @@
+---
+features:
+  - Added Zuul jobs for building wheels and publishing them
\ No newline at end of file
diff --git a/roles/build_openstack_requirements/defaults/main.yml b/roles/build_openstack_requirements/defaults/main.yml
new file mode 100644
index 0000000..e9afac1
--- /dev/null
+++ b/roles/build_openstack_requirements/defaults/main.yml
@@ -0,0 +1,40 @@
+---
+# .. vim: foldmarker=[[[,]]]:foldmethod=marker
+
+# .. Copyright (C) 2022 VEXXHOST, Inc.
+# .. SPDX-License-Identifier: Apache-2.0
+
+# Default variables
+# =================
+
+# .. contents:: Sections
+#    :local:
+
+
+# .. envvar:: build_openstack_requirements_wheels_directory [[[
+#
+# Root directory where all work for wheels is built.
+build_openstack_requirements_wheels_directory: /tmp
+
+                                                                   # ]]]
+# .. envvar:: build_openstack_requirements_wheels_folder [[[
+#
+# Folder name inside the ``build_openstack_requirements_wheels_directory``
+build_openstack_requirements_wheels_folder:
+  wheels-{{ build_openstack_requirements_release }}-{{ ansible_distribution_release }}-{{ ansible_architecture }}
+
+                                                                   # ]]]
+# .. envvar:: build_openstack_requirements_wheels_path [[[
+#
+# Path to generate the wheels inside of
+build_openstack_requirements_wheels_path:
+  "{{ build_openstack_requirements_wheels_directory }}/{{ build_openstack_requirements_wheels_folder }}"
+
+                                                                   # ]]]
+# .. envvar:: build_openstack_requirements_archive_path [[[
+#
+# Path to the archive of the wheels
+build_openstack_requirements_archive_path:
+  "{{ build_openstack_requirements_wheels_path }}.tar.gz"
+
+                                                                   # ]]]
diff --git a/roles/build_openstack_requirements/tasks/main.yml b/roles/build_openstack_requirements/tasks/main.yml
new file mode 100644
index 0000000..d56e18e
--- /dev/null
+++ b/roles/build_openstack_requirements/tasks/main.yml
@@ -0,0 +1,92 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- name: Generate temporary file for upper-constraints.txt
+  ansible.builtin.tempfile:
+    state: file
+    prefix: upper-constraints-
+  register: _upper_constraints_file
+
+- name: Fetch the exact version of upper-constraints.txt
+  ansible.builtin.get_url:
+    url: "https://releases.openstack.org/constraints/upper/{{ build_openstack_requirements_release }}"
+    dest: "{{ _upper_constraints_file.path }}"
+    mode: 0644
+
+- name: Install Ubuntu Cloud Archive keyring
+  become: true
+  ansible.builtin.apt:
+    name: ubuntu-cloud-keyring
+    state: present
+
+- name: Add the Ubuntu Cloud Archive repository
+  become: true
+  ansible.builtin.apt_repository:
+    repo: deb http://ubuntu-cloud.archive.canonical.com/ubuntu focal-updates/wallaby main
+    state: present
+
+- name: Install build requirements
+  become: true
+  ansible.builtin.apt:
+    name:
+      - liberasurecode-dev
+      - libkrb5-dev
+      - libldap2-dev
+      - libnss3-dev
+      - libpcre3-dev
+      - libsasl2-dev
+      - libssl-dev
+      - libsystemd-dev
+      - libvirt-dev
+    state: present
+
+- name: Remove certain dependencies from upper-constraints.txt
+  ansible.builtin.lineinfile:
+    path: "{{ _upper_constraints_file.path }}"
+    regexp: "^{{ item.name }}"
+    state: absent
+  when: item.when | default(true)
+  loop:
+    # confluent-kafka-python requires librdkafka v1.6.0 or later.
+    - name: confluent-kafka
+      when: ansible_architecture == 'aarch64'
+    # We don't support or use PostgreSQL
+    - name: psycopg2
+    # Not used in any projects that we build
+    - name: grpcio
+    - name: APScheduler
+
+- name: Generate temporary directory for wheels
+  ansible.builtin.file:
+    path: "{{ build_openstack_requirements_wheels_path }}"
+    state: directory
+    mode: 0755
+
+- name: Build all wheels
+  changed_when: false
+  ansible.builtin.command:
+    pip wheel --no-deps -r {{ _upper_constraints_file.path }}
+  args:
+    chdir: "{{ build_openstack_requirements_wheels_path }}"
+  environment:
+    CASS_DRIVER_BUILD_CONCURRENCY: "{{ ansible_processor_vcpus }}"
+    MAKEFLAGS: "-j{{ ansible_processor_vcpus }}"
+
+- name: Create archive with all wheels
+  # TODO(mnaser): Switch this to "community.general.archive" once Zuul is using
+  #               a new enough Ansible.
+  archive:
+    path: "{{ build_openstack_requirements_wheels_path }}/*.whl"
+    dest: "{{ build_openstack_requirements_archive_path }}"
+    mode: 0644
diff --git a/zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/post-run.yml b/zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/post-run.yml
new file mode 100644
index 0000000..09b306e
--- /dev/null
+++ b/zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/post-run.yml
@@ -0,0 +1,55 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- hosts: all
+  tasks:
+    - name: Find tarballs in the project directory
+      find:
+        file_type: file
+        paths: "{{ build_openstack_requirements_wheels_directory }}"
+        patterns: "*.tar.gz"
+      register: result
+
+    - name: Display stat for tarballs
+      stat:
+        path: "{{ item.path }}"
+      with_items: "{{ result.files }}"
+
+    - name: Create destination directory on executor
+      delegate_to: localhost
+      file:
+        path: "{{ zuul.executor.work_root }}/artifacts"
+        state: directory
+        mode: 0755
+
+    - name: Collect tarball artifacts
+      synchronize:
+        dest: "{{ zuul.executor.work_root }}/artifacts"
+        mode: pull
+        src: "{{ item.path }}"
+        verify_host: true
+        owner: no
+        group: no
+      with_items: "{{ result.files }}"
+
+    - name: Return artifacts to Zuul
+      loop: "{{ result.files }}"
+      zuul_return:
+        data:
+          zuul:
+            artifacts:
+              - name: "Wheels ({{ ansible_architecture }})"
+                url: "artifacts/{{ item.path | basename }}"
+                metadata:
+                  type: wheels
\ No newline at end of file
diff --git a/zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/pre-run.yml b/zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/pre-run.yml
new file mode 100644
index 0000000..17680c5
--- /dev/null
+++ b/zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/pre-run.yml
@@ -0,0 +1,17 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- hosts: all
+  roles:
+    - ensure-pip
\ No newline at end of file
diff --git a/zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/post-run.yml b/zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/post-run.yml
new file mode 100644
index 0000000..073c2fe
--- /dev/null
+++ b/zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/post-run.yml
@@ -0,0 +1,45 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- hosts: all
+  tasks:
+    - name: Display stat for tarballs
+      stat:
+        path: /tmp/wheels.tar.gz
+
+    - name: Create destination directory on executor
+      delegate_to: localhost
+      file:
+        path: "{{ zuul.executor.work_root }}/artifacts"
+        state: directory
+        mode: 0755
+
+    - name: Collect tarball artifacts
+      synchronize:
+        dest: "{{ zuul.executor.work_root }}/artifacts"
+        mode: pull
+        src: /tmp/wheels.tar.gz
+        verify_host: true
+        owner: no
+        group: no
+
+    - name: Return artifacts to Zuul
+      zuul_return:
+        data:
+          zuul:
+            artifacts:
+              - name: Wheels
+                url: artifacts/wheels.tar.gz
+                metadata:
+                  type: wheels
\ No newline at end of file
diff --git a/zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/run.yml b/zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/run.yml
new file mode 100644
index 0000000..519aabb
--- /dev/null
+++ b/zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/run.yml
@@ -0,0 +1,44 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- hosts: all
+  gather_facts: false
+  tasks:
+    - name: Download all artifacts
+      ansible.builtin.get_url:
+        url: "{{ item.url }}"
+        dest: "/tmp/{{ item.url | basename }}"
+        mode: '0440'
+      loop: "{{ zuul.artifacts }}"
+      when: item.metadata.get("type") == "wheels"
+
+    - name: Create a folder for all wheels
+      ansible.builtin.file:
+        path: "/tmp/wheels"
+        state: directory
+
+    - name: Extract all wheels into the same folder
+      ansible.builtin.unarchive:
+        src: "/tmp/{{ item.url | basename }}"
+        dest: /tmp/wheels
+        remote_src: true
+      loop: "{{ zuul.artifacts }}"
+      when: item.metadata.get("type") == "wheels"
+
+    - name: Create archive with all wheels
+      # TODO(mnaser): Switch this to "community.general.archive" once Zuul is using
+      #               a new enough Ansible.
+      archive:
+        path: "/tmp/wheels/*.whl"
+        dest: /tmp/wheels.tar.gz
\ No newline at end of file
diff --git a/zuul.d/wheels-wallaby.yaml b/zuul.d/wheels-wallaby.yaml
new file mode 100644
index 0000000..2f10d80
--- /dev/null
+++ b/zuul.d/wheels-wallaby.yaml
@@ -0,0 +1,57 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- job:
+    name: ansible-collection-atmosphere-build-wheels-wallaby
+    parent: ansible-collection-atmosphere-build-wheels
+    vars:
+      build_openstack_requirements_release: wallaby
+
+- job:
+    name: ansible-collection-atmosphere-build-wheels-wallaby-amd64
+    parent: ansible-collection-atmosphere-build-wheels-wallaby
+    nodeset: ubuntu-focal
+
+- job:
+    name: ansible-collection-atmosphere-build-wheels-wallaby-aarch64
+    parent: ansible-collection-atmosphere-build-wheels-wallaby
+    nodeset: ubuntu-focal-arm64
+
+- job:
+    name: ansible-collection-atmosphere-merge-wheels-wallaby
+    parent: ansible-collection-atmosphere-merge-wheels
+    dependencies:
+      - ansible-collection-atmosphere-build-wheels-wallaby-amd64
+      - ansible-collection-atmosphere-build-wheels-wallaby-aarch64
+
+- job:
+    name: ansible-collection-atmosphere-promote-wheels-wallaby
+    parent: ansible-collection-atmosphere-promote-wheels
+    vars:
+      artifact_extra_name: wheels-wallaby
+
+- project:
+    check:
+      jobs:
+        - ansible-collection-atmosphere-build-wheels-wallaby-amd64
+        - ansible-collection-atmosphere-build-wheels-wallaby-aarch64
+        - ansible-collection-atmosphere-merge-wheels-wallaby
+    gate:
+      jobs:
+        - ansible-collection-atmosphere-build-wheels-wallaby-amd64
+        - ansible-collection-atmosphere-build-wheels-wallaby-aarch64
+        - ansible-collection-atmosphere-merge-wheels-wallaby
+    promote:
+      jobs:
+        - ansible-collection-atmosphere-promote-wheels-wallaby
\ No newline at end of file
diff --git a/zuul.d/wheels.yaml b/zuul.d/wheels.yaml
new file mode 100644
index 0000000..ee7ab18
--- /dev/null
+++ b/zuul.d/wheels.yaml
@@ -0,0 +1,47 @@
+# Copyright (c) 2022 VEXXHOST, 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.
+
+- job:
+    name: ansible-collection-atmosphere-build-wheels
+    abstract: true
+    pre-run:
+      - zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/pre-run.yml
+    run:
+      - playbooks/build_wheels.yml
+    post-run:
+      - zuul.d/playbooks/ansible-collection-atmosphere-build-wheels/post-run.yml
+    files: &build-wheels-files
+      - playbooks/build_wheels.yml
+      - roles/build_openstack_requirements/.*
+    vars:
+      build_openstack_requirements_wheels_directory: /tmp
+
+- job:
+    name: ansible-collection-atmosphere-merge-wheels
+    abstract: true
+    run:
+      - zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/run.yml
+    post-run:
+      - zuul.d/playbooks/ansible-collection-atmosphere-merge-wheels/post-run.yml
+    files: *build-wheels-files
+
+- job:
+    name: ansible-collection-atmosphere-promote-wheels
+    parent: opendev-promote-python
+    abstract: true
+    vars:
+      download_artifact_job: "{{ zuul.job | replace('promote', 'merge') }}"
+      download_artifact_type:
+        - wheels
+    files: *build-wheels-files
\ No newline at end of file