@crvouga/dev-fleet 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -9
- package/agents/audit.md +18 -18
- package/ansible/agents/agents.yml +134 -0
- package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
- package/ansible/agents/templates/claude.settings.json.j2 +3 -0
- package/ansible/agents/templates/codex.config.toml.j2 +11 -0
- package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
- package/ansible/agents/templates/grok.config.toml.j2 +19 -0
- package/ansible/agents/templates/opencode.json.j2 +24 -0
- package/ansible/agents/templates/receipt.json.j2 +10 -0
- package/ansible/ansible.cfg +14 -0
- package/ansible/github/git.yml +82 -0
- package/ansible/macos/manager-privilege.yml +55 -9
- package/ansible/macos/manager.yml +154 -7
- package/ansible/macos/policy.json +13 -0
- package/ansible/macos/t3-policy.yml +5 -3
- package/ansible/macos/vault-renew.yml +85 -0
- package/ansible/monitoring/agent-darwin.yml +3 -0
- package/ansible/monitoring/agent.yml +2 -0
- package/ansible/monitoring/hub-darwin.yml +3 -0
- package/ansible/monitoring/hub-route-darwin.yml +1 -0
- package/ansible/net/remote-probe.sh +4 -1
- package/ansible/platforms.json +2 -2
- package/ansible/pyproject.toml +5 -0
- package/ansible/ubuntu/baseline.yml +39 -13
- package/ansible/ubuntu/disk-gc.py +6 -2
- package/ansible/ubuntu/firewall.nft.j2 +50 -0
- package/ansible/ubuntu/policy.json +125 -6
- package/ansible/ubuntu/reboot.yml +89 -0
- package/ansible/ubuntu/recovery.yml +13 -3
- package/ansible/ubuntu/t3/policy.json +3 -0
- package/ansible/ubuntu/t3-desktop.yml +23 -2
- package/ansible/ubuntu/t3-headless.yml +128 -0
- package/ansible/ubuntu/t3-health.sh +24 -10
- package/ansible/ubuntu/t3-idle.py +37 -2
- package/ansible/ubuntu/t3-policy.yml +4 -1
- package/ansible/ubuntu/t3-route.yml +1 -0
- package/ansible/ubuntu/tailscale.yml +54 -8
- package/ansible/ubuntu/tasks/browser.yml +3 -0
- package/ansible/ubuntu/tasks/containers-off.yml +43 -0
- package/ansible/ubuntu/tasks/docker.yml +53 -6
- package/ansible/ubuntu/tasks/dpkg.yml +4 -0
- package/ansible/ubuntu/tasks/features.yml +22 -0
- package/ansible/ubuntu/tasks/firewall.yml +172 -0
- package/ansible/ubuntu/tasks/install.yml +2 -1
- package/ansible/ubuntu/tasks/packages.yml +4 -2
- package/ansible/ubuntu/tasks/release.yml +6 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -4
- package/ansible/ubuntu/tasks/services.yml +2 -2
- package/ansible/ubuntu/tasks/sizing.yml +49 -0
- package/ansible/ubuntu/tasks/snapshot.yml +14 -3
- package/ansible/ubuntu/tasks/ssh.yml +61 -4
- package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
- package/ansible/ubuntu/tasks/user.yml +1 -0
- package/ansible/ubuntu/worker-account.yml +117 -0
- package/ansible/uv.lock +265 -0
- package/bin/devfleet.mjs +42 -4
- package/bootstrap/fleet-remote-bootstrap.sh +91 -25
- package/bootstrap/global-config.ts +29 -3
- package/bootstrap/package-smoke.ts +127 -0
- package/bootstrap/pins.ts +189 -0
- package/bootstrap/release-notes.ts +88 -0
- package/bootstrap/release-version.ts +41 -7
- package/bootstrap/worker-features.ts +114 -0
- package/bootstrap/worker-seed.yaml +7 -3
- package/bootstrap/worker-tools.ts +39 -9
- package/cliproxy/README.md +1 -1
- package/cliproxy/docs/health-workflow.example.yml +20 -143
- package/docs/authentication.md +19 -11
- package/docs/consumer-setup.md +42 -27
- package/docs/convergence.md +67 -0
- package/docs/declared.md +61 -34
- package/docs/machine-setup.md +73 -7
- package/docs/manager-setup.md +10 -3
- package/docs/operations.md +64 -9
- package/docs/releasing.md +49 -1
- package/docs/runbooks.md +98 -7
- package/docs/state.md +23 -11
- package/docs/support-matrix.md +40 -4
- package/docs/topology.md +67 -6
- package/docs/ubuntu-sync.md +78 -28
- package/docs/worker-contract.md +122 -19
- package/docs/worker-requirements.md +107 -0
- package/examples/fleet.json +1 -0
- package/examples/workers-from-infrastructure/main.tf +132 -0
- package/examples/workers-from-infrastructure/worker-records.json +14 -0
- package/llms.txt +3 -1
- package/mise.lock +29 -0
- package/mise.toml +5 -3
- package/package.json +2 -2
- package/schema/check-ids.json +1967 -170
- package/schema/check-ids.schema.json +34 -0
- package/schema/fleet.schema.json +327 -8
- package/schema/output/check.schema.json +1 -1
- package/schema/output/fleet-state-definition.schema.json +14 -0
- package/schema/output/fleet-sync-schedule.schema.json +26 -0
- package/schema/output/fleet-sync.schema.json +18 -0
- package/schema/output/report.schema.json +3 -2
- package/schema/worker-record.schema.json +76 -0
- package/src/agents/converge.ts +140 -24
- package/src/agents/files.ts +2 -32
- package/src/agents/native-verify.ts +34 -3
- package/src/agents/secrets.ts +39 -4
- package/src/check-owners.ts +61 -2
- package/src/check-registry.ts +630 -0
- package/src/cli/agent-guide.ts +3 -3
- package/src/cli/cli.ts +4 -0
- package/src/cli/fleet-manifest.ts +9 -3
- package/src/cli/fleet-node-lifecycle.ts +313 -64
- package/src/cli/fleet-node-park.ts +238 -0
- package/src/cli/fleet-node-seed.ts +51 -35
- package/src/cli/fleet-state.ts +2 -1
- package/src/cli/fleet-sync-checkout.ts +7 -0
- package/src/cli/fleet-sync-enrollment.ts +193 -32
- package/src/cli/fleet-sync-forward.ts +4 -0
- package/src/cli/fleet-sync-manager.ts +103 -33
- package/src/cli/fleet-sync-output.ts +31 -11
- package/src/cli/fleet-sync-plan.ts +65 -9
- package/src/cli/fleet-sync-records.ts +113 -0
- package/src/cli/fleet-sync-remote.ts +151 -9
- package/src/cli/fleet-sync-schedule.ts +14 -4
- package/src/cli/fleet-sync-state.ts +28 -16
- package/src/cli/fleet-sync-vault.ts +77 -0
- package/src/cli/fleet-sync-worker.ts +214 -0
- package/src/cli/fleet-sync.ts +150 -19
- package/src/cli/fleet-validate.ts +59 -21
- package/src/cli/fleet-watch.ts +7 -1
- package/src/cli/fleet.ts +87 -5
- package/src/cli/managed-health.ts +25 -1
- package/src/cli/register-fleet.ts +115 -4
- package/src/cli/registry.ts +14 -0
- package/src/cli/tailnet.ts +43 -10
- package/src/cli/ui-data.ts +3 -1
- package/src/cli/worker-usage-report.ts +63 -0
- package/src/clock.ts +42 -0
- package/src/definition.ts +332 -9
- package/src/diagnostics.ts +5 -2
- package/src/exposure.ts +102 -0
- package/src/fleet-config.ts +3 -1
- package/src/git-identity.ts +76 -3
- package/src/ide/ide.ts +5 -0
- package/src/ide/t3.ts +12 -2
- package/src/input-hashes.ts +6 -0
- package/src/machine/machine-class.ts +59 -0
- package/src/machine/platform.ts +90 -11
- package/src/machine/state.ts +37 -15
- package/src/machine/support-matrix.ts +11 -9
- package/src/machine-health.ts +28 -5
- package/src/manager-health.ts +148 -2
- package/src/manager-sync.ts +65 -4
- package/src/monitoring/beszel.ts +8 -8
- package/src/monitoring/enroll.ts +8 -1
- package/src/monitoring/hub-access.ts +10 -4
- package/src/monitoring/systems.ts +138 -0
- package/src/network-definition.ts +13 -4
- package/src/node-converge-linux.ts +136 -27
- package/src/node-converge.ts +13 -3
- package/src/platform/ansible-receipt.ts +17 -2
- package/src/platform/ansible.ts +40 -4
- package/src/platform/manager-host.ts +25 -8
- package/src/platform/resource-verify.ts +18 -7
- package/src/platform/t3-policy.ts +24 -10
- package/src/platform/t3-service.ts +109 -34
- package/src/platform/ubuntu-release.ts +24 -0
- package/src/platform/ubuntu-verify.ts +31 -3
- package/src/platform/ubuntu-worker.ts +58 -0
- package/src/platform/worker-platform.ts +72 -0
- package/src/protocol.ts +1 -1
- package/src/public-reachability.ts +186 -0
- package/src/remote-bootstrap.ts +50 -12
- package/src/remote-operation.ts +18 -4
- package/src/repair-plan.ts +23 -12
- package/src/report.ts +2 -1
- package/src/schedule-check.ts +21 -2
- package/src/secrets.ts +4 -0
- package/src/service-auth.ts +55 -48
- package/src/ssh-connection.ts +141 -0
- package/src/ssh.ts +63 -24
- package/src/state/store.ts +4 -2
- package/src/state/sync.ts +35 -6
- package/src/state/workers.ts +140 -0
- package/src/t3/pairing.ts +59 -0
- package/src/t3/peer-link.ts +333 -0
- package/src/t3/version.ts +1 -1
- package/src/tailnet-cleanup.ts +185 -0
- package/src/tailnet-control.ts +346 -0
- package/src/tailscale.ts +1 -1
- package/src/toolchain-checks.ts +11 -1
- package/src/types.ts +4 -2
- package/src/worker-account.ts +63 -0
- package/src/worker-activity.ts +303 -0
- package/src/worker-capability-checks.ts +55 -16
- package/src/worker-capacity.ts +31 -0
- package/src/worker-exposure.ts +388 -0
- package/src/worker-features.ts +201 -0
- package/src/worker-host-checks.ts +71 -20
- package/src/worker-manifest.ts +27 -4
- package/src/worker-parking.ts +70 -0
- package/src/worker-policy.ts +17 -1
- package/src/worker-readiness.ts +15 -3
- package/src/worker-reboot.ts +129 -0
- package/src/worker-records.ts +305 -0
- package/src/worker-seed.ts +16 -11
- package/src/worker-sizing.ts +76 -0
- package/src/cli/schedule-logs.ts +0 -36
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
vars_files:
|
|
11
11
|
- policy.json
|
|
12
12
|
tasks:
|
|
13
|
-
-
|
|
13
|
+
- name: Require a supported Ubuntu release
|
|
14
|
+
ansible.builtin.import_tasks: tasks/release.yml
|
|
14
15
|
- name: Inspect installed packages before possible network interruption
|
|
15
16
|
ansible.builtin.package_facts:
|
|
16
17
|
manager: apt
|
|
@@ -55,12 +56,16 @@
|
|
|
55
56
|
group: root
|
|
56
57
|
mode: '0644'
|
|
57
58
|
timeout: 30
|
|
59
|
+
# The same one-line file apt_repository wrote and the first-boot seed
|
|
60
|
+
# writes, without apt_repository's need for gpg, which a minimal image
|
|
61
|
+
# does not have.
|
|
58
62
|
- name: Declare the signed upstream package source
|
|
59
|
-
ansible.builtin.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
ansible.builtin.copy:
|
|
64
|
+
dest: /etc/apt/sources.list.d/tailscale.list
|
|
65
|
+
content: "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu {{ ubuntu_release.codename }} main\n"
|
|
66
|
+
owner: root
|
|
67
|
+
group: root
|
|
68
|
+
mode: '0644'
|
|
64
69
|
register: tailscale_source
|
|
65
70
|
- name: Install the approved version only when absent
|
|
66
71
|
ansible.builtin.apt:
|
|
@@ -81,11 +86,52 @@
|
|
|
81
86
|
ansible.builtin.command:
|
|
82
87
|
argv: [tailscale, status, '--json']
|
|
83
88
|
changed_when: false
|
|
89
|
+
check_mode: false
|
|
84
90
|
failed_when: false
|
|
85
91
|
register: tailscale_status
|
|
86
92
|
no_log: true
|
|
93
|
+
# A new worker joins with a single-use, pre-authorised, tagged key that
|
|
94
|
+
# the seed or the bootstrap left in a root-only file. It joins only when
|
|
95
|
+
# it has never been logged in (NeedsLogin): a node with Tailscale state
|
|
96
|
+
# keeps its identity, and is never re-joined or reset. The key is read
|
|
97
|
+
# from the file by tailscale itself, so it is never an argument. The tag
|
|
98
|
+
# comes with the key and is never advertised as well: Headscale refuses a
|
|
99
|
+
# pre-authorised join that advertises tags (features/tailnet-box).
|
|
100
|
+
- name: Look for a single-use enrolment key
|
|
101
|
+
ansible.builtin.stat:
|
|
102
|
+
path: /run/devfleet/tailnet-auth-key
|
|
103
|
+
register: tailnet_key
|
|
104
|
+
- name: Join the tailnet as the declared worker, with its tag, when never logged in
|
|
105
|
+
ansible.builtin.command:
|
|
106
|
+
argv: >-
|
|
107
|
+
{{ ['tailscale', 'up', '--auth-key=file:/run/devfleet/tailnet-auth-key',
|
|
108
|
+
'--hostname=' ~ tailnet_hostname, '--ssh=false', '--timeout=120s']
|
|
109
|
+
+ (['--login-server=' ~ tailnet_login_server] if tailnet_login_server | default('') else []) }}
|
|
110
|
+
when:
|
|
111
|
+
- tailnet_key.stat.exists
|
|
112
|
+
- tailnet_hostname | default('') is match('^[a-z0-9][a-z0-9-]{0,62}$')
|
|
113
|
+
- tailscale_status.rc == 0
|
|
114
|
+
- (tailscale_status.stdout | from_json).BackendState == 'NeedsLogin'
|
|
115
|
+
changed_when: true
|
|
116
|
+
# A used or expired key fails here; the assertion below names it.
|
|
117
|
+
failed_when: false
|
|
118
|
+
register: tailnet_join
|
|
119
|
+
no_log: true
|
|
120
|
+
- name: Forget the enrolment key once it was offered
|
|
121
|
+
ansible.builtin.file:
|
|
122
|
+
path: /run/devfleet/tailnet-auth-key
|
|
123
|
+
state: absent
|
|
124
|
+
- name: Inspect native authentication after the join
|
|
125
|
+
ansible.builtin.command:
|
|
126
|
+
argv: [tailscale, status, '--json']
|
|
127
|
+
changed_when: false
|
|
128
|
+
check_mode: false
|
|
129
|
+
failed_when: false
|
|
130
|
+
register: tailscale_joined
|
|
131
|
+
no_log: true
|
|
132
|
+
when: tailnet_join is changed
|
|
87
133
|
- name: Require genuine device authentication before claiming connectivity
|
|
88
134
|
ansible.builtin.assert:
|
|
89
|
-
that: (tailscale_status.stdout | from_json).BackendState == 'Running'
|
|
90
|
-
fail_msg: TAILSCALE_LOGIN_REQUIRED; authorize this device through its native login. Existing state is never copied or reset.
|
|
135
|
+
that: ((tailscale_joined if tailnet_join is changed else tailscale_status).stdout | from_json).BackendState == 'Running'
|
|
136
|
+
fail_msg: TAILSCALE_LOGIN_REQUIRED; authorize this device through its native login, or declare tailnet in fleet.json so fleet-sync enrols it with a single-use key. Existing state is never copied or reset.
|
|
91
137
|
no_log: true
|
|
@@ -51,6 +51,9 @@
|
|
|
51
51
|
- --disable-gpu
|
|
52
52
|
- --dump-dom
|
|
53
53
|
- about:blank
|
|
54
|
+
become: true
|
|
54
55
|
become_user: '{{ development_user }}'
|
|
55
56
|
timeout: '{{ browser.launchTimeoutSeconds }}'
|
|
56
57
|
changed_when: false
|
|
58
|
+
# Verifies a change a dry run does not make.
|
|
59
|
+
when: not ansible_check_mode
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
# A worker whose `containers` feature is off (fleet.json features.containers)
|
|
3
|
+
# keeps none of what tasks/docker.yml and the pinned packages gave it: the
|
|
4
|
+
# developer leaves the docker group, the restart drop-in goes, and the Ubuntu
|
|
5
|
+
# Docker packages dev-fleet pinned are removed (not purged: images and volumes
|
|
6
|
+
# under /var/lib/docker stay until a person deletes them). A Docker that
|
|
7
|
+
# another distribution owns (docker-ce) was never dev-fleet's and is left
|
|
8
|
+
# alone. fleet-sync --dry-run lists what this removes; the worker:features
|
|
9
|
+
# check reports what it removed.
|
|
10
|
+
- name: Read the members of the groups containers gave the developer
|
|
11
|
+
ansible.builtin.getent:
|
|
12
|
+
database: group
|
|
13
|
+
key: '{{ item }}'
|
|
14
|
+
fail_key: false
|
|
15
|
+
loop: '{{ features.containers.groups }}'
|
|
16
|
+
register: container_groups
|
|
17
|
+
- name: Take the developer out of the groups containers gave it
|
|
18
|
+
ansible.builtin.command:
|
|
19
|
+
argv: [gpasswd, --delete, '{{ development_user }}', '{{ item.item }}']
|
|
20
|
+
loop: '{{ container_groups.results }}'
|
|
21
|
+
loop_control:
|
|
22
|
+
label: '{{ item.item }}'
|
|
23
|
+
when: >-
|
|
24
|
+
(item.ansible_facts.getent_group[item.item] | default(none)) is not none
|
|
25
|
+
and development_user in (item.ansible_facts.getent_group[item.item][2] | default('')).split(',')
|
|
26
|
+
changed_when: true
|
|
27
|
+
- name: Remove the restart drop-in of the container services
|
|
28
|
+
ansible.builtin.file:
|
|
29
|
+
path: '/etc/systemd/system/{{ item }}.service.d/60-personal-fleet.conf'
|
|
30
|
+
state: absent
|
|
31
|
+
loop: '{{ features.containers.services }}'
|
|
32
|
+
register: container_dropins
|
|
33
|
+
- name: Reload unit definitions after removing the drop-ins
|
|
34
|
+
ansible.builtin.systemd_service:
|
|
35
|
+
daemon_reload: true
|
|
36
|
+
when: container_dropins.changed
|
|
37
|
+
- name: Remove the Docker packages dev-fleet pinned
|
|
38
|
+
ansible.builtin.apt:
|
|
39
|
+
name: '{{ features.containers.packages }}'
|
|
40
|
+
state: absent
|
|
41
|
+
purge: false
|
|
42
|
+
autoremove: false
|
|
43
|
+
lock_timeout: '{{ aptLockTimeoutSeconds }}'
|
|
@@ -29,14 +29,26 @@
|
|
|
29
29
|
- [docker, compose, version]
|
|
30
30
|
- [docker, buildx, version]
|
|
31
31
|
changed_when: false
|
|
32
|
+
# Verifies a change a dry run does not make.
|
|
33
|
+
when: not ansible_check_mode
|
|
32
34
|
# The engine answering is not proof that an agent can use it: run a pinned
|
|
33
35
|
# image and build a one-line image as the developer, in a fresh session that
|
|
34
36
|
# reads the docker group from scratch. The image is cached here so the same
|
|
35
37
|
# test works offline in worker:docker-run.
|
|
38
|
+
#
|
|
39
|
+
# The fresh session is made here, by runuser as root, and not by
|
|
40
|
+
# `become_user`: Ansible skips becoming the user who already runs the play,
|
|
41
|
+
# which the developer does on a worker. The command then ran in the session
|
|
42
|
+
# that started before the group was granted, and the first sync of a new
|
|
43
|
+
# worker ended with "permission denied" on the Docker socket.
|
|
36
44
|
- name: Look for the cached smoke-test image
|
|
37
45
|
ansible.builtin.command:
|
|
38
46
|
argv:
|
|
39
47
|
[
|
|
48
|
+
runuser,
|
|
49
|
+
-u,
|
|
50
|
+
'{{ development_user }}',
|
|
51
|
+
--,
|
|
40
52
|
docker,
|
|
41
53
|
image,
|
|
42
54
|
inspect,
|
|
@@ -44,21 +56,37 @@
|
|
|
44
56
|
'{{ "{{.Id}}" }}',
|
|
45
57
|
'{{ dockerSmokeTest.image }}',
|
|
46
58
|
]
|
|
47
|
-
|
|
59
|
+
environment:
|
|
60
|
+
HOME: '{{ development_home }}'
|
|
48
61
|
check_mode: false
|
|
49
62
|
changed_when: false
|
|
50
63
|
failed_when: false
|
|
51
64
|
register: docker_smoke_image
|
|
52
65
|
- name: Cache the pinned smoke-test image
|
|
53
66
|
ansible.builtin.command:
|
|
54
|
-
argv:
|
|
55
|
-
|
|
67
|
+
argv:
|
|
68
|
+
[
|
|
69
|
+
runuser,
|
|
70
|
+
-u,
|
|
71
|
+
'{{ development_user }}',
|
|
72
|
+
--,
|
|
73
|
+
docker,
|
|
74
|
+
pull,
|
|
75
|
+
--quiet,
|
|
76
|
+
'{{ dockerSmokeTest.image }}',
|
|
77
|
+
]
|
|
78
|
+
environment:
|
|
79
|
+
HOME: '{{ development_home }}'
|
|
56
80
|
when: docker_smoke_image.rc | default(0) != 0
|
|
57
81
|
changed_when: true
|
|
58
82
|
- name: Run a container as the developer, offline (DOCKER_RUN_FAILED)
|
|
59
83
|
ansible.builtin.command:
|
|
60
84
|
argv:
|
|
61
85
|
[
|
|
86
|
+
runuser,
|
|
87
|
+
-u,
|
|
88
|
+
'{{ development_user }}',
|
|
89
|
+
--,
|
|
62
90
|
docker,
|
|
63
91
|
run,
|
|
64
92
|
--rm,
|
|
@@ -69,15 +97,34 @@
|
|
|
69
97
|
'{{ dockerSmokeTest.image }}',
|
|
70
98
|
'true',
|
|
71
99
|
]
|
|
72
|
-
|
|
100
|
+
environment:
|
|
101
|
+
HOME: '{{ development_home }}'
|
|
73
102
|
timeout: '{{ dockerSmokeTest.timeoutSeconds }}'
|
|
74
103
|
changed_when: false
|
|
104
|
+
# Verifies a change a dry run does not make.
|
|
105
|
+
when: not ansible_check_mode
|
|
75
106
|
- name: Build a one-line image as the developer, offline (DOCKER_BUILD_FAILED)
|
|
76
107
|
ansible.builtin.command:
|
|
77
|
-
argv:
|
|
108
|
+
argv:
|
|
109
|
+
[
|
|
110
|
+
runuser,
|
|
111
|
+
-u,
|
|
112
|
+
'{{ development_user }}',
|
|
113
|
+
--,
|
|
114
|
+
docker,
|
|
115
|
+
build,
|
|
116
|
+
--quiet,
|
|
117
|
+
--pull=false,
|
|
118
|
+
--network,
|
|
119
|
+
none,
|
|
120
|
+
'-',
|
|
121
|
+
]
|
|
78
122
|
stdin: |
|
|
79
123
|
FROM {{ dockerSmokeTest.image }}
|
|
80
124
|
RUN true
|
|
81
|
-
|
|
125
|
+
environment:
|
|
126
|
+
HOME: '{{ development_home }}'
|
|
82
127
|
timeout: '{{ dockerSmokeTest.timeoutSeconds }}'
|
|
83
128
|
changed_when: false
|
|
129
|
+
# Verifies a change a dry run does not make.
|
|
130
|
+
when: not ansible_check_mode
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
fcntl.lockf(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
14
14
|
register: apt_idle
|
|
15
15
|
changed_when: false
|
|
16
|
+
check_mode: false
|
|
16
17
|
retries: 24
|
|
17
18
|
delay: 5
|
|
18
19
|
until: apt_idle.rc == 0
|
|
19
20
|
- name: Inspect package database consistency
|
|
20
21
|
ansible.builtin.command: dpkg --audit
|
|
21
22
|
changed_when: false
|
|
23
|
+
check_mode: false
|
|
22
24
|
register: dpkg_audit
|
|
23
25
|
# Power lost during a package run leaves dpkg half-configured. Finishing
|
|
24
26
|
# that run is the standard safe repair: it is tried once, bounded, after
|
|
@@ -30,11 +32,13 @@
|
|
|
30
32
|
- name: Finish configuring every unpacked package, bounded
|
|
31
33
|
ansible.builtin.command:
|
|
32
34
|
argv: [timeout, '600', dpkg, --configure, -a]
|
|
35
|
+
changed_when: true
|
|
33
36
|
environment:
|
|
34
37
|
DEBIAN_FRONTEND: noninteractive
|
|
35
38
|
- name: Inspect package database consistency after the repair
|
|
36
39
|
ansible.builtin.command: dpkg --audit
|
|
37
40
|
changed_when: false
|
|
41
|
+
check_mode: false
|
|
38
42
|
register: dpkg_reaudit
|
|
39
43
|
rescue:
|
|
40
44
|
- name: Report a package run the repair could not finish
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The declared worker features (fleet.json `features`, resolved by
|
|
3
|
+
# bootstrap/worker-features.ts): `worker_containers` and `worker_session` are
|
|
4
|
+
# passed by fleet-sync. Absent, a worker has containers and a desktop session,
|
|
5
|
+
# as every worker had before features existed. What a feature that is off
|
|
6
|
+
# would install is left out here, as policy.json `features` lists it.
|
|
7
|
+
- name: Require a known session type
|
|
8
|
+
ansible.builtin.assert:
|
|
9
|
+
that: worker_session | default('desktop') in ['desktop', 'headless']
|
|
10
|
+
fail_msg: WORKER_FEATURES_INVALID; the session is desktop or headless (fleet.json features.session).
|
|
11
|
+
tags: [always]
|
|
12
|
+
- name: Select what the features this worker lacks would install
|
|
13
|
+
ansible.builtin.set_fact:
|
|
14
|
+
worker_containers_on: '{{ worker_containers | default(true) | bool }}'
|
|
15
|
+
worker_headless: "{{ worker_session | default('desktop') == 'headless' }}"
|
|
16
|
+
worker_packages_off: "{{ ([] if worker_containers | default(true) | bool else features.containers.packages) + (features.desktop.packages if worker_session | default('desktop') == 'headless' else []) }}"
|
|
17
|
+
worker_always_on_services: '{{ alwaysOnServices | difference([] if worker_containers | default(true) | bool else features.containers.services) }}'
|
|
18
|
+
tags: [always]
|
|
19
|
+
- name: Pin only the packages of the features this worker has
|
|
20
|
+
ansible.builtin.set_fact:
|
|
21
|
+
packages: "{{ packages | dict2items | rejectattr('key', 'in', worker_packages_off) | items2dict }}"
|
|
22
|
+
tags: [always]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Inbound traffic reaches a worker only over loopback and the tailnet, apart
|
|
3
|
+
# from what tailscaled itself needs and what the exposure policy opens
|
|
4
|
+
# (worker_exposure, src/exposure.ts). The same rules apply with or without a
|
|
5
|
+
# public address. Measured as worker:exposure.
|
|
6
|
+
#
|
|
7
|
+
# The manager is never locked out. The public SSH port closes only when the
|
|
8
|
+
# session running this play arrived over the tailnet; a session from a public
|
|
9
|
+
# address, a tailnet that is not up, or a run with no session that finds the
|
|
10
|
+
# port open keeps it open (worker:exposure then reports
|
|
11
|
+
# WORKER_PUBLIC_SSH_UNDECLARED). Closing it is verified with new connections
|
|
12
|
+
# over the tailnet, and a failed verification restores the previous rules.
|
|
13
|
+
- name: Read the exposure policy of this worker
|
|
14
|
+
ansible.builtin.set_fact:
|
|
15
|
+
firewall_policy: "{{ {'publicSsh': false, 'publicPorts': []} | combine(worker_exposure | default({})) }}"
|
|
16
|
+
firewall_session_client: "{{ (lookup('ansible.builtin.env', 'SSH_CONNECTION') | default('', true)).split(' ') | first }}"
|
|
17
|
+
- name: Inspect the managed firewall rules
|
|
18
|
+
ansible.builtin.stat:
|
|
19
|
+
path: /etc/personal-fleet/firewall.nft
|
|
20
|
+
register: firewall_owned_file
|
|
21
|
+
- name: Read the previous rules for rollback
|
|
22
|
+
ansible.builtin.slurp:
|
|
23
|
+
src: /etc/personal-fleet/firewall.nft
|
|
24
|
+
when: firewall_owned_file.stat.exists
|
|
25
|
+
register: firewall_previous
|
|
26
|
+
- name: Refuse to overwrite unmanaged firewall rules
|
|
27
|
+
ansible.builtin.assert:
|
|
28
|
+
that:
|
|
29
|
+
- not firewall_owned_file.stat.exists or (firewall_previous.content | b64decode).startswith('# Managed by personal Fleet')
|
|
30
|
+
fail_msg: FIREWALL_UNMANAGED_CONFLICT; review the existing rules file before adoption.
|
|
31
|
+
- name: Read the ports sshd listens on
|
|
32
|
+
ansible.builtin.command: /usr/sbin/sshd -T
|
|
33
|
+
changed_when: false
|
|
34
|
+
check_mode: false
|
|
35
|
+
register: firewall_sshd
|
|
36
|
+
- name: Read this worker's tailnet address
|
|
37
|
+
ansible.builtin.command:
|
|
38
|
+
argv: [tailscale, ip, '-4']
|
|
39
|
+
changed_when: false
|
|
40
|
+
check_mode: false
|
|
41
|
+
failed_when: false
|
|
42
|
+
register: firewall_tailnet_address
|
|
43
|
+
- name: Inspect the tailnet interface
|
|
44
|
+
ansible.builtin.stat:
|
|
45
|
+
path: '/sys/class/net/{{ exposure.tailnetInterface }}'
|
|
46
|
+
register: firewall_tailnet_interface
|
|
47
|
+
- name: Look for Docker, whose published ports the rules close
|
|
48
|
+
ansible.builtin.stat:
|
|
49
|
+
path: /usr/lib/systemd/system/docker.service
|
|
50
|
+
register: firewall_docker_unit
|
|
51
|
+
- name: Decide whether the public SSH path may close
|
|
52
|
+
vars:
|
|
53
|
+
tailnet_up: '{{ firewall_tailnet_address.rc == 0 and firewall_tailnet_address.stdout | trim | length > 0 and firewall_tailnet_interface.stat.exists }}'
|
|
54
|
+
session_over_tailnet: "{{ firewall_session_client is match('^100\\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\\.') or firewall_session_client is match('(?i)^fd7a:115c:a1e0:') }}"
|
|
55
|
+
previously_public: '{{ not firewall_owned_file.stat.exists or ''comment "public-ssh"'' in (firewall_previous.content | b64decode) }}'
|
|
56
|
+
ansible.builtin.set_fact:
|
|
57
|
+
firewall_docker: '{{ firewall_docker_unit.stat.exists }}'
|
|
58
|
+
firewall_ssh_ports: "{{ firewall_sshd.stdout_lines | select('match', '^port [0-9]+$') | map('regex_replace', '^port ', '') | list }}"
|
|
59
|
+
firewall_tailnet_ip: '{{ firewall_tailnet_address.stdout | trim }}'
|
|
60
|
+
firewall_verify_tailnet: '{{ tailnet_up and session_over_tailnet }}'
|
|
61
|
+
firewall_public_ssh: >-
|
|
62
|
+
{{ firewall_policy.publicSsh or not tailnet_up
|
|
63
|
+
or (firewall_session_client | length > 0 and not session_over_tailnet)
|
|
64
|
+
or (firewall_session_client | length == 0 and previously_public) }}
|
|
65
|
+
- name: Create the managed configuration directory
|
|
66
|
+
ansible.builtin.file:
|
|
67
|
+
path: /etc/personal-fleet
|
|
68
|
+
state: directory
|
|
69
|
+
owner: root
|
|
70
|
+
group: root
|
|
71
|
+
mode: '0755'
|
|
72
|
+
- name: Declare the unit that loads the rules at boot
|
|
73
|
+
ansible.builtin.copy:
|
|
74
|
+
dest: /etc/systemd/system/personal-fleet-firewall.service
|
|
75
|
+
owner: root
|
|
76
|
+
group: root
|
|
77
|
+
mode: '0644'
|
|
78
|
+
content: |
|
|
79
|
+
# Managed by personal fleet-sync: ansible/ubuntu/tasks/firewall.yml
|
|
80
|
+
[Unit]
|
|
81
|
+
Description=Personal Fleet firewall (inbound only over loopback and the tailnet)
|
|
82
|
+
DefaultDependencies=no
|
|
83
|
+
Wants=network-pre.target
|
|
84
|
+
Before=network-pre.target shutdown.target
|
|
85
|
+
Conflicts=shutdown.target
|
|
86
|
+
[Service]
|
|
87
|
+
Type=oneshot
|
|
88
|
+
RemainAfterExit=yes
|
|
89
|
+
ExecStart=/usr/sbin/nft -f /etc/personal-fleet/firewall.nft
|
|
90
|
+
ExecReload=/usr/sbin/nft -f /etc/personal-fleet/firewall.nft
|
|
91
|
+
[Install]
|
|
92
|
+
WantedBy=sysinit.target
|
|
93
|
+
register: firewall_unit
|
|
94
|
+
- name: Read the rules the kernel holds now
|
|
95
|
+
ansible.builtin.command:
|
|
96
|
+
argv: [nft, list, table, inet, personal_fleet]
|
|
97
|
+
changed_when: false
|
|
98
|
+
check_mode: false
|
|
99
|
+
failed_when: false
|
|
100
|
+
register: firewall_loaded
|
|
101
|
+
- name: Apply the rules and verify the tailnet path, or restore the previous ones
|
|
102
|
+
block:
|
|
103
|
+
- name: Write the rules, validated by nft (FIREWALL_RULES_REJECTED)
|
|
104
|
+
ansible.builtin.template:
|
|
105
|
+
src: '{{ playbook_dir }}/firewall.nft.j2'
|
|
106
|
+
dest: /etc/personal-fleet/firewall.nft
|
|
107
|
+
owner: root
|
|
108
|
+
group: root
|
|
109
|
+
mode: '0644'
|
|
110
|
+
validate: /usr/sbin/nft --check --file %s
|
|
111
|
+
register: firewall_rules
|
|
112
|
+
- name: Load the rules
|
|
113
|
+
ansible.builtin.systemd_service:
|
|
114
|
+
name: personal-fleet-firewall.service
|
|
115
|
+
daemon_reload: '{{ firewall_unit.changed }}'
|
|
116
|
+
enabled: true
|
|
117
|
+
state: "{{ 'restarted' if firewall_rules.changed or firewall_unit.changed or firewall_loaded.rc != 0 else 'started' }}"
|
|
118
|
+
- name: Verify the manager's tailnet path with a new probe (FIREWALL_TAILNET_UNVERIFIED)
|
|
119
|
+
ansible.builtin.command:
|
|
120
|
+
argv:
|
|
121
|
+
[
|
|
122
|
+
tailscale,
|
|
123
|
+
ping,
|
|
124
|
+
'--c=1',
|
|
125
|
+
'--timeout=5s',
|
|
126
|
+
'{{ firewall_session_client }}',
|
|
127
|
+
]
|
|
128
|
+
changed_when: false
|
|
129
|
+
when: firewall_verify_tailnet | bool and not firewall_public_ssh | bool and not ansible_check_mode
|
|
130
|
+
- name: Verify sshd accepts a new connection on the tailnet address (FIREWALL_TAILNET_UNVERIFIED)
|
|
131
|
+
ansible.builtin.wait_for:
|
|
132
|
+
host: '{{ firewall_tailnet_ip }}'
|
|
133
|
+
port: '{{ firewall_ssh_ports | first | default(22) | int }}'
|
|
134
|
+
search_regex: SSH-
|
|
135
|
+
timeout: 10
|
|
136
|
+
when: firewall_verify_tailnet | bool and not firewall_public_ssh | bool and not ansible_check_mode
|
|
137
|
+
rescue:
|
|
138
|
+
- name: Restore the previous rules
|
|
139
|
+
ansible.builtin.copy:
|
|
140
|
+
content: '{{ firewall_previous.content | b64decode }}'
|
|
141
|
+
dest: /etc/personal-fleet/firewall.nft
|
|
142
|
+
owner: root
|
|
143
|
+
group: root
|
|
144
|
+
mode: '0644'
|
|
145
|
+
when: firewall_owned_file.stat.exists
|
|
146
|
+
- name: Reload the previous rules
|
|
147
|
+
ansible.builtin.systemd_service:
|
|
148
|
+
name: personal-fleet-firewall.service
|
|
149
|
+
state: restarted
|
|
150
|
+
when: firewall_owned_file.stat.exists
|
|
151
|
+
- name: Remove the rejected first rules
|
|
152
|
+
ansible.builtin.file:
|
|
153
|
+
path: /etc/personal-fleet/firewall.nft
|
|
154
|
+
state: absent
|
|
155
|
+
when: not firewall_owned_file.stat.exists
|
|
156
|
+
- name: Stop applying rules that were never verified
|
|
157
|
+
ansible.builtin.systemd_service:
|
|
158
|
+
name: personal-fleet-firewall.service
|
|
159
|
+
enabled: false
|
|
160
|
+
state: stopped
|
|
161
|
+
when: not firewall_owned_file.stat.exists
|
|
162
|
+
- name: Drop the unverified table from the kernel
|
|
163
|
+
ansible.builtin.command:
|
|
164
|
+
argv: [nft, delete, table, inet, personal_fleet]
|
|
165
|
+
changed_when: true
|
|
166
|
+
failed_when: false
|
|
167
|
+
when: not firewall_owned_file.stat.exists
|
|
168
|
+
- name: Report the failure with the previous rules in place
|
|
169
|
+
ansible.builtin.fail:
|
|
170
|
+
msg: >-
|
|
171
|
+
{{ 'FIREWALL_RULES_REJECTED' if firewall_rules is defined and firewall_rules is failed else 'FIREWALL_TAILNET_UNVERIFIED' }};
|
|
172
|
+
the previous rules are back in place. Check tailscale status on the worker, then rerun fleet-sync.
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
# package Ubuntu's security updates moved past its pin is left as it is and
|
|
5
5
|
# reported as a warning (docs/worker-contract.md). `install_pins` maps each
|
|
6
6
|
# package to its pinned version.
|
|
7
|
-
-
|
|
7
|
+
- name: Point APT at the pinned snapshot
|
|
8
|
+
ansible.builtin.import_tasks: snapshot.yml
|
|
8
9
|
- name: Read the installed package versions
|
|
9
10
|
ansible.builtin.package_facts:
|
|
10
11
|
manager: apt
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
-
|
|
2
|
+
- name: Install the pinned packages
|
|
3
|
+
ansible.builtin.include_tasks: install.yml
|
|
3
4
|
vars:
|
|
4
5
|
install_pins: '{{ packages }}'
|
|
5
|
-
-
|
|
6
|
+
- name: Configure unattended upgrades
|
|
7
|
+
ansible.builtin.import_tasks: unattended-upgrades.yml
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
- ansible_facts.distribution_release == ubuntu_release.codename
|
|
13
13
|
- "(ansible_facts.architecture | replace('x86_64', 'amd64') | replace('aarch64', 'arm64')) in ubuntu_release.architectures"
|
|
14
14
|
fail_msg: UBUNTU_BASELINE_MISMATCH; workers run a release docs/support-matrix.md lists. No distribution upgrade is permitted.
|
|
15
|
+
# What differs between accepted releases is data of the release's entry in
|
|
16
|
+
# policy.json, selected above by the release the facts report: its package
|
|
17
|
+
# pins, its browser library pins and, where Tailscale signs its suite with
|
|
18
|
+
# another key, that key's digest. Everything not named there is common.
|
|
15
19
|
- name: Pin the packages of this release, its own pins over the common ones
|
|
16
20
|
ansible.builtin.set_fact:
|
|
17
21
|
packages: '{{ packages | combine(ubuntu_release.packages | default({})) }}'
|
|
22
|
+
browser: "{{ browser | combine({'packages': browser.packages | combine(ubuntu_release.browserPackages | default({}))}) }}"
|
|
23
|
+
tailscale: "{{ tailscale | combine({'keySha256': ubuntu_release.tailscaleKeySha256 | default(tailscale.keySha256)}) }}"
|
|
18
24
|
- name: Read the kernel release to tell WSL2 apart from a machine of its own
|
|
19
25
|
ansible.builtin.slurp:
|
|
20
26
|
src: /proc/sys/kernel/osrelease
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
when: active_swap.stdout | trim == ''
|
|
21
21
|
- name: Refuse to create swap on a nearly full disk
|
|
22
22
|
ansible.builtin.assert:
|
|
23
|
-
that: (swap_capacity.stdout_lines | last | trim | regex_replace('M$', '') | int) >=
|
|
23
|
+
that: (swap_capacity.stdout_lines | last | trim | regex_replace('M$', '') | int) >= worker_swap_file_mib | int + worker_minimum_free_disk_mib | int
|
|
24
24
|
fail_msg: SWAP_CAPACITY_REQUIRED; free disk space so a swap file can be created. No swap is active.
|
|
25
25
|
when: active_swap.stdout | trim == ''
|
|
26
26
|
- name: Create a swap file when the node has no swap
|
|
27
27
|
ansible.builtin.command:
|
|
28
|
-
argv: [fallocate, -l, '{{
|
|
28
|
+
argv: [fallocate, -l, '{{ worker_swap_file_mib }}M', /swap.fleet]
|
|
29
29
|
creates: /swap.fleet
|
|
30
30
|
when: active_swap.stdout | trim == ''
|
|
31
31
|
- name: Restrict the fleet swap file
|
|
@@ -39,10 +39,12 @@
|
|
|
39
39
|
ansible.builtin.command:
|
|
40
40
|
argv: [mkswap, /swap.fleet]
|
|
41
41
|
when: active_swap.stdout | trim == ''
|
|
42
|
+
changed_when: true
|
|
42
43
|
- name: Activate the fleet swap file
|
|
43
44
|
ansible.builtin.command:
|
|
44
45
|
argv: [swapon, /swap.fleet]
|
|
45
46
|
when: active_swap.stdout | trim == ''
|
|
47
|
+
changed_when: true
|
|
46
48
|
- name: Activate the fleet swap file at boot
|
|
47
49
|
ansible.builtin.lineinfile:
|
|
48
50
|
path: /etc/fstab
|
|
@@ -225,7 +227,7 @@
|
|
|
225
227
|
StartLimitIntervalSec=0
|
|
226
228
|
[Service]
|
|
227
229
|
Type=simple
|
|
228
|
-
ExecStart=/usr/bin/python3 -I -B {{ disk_guard_source.stat.path }} --path / --path {{ development_home }} --floor-mib {{
|
|
230
|
+
ExecStart=/usr/bin/python3 -I -B {{ disk_guard_source.stat.path }} --path / --path {{ development_home }} --floor-mib {{ worker_disk_guard_floor_mib }} --inode-floor-percent {{ resourceSafety.inodeFailFreePercent }} --write-mib {{ resourceSafety.diskGuardWriteMiB }} --interval {{ resourceSafety.diskGuardIntervalSeconds }}
|
|
229
231
|
Restart=always
|
|
230
232
|
RestartSec=5
|
|
231
233
|
OOMScoreAdjust=-1000
|
|
@@ -277,7 +279,7 @@
|
|
|
277
279
|
After=docker.service
|
|
278
280
|
[Service]
|
|
279
281
|
Type=oneshot
|
|
280
|
-
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{
|
|
282
|
+
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ worker_disk_gc_pressure_mib }} --pressure-inode-percent {{ resourceSafety.inodeWarnFreePercent }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}{% for root in resourceSafety.diskGcWorktreeRoots %} --worktree-root {{ root }}{% endfor %} --worktree-idle-days {{ resourceSafety.diskGcWorktreeIdleDays }}{% for cache, mib in resourceSafety.diskGcCacheCapsMiB.items() %} --cache-cap {{ cache }}={{ mib }}{% endfor %} --keep-newest /var/cache/personal-fleet={{ resourceSafety.diskGcPackagesKept }} --mise {{ development_home }}/.local/bin/mise --user {{ development_user }}{{ '' if worker_containers_on | default(true) | bool else ' --no-docker' }}
|
|
281
283
|
|
|
282
284
|
TimeoutStartSec=2h
|
|
283
285
|
Nice=19
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
owner: root
|
|
9
9
|
group: root
|
|
10
10
|
mode: '0755'
|
|
11
|
-
loop: '{{ alwaysOnServices }}'
|
|
11
|
+
loop: '{{ worker_always_on_services | default(alwaysOnServices) }}'
|
|
12
12
|
- name: Declare unlimited automatic restart for required system services
|
|
13
13
|
ansible.builtin.copy:
|
|
14
14
|
dest: '/etc/systemd/system/{{ item }}.service.d/60-personal-fleet.conf'
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
[Service]
|
|
24
24
|
Restart=always
|
|
25
25
|
RestartSec=5
|
|
26
|
-
loop: '{{ alwaysOnServices }}'
|
|
26
|
+
loop: '{{ worker_always_on_services | default(alwaysOnServices) }}'
|
|
27
27
|
register: recovery_dropins
|
|
28
28
|
- name: Reload unit definitions without restarting running services
|
|
29
29
|
ansible.builtin.systemd_service:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The size of this machine against the stated minimum, and the disk and memory
|
|
3
|
+
# lines of the worker policy: each a share of the root filesystem or of memory
|
|
4
|
+
# between a lower and an upper bound (policy.json `sizing`, the same numbers
|
|
5
|
+
# src/worker-sizing.ts computes). Runs before anything is installed.
|
|
6
|
+
- name: Measure the root filesystem
|
|
7
|
+
ansible.builtin.command:
|
|
8
|
+
argv:
|
|
9
|
+
- /usr/bin/python3
|
|
10
|
+
- '-c'
|
|
11
|
+
- |
|
|
12
|
+
import os
|
|
13
|
+
v = os.statvfs('/')
|
|
14
|
+
print((v.f_blocks * v.f_frsize) // (1024 * 1024))
|
|
15
|
+
changed_when: false
|
|
16
|
+
check_mode: false
|
|
17
|
+
register: worker_root_size
|
|
18
|
+
tags: [always]
|
|
19
|
+
- name: Record the size of this machine
|
|
20
|
+
ansible.builtin.set_fact:
|
|
21
|
+
worker_size:
|
|
22
|
+
memoryMiB: '{{ ansible_facts.memtotal_mb | int }}'
|
|
23
|
+
diskMiB: '{{ worker_root_size.stdout | trim | int }}'
|
|
24
|
+
cpus: '{{ ansible_facts.processor_vcpus | int }}'
|
|
25
|
+
tags: [always]
|
|
26
|
+
- name: Refuse a machine below the stated minimum worker size before installing anything
|
|
27
|
+
ansible.builtin.assert:
|
|
28
|
+
that:
|
|
29
|
+
- worker_size.memoryMiB | int >= sizing.minimumWorker.memoryMiB
|
|
30
|
+
- worker_size.diskMiB | int >= sizing.minimumWorker.diskMiB
|
|
31
|
+
- worker_size.cpus | int >= sizing.minimumWorker.cpus
|
|
32
|
+
fail_msg: WORKER_SIZE_BELOW_MINIMUM; a worker needs {{ sizing.minimumWorker.memoryMiB }} MiB of memory, a {{ sizing.minimumWorker.diskMiB }} MiB root filesystem and {{ sizing.minimumWorker.cpus }} CPUs (docs/worker-contract.md). Resize the machine; nothing was installed.
|
|
33
|
+
tags: [always]
|
|
34
|
+
- name: Compute the disk and memory lines for this machine
|
|
35
|
+
ansible.builtin.set_fact:
|
|
36
|
+
worker_thresholds: "{{ worker_thresholds | default({}) | combine({item.key: [[((item.value.share * ((worker_size.memoryMiB if item.value.of == 'memory' else worker_size.diskMiB) | int)) | int), item.value.minMiB] | max, item.value.maxMiB] | min}) }}"
|
|
37
|
+
loop: '{{ sizing.thresholds | dict2items }}'
|
|
38
|
+
loop_control:
|
|
39
|
+
label: '{{ item.key }}'
|
|
40
|
+
tags: [always]
|
|
41
|
+
# Each line may be given as an extra variable instead (the VM test injects a
|
|
42
|
+
# capacity gate this way); an extra variable always wins over a fact.
|
|
43
|
+
- name: Name the lines the tasks use
|
|
44
|
+
ansible.builtin.set_fact:
|
|
45
|
+
worker_swap_file_mib: '{{ worker_thresholds.swapFile }}'
|
|
46
|
+
worker_minimum_free_disk_mib: '{{ worker_thresholds.minimumFreeDisk }}'
|
|
47
|
+
worker_disk_guard_floor_mib: '{{ worker_thresholds.diskGuardFloor }}'
|
|
48
|
+
worker_disk_gc_pressure_mib: '{{ worker_thresholds.diskGcPressure }}'
|
|
49
|
+
tags: [always]
|
|
@@ -23,8 +23,18 @@
|
|
|
23
23
|
Components: main universe restricted multiverse
|
|
24
24
|
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
|
25
25
|
register: snapshot_sources
|
|
26
|
-
#
|
|
27
|
-
#
|
|
26
|
+
# The snapshot's indexes live in a directory of their own. An apt update
|
|
27
|
+
# of the system's own sources (unattended upgrades, the Tailscale play)
|
|
28
|
+
# removes the index files of every source it does not know, so kept beside
|
|
29
|
+
# them the snapshot was fetched again on every sync; and fetching the
|
|
30
|
+
# snapshot leaves the system's indexes alone.
|
|
31
|
+
- name: Create the snapshot's own index directory
|
|
32
|
+
ansible.builtin.file:
|
|
33
|
+
path: /var/lib/personal-fleet/apt-lists/partial
|
|
34
|
+
state: directory
|
|
35
|
+
owner: root
|
|
36
|
+
group: root
|
|
37
|
+
mode: '0755'
|
|
28
38
|
- name: Isolate snapshot selection from unrelated APT operations
|
|
29
39
|
ansible.builtin.copy:
|
|
30
40
|
dest: /etc/personal-fleet/apt.conf
|
|
@@ -34,11 +44,12 @@
|
|
|
34
44
|
content: |
|
|
35
45
|
Dir::Etc::sourcelist "/etc/personal-fleet/ubuntu.sources";
|
|
36
46
|
Dir::Etc::sourceparts "-";
|
|
47
|
+
Dir::State::Lists "/var/lib/personal-fleet/apt-lists/";
|
|
37
48
|
APT::Get::List-Cleanup "false";
|
|
38
49
|
DPkg::Lock::Timeout "{{ aptLockTimeoutSeconds }}";
|
|
39
50
|
- name: Inspect immutable snapshot package index
|
|
40
51
|
ansible.builtin.stat:
|
|
41
|
-
path: '/var/lib/apt
|
|
52
|
+
path: '/var/lib/personal-fleet/apt-lists/snapshot.ubuntu.com_ubuntu_{{ snapshot }}_dists_{{ ubuntu_release.codename }}_InRelease'
|
|
42
53
|
register: snapshot_index
|
|
43
54
|
- name: Fetch signed snapshot indexes after source change or interrupted setup
|
|
44
55
|
ansible.builtin.apt:
|