Latest posts for tag transilience
Ansible recurse and follow quirks
I'm reading Ansible's builtin.file
sources for, uhm, reasons,
and the use of follow
stood out to my eyes. Reading on, not only that. I feel
like the ansible codebase needs a serious review, at least in essential core
modules like this one.
In the file module documentation it says:
This flag indicates that filesystem links, if they exist, should be followed.
In the recursive_set_attributes implementation instead, follow means "follow symlinks to directories", but if a symlink to a file is found, it does not get followed, kind of.
What happens is that ansible will try to change the mode of the symlink, which
makes sense on some operating systems. And it does try to use lchmod
if
present. Buf if not, this happens:
# Attempt to set the perms of the symlink but be
# careful not to change the perms of the underlying
# file while trying
underlying_stat = os.stat(b_path)
os.chmod(b_path, mode)
new_underlying_stat = os.stat(b_path)
if underlying_stat.st_mode != new_underlying_stat.st_mode:
os.chmod(b_path, stat.S_IMODE(underlying_stat.st_mode))
So it tries doing chmod on the symlink, and if that changed the mode of the actual file, switch it back.
I would have appreciated a comment documenting on which systems a hack like this makes sense. As it is, it opens a very short time window in which a symlink attack can make a system file vulerable, and an exception thrown by the second stat will make it vulnerable permanently.
What about follow
following links during recursion: how does it avoid loops?
I don't see a cache of (device, inode)
pairs visited. Let's try:
fatal: [localhost]: FAILED! => {"changed": false, "details": "maximum recursion depth exceeded", "gid": 1000, "group": "enrico", "mode": "0755", "msg": "mode must be in octal or symbolic form", "owner": "enrico", "path": "/tmp/test/test1", "size": 0, "state": "directory", "uid": 1000}
Ok, it, uhm, delegates handling that to the Python stack size. I guess it means
that a ln -s .. foo
in a directory that gets recursed will always fail the
task. Fun!
More quirks
Turning a symlink into a hardlink is considered a noop if the symlink points to the same file:
---
- hosts: localhost
tasks:
- name: create test file
file:
path: /tmp/testfile
state: touch
- name: create test link
file:
path: /tmp/testlink
state: link
src: /tmp/testfile
- name: turn it into a hard link
file:
path: /tmp/testlink
state: hard
src: /tmp/testfile
gives:
$ ansible-playbook test3.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [localhost] ************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [create test file] *****************************************************************************************************************************************************************************************************
changed: [localhost]
TASK [create test link] *****************************************************************************************************************************************************************************************************
changed: [localhost]
TASK [turn it into a hard link] *********************************************************************************************************************************************************************************************
ok: [localhost]
PLAY RECAP ******************************************************************************************************************************************************************************************************************
localhost : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
More quirks
Converting a directory into a hardlink should work, but it doesn't because unlink is used instead of rmdir:
---
- hosts: localhost
tasks:
- name: create test dir
file:
path: /tmp/testdir
state: directory
- name: turn it into a symlink
file:
path: /tmp/testdir
state: hard
src: /tmp/
force: yes
gives:
$ ansible-playbook test4.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [localhost] ************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [create test dir] ******************************************************************************************************************************************************************************************************
changed: [localhost]
TASK [turn it into a symlink] ***********************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "gid": 1000, "group": "enrico", "mode": "0755", "msg": "Error while replacing: [Errno 21] Is a directory: b'/tmp/testdir'", "owner": "enrico", "path": "/tmp/testdir", "size": 0, "state": "directory", "uid": 1000}
PLAY RECAP ******************************************************************************************************************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
More quirks
This is hard to test, but it looks like if source and destination are hardlinks to the same inode numbers, but on different filesystems, the operation is considered a successful noop: https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/file.py#L821
It should probably be something like:
if (st1.st_dev, st1.st_ino) == (st2.st_dev, st2.st_ino):
Ansible blockinfile oddity
I was reading Ansible's blockinfile sources for, uhm, reasons, and the code flow looked a bit odd.
So I checked what happens if a file has spurious block markers.
Give this file:
$ cat /tmp/test.orig
line0
# BEGIN ANSIBLE MANAGED BLOCK
line1
# END ANSIBLE MANAGED BLOCK
line2
# END ANSIBLE MANAGED BLOCK
line3
# BEGIN ANSIBLE MANAGED BLOCK
line4
And this playbook:
$ cat test.yaml
---
- hosts: localhost
tasks:
- name: test blockinfile
blockinfile:
block: NEWLINE
path: /tmp/test
You get this result:
$ cat /tmp/test
line0
# BEGIN ANSIBLE MANAGED BLOCK
line1
# END ANSIBLE MANAGED BLOCK
line2
# BEGIN ANSIBLE MANAGED BLOCK
NEWLINE
# END ANSIBLE MANAGED BLOCK
line4
I was hoping that I was reading the code incorrectly, but it turns out that Ansible's blockinfile matches the last pair of begin-end markers it finds, in whatever order it finds them.