knife-proxmox-ve 0.1.3 → 0.1.5
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.
- checksums.yaml +4 -4
- data/README.md +19 -0
- data/lib/chef/knife/helpers/proxmox_vm_provision.rb +38 -0
- data/lib/chef/knife/proxmox_vm_bootstrap.rb +26 -9
- data/lib/knife-proxmox-ve/api.rb +9 -0
- data/lib/knife-proxmox-ve/client.rb +6 -0
- data/lib/knife-proxmox-ve/provisioner.rb +80 -3
- data/lib/knife-proxmox-ve/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 918852ed3cc22ff0316c1eb9f3ae5e9da00254225926e37f0642ca5986085919
|
|
4
|
+
data.tar.gz: b096cd71927d635a5b3782a9b77cd558e8d782256120b6ab7b2ead348dfd1dea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15d9f3e83831755684014fff78b5ed931e1951ebd99343406172f60bb307590626765505685ed52f57816b9a128f125d10bdd9c150f7b84b2bae84b827449981
|
|
7
|
+
data.tar.gz: ded4dcf52c6e1ae6f5dc58a850919fec0b7684cc62deda036c6b0f8e4deed321f7591ede5c4269dec282c1589068a7476edb25de1b2b17e138f698e9c6562891
|
data/README.md
CHANGED
|
@@ -151,6 +151,8 @@ Both commands share the provisioning flags below.
|
|
|
151
151
|
| `--linked-clone` | Linked clone instead of the default full clone (`--storage` is then ignored). |
|
|
152
152
|
| `--storage NAME` / `--pool NAME` | Target storage / resource pool. |
|
|
153
153
|
| `--cores N` / `--sockets N` / `--memory MiB` | CPU / RAM. |
|
|
154
|
+
| `--disk-size SIZE` | Grow the disk before the first boot: `+20G` adds to the template's size, `50G` sets it. |
|
|
155
|
+
| `--disk DISK` | Disk to resize (`scsi0`, `virtio0`, …). Defaults to the cloned VM's boot disk. |
|
|
154
156
|
| `--bridge vmbrN` / `--vlan TAG` | net0 bridge and VLAN tag (needs a VLAN-aware bridge). |
|
|
155
157
|
| `--ip CIDR\|dhcp` / `--gateway IP` / `--prefix N` | Static IPv4 (`10.0.10.5/24`, or a bare IP with `--prefix`) or `dhcp`. |
|
|
156
158
|
| `--nameserver IP` / `--searchdomain DOMAIN` | cloud-init DNS. |
|
|
@@ -169,6 +171,23 @@ freshly-created host), `--yes`, etc. They do not apply to `vm create`, which nev
|
|
|
169
171
|
clone (faster, thin) — it requires a storage that supports it, and any `--storage` (CLI or
|
|
170
172
|
cluster default) is then dropped, because Proxmox rejects a target storage on a linked clone.
|
|
171
173
|
|
|
174
|
+
**Disk resize.** `--disk-size` grows the clone's disk **between the clone and the first start**,
|
|
175
|
+
so cloud-init's `growpart`/`resizefs` claim the extra space on that first boot and the guest
|
|
176
|
+
filesystem comes up at full size — no second run, no manual step in the guest. The size format is
|
|
177
|
+
Proxmox's own: `+20G` adds 20 GiB to whatever the template had (the portable choice), a bare `50G`
|
|
178
|
+
sets an absolute size. **Proxmox cannot shrink a disk** — an absolute size below the current one is
|
|
179
|
+
rejected by the API.
|
|
180
|
+
|
|
181
|
+
Without `--disk`, the disk is the cloned VM's boot disk: the first entry of its `boot` order that is
|
|
182
|
+
a real disk (cloud-init drives and CD-ROMs are skipped), falling back to its lowest-sorting disk key.
|
|
183
|
+
Pass `--disk scsi1` to grow a specific one — naming a disk the VM does not have fails before the VM
|
|
184
|
+
is started, rather than silently doing nothing. Growing the disk requires `VM.Config.Disk` plus
|
|
185
|
+
`Datastore.AllocateSpace` on the target storage.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
knife proxmox vm create web-01 --template ubuntu-2404-template --disk-size +40G
|
|
189
|
+
```
|
|
190
|
+
|
|
172
191
|
**Bootstrap product (CINC by default).** A `vm bootstrap`'d VM is bootstrapped with **CINC** out of the
|
|
173
192
|
box — the install is pinned to the CINC omnitruck regardless of whether you invoke a CINC- or
|
|
174
193
|
Chef-branded `knife`, which also avoids Chef's commercial license gate. To bootstrap with Chef
|
|
@@ -28,6 +28,13 @@ class Chef
|
|
|
28
28
|
# ENV override for the cloud-init password so it never lands in shell history.
|
|
29
29
|
ENV_CIPASSWORD = "KNIFE_PROXMOX_CIPASSWORD"
|
|
30
30
|
|
|
31
|
+
# A qemu disk key: bus name plus its index (scsi0, virtio1, sata0, ide0).
|
|
32
|
+
DISK_PATTERN = /\A(?:scsi|virtio|sata|ide)\d+\z/
|
|
33
|
+
|
|
34
|
+
# Proxmox's own size format for the resize endpoint: an optional "+" (grow by), a
|
|
35
|
+
# number, and an optional K/M/G/T unit (no unit means bytes).
|
|
36
|
+
DISK_SIZE_PATTERN = /\A\+?\d+(\.\d+)?[KMGT]?\z/
|
|
37
|
+
|
|
31
38
|
def self.included(includer)
|
|
32
39
|
includer.class_eval do
|
|
33
40
|
# --- Source template / clone placement --------------------------------
|
|
@@ -72,6 +79,15 @@ class Chef
|
|
|
72
79
|
long: "--memory MiB",
|
|
73
80
|
description: "Memory in MiB."
|
|
74
81
|
|
|
82
|
+
option :disk,
|
|
83
|
+
long: "--disk DISK",
|
|
84
|
+
description: "Disk to resize (e.g. scsi0). Defaults to the cloned VM's boot disk."
|
|
85
|
+
|
|
86
|
+
option :disk_size,
|
|
87
|
+
long: "--disk-size SIZE",
|
|
88
|
+
description: "Resize the disk before the first boot: '+20G' adds to the template's " \
|
|
89
|
+
"size, '50G' sets it. Proxmox cannot shrink a disk."
|
|
90
|
+
|
|
75
91
|
# --- Networking -------------------------------------------------------
|
|
76
92
|
|
|
77
93
|
option :bridge,
|
|
@@ -147,6 +163,7 @@ class Chef
|
|
|
147
163
|
|
|
148
164
|
validate_scalars!
|
|
149
165
|
validate_network!
|
|
166
|
+
validate_disk!
|
|
150
167
|
resolve_ssh_auth!(require_credential: require_ssh_auth)
|
|
151
168
|
true
|
|
152
169
|
end
|
|
@@ -222,6 +239,23 @@ class Chef
|
|
|
222
239
|
ui.fatal!("--gateway #{gateway.inspect} is not a valid IPv4 address")
|
|
223
240
|
end
|
|
224
241
|
|
|
242
|
+
# The size format is Proxmox's, so it is checked here rather than server-side: a typo
|
|
243
|
+
# like "20GB" would otherwise surface as an opaque HTTP 400 after the clone already ran.
|
|
244
|
+
def validate_disk!
|
|
245
|
+
disk = config[:disk]
|
|
246
|
+
size = config[:disk_size]
|
|
247
|
+
|
|
248
|
+
if !disk.nil? && !DISK_PATTERN.match?(disk.to_s)
|
|
249
|
+
ui.fatal!("--disk #{disk.inspect} is not a disk key (e.g. scsi0, virtio0, sata0, ide0)")
|
|
250
|
+
end
|
|
251
|
+
ui.fatal!("--disk needs --disk-size; naming a disk alone changes nothing") if !disk.nil? && size.nil?
|
|
252
|
+
return if size.nil?
|
|
253
|
+
return if DISK_SIZE_PATTERN.match?(size.to_s)
|
|
254
|
+
|
|
255
|
+
ui.fatal!("--disk-size #{size.inspect} is not a Proxmox size " \
|
|
256
|
+
"(e.g. +20G to grow by 20 GiB, or 50G for an absolute size)")
|
|
257
|
+
end
|
|
258
|
+
|
|
225
259
|
def positive_integer!(key, value)
|
|
226
260
|
int = integer!(key, value)
|
|
227
261
|
ui.fatal!("--#{key.to_s.tr("_", "-")} must be greater than 0 (got #{int})") unless int > 0
|
|
@@ -305,6 +339,10 @@ class Chef
|
|
|
305
339
|
# clones only — for a linked clone the CLI/cluster storage is dropped entirely.
|
|
306
340
|
storage: full ? from_cli_or_cluster(:storage) : nil,
|
|
307
341
|
pool: config[:pool],
|
|
342
|
+
# Disk growth is applied to the clone before its first start (see Provisioner);
|
|
343
|
+
# nil disk_size means the template's disk is kept as-is.
|
|
344
|
+
disk: config[:disk],
|
|
345
|
+
disk_size: config[:disk_size],
|
|
308
346
|
clone_timeout: config[:clone_timeout],
|
|
309
347
|
boot_timeout: config[:boot_timeout],
|
|
310
348
|
# Post-start wait policy (see Provisioner#wait_for_ip). Defaults suit bootstrap;
|
|
@@ -28,13 +28,30 @@ class Chef
|
|
|
28
28
|
CINC_PRODUCT = "cinc"
|
|
29
29
|
CINC_INSTALL_URL = "https://omnitruck.cinc.sh/install.sh"
|
|
30
30
|
|
|
31
|
-
# A freshly cloned VM
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
# A freshly cloned VM is still settling when SSH first answers, and two independent
|
|
32
|
+
# subsystems contend for the dpkg lock: cloud-init (which itself drives apt) and the
|
|
33
|
+
# apt-daily / unattended-upgrades systemd timers, which fire on their own schedule even
|
|
34
|
+
# after cloud-init reports done. Installing the client — or the first client run's package
|
|
35
|
+
# resources — then races them for the lock and fails non-deterministically. Clear the field
|
|
36
|
+
# before the omnibus install, in order:
|
|
37
|
+
# 1. wait for cloud-init to finish;
|
|
38
|
+
# 2. stop the apt-daily / unattended-upgrades timers and any in-flight run, so nothing
|
|
39
|
+
# grabs the lock again for the rest of this boot (`stop`, not `disable`/`mask`: the
|
|
40
|
+
# services return on the next reboot, leaving the image unchanged);
|
|
41
|
+
# 3. block (bounded) until the dpkg lock a stopped run may still hold is released.
|
|
42
|
+
# Every step is guarded on the tool existing and `|| true`, so non-cloud-init / non-systemd
|
|
43
|
+
# / non-apt images and degraded states still let the bootstrap proceed.
|
|
44
|
+
PREINSTALL_WAIT_COMMAND = <<~SH
|
|
45
|
+
if command -v cloud-init >/dev/null 2>&1; then cloud-init status --wait >/dev/null 2>&1 || true; fi
|
|
46
|
+
if command -v systemctl >/dev/null 2>&1; then
|
|
47
|
+
systemctl stop apt-daily.timer apt-daily-upgrade.timer apt-daily.service apt-daily-upgrade.service unattended-upgrades.service >/dev/null 2>&1 || true
|
|
48
|
+
fi
|
|
49
|
+
if command -v flock >/dev/null 2>&1; then
|
|
50
|
+
for lock in /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock; do
|
|
51
|
+
[ -e "$lock" ] && flock -w 300 "$lock" true >/dev/null 2>&1 || true
|
|
52
|
+
done
|
|
53
|
+
fi
|
|
54
|
+
SH
|
|
38
55
|
|
|
39
56
|
deps do
|
|
40
57
|
require "chef/knife/bootstrap"
|
|
@@ -60,8 +77,8 @@ class Chef
|
|
|
60
77
|
# TOFU: a freshly cloned VM has no entry in known_hosts. Accept its key on
|
|
61
78
|
# first connect rather than failing the bootstrap or disabling verification.
|
|
62
79
|
config[:ssh_verify_host_key] ||= :accept_new
|
|
63
|
-
#
|
|
64
|
-
config[:bootstrap_preinstall_command] ||=
|
|
80
|
+
# Drain cloud-init and the dpkg lock before the bootstrap installs the client (see constant).
|
|
81
|
+
config[:bootstrap_preinstall_command] ||= PREINSTALL_WAIT_COMMAND
|
|
65
82
|
end
|
|
66
83
|
|
|
67
84
|
# The bootstrap target host does not exist yet — it is resolved to the VM's IP
|
data/lib/knife-proxmox-ve/api.rb
CHANGED
|
@@ -76,6 +76,15 @@ module Knife
|
|
|
76
76
|
@client.post("/nodes/#{enc(node)}/qemu/#{enc(vmid)}/config", params)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Grow a virtual disk. +size+ uses Proxmox's own format: "+<n>[KMGT]" adds to the
|
|
80
|
+
# current size, a bare "<n>[KMGT]" sets the new absolute size (Proxmox refuses to
|
|
81
|
+
# shrink). PVE 8 runs the resize in a worker and returns a UPID; earlier releases do
|
|
82
|
+
# the work inline and return nil — the caller decides whether there is a task to wait
|
|
83
|
+
# on, since sequencing is not this class's job.
|
|
84
|
+
def resize_disk(node:, vmid:, disk:, size:)
|
|
85
|
+
@client.put("/nodes/#{enc(node)}/qemu/#{enc(vmid)}/resize", { disk:, size: })
|
|
86
|
+
end
|
|
87
|
+
|
|
79
88
|
# Start a VM. Returns the UPID of the async start task.
|
|
80
89
|
def start(node:, vmid:)
|
|
81
90
|
@client.post("/nodes/#{enc(node)}/qemu/#{enc(vmid)}/status/start")
|
|
@@ -62,6 +62,12 @@ module Knife
|
|
|
62
62
|
send_with_body(Net::HTTP::Post, path, body)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# PUT +path+ with a form-encoded body. Proxmox reserves PUT for in-place mutations of
|
|
66
|
+
# an existing object (e.g. the qemu disk resize endpoint).
|
|
67
|
+
def put(path, body = {})
|
|
68
|
+
send_with_body(Net::HTTP::Put, path, body)
|
|
69
|
+
end
|
|
70
|
+
|
|
65
71
|
# Redact secret-bearing values from arbitrary text (e.g. a Proxmox task log, which is
|
|
66
72
|
# returned with HTTP 200 and so does not pass through the error-body scrub path).
|
|
67
73
|
def scrub_text(str)
|
|
@@ -31,6 +31,18 @@ module Knife
|
|
|
31
31
|
CLOUDINIT_DRIVE_KEY = /\A(?:ide|scsi|sata)\d+\z/
|
|
32
32
|
CLOUDINIT_DRIVE_VALUE = /cloudinit/
|
|
33
33
|
|
|
34
|
+
# Config keys that can carry a resizable virtual disk.
|
|
35
|
+
DISK_KEY = /\A(?:scsi|virtio|sata|ide)\d+\z/
|
|
36
|
+
|
|
37
|
+
# Drives that are not resizable storage: the cloud-init drive and any CD-ROM.
|
|
38
|
+
NON_DISK_VALUE = /cloudinit|media=cdrom/
|
|
39
|
+
|
|
40
|
+
# Modern boot config: "boot: order=scsi0;ide2;net0".
|
|
41
|
+
BOOT_ORDER = /\border=([^,]+)/
|
|
42
|
+
|
|
43
|
+
# A Proxmox worker id, as returned by endpoints that offload their work to a task.
|
|
44
|
+
UPID_PREFIX = "UPID:"
|
|
45
|
+
|
|
34
46
|
# Proxmox reports a VMID collision as a non-2xx body mentioning the existing id.
|
|
35
47
|
DUPLICATE_ID = /already exists|config file already exists|VM \d+ already/i
|
|
36
48
|
|
|
@@ -49,8 +61,12 @@ module Knife
|
|
|
49
61
|
node = spec[:target_node] || template[:node]
|
|
50
62
|
|
|
51
63
|
params = build_config(spec.fetch(:config, {}) || {})
|
|
52
|
-
|
|
64
|
+
# One read serves both the cloud-init drive guard and disk discovery; a hardware-only
|
|
65
|
+
# provision needs neither and never reads the config back.
|
|
66
|
+
vm_config = read_vm_config(node, newid, params, spec)
|
|
67
|
+
guard_cloud_init_drive!(vm_config, spec[:name]) if cloud_init?(params)
|
|
53
68
|
@api.update_config(node:, vmid: newid, **params) unless params.empty?
|
|
69
|
+
resize_disk!(node, newid, vm_config, spec) if spec[:disk_size]
|
|
54
70
|
|
|
55
71
|
boot(node, newid)
|
|
56
72
|
ip = wait_for_ip(spec, node, newid)
|
|
@@ -154,10 +170,16 @@ module Knife
|
|
|
154
170
|
params.key?(:ipconfig0) || params.key?(:nameserver) || params.key?(:searchdomain)
|
|
155
171
|
end
|
|
156
172
|
|
|
173
|
+
# Read the cloned VM's config only when something downstream actually needs it.
|
|
174
|
+
def read_vm_config(node, vmid, params, spec)
|
|
175
|
+
return nil unless cloud_init?(params) || spec[:disk_size]
|
|
176
|
+
|
|
177
|
+
@api.vm_config(node:, vmid:)
|
|
178
|
+
end
|
|
179
|
+
|
|
157
180
|
# Fail fast before a doomed bootstrap: if the (template-derived) VM has no cloud-init
|
|
158
181
|
# drive, the keys we are about to set would be silently ignored by the guest.
|
|
159
|
-
def guard_cloud_init_drive!(
|
|
160
|
-
config = @api.vm_config(node:, vmid:)
|
|
182
|
+
def guard_cloud_init_drive!(config, name)
|
|
161
183
|
present = config.any? do |key, value|
|
|
162
184
|
CLOUDINIT_DRIVE_KEY.match?(key.to_s) && CLOUDINIT_DRIVE_VALUE.match?(value.to_s)
|
|
163
185
|
end
|
|
@@ -168,6 +190,61 @@ module Knife
|
|
|
168
190
|
"cloud-init settings would be silently ignored"
|
|
169
191
|
end
|
|
170
192
|
|
|
193
|
+
# Grow a disk of the fresh clone, BEFORE its first start: cloud-init's growpart/resizefs
|
|
194
|
+
# only run on boot, so a disk grown now ends up usable inside the guest, while a resize
|
|
195
|
+
# after boot leaves the extra space unclaimed until something in the guest is told to
|
|
196
|
+
# take it.
|
|
197
|
+
def resize_disk!(node, vmid, vm_config, spec)
|
|
198
|
+
disk = resolve_disk(vm_config, spec[:disk])
|
|
199
|
+
size = spec.fetch(:disk_size)
|
|
200
|
+
@ui&.info("Resizing #{disk} of VM #{vmid} to #{size}...")
|
|
201
|
+
upid = @api.resize_disk(node:, vmid:, disk:, size:)
|
|
202
|
+
# PVE 8 offloads the resize to a worker and hands back its UPID; older releases resize
|
|
203
|
+
# inline and return nothing, so there is nothing to wait on.
|
|
204
|
+
@api.wait_task(upid:) if task_upid?(upid)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# An explicitly named disk must exist on the VM — resizing a key Proxmox does not know
|
|
208
|
+
# is a silent no-op otherwise. Without one, fall back to the detected boot disk.
|
|
209
|
+
def resolve_disk(vm_config, disk)
|
|
210
|
+
return detect_disk(vm_config) if disk.nil?
|
|
211
|
+
return disk.to_s if vm_config.key?(disk.to_s)
|
|
212
|
+
|
|
213
|
+
raise Knife::Proxmox::Error,
|
|
214
|
+
"VM has no disk #{disk} (found: #{disk_keys(vm_config).join(", ")})"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The disk to grow when the caller named none: the first entry of the VM's boot order
|
|
218
|
+
# that is a real disk, else the lowest-sorting disk key. cloud-init drives and CD-ROMs
|
|
219
|
+
# are never candidates.
|
|
220
|
+
def detect_disk(vm_config)
|
|
221
|
+
candidates = disk_keys(vm_config)
|
|
222
|
+
# Array#& keeps the receiver's order, so the boot order wins where it says anything.
|
|
223
|
+
disk = (boot_order(vm_config) & candidates).first || candidates.min
|
|
224
|
+
return disk if disk
|
|
225
|
+
|
|
226
|
+
raise Knife::Proxmox::Error,
|
|
227
|
+
"cannot tell which disk to resize (no disk found on the VM); pass --disk scsi0"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def disk_keys(vm_config)
|
|
231
|
+
vm_config.select do |key, value|
|
|
232
|
+
DISK_KEY.match?(key.to_s) && !NON_DISK_VALUE.match?(value.to_s)
|
|
233
|
+
end.keys.map(&:to_s)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def boot_order(vm_config)
|
|
237
|
+
match = BOOT_ORDER.match(vm_config["boot"].to_s)
|
|
238
|
+
return match[1].split(";") if match
|
|
239
|
+
|
|
240
|
+
# Pre-6.4 layout: "boot: cdn" plus a separate "bootdisk: scsi0".
|
|
241
|
+
[vm_config["bootdisk"]].compact.map(&:to_s)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def task_upid?(value)
|
|
245
|
+
value.is_a?(String) && value.start_with?(UPID_PREFIX)
|
|
246
|
+
end
|
|
247
|
+
|
|
171
248
|
def boot(node, vmid)
|
|
172
249
|
@ui&.info("Starting VM #{vmid}...")
|
|
173
250
|
upid = @api.start(node:, vmid:)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: knife-proxmox-ve
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Piotr Wojcieszonek
|
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
77
|
- !ruby/object:Gem::Version
|
|
78
78
|
version: '0'
|
|
79
79
|
requirements: []
|
|
80
|
-
rubygems_version: 4.0.
|
|
80
|
+
rubygems_version: 4.0.16
|
|
81
81
|
specification_version: 4
|
|
82
82
|
summary: Knife plugin to create and automatically bootstrap Proxmox VE VMs with Chef/CINC.
|
|
83
83
|
test_files: []
|