In this exercise, we’re going to build a jinja2 template that will build us a kickstart file to be used by our edge devices when we provision them. This file allows us to get the operating system down without needing to manually enter values or click through the Anaconda installer.
We’re writing this as a jinja2 template so that:
Note
Ensure you’re building this template inside your project repository using whatever means you’ve decided to do that. Convention for an Ansible repository is that you place the template adjacent to your playbook in the
templates
directory, if you’re not using a role - so consider creating that directory and placing it there.
The most basic kickstart for Device Edge contains the following:
keyboard --xlayouts='us'
lang en_US.UTF-8
timezone UTC
zerombr
clearpart --all --initlabel
autopart --type=plain --fstype=xfs --nohome
reboot
text
user --name your-user-here --groups=wheel --password=your-password-here
services --enabled=ostree-remount
ostreesetup --nogpg --url=http://1.2.3.4:80/repo --osname=rhel --ref=rhel/8/x86_64/edge
Most of these are the defaults for kickstarting standard RHEL, and they apply here to Red Hat Device Edge as well.
The line that handles the setup of Device Edge is the last one:
ostreesetup --nogpg --url=http://1.2.3.4:80/repo --osname=rhel --ref=rhel/8/x86_64/edge
Let’s take a look at these options:
ostreesetup
- setup an ostree-based operating system over a traditional RPM-based OS.--nogpg
- for this lab, we’re going to ignore the gpg keys on packages.--url
- the location to obtain the edge-commit, typically a web server. This can include port, http/https, and the path on the server (/repo
for example).--osname
- the name of the operating system that will be deployed.--ref
- the ref of the OS being deployed. This can be thought of like a git repo that contains commits.The above is just an example. We’ll need to modify this section slightly for our purposes. First, create a new directory in your source control repo: playbooks/templates
. Within the newly created directory, create a file named student(your-student-number).ks.j2
, for example: student10.ks.j2
. Open the file with your editor of choice and add some ansible variables:
keyboard --xlayouts='us'
lang en_US.UTF-8
timezone UTC
zerombr
clearpart --all --initlabel
autopart --type=plain --fstype=xfs --nohome
reboot
text
user --name={{ kickstart_user_username }} --groups=wheel --password={{ kickstart_user_password }}
rootpw --plaintext --lock {{ kickstart_user_password }}
services --enabled=ostree-remount
ostreesetup --nogpg --url={{ ostree_repo_protocol }}://{{ ostree_repo_host }}:{{ ostree_repo_port }}/{{ ostree_repo_path }} --osname={{ ostree_os_name }} --ref={{ ostree_ref }}
Note
We are setting a root password due to a bug in RHEL, however we are locking the acocunt.
Here we’ve converted a few lines to be more dynamic so this template can be re-usable. Some of these changes are to enable the ability to store as variables in Controller, while others, we’ll create a custom credential type and credential so that the values can be stored securely.
Since we’re provisioning these systems over the network, it may be necessary to include networking information in the kickstart to ensure the device is on the network prior to trying to pulling the OS.
If wired networking and DHCP is available, then most likely things will just work “out of the box” by specifying network --bootproto=dhcp --onboot=true
in the kickstart file. If you’re using virtualized devices in AWS, this will most likely be your experience as the workshop’s VPC will have DHCP available, and the virtual machines will be presented with a “wired” connection.
Wifi connections are not supported in the network
line of a kickstart, so we’ll establish the connection using nmcli
in the %pre
section of the kickstart. Additionally, we’ll conditionalize this via jinja if/endif
statements, so this section is only present if wireless credentials have been provided. Add the following to the beginning of the kickstart file.
{% if wifi_network is defined and wifi_password is defined %}
%pre
nmcli dev wifi connect "{{ wifi_network }}" password "{{ wifi_password }}"
%end
{% endif %}
NetworkManager should automatically select the correct device for us to connect to a wireless network.
We’ll also add the following for wired connections immediately below the previously added content:
{% if wifi_network is not defined %}
network --bootproto=dhcp --onboot=true
{% endif %}
Take note that this is not part of %pre
declaration, but it goes in the same section as the rest of of the kickstart options. Check the solutions section for more info.
Our kickstart file will install an operating system, but it does not yet include everything we would like it to contain. After devices boot up the first time, we want them to attempt to call home and register themselves with Ansible Controller so that they can be automated.
To accomplish this, we’ll use the %post
section of our kickstart, along with some additional jinja templating that will lay down an Ansible playbook performing the following:
We’ll also be leveraging the raw
capibilities of jinja as enclosed by raw
and endraw
so Ansible doesn’t attempt to template out vars in the playbook tasks
, but we do want the variables in vars
and module_defaults
instantiated.
Add the following to the bottom of the kickstart file:
%post
# create playbook for controller registration
cat > /var/tmp/aap-auto-registration.yml <<EOF
---
- name: register a r4e system to ansible controller
hosts:
- localhost
vars:
ansible_connection: local
controller_url: https://{{ controller_host }}/api/v2
controller_inventory: Edge Systems
provisioning_template: Provision Edge Device
module_defaults:
ansible.builtin.uri:
user: "{{ controller_api_username }}"
password: "{{ controller_api_password }}"
force_basic_auth: yes
validate_certs: no
{% raw %}
tasks:
- name: find the id of {{ controller_inventory }}
ansible.builtin.uri:
url: "{{ controller_url }}/inventories?name={{ controller_inventory | regex_replace(' ', '%20') }}"
register: controller_inventory_lookup
- name: set inventory id fact
ansible.builtin.set_fact:
controller_inventory_id: "{{ controller_inventory_lookup.json.results[0].id }}"
- name: create host in inventory {{ controller_inventory }}
ansible.builtin.uri:
url: "{{ controller_url }}/inventories/{{ controller_inventory_id }}/hosts/"
method: POST
body_format: json
body:
name: "edge-{{ ansible_default_ipv4.macaddress | replace(':','') }}"
variables:
'ansible_host: {{ ansible_default_ipv4.address }}'
register: create_host
changed_when:
- create_host.status | int == 201
failed_when:
- create_host.status | int != 201
- "'already exists' not in create_host.content"
- name: find the id of {{ provisioning_template }}
ansible.builtin.uri:
url: "{{ controller_url }}/workflow_job_templates?name={{ provisioning_template | regex_replace(' ', '%20') }}"
register: job_template_lookup
- name: set the id of {{ provisioning_template }}
ansible.builtin.set_fact:
job_template_id: "{{ job_template_lookup.json.results[0].id }}"
- name: trigger {{ provisioning_template }}
ansible.builtin.uri:
url: "{{ controller_url }}/workflow_job_templates/{{ job_template_id }}/launch/"
method: POST
status_code:
- 201
body_format: json
body:
limit: "edge-{{ ansible_default_ipv4.macaddress | replace(':','') }}"
{% endraw %}
EOF
Finally, we’ll use systemd to run the playbook once the system boots up the first time. There’s a few conditions to running this playbook that we can specify in our systemd-service file:
These conditions can be handled using options in the Unit
and Service
sections of the systemd service file:
# create systemd runonce file to trigger playbook
cat > /etc/systemd/system/aap-auto-registration.service <<EOF
[Unit]
Description=Ansible Automation Platform Auto-Registration
After=local-fs.target
After=network.target
ConditionPathExists=!/var/tmp/post-installed
[Service]
ExecStartPre=/usr/bin/sleep 20
ExecStart=/usr/bin/ansible-playbook /var/tmp/aap-auto-registration.yml
ExecStartPost=/usr/bin/touch /var/tmp/post-installed
User=root
RemainAfterExit=true
Type=oneshot
[Install]
WantedBy=multi-user.target
EOF
# Enable the service
systemctl enable aap-auto-registration.service
%end
The finished kickstart should look be similar to the following:
{% if wifi_network is defined and wifi_password is defined %}
%pre
nmcli dev wifi connect "{{ wifi_network }}" password "{{ wifi_password }}"
%end
{% endif %}
{% if wifi_network is not defined %}
network --bootproto=dhcp --onboot=true
{% endif %}
keyboard --xlayouts='us'
lang en_US.UTF-8
timezone UTC
zerombr
clearpart --all --initlabel
autopart --type=plain --fstype=xfs --nohome
reboot
text
user --name {{ kickstart_user_username }} --groups=wheel --password={{ kickstart_user_password }}
rootpw --plaintext --lock {{ kickstart_user_password }}
services --enabled=ostree-remount
ostreesetup --nogpg --url={{ ostree_repo_protocol }}://{{ ostree_repo_host }}:{{ ostree_repo_port }}/{{ ostree_repo_path }} --osname={{ ostree_os_name }} --ref={{ ostree_ref }}
%post
# create playbook for controller registration
cat > /var/tmp/aap-auto-registration.yml <<EOF
---
- name: register a r4e system to ansible controller
hosts:
- localhost
vars:
ansible_connection: local
controller_url: https://{{ controller_host }}/api/v2
controller_inventory: Edge Systems
provisioning_template: Provision Edge Device
module_defaults:
ansible.builtin.uri:
user: "{{ controller_api_username }}"
password: "{{ controller_api_password }}"
force_basic_auth: yes
validate_certs: no
{% raw %}
tasks:
- name: find the id of {{ controller_inventory }}
ansible.builtin.uri:
url: "{{ controller_url }}/inventories?name={{ controller_inventory | regex_replace(' ', '%20') }}"
register: controller_inventory_lookup
- name: set inventory id fact
ansible.builtin.set_fact:
controller_inventory_id: "{{ controller_inventory_lookup.json.results[0].id }}"
- name: create host in inventory {{ controller_inventory }}
ansible.builtin.uri:
url: "{{ controller_url }}/inventories/{{ controller_inventory_id }}/hosts/"
method: POST
body_format: json
body:
name: "edge-{{ ansible_default_ipv4.macaddress | replace(':','') }}"
variables:
'ansible_host: {{ ansible_default_ipv4.address }}'
register: create_host
changed_when:
- create_host.status | int == 201
failed_when:
- create_host.status | int != 201
- "'already exists' not in create_host.content"
- name: find the id of {{ provisioning_template }}
ansible.builtin.uri:
url: "{{ controller_url }}/workflow_job_templates?name={{ provisioning_template | regex_replace(' ', '%20') }}"
register: job_template_lookup
- name: set the id of {{ provisioning_template }}
ansible.builtin.set_fact:
job_template_id: "{{ job_template_lookup.json.results[0].id }}"
- name: trigger {{ provisioning_template }}
ansible.builtin.uri:
url: "{{ controller_url }}/workflow_job_templates/{{ job_template_id }}/launch/"
method: POST
status_code:
- 201
body_format: json
body:
limit: "edge-{{ ansible_default_ipv4.macaddress | replace(':','') }}"
{% endraw %}
EOF
# create systemd runonce file to trigger playbook
cat > /etc/systemd/system/aap-auto-registration.service <<EOF
[Unit]
Description=Ansible Automation Platform Auto-Registration
After=local-fs.target
After=network.target
ConditionPathExists=!/var/tmp/post-installed
[Service]
ExecStartPre=/usr/bin/sleep 20
ExecStart=/usr/bin/ansible-playbook /var/tmp/aap-auto-registration.yml
ExecStartPost=/usr/bin/touch /var/tmp/post-installed
User=root
RemainAfterExit=true
Type=oneshot
[Install]
WantedBy=multi-user.target
EOF
# Enable the service
systemctl enable aap-auto-registration.service
%end
Once you’ve assembled your kickstart template, be sure to commit and push it into your git repo.
Navigation
Previous Exercise | Next Exercise |