Ansible list has length when empty - amazon-web-services

I'm doing "aws rds describe-db-instances":
- name: list recovery points by resource
shell: aws rds describe-db-instances --db-instance-identifier {{ item }} --query "DBInstances[?(LatestRestorableTime>'{{ time }}')].[DBInstanceIdentifier,LatestRestorableTime]" --output text
loop: "{{ db_identifier }}"
register: recovery_points
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
AWS_SESSION_TOKEN: "{{ security_token }}"
-->until: (recovery_points.results|map(attribute='stdout')|list|length) > 0
-->retries: 20
-->delay: 30
when: db_identifier is defined
That should check for recovery points older than "time", also db_identifier|lenght = 2.
- debug:
msg: "{{ recovery_points.results | map(attribute='stdout') | list }}"
result:
['', '']
The issue I'm facing is that when there are no recovery points and it returns this empty list, it says the length of this list is 2.
- debug:
msg: "{{ recovery_points.results | map(attribute='stdout') | list | length }}"
result:
2
Having this, I wanted to include the "until" condition in the shell command (that you can see preceded by -->) but I cannot do it because of this length.
How can I work around it?

Related

Is it possible to loop into two different lists in the same playbook (Ansible)?

I'm writing a Playbook Ansible and I want to loop into two different lists.
I now that I can use with_items to loop in a list, but can I use with_items twice in the same playbook?
Here is what I want to do:
- name: Deploy the network in fabric 1 and fabric 2
tags: [merged]
role_network:
config:
- net_name: "{{ networkName }}"
vrf_name: "{{ vrf }}"
net_id: 30010
net_template: "{{ networkTemplate }}"
net_extension_template: "{{ networkExtensionTemplate }}"
vlan_id: "{{ vlan }}"
gw_ip_subnet: "{{ gw }}"
attach: "{{ item }}"
deploy: false
fabric: "{{ item }}"
state: merged
with_items:
- "{{ attachs }}"
"{{ fabric }}"
register: networks
So for the first call, I want to use the playbook with fabric[0] and attachs[0].
For the second call, I want to use the playbook with fabric[1] and attachs[1].
And so on...
Can someone help me please?
What you are looking to achieve is what was with_together and that is, now, recommanded to achieve with the zip filter.
So: loop: "{{ attachs | zip(fabric) | list }}".
Where the element of the first list (attachs) would be item.0 and the element of the second list (fabric) would be item.1.
- name: Deploy the network in fabric 1 and fabric 2
tags: [merged]
role_network:
config:
- net_name: "{{ networkName }}"
vrf_name: "{{ vrf }}"
net_id: 30010
net_template: "{{ networkTemplate }}"
net_extension_template: "{{ networkExtensionTemplate }}"
vlan_id: "{{ vlan }}"
gw_ip_subnet: "{{ gw }}"
attach: "{{ item.0 }}"
deploy: false
fabric: "{{ item.1 }}"
state: merged
loop: "{{ attachs | zip(fabric) | list }}"
register: networks

Ansible in AWS, list processing question using ec2_instance_info for several nodes

I am running several Ansible playbooks in AWS and I am having difficulty with a test yaml file. The purpose of the yaml file is to query AWS for a list of servers using a filter and set_fact the instance name, the instance ID the instance size and the private IP.
The code I have is returning only data for the first node in the list and repeats the debug line every 12 lines. All the other lines show no data returned. I am using the ec2_instance_info to get the various data about the instances.
Here is the Ansible yaml file
---
# This script gathers the Instance ID's et. al.
- name: Get EC2 Info
ec2_instance_info:
region: '{{ aws_region }}'
aws_access_key: "{{ lookup('ini', 'aws_access_key_id section=saml file=~/.aws/credentials') }}"
aws_secret_key: "{{ lookup('ini', 'aws_secret_access_key section=saml file=~/.aws/credentials') }}"
security_token: "{{ lookup('ini', 'aws_session_token section=saml file=~/.aws/credentials') }}"
filters:
"tag:Name": "test-envMan*"
register: Instance_ID
- name: Get Instance ID
debug:
msg: "{{ item.0 }} | {{ item.1 }} | {{ item.2 }} | {{ item.3 }}"
with_together:
- "{{ Instance_ID.instances | map(attribute='tags.Name') | list }}"
- "{{ Instance_ID.instances[0].instance_id }}"
- "{{ Instance_ID.instances[1].instance_type }}"
- "{{ Instance_ID.instances[2].private_ip_address }}"
- name: Gather and Save info
set_fact:
Tag_Name: "{{ Instance_ID.instances | map(attribute='tags.Name') | list }}"
Instance_ID: "{{ Instance_ID.instances[0].instance_id }}"
Instance_Size: "{{ Instance_ID.instances[1].instance_type }}"
Instance_PrivIP: "{{ Instance_ID.instances[2].private_ip_address }}"
The output shows 12 lines of Ansible "ok" output for each server. The first line of which includes the debug output of the expected fields for the first node.
So 1 line of "ok" log output, then the debug line. Then 11 lines of "ok" log output of the same node. Then 1 line of "ok" output for the second node, the the debug line for the first node. etc.
I need to discover what I am doing incorrectly and how to make it behave.
Any comments, suggestions or pointers are appreciated.
Thanks.

Ansible querying AWS AMIs

I'm trying to query AWS EC2 AMIs from Ansible but keep running into an error when looping through the results:
- hosts: localhost
tasks:
- name: Get AMI
ec2_ami_facts:
owner: amazon
filters:
architecture: x86_64
root-device-type: ebs
register: amis
- name: return filtered data
debug:
msg: "{{ item }}"
loop: " {{ amis \
| json_query( 'Images[?Description!=`null`] \
| [?starts_with(Description,`Amazon Linux`)]' ) \
}} "
The idea is to return the image documents, and later just the image IDs with more filtering (end goal is to get the most recent ami id for a given description). But with the current example, and anything else I try I get this error:
TASK [return filtered data] ****************************************************
fatal: [localhost]: FAILED! => {"msg": "Invalid data passed to 'loop',
it requires a list, got this instead: . Hint: If you passed a
list/dict of just one element, try adding wantlist=True to your lookup
invocation or use q/query instead of lookup."}
I can look at the 'amis' in its entirety and it looks good, but any filtering I try fails. What is the correct method?
This works, thanks for the folks at #ansible on freenode.
- hosts: localhost
tasks:
- name: Get AMI
ec2_ami_facts:
owner: amazon
filters:
architecture: x86_64
root-device-type: ebs
register: amis
- name: return latest AMI
set_fact:
my_ami: "{{ amis.images \
| selectattr('description', 'defined') \
| selectattr('description', 'match', '^Amazon Linux.*GP2$') \
| selectattr('description', 'match', '[^(Candidate)]') \
| sort(attribute='creation_date') \
| last }} "
- debug:
msg: "ami = {{ my_ami | to_nice_yaml }}"
Also see here: https://bitbucket.org/its-application-delivery/ansible-aws/src/master/ansible/task_find_ami.yml?fileviewer=file-view-default
Use following to dynamically fetch the latest AMI.
---
- name: Find latest AMI
ec2_ami_facts:
owners: 099720109477
region: "{{ AWS_REGION }}"
filters:
name: "ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"
register: findami
- name: Sort the latest AMI
set_fact:
latest_ami: >
{{ findami.images | sort(attribute='creation_date') | last }}
- name: Launch Instance with latest AMI
ec2:
instance_type: "{{ INSTANCE_TYPE }}"
image: "{{ latest_ami.image_id }}"
key_name: "{{ KEY_NAME }}"
region: "{{ AWS_REGION }}"
group_id: "{{ sg.group_id }}"
wait: yes
count: "{{ INSTANCES_COUNT }}"
vpc_subnet_id: "{{ subnet.subnet.id }}"
assign_public_ip: no

Ansible - force a variable/fact to be undefined

I'm trying to run a playbook multiple times in a loop which creates AWS route53 records.
My task to create the route53 record looks like this:
- name: Create Public DNS record
route53:
profile: "{{ route53_profile_id }}"
command: "{{ dns_command }}"
zone: "{{ dns_zone }}"
record: "{{ dns_record_name }}.{{ dns_zone }}"
type: "{{ dns_type }}"
value: "{{ dns_value }}"
overwrite: "{{ dns_overwrite }}"
ttl: "{{ dns_ttl }}"
health_check: "{{ healthcheck.health_check.id | default(omit) }}"
failover: "{{ dns_setting.failover | default(omit) }}"
weight: "{{ dns_setting.weight | default(omit) }}"
region: "{{ region | default(omit) }}"
identifier: "{{ identifier | default(omit) }}"
My problem is that the health check isn't always defined every time.
Creation of the health check looks like this:
- name: Create healthcheck with IP address for EC2 instance
route53_health_check:
state: "{{ healthcheck.state | default( healthcheck_defaults.state ) }}"
profile: "{{ route53_profile_id }}"
region: "{{ vpc.region }}"
ip_address: "{{ dns_value }}"
type: "{{ healthcheck.type | default( healthcheck_defaults.type ) }}"
resource_path: "{{ healthcheck.resource_path | default( omit ) }}"
port: "{{ healthcheck.port | default( omit ) }}"
security_token: "{{ healthcheck.security_token | default( omit ) }}"
validate_certs: "{{ healthcheck.validate_certs | default( omit ) }}"
string_match: "{{ healthcheck.string_match | default( omit ) }}"
request_interval: "{{ healthcheck.request_interval | default( healthcheck_defaults.request_interval ) }}"
failure_threshold: "{{ healthcheck.failure_threshold | default( healthcheck_defaults.failure_threshold ) }}"
register: healthcheck
when:
- dns_type == "A"
- dns_setting.healthcheck is defined
If the loop runs 5 times, it may only be defined in one iteration. If the health check runs then the 'healthcheck' variable contains the details of the health check, e.g. the ID. If it does not run on a given loop, the 'healthcheck' variable contains the following:
"healthcheck": {
"changed": false,
"skip_reason": "Conditional check failed",
"skipped": true
}
In my route53 creation, the health check is omitted if the 'healthcheck' variable is undefined. However if it is defined, ansible attempts to dereference the id parameter of the health_check parameter of healthcheck, which doesn't exist.
If I try to set health check to a default value when not in use, e.g. {} then it is still defined, and my route53 creation fails.
Is there a way to force a variable or fact to be undefined? Something like:
- name: Undefine health check
set_fact:
healthcheck: undef
Try something like this:
- name: Create Public DNS record
route53:
... cut ...
health_check: "{{ healthcheck | skipped | ternary( omit, (healthcheck.health_check | default({})).id ) }}"
... cut ...
This will pass omit if healthcheck was skipped and healthcheck.health_check.id otherwise.
From my experience, default is not working properly with nested dicts of 2+ levels (i.e. works with mydict.myitem | default('ok') but fails with mydict.mysubdict.myitem | default('ok'), so I used the hack to default subdict to {} first to safely access id: (healthcheck.health_check | default({})).id

Assign ansible vars based on AWS tags

I'm trying to figure out a way to assign variables in Ansible based on tags I have in AWS. I was experimenting with ec2_remote_tags but it's returning alot more information than I need. It seems like there should be an easier way to do this and I'm just not thinking of it.
For example, if I have a tag called function that creates the tag_function_api group using dynamic inventory and I want to assign a variable function to the value api. Any ideas on an efficient way to do this?
I've managed to make a dict of tags with lists of values:
- hosts: localhost
tasks:
- ec2_remote_facts:
region: eu-west-1
register: ec2_facts
# get all possible tag names
- set_fact: tags="{{ item.keys() }}"
with_items: "{{ ec2_facts.instances | map(attribute='tags') | list }}"
register: tmp_tags
# get flattened list of tags (for some reason lookup() returns string, so we use with_)
- assert: that=true
with_flattened: "{{ tmp_tags.results | map(attribute='ansible_facts.tags') | list }}"
register: tmp_tags
# get unique tag names
- set_fact: tags="{{ tmp_tags.results | map(attribute='item') | list | unique }}"
- set_fact: my_tags="{{ {} }}"
# get all possible values for a given tag
- set_fact:
my_tags: "{{ my_tags | combine( {''+item: ec2_facts.instances | map(attribute='tags.'+item) | select('defined') | list | unique}) }}"
with_items: "{{ tags }}"
- debug: var=my_tags
If you are using Ansible's ec2.py dynamic inventory script it makes all tags available as host variables in the form ec2_tag_<tag name> = <tag value>. It also adds all EC2 hosts to the group ec2.
So if your EC2 instance has a tag AwesomeVariable = "Greatness" and you want that value assigned to the Ansible host variable stupendous you can do this:
- name: Register variables based on tags
set_fact:
stupendous: "{{ ec2_tag_AwesomeVariable }}"
when: "'ec2' in group_names"
After this runs you can use the variable stupendous for your EC2 hosts and it has the value set for the AwesomeVariable tag.
Was able to get this to work based on some more information I found here: https://groups.google.com/forum/#!topic/ansible-project/ES2CjMPps3M
Here is the code that worked for us:
- name: Retrieve all tags on an instance
ec2_tag:
region: '{{ ec2_region }}'
resource: '{{ ec2_id }}'
state: list
aws_access_key: "{{ ANSIBLE_IAM_KEY }}"
aws_secret_key: "{{ ANSIBLE_IAM_SECRET }}"
register: ec2_facts
- name: register variables based on tag
set_fact:
tt_function: "{{ ec2_facts.tags.Function }}"
tt_release: "{{ ec2_facts.tags.Release }}"
tt_client: "{{ ec2_facts.tags.Client }}"