Skip to content

Dynamic Scheduler in Ansible Automation Platform

Avatar photo

https://www.linkedin.com/in/alpha-wolf-jin/

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.

Disclaimer:

The views expressed and the content shared in all published articles on this website are solely those of the respective authors, and they do not necessarily reflect the views of the author’s employer or the techbeatly platform. We strive to ensure the accuracy and validity of the content published on our website. However, we cannot guarantee the absolute correctness or completeness of the information provided. It is the responsibility of the readers and users of this website to verify the accuracy and appropriateness of any information or opinions expressed within the articles. If you come across any content that you believe to be incorrect or invalid, please contact us immediately so that we can address the issue promptly.

Avatar photo


https://www.linkedin.com/in/alpha-wolf-jin/
I’m Jin, Red Hat ASEAN Senior Platform Consultant. My primary focus is Ansible Automation (Infrastructure as Code), OpenShift, and OpenStack.

Comments

2 Responses

  1. VIjesh says:

    Good Article! Is there a way to get the current schedule name or id inside the playbook?

  2. Jin Zhang says:

    The schedule name is inside the playbook. You can use the “debug” module to explicitly show it.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.