使用Ansible如何从目录中删除未管理的文件?

53

我希望能够递归复制一个目录,并将其中所有的 .j2 文件作为模板进行渲染。目前,我正在使用以下几行代码:

- template: >
            src=/src/conf.d/{{ item }}
            dest=/dest/conf.d/{{ item|replace('.j2','') }}
  with_lines: find /src/conf.d/ -type f -printf "%P\n"

现在我正在寻找一种方法,可以从这个目录中删除未管理的文件。例如,如果我从/src/conf.d/中删除一个文件/模板,我希望Ansible也会从/dest/conf.d/中删除它。

有没有什么办法可以做到这一点?我尝试了使用rsync --delete进行修改,但是我遇到了一个问题,即模板的后缀.j2被删除了。

9个回答

55

假设在上方定义了一个名为“managed_files”的列表变量,我会这样做。

- shell: ls -1 /some/dir
  register: contents

- file: path=/some/dir/{{ item }} state=absent
  with_items: contents.stdout_lines
  when: item not in managed_files

谢谢。运行得非常好。现在,如果我的“managed_files”没有文件名扩展名,但是我的文件有,你有什么建议可以帮我删除这些文件吗?也许这应该完全是另一个问题了。 - Batandwa
4
确实有效。帮我省去了完成头发的时间。 - Bwire
1
@Batandwa 你可以使用Jinja过滤器,像这样:when: item|regex_replace('^(.*)\\.ext$', '\\1') not in managed_files - menghan
为了基于本地目录确定managed_files,请使用set_fact: managed_files="{{lookup('fileglob','conf.d / *').split(',')| map('basename') | list}}" - simon04
你可能想使用Ansible的find任务代替shell,这样它就可以在--check模式下工作。我会添加一个带有示例代码的答案。 - dreua

12
我们使用nginx文件来实现这一点,因为我们希望它们按照特定的顺序来自模板,但是删除未被管理的文件,以下是可行的方法:
# loop through the nginx sites array and create a conf for each file in order 
# file will be name 01_file.conf, 02_file.conf etc
- name: nginx_sites conf
  template: >
    src=templates/nginx/{{ item.1.template }}
    dest={{ nginx_conf_dir }}/{{ '%02d' % item.0 }}_{{ item.1.conf_name|default(item.1.template) }}
    owner={{ user }}
    group={{ group }}
    mode=0660
  with_indexed_items: nginx_sites
  notify:
    - restart nginx 
  register: nginx_sites_confs

# flatten and map the results into simple list
# unchanged files have attribute dest, changed have attribute path
- set_fact:
    nginx_confs: "{{ nginx_sites_confs.results|selectattr('dest', 'string')|map(attribute='dest')|list + nginx_sites_confs.results|selectattr('path', 'string')|map(attribute='path')|select|list }}"
  when: nginx_sites

# get contents of conf dir
- shell: ls -1 {{ nginx_conf_dir }}/*.conf
  register: contents
  when: nginx_sites

# so we can delete the ones we don't manage
- name: empty old confs 
  file: path="{{ item }}" state=absent
  with_items: contents.stdout_lines
  when: nginx_sites and item not in nginx_confs

这个技巧(正如你所看到的)在于register结果中,template和with_items有不同的属性。然后你将它们转化为一个你管理的文件列表,然后获取目录列表并删除那个列表中没有的文件。

如果你已经有一个文件列表,这个过程可以使用更少的代码实现。但在这种情况下,我需要创建一个带索引的列表,因此还需要使用map来创建列表。


2
这很酷 - 已点赞。但我遇到了困难:如果您没有指定通配符,它将比较完整的路径和文件名与仅文件名,因此它将删除所有内容。在这种情况下,请尝试使用find {{ nginx_conf_dir }} -type f而不是ls -l - Erfan

7

我希望分享一下我的经验。

从Ansible 2.2开始,使用with_filetree循环提供了一种简单的方法来上传目录、链接、静态文件甚至模板。这是保持我的配置目录同步的最佳方式。

- name: etc config - Create directories
  file:
    path: "{{ nginx_conf_dir }}/{{ item.path }}"
    state: directory
    mode: 0755
  with_filetree: etc/nginx
  when: item.state == 'directory'

- name: etc config - Creating configuration files from templates
  template:
    src: "{{ item.src }}"
    dest: "{{ nginx_conf_dir }}/{{ item.path | regex_replace('\\.j2$', '') }}"
    mode: 0644
  with_filetree: etc/nginx
  when:
    - item.state == "file"
    - item.path | match('.+\.j2$') | bool

- name: etc config - Creating staic configuration files
  copy:
    src: "{{ item.src }}"
    dest: "{{ nginx_conf_dir }}/{{ item.path }}"
    mode: 0644
  with_filetree: etc/nginx
  when:
    - item.state == "file"
    - not (item.path | match('.+\.j2$') | bool)

- name: etc config - Recreate symlinks
  file:
    src: "{{ item.src }}"
    dest: "{{ nginx_conf_dir }}/{{ item.path }}"
    state: link
    force: yes
    mode: "{{ item.mode }}"
  with_filetree: etc/nginx
  when: item.state == "link"

接下来,我们可能想要从配置目录中删除未使用的文件。这很简单。 我们收集已上传文件和远程服务器上存在的文件列表,然后删除差异。

但是我们可能希望在配置目录中保留未管理的文件。 我使用了find-prune功能,以避免清除具有未管理文件的文件夹。

注:_(Y)_ 在删除一些未管理文件之后,请确保。

- name: etc config - Gathering managed files
  set_fact:
    __managed_file_path: "{{ nginx_conf_dir }}/{{ item.path | regex_replace('\\.j2$', '') }}"
  with_filetree: etc/nginx
  register: __managed_files

- name: etc config - Convert managed files to list
  set_fact: managed_files="{{ __managed_files.results | map(attribute='ansible_facts.__managed_file_path') | list }}"

- name: etc config - Gathering exist files (excluding .ansible_keep-content dirs)
  shell: find /etc/nginx -mindepth 1 -type d -exec test -e '{}/.ansible_keep-content' \; -prune -o -print
  register: exist_files
  changed_when: False

- name: etc config - Delete unmanaged files
  file: path="{{ item }}" state=absent
  with_items: "{{ exist_files.stdout_lines }}"
  when:
    - item not in managed_files

3
这是我想到的一些内容:
- 模板:src=/source/directory{{ item }}.j2 dest=/target/directory/{{ item }}
  register: template_results
  with_items:
    - a_list.txt
    - of_all.txt
    - templates.txt
- set_fact:
    managed_files: "{{ template_results.results|selectattr('invocation', 'defined')|map(attribute='invocation.module_args.dest')|list }}"
- debug: var: managed_files verbosity: 0
- find: paths: "/target/directory/" patterns: "*.txt" register: all_files - set_fact: files_to_delete: "{{ all_files.files|map(attribute='path')|difference(managed_files) }}"
- debug: var: all_files verbosity: 0 - debug: var: files_to_delete verbosity: 0
- 文件:path={{ item }} state=absent with_items: "{{ files_to_delete }}"
  • 这将生成模板(以任何你想要的方式),并记录结果在“template_results”中。
  • 结果被处理成一个简单的列表,其中包含每个模板的“dest”。跳过的模板(由于when条件而未显示)没有“invocation”属性,因此它们被过滤掉。
  • 然后使用“find”获取应该不存在的所有文件的列表,除非明确写入。
  • 然后对其进行处理,以获取存在的原始文件列表,然后删除“应该在那里”的文件。
  • 然后删除其余的“files_to_delete”。

优点:在删除期间避免出现多个“skipped”条目。

缺点:如果您想在执行查找/删除之前执行多个模板任务,则需要连接每个template_results.results。


2

有几种处理方法,但是在模板步骤之前,在任务中完全清空目标目录是否可行?或者将模板文件放入临时目录,然后在后续步骤中进行删除+重命名操作?


3
在复制之前完全清空目标目录意味着每次都会发生变化,即使源目录没有任何更改。当本地重命名文件时(例如rsync-> render-> rename-> rsync),总是存在这样一个问题,即ansible会报告更改(重命名),即使实际上没有更改。 - Michael Krupp
1
@keks changed_when: false 将解决这个问题。 - ffghfgh
但是您想知道何时/如果配置文件已更改以启动处理程序。 - dalore
@ffghfgh 不,它不会改变,报告从未改变与报告总是改变一样糟糕,甚至更糟。 - augurar

1
我正在使用 Ansible 版本 2.9.20。
---
# tasks file for delete_unmanaged_files
- name: list files in dest
  shell: ls -1 dest/conf.d
  register: files_in_dest

- name: list files in src
  shell: ls -1 src/conf.d
  register: files_in_src

- name: Managed files - dest
  command: echo {{ item|replace('.j2','') }}
  with_items: "{{ files_in_dest.stdout_lines }}"
  register: managed_files_dest

- name: Managed files - src
  command: echo {{ item|replace('.j2','') }}
  with_items: "{{ files_in_src.stdout_lines }}"
  register: managed_files_src

- name: Convert src managed files to list
  set_fact: managed_files_src_list="{{ managed_files_src.results | map(attribute='stdout') | list }}"

- name: Delete unmanaged files in dest
  file: path=dest/conf.d/{{ item.stdout }} state=absent
  with_items: "{{ managed_files_dest.results }}"
  when: item.stdout not in managed_files_src_list

我认为根据该问题的使用情况,我发现上述解决方案可能会对您有所帮助。在此,我创建了6个任务。
说明:
- 任务1和任务2将帮助将文件名存储在变量“files_in_dest”和“files_in_src”中。 - 任务3和任务4将继承来自任务1和任务2的输出,然后替换j2文件(用于用例)。然后这些任务将把输出存储在“managed_files_dest”和“managed_files_src”变量中。 - 任务5将转换“managed_files_src”的输出为列表,以便我们可以将当前状态下src目录中所有存在的文件存储在一个适当或单一的列表中,然后我们可以在下一个任务中使用此列表来了解dest目录中未受管理的文件。 - 任务6将删除dest中未受管辖的文件。

1
通常我不会删除文件,而是在文件名后添加-unmanaged后缀。 示例ansible任务:
- name: Get sources.list.d files
  shell: grep -r --include=\*.list -L '^# Ansible' /etc/apt/sources.list.d || true
  register: grep_unmanaged
  changed_when: grep_unmanaged.stdout_lines

- name: Add '-unmanaged' suffix
  shell: rename 's/$/-unmanaged/' {{ item }}
  with_items: grep_unmanaged.stdout_lines

解释

Grep命令用于:

  • -r 递归搜索
  • --include=\*.list - 仅在递归搜索期间处理扩展名为.list的文件
  • -L '^# Ansible' - 显示不以'# Ansible'开头的行的文件名
  • || true - 这用于忽略错误。Ansible的ignore_errors也可以使用,但在忽略错误之前,ansible会在ansible-playbook运行期间以红色显示它,这是不希望看到的(至少对我来说是这样)。

然后,我将grep命令的输出注册为一个变量。当grep显示任何输出时,我将此任务设置为已更改(changed_when行负责此操作)。

在下一个任务中,我迭代grep输出(即grep返回的文件名),并运行重命名命令以向每个文件添加后缀。

就这样。下次运行命令时,第一个任务应该是绿色的,第二个任务被跳过。


谢谢!这是我现在做事的首选方式,因为得票最高的评论不能很好地处理“没有变化”的情况。 - Karl Katzke

0

显然,目前ansible无法实现这一点。我在IRC上与mdehaan进行了交谈,问题在于ansible没有资源的有向无环图,这使得像这样的事情非常困难。

询问mdehaan是否有例子,例如权威地管理sudoers.d目录,他提出了以下建议:

14:17 < mdehaan> Robe: http://pastebin.com/yrdCZB0y
14:19 < Robe> mdehaan: HM
14:19 < Robe> mdehaan: that actually looks relatively sane
14:19 < mdehaan> thanks :)
14:19 < Robe> the problem I'm seeing is that I'd have to gather the managed files myself
14:19 < mdehaan> you would yes
14:19 < mdehaan> ALMOST
14:20 < mdehaan> you could do a fileglob and ... well, it would be a little gross
[..]
14:32 < mdehaan> eh, theoretical syntax, nm
14:33 < mdehaan> I could do it by writing a lookup plugin that filtered a list
14:34 < mdehaan> http://pastebin.com/rjF7QR24
14:34 < mdehaan> if that plugin existed, for instance, and iterated across lists in A that were also in B

0
在 @user2645850 的回答基础上,我想出了这个改进版本,这个版本管理 Apache 的虚拟主机配置。它不使用 shell,因此也可以在 --check 模式下工作。
# Remove unmanged vhost configs left over from renaming or removing apps
# all managed configs need to be added to "managed_sites" in advance
- find:
    paths: /etc/apache2/sites-available
    patterns: '*.conf'
  register: sites_available_contents

- name: Remove unmanaged vhost config files
  file:
    path: /etc/apache2/sites-available/{{ item }}
    state: absent
  with_items: "{{ sites_available_contents.files | map(attribute='path') | map('basename') | list }}"
  when: item not in managed_sites
  
# links may differ from files, therefore we need our own find task for them
- find:
    paths: /etc/apache2/sites-enabled
    file_type: any 
  register: sites_enabled_contents

- name: Remove unmanaged vhost config links
  file:
    path: /etc/apache2/sites-enabled/{{ item }}
    state: absent
  with_items: "{{ sites_enabled_contents.files | map(attribute='path') | map('basename') | list }}"
  when: item not in managed_sites

构建 managed_sites 的示例:

# Add single conf and handle managed_sites being unset
- set_fact:
    managed_sites: "{{ (managed_sites | default([])) + [ '000-default.conf' ] }}"

# Add a list of vhosts appending ".conf" to each entry of vhosts
- set_fact:
    managed_sites: "{{ managed_sites + ( vhosts | map(attribute='app') | product(['.conf']) | map('join') | list ) }}"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接