I'm using Ansible to add a user to a variety of servers. Some of the servers have different UNIX groups defined. I'd like to find a way for Ansible to check for the existence of a group that I specify, and if that group exists, add it to a User's secondary groups list (but ignore the statement it if the group does not exist).
Any thoughts on how I might do this with Ansible?
Here is my starting point.
Command
ansible-playbook -i 'localhost,' -c local ansible_user.yml
ansible_user.yml
---
- hosts: all
user: root
become: yes
vars:
password: "!"
user: testa
tasks:
- name: add user
user: name="{{user}}"
state=present
password="{{password}}"
shell=/bin/bash
append=yes
comment="test User"
Updated: based on the solution suggested by #udondan, I was able to get this working with the following additional tasks.
- name: Check if user exists
shell: /usr/bin/getent group | awk -F":" '{print $1}'
register: etc_groups
- name: Add secondary Groups to user
user: name="{{user}}" groups="{{item}}" append=yes
when: '"{{item}}" in etc_groups.stdout_lines'
with_items:
- sudo
- wheel
The getent module can be used to read /etc/group
- name: Determine available groups
getent:
database: group
- name: Add additional groups to user
user: name="{{user}}" groups="{{item}}" append=yes
when: item in ansible_facts.getent_group
with_items:
- sudo
- wheel
Do you have anything to identify those different host types?
If not, you first need to check which groups exist on that host. You can do this with the command getent group | cut -d: -f1 which will output one group per line.
You can use this as separate task like so:
- shell: getent group | cut -d: -f1
register: unix_groups
The registered result then can be used later when you want to add the user group
- user: ...
when: "'some_group' in unix_groups.stdout_lines"
This is how I'm dealing with this in my playbooks. The idea is simple - to take a list of existing groups and find an intersection between groups a user wants to be member of and groups that exist:
- name: Get Existing Groups
getent:
database: group
- name: Configure Users
user:
name: username
groups: "{{ ['wheel', 'docker', 'video'] | intersect(ansible_facts['getent_group'] | list) }}"
The getent module outputs the 'getent_group' Ansible fact containing all existing groups with their details. By piping it to | list filter I get plain list of group names. The intersect filter finds what's common between two lists.
One of the advantages of this solution is that I don't have to use append parameter of the user module. This way user may be correctly removed from groups that I don't want it to be member of anymore.
I had the similar requirement, then I did the followings,
ansible [core 2.12.2]
---
- hosts: all
become: yes
become_user: root
tasks:
- ansible.builtin.user:
name: test1
password: "&&**%%^^"
uid: 1234
shell: /bin/bash
- ansible.builtin.shell:
"cat /etc/group| grep -o sysadmin"
register: output
#you can omit the debug part
- debug:
var: output
- name: assign user the group
ansible.builtin.shell:
"usermod -G sysadmin test1"
when: "'sysadmin' in output.stdout_lines"
Update: Thanks for the suggestion #Jeter-work
- hosts: localhost
tasks:
- name: Get all groups
ansible.builtin.getent:
database: group
split: ':'
- debug:
var: ansible_facts.getent_group
Related
I'm writing a health-check playbook, and when a host is clustered (VCS), I want to make sure all cluster Service Groups are running.
The output of hastatus looks like this:
[root#node1 ~]# hastatus -sum
-- SYSTEM STATE
-- System State Frozen
A node1 RUNNING 0
A node2 RUNNING 0
-- GROUP STATE
-- Group System Probed AutoDisabled State
B ClusterService node1 Y N ONLINE
B ClusterService node2 Y N OFFLINE
B NFSExport node1 Y N ONLINE
B NFSExport node2 Y N OFFLINE
B Database node1 Y N ONLINE
B Database node2 Y N OFFLINE
B Application node1 Y N OFFLINE
B Application node2 Y N ONLINE
[root#node1 ~]#
A Service Group can run on any cluster node, and the status of every service group is reported for every cluster node, so the actual number of services groups is (servicegroups / nodes).
I've tried with and without the double braces {{ }} , but no matter what, the last debug task always produces a divide by zero error.
Any help would be appreciated.
# START OF BLOCK
- name: Check cluster status
block:
- name: How many cluster nodes?
shell: hastatus -sum|grep "^A"|wc -l
register: numnodes
- name: How many running cluster nodes?
shell: hastatus -sum|grep "^A"|grep "RUNNING"|wc -l
register: numrunningnodes
- name: report if not all nodes are running
debug:
msg: "ACTION: Not all cluster nodes are running!"
when: numnodes.stdout != numrunningnodes.stdout
# The number of cluster Service Groups == totalsgs / numnodes
- name: How many SGs ("B" lines)?
shell: hastatus -sum|grep "^B"|wc -l
register: totalsgs
- name: How many running SGs?
shell: hastatus -sum|grep "^B"|grep "RUNNING"|wc -l
register: runningsgs
- name: Is everything running somewhere?
debug:
msg: "ACTION: Not all SGs are running!"
when: {{ runningsgs.stdout|int }} != {{ totalsgs.stdout|int / numnodes.stdout|int }}
It's the second worst Ansible you can write. If you throw in include_role with when and loop on top of that, you will have the worst worst.
Ansible is not designed to be a good algorithmic language. It's really good at doing side-effects and juggling inventories, but it's terrible at doing math. You can do it, but it will be unreadable, non-debuggable, non-testable, and you will have your variables littered with global variables (which will for sure bite you later when you do not expect it).
How to do it right? The best best way is to write own module. Which is easier than you think (if you know Python).
The second best (which may be even better for small projects than custom module) is to use script or command module.
Just shovel data as input to the stdin script, and get well processed data back from stdout. The main trick is to produce stdout output in json format and parse it with |from_json filter.
This is example of use of command to parse data:
- name: Get data from somewhere
shell: hastatus -sum
changed_when: false
register: hastatus_cmd
- name: Process data
delegate_to: localhost
command:
cmd: process_hastatus_output.py
stdin: '{{ hastatus_cmd.stdout }}'
changed_when: false
register: hastatus_data
- name: Use it
command: ...?...
when: hastatus.running != hastatus.nodes
vars:
hastatus: '{{ hastatus_data.stdout|from_json }}'
For process_hastatus_output.py you can write tests, you can run them without ansible to check of edge cases, and you'll have the beautiful, cosy language to transform your data.
Or, you can do it in mix of Jinja and Ansible, causing irreparable harm to yourself, and everyone reading your code later.
I am running Ansible version 2.7 on Centos7 using the network_cli connection method.
I have a playbook that:
Instructs a networking device to pull in a new firmware image via TFTP
Instructs the networking device to calculate the md5 hash value
Stores the output of the calculation in .stdout
Has a conditional When: statment that checks for a given md5 value in the .stdout before proceeding with the task block.
No matter what md5 value I give, it always runs the task block.
The conditional statement is:
when: '"new_ios_md5" | string in md5_result.stdout'
Here is the full playbook:
- name: UPGRADE SUP8L-E SWITCH FIRMWARE
hosts: switches
connection: network_cli
gather_facts: no
vars_prompt:
- name: "compliant_ios_version"
prompt: "What is the compliant IOS version?"
private: no
- name: "new_ios_bin"
prompt: "What is the name of the new IOS file?"
private: no
- name: "new_ios_md5"
prompt: "What is the MD5 value of the new IOS file?"
private: no
- name: "should_reboot"
prompt: "Do you want Ansible to reboot the hosts? (YES or NO)"
private: no
tasks:
- name: GATHER SWITCH FACTS
ios_facts:
- name: UPGRADE IOS IMAGE IF NOT COMPLIANT
block:
- name: COPY OVER IOS IMAGE
ios_command:
commands:
- command: "copy tftp://X.X.X.X/45-SUP8L-E/{{ new_ios_bin }} bootflash:"
prompt: '[{{ new_ios_bin }}]'
answer: "\r"
vars:
ansible_command_timeout: 1800
- name: CHECK MD5 HASH
ios_command:
commands:
- command: "verify /md5 bootflash:{{ new_ios_bin }}"
register: md5_result
vars:
ansible_command_timeout: 300
- name: CONTINUE UPGRADE IF MD5 HASH MATCHES
block:
- name: SETTING BOOT IMAGE
ios_config:
lines:
- no boot system
- boot system flash bootflash:{{ new_ios_bin }}
match: none
save_when: always
- name: REBOOT SWITCH IF INSTRUCTED
block:
- name: REBOOT SWITCH
ios_command:
commands:
- command: "reload"
prompt: '[confirm]'
answer: "\r"
vars:
ansible_command_timeout: 30
- name: WAIT FOR SWITCH TO RETURN
wait_for:
host: "{{inventory_hostname}}"
port: 22
delay: 60
timeout: 600
delegate_to: localhost
- name: GATHER ROUTER FACTS FOR VERIFICATION
ios_facts:
- name: ASSERT THAT THE IOS VERSION IS CORRECT
assert:
that:
- compliant_ios_version == ansible_net_version
msg: "New IOS version matches compliant version. Upgrade successful."
when: should_reboot == "YES"
when: '"new_ios_md5" | string in md5_result.stdout'
when: ansible_net_version != compliant_ios_version
...
The other two conditionals in the playbook work as expected. I cannot figure out how to get ansible to fail the when: '"new_ios_md5" | string in md5_result.stdout' conditional and stop the play if the md5 value is wrong.
When you run the play with debug output the value of stdout is:
"stdout": [
".............................................................................................................................................Done!",
"verify /md5 (bootflash:cat4500es8-universalk9.SPA.03.10.02.E.152-6.E2.bin) = c1af921dc94080b5e0172dbef42dc6ba"
]
You can clearly see the calculated md5 in the string but my conditional doesn't seem to care either way.
Does anyone have any advice?
When you write:
when: '"new_ios_md5" | string in md5_result.stdout'
You are looking for the literal string "new_ios_md5" inside the variable md5_result.stdout. Since you actually want to refer to the new new_ios_md5 variable, you ened to remove the quotes around it:
when: 'new_ios_md5 | string in md5_result.stdout'
Credit goes to zoredache on reddit for the final solution:
BTW, you know that for most of the various networking commands ios_command the results come back as a list right? So you need to index into the list relative to the command you run.
Say you had this. task
ios_command:
commands:
- command: "verify /md5 bootflash:{{ new_ios_bin }}"
- command: show version
- command: show config
register: results
You would have output in the list like this.
# results.stdout[0] = verify
# results.stdout[1] = show version
# results.stdout[2] = show config
So the correct conditional statement would be:
when: 'new_ios_md5 in md5_result.stdout[0]'
I have 4 VM's which all have a different ssh users.
In order to use Ansible to manipulate the Vms I set my file /etc/ansible/hosts to this:
someserver1 ansible_ssh_host=123.123.123.121 ansible_ssh_port=222 ansible_ssh_user=someuser1 ansible_ssh_pass=somepass1
someserver2 ansible_ssh_host=123.123.123.122 ansible_ssh_port=22 ansible_ssh_user=someuser2 ansible_ssh_pass=somepass2
someserver3 ansible_ssh_host=123.123.123.123 ansible_ssh_port=222 ansible_ssh_user=someuser3 ansible_ssh_pass=somepass3
someserver4 ansible_ssh_host=123.123.123.124 ansible_ssh_port=222 ansible_ssh_user=someuser4 ansible_ssh_pass=somepass4
Lets say i have this playbook which only does an ls inside the /root folder:
- name: root access test
hosts: all
tasks:
- name: ls the root folder on my Vms
become: yes
become_user: root
become_method: su
command: chdir=/root ls -all
Using this call ansible-playbook -v my-playbook.yml --extra-vars='ansible_become_pass=xxx-my-secret-root-password-for-someserver1' i can become root on one of my machines but not on all.
How is it possible to supply somepass2, somepass3 and somepass4?
Why not just define ansible_become_pass as an in-line host variable in the inventory like you already have done with the SSH password? So your inventory would now look like this:
someserver1 ansible_ssh_host=123.123.123.121 ansible_ssh_port=222 ansible_ssh_user=someuser1 ansible_ssh_pass=somepass1 ansible_become_pass=somesudopass1
someserver2 ansible_ssh_host=123.123.123.122 ansible_ssh_port=22 ansible_ssh_user=someuser2 ansible_ssh_pass=somepass2 ansible_become_pass=somesudopass2
someserver3 ansible_ssh_host=123.123.123.123 ansible_ssh_port=222 ansible_ssh_user=someuser3 ansible_ssh_pass=somepass3 ansible_become_pass=somesudopass3
someserver4 ansible_ssh_host=123.123.123.124 ansible_ssh_port=222 ansible_ssh_user=someuser4 ansible_ssh_pass=somepass4 ansible_become_pass=somesudopass4
Or, if your login password and sudo password are the same then simply add:
ansible_become_pass='{{ ansible_ssh_pass }}'
Either to an all group_vars file or in an in-line group vars block in the inventory file like this:
[all:vars]
ansible_become_pass='{{ ansible_ssh_pass }}'
I'm trying SaltStack after using Puppet for a while, but I can't understand their use of the word "state".
My understanding is that, for example, a light switch has 2 possible states - on or off. When I write my SLS configuration I am describing what state a server should be in. When I ask SaltStack to provision a server I issue the command salt '*' state.highstate. I understand that a server can be in a highstate (as described in my config) or not. All good so far.
But this page describes other states. It describes lowstate, highstate and overstate (amongst others) as layers. Does this mean a server passes through several states to get to a highstate? Or all states are maintained simultaneously as layers? Or can I configure multiple possible states in my SLS and have SaltStack switch between them? Or are they just layers to SaltStack that have 'state' in the name and I'm confused?
I'm probably missing something obvious, if anyone can nudge me in the right direction I think a lot of the documentation will become clear to me!
Here, top.sls wihch contain,
# cat top.sls
base:
'*':
- httpd_require
and,
# cat httpd_require.sls
install_httpd:
pkg.installed:
- name: httpd
service.running:
- name: httpd
- enable: True
- require:
- file: install_httpd
file.managed:
- name: /var/www/html/index.html
- source: salt://index1.html
- user: root
- group: root
- mode: 644
- require:
- pkg: install_httpd
High state:
We can see all the aspects of high state system while working with state files( .sls), There are three specific components.
High data:
SLS file:
High State
Each individual State represents a piece of high data(pkg.installed:'s block), Salt will compile all relevant SLS inside the top.sls, When these files are tied together using includes, and further glued together for use inside an environment using a top.sls file, they form a High State.
# salt 'remote_minion' state.show_highstate --out yaml
remote_minion:
install_httpd:
__env__: base
__sls__: httpd_require
file:
- name: /var/www/html/index.html
- source: salt://index1.html
- user: root
- group: root
- mode: 644
- require:
- pkg: install_httpd
- managed
- order: 10002
pkg:
- name: httpd
- installed
- order: 10000
service:
- name: httpd
- enable: true
- require:
- file: install_httpd
- running
- order: 10001
First, an order is declared, All States that are set to be first will have their order adjusted accordingly. Salt will then add 10000 to the last defined number (which is 0 by default), and add any States that are not explicitly ordered.
Salt will also add some variables that it uses internally, to know which environment (__env__) to execute the State in, and which SLS file (__sls__) the State declaration came from, Remember that the order is still no more than a starting point; the actual High State will be executed based first on requisites, and then on order.
"In other words, "High" data refers generally to data as it is seen by the user."
Low States:
""Low" data refers generally to data as it is ingested and used by Salt."
Once the final High State has been generated, it will be sent to the State compiler. This will reformat the State data into a format that Salt uses internally to evaluate each declaration, and feed data into each State module (which will in turn call the execution modules, as necessary). As with high data, low data can be broken into individual components:
Low State
Low chunks
State module
Execution module(s)
# salt 'remote_minion' state.show_lowstate --out yaml
remote_minion:
- __env__: base
__id__: install_httpd
__sls__: httpd_require
fun: installed
name: httpd
order: 10000
state: pkg
- __env__: base
__id__: install_httpd
__sls__: httpd_require
enable: true
fun: running
name: httpd
order: 10001
require:
- file: install_httpd
state: service
- __env__: base
__id__: install_httpd
__sls__: httpd_require
fun: managed
group: root
mode: 644
name: /var/www/html/index.html
order: 10002
require:
- pkg: install_httpd
source: salt://index1.html
state: file
user: root
Together, all this comprises a Low State. Each individual item is a Low Chunk. The first Low Chunk on this list looks like this:
- __env__: base
__id__: install_httpd
__sls__: httpd_require
fun: installed
name: http
order: 10000
state: pkg
Each low chunk maps to a State module (in this case, pkg) and a function inside that State module (in this case, installed). An ID is also provided at this level (__id__). Salt will map relationships (that is, requisites) between States using a combination of State and __id__. If a name has not been declared by the user, then Salt will automatically use the __id__ as the name.Once a function inside a State module has been called, it will usually map to one or more execution modules which actually do the work.
salt '\*' state.highstate
'*' refers to all the minions connected to the master.
'state.highstate' is used to run all modules / scripts mentioned in top.sls defined in master
To invoke a specific module / script on all minions, use the following salt command where the state information is defined in state.sls for apache in the example given below.
salt '\*' state.sls apache
To invoke the above salt call only on a specific minion, use the below command.
salt 'minion-name' state.sls apache
I don't know all levels of state, but when you run :
salt '*' state.highstate
Saltstack apply the states you provide in /srv/salt/top.sls.
If you write nothing in it, you can't apply an highstate.
You can apply other state with this command :
salt '*' state.sls state.example
A highstate is just the collection of states that is applied to your server. There is a process in the background where Salt's "state compiler" goes through several stages preparing the data in order to produce the highstate, but you don't really need to worry about those.
Things like the lowstate can help with debugging, but aren't necessary for day to day usage. The highstate is only applied once.
How can I get the current role name in an ansible task yaml file?
I would like to do something like this
---
# role/some-role-name/tasks/main.yml
- name: Create a directory which is called like the current role name
action: file
path=/tmp/"{{ role_name }}"
mode=0755
state=directory
The result of this task should be a directory /tmp/some-role-name on the server
The simplest way is to just use the following
{{role_path|basename}}
As of Ansible 2.2:
{{role_name}}
As of Ansible 2.1:
{{role_path|basename}}
Older versions:
There is no way to do this in the current version of Ansible, here are a couple options that might work for you instead:
1) Use set_fact to set a role_name var to the name the of role as the first task in your tasks/main.yml file
- set_fact: role_name=some-role-name
2) Pass a parameter to your role that has the name
- roles:
- role: some-role-name
role_name: some-role-name
See this post:
To get the role directory:
role_dir: "{{ lookup('pipe', 'pwd') | dirname }}"
To get the role name:
role_name: "{{ lookup('pipe', 'pwd') | dirname | basename }}"
As of Ansible 2.8 there is ansible_play_name which contains the name of the currently executed play.
https://github.com/ansible/ansible/pull/48562
https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html