heat_template_version: wallaby description: > OpenStack containerized Ovn Controller agent. parameters: RootStackName: description: The name of the stack/plan. type: string EndpointMap: default: {} description: Mapping of service endpoint -> protocol. Typically set via parameter_defaults in the resource registry. type: json ServiceNetMap: default: {} description: Mapping of service_name -> network name. Typically set via parameter_defaults in the resource registry. Use parameter_merge_strategies to merge it with the defaults. type: json ServiceData: default: {} description: Dictionary packing service data type: json ContainerOvnControllerImage: description: image type: string tags: - role_specific ContainerOvnControllerConfigImage: description: The container image to use for the ovn_controller config_volume type: string tags: - role_specific RoleName: default: '' description: Role name on which the service is applied type: string RoleParameters: default: {} description: Parameters specific to the role type: json DeployIdentifier: default: '' type: string description: > Setting this to a unique value will re-run any deployment tasks which perform configuration on a Heat stack-update. OVNSouthboundServerPort: description: Port of the Southbound DB Server type: number default: 6642 NeutronBridgeMappings: description: > The OVS logical->physical bridge mappings to use. See the Neutron documentation for details. Defaults to mapping br-ex - the external bridge on hosts - to a physical name 'datacentre' which can be used to create provider networks (and we use this for the default floating network) - if changing this either use different post-install network scripts or be sure to keep 'datacentre' as a mapping network name. type: comma_delimited_list default: "datacentre:br-ex" tags: - role_specific EnableVLANTransparency: default: false description: > If True, then allow plugins that support it to create VLAN transparent networks. type: boolean OVNEncapType: description: > Type of encapsulation used in OVN. It can be "geneve" or "vxlan". type: string default: "geneve" constraints: - allowed_values: ["geneve", "vxlan"] OVNIntegrationBridge: description: > Name of the OVS bridge to use as integration bridge by OVN Controller. type: string default: "br-int" OVNMetadataEnabled: description: Whether Metadata Service has to be enabled type: boolean default: true OVNAvailabilityZone: description: The az options to configure in ovs db. eg. ['az-0', 'az-1', 'az-2'] type: comma_delimited_list default: [] tags: - role_specific OVNCMSOptions: description: The CMS options to configure in ovs db type: string default: "" tags: - role_specific OVNEncapTos: description: > The value to be applied to OVN tunnel interface's option:tos as specified in the Open_vSwitch database Interface table. This feature is supported from OVN v21.12.0. type: string default: "0" tags: - role_specific OvsHwOffload: default: false description: | Enable OVS Hardware Offload. This feature supported from OVS 2.8.0 type: boolean tags: - role_specific OVNRemoteProbeInterval: description: Probe interval in ms type: number default: 60000 EnableInternalTLS: type: boolean default: false InternalTLSCAFile: default: '/etc/ipa/ca.crt' type: string description: Specifies the default CA cert to use if TLS is used for services in the internal network. OVNOpenflowProbeInterval: description: > The inactivity probe interval of the OpenFlow connection to the OpenvSwitch integration bridge, in seconds. type: number default: 60 OVNOfctrlWaitBeforeClear: description: > Sets the time ovn-controller will wait on startup before clearing all openflow rules and installing the new ones, in ms. type: number default: 8000 CertificateKeySize: type: string default: '2048' description: Specifies the private key size used when creating the certificate. ContainerOvnCertificateKeySize: type: string default: '' description: Override the private key size used when creating the certificate for this service OVNContainerCpusetCpus: description: > Limit the specific CPUs or cores a container can use. It can be specified as a single core (ex. 0), as a comma-separated list (ex. 0,1), as a range (ex. 0-3) or a combination if previous methods (ex 0-3,7,11-15). The selected cores should be isolated from guests and hypervisor in order to obtain best possible performance. type: string default: '' tags: - role_specific OVNStaticBridgeMacMappings: type: json default: {} description: | Static OVN Bridge MAC address mappings. Unique OVN bridge mac addresses is dynamically allocated by creating neutron ports. When neutron isn't available, for instance in the standalone deployment, use this parameter to provide static OVN bridge mac addresses. For example: controller-0: datacenter: 00:00:5E:00:53:00 provider: 00:00:5E:00:53:01 compute-0: datacenter: 00:00:5E:00:54:00 provider: 00:00:5E:00:54:01 tags: - role_specific AuthCloudName: description: Entry in clouds.yaml to use for authentication type: string default: "" OVNControllerImageUpdateTimeout: description: > During update, how long we wait for the container image to be updated, in seconds. type: number default: 600 OVNControllerUpdateTimeout: description: > During update, how long we wait for the container to be updated, in seconds. type: number default: 600 OVNControllerGarpMaxTimeout: description: > When used, this configuration value specifies the maximum timeout (in seconds) between two consecutive GARP packets sent by ovn-controller. type: number default: 0 conditions: force_config_drive: {equals: [{get_param: OVNMetadataEnabled}, false]} internal_tls_enabled: {equals: [{get_param: EnableInternalTLS}, true]} key_size_override_unset: {equals: [{get_param: ContainerOvnCertificateKeySize}, '']} enable_vlan_transparency: {equals: [{get_param: EnableVLANTransparency}, true]} auth_cloud_name_set: not: {equals: [{get_param: AuthCloudName}, ""]} ovn_cpu_set: or: - not: {equals: [{get_param: OVNContainerCpusetCpus}, '']} - not: {equals: [{get_param: [RoleParameters, OVNContainerCpusetCpus]}, '' ]} key_size_override_set: not: {equals: [{get_param: ContainerOvnCertificateKeySize}, '']} az_ovn_unset: and: - {equals: [{get_param: OVNAvailabilityZone}, []]} - {equals: [{get_param: [RoleParameters, OVNAvailabilityZone]}, '']} resources: ContainersCommon: type: ../containers-common.yaml # Merging role-specific parameters (RoleParameters) with the default parameters. # RoleParameters will have the precedence over the default parameters. RoleParametersValue: type: OS::Heat::Value properties: type: json value: map_replace: - map_replace: - ovn::controller::ovn_bridge_mappings: NeutronBridgeMappings ovn::controller::ovn_cms_options: if: - az_ovn_unset - OVNCMSOptions - OVNCMSOptionsMerged ovn::controller::ovn_encap_tos: OVNEncapTos vswitch::ovs::enable_hw_offload: OvsHwOffload container_cpuset_cpus: OVNContainerCpusetCpus ContainerOvnControllerImage: ContainerOvnControllerImage ContainerOvnControllerConfigImage: ContainerOvnControllerConfigImage - values: {get_param: [RoleParameters]} - values: NeutronBridgeMappings: {get_param: NeutronBridgeMappings} OVNCMSOptions: {get_param: OVNCMSOptions} OVNCMSOptionsMerged: list_join: - '' - - str_replace: template: $OVNCMSOptions params: if: - not: {equals: [{get_param: [RoleParameters, OVNCMSOptions]}, '']} - $OVNCMSOptions: {get_param: [RoleParameters, OVNCMSOptions]} - $OVNCMSOptions: {get_param: OVNCMSOptions} - str_replace: template: ",availability-zones=$OVNAvailabilityZone" params: if: - not: {equals: [{get_param: [RoleParameters, OVNAvailabilityZone]}, '']} - $OVNAvailabilityZone: {get_param: [RoleParameters, OVNAvailabilityZone]} - $OVNAvailabilityZone: list_join: [',', {get_param: OVNAvailabilityZone}] OVNEncapTos: {get_param: OVNEncapTos} OvsHwOffload: {get_param: OvsHwOffload} OVNContainerCpusetCpus: {get_param: OVNContainerCpusetCpus} ContainerOvnControllerImage: {get_param: ContainerOvnControllerImage} ContainerOvnControllerConfigImage: {get_param: ContainerOvnControllerConfigImage} OVNBridgeMappingsValue: type: OS::Heat::Value properties: type: json value: map_replace: - map_replace: - ovn_bridge_mappings: NeutronBridgeMappings ovn_static_bridge_mac_mappings: OVNStaticBridgeMacMappings - values: {get_param: [RoleParameters]} - values: NeutronBridgeMappings: {get_param: NeutronBridgeMappings} OVNStaticBridgeMacMappings: {get_param: OVNStaticBridgeMacMappings} outputs: role_data: description: Role data for the Ovn Controller agent. value: service_name: ovn_controller firewall_rules: '118 neutron vxlan networks': proto: 'udp' dport: 4789 state: [] '119 neutron geneve networks': proto: 'udp' dport: 6081 state: [] '120 neutron geneve networks no conntrack': proto: 'udp' dport: 6081 table: 'raw' chain: 'OUTPUT' jump: 'NOTRACK' action: 'append' state: ['INVALID'] '121 neutron geneve networks no conntrack': proto: 'udp' dport: 6081 table: 'raw' chain: 'PREROUTING' jump: 'NOTRACK' action: 'append' state: ['INVALID'] config_settings: map_merge: - get_attr: [RoleParametersValue, value] - ovn::southbound::port: {get_param: OVNSouthboundServerPort} ovn::controller::ovn_encap_ip: str_replace: template: "%{hiera('$NETWORK')}" params: $NETWORK: {get_param: [ServiceNetMap, NeutronTenantNetwork]} ovn::controller::ovn_encap_type: {get_param: OVNEncapType} ovn::controller::ovn_bridge: {get_param: OVNIntegrationBridge} ovn::controller::hostname: "%{hiera('fqdn_canonical')}" # Important: If an ovn-controller parameter needs to # change during update the external_update_tasks section # needs to be updated as well. ovn::controller::ovn_remote_probe_interval: {get_param: OVNRemoteProbeInterval} ovn::controller::ovn_openflow_probe_interval: {get_param: OVNOpenflowProbeInterval} ovn::controller::ovn_ofctrl_wait_before_clear: {get_param: OVNOfctrlWaitBeforeClear} ovn::controller::garp_max_timeout_sec: {get_param: OVNControllerGarpMaxTimeout} ovn::controller::ovn_monitor_all: true - if: - force_config_drive - nova::compute::force_config_drive: true - {} - if: - internal_tls_enabled - tripleo::profile::base::neutron::agents::ovn::protocol: 'ssl' - {} - if: - enable_vlan_transparency - vswitch::ovs::vlan_limit: 0 - {} service_config_settings: {} # BEGIN DOCKER SETTINGS puppet_config: puppet_tags: vs_config,exec config_volume: ovn_controller step_config: | include tripleo::profile::base::neutron::agents::ovn config_image: {get_attr: [RoleParametersValue, value, ContainerOvnControllerConfigImage]} # We need to mount /run for puppet_config step. This is because # puppet-vswitch runs the commands "ovs-vsctl set open_vswitch . external_ids:..." # to configure the required parameters in ovs db which will be read # by ovn-controller. And ovs-vsctl talks to the ovsdb-server (hosting conf.db) # on the unix domain socket - /run/openvswitch/db.sock volumes: - /lib/modules:/lib/modules:ro - /run/openvswitch:/run/openvswitch:shared,z # Needed for creating module load files - /etc/sysconfig/modules:/etc/sysconfig/modules kolla_config: /var/lib/kolla/config_files/ovn_controller.json: command: list_join: - ' ' - - /usr/bin/ovn-controller --pidfile --log-file unix:/run/openvswitch/db.sock - if: - internal_tls_enabled - list_join: - ' ' - - -p /etc/pki/tls/private/ovn_controller.key -c /etc/pki/tls/certs/ovn_controller.crt -C - {get_param: InternalTLSCAFile} - '' permissions: - path: /var/log/openvswitch owner: root:root recurse: true - path: /var/log/ovn owner: root:root recurse: true metadata_settings: if: - internal_tls_enabled - - service: ovn_controller network: {get_param: [ServiceNetMap, OvnDbsNetwork]} type: node - null docker_config: step_4: configure_cms_options: start_order: 0 detach: false net: host privileged: true user: root command: ['/bin/bash', '-c', 'CMS_OPTS=$(hiera ovn::controller::ovn_cms_options -c /etc/puppet/hiera.yaml); if [ X"$CMS_OPTS" != X ]; then ovs-vsctl set open . external_ids:ovn-cms-options=$CMS_OPTS;else ovs-vsctl remove open . external_ids ovn-cms-options; fi'] image: &ovn_controller_image {get_attr: [RoleParametersValue, value, ContainerOvnControllerImage]} volumes: list_concat: - {get_attr: [ContainersCommon, volumes]} - - /lib/modules:/lib/modules:ro - /run/openvswitch:/run/openvswitch:shared,z environment: TRIPLEO_DEPLOY_IDENTIFIER: {get_param: DeployIdentifier} ovn_controller: map_merge: - start_order: 1 image: *ovn_controller_image net: host privileged: true user: root restart: always depends_on: - openvswitch.service - if: - ovn_cpu_set - cpuset_cpus: {get_attr: [RoleParametersValue, value, container_cpuset_cpus]} - healthcheck: test: list_join: - ' ' - - '/openstack/healthcheck' - yaql: expression: str($.data.port) data: port: {get_param: OVNSouthboundServerPort} volumes: list_concat: - - /var/lib/kolla/config_files/ovn_controller.json:/var/lib/kolla/config_files/config.json:ro - /lib/modules:/lib/modules:ro # TODO(numans): This is temporary. Mount /run/openvswitch once # openvswitch systemd script is fixed to not delete /run/openvswitch # folder in the host when openvswitch service is stopped. - /run:/run - /var/lib/openvswitch/ovn:/run/ovn:shared,z - /var/log/containers/openvswitch:/var/log/openvswitch:z - /var/log/containers/openvswitch:/var/log/ovn:z - if: - internal_tls_enabled - - list_join: - ':' - - {get_param: InternalTLSCAFile} - {get_param: InternalTLSCAFile} - 'ro' - /etc/pki/tls/certs/ovn_controller.crt:/etc/pki/tls/certs/ovn_controller.crt - /etc/pki/tls/private/ovn_controller.key:/etc/pki/tls/private/ovn_controller.key - null environment: KOLLA_CONFIG_STRATEGY: COPY_ALWAYS deploy_steps_tasks: - name: Certificate generation when: - step|int == 1 - enable_internal_tls block: - include_role: name: linux-system-roles.certificate vars: certificate_requests: - name: ovn_controller dns: str_replace: template: "{{fqdn_$NETWORK}}" params: $NETWORK: {get_param: [ServiceNetMap, OvnDbsNetwork]} principal: str_replace: template: "ovn_controller/{{fqdn_$NETWORK}}@{{idm_realm}}" params: $NETWORK: {get_param: [ServiceNetMap, OvnDbsNetwork]} run_after: | systemctl restart tripleo_ovn_controller key_size: if: - key_size_override_unset - {get_param: CertificateKeySize} - {get_param: ContainerOvnCertificateKeySize} ca: ipa host_prep_tasks: - name: create persistent directories file: path: "{{ item.path }}" state: directory setype: "{{ item.setype }}" mode: "{{ item.mode|default(omit) }}" with_items: - { 'path': /var/log/containers/openvswitch, 'setype': container_file_t, 'mode': '0750' } - { 'path': /var/lib/openvswitch/ovn, 'setype': container_file_t } - name: enable virt_sandbox_use_netlink for healthcheck seboolean: name: virt_sandbox_use_netlink persistent: true state: true when: - ansible_facts.selinux is defined - ansible_facts.selinux.status == "enabled" - name: Copy in cleanup script copy: content: {get_file: ../neutron/neutron-cleanup} dest: '/usr/libexec/neutron-cleanup' force: yes mode: '0755' - name: Copy in cleanup service copy: content: {get_file: ../neutron/neutron-cleanup.service} dest: '/usr/lib/systemd/system/neutron-cleanup.service' force: yes - name: Enabling the cleanup service service: name: neutron-cleanup enabled: yes when: not (ansible_check_mode|bool) external_deploy_tasks: - when: - step|int == 0 name: ovn_controller_external_deploy_init block: - name: str_replace: template: create ovn mac address for $ROLE_NAME role nodes params: $ROLE_NAME: {get_param: RoleName} tripleo_ovn_mac_addresses: playbook_dir: "{{ playbook_dir }}" stack_name: {get_param: RootStackName} role_name: {get_param: RoleName} server_resource_names: str_replace: template: '{{ groups["$ROLE_NAME"] }}' params: $ROLE_NAME: {get_param: RoleName} ovn_bridge_mappings: {get_attr: [OVNBridgeMappingsValue, value, ovn_bridge_mappings]} ovn_static_bridge_mac_mappings: {get_attr: [OVNBridgeMappingsValue, value, ovn_static_bridge_mac_mappings]} external_update_tasks: - name: Force pull image in case image name doesn't change. when: step|int == 1 tags: - ovn - ovn_image become: true loop: "{{ groups['ovn_controller'] | difference(groups['excluded_overcloud']) }}" delegate_to: "{{ item }}" async: {get_param: OVNControllerImageUpdateTimeout} poll: 0 register: ovn_controller_image_update containers.podman.podman_image: name: {get_param: ContainerOvnControllerConfigImage} validate_certs: false force: true - name: Was the ovn_controller image pull successful. when: - step|int == 1 - "'results' in ovn_controller_image_update" tags: - ovn - ovn_image become: true delegate_to: "{{ async_result_item.item }}" async_status: jid: "{{ async_result_item.ansible_job_id }}" loop: "{{ovn_controller_image_update.results }}" loop_control: loop_var: "async_result_item" register: async_poll_results until: async_poll_results.finished retries: {get_param: OVNControllerImageUpdateTimeout} delay: 1 - name: OVN Container image used debug: msg: "ovn container will be using {{ image }}" vars: image: {get_param: ContainerOvnControllerConfigImage} when: step|int == 1 tags: ovn - name: Update OVN OVS related parameters before update. when: - step|int == 1 tags: - ovn become: true vars: timeout: {get_param: OVNOfctrlWaitBeforeClear} # Place to add all new OVN parameters required during an update. shell: | set -e ovs-vsctl set Open_vSwitch . external_ids:ovn-ofctrl-wait-before-clear={{ timeout }} ovs-vsctl set Open_vSwitch . external_ids:ovn-monitor-all=true ovs-vsctl set Open_vSwitch . external_ids:ovn-match-northd-version=false async: {get_param: OVNControllerUpdateTimeout} poll: 0 register: ovs_vsctl loop: "{{ groups['ovn_controller'] | difference(groups['excluded_overcloud']) }}" delegate_to: "{{ item }}" - name: Was the update of OVN OVS related parameter successful. when: - step|int == 1 - "'results' in ovs_vsctl" become: true tags: - ovn delegate_to: "{{ async_result_item.item }}" async_status: jid: "{{ async_result_item.ansible_job_id }}" loop: "{{ovs_vsctl.results }}" loop_control: loop_var: "async_result_item" register: async_poll_results until: async_poll_results.finished retries: {get_param: OVNControllerUpdateTimeout} delay: 1 - set_fact: any_ovn_host: "{{groups['ovn_controller'] | difference(groups['excluded_overcloud']) | first }}" tags: ovn when: step|int == 1 - name: Find ovn_controller configs in container-startup-configs find: paths: /var/lib/tripleo-config/container-startup-config/ patterns: "*ovn_controller.json" recurse: yes register: ovn_cont_17_0 delegate_to: "{{ any_ovn_host }}" when: (step|int == 1) and (any_ovn_host is defined) and (any_ovn_host|length > 0) tags: - ovn become: true - name: get directory path from the ovn_cont_17_0 set_fact: ovn_config_path: "{{ ovn_cont_17_0.files.0.path | dirname }}" when: step|int == 1 tags: ovn - name: Get PIDfile used by systemd on each ovn node when: - step|int == 1 tags: - ovn become: true shell: | set -e grep PID /etc/systemd/system/tripleo_ovn_controller.service | cut -d= -f2 register: pidfile loop: "{{ groups['ovn_controller'] | difference(groups['excluded_overcloud']) }}" delegate_to: "{{ item }}" - name: Update ovn_controller. vars: config_step: "{{ ('step_4' in ovn_config_path) | ternary('4', '3')}}" query: "results[?item == '{{item}}'].stdout" when: step|int == 1 tags: ovn become: true loop: "{{ groups['ovn_controller'] | difference(groups['excluded_overcloud']) }}" delegate_to: "{{ item }}" async: {get_param: OVNControllerUpdateTimeout} poll: 0 register: ovn_controller_update tripleo_container_manage: config_dir: "{{ ovn_config_path }}" config_patterns: '*ovn_controller.json' config_id: - 'tripleo_step{{config_step}}' log_base_path: "{{ container_log_stdout_path }}" debug: "{{ enable_debug | bool }}" config_overrides: '.*ovn_controller': image: {get_param: ContainerOvnControllerConfigImage} conmon_pidfile: "{{ pidfile | json_query(query) | first }}" name: "ovn_controller" - name: Was the ovn_controller successful. when: - step|int == 1 - "'results' in ovn_controller_update" tags: ovn become: true delegate_to: "{{ async_result_item.item }}" async_status: jid: "{{ async_result_item.ansible_job_id }}" loop: "{{ovn_controller_update.results }}" loop_control: loop_var: "async_result_item" register: async_poll_results until: async_poll_results.finished retries: {get_param: OVNControllerUpdateTimeout} delay: 1 - name: Pause for 30s to give ovn_controllers time to reconnect to dbs when: - step|int == 1 tags: ovn wait_for: timeout: 30 upgrade_tasks: - name: Fetch running ovn_controller image shell: | set -e podman inspect --format "{{'{{'}}.ImageName{{'}}'}}" ovn_controller register: running_ovn_image when: - step|int == 4 tags: - ovn - ovn_image - name: Run ovn_controller upgrade tags: - ovn - ovn_image vars: ovn_controller_image: {get_param: ContainerOvnControllerConfigImage} when: - step|int == 4 - running_ovn_image.stdout != ovn_controller_image block: - name: Force pull image in case image name doesn't change. when: step|int == 4 tags: - ovn - ovn_image containers.podman.podman_image: name: {get_param: ContainerOvnControllerConfigImage} validate_certs: false force: true - name: Update OVN OVS related parameters before update. when: - step|int == 4 tags: - ovn vars: timeout: {get_param: OVNOfctrlWaitBeforeClear} # Place to add all new OVN parameters required during an update. shell: | set -e ovs-vsctl set Open_vSwitch . external_ids:ovn-ofctrl-wait-before-clear={{ timeout }} ovs-vsctl set Open_vSwitch . external_ids:ovn-monitor-all=true ovs-vsctl set Open_vSwitch . external_ids:ovn-match-northd-version=false - name: Find ovn_controller configs in container-startup-configs find: paths: /var/lib/tripleo-config/container-startup-config/ patterns: "*ovn_controller.json" recurse: yes register: ovn_cont_17_0 when: - step|int == 4 tags: - ovn - name: get directory path from the ovn_cont_17_0 set_fact: ovn_config_path: "{{ ovn_cont_17_0.files.0.path | dirname }}" when: step|int == 4 tags: ovn - name: Get PIDfile used by systemd on each ovn node when: - step|int == 4 tags: - ovn shell: | set -e grep PID /etc/systemd/system/tripleo_ovn_controller.service | cut -d= -f2 register: pidfile - name: Update ovn_controller. vars: config_step: "{{ ('step_4' in ovn_config_path) | ternary('4', '3')}}" when: step|int == 4 tags: ovn tripleo_container_manage: config_dir: "{{ ovn_config_path }}" config_patterns: '*ovn_controller.json' config_id: - 'tripleo_step{{config_step}}' log_base_path: "{{ container_log_stdout_path }}" debug: "{{ enable_debug | bool }}" config_overrides: '.*ovn_controller': image: {get_param: ContainerOvnControllerConfigImage} conmon_pidfile: "{{ pidfile.stdout }}" name: "ovn_controller" - name: Pause for 30s to give ovn_controllers time to reconnect to dbs when: - step|int == 4 tags: ovn wait_for: timeout: 30 scale_tasks: - when: - step|int == 1 - container_cli == 'podman' tags: down become: true environment: OS_CLOUD: if: - auth_cloud_name_set - {get_param: AuthCloudName} - {get_param: RootStackName} block: # Some tasks are running from the Undercloud which has # the OpenStack clients installed. - name: Get neutron agents ID command: openstack network agent list --column ID --column Host --column Binary --format yaml register: neutron_agents_result delegate_to: "{{ groups['Undercloud'] | first }}" check_mode: false changed_when: false - name: Filter only current host set_fact: neutron_agents: "{{ neutron_agents_result.stdout | from_yaml | selectattr('Host', 'match', ansible_facts['fqdn'] ~ '.*') | list }}" delegate_to: "{{ groups['Undercloud'] | first }}" check_mode: false - name: Deleting OVN agents block: - name: Stop OVN containers loop: - tripleo_ovn_controller - tripleo_ovn_metadata_agent service: name: "{{ item }}" state: stopped enabled: false become: true register: stop_containers failed_when: "('msg' in stop_containers and 'Could not find the requested service' not in stop_containers.msg) or ('rc' in stop_containers and stop_containers.rc != 0)" - name: Delete neutron agents loop: "{{ neutron_agents }}" loop_control: loop_var: agent label: "{{ agent.Host }}/{{ agent.Binary }}" command: openstack network agent delete {{ agent.ID }} delegate_to: "{{ groups['Undercloud'] | first }}" check_mode: false