Skip to content

Commit 5f9e06a

Browse files
82-salt-minion.yml: ARM64 Salt minion support via host_arch selection
Robert's added a custom ARM64 Windows Salt minion MSI, built by tracking three still-open, unmerged upstream PRs (confirmed live against the GitHub API, none merged as of 2026-08-10): - saltstack/salt#70003 -- native Windows arm64 minion MSI support - saltstack/relenv#318 -- Windows arm64 support (relenv, Salt's own relocatable Python builder, a real dependency of the minion build) - pymssql/pymssql#1013 -- native Windows arm64 wheel builds (a Salt dependency needed for the build to complete on arm64 at all) Renamed the existing MSI from the unsuffixed Salt-Minion-Setup.msi to Salt-Minion-Setup-x86_64.msi, and wired 82-salt-minion.yml to select Salt-Minion-Setup-{{ host_arch }}.msi -- reuses tasks/arch_facts.yml's own host_arch fact and naming convention (x86_64/arm64/x86) rather than inventing a new one, matching 50-binaries.yml's already-established pattern for exactly this problem. Added a loud, explicit failure for any architecture with no matching MSI at all, instead of a confusing win_copy "file not found" further down the task list. Also fixes a real harness gap check_playbook_dir_paths.py had: its own tail-matching regex excluded "{"/"}" outright, so it silently truncated and misreported the new host_arch-templated path as unresolvable -- a false positive, not a real bug. Fixed generically (glob-match any embedded {{ ... }} Jinja expression, require at least one real file to match) rather than special-cased to this one file, so it holds for any future playbook_dir path with a runtime-templated segment. Both the pass and fail paths of the fix tested directly before landing.
1 parent d2ce513 commit 5f9e06a

4 files changed

Lines changed: 81 additions & 7 deletions

File tree

‎ansible/playbooks/salt/playbooks/10-master.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,8 +564,9 @@
564564
- " Ports : 4505/tcp (publish), 4506/tcp (return)"
565565
- "════════════════════════════════════════════════════════════════"
566566
- " Remaining manual steps:"
567-
- " 1. Confirm bootstrap/web/windows/Salt-Minion-Setup.msi is the"
568-
- " matching {{ salt_version_major }}.x build (buildsheet-salt-minion.md)"
567+
- " 1. Confirm bootstrap/web/windows/Salt-Minion-Setup-{x86_64,arm64}.msi"
568+
- " are the matching {{ salt_version_major }}.x build (buildsheet-salt-minion.md --"
569+
- " arm64 is a custom build, not an official release, see that doc's own caveat)"
569570
- " 2. Build/onboard a client endpoint (unattend XML or manual fallback)"
570571
- " 3. salt-key -L on this host to see the new minion's unaccepted key"
571572
- " 4. salt-key -a <minion-name> to accept it"

‎ansible/playbooks/windows_bootstrap/playbooks/82-salt-minion.yml‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@
5555
# against installed products and skips the install if already present. Re-running this
5656
# playbook against an already-bootstrapped node does no work.
5757
#
58+
# ARM64 (added 2026-08-10, Robert): reuses tasks/arch_facts.yml's own host_arch fact/naming
59+
# convention (x86_64/arm64/x86, matching 50-binaries.yml's established pattern) rather than
60+
# inventing a new one -- two arch-suffixed MSIs now live side by side in bootstrap/web/windows/
61+
# (Salt-Minion-Setup-x86_64.msi, Salt-Minion-Setup-arm64.msi), selected by the same host_arch
62+
# fact every other arch-aware Windows playbook already uses. Unlike the x86_64 build (Salt
63+
# Project's own official release, SHA256-verified against Broadcom's response header -- see
64+
# docs/buildsheets/buildsheet-salt-minion.md), the arm64 build is NOT an official upstream
65+
# release -- native Windows arm64 support for the Salt minion doesn't exist upstream yet.
66+
# It's a custom build tracking three still-OPEN, unmerged PRs (salt#70003, relenv#318,
67+
# pymssql#1013 -- confirmed live against the GitHub API before this comment was written, all
68+
# three genuinely open, none merged) that together add the arm64 build capability. Treat this
69+
# build as experimental: it can't be refreshed the same "fetch + verify vendor checksum" way
70+
# as the x86_64 one, since there's no vendor release to fetch from -- rebuilding it means
71+
# re-applying those three PRs (or whatever they've evolved into) by hand. See that same
72+
# buildsheet section for the full caveat.
73+
#
5874
# FUTURE WORK -- custom grains (deferred, Robert has his own notes to bring back first, see
5975
# memory): when that lands, implement it as its OWN task(s), separate from the install task
6076
# above -- must not be nested inside or conditioned on "Salt wasn't already installed". Grains
@@ -71,16 +87,34 @@
7187
# Version history
7288
# =============================================================================
7389
# v1.0.0 2026-07-20 Initial release.
90+
# v1.1.0 2026-08-10 ARM64 support -- host_arch-based MSI selection (reuses
91+
# tasks/arch_facts.yml/50-binaries.yml's own convention),
92+
# Salt-Minion-Setup-x86_64.msi renamed from the previous
93+
# unsuffixed filename, Salt-Minion-Setup-arm64.msi added
94+
# (custom build, see header for the real caveat).
7495
# =============================================================================
7596

7697
- name: Windows — Salt minion install
7798
hosts: "windows_nodes"
7899
gather_facts: false
79100
tasks:
80101

102+
- name: Detect CPU architecture
103+
include_tasks: ../tasks/arch_facts.yml
104+
tags: salt
105+
106+
- name: Fail loudly on an architecture with no Salt minion MSI at all
107+
ansible.builtin.fail:
108+
msg: >-
109+
host_arch={{ host_arch }} has no Salt minion MSI in
110+
bootstrap/web/windows/ -- only x86_64 (official) and arm64
111+
(custom build, see this playbook's own header) exist.
112+
when: host_arch not in ['x86_64', 'arm64']
113+
tags: salt
114+
81115
- name: Copy Salt minion MSI to the target host
82116
ansible.windows.win_copy:
83-
src: "{{ playbook_dir }}/../../../../bootstrap/web/windows/Salt-Minion-Setup.msi"
117+
src: "{{ playbook_dir }}/../../../../bootstrap/web/windows/Salt-Minion-Setup-{{ host_arch }}.msi"
84118
dest: "C:\\Windows\\Temp\\Salt-Minion-Setup.msi"
85119
tags: salt
86120

‎at_have_ryggen_fri/check_playbook_dir_paths.py‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@
4949
otherwise self-match as a phantom failure the same way a similar comment
5050
once did for a different check in this harness.
5151
52+
Found 2026-08-10, a third variant: 82-salt-minion.yml's ARM64-support change
53+
introduced the first playbook_dir path with a genuine RUNTIME Jinja
54+
expression embedded in the trailing filename ("Salt-Minion-Setup-
55+
{{ host_arch }}.msi", host_arch resolved per-target at play time, not
56+
knowable statically). The original tail pattern excluded "{"/"}" outright,
57+
so it silently truncated the match at "Salt-Minion-Setup-" and reported a
58+
real, correctly-committed file as unresolvable -- a false positive, not a
59+
real bug. Fixed generically rather than special-cased to this one file:
60+
the tail pattern now matches a complete {{ ... }} block as a unit, and
61+
resolution substitutes any embedded Jinja expression with a glob wildcard,
62+
requiring at least one real file to match rather than one exact literal
63+
path -- holds for any future playbook_dir path with a runtime-templated
64+
segment, not just this one.
65+
5266
Exit code: 0 if every playbook_dir-relative path resolves, 1 otherwise.
5367
"""
5468
import re
@@ -58,9 +72,33 @@
5872
REPO_ROOT = Path(__file__).resolve().parents[1]
5973
ANSIBLE_DIR = REPO_ROOT / "ansible"
6074

75+
JINJA_EXPR = re.compile(r"\{\{[^{}]*\}\}")
76+
77+
78+
def path_exists(base_dir, tail):
79+
"""A literal tail must exist exactly. A tail containing a runtime Jinja
80+
expression (e.g. "Salt-Minion-Setup-{{ host_arch }}.msi") can't be
81+
resolved to one real path statically -- substitute each {{ ... }} with
82+
a glob wildcard instead and require at least one real file to match,
83+
so a genuinely broken path (typo, wrong directory) still fails while a
84+
correct runtime-templated one passes."""
85+
if not JINJA_EXPR.search(tail):
86+
resolved = (base_dir / tail).resolve()
87+
return resolved.exists(), resolved
88+
glob_pattern = JINJA_EXPR.sub("*", tail)
89+
resolved = (base_dir / glob_pattern).resolve()
90+
matches = list(base_dir.glob(glob_pattern))
91+
return len(matches) > 0, resolved
92+
6193
# {{ playbook_dir }}, then one or more /.. segments, then a trailing path
62-
# fragment up to whitespace, a quote, or a closing brace.
63-
PATTERN = re.compile(r"\{\{\s*playbook_dir\s*\}\}((?:/\.\.)+)/([^\s\"'{}]+)")
94+
# fragment up to whitespace or a quote. The fragment itself may contain a
95+
# runtime Jinja expression (e.g. "Salt-Minion-Setup-{{ host_arch }}.msi",
96+
# added 2026-08-10 for ARM64 Salt minion support) -- matched as a complete
97+
# {{ ... }} unit (internal spaces allowed) rather than excluded outright,
98+
# since the original tail pattern stopped dead at the first "{" and
99+
# silently truncated the match instead of flagging it as unresolvable.
100+
TAIL = r"(?:[^\s\"'{}]|\{\{[^{}]*\}\})+"
101+
PATTERN = re.compile(r"\{\{\s*playbook_dir\s*\}\}((?:/\.\.)+)/(" + TAIL + r")")
64102

65103
# example_music_gate_repo_root: "{{ playbook_dir }}(/..)+ " -- bare, no
66104
# trailing path fragment, only ever combined with /benarbejde later inside
@@ -84,8 +122,9 @@ def main():
84122
up_segments, tail = match.groups()
85123
checked += 1
86124
levels = up_segments.count("/..")
87-
resolved = (path.parent / ("../" * levels) / tail).resolve()
88-
if not resolved.exists():
125+
base_dir = (path.parent / ("../" * levels)).resolve()
126+
exists, resolved = path_exists(base_dir, tail)
127+
if not exists:
89128
failures.append((rel, match.group(0), levels, resolved))
90129

91130
for match in GATE_ROOT_PATTERN.finditer(line):
File renamed without changes.

0 commit comments

Comments
 (0)