I've created two very simple Ansible roles called base and web. web is dependent on base and this dependency is defined in web/meta/main.yml.
base has the following task defined in base/tasks/main.yml:
- name: install required packages
apt:
name: "{{ install_packages }}"
update_cache: "{{ apt_update_cache }}"
cache_valid_time: "{{ apt_cache_valid_time }}"
The variables in base are defined in base/defaults/main.yml:
apt_update_cache: yes
apt_cache_valid_time: 3600
install_packages:
- ufw
- sshguard
The variables in web are defined in web/defaults/main.yml:
install_packages:
- nginx
What I want to do is:
Call install required packages from base, using the install_packages variable from base, and apt_update_cache and apt_cache_valid_time from base.
Call install required packages from web, using the install_packages variable from web, and apt_update_cache and apt_cache_valid_time from base (since I haven't overridden either of those two in web).
Is this possible, and if so how would I go about doing it? Alternatively, is there a 'better' way to achieve the same result, without reproducing the task in every dependent role (I'm likely to create several others which depend on base, and they will all have their own role-specific tasks too)?
Here's one option.
For clarity, rename install_packages in roles/base/defaults/main.yml to base_packages:
---
base_packages:
- ufw
- sshguard
Rewrite your package installation task in roles/base/main.yml so that it looks like this:
---
- name: install required packages
apt:
name: "{{ base_packages + extra_packages|default([]) }}"
update_cache: "{{ apt_update_cache }}"
cache_valid_time: "{{ apt_cache_valid_time }}"
In roles/web/defaults/main.yml, define web_packages:
---
web_packages:
- nginx
And in roles/web/meta/main.yml, declare your dependency like this:
---
dependencies:
- role: base
vars:
extra_packages: "{{ web_packages }}"
If I replace that apt task with a debug task:
- name: install required packages
debug:
msg:
apt:
name: "{{ base_packages + extra_packages|default([]) }}"
update_cache: "{{ apt_update_cache }}"
cache_valid_time: "{{ apt_cache_valid_time }}"
And use this playbook:
---
- hosts: localhost
gather_facts: false
roles:
- web
I get the following output:
PLAY [localhost] ******************************************************************************
TASK [base : install required packages] *******************************************************
ok: [localhost] => {
"msg": {
"apt": {
"cache_valid_time": 3600,
"name": [
"ufw",
"sshguard",
"nginx"
],
"update_cache": true
}
}
}
PLAY RECAP ************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0
As you can see, this installs both the packages declared in your base role as well as the packages from the web role.
Related
I am trying to debug a playbook I've written which uses a couple of roles to spin up and then configure an AWS instance.
The basic structure is one playbook (new-server.yml) imports two roles -- roles/ec2_instance and roles/start_env. The ec2_instance role should be ran on localhost with my AWS tokens and then the start_env role gets ran on servers which are generated by the first role.
My playbook new-server.yml starts off like this:
- name: provision new instance
include_role:
name: ec2_instance
public: yes
vars:
instance_name: "{{ item.host_name }}"
env: "{{ item.git_branch }}"
env_type: "{{ item.env_type }}"
loop:
- { host_name: 'prod', git_branch: 'master', env_type: 'prod' }
- { host_name: 'test', git_branch: 'test', env_type: 'devel'}
This role builds an ec2 instance, updates route 53, uses add_host to add the host to the in-memory inventory in the just_created group.
Next, I have this in the new_server.yml playbook. Both of my IPs show up here just fine. My localhost does not show up here.
- name: debug just_created group
debug: msg="{{ groups['just_created'] }}"
Finally, again in new_server.yml, I try to do the last mile configuration and start my application on the new instance:
- name: Configure and start environment on new instance
include_role:
name: start_env
apply:
become: yes
delegate_to: "{{ item }}"
with_items:
- "{{ groups['just_created'] }}"
However, it doesnt look like the task is delegating properly, because I have this task in roles/start_env/main.yml:
- name: debug hostname
debug: msg="{{ ansible_hostname }}"
And what I'm seeing in my output is
TASK [start_env : debug hostname] ************************************************************************************************************************************
Monday 11 January 2021 12:00:05 -0800 (0:00:00.111) 0:00:37.374 ********
ok: [localhost -> 10.20.15.225] => {
"msg": "My-Local-MBP"
}
TASK [start_env : debug hostname] ************************************************************************************************************************************
Monday 11 January 2021 12:00:05 -0800 (0:00:00.043) 0:00:37.417 ********
ok: [localhost -> 10.20.31.35] => {
"msg": "My-Local-MBP"
}
I've read a lot about delegate_to, include_role and loops this morning. It sounds like Ansible has made things pretty complicated when you want to combine these things, but it also seems like the way I am trying to invoke these should be right. Any idea what I'm doing wrong (or if there is a smarter way to do this? I found this and while its a clever workaround, it doesn't quite fit what I'm seeing and I'd like to avoid creating another tasks file in my roles. Not exactly how I want to manage something like this. Most of the information I've been going off of has been this thread https://github.com/ansible/ansible/issues/35398
I guess this is a known issue... the output shows [localhost -> 10.20.31.35] which indicates it is delegating from localhost to 10.20.31.35, however this is only for the connection. Any templating done in the task definition uses the values of the host in the loop, which is localhost.
I figured out something in my own way that allows me to most keep what I've already written the same. I modified my add_host task to use the instance_name var as the hostname and the ec2 IP as the ansible_host instance var and then updated my last task to
roles/aws.yml:
- name: Add new instance to inventory
add_host:
hostname: "{{ instance_name }}"
ansible_host: "{{ ec2_private_ip }}"
ansible_user: centos
ansible_ssh_private_key_file: ../keys/my-key.pem
groups: just_created
new_servers.yml:
tasks:
- name: provision new instance
include_role:
name: ec2_instance
public: yes
vars:
instance_name: "{{ item.host_name }}"
env: "{{ item.git_branch }}"
env_type: "{{ item.env_type }}"
loop:
- { host_name: 'prod', git_branch: 'master', env_type: 'prod' }
- { host_name: 'test', git_branch: 'test', env_type: 'devel'}
- name: Configure and start environment on new instance
include_role:
name: start_env
apply:
become: yes
delegate_to: "{{ item }}"
vars:
instance_name: "{{ item }}"
with_items:
- "{{ groups['just_created'] }}"
Not pretty but it works well enough and lets me avoid duplicate code in the subsequent included roles.
I am trying to create new users and groups using Ansible playbook. Below is my folder structure.
tree
.
├── create-users.yaml
└── ubuntu
create-users.yaml playbook contains create user and group tasks. Note, I am not having any group (admin_group) and users (Rajini, Kamal) in my target machine, instead they will be created when running the playbook.
---
- name: Create Users & Groups
hosts: target1
gather_facts: false
tasks:
- name: Create Users Task
user:
name: "{{ item }}"
state: present
password: "{{ 'default_user_password' | password_hash('sha512','A512') }}"
shell: /bin/bash
groups: "{{ admin_group }}"
loop:
- Rajini
- Kamal
I have another file called ubuntu to pick group name and password. When running the playbook I am getting below error.
ansible-playbook --vault-id #prompt create-users.yaml -K
BECOME password:
Vault password (default):
PLAY [Create Users & Groups] *****************************************************************************************************************************************************************
TASK [Create Users Task] *********************************************************************************************************************************************************************
fatal: [target1]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'admin_group' is undefined\n\nThe error appears to be in '/home/osboxes/Ansible_Project/web_deployment/Ansible/groups_vars/create-users.yaml': line 6, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - name: Create Users Task\n ^ here\n"}
PLAY RECAP ***********************************************************************************************************************************************************************************
target1 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
admin_group: admin
default_user_password: Password1
Can somebody please help me on this?
Updating Output after getting help from user Moon.
ansible-playbook --vault-id #prompt create-users.yaml -K
BECOME password:
Vault password (default):
PLAY [Create Users & Groups] *****************************************************************************************************************************************************************
TASK [Create Users Task] *********************************************************************************************************************************************************************
changed: [target1] => (item=Rajini)
changed: [target1] => (item=Kamal)
PLAY RECAP ***********************************************************************************************************************************************************************************
target1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ssh Kamal#192.168.0.1
Kamal#192.168.0.1's password:
Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 5.0.0-23-generic x86_64)
Kamal#Ansible_Target1:~$ id
uid=1005(Kamal) gid=1001(admin) groups=1001(admin)
Couple of things:
To use variables from ubuntu file you need specify the vars file in playbook.
To use default_user_password as a variable, remove the quotes '
If you want admin as the users primary group then use group attribute instead. groups on the other hand takes a list and add the users to the listed groups.
And, if the group isn't created yet on the target machine then first create the group using group module.
Playbook after the above changes.
---
- name: Create Users & Groups
hosts: target1
gather_facts: false
vars_files: ubuntu
tasks:
- name: Create group
group:
name: "{{ admin_group }}"
state: present
- name: Create Users Task
user:
name: "{{ item }}"
state: present
password: "{{ default_user_password | password_hash('sha512','A512') }}"
shell: /bin/bash
group: "{{ admin_group }}"
loop:
- Rajini
- Kamal
This will help you make your playbook more dynamic:
create a secret.yml file for storing user password using below command:
ansible-vault create secret.yml
#sample:
user_password: mypass#155
create userlist.yml to specify the list of user and their department:
vim userlist.yml
#sample:
##user matching job role mentioned in create-user.yml playbook will be created
- users:
- name: "user1"
job: "developer"
- name: "user2"
job: "devops"
- name: "user3"
job: "developer"
Now create your playbook as follows:
vim user-create.yml
- hosts: appserver1
vars_files:
- secret.yml
- userlist.yml
tasks:
- name: "create group"
group:
name: "{{ item }}"
loop:
- "dev"
- "devops"
- name: "create user when Job=developer from userlist.yml file with password from secret.yml file and add to secondary group 'dev' "
user:
name: "{{ item['name'] }}"
password: "{{ user_password | password_hash('sha512') }}"
update_password: on_create
groups: "dev"
append: yes
loop: "{{ users }}"
when: "item['job'] == 'developer'"
- name: "create user when Job=devops from userlist.yml file with password from secret.yml file and add to secondary group 'devops' "
user:
name: "{{ item['name'] }}"
password: "{{ user_password | password_hash('sha512') }}"
update_password: on_create
groups: "devops"
append: yes
loop: "{{ users }}"
when: "item['job'] == 'devops'"
Run the playbook
ansible-playbook -i inventory user-create.yml --ask-vault-pass
Things to remember
If you do not specify the update_password: on_create option, Ansible re-sets the user password every time the playbook is run: if the user has changed the password since the last time the playbook was run, Ansible re-sets password.
I've set up some application information in my Ansible group_vars like this:
applications:
- name: app1
- name: app2
- name: app3
- name: app4
settings:
log_dir: /var/logs/app4
associated_files:
- auth/key.json
- name: app5
settings:
log_dir: /var/logs/app5
repo_path: new_apps/app5
I'm struggling to get my head around how I can use these "sub loops".
My tasks for each application are:
Create some folders based purely on the name value
Create a log folder if a settings/log_dir value exists
Copy associated files over, if specified
The syntax for these tasks isn't the problem here, I'm comfortable with those - I just need to know how to access the information from this applications variable. Number 3 in particular seems troublesome to me - I need to loop within a loop.
To debug this, I've been trying to run the following task:
- debug:
msg: "{{ item }}"
with_subelements:
- "{{ applications }}"
- settings
Here's the output:
with_items: I get the error with_items expects a list or a set
with_nested: I can see the top level information (e.g. msg: {{ item }} outputs an array of app1, app2 etc)
with_subelements: I get the error subelements lookup expects a dictionary, got 'None'
It's possible/probable that the way I've set the variable up in the first instance is wrong. If there's a better way to do this, it's not a problem to change it.
You can't use with_subelements because settings is a dictionary, not a list. If you were to restructure your data so that settings is a list, like this:
applications:
- name: app1
- name: app2
- name: app3
- name: app4
settings:
- name: log_dir
value: /var/logs/app4
- name: associated_files
value:
- auth/key.json
- name: app5
settings:
- name: log_dir
value: /var/logs/app5
- name: repo_path
value: new_apps/app5
You could then write something like the following to iterate over each setting for each application:
---
- hosts: localhost
gather_facts: false
vars_files:
- applications.yml
tasks:
- debug:
msg: "set {{ item.1.name }} to {{ item.1.value }} for {{ item.0.name }}"
loop: "{{ applications|subelements('settings', skip_missing=true) }}"
loop_control:
label: "{{ item.0.name }}.{{ item.1.name }} = {{ item.1.value }}"
(I'm using loop_control here just to make the output nicer.)
Using the sample data you posted in applications.yml, this will produce as output:
PLAY [localhost] *********************************************************************
TASK [debug] *************************************************************************
ok: [localhost] => (item=app4.log_dir = /var/logs/app4) => {
"msg": "set log_dir to /var/logs/app4 for app4"
}
ok: [localhost] => (item=app4.associated_files = ['auth/key.json']) => {
"msg": "set associated_files to ['auth/key.json'] for app4"
}
ok: [localhost] => (item=app5.log_dir = /var/logs/app5) => {
"msg": "set log_dir to /var/logs/app5 for app5"
}
ok: [localhost] => (item=app5.repo_path = new_apps/app5) => {
"msg": "set repo_path to new_apps/app5 for app5"
}
PLAY RECAP ***************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
I'm trying to create an Ansible playbook which should create VLANs defined in file vlans.dat on a Cisco Nexus switch only when they don't exist on device.
File vlans.dat contains:
---
vlans:
- { vlan_id: 2, name: TEST }
And Ansible file:
---
- name: Verify and create VLANs
hosts: switches_group
gather_facts: no
vars_files:
- vlans.dat
tasks:
- name: Get Nexus facts
nxos_facts:
register: data
- name: Create new VLANs only
nxos_vlan:
vlan_id: "{{ item.vlan_id }}"
name: "{{ item.name }}"
state: "{{item.state | default('present') }}"
with_items: "{{ vlans }}"
when: item.vlan_id not in data.ansible_facts.vlan_list
In the when statement I'm trying to limit execution only to the case when vlan_id (defined in the file) doesn't exist in the vlan_list gathered by nxos_facts module. Unfortunately it gets executed even when the vlan_id already exists in the vlan_list and I don't know why?
PLAY [Verify and create VLANs]
TASK [Get Nexus facts]
ok: [10.1.1.1]
TASK [Create new VLANs only]
ok: [10.1.1.1] => (item={u'name': u'TEST', u'vlan_id': 2})
TASK [debug]
skipping: [10.1.1.1]
PLAY RECAP
10.1.1.1 : ok=2 changed=0 unreachable=0 failed=0
Can you help me with that or provide some solution what I'm doing wrong here?
It appears you have stumbled upon a side-effect of YAML having actual types. Because in {vlan_id: 2} the 2 is an int but the list is strings. As you might imagine {{ 1 in ["1"] }} is False.
There are two ways out of that situation: make the vlan_id a string via - { vlan_id: "2" } or coerce the vlan_id to a string just for testing the list membership:
when: (item.vlan_id|string) not in data.ansible_facts.vlan_list
I have a use case for a play where the installation path for tomcat changes based on the hostname and the value of a variable. Not sure how to handle this. For example, I have the following inventory:
[servers]
server1
server2
server3
I have a global_var that specifies the type of platform for my install like so:
platform: training
My platform variable could be set to training, production, development
Based on the value of platform and the hostname, my tomcat installation path will be different, so I can't just have:
host_vars/server1.yml
tomcat_path: /somepath1
host_vars/server2.yml
tomcat_path: /somepath2
host_vars/server3.yml
tomcat_path: /somepath3
I'm looking to do something akin too:
server1.yml
tomcat_path: /somepath1
when: "{{ platform }} == training"
tomcat_path: /somepath2
when: "{{ platform }} == production"
tomcat_path: /somepath3
when: "{{ platform }} == development"
How do you handle such a case in ansible?
you could define all possible platform-tomcat_path options in a dictionary variable, and then select the desired combination with several ways.
check below example (with 3 different ways to reference the variables):
- hosts: localhost
gather_facts: false
vars:
tomcat_path: { training: /somepath1, production: /somepath2, development: /somepath3 }
your_selected_mode: development
tasks:
- name: print
debug:
var: tomcat_path.training
- name: print
debug:
var: tomcat_path['production']
- name: print
debug:
var: tomcat_path.{{your_selected_mode}}
you can use the your_selected_mode to select the mode you need.
output:
TASK [print] ********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"tomcat_path.training": "/somepath1"
}
TASK [print] ********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"tomcat_path['production']": "/somepath2"
}
TASK [print] ********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"tomcat_path.development": "/somepath3"
}
if needed to further customize it per host, you can have this tomcat_path variable declaration in the host_vars file and alter it to fit your needs.
You could use conditional imports but it requires you to have one variables file per host and platform:
- hosts: localhost
connection: local
vars_files:
- "vars/{{ inventory_hostname }}_{{ platform }}.yml"
tasks:
- name: echo path
debug: msg="Tomcat path is {{ tomcat_path }}"
You would need to define the variable files vars/server1_training.yml, vars/server2_training.yml, vars/server3_training.yml, vars/server1_production.yml, ....