101

I can do that with shell using combination of getent and awk like this:

getent passwd $user | awk -F: '{ print $6 }'

For the reference, in Puppet I can use a custom fact, like this:

require 'etc'

Etc.passwd { |user|

   Facter.add("home_#{user.name}") do
      setcode do
         user.dir
      end
   end

}

which makes the user's home directory available as a home_<user name> fact.

How do I get the home directory of an arbitrary remote user?

Kevin C
  • 4,851
  • 8
  • 30
  • 64
Adam Ryczkowski
  • 7,592
  • 13
  • 42
  • 68

10 Answers10

92

Ansible (from 1.4 onwards) already reveals environment variables for the user under the ansible_env variable.

- hosts: all
  tasks:
    - name: debug through ansible.env
      debug: var=ansible_env.HOME

Unfortunately you can apparently only use this to get environment variables for the connected user as this playbook and output shows:

- hosts: all
  tasks:
    - name: debug specified user's home dir through ansible.env
      debug: var=ansible_env.HOME
      become: true
      become_user: "{{ user }}"

    - name: debug specified user's home dir through lookup on env
      debug: var=lookup('env','HOME')
      become: true
      become_user: "{{ user }}"

OUTPUT:

vagrant@Test-01:~$ ansible-playbook -i "inventory/vagrant" env_vars.yml -e "user=testuser"

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.0.30]

TASK: [debug specified user's home dir through ansible.env] *******************
ok: [192.168.0.30] => {
    "var": {
        "/home/vagrant": "/home/vagrant"
    }
}

TASK: [debug specified user's home dir through lookup on env] *****************
ok: [192.168.0.30] => {
    "var": {
        "/home/vagrant": "/home/vagrant"
    }
}

PLAY RECAP ********************************************************************
192.168.0.30               : ok=3    changed=0    unreachable=0    failed=0

As with anything in Ansible, if you can't get a module to give you what you want then you are always free to shell out (although this should be used sparingly as it may be fragile and will be less descriptive) using something like this:

- hosts: all
  tasks:
    - name: get user home directory
      shell: >
             getent passwd {{ user }}  | awk -F: '{ print $6 }'
      changed_when: false
      register: user_home

    - name: debug output
      debug:
        var: user_home.stdout

There may well be a cleaner way of doing this and I'm a little surprised that using become_user to switch to the user specified doesn't seem to affect the env lookup but this should give you what you want.

Ken Williams
  • 22,756
  • 10
  • 85
  • 147
ydaetskcoR
  • 53,225
  • 8
  • 158
  • 177
  • I know that, but it works only for the current user. I want to get home directory of any other user (for e.g. configuration of shared workstations) – Adam Ryczkowski Oct 26 '15 at 10:48
  • 1
    Looks like `become_user` doesn't update the `env` so I'm not sure if there's a cleaner approach than just shelling out but that should work – ydaetskcoR Oct 26 '15 at 11:46
  • This will not work all around for hosts that are connected to LDAP or some other directory server. For that you'll need to use `getent` instead. – TrinitronX Nov 18 '15 at 17:47
  • `sudo_user` and `become_user` are not as portable across different versions of Ansible also. `getent` still seems to me to be the best solution here. – TrinitronX Nov 18 '15 at 19:40
  • better make it: egrep "^{{ user }}" /etc/passwd | awk -F: '{ print $6 }' – Alien Life Form Apr 13 '16 at 09:00
  • even better: egrep "^{{ user }}:" /etc/passwd | awk -F: '{ print $6 }' - imagine for example you have two users `mike` and `mike2` – TNT Jun 07 '16 at 07:18
  • @TNT (and AlienLifeForm) I've updated my answer to include your feedback. Thanks for spotting those edge cases. – ydaetskcoR Jun 07 '16 at 10:26
  • 5
    Tried this today (ansible 2.5). `ansible_env.HOME` does return the HOME of the remote user (but it is not affected by `become`). However `lookup('env','HOME')` returns the HOME of the user running the playbook at the controller. – jsantander Apr 18 '18 at 05:21
  • Yes, using the shell is always an option, but our team feels it's better to use an Ansible module when provided, such as the user module or the [getent module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/getent_module.html). – reinierpost Mar 29 '21 at 08:47
  • @reinierpost yep me too. The `getent` module didn't exist at the time of writing and the `user` module has added this information too. I've upvoted both of the answers below this that mention those modules as they are better options using functionality made available since this was written 5 and a half years ago. The OP should probably consider switching the accepted answer to the `user` module answer I think. – ydaetskcoR Mar 29 '21 at 09:02
  • Thanks! I thought that might be the case. I've had this happen to my own answers, too ... – reinierpost Mar 29 '21 at 09:23
  • Link is dead. New link: https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#how-do-i-access-shell-environment-variables – Andrey Bienkowski Mar 02 '22 at 03:51
62

I think there are several answers given here that would work, but I thought I'd show that you can get this from the ansible user module, by registering it as a variable.

- user:
    name: www-data
    state: present
  register: webserver_user_registered

Note: it will create the user if it doesn't exist...

So we can use debug to show the values of that var, including the path...

- debug:
    var: webserver_user_registered

TASK [wordpress : debug] ******************
ok: [wordpresssite.org] => {
    "webserver_user_registered": {
        "append": false,
        "changed": false,
        "comment": "www-data",
        "failed": false,
        "group": 33,
        "home": "/var/www",      <<------ this is the user home dir
        "move_home": false,
        "name": "www-data",
        "shell": "/usr/sbin/nologin",
        "state": "present",
        "uid": 33
    }
}

And you can use those properties in other modules like this;

- file:
    name: "{{ webserver_user_registered.home }}/.wp-cli"
    state: directory
Sean Saleh
  • 460
  • 5
  • 17
Tom
  • 3,324
  • 1
  • 31
  • 42
  • 5
    This should be an accepted correct answer as this is the only cross-platform solution which is also quite elegant. `getent` solution even with ansible getent module doesn't work on MacOS (at least High Sierra) as the non-system user's info removed from the passwd database. And the solution in the accepted answer works only for linux managed host. – Drew Jun 03 '18 at 00:51
  • I meant it's "quite elegant" because it's short and you getting the object or even list of objects with nicely named properties like `.name` and `.home`. – Drew Jun 03 '18 at 01:07
  • 3
    I like this but I worry that the `user` module will create the user if it doesn't exist instead of failing. That would be a seriously odd case and I guess you could do something like use a fail task with a `when: user.changed` but it won't undo the creation of the user. I guess you could wrap it in a block and rescue the failure with a command that removed the user and then add another failure in the rescue block? Kinda awkward :/ – ydaetskcoR Jun 19 '18 at 16:41
  • 1
    @ydaetskcoR Yeah, the default for `state` is `present`, so as written, it will create the user if it doesn't exist. However it doesn't make any sense to get the home directory of a non-existing user, so if creating it would cause problems, then you would have to guard for that. – Tom Jun 19 '18 at 20:31
  • 3
    This is the most elegant way of doing the home directory lookup on a pre-existing linux user. – aidanmelen Jan 20 '19 at 03:11
  • 3
    @Tom : if you add `check_mode: yes` then you will not risk creating (or changing) the user. – Tomáš Pospíšek Aug 10 '22 at 16:38
40

Ansible 1.8 introduced the getent module. It registers the getent result as a host fact—in this case, it's getent_passwd.

examples:

Print the home folder for a given user:

---

- getent:
    database: passwd
    key: "{{ user }}"
    split: ":"

- debug:
    msg: "{{ getent_passwd[user][4] }}"

Accumulate a lookup table (user_homes), leveraging set_fact and the Jinja2 combine() filter:

---

- assert:
    that:
      - user_name is defined

- when: user_homes is undefined or user_name not in user_homes
  block:
    - name: getent
      become: yes
      getent:
        database: passwd
        key: "{{ user_name }}"
        split: ":"

    - name: set fact
      set_fact:
        "user_homes": "{{ user_homes | d({}) | combine({user_name: getent_passwd[user_name][4]}) }}"

Would be better with a custom fact module though.

Jakuje
  • 24,773
  • 12
  • 69
  • 75
masu
  • 1,919
  • 21
  • 25
  • [`getent`](http://docs.ansible.com/ansible/latest/getent_module.html) was introduced in Ansible 1.8 – myrdd Sep 03 '18 at 13:26
  • `msg: "{{ getent_passwd[user][4] }}"` is wrong, it gives: `template error while templating string: expected name or number. String: {{ getent_passwd.[user][4] }}. expected name or number`. The correct is: `msg: "{{ getent_passwd.user[4] }}"` – Pavel Tankov Jul 21 '23 at 08:58
14

The Problem

The lookup() or ENV var methods for finding an arbitrary user's home sadly won't work reliably with Ansible because it runs as the user specified with --user=REMOTE_USER, and optionally with sudo (if sudo: yes in playbook or --sudo passed). These two run modes (sudo or no sudo) will change the shell environment that Ansible is running within, and even then you will be limited to the user specified as -u REMOTE_USER or root.

You could try to use sudo: yes, and sudo_user: myarbitraryuser together... however due to a bug in certain versions of Ansible you may see that it does not behave as it should. If you are on Ansible >= 1.9, you can use become: true, and become_user: myarbitraryuser instead. However, this means that the playbooks and roles you write will not work on previous versions of Ansible.

If you are looking for a portable way to get a user's home dir that also will work with LDAP or some other directory service, use getent.

Ansible getent Example

Create a simple playbook named: playbooks/ad-hoc/get-user-homedir.yml

- hosts: all
  tasks:
    - name:
      shell: >
        getent passwd {{ user }} | cut -d: -f6
      changed_when: false
      register: user_home

    - name: debug output
      debug: var=user_home.stdout

Run it with:

ansible-playbook -i inventory/racktables.py playbooks/ad-hoc/get-user-homedir.yml -e "user=someuser"
TrinitronX
  • 4,959
  • 3
  • 39
  • 66
  • getent is not on mac osx 10.12.6 – AnneTheAgile Sep 06 '17 at 17:26
  • 1
    @AnneTheAgile: You're right, looks like OSX `10.12.6` does not have this utility.Alternatives are ` dscacheutil -q user -a name {{ user }}`, `sudo dscl . -ls /Users` to list users, and `dscl . -read /Users/{{ user }}` to dump information about a user. For single-user mode users and groups, there's also always `/etc/passwd` and `/etc/group` because OSX at least is Unix compatible with those files when run in single-user mode. Other users are in `opendirectoryd` as the comment at the top of those files states. – TrinitronX Oct 06 '17 at 23:30
  • 1
    @AnneTheAgile: Alternatively, it looks like someone created a `getent` command line utility that you can [compile and use on OSX here](https://github.com/petere/getent-osx). `git clone https://github.com/petere/getent-osx.git && cd getent-osx` `make ; make install` – TrinitronX Oct 06 '17 at 23:32
  • its less about software reliability - its more about predictable behavior. when a certain function works as it was designed then its fine. but if you expect something different then there is a discrepancy between you and the design. maybe the design is not consistent and thus it is likely inviting for the coders to trap into such not so evident pitfalls that arise from those lack of consistency. – Alexander Stohr Jun 23 '20 at 12:34
4

I know this is quite old thread, but I think this is a bit simpler way for getting the users home directory

- name: Get users homedir
  local_action: command echo ~
  register: homedir

On Linux (or Unix) systems the tilde-sign points to the users home directory.

Matti
  • 147
  • 5
  • Using echo ~username is probably one of the more portable ways to accomplish what the original question asked. One big advantage is that there is no need to mess around with become using this method. – Brian Aker May 02 '19 at 21:07
  • "~username" wont fail if those subdir fails to resolve but only return a string that is identical to the input query – Alexander Stohr Jun 23 '20 at 09:56
4

I think it's best to do this 'natively' in Ansible rather than call an external command: Basically @Tom's answer with user: combined with @Tomáš Pospíšek's comment on that answer to prevent the user being created if it doesn't already exist:

- ansible.builtin.user:
    name: www-data
    state: present
  register: user_info
  check_mode: true  # Important, otherwise user will be created

Now interrogate user_info: The changed attribute will tell you if the user would have been created - i.e. it doesn't yet exist. If changed is not set (so user already exists), the home directory will be in user_info.home,

- ansible.builtin.debug:
    var: user_info.home

Alternatively if it's not guaranteed that the user already exists you might find one of the following helpful, using the changed attribute to steer your actions,

- ansible.builtin.debug:
    var: user_info.home
  when:
    not user_info.changed

- ansible.builtin.debug:
    msg: "{% if user_info.changed|bool %}user doesn't exist{% else %}{{ user_info.home }}{% endif %}"

- ansible.builtin.fail:
    msg: "User doesn't exist. Create user before using this playbook."
  when: user_info.changed
Andrew Richards
  • 1,392
  • 11
  • 18
3

Every answer mentions about how to print the home directory details while running the playbook and displaying it on screen using debug and var.

Adapting to @TrinitronX answer

An additional information on using this information to a new task.

I have a list of users whose home directory needs to be extracted. So I have added the user details to a list

- name: Get home directory
  shell: >
         getent passwd {{ item.user }} | cut -d: -f6
  changed_when: false
  with_items:
   - "{{java}}"
  register: user_home

Here this step will loop through all user list and will register that details to user_home. And this will be in the form of an array.

Then next step is to use this information to a new task, which is say for example sourcing a file into bash profile. This is just an example and can be any scenario, but method will remain the same.

- name: Set Java home in .bash_profile
  lineinfile: path="{{ item.stdout }}/.bash_profile" regexp='^source "{{ java_dir }}/.bash_profile_java"' line='source "{{ java_dir }}/.bash_profile_java"' state=present
  with_items:
   - "{{ user_home.results }}"
  loop_control:
    label: "{{ item.stdout }}"

I have set a fact for java_dir to /usr/java/latest in the same playbook.

Array user_home.results will contain the details of the Get home directory task. Now we loop through this array and take out the stdout value which contains the home directory path.

I have put loop_control for printing the home directory only, else it will print the entire array.

By this process, we can ensure that if n number of users are there, we can follow this method and all will be taken care.

Note: I have started to learn the Ansible, in case if any terminology I have used is wrong, please excuse. I have spend some time for figuring out on how to do this and thought of sharing the same.

nirmalraj17
  • 494
  • 7
  • 20
  • 2
    getent is not portable. OSX is the most obvious example of where you will not find getentt. – Brian Aker May 02 '19 at 21:09
  • using "cut" instead of "awk" is helpful in cases where awk is not installed. cut is much more likely to be present in the major amount of cases so i would set preference on it. – Alexander Stohr Jun 23 '20 at 09:51
2

I came to this thread because i needed to print the PGDATA env variable from the postgres user, i have not found how to do it more "natively" in ansible, but i ended having this that works:

    - name: Find postgresql data directory
        shell: 'echo $PGDATA'
        become: yes
        become_user: postgres
        become_flags: "-i "
        register: pgdata_dir

Then i can reference that in another job using "{{ pgdata_dir.stdout }}"

Gotxi
  • 41
  • 1
  • good point using the "-i" option. it might have some extra effects on shell options like abort behavior or the prompt - but as long as it does not turn out to interfere in a destructive way you will just use it to serve your particular job. – Alexander Stohr Jun 23 '20 at 09:49
1

There is no easy way to do this in Ansible at this moment and that's why you should add your votes to this issue

https://github.com/ansible/ansible/issues/15901

While you can use this workaround: https://stackoverflow.com/a/33343455/99834 you should not forget to send the feedback that you want this to be easy to be used.

Community
  • 1
  • 1
sorin
  • 161,544
  • 178
  • 535
  • 806
  • The issue is still not resolved and Ansible has closed the issue and locked it from comments and votes... :/ – Hubro May 02 '20 at 12:25
0

You can use expanduser.

For instance, while looping over a user list:

- name: Deploys .bashrc
  template:
    src: bashrc.j2
    dest: "{{ '~' + item | expanduser }}/.bashrc"
    mode: 0640
    owner: "{{ item }}"
    group: "{{ item }}"
  with_items: user_list
leucos
  • 17,661
  • 1
  • 44
  • 34