ansible-collections / community.vmware

Ansible Collection for VMware
GNU General Public License v3.0
348 stars 337 forks source link

mac_learning causes dvsport_group to report changes #1873

Open TheMysteriousX opened 1 year ago

TheMysteriousX commented 1 year ago
SUMMARY

With the following values set in community.vmware.vmware_dvs_portgroup:

mac_learning:
  allow_unicast_flooding: true
  enabled: true
  limit: 4096
  limit_policy: allow

The task always appears to report a change has happened. Removing that attribute from the task makes it report 'OK'.

This is similar to the elastic/static issue, however I already had these parameters set.

ISSUE TYPE
COMPONENT NAME

vmware_dvs_portgroup

ANSIBLE VERSION
ansible [core 2.15.5]
  config file = /Users/user/Developer/ansible/ansible.cfg
  configured module search path = ['/Users/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules', '/Users/user/Developer/ansible/plugins/modules', '/Users/user/.ansible/collections/*/modules']
  ansible python module location = /usr/local/lib/python3.11/site-packages/ansible
  ansible collection location = /Users/user/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.11.5 (main, Aug 24 2023, 15:18:16) [Clang 14.0.3 (clang-1403.0.22.14.1)] (/usr/local/opt/python@3.11/bin/python3.11)
  jinja version = 3.1.2
  libyaml = True
COLLECTION VERSION
# /usr/local/lib/python3.11/site-packages/ansible_collections
Collection       Version
---------------- -------
community.vmware 3.10.0
CONFIGURATION
ANSIBLE_NOCOWS(/Users/user/Developer/ansible/ansible.cfg) = True
CONFIG_FILE() = /Users/user/Developer/ansible/ansible.cfg
DEFAULT_ACTION_PLUGIN_PATH(/Users/user/Developer/ansible/ansible.cfg) = ['/Users/user/.ansible/plugins/action', '/usr/share/ansible/plugins/action', '/Users/user/Developer/ansible/plugins/action']
DEFAULT_FILTER_PLUGIN_PATH(/Users/user/Developer/ansible/ansible.cfg) = ['/Users/user/.ansible/plugins/filter', '/usr/share/ansible/plugins/filter', '/Users/user/Developer/ansible/plugins/filter']
DEFAULT_FORKS(/Users/user/Developer/ansible/ansible.cfg) = 9
DEFAULT_HOST_LIST(/Users/user/Developer/ansible/ansible.cfg) = ['/Users/user/Developer/ansible/inventory']
DEFAULT_JINJA2_NATIVE(/Users/user/Developer/ansible/ansible.cfg) = True
DEFAULT_LOAD_CALLBACK_PLUGINS(/Users/user/Developer/ansible/ansible.cfg) = True
DEFAULT_LOG_PATH(/Users/user/Developer/ansible/ansible.cfg) = /Users/user/.ansible/ansible.log
DEFAULT_LOOKUP_PLUGIN_PATH(/Users/user/Developer/ansible/ansible.cfg) = ['/Users/user/.ansible/plugins/lookup', '/usr/share/ansible/plugins/lookup', '/Users/user/Developer/ansible/plugins/lookup']
DEFAULT_MANAGED_STR(/Users/user/Developer/ansible/ansible.cfg) = Ansible managed: {{{{ lookup('git_info', template_fullpath | quote) }}}}
DEFAULT_MODULE_PATH(/Users/user/Developer/ansible/ansible.cfg) = ['/Users/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules', '/Users/user/Developer/ansible/plugins/modules', '/Users/user/.ansible/col>
DEFAULT_MODULE_UTILS_PATH(/Users/user/Developer/ansible/ansible.cfg) = ['/Users/user/.ansible/plugins/module_utils', '/usr/share/ansible/plugins/module_utils', '/Users/user/.ansible/collections/*/module_utils']
DEFAULT_ROLES_PATH(/Users/user/Developer/ansible/ansible.cfg) = ['/Users/user/.ansible/roles', '/usr/share/ansible/roles', '/etc/ansible/roles', '/Users/user/Developer/ansible/roles', '/Users/user/.ansible/collections/>
DEFAULT_STDOUT_CALLBACK(/Users/user/Developer/ansible/ansible.cfg) = yaml
DEFAULT_VAULT_PASSWORD_FILE(/Users/user/Developer/ansible/ansible.cfg) = /Users/user/Developer/ansible/contrib/vault/lastpass-client.py
DEPRECATION_WARNINGS(/Users/user/Developer/ansible/ansible.cfg) = False
EDITOR(env: EDITOR) = nano
HOST_KEY_CHECKING(/Users/user/Developer/ansible/ansible.cfg) = False
INTERPRETER_PYTHON(/Users/user/Developer/ansible/ansible.cfg) = python3
MAX_FILE_SIZE_FOR_DIFF(/Users/user/Developer/ansible/ansible.cfg) = 208896
PAGER(env: PAGER) = less
PLAYBOOK_DIR(/Users/user/Developer/ansible/ansible.cfg) = /Users/user/Developer/ansible/playbooks:./jobs
USE_PERSISTENT_CONNECTIONS(/Users/user/Developer/ansible/ansible.cfg) = True
OS / ENVIRONMENT

macOS 13.6

STEPS TO REPRODUCE
- name: Create VM portgroup (always causes a change)
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    mac_learning:
      allow_unicast_flooding: true
      enabled: true
      limit: 4096
      limit_policy: allow
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false

- name: Create VM portgroup (no change)
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false
EXPECTED RESULTS

Task should report 'OK' after the initial run.

ACTUAL RESULTS
changed: [hypervisor] => (item={'name': 'No Connectivity', 'vlan': 999}) => changed=true
  ansible_loop_var: dvport
  dvport:
    name: No Connectivity
    vlan: 999
  invocation:
    module_args:
      hostname: example
      in_traffic_shaping:
        average_bandwidth: null
        burst_size: null
        enabled: null
        inherited: true
        peak_bandwidth: null
      mac_learning:
        allow_unicast_flooding: true
        enabled: true
        limit: 4096
        limit_policy: allow
      net_flow: inherited
      network_policy:
        forged_transmits: null
        inherited: true
        mac_changes: null
        promiscuous: null
      num_ports: 8
      out_traffic_shaping:
        average_bandwidth: null
        burst_size: null
        enabled: null
        inherited: true
        peak_bandwidth: null
      password: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
      port: 443
      port_allocation: elastic
      port_binding: static
      port_policy:
        block_override: true
        ipfix_override: false
        live_port_move: false
        mac_management_override: false
        network_rp_override: false
        port_config_reset_at_disconnect: true
        shaping_override: false
        traffic_filter_override: false
        uplink_teaming_override: false
        vendor_config_override: false
        vlan_override: false
      portgroup_name: '0999: No Connectivity'
      proxy_host: null
      proxy_port: null
      state: present
      switch_name: switch0
      teaming_policy:
        active_uplinks: null
        inbound_policy: null
        load_balance_policy: loadbalance_srcid
        notify_switches: true
        rolling_order: false
        standby_uplinks: null
      username: example
      validate_certs: false
      vlan_id: '999'
      vlan_private: false
      vlan_trunk: false
  result: None
ihumster commented 1 year ago

Now check your code on my lab. Get idempotent result. image

TheMysteriousX commented 1 year ago

I'm not sure I following what you mean - the first task in your screenshot Create VM portgroup (always causes a change) is marked as changed in the log - which is the same behaviour I'm seeing.

Perhaps this is a more understandable test case, though less minimal as it includes a loop which isn't required to demonstrate the issue.

- name: Create VM portgroup (always causes a change)
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    mac_learning:
      allow_unicast_flooding: true
      enabled: true
      limit: 4096
      limit_policy: allow
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false
  with_sequence: count=5

- name: Create VM portgroup (no change)
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false
  with_sequence: count=5
ihumster commented 1 year ago

@TheMysteriousX Now I run playbook again (portgroup1 was created yesterday) and both play get same result. Module is idempotent. What do you think is wrong?

image

TheMysteriousX commented 1 year ago

I did some further testing and it looks like there's an additional element. I'd overlooked that in the task prior to setting mac_learning, we were setting network_policy.

It doesn't appear to matter if network_policy is set at the same time, or prior to setting mac_learning so I've adapted the test case to highlight both.

- name: Set network_policy then mac_learning, alternating - incorrect behaviour
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    network_policy: "{{ network_policy_dict if ansible_loop.index0 is even else omit }}"
    mac_learning: "{{ mac_learn_dict if ansible_loop.index0 is odd else omit }}"
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false
  with_sequence: count=6
  loop_control:
    extended: true
  vars:
    mac_learn_dict:
      allow_unicast_flooding: true
      enabled: true
      limit: 4096
      limit_policy: allow
    network_policy_dict:
      inherited: true

- name: Always causes a change - incorrect behaviour
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    mac_learning:
      allow_unicast_flooding: true
      enabled: true
      limit: 4096
      limit_policy: allow
    network_policy:
      inherited: true
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false
  with_sequence: count=5

- name: Does not cause a change - correct behaviour
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    network_policy:
      inherited: true
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false
  with_sequence: count=5

- name: Causes a change the first iteration - incorrect behaviour
  community.vmware.vmware_dvs_portgroup:
    portgroup_name: portgroup1
    switch_name: dvswitch1
    vlan_id: 999
    port_binding: static
    port_allocation: elastic
    mac_learning:
      allow_unicast_flooding: true
      enabled: true
      limit: 4096
      limit_policy: allow
    state: present
    hostname: "{{ vcenter_url }}"
    username: "{{ vsphere_username }}"
    password: "{{ vsphere_password }}"
    validate_certs: false
  with_sequence: count=5
TASK [vsphere : Set network_policy then mac_learning, alternating - incorrect behaviour, mac_learning is not controlled by policy/inheritable] **********************************************************
changed: [host] => (item=1)
changed: [host] => (item=2)
changed: [host] => (item=3)
changed: [host] => (item=4)
changed: [host] => (item=5)
changed: [host] => (item=6)

TASK [vsphere : Always causes a change - incorrect behaviour] ******************************************************************************************************************************************
changed: [host] => (item=1)
changed: [host] => (item=2)
changed: [host] => (item=3)
changed: [host] => (item=4)
changed: [host] => (item=5)

TASK [vsphere : Does not cause a change - correct behaviour] ******************************************************************************************************************************************
ok: [host] => (item=1)
ok: [host] => (item=2)
ok: [host] => (item=3)
ok: [host] => (item=4)
ok: [host] => (item=5)

TASK [vsphere : Causes a change the first iteration - incorrect behaviour] *****************************************************************************************************************************
changed: [host] => (item=1)
ok: [host] => (item=2)
ok: [host] => (item=3)
ok: [host] => (item=4)
ok: [host] => (item=5)
ihumster commented 1 year ago

I run your code in my lab and I see that every second reconfigure action change config.defaultPortConfig.securityPolicy.inherited, config.defaultPortConfig.macManagementPolicy.inherited and config.defaultPortConfig.macManagementPolicy.macLearningPolicy.* config parts (from true to false or vice versa, and another changes in config.defaultPortConfig).

Need to dive more thoughtfully into the module code to understand why it behaves this way.

ihumster commented 1 year ago

@mariolenz Mario, this module is terrible.

    def update_port_group(self):
        config = self.build_config()
        config.configVersion = self.dvs_portgroup.config.configVersion
        task = self.dvs_portgroup.ReconfigureDVPortgroup_Task(config)
        changed, result = wait_for_task(task)
        return changed, result

    def create_port_group(self):
        config = self.build_config()
        task = self.dv_switch.AddDVPortgroup_Task([config])
        changed, result = wait_for_task(task)
        return changed, result

It no change any difference in current config and desired config. Just create new config (without some parts of config). This make module not idempotent. =(