diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/ansible/.gitignore b/ansible/.gitignore new file mode 100644 index 0000000..9b14a9f --- /dev/null +++ b/ansible/.gitignore @@ -0,0 +1,2 @@ +inventory.yaml +vault.yaml diff --git a/ansible/README.MD b/ansible/README.MD new file mode 100644 index 0000000..ffa5f9a --- /dev/null +++ b/ansible/README.MD @@ -0,0 +1,55 @@ +# Installation on RHEL 9 Automatisation + +Ansible Playbook allowing to create ostree images for edge using Ansible. + +## Pre-requisites + +RHEL 9 pre-requisites : + +- RHEL 9 is installed +- The Red Hat repositories **baseos** and **appstream** are reachable + +Microshift pre-requisites : + +- RHEL 9.2 or 9.3 +- LVM volume group (VG) with unused space + +## Pre-requisites on the target machine + +```sh +sudo subscription-manager register --username $RHN_LOGIN --auto-attach +sudo subscription-manager attach --pool=$RHN_POOL_ID +``` + +## Ansible Config + +Create a `inventory.yaml` file inside the ansible folder or define the inventory path inside the `ansible.cfg` file + +Update `config.yaml` in `ansible/group_vars/all/` to match your environment. + +Create an ansible vault named `vault.yaml` in `ansible/group_vars/all/` with the following content. + +```yaml +blueprint_admin_password_hash: # Generate one with "mkpasswd -m bcrypt" +blueprint_kiosk_password_hash: # Generate one with "mkpasswd -m bcrypt" +kickstart_microshift_pull_secret: # Generate one on https://console.redhat.com/openshift/install/pull-secret +``` + +Install the required collections. + +```sh +ansible-galaxy collection install -r requirements.yaml +``` + +## Prepare the target machine + +```sh +ansible-playbook prerequisites.yaml +ansible-playbook bootstrap-ostree.yaml +``` + +## Regular builds + +```sh +ansible-playbook build.yaml +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..2d1ae5d --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,6 @@ +[defaults] +# Use the provided inventory +inventory = inventory.yaml + +# Use a forked copy of the infra.osbuild plugins +library = plugins/modules diff --git a/ansible/bootstrap-ostree.yaml b/ansible/bootstrap-ostree.yaml new file mode 100755 index 0000000..b5e1d33 --- /dev/null +++ b/ansible/bootstrap-ostree.yaml @@ -0,0 +1,77 @@ +- name: Create the initial ostree repo + hosts: all + become: false + tasks: + - name: Read blueprint + register: results + args: + executable: /usr/bin/python3 + stdin: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/files/minimal.toml') }}" + shell: | + import toml + import json + import sys + str=sys.stdin.read() + obj=toml.loads(str) + print(json.dumps(obj)) + delegate_to: localhost + become: false + changed_when: false + + - set_fact: + blueprint_name: '{{ blueprint_object.name }}' + vars: + blueprint_object: '{{ results.stdout | from_json }}' + + - name: Push blueprint + infra.osbuild.push_blueprint: + blueprint: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/files/minimal.toml') }}" + + - name: Start ostree compose + infra.osbuild.start_compose: + blueprint: "{{ blueprint_name }}" + allow_duplicate: true + compose_type: edge-commit + timeout: "{{ compose_timeout }}" + register: builder_compose_start_out + + - ansible.builtin.set_fact: + compose_id: "{{ builder_compose_start_out['result']['body']['build_id'] }}" + + - name: Wait for compose to finish + infra.osbuild.wait_compose: + compose_id: "{{ compose_id }}" + timeout: 3600 + + - ansible.builtin.tempfile: + state: directory + suffix: build + register: tmp + + - name: Export the compose artifact + infra.osbuild.export_compose: # noqa only-builtins + compose_id: "{{ compose_id }}" + dest: "{{ tmp.path }}/{{ compose_id }}.tar" + + - name: Clear directory /var/www/repo + ansible.builtin.file: + path: "{{ www_location }}/repo" + state: absent + + - name: Extract compose artifact into /var/www/repo + ansible.builtin.unarchive: + src: "{{ tmp.path }}/{{ compose_id }}.tar" + dest: "{{ www_location }}" + remote_src: true + become: true + + - name: Create an empty tree + ansible.builtin.file: + path: "{{ tmp.path }}/empty-tree" + mode: '0755' + state: directory + become: true + + - name: Create an empty commit + ansible.builtin.shell: "ostree --repo={{ www_location }}/repo commit -b 'empty' --tree=dir={{ tmp.path }}/empty-tree" + become: true diff --git a/ansible/build.yaml b/ansible/build.yaml new file mode 100644 index 0000000..2b32ea6 --- /dev/null +++ b/ansible/build.yaml @@ -0,0 +1,270 @@ +- name: Build the Kiosk images + hosts: all + become: false + tasks: + - name: Checkout the git repo + ansible.builtin.git: + repo: 'https://github.com/nmasse-itix/red-hat-kiosk.git' + dest: "{{ ansible_user_dir }}/red-hat-kiosk" + update: yes + clone: yes + + - ansible.builtin.tempfile: + state: directory + suffix: -build + register: tmp + + ## + ## Cleanup + ## + + - name: Get all images for removal + ansible.builtin.command: /usr/bin/composer-cli compose list + register: builder_output + changed_when: false + + - name: Remove each image by UUID + ansible.builtin.command: "/usr/bin/composer-cli compose delete {{ (item | split)[0] }}" + loop: "{{ builder_output.stdout_lines }}" + loop_control: + label: "{{ (item | split)[0] }}" + changed_when: true + when: (item | split)[0] != "ID" + + ## + ## RPM construction + ## + + - debug: + msg: "Starting RPM build..." + + - name: Ensure ~/rpmbuild is a symbolic link + ansible.builtin.file: + src: "{{ ansible_user_dir }}/red-hat-kiosk/rpms" + dest: "{{ ansible_user_dir }}/rpmbuild" + state: link + + - name: Build the kiosk-config RPMS + ansible.builtin.shell: | + spectool -g -R {{ ansible_user_dir }}/rpmbuild/SPECS/kiosk-config.spec + rpmbuild -ba {{ ansible_user_dir }}/rpmbuild/SPECS/kiosk-config.spec + + - name: Build the microshift-manifests RPM + ansible.builtin.shell: | + spectool -g -R {{ ansible_user_dir }}/rpmbuild/SPECS/microshift-manifests.spec + rpmbuild -ba {{ ansible_user_dir }}/rpmbuild/SPECS/microshift-manifests.spec + + - name: Ensure the VENDOR directory exists + ansible.builtin.file: + path: "{{ ansible_user_dir }}/rpmbuild/VENDOR" + state: directory + mode: '0755' + + - name: Download Google Chrome RPM + ansible.builtin.get_url: + url: https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm + dest: "{{ ansible_user_dir }}/rpmbuild/VENDOR/google-chrome-stable_current_x86_64.rpm" + + - name: Rebuild the Google Chrome RPM + ansible.builtin.shell: | + set -Eeuo pipefail + rpmrebuild -s {{ ansible_user_dir }}/rpmbuild/SPECS/google-chrome-stable.spec -p {{ ansible_user_dir }}/rpmbuild/VENDOR/google-chrome-stable_current_x86_64.rpm + RPM=$(rpm -q {{ ansible_user_dir }}/rpmbuild/VENDOR/google-chrome-stable_current_x86_64.rpm) + mkdir -p {{ ansible_user_dir }}/rpmbuild/BUILDROOT/$RPM/ + rpm2cpio {{ ansible_user_dir }}/rpmbuild/VENDOR/google-chrome-stable_current_x86_64.rpm | cpio -idmv -D {{ ansible_user_dir }}/rpmbuild/BUILDROOT/$RPM/ + mv {{ ansible_user_dir }}/rpmbuild/BUILDROOT/$RPM/opt/google/ {{ ansible_user_dir }}/rpmbuild/BUILDROOT/$RPM/usr/bin/ + cd {{ ansible_user_dir }}/rpmbuild/BUILDROOT/$RPM/usr/bin/ + rm -f google-chrome-stable + ln -s google/chrome/google-chrome google-chrome-stable + ln -s google/chrome/google-chrome chrome + sed -i.${EPOCHREALTIME:-bak} 's|/opt/google|/usr/bin/google|g' {{ ansible_user_dir }}/rpmbuild/SPECS/google-chrome-stable.spec + rpmbuild -bb {{ ansible_user_dir }}/rpmbuild/SPECS/google-chrome-stable.spec + args: + executable: /bin/bash + register: rebuild_result + failed_when: rebuild_result.rc != 0 + + - name: Get built RPMS + ansible.builtin.find: + path: "{{ ansible_user_dir }}/rpmbuild/RPMS/x86_64/" + patterns: "*.rpm" + register: build_rpms + + - name: Extract filenames from paths of built RPMs + ansible.builtin.set_fact: + rpm_filenames: "{{ build_rpms.files | map(attribute='path') | list }}" + + - name: Copy RPMs to the repository location + ansible.builtin.copy: + src: '{{ item }}' + dest: "{{ repo_location }}" + owner: root + group: root + mode: '0644' + remote_src: yes + loop: '{{ rpm_filenames }}' + loop_control: + label: "{{ item | basename }}" + become: true + + - name: Update the repository with createrepo + become: true + ansible.builtin.command: + cmd: "createrepo {{ repo_location }}" + + - name: Clean dnf cache + become: true + ansible.builtin.command: + cmd: dnf clean all + + ## + ## Ostree construction + ## + + - debug: + msg: "Starting ostree build..." + + - name: Parse blueprint + register: results + args: + executable: /usr/bin/python3 + stdin: "{{ lookup('ansible.builtin.template', 'kiosk.toml.j2') }}" + shell: | + import toml + import json + import sys + str=sys.stdin.read() + obj=toml.loads(str) + print(json.dumps(obj)) + become: false + changed_when: false + + - set_fact: + blueprint_name: '{{ blueprint_object.name }}' + vars: + blueprint_object: '{{ results.stdout | from_json }}' + + - name: Push Blueprint + infra.osbuild.push_blueprint: + blueprint: "{{ lookup('ansible.builtin.template', 'kiosk.toml.j2') }}" + + - name: Start ostree compose + start_compose2: + blueprint: "{{ blueprint_name }}" + allow_duplicate: true + compose_type: edge-commit + ostree_ref: "rhel/9/{{ ansible_facts['userspace_architecture'] }}/edge-kiosk" + ostree_parent: "rhel/9/{{ ansible_facts['userspace_architecture'] }}/edge" + ostree_url: http://{{ ansible_default_ipv4.address }}/repo + timeout: "{{ compose_timeout }}" + register: builder_compose_start_out + + - ansible.builtin.set_fact: + compose_id: "{{ builder_compose_start_out['result']['body']['build_id'] }}" + + - name: Wait for compose to finish + infra.osbuild.wait_compose: + compose_id: "{{ compose_id }}" + timeout: 3600 + + - name: Export the compose artifact + infra.osbuild.export_compose: # noqa only-builtins + compose_id: "{{ compose_id }}" + dest: "{{ tmp.path }}/{{ compose_id }}.tar" + + - name: Create commit directory + ansible.builtin.file: + path: "{{ tmp.path }}/{{ compose_id }}" + mode: '0755' + state: directory + + - name: Extract compose artifact + ansible.builtin.unarchive: + src: "{{ tmp.path }}/{{ compose_id }}.tar" + dest: "{{ tmp.path }}/{{ compose_id }}" + remote_src: true + + - name: Pull local ostree repository + ansible.builtin.shell: ostree --repo={{ www_location }}/repo pull-local "{{ tmp.path }}/{{ compose_id }}/repo" + become: true + + ## + ## ISO Construction + ## + + - debug: + msg: "Starting ISO build..." + + - name: Read blueprint + register: results + args: + executable: /usr/bin/python3 + stdin: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/files/edge-installer.toml') }}" + shell: | + import toml + import json + import sys + str=sys.stdin.read() + obj=toml.loads(str) + print(json.dumps(obj)) + delegate_to: localhost + become: false + changed_when: false + + - set_fact: + blueprint_name: '{{ blueprint_object.name }}' + vars: + blueprint_object: '{{ results.stdout | from_json }}' + + - name: Push blueprint + infra.osbuild.push_blueprint: + blueprint: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/files/edge-installer.toml') }}" + + - name: Start ostree compose + start_compose2: + blueprint: "{{ blueprint_name }}" + allow_duplicate: true + compose_type: edge-installer + ostree_ref: empty + ostree_url: http://{{ ansible_default_ipv4.address }}/repo + timeout: "{{ compose_timeout }}" + register: builder_compose_start_out + + - ansible.builtin.set_fact: + compose_id: "{{ builder_compose_start_out['result']['body']['build_id'] }}" + + - name: Wait for compose to finish + infra.osbuild.wait_compose: + compose_id: "{{ compose_id }}" + timeout: 3600 + + - name: Export the compose artifact + infra.osbuild.export_compose: # noqa only-builtins + compose_id: "{{ compose_id }}" + dest: "{{ tmp.path }}/{{ compose_id }}.iso" + + - name: Create kiosk.ks from template + ansible.builtin.template: + src: "kiosk.ks.j2" + dest: "{{ tmp.path }}/kiosk.ks" + + - name: Validate kiosk.ks using ksvalidator + ansible.builtin.command: + cmd: "ksvalidator {{ tmp.path }}/kiosk.ks" + + - name: Create new kiosk.iso file + ansible.builtin.command: + cmd: "mkksiso -r 'inst.ks' --ks {{ tmp.path }}/kiosk.ks {{ tmp.path }}/{{ compose_id }}.iso {{ tmp.path }}/kiosk.iso" + + - name: Copy new ISO to /var/www + copy: + src: "{{ tmp.path }}/kiosk.iso" + dest: "{{ www_location }}/kiosk.iso" + remote_src: true + become: true + + post_tasks: + - ansible.builtin.file: + path: "{{ tmp.path }}.iso" + state: absent + when: tmp is defined diff --git a/ansible/files/edge-installer.toml b/ansible/files/edge-installer.toml new file mode 100644 index 0000000..8364105 --- /dev/null +++ b/ansible/files/edge-installer.toml @@ -0,0 +1,6 @@ +name = "edge-installer" +description = "" +version = "0.0.0" +modules = [] +groups = [] +packages = [] diff --git a/ansible/files/minimal.toml b/ansible/files/minimal.toml new file mode 100755 index 0000000..9b6bd48 --- /dev/null +++ b/ansible/files/minimal.toml @@ -0,0 +1,6 @@ +name = "minimal-rhel9" +description = "minimal blueprint for ostree commit" +version = "1.1.0" +modules = [] +groups = [] +distro = "rhel-93" diff --git a/ansible/group_vars/all/config.yaml b/ansible/group_vars/all/config.yaml new file mode 100644 index 0000000..6ae268f --- /dev/null +++ b/ansible/group_vars/all/config.yaml @@ -0,0 +1,5 @@ +repo_location: /opt/custom-rpms +blueprint_admin_ssh_public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFW62WJXI1ZCMfNA4w0dMpL0fsldhbEfULNGIUB0nQui nmasse@localhost.localdomain +www_location: /var/www +compose_timeout: 300 +kickstart_root_disk: /dev/disk/by-path/pci-0000:00:12.0-ata-1 diff --git a/ansible/plugins/modules/README.md b/ansible/plugins/modules/README.md new file mode 100644 index 0000000..c296fea --- /dev/null +++ b/ansible/plugins/modules/README.md @@ -0,0 +1,5 @@ +# README + +This is a modified version of the start_compose module from the repo https://github.com/redhat-cop/infra.osbuild, commit 6e3416233c84623b2edd503a4b50d15c61d6c155. + +The module has been patched to specify the ostree ref when starting a compose of type "ostree-commit". diff --git a/ansible/plugins/modules/start_compose2.py b/ansible/plugins/modules/start_compose2.py new file mode 100644 index 0000000..7280673 --- /dev/null +++ b/ansible/plugins/modules/start_compose2.py @@ -0,0 +1,339 @@ +#!/usr/bin/python +# Copyright: Red Hat Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = """ +--- +module: start_compose2 +short_description: Start an ostree compose +description: + - Start an ostree compose +author: + - Adam Miller (@maxamillion) + - Chris Santiago (@resoluteCoder) +options: + blueprint: + description: + - Name of blueprint to iniate a build for + type: str + required: true + size: + description: + - Image size expressed in MiB + type: int + default: 0 + required: false + profile: + description: + - Path to profile toml file + type: str + default: "" + required: false + image_name: + description: + - Image name + type: str + default: "" + required: false + allow_duplicate: + description: + - Allow a duplicate version'd compose. + - (Default osbuild composer functionality is to allow duplicate composes) + type: bool + default: True + required: false + compose_type: + description: + - type of compose + type: str + default: "edge-commit" + required: false + choices: + - ami + - edge-commit + - edge-container + - edge-installer + - edge-raw-image + - edge-simplified-installer + - image-installer + - oci + - openstack + - qcow2 + - tar + - vhd + - vmdk + - iot-commit + - iot-container + - iot-installer + - iot-raw-image + - container + ostree_ref: + description: + - ostree ref + type: str + default: "" + required: false + ostree_parent: + description: + - ostree parent + type: str + default: "" + required: false + ostree_url: + description: + - ostree URL + type: str + default: "" + required: false + timeout: + description: + - timeout for osbuild-compose requests, in seconds + type: int + default: 120 + required: false +notes: + - THIS MODULE IS NOT IDEMPOTENT UNLESS C(allow_duplicate) is set to C(false) + - The params C(profile) and C(image_name) are required together. + - The C(profile) option is not fully implemented at this time. +""" + +EXAMPLES = """ +- name: Start ostree compose size 4096 + start_compose2: + blueprint: rhel-for-edge-demo + image_name: testimage + size: 4096 + +- name: Start ostree compose with idempotent transaction + start_compose2: + blueprint: rhel-for-edge-demo + allow_duplicate: false +""" +import json # noqa E402 +import socket +from typing import Any # noqa E402 + +from ansible.module_utils.basic import AnsibleModule # noqa E402 +from ansible_collections.infra.osbuild.plugins.module_utils.weldr import Weldr # noqa E402 + +argument_spec = dict( + blueprint=dict(type="str", required=True), + size=dict(type="int", required=False, default=0), + profile=dict(type="str", required=False, default=""), + image_name=dict(type="str", required=False, default=""), + allow_duplicate=dict(type="bool", required=False, default=True), + compose_type=dict( + type="str", + required=False, + default="edge-commit", + choices=[ + "ami", + "edge-commit", + "edge-container", + "edge-installer", + "edge-raw-image", + "edge-simplified-installer", + "image-installer", + "oci", + "openstack", + "qcow2", + "tar", + "vhd", + "vmdk", + "iot-commit", + "iot-container", + "iot-installer", + "iot-raw-image", + "container", + ], + ), + ostree_ref=dict(type="str", required=False, default=""), + ostree_parent=dict(type="str", required=False, default=""), + ostree_url=dict(type="str", required=False, default=""), + timeout=dict(type="int", required=False, default=120), +) + + +def start_compose(module, weldr): + changed: bool = False + dupe_compose: list = [] + blueprint_info: dict = weldr.api.get_blueprints_info(module.params["blueprint"]) + blueprint_version: int = blueprint_info["blueprints"][0]["version"] + + # Add check if compose_type is supported + supported_compose_type: dict = weldr.api.get_compose_types() + + is_supported: dict = next((item for item in supported_compose_type["types"] if item["name"] == module.params["compose_type"]), {}) + + if not is_supported: + module.fail_json( + msg="%s is not a valid image type, valid types are: %s" + % (module.params["compose_type"], [[v for k, v in t.items() if k == "name"] for t in supported_compose_type["types"]]), + changed=changed + ) + else: + if not is_supported["enabled"]: + module.fail_json( + msg="%s is not a supported image type, supported image types are: %s" + % (module.params["compose_type"], [[v for k, v in t.items() if k == "enabled" and v is True] for t in supported_compose_type["types"]]), + changed=changed + ) + + if not module.params["allow_duplicate"]: + # only do all this query and filtering if needed + + compose_queue: dict = weldr.api.get_compose_queue() + # {"new":[],"run":[{"id":"930a1584-8737-4b61-ba77-582780f0ff2d","blueprint":"base-image-with-tmux","version":"0.0.5","compose_type":"edge-commit","image_size":0,"queue_status":"RUNNING","job_created":1654620015.4107578,"job_started":1654620015.415151}]} + + compose_queue_run_dupe: list = [ + compose for compose in compose_queue["run"] if (compose["blueprint"] == module.params["blueprint"]) and (compose["version"] == blueprint_version) + ] + compose_queue_new_dupe: list = [ + compose for compose in compose_queue["new"] if (compose["blueprint"] == module.params["blueprint"]) and (compose["version"] == blueprint_version) + ] + + compose_finished: dict = weldr.api.get_compose_finished() + # {"finished":[{"id":"930a1584-8737-4b61-ba77-582780f0ff2d","blueprint":"base-image-with-tmux","version":"0.0.5","compose_type":"edge-commit","image_size":8192,"queue_status":"FINISHED","job_created":1654620015.4107578,"job_started":1654620015.415151,"job_finished":1654620302.9069786}]} + compose_finished_dupe: list = [ + compose + for compose in compose_finished["finished"] + if (compose["blueprint"] == module.params["blueprint"]) and (compose["version"] == blueprint_version) + ] + + compose_failed: dict = weldr.api.get_compose_failed() + # {"failed":[]} + compose_failed_dupe: list = [ + compose + for compose in compose_failed["failed"] + if (compose["blueprint"] == module.params["blueprint"]) and (compose["version"] == blueprint_version) + ] + + dupe_compose: list = compose_queue_run_dupe + compose_queue_new_dupe + compose_failed_dupe + compose_finished_dupe + + if module.params["allow_duplicate"] or (len(dupe_compose) == 0): + # FIXME - build to POST payload and POST that ish + compose_settings: dict[str, Any] = { + "blueprint_name": module.params["blueprint"], + "compose_type": module.params["compose_type"], + "branch": "master", + "size": module.params["size"], + } + + if "edge-commit" in module.params["compose_type"] or "installer" in module.params["compose_type"] or "raw" in module.params["compose_type"]: + compose_settings["ostree"] = { + "ref": module.params["ostree_ref"], + "parent": module.params["ostree_parent"], + "url": module.params["ostree_url"], + } + + try: + result: dict = weldr.api.post_compose(json.dumps(compose_settings), timeout=module.params["timeout"]) + except socket.timeout: + # it's possible we don't get a response back from weldr because on the + # very first run including a new content source composer will build a repo cache + # and when that happens we get an empty JSON response + + compose_queue: dict = weldr.api.get_compose_queue() + # {"new":[],"run":[{"id":"930a1584-8737-4b61-ba77-582780f0ff2d","blueprint":"base-image-with-tmux","version":"0.0.5","compose_type":"edge-commit","image_size":0,"queue_status":"RUNNING","job_created":1654620015.4107578,"job_started":1654620015.415151}]} + + submitted_compose_uuid: str = "" + + submitted_compose_found_run: list[dict[str, str]] = [ + compose + for compose in compose_queue["run"] + if (compose["blueprint"] == module.params["blueprint"]) and (compose["version"] == blueprint_version) + ] + if submitted_compose_found_run: + # we expect it to be RUNNING, so check that first + submitted_compose_uuid: str = submitted_compose_found_run[0]["id"] + else: + # didn't find it running, check for NEW queue status + submitted_compose_found_new: list = [ + compose + for compose in compose_queue["new"] + if (compose["blueprint"] == module.params["blueprint"]) and (compose["version"] == blueprint_version) + ] + + if submitted_compose_found_new: + submitted_compose_uuid: str = submitted_compose_found_new[0]["id"] + + else: + # it's not RUNNING and not NEW, so check for FAILURE state + compose_failed: dict = weldr.api.get_compose_failed() + # {"failed":[]} + submitted_compose_found_failed: list = [ + compose + for compose in compose_failed["failed"] + if (compose["blueprint"] == module.params["blueprint"]) and (compose["version"] == blueprint_version) + ] + if submitted_compose_found_failed: + submitted_compose_uuid: str = submitted_compose_found_failed[0]["id"] + else: + module.fail_json( + msg="Unable to determine state of build, check osbuild-composer system logs. Also, consider increasing the request timeout", + changed=changed + ) + + if submitted_compose_uuid: + result: dict = weldr.api.get_compose_status(submitted_compose_uuid) + result['body'] = { + 'build_id': submitted_compose_uuid + } + + if "status_code" in result.keys(): + if result["status_code"] >= 400: + module.fail_json( + msg="Compose returned body: {0}, msg {1}, and status_code {2}".format(result["body"], result["error_msg"], result["status_code"]), + changed=changed + ) + + # Having received a non-400+ response, we know a compose has started + changed: bool = True + + compose_output_types: dict[str, list[str]] = { + "tar": ["tar", "edge-commit", "iot-commit", "edge-container", "iot-container", "container"], + "iso": ["edge-installer", "edge-simplified-installer", "iot-installer", "image-installer"], + "qcow2": ["qcow2", "openstack", "oci"], + "vmdk": ["vmdk"], + "vhd": ["vhd"], + "raw.xz": ["edge-raw-image", "iot-raw-image"], + "ami": ["ami"], + } + + output_type: str = "" + for compose_type, compose_type_list in compose_output_types.items(): + if module.params["compose_type"] in compose_type_list: + output_type: str = compose_type + result["output_type"] = output_type + + module.exit_json(msg="Compose submitted to queue", result=result, changed=changed) + + else: + changed: bool = False + module.exit_json( + msg="Not queuing a duplicate versioned compose without allow_duplicate set to true", + changed=changed, + ) + + +def main() -> None: + module: AnsibleModule = AnsibleModule( + argument_spec=argument_spec, + required_together=[["image_name", "profile"]], + required_if=[ + ["compose_type", "edge-installer", ["ostree_url"]], + ["compose_type", "iot-installer", ["ostree_url"]], + ], + ) + weldr: Weldr = Weldr(module) + start_compose(module, weldr) + + +if __name__ == "__main__": + main() diff --git a/ansible/prerequisites.yaml b/ansible/prerequisites.yaml new file mode 100644 index 0000000..ce690b1 --- /dev/null +++ b/ansible/prerequisites.yaml @@ -0,0 +1,148 @@ +- name: Install prerequisites + hosts: all + become: true + tasks: + - community.general.rhsm_repository: + name: + - rhocp-4.14-for-rhel-9-{{ ansible_facts['userspace_architecture'] }}-rpms + - fast-datapath-for-rhel-9-{{ ansible_facts['userspace_architecture'] }}-rpms + state: enabled + + - name: Install EPEL release package + become: true + ansible.builtin.dnf: + name: https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm + state: present + disable_gpg_check: true + + - name: Install packages + ansible.builtin.dnf: + name: + - python3-toml + - createrepo + - git + - rpm-build + - rpmdevtools + - rpmrebuild + - mkpasswd + - podman + - buildah + - nginx + - lorax + - pykickstart + - osbuild-composer + - composer-cli + - cockpit-composer + - git + - firewalld + state: installed + + - name: Start services + ansible.builtin.systemd: + name: "{{ item }}" + enabled: yes + state: started + loop: + - osbuild-composer.socket + - firewalld.service + - cockpit.socket + - nginx.service + + - name: Adding ansible_user to the weldr group + ansible.builtin.user: + name: '{{ ansible_user | default(ansible_env.SUDO_USER) }}' + groups: weldr + append: yes + + - name: Allow HTTP and HTTPS + ansible.posix.firewalld: + service: '{{ item }}' + permanent: true + immediate: true + state: enabled + loop: + - http + - https + + - name: Ensure the ostree directory exists + become: true + ansible.builtin.file: + path: "{{ www_location }}" + state: directory + mode: '0755' + serole: object_r + setype: httpd_sys_content_t + seuser: system_u + + - name: Configure nginx + lineinfile: + path: /etc/nginx/nginx.conf + line: "root {{ www_location }};" + regexp: "^\\s*root\\s+.*;" + + - name: Restart nginx + ansible.builtin.systemd: + name: nginx.service + state: restarted + + - name: Ensure the repository directory exists + become: true + ansible.builtin.file: + path: "{{ repo_location }}" + state: directory + mode: '0755' + + - name: Update the repository with createrepo + become: true + ansible.builtin.command: + cmd: "createrepo {{ repo_location }}" + + - name: Add custom repository + ansible.builtin.yum_repository: + name: custom + file: custom + description: Custom RPMS + baseurl: file://{{ repo_location }} + enabled: true + gpgcheck: false + + - name: Add sources + infra.osbuild.repository: '{{ item }}' + loop: + - repo_name: custom packages for RHEL + type: yum-baseurl + base_url: file://{{ repo_location }} + check_gpg: false + check_ssl: false + rhsm: false + state: present + - repo_name: Red Hat OpenShift Container Platform 4.14 for RHEL 9 + type: yum-baseurl + base_url: https://cdn.redhat.com/content/dist/layered/rhel9/{{ ansible_facts['userspace_architecture'] }}/rhocp/4.14/os + check_gpg: true + check_ssl: true + rhsm: true + state: present + - repo_name: Fast Datapath for RHEL 9 + type: yum-baseurl + base_url: https://cdn.redhat.com/content/dist/layered/rhel9/{{ ansible_facts['userspace_architecture'] }}/fast-datapath/os + check_gpg: true + check_ssl: true + rhsm: true + state: present + - repo_name: Extra Packages for Enterprise Linux + type: yum-baseurl + base_url: http://mirror.in2p3.fr/pub/epel/9/Everything/{{ ansible_facts['userspace_architecture'] }}/ + check_gpg: false + check_ssl: false + rhsm: false + state: present + loop_control: + label: '{{ item.repo_name }}' + + - name: Install packages on the ansible controller + dnf: + name: + - python3-toml + state: installed + delegate_to: localhost diff --git a/ansible/requirements.yaml b/ansible/requirements.yaml new file mode 100644 index 0000000..4a8d5a8 --- /dev/null +++ b/ansible/requirements.yaml @@ -0,0 +1,4 @@ +collections: +- infra.osbuild +- community.general +- ansible.posix diff --git a/ansible/templates/kiosk.ks.j2 b/ansible/templates/kiosk.ks.j2 new file mode 100644 index 0000000..7ffd110 --- /dev/null +++ b/ansible/templates/kiosk.ks.j2 @@ -0,0 +1,68 @@ +## +## Environment setup +## + +# French I18n +lang fr_FR.UTF-8 + +# French keyboard layout +keyboard fr + +# Timezone is UTC to avoid issue with DST +timezone UTC --utc + +# Configure NTP +timesource --ntp-server=rhel.pool.ntp.org + +# Which action to perform after install: poweroff or reboot +reboot + +# Install mode: text (interactive installs) or cmdline (unattended installs) +text + +## +## Storage configuration for only one disk +## /dev/disk/by-path/pci-0000:00:12.0-ata-1 instead of sda when sda is taken by the usb stick +## +zerombr +clearpart --all --initlabel +reqpart --add-boot +part pv.01 --size=1024 --grow --ondisk={{ kickstart_root_disk }} +volgroup rhel pv.01 +logvol / --fstype="xfs" --size=10240 --name=root --vgname=rhel + +## +## Network configuration +## + +# Configure the first network device +network --bootproto=dhcp --device=enp1s0 --noipv6 --activate + +# Configure hostname +network --hostname=kiosk.localdomain + +## +## Ostree installation +## + +# Use this to fetch from a remote URL +ostreesetup --nogpg --osname=rhel --remote=edge --url=http://{{ ansible_default_ipv4.address }}/repo --ref=rhel/9/x86_64/edge-kiosk + +## +## Post install scripts +## +%post --log=/var/log/anaconda/post-install.log --erroronfail +# Add the pull secret to CRI-O and set root user-only read/write permissions +cat > /etc/crio/openshift-pull-secret << 'EOF' +{{ kickstart_microshift_pull_secret }} +EOF +chmod 600 /etc/crio/openshift-pull-secret + +# Configure the firewall with the mandatory rules for MicroShift +firewall-offline-cmd --zone=trusted --add-source=10.42.0.0/16 +firewall-offline-cmd --zone=trusted --add-source=169.254.169.1 + +# Do not ask password for sudo +sed -i.post-install -e "s/^%wheel\tALL=(ALL)\tALL/%wheel ALL=(ALL) NOPASSWD: ALL/" /etc/sudoers + +%end diff --git a/ansible/templates/kiosk.toml.j2 b/ansible/templates/kiosk.toml.j2 new file mode 100644 index 0000000..1992f8f --- /dev/null +++ b/ansible/templates/kiosk.toml.j2 @@ -0,0 +1,74 @@ +name = "kiosk" +description = "Example Kiosk" +version = "0.0.8" +modules = [] +groups = [] + +[[packages]] +name = "kiosk-config" +version = "*" + +[[packages]] +name = "cockpit" + +[[packages]] +name = "microshift-manifests" +version = "*" + +[[packages]] +name = "cockpit-system" + +[customizations] +hostname = "kiosk.local" + +[customizations.services] +enabled = ["cockpit.socket", "sshd", "microshift", "rpm-ostreed", "rpm-ostreed-automatic.timer"] + +[customizations.timezone] +timezone = "Europe/Paris" +ntpservers = ["0.fr.pool.ntp.org", "1.fr.pool.ntp.org"] + +[customizations.locale] +languages = ["fr_FR.UTF-8"] +keyboard = "fr" + +#22 ssh / 9090 cockpit / 6443 microshift +[customizations.firewall] +ports = ["22:tcp", "30000:tcp", "9090:tcp", "6443:tcp"] + +## +## Automatic updates +## +## This file is used by the rpm-ostreed service that is triggered by the +## rpm-ostreed-automatic systemd timer: +## +## [Timer] +## OnBootSec=1h # 1 hour after boot +## OnUnitInactiveSec=1d # 1 day after last check +## +## But you can trigger a check manually with: +## +## sudo rpm-ostree upgrade --trigger-automatic-update-policy +## +[[customizations.files]] +path = "/etc/rpm-ostreed.conf" +data = """[Daemon] +AutomaticUpdatePolicy=apply +""" + +[[customizations.user]] +name = "admin" +description = "admin" +password = '{{ blueprint_admin_password_hash }}' +key = "{{ blueprint_admin_ssh_public_key }}" +home = "/home/admin/" +shell = "/usr/bin/bash" +groups = ["users", "wheel"] + +[[customizations.user]] +name = "kiosk" +description = "kiosk" +password = '{{ blueprint_kiosk_password_hash }}' +home = "/home/kiosk/" +shell = "/bin/bash" + diff --git a/documentation/INSTALL_RHEL9.md b/documentation/INSTALL_RHEL9.md index c6b0043..156ff63 100644 --- a/documentation/INSTALL_RHEL9.md +++ b/documentation/INSTALL_RHEL9.md @@ -17,8 +17,9 @@ Microshift pre-requisites : ```sh sudo subscription-manager register --username $RHN_LOGIN --auto-attach sudo subscription-manager attach --pool=$RHN_POOL_ID -sudo dnf install -y osbuild-composer composer-cli cockpit-composer +sudo dnf install -y osbuild-composer composer-cli cockpit-composer git firewalld python3-toml sudo systemctl enable --now osbuild-composer.socket +sudo systemctl enable --now firewalld sudo systemctl enable --now cockpit.socket sudo systemctl restart osbuild-composer sudo usermod -a -G weldr "$(id -un)" @@ -45,7 +46,7 @@ baseos ## Clone this repository ```sh -git clone https://github.com/nmasse-itix/red-hat-kiosk.git +git clone https://github.com/ePietry/red-hat-kiosk.git cd red-hat-kiosk export GIT_REPO_CLONE="$PWD" ``` @@ -181,6 +182,7 @@ rpmbuild -ba $HOME/rpmbuild/SPECS/microshift-manifests.spec Rebuild the Google Chrome RPM ```sh +sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm mkdir $HOME/rpmbuild/VENDOR curl -s -Lo $HOME/rpmbuild/VENDOR/google-chrome-stable_current_x86_64.rpm https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm rpmrebuild -s $HOME/rpmbuild/SPECS/google-chrome-stable.spec -p $HOME/rpmbuild/VENDOR/google-chrome-stable_current_x86_64.rpm diff --git a/imagebuilder/kiosk.ks b/imagebuilder/kiosk.ks index 1b463ef..2126483 100644 --- a/imagebuilder/kiosk.ks +++ b/imagebuilder/kiosk.ks @@ -21,32 +21,15 @@ reboot text ## -## Storage configuration -## - -# Clear the target disk -zerombr - -# Remove existing partitions -clearpart --all --initlabel - -# Automatically create partitions required by hardware platform -# and add a separate /boot partition -reqpart --add-boot - - -## -## Alternative partitioning on only one disk +## Storage configuration for only one disk ## /dev/disk/by-path/pci-0000:00:12.0-ata-1 instead of sda when sda is taken by the usb stick ## zerombr clearpart --all --initlabel reqpart --add-boot -part pv.01 --size=10G --ondisk=/dev/disk/by-path/pci-0000:00:12.0-ata-1 -volgroup system pv.01 +part pv.01 --size=10240 --ondisk=/dev/disk/by-path/pci-0000:00:12.0-ata-1 +volgroup rhel pv.01 logvol / --fstype="xfs" --size=1 --grow --name=root --vgname=system -part pv.02 --size=1 --grow --ondisk=/dev/disk/by-path/pci-0000:00:12.0-ata-1 -volgroup data pv.02 ## ## Network configuration diff --git a/imagebuilder/kiosk.toml b/imagebuilder/kiosk.toml index 4239c93..7973870 100644 --- a/imagebuilder/kiosk.toml +++ b/imagebuilder/kiosk.toml @@ -22,7 +22,7 @@ name = "cockpit-system" hostname = "kiosk.local" [customizations.services] -enabled = ["cockpit.socket", "sshd", "microshift"] +enabled = ["cockpit.socket", "sshd", "microshift", "rpm-ostreed", "rpm-ostreed-automatic.timer"] [customizations.timezone] timezone = "Europe/Paris" @@ -36,6 +36,26 @@ keyboard = "fr" [customizations.firewall] ports = ["22:tcp", "30000:tcp", "9090:tcp", "6443:tcp"] +## +## Automatic updates +## +## This file is used by the rpm-ostreed service that is triggered by the +## rpm-ostreed-automatic systemd timer: +## +## [Timer] +## OnBootSec=1h # 1 hour after boot +## OnUnitInactiveSec=1d # 1 day after last check +## +## But you can trigger a check manually with: +## +## sudo rpm-ostree upgrade --trigger-automatic-update-policy +## +[[customizations.files]] +path = "/etc/rpm-ostreed.conf" +data = """[Daemon] +AutomaticUpdatePolicy=apply +""" + [[customizations.user]] name = "admin" description = "admin" @@ -44,3 +64,10 @@ key = "__ADMIN_SSH_PUBLIC_KEY__" home = "/home/admin/" shell = "/usr/bin/bash" groups = ["users", "wheel"] + +[[customizations.user]] +name = "kiosk" +description = "kiosk" +password = '__KIOSK_PASSWORD__' +home = "/home/kiosk/" +shell = "/bin/bash" diff --git a/rpms/SPECS/kiosk-config.spec b/rpms/SPECS/kiosk-config.spec index 1682805..c565a84 100644 --- a/rpms/SPECS/kiosk-config.spec +++ b/rpms/SPECS/kiosk-config.spec @@ -72,8 +72,13 @@ install -m 0755 -D kiosk-app %{buildroot}/usr/bin/kiosk-app %attr(0755, root, root) /usr/bin/kiosk-app %pre -getent group kiosk >/dev/null 2>&1 || groupadd -r kiosk -getent passwd kiosk >/dev/null 2>&1 || useradd -r -N -g kiosk -d /home/kiosk -m kiosk +## +## HEADS UP !!! +## +## The kiosk user needs to be created in the kickstart now. +## +#getent group kiosk >/dev/null 2>&1 || groupadd -r kiosk +#getent passwd kiosk >/dev/null 2>&1 || useradd -r -N -g kiosk -d /home/kiosk -m kiosk %post %systemd_user_post com.redhat.Kiosk.SampleApp.service