kitchen-proxmox 0.4.0 → 0.5.0
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/lib/kitchen/driver/proxmox/api_client.rb +6 -1
- data/lib/kitchen/driver/proxmox/errors.rb +6 -0
- data/lib/kitchen/driver/proxmox.rb +102 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa03e257c553dacbc24d63abaa1a21b0bdf7be8dd2434afef6d304b2925bd23e
|
|
4
|
+
data.tar.gz: eea6142b73c7f1b0d82ec61f628b0aae64fca365df598c815c282e37c47dbaaf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c558a3b1e537092dcc396e610f42a7695d088c316ae1cb3bfceacd1dbaab918c2b1751adc1123bf2e5855407bd2e840e7e698c34739b2524a8e9de3a43ff987
|
|
7
|
+
data.tar.gz: c59edb23fa7d0f06bc247db4c10a93b6c82c29402d3138e9477e6f815538bcaae5235651f058c914928626aeb11eaeaeba511f4c1e3ec3cc3884ef6b8a51cdaa
|
|
@@ -58,7 +58,8 @@ module Kitchen
|
|
|
58
58
|
|
|
59
59
|
def clone_vm(node:, template_id:, new_id:, **options)
|
|
60
60
|
full = options.fetch(:full, true)
|
|
61
|
-
|
|
61
|
+
target = options.fetch(:target, node)
|
|
62
|
+
body = { newid: new_id, full: full ? 1 : 0, target: }
|
|
62
63
|
body[:name] = options[:name] if options[:name]
|
|
63
64
|
body[:pool] = options[:pool] if options[:pool]
|
|
64
65
|
body[:storage] = options[:storage] if options[:storage]
|
|
@@ -130,6 +131,10 @@ module Kitchen
|
|
|
130
131
|
resources.select { |r| r['template'] == 1 }
|
|
131
132
|
end
|
|
132
133
|
|
|
134
|
+
def list_storage(node:)
|
|
135
|
+
get("/api2/json/nodes/#{node}/storage")
|
|
136
|
+
end
|
|
137
|
+
|
|
133
138
|
private
|
|
134
139
|
|
|
135
140
|
def get(path)
|
|
@@ -33,6 +33,12 @@ module Kitchen
|
|
|
33
33
|
response_body.match?(/does not exist/i) ||
|
|
34
34
|
response_body.match?(/can't lock file.*lock-\d+/i)
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
# Returns true when clone fails because the template uses local storage
|
|
38
|
+
# and cannot be cloned to a different node.
|
|
39
|
+
def local_storage_conflict?
|
|
40
|
+
response_body.match?(/uses local storage/i)
|
|
41
|
+
end
|
|
36
42
|
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
@@ -26,6 +26,7 @@ module Kitchen
|
|
|
26
26
|
default_config :node_pool, nil
|
|
27
27
|
default_config :template_id, nil
|
|
28
28
|
default_config :template_name, nil
|
|
29
|
+
default_config :clone_type, 'auto'
|
|
29
30
|
|
|
30
31
|
default_config :ssl_verify, true
|
|
31
32
|
default_config :connect_timeout, 10
|
|
@@ -48,6 +49,7 @@ module Kitchen
|
|
|
48
49
|
resolve_node(state)
|
|
49
50
|
resolve_template(state)
|
|
50
51
|
validate_config!
|
|
52
|
+
resolve_clone_strategy(state)
|
|
51
53
|
clone_and_start(state)
|
|
52
54
|
end
|
|
53
55
|
|
|
@@ -80,11 +82,13 @@ module Kitchen
|
|
|
80
82
|
last_error = nil
|
|
81
83
|
node = state[:node]
|
|
82
84
|
template_id = state[:template_id] || config[:template_id]
|
|
85
|
+
template_node = state[:template_node] || node
|
|
86
|
+
full_clone = state[:full_clone]
|
|
83
87
|
|
|
84
88
|
retries.times do |attempt|
|
|
85
89
|
info("Creating Proxmox VM from template #{template_id}...")
|
|
86
90
|
|
|
87
|
-
vm_id, vm_name = allocate_and_clone(node, template_id)
|
|
91
|
+
vm_id, vm_name = allocate_and_clone(node, template_id, template_node, full_clone)
|
|
88
92
|
state[:vm_id] = vm_id
|
|
89
93
|
state[:vm_name] = vm_name
|
|
90
94
|
|
|
@@ -108,7 +112,7 @@ module Kitchen
|
|
|
108
112
|
raise last_error
|
|
109
113
|
end
|
|
110
114
|
|
|
111
|
-
def allocate_and_clone(node, template_id)
|
|
115
|
+
def allocate_and_clone(node, template_id, template_node, full_clone)
|
|
112
116
|
retries = config[:clone_retries]
|
|
113
117
|
last_error = nil
|
|
114
118
|
|
|
@@ -117,7 +121,7 @@ module Kitchen
|
|
|
117
121
|
vm_name = generate_vm_name(instance.name)
|
|
118
122
|
|
|
119
123
|
begin
|
|
120
|
-
clone_template(node, vm_id, vm_name, template_id)
|
|
124
|
+
clone_template(node, vm_id, vm_name, template_id, template_node, full_clone)
|
|
121
125
|
return [vm_id, vm_name]
|
|
122
126
|
rescue ApiError => e
|
|
123
127
|
raise unless e.vmid_conflict?
|
|
@@ -145,13 +149,29 @@ module Kitchen
|
|
|
145
149
|
"#{config[:vm_name_prefix]}#{suite_name}-#{Time.now.to_i}"
|
|
146
150
|
end
|
|
147
151
|
|
|
148
|
-
def clone_template(
|
|
152
|
+
def clone_template(target_node, vm_id, vm_name, template_id, template_node, full_clone)
|
|
149
153
|
upid = api_client.clone_vm(
|
|
150
|
-
node
|
|
154
|
+
node: template_node, template_id:,
|
|
151
155
|
new_id: vm_id, name: vm_name,
|
|
156
|
+
target: target_node, full: full_clone,
|
|
152
157
|
pool: config[:pool], storage: config[:storage]
|
|
153
158
|
)
|
|
154
|
-
api_client.wait_for_task(node
|
|
159
|
+
api_client.wait_for_task(node: template_node, upid:, timeout: config[:clone_timeout])
|
|
160
|
+
rescue ApiError => e
|
|
161
|
+
raise local_storage_error(template_id, template_node, target_node) if e.local_storage_conflict?
|
|
162
|
+
|
|
163
|
+
raise
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def local_storage_error(template_id, template_node, target_node)
|
|
167
|
+
Kitchen::UserError.new(
|
|
168
|
+
"Cannot clone template #{template_id} from node '#{template_node}' to node '#{target_node}' " \
|
|
169
|
+
"because the template uses local storage.\n\n" \
|
|
170
|
+
"To fix this, either:\n" \
|
|
171
|
+
" - Move the template disk to shared storage (e.g. NFS, Ceph, iSCSI)\n" \
|
|
172
|
+
" - Pin the node to '#{template_node}' in .kitchen.yml: node: #{template_node}\n" \
|
|
173
|
+
" - Create a copy of the template on '#{target_node}'"
|
|
174
|
+
)
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
def configure_hardware(node, vm_id)
|
|
@@ -215,6 +235,79 @@ module Kitchen
|
|
|
215
235
|
nil
|
|
216
236
|
end
|
|
217
237
|
|
|
238
|
+
def resolve_clone_strategy(state)
|
|
239
|
+
return if state[:full_clone] != nil # rubocop:disable Style/NonNilCheck
|
|
240
|
+
|
|
241
|
+
# Can't determine strategy without a template
|
|
242
|
+
template_id = state[:template_id] || config[:template_id]
|
|
243
|
+
return unless template_id
|
|
244
|
+
|
|
245
|
+
case config[:clone_type]
|
|
246
|
+
when 'full'
|
|
247
|
+
state[:full_clone] = true
|
|
248
|
+
when 'linked'
|
|
249
|
+
state[:full_clone] = false
|
|
250
|
+
else
|
|
251
|
+
detect_clone_strategy(state)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def detect_clone_strategy(state)
|
|
256
|
+
template_id = state[:template_id] || config[:template_id]
|
|
257
|
+
template_node = state[:template_node] || state[:node]
|
|
258
|
+
storage_info = template_storage_info(template_node, template_id)
|
|
259
|
+
shared = storage_info && storage_info['shared'] == 1
|
|
260
|
+
supports_linked = shared && linked_clone_capable_type?(storage_info['type'])
|
|
261
|
+
|
|
262
|
+
if supports_linked
|
|
263
|
+
state[:full_clone] = false
|
|
264
|
+
elsif shared
|
|
265
|
+
# Shared but doesn't support linked clones (e.g. plain LVM)
|
|
266
|
+
state[:full_clone] = true
|
|
267
|
+
warn("Storage '#{storage_info['storage']}' (type: #{storage_info['type']}) does not support linked clones. " \
|
|
268
|
+
'Using full clone. For faster clones, use lvmthin, ZFS, Ceph RBD, or file-based storage (NFS/dir).')
|
|
269
|
+
else
|
|
270
|
+
# Local storage — full clone and pin to template node
|
|
271
|
+
state[:full_clone] = true
|
|
272
|
+
pin_to_template_node(state, template_node)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
LINKED_CLONE_STORAGE_TYPES = %w[lvmthin nfs dir cephfs rbd zfspool btrfs].freeze
|
|
277
|
+
|
|
278
|
+
def linked_clone_capable_type?(type)
|
|
279
|
+
LINKED_CLONE_STORAGE_TYPES.include?(type.to_s)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def template_storage_info(template_node, template_id)
|
|
283
|
+
vm_conf = api_client.vm_config(node: template_node, vm_id: template_id)
|
|
284
|
+
storage_name = extract_storage_name(vm_conf)
|
|
285
|
+
return nil unless storage_name
|
|
286
|
+
|
|
287
|
+
storages = api_client.list_storage(node: template_node)
|
|
288
|
+
storages.find { |s| s['storage'] == storage_name }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def extract_storage_name(vm_conf)
|
|
292
|
+
# Find the first real disk field (scsi0, virtio0, ide0, sata0, etc.)
|
|
293
|
+
disk_field = vm_conf.find do |k, v|
|
|
294
|
+
k.match?(/^(scsi|virtio|ide|sata)\d+$/) && !v.start_with?('none')
|
|
295
|
+
end
|
|
296
|
+
return nil unless disk_field
|
|
297
|
+
|
|
298
|
+
# Format: "StorageName:disk-name,option=val,..."
|
|
299
|
+
disk_field[1].split(':').first
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def pin_to_template_node(state, template_node)
|
|
303
|
+
return if state[:node] == template_node
|
|
304
|
+
|
|
305
|
+
warn("Template uses local storage on node '#{template_node}' — " \
|
|
306
|
+
"pinning clone target to '#{template_node}'. " \
|
|
307
|
+
'Move template to shared storage for cross-node flexibility.')
|
|
308
|
+
state[:node] = template_node
|
|
309
|
+
end
|
|
310
|
+
|
|
218
311
|
def validate_config!
|
|
219
312
|
errors = []
|
|
220
313
|
errors << 'Set template_id OR template_name, not both.' if config[:template_id] && config[:template_name]
|
|
@@ -249,7 +342,9 @@ module Kitchen
|
|
|
249
342
|
node = state[:node]
|
|
250
343
|
selected = matches.find { |t| t['node'] == node } || matches.first
|
|
251
344
|
state[:template_id] = selected['vmid']
|
|
252
|
-
|
|
345
|
+
state[:template_node] = selected['node']
|
|
346
|
+
info("Resolved template '#{config[:template_name]}' to VMID #{state[:template_id]} " \
|
|
347
|
+
"on node #{state[:template_node]}")
|
|
253
348
|
end
|
|
254
349
|
|
|
255
350
|
def resolve_node(state)
|