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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3ed7c8556ee380cd5845babc8b085c3a660736b81ac8f1b1ac92cc62ec4840d
4
- data.tar.gz: 8a82f387b8a24b8dfafc4dd99cb9baaa65c62483d3b564974d46a469ec40582e
3
+ metadata.gz: fa03e257c553dacbc24d63abaa1a21b0bdf7be8dd2434afef6d304b2925bd23e
4
+ data.tar.gz: eea6142b73c7f1b0d82ec61f628b0aae64fca365df598c815c282e37c47dbaaf
5
5
  SHA512:
6
- metadata.gz: 750a0b65316bcce6dccdb70c6c0c5061e830ddd7bb26011a034d1fe2e081805b6ca928ee7f027a4d5ee4e472916fed0b56ca6c7914747953596a579d64682b48
7
- data.tar.gz: 71499dc0562fa3aa1691d6712fc4b70c9a4f608e27d1cb22f9370c7f604915aa869ca800e0adc817b8c0b3f5b5d8670a32ec2630b69d6eb30b737dad7ed5f17a
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
- body = { newid: new_id, full: full ? 1 : 0, target: node }
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(node, vm_id, vm_name, template_id)
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:, template_id:,
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:, upid:, timeout: config[:clone_timeout])
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
- info("Resolved template '#{config[:template_name]}' to VMID #{state[:template_id]}")
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)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-proxmox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Nixon