This blog starts with one patching automation workshop. The customer’s procedure is that the end user raises a patching request. The request includes the server list, patch list, and specified time for this patching task. The customer not only wants to automate the patching task itself but also wants the playbook to auto-schedule the patching task at a given time within the request.
But Ansible Automation Platform (AAP) is suitable to schedule the patching task in the fix time window, like the cron job in Linux. The scheduler tool is more suitable for this scenario. This blog is talking about how to use AAP to handle “scheduler” tasks.
In AAP web console, schedules can be created from a template, project or inventory source but not directly on the main Schedules screen itself. It looks like this.
This is good for static schedules that require manual configuration but not for dynamic schedules.
Recently, I came across a patch automation use case that caught my attention. The client wants scheduling to be automated. They want a playbook to execute the patch at a time specified inside their change request. The original schedule function from AAP cannot meet this requirement.
In order to make scheduling dynamic, I created 2 templates.
- Patching Template: to install the patch. Once patching is over, the schedule will be deleted.
- Schedule Template: to schedule when to execute the patch.
I use the REST API to add/delete schedules.
Below is my POC on dynamic scheduling
The playbook patch.yaml
Task: “- name: Report File” & “- name: Schedule Name”
This is to verify the content of the variables ‘report_file’ and ‘delete_schedule’, which should come from the schedule template.
Task: “- name: retrieve the AAP Token”
This is is to get AAP token for following REST API authentication.
Task: “- name: Collect all schedules’ info”
This is to collect all schedules’ information.
Task: “- name: Extract schedule ID”
This is to figure out the schedule ID based on the given schedule name.
Task: “- name: Delete used schedule”
This is to delete the schedule based on the schedule ID.
# cat patch.yaml
- name: Patch
hosts: all
vars:
app_ip: 192.168.122.31
user_id: 1
job_template_id: 9
tasks:
- name: Report File
debug:
msg: "{{ report_file | default('Hello World!') }}"
- name: Schedule Name
debug:
msg: "{{ delete_schedule | default('Hello World!') }}"
- name: retrieve the AAP Token
uri:
url: https://{{ app_ip }}/api/v2/users/{{ user_id }}/personal_tokens/
user: admin
password: redhat
method: POST
force_basic_auth: yes
headers:
Content-Type: application/json
return_content: yes
validate_certs: no
status_code: [200, 201]
body_format: json
body:
extra_vars:
description: "Tower CLI"
application: null
scope: write
register: result
- set_fact:
token: "{{ result['json']['token'] }}"
- name: Collect all schedules' info
uri:
url: https://{{ app_ip }}/api/v2/schedules/
method: GET
headers:
Authorization: "Bearer {{ token }}"
return_content: yes
validate_certs: no
status_code: [200, 201]
body_format: json
register: result
- name: Extract schedule ID
register: result_01
args:
stdin: |
for schedule in {{ result.json.results }}:
if schedule['name'] == "{{ delete_schedule }}":
print(schedule['id'])
command: /usr/bin/python3
- name: Delete used schedule
uri:
url: https://{{ app_ip }}/api/v2/schedules/{{ result_01.stdout }}/
method: DELETE
headers:
Authorization: "Bearer {{ token }}"
return_content: yes
validate_certs: no
status_code: [200, 201, 204]
body_format: json
body:
extra_vars:
id: "{{ result_01.stdout }}"
register: result
Job Template patch
Job Template patch
– Parameters
In order to accept the parameters from adding a schedule REST API call, the parameters need to be defined in the Survey.
Parameter – report_file
Parameter – delete_schedule
The playbook scheduler.yaml
Task: “- name: python code converts SGT to UTC”
This is to use Python code to convert SGT time to UTC time.
Task: “- name: retrieve the Ansible Tower Token”
This is to get AAP token for following REST API authentication.
Task: “- name: Create a unique schedule name
This is to generate a unique schedule name. The playbook “scheduler.yaml” passes this parameter to the playbook “patch.yaml”. The playbook “patch.yaml” will delete the schedule based on this unique name once patching is finished.
Task: “- name: Add the schdule to the job template patch.yaml”
This is to add a schedule to the job template patch.yaml, and pass the below parameters to the job template patch.yaml.
- report_file – contains the hostname and patch which should be applied on this server
- delete_schedule – used to delete schedule once patching finishes
# cat scheduler.yaml
---
- hosts: localhost
connection: local
become: true
gather_facts: false
vars:
app_ip: 192.168.122.31
user_id: 1
job_template_id: 9
tasks:
- debug:
msg: "Singapore Time: {{ year }}-{{ month }}-{{ date }}T{{ hour }}:{{ min }}:{{ sec }}"
# it is easy to do time conversion in the python
# Below is to directly use python code for time conversion
# It requires tzdata packge and environment TZ is SGT
- name: python code converts SGT to UTC
register: results
args:
stdin: |
from datetime import datetime
import pytz
dt_str = "{{ year }}{{ month }}{{ date }}T{{ hour }}{{ min }}{{ sec }}"
format = "%Y%m%dT%H%M%S"
local_dt = datetime.strptime(dt_str, format)
dt_utc = local_dt.astimezone(pytz.UTC)
format = "%Y-%m-%dT%H:%M:%SZ"
dt_utc_str = dt_utc.strftime(format)
print(dt_utc_str)
command: /usr/bin/python3
- debug:
msg: "UTC Time: {{ results.stdout }}"
- set_fact:
utc_date: "{{ results.stdout }}"
- name: retrieve the Ansible Tower Token
uri:
url: https://{{ app_ip }}/api/v2/users/{{ user_id }}/personal_tokens/
user: admin
password: redhat
method: POST
force_basic_auth: yes
headers:
Content-Type: application/json
return_content: yes
validate_certs: no
status_code: [200, 201]
body_format: json
body:
extra_vars:
description: "Tower CLI"
application: null
scope: write
register: result
# the schedule name has to be unique
- name: Create unique schedule name
set_fact:
token: "{{ result['json']['token'] }}"
sgt_date: "{{ year }}{{ month }}{{ date }}T{{ hour }}{{ min }}{{ sec }}"
schedule_name: "schedule-{{ 99999999 | random | to_uuid }}"
# the schdule can have it's own extra_data pasing the extr varaibles' value
# to the job tempalte patch
# The REST API needs boht SGT and UTC times :-)
- name: Add the schdule to the job template patch.yaml
uri:
url: https://{{ app_ip }}/api/v2/job_templates/{{ job_template_id }}/schedules/
method: POST
headers:
Authorization: "Bearer {{ token }}"
return_content: yes
validate_certs: no
status_code: [200, 201]
body_format: json
body:
rrule: "DTSTART;TZID=Asia/Singapore:{{ sgt_date }} RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY"
name: "{{ schedule_name }}"
description: ""
extra_data:
report_file: "{{ report_fie_location }}"
delete_schedule: "{{ schedule_name }}"
inventory: null
scm_branch: ""
job_type: null
job_tags: ""
skip_tags: ""
limit: ""
diff_mode: null
verbosity: null
enabled: true
unified_job_template: 9
dtstart: "{{ utc_date }}"
dtend: "{{ utc_date }}"
next_run: null
timezone: "Asia/Singapore"
until: ""
register: result
“python code converts SGT to UTC” task directly calls the python code from the date conversion. Please refer to https://www.techbeatly.com/python-inside-ansible-playbook/
This works well when I run it on the local VM. But it does not work as expected we I put it into the container. And I realize that the customization has to be done on the container images I used.
Create a customized execute image
I did the below customization for the container image
- missing rpm
tzdata
in the image. Install the rpm,tzdata
. - the timezone in the image is UTC instead of SGT as the VM. Set the timezone to SGT in the image
Create the rpm dependency file
# cat bindep.txt
tzdata [platform:rpm]
Set TimeZone from the environment file
# cat execution-environment.yml
version: 1
ansible_config: 'ansible.cfg'
dependencies:
system: bindep.txt
additional_build_steps:
prepend: |
RUN cp /usr/share/zoneinfo/Asia/Singapore /etc/localtime && echo "Asia/Singapore" >/etc/timezone
# cat bindep.txt
tzdata [platform:rpm]
Clean up the ansible.cfg
# > ./ansible.cfg
Build the new image
# podman login -u=admin -p=redhat aap-hub-01.example.com --tls-verify=false
Login Succeeded!
# pip3 install ansible-builder
# ansible-builder build -t utc_ee_image_sgt
Running command:
podman build -f context/Containerfile -t utc_ee_image_sgt context
# podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/utc_ee_image_sgt latest f5bb2d1bd3ab 5 minutes ago 994 MB
# podman tag f5bb2d1bd3ab aap-hub-01.example.com/utc_ee_image_sgt
# podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/utc_ee_image latest f5bb2d1bd3ab 11 minutes ago 994 MB
aap-hub-01.example.com/utc_ee_image latest f5bb2d1bd3ab 11 minutes ago 994 MB
# podman push aap-hub-01.example.com/utc_ee_image_sgt:latest --tls-verify=false
Verify TZ and Python TZ convert code in the new image
# podman run -it --name test-03 --entrypoint /bin/bash localhost/utc_ee_image_sgt:latest
bash-4.4# date
Sat Aug 6 20:51:14 +08 2022
bash-4.4# python3
Python 3.8.13 (default, Jun 24 2022, 15:27:57)
[GCC 8.5.0 20210514 (Red Hat 8.5.0-13)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import datetime
>>> import pytz
>>> dt_str = "20220806T145000"
>>> format = "%Y%m%dT%H%M%S"
>>> local_dt = datetime.strptime(dt_str, format)
>>> print(local_dt)
2022-08-06 14:50:00
>>> dt_utc = local_dt.astimezone(pytz.UTC)
>>> dt_utc = local_dt.astimezone(pytz.UTC)
>>> format = "%Y-%m-%dT%H:%M:%SZ"
>>> dt_utc_str = dt_utc.strftime(format)
>>> print(dt_utc_str)
2022-08-06T06:50:00Z
>>> exit()
Configure the new execution image in AAP
Job Template scheduler
Job Template schedule
– Survey
Parameter – report_fie_location
The value of “report_fie_location” will be passed to the playbook “patch.yaml”. The other parameters will be consumed by the playbook “schedule.yaml”.
Verification of the schedule function
The value of the input parameters
- report file : medc-report-07-Aug-2022-14-15
- Year : 2022
- Month : 08
- Date : 7
- hour : 14
- Minute : 15
- Second : 00
The output of the job template scheduler
New schedule added as required
The job started at the scheduled time
The parameter values are right in patch
, which received from scheduler
The schedule is deleted
Schedule Name: schedule-0d70dc58-d9d7-5adc-98d8-c7d7a4e006ff
The entire schedule (adding schedules, executing scheduled tasks and deleting schedule) cycle completes successfully. The input value can pass successfully from the beginning into the embeded “patch.yaml” playbook. The above POC codes can be used for real schedule tasks.