介绍
Field Notes 的编辑需要处理两台照片工作站上不同的选片。联系表及其对应的文件清单能帮助编辑挑选照片,但后台任务已启动,或存在一个同名旧文件,都不代表交付完成。先制作一个小样本,再用 Ansible 等待并收集每台工作站实际生成的本次结果。
你需要具备基本终端导航能力,并练习过小型 Ansible inventory、变量以及 command/file 任务。SSH 访问、获许可的照片和图像工具均已准备好。本实验会在使用前解释参数列表、循环、逐主机任务 ID、完成检查和 fetch 目标。时长为作者估计,包含阅读和普通错误修正,并非真实初学者完成时间的测量结果。
制作小型联系表
在这一步,你将通过 Ansible 运行一个小型照片任务,收集真实图片和文件清单,并将结果作为后续较大任务的参照。
打开 Photo Desk 标签页。在 A previous walk 中点击 Open contact sheet。联系表将多张照片的缩小版本排在一页,方便编辑比较画面,不必逐一打开原图。这个历史样例标有 previous-walk,是参考资料,不是今天完成的工作。在窄屏上,可在图片框内部滚动查看文件名。点击 Back to photo desk,返回同一标签页中的主页。
故事和工作站分配是虚构的。风景照片是获许可的教学素材;Photo credits 列出了真实摄影师和来源。准备好的图像工具会读取真实 JPEG 文件,生成 PNG 联系表及 CSV 文件清单。你要自动化运行这个工具,无须编写图像处理代码。
切换到 Controller 标签页,起始目录为 /home/labex/project。控制端运行 Ansible。两个 SSH 目标是共享训练 VM 内核的容器,不是两台独立 VM。即使路径名称相同,每个目标仍有自己的文件。
pwd 打印当前目录,cat 打印文件。阅读准备好的 inventory 和变量:
pwd
cat inventory.yml
cat group_vars/all.yml
cat host_vars/camera-north.yml
cat host_vars/camera-south.yml
inventory 的 cameras 组包含 camera-north 和 camera-south,保存了真实 SSH 连接设置。项目的 ansible.cfg 选择这个 inventory.yml 及准备好的主机信任设置。镜像原有的 inventory.ini 不是本课程的 inventory。请保持配置、inventory 和变量文件不变。
这些文件采用 JSON 风格的大括号,仍然是合法 YAML。group_vars/all.yml 提供两个目标共享的变量:input_root 为 /srv/photo-sheets/batches,output_root 为 /srv/photo-sheets/rendered,collection_root 是控制端 Playbook 旁边的 collected 目录。host_vars 中的文件应用于其同名主机。这里,photo_batch 为北侧工作站选择 morning-walk,为南侧工作站选择 afternoon-walk。今天的两组选片在内容和数量上都不同。
先在北侧工作站使用准备好的两张照片的 sample 输入。Ansible 的 command 模块在该 SSH 目标上运行现成程序。它的 argv 列表每项传递一个参数,不让 shell 解释命令字符串。工具的 --input 选项指定源目录,--output 指定结果目录,--station 在结果中标明工作站。源照片不会被编辑。
{{ variable }} 插入 Ansible 变量值。inventory_hostname 是当前主机在 inventory 中的名称;playbook_dir 是控制端存放 Playbook 的目录。gather_facts: false 跳过本照片任务不需要的机器 facts 收集。
fetch 模块将文件从 SSH 目标传回控制端,与 copy 的方向相反。loop 对列出的两个文件名重复执行任务;{{ item }} 表示当前文件名。默认情况下,fetch 保留包含主机名和源路径的目录树。这里,flat: true 去掉源路径,因此目标目录明确包含 inventory_hostname。这样可以将不同工作站的同名文件分开。fetch 会创建所需的目标目录。
创建 sample.yml。cat > filename 写入文件,并替换已有的同名文件。<<'YAML' 开始一个 here-document:粘贴到最后独占一行的 YAML 为止。带引号的分隔符让粘贴文本保持字面内容;Ansible 在运行 Playbook 时处理表达式。YAML 缩进使用空格:
cat > sample.yml <<'YAML'
- name: Build a small selection before scheduling both workstations
hosts: camera-north
gather_facts: false
tasks:
- name: Build the two-photo practice sheet
ansible.builtin.command:
argv:
- /usr/local/bin/photo-sheet
- --input
- "{{ input_root }}/sample"
- --output
- "{{ output_root }}/sample"
- --station
- "{{ inventory_hostname }}"
- name: Collect the practice image and file list
ansible.builtin.fetch:
src: "{{ output_root }}/sample/{{ item }}"
dest: "{{ playbook_dir }}/sample/{{ inventory_hostname }}/"
flat: true
loop:
- contact-sheet.png
- files.csv
YAML
运行任务前先检查语法。--syntax-check 检查 Playbook 结构,不处理照片:
ansible-playbook sample.yml --syntax-check
如果有拼写或缩进错误,重新复制完整 Playbook,再次检查语法。现在运行它:
ansible-playbook sample.yml
图片任务默认同步执行:Ansible 等待程序结束后才收集文件。汇总应显示 failed=0 和 unreachable=0。照片任务执行实际工作,可能报告 changed;重复进行图像处理并不保证没有变化。
读取收集到的文件清单:
cat sample/camera-north/files.csv
表头为 station,batch,position,filename,sha256,width,height。应有两条属于 camera-north 和 sample 的数据行,顺序与图片从左到右的排列一致。文件名为 frame-01.jpg 和 frame-02.jpg。SHA256 是源文件内容的指纹,无须记住长数值。尺寸描述的是原始照片,而非小预览图。
切换到 Photo Desk 并重新加载。在 Small practice sheet 下,使用 Open contact sheet 查看真实的两张照片结果。使用 Back to photo desk 返回,再点击练习卡片中的 Download contact sheet (PNG) 和 **Download file list (CSV)**。打开下载的图片和 CSV:两张图片、文件名和行顺序必须对应。浏览器可能为重复下载的文件名添加数字。
下面的练习卡片展示实际的两张照片图片及两个下载链接。使用文件前,先核对工作站和 sample 标签。

如果命令报告“No such file or directory”,可用 ls 列出当前目录来检查拼写。使用 cd /home/labex/project 返回工作区,再重复相应命令。请保留所有源照片、历史参考文件、编辑备注和准备好的连接设置。较大的本次选片尚未生成。
等待各任务成功后再收集
在这一步,你将启动两台工作站的本次选片处理,等待每台主机自己的成功结果,并收集对应文件,避免文件名冲突。
留在 Controller,工作目录为 /home/labex/project。cameras 组选择两个目标;每个目标的 photo_batch 仍选择其准备好的输入。使用这些变量,让同一流程能处理其他成员、批次名称和目录,无须写死两台工作站的名称或输入路径。
同步任务会等待结束,Ansible 才继续下一个任务。使用 async: 120 时,Ansible 为远程操作提供 120 秒运行预算。poll: 0 启动操作,但不在这个任务中等待完成。成功启动不等于成功处理:远程任务仍可能以错误结束。
register: photo_job 分别为每个 inventory 主机保存启动结果。其中的 ansible_job_id 标识该主机上的这个任务。一台工作站的 JID 不能拿到另一台工作站上查询结果。debug 模块打印保存的观察值,方便你看到不同的任务分配。
async_status 向同一主机查询其 JID,并将每次响应保存为 photo_result。until: photo_result is finished 重复状态查询,直到原生完成标志为真;retries: 60 限定重试次数,delay: 1 在查询之间等待一秒。这些重试只是查询状态,不会新建照片任务。实际 SSH 和处理时间还有额外开销。真实的小批次在第一次查询到达时就可能结束;本练习没有添加人为延迟。
仅完成并不能证明成功。远程程序的 rc 是退出状态,零表示成功。async_status 默认将非零结果作为失败传播。额外的 assert 明确要求零退出状态,不会把错误变成成功。后面的调试输出让你在收集前读到实际的 finished 和 rc。等效流程可以依赖模块原生的失败传播,而不添加这个额外断言。
两台主机在各自的文件系统上生成相同文件名 contact-sheet.png 和 files.csv。只在成功完成后收集,并在控制端按 inventory 主机目录分别保存文件对。Photo Desk 可以读取这里明确指定的平铺布局,也可以读取工作站收集文件夹内部默认的主机及源路径层次。它不会从参考文件中补齐缺失的收集结果。
使用上一步带引号的 here-document 方法编写 process-photos.yml:
cat > process-photos.yml <<'YAML'
- name: Build and collect each workstation's current photo selection
hosts: cameras
gather_facts: false
tasks:
- name: Start this workstation's contact sheet
ansible.builtin.command:
argv:
- /usr/local/bin/photo-sheet
- --input
- "{{ input_root }}/{{ photo_batch }}"
- --output
- "{{ output_root }}"
- --station
- "{{ inventory_hostname }}"
async: 120
poll: 0
register: photo_job
- name: Show the job assigned to this workstation
ansible.builtin.debug:
msg:
workstation: "{{ inventory_hostname }}"
job: "{{ photo_job.ansible_job_id }}"
- name: Wait for this workstation's own job
ansible.builtin.async_status:
jid: "{{ photo_job.ansible_job_id }}"
register: photo_result
until: photo_result is finished
retries: 60
delay: 1
- name: Require successful processing before collecting
ansible.builtin.assert:
that: photo_result.rc == 0
- name: Read the actual completion result
ansible.builtin.debug:
msg:
workstation: "{{ inventory_hostname }}"
finished: "{{ photo_result.finished }}"
rc: "{{ photo_result.rc }}"
- name: Collect the image and its file list by workstation
ansible.builtin.fetch:
src: "{{ output_root }}/{{ item }}"
dest: "{{ collection_root }}/{{ inventory_hostname }}/"
flat: true
loop:
- contact-sheet.png
- files.csv
YAML
检查语法:
ansible-playbook process-photos.yml --syntax-check
然后开始实际工作:
ansible-playbook process-photos.yml
每台工作站应显示自己的任务 ID,随后在完成消息中显示 finished: true 和 rc: 0。最终汇总中,两台主机都应为 failed=0 和 unreachable=0。北侧选片包含三张照片,南侧包含六张。实际任务可能在不同时间结束。这次运行展示真实异步调度,不保证更大工作负载的性能。
读取两份收集到的清单:
cat collected/camera-north/files.csv
cat collected/camera-south/files.csv
station 和 batch 字段必须符合各主机的分配。同名文件不代表源照片相同。每行的位置、文件名、指纹和尺寸描述一张真实输入照片,图片采用相同顺序。
切换到 Photo Desk 并重新加载。在 Current collections 中,打开每台工作站的 Open contact sheet 链接,以完整尺寸查看三张与六张照片的联系表;在两者之间使用 Back to photo desk 返回。下载每台工作站的 PNG 和 CSV,打开两个文件,确认图片与完整数据行对应。参考资料和小型练习结果都必须仍可访问。
比较两张卡片上的主机和批次标签。北侧联系表有一行三张图片;南侧有两行,每行三张,所用源选片也不同。

已结束的任务仍可能失败
切换回 Controller。现在有意只为北侧工作站选择不存在的输入。--limit camera-north 将本次调用限制在这个 inventory 主机。--extra-vars photo_batch=missing 仅在这次调用中覆盖批次变量,不编辑准备好的变量文件。任务会启动,随后报告处理已结束但失败。不能将已有的正确文件视为本次新任务成功的结果。
下面的命令预期会失败:
ansible-playbook process-photos.yml --limit camera-north --extra-vars photo_batch=missing
立即打印上一条命令的状态。$? 保存最后一条命令的退出状态,echo 打印它。如果先运行其他命令,该值就会被替换:
echo $?
应看到非零 Ansible CLI 状态,通常为 2。失败消息中,远程结果已结束,程序的 rc 为 2,错误中包含 /srv/photo-sheets/batches/missing。CLI 状态与远程程序状态是两个不同的值,只是在这里恰好同为 2。已结束的失败任务不是成功的收集。本次失败尝试不应执行 fetch 任务。南侧工作站不在这次调用范围内。
返回准备好的默认值来修正选择,并只重跑北侧工作站:
ansible-playbook process-photos.yml --limit camera-north
这次运行必须成功并收集真实的 morning-walk 结果。南侧工作站成功任务产生的收集结果仍可访问。回到 Photo Desk,重新加载,再次下载北侧 PNG 和 CSV,确认正确的三张照片选片。保持 process-photos.yml 可复用,保留所有源照片和历史输出,并保持编辑备注的私密性。
对于普通 YAML 或文件名错误,使用上一步学过的 ls、目录恢复和完整代码块修正方法,再次检查语法。不要重置 VM,也不要修改源照片来掩盖输入错误。重复成功的照片任务会重新进行处理;fetch 校验和不变,不代表没有执行处理。
在 poll: 0 之后,Ansible 会保留任务状态缓存。检查过已结束的任务后,可以用 async_status、mode: cleanup 和该主机的 jid 删除对应缓存项。这里的维护是可选的。清理缓存不会取消运行中的任务,也不会删除联系表。
总结
你制作了真实的小型联系表,再用 Ansible 启动并等待两个分别属于各自主机的异步任务。你按工作站收集了完整图片/CSV 文件对,区分了已结束的失败任务与成功任务,并通过只重试受影响的主机修正了输入缺失问题。



