F5Networks / f5-ansible-bigip

Declarative Ansible collection for managing F5 BIG-IP/BIG-IQ.
37 stars 17 forks source link

bigip_as3_deploy: Assumes declaration uses class AS3 and fails when class ADC is used #56

Closed simonkowallik closed 1 year ago

simonkowallik commented 1 year ago
COMPONENT NAME

f5networks.f5_bigip.bigip_as3_deploy version 1.12.0

Environment

ANSIBLE VERSION
2.14.1
BIGIP VERSION
16.1.3.3
CONFIGURATION
[defaults]
collections_paths=./collections
filter_plugins=./filter_plugins
module_utils=./module_utils
roles_path=./roles
inventory=./inventory
OS / ENVIRONMENT

N/A

SUMMARY

bigip_as3_deploy assumes that a declaration starts with the AS3 class, but AS3 does allow declarations to omit the AS3 class and directly start with the ADC class.

There are many example declarations in the Postman collection on the release page of the f5-appsvcs-extension omitting the AS3 class and directly using the ADC class.

STEPS TO REPRODUCE

Below is a playbook with a full repro of the issue. Obviously the &bigip_provider needs to be modified to match the test bed.

---
- name: "Repro"
  hosts: all
  gather_facts: false
  connection: httpapi
  vars:
    provider: &bigip_provider
      server: "10.1.1.5"
      user: "admin"
      password: "Secret_245"
      validate_certs: "no"
      server_port: 443
    ansible_host: "{{ provider.server }}"
    ansible_user: "{{ provider.user }}"
    ansible_httpapi_password: "{{ provider.password }}"
    ansible_httpapi_port: "{{ provider.server_port }}"
    ansible_network_os: "f5networks.f5_bigip.bigip"
    ansible_httpapi_use_ssl: "yes"
    ansible_httpapi_validate_certs: "{{ provider.validate_certs }}"

  tasks:
    - name: "F5 Collection Versions"
      ansible.builtin.debug:
        msg: >-
          Ansible version:{{ ansible_version.full }},
          f5networks.f5_bigip:{{ lookup('community.general.collection_version', 'f5networks.f5_bigip') }},
          f5networks.f5_modules:{{ lookup('community.general.collection_version', 'f5networks.f5_modules') }}

    - name: "(Test #1) Create Declaration"
      ansible.builtin.set_fact:
      # Copied from example Postman collection: https://github.com/F5Networks/f5-appsvcs-extension/releases/tag/v.3.42.0
      # Name: example-service-generic
      # This declaration works perfectly fine when POSTed to https://{{host}}/mgmt/shared/appsvcs/declare (eg. via Postman)
        declaration: |-
          {
              "class": "ADC",
              "schemaVersion": "3.5.0",
              "id": "Service_Generic",
              "Sample_misc_02": {
                  "class": "Tenant",
                  "Application": {
                      "class": "Application",
                      "generic_virtual": {
                          "class": "Service_Generic",
                          "virtualAddresses": [
                              "192.0.2.140"
                          ],
                          "virtualPort": 8080
                      }
                  }
              }
          }

    - name: "(Test #1) AS3 send declaration with ADC"
      f5networks.f5_bigip.bigip_as3_deploy:
        content: "{{ declaration }}"
        state: present
      ignore_errors: yes # continue with next task to demonstrate repro

    - name: "(Test #2) Wrap declaration in AS3 class"
      ansible.builtin.set_fact:
        # Copied from example Postman collection: https://github.com/F5Networks/f5-appsvcs-extension/releases/tag/v.3.42.0
        # Name: example-service-generic
        declaration_wrapped_in_AS3_class: >-
          {
              "class": "AS3",
              "persist": false,
              "declaration": {
                  "class": "ADC",
                  "schemaVersion": "3.5.0",
                  "id": "Service_Generic",
                  "Sample_misc_02": {
                      "class": "Tenant",
                      "Application": {
                          "class": "Application",
                          "generic_virtual": {
                              "class": "Service_Generic",
                              "virtualAddresses": [
                                  "192.0.2.140"
                              ],
                              "virtualPort": 8080
                          }
                      }
                  }
              }
          }

    - name: "(Test #1) AS3 send declaration wrapped in AS3 class"
      f5networks.f5_bigip.bigip_as3_deploy:
        content: "{{ declaration_wrapped_in_AS3_class }}"
        state: present
EXPECTED RESULTS

The expectation is that bigip_as3_deploy implements the provided AS3 declaration and not require explicitly wrapping it in the AS3 class. The AS3 API perfectly supports this use-case.

ACTUAL RESULTS
$ ansible-playbook -i inventory/dev play-f5as3-issueX.yml -l b16

PLAY [Repro] ****************************************************************************************************************************************************************************************************************************************************************************************************

TASK [F5 Collection Versions] ***********************************************************************************************************************************************************************************************************************************************************************************
ok: [b16] => {
    "msg": "Ansible version:2.14.1, f5networks.f5_bigip:1.12.0, f5networks.f5_modules:1.22.0"
}

TASK [(Test #1) Create Declaration] *****************************************************************************************************************************************************************************************************************************************************************************
ok: [b16]

TASK [(Test #1) AS3 send declaration with ADC] ******************************************************************************************************************************************************************************************************************************************************************
fatal: [b16]: FAILED! => {"changed": false, "msg": "{'code': 422, 'errors': ['/action: should be object'], 'declarationFullId': '', 'message': 'declaration is invalid'}"}
...ignoring

TASK [(Test #2) Wrap declaration in AS3 class] ******************************************************************************************************************************************************************************************************************************************************************
ok: [b16]

TASK [(Test #1) AS3 send declaration wrapped in AS3 class] ******************************************************************************************************************************************************************************************************************************************************
ok: [b16]

PLAY RECAP ******************************************************************************************************************************************************************************************************************************************************************************************************
b16                        : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1   
ROOT CAUSE

The root cause lies in the exists() method, specifically in the below line:

https://github.com/F5Networks/f5-ansible-bigip/blob/4c65ba594dac5c1018550a8ab923eb74753a2453/ansible_collections/f5networks/f5_bigip/plugins/modules/bigip_as3_deploy.py#L251

The above code inserts (or overwrites) the action property without checking if the AS3 class is actually used. This inserts the action property into the ADC class which results in a validation error.

This can be easily fixed by checking for class AS3 and wrapping the actual declaration in an AS3 class if it isn't used.

        if declaration.get('class') == 'AS3': # declaration uses AS3 class
            declaration['action'] = 'dry-run'
        else: # declaration needs to be wrapped in AS3 class to perform a dry-run
            declaration = {
                'class': 'AS3',
                'persist': False,
                'action': 'dry-run',
                'declaration': declaration,
            }
simonkowallik commented 1 year ago

The exact ansible environment can be build using the below Dockerfile.

FROM python:3.10-bullseye

RUN useradd ansible -d /ansible -M -u 1000

RUN apt update; apt upgrade -y;
RUN apt install -y \
            jq \
            less \
            vim \
            iproute2 \
            rpm \
            ;

RUN pip3 install \
            'ansible-core>=2.14.1' \
            ansible \
            ansible-lint \
            yamllint \
            ;

# requirements.txt of f5_modules and f5_bigip
RUN pip3 install \
            cryptography \
            objectpath \
            ordereddict \
            simplejson \
            paramiko \
            jinja2 \
            netaddr \
            ;

RUN \
echo '    UserKnownHostsFile /dev/null' >> /etc/ssh/ssh_config; \
echo '    StrictHostKeyChecking no' >> /etc/ssh/ssh_config;

USER ansible

WORKDIR /ansible
docker build -t ansible-runner .
wojtek0806 commented 1 year ago

fixed in 1.13.0