kitchen-proxmox 0.3.0 → 0.3.2

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: 8b86aaf1627109dc3fc11a425f023a03da6601eb6ef5c19e7d426654f40a1363
4
- data.tar.gz: 71a7a993ce7729691b6887f84df0854023b32953b843fd7b0c1f29a3d07c253f
3
+ metadata.gz: 8592a8c21a5245c5517893fb3de6c2c2a9cae921cdb8c499fd3a7bcfcc8dc35a
4
+ data.tar.gz: eb18680f687a41ef9a5a5c2f4b290e586e867de4795cdc5733998189fb3b4e85
5
5
  SHA512:
6
- metadata.gz: 5c7640ad5482862220e38d3fb3ff3cdc3d6943b4656e5bdbb9c49169f5887c82c23044abcc6361e870625fda7fa944d7f81944e70d10255b095e230fbdd777b6
7
- data.tar.gz: 9f919e44967c325720f05724b97318884719fd21b46717838d38199beebbb062c1a829c1a635092461d56fe9258c2f917e2ed97a5f18cf4af44d38b19b03c858
6
+ metadata.gz: 8da473dbe9ddf5891bff39a77fe3a55db3fd263a4413592e0c55d826387dbde2e60f77287516f31dd5083200d8d5c962ceaf811f43ad0b89e1a794311ece678b
7
+ data.tar.gz: c93d6729fa61fbc286cdd6b64e03febad2ea3995c1a29e346e5c35037f51339f84aef79370c90443ce96f4ec94fb494225fbf05a3ad27942378c97b54e2d9e36
@@ -5,6 +5,7 @@ require 'json'
5
5
  require 'uri'
6
6
  require 'openssl'
7
7
  require 'kitchen'
8
+ require_relative 'errors'
8
9
 
9
10
  module Kitchen
10
11
  module Driver
@@ -26,6 +27,10 @@ module Kitchen
26
27
  get('/api2/json/cluster/nextid')
27
28
  end
28
29
 
30
+ def validate_vmid(vm_id)
31
+ get("/api2/json/cluster/nextid?vmid=#{vm_id}")
32
+ end
33
+
29
34
  def clone_vm(node:, template_id:, new_id:, **options)
30
35
  full = options.fetch(:full, true)
31
36
  body = { newid: new_id, full: full ? 1 : 0, target: node }
@@ -75,7 +80,14 @@ module Kitchen
75
80
  deadline = Time.now + timeout
76
81
  loop do
77
82
  status = task_status(node:, upid:)
78
- return status if status['status'] == 'stopped'
83
+ if status['status'] == 'stopped'
84
+ exitstatus = status['exitstatus'].to_s
85
+ unless exitstatus == 'OK'
86
+ raise ProxmoxErrors::ApiError.new(500, exitstatus)
87
+ end
88
+
89
+ return status
90
+ end
79
91
  raise "Task timeout after #{timeout}s: #{upid}" if Time.now > deadline
80
92
 
81
93
  sleep interval
@@ -145,7 +157,7 @@ module Kitchen
145
157
  end
146
158
 
147
159
  def handle_response(response)
148
- raise "Proxmox API error #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
160
+ raise ProxmoxErrors::ApiError.new(response.code, response.body) unless response.is_a?(Net::HTTPSuccess)
149
161
 
150
162
  JSON.parse(response.body)['data']
151
163
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Driver
5
+ module ProxmoxErrors
6
+ # Structured error raised by ApiClient on non-2xx responses.
7
+ class ApiError < ::StandardError
8
+ attr_reader :status_code, :response_body
9
+
10
+ def initialize(status_code, response_body)
11
+ @status_code = status_code.to_i
12
+ @response_body = response_body
13
+ super("Proxmox API error #{status_code}: #{response_body}")
14
+ end
15
+
16
+ # Returns true when the error indicates a VMID is already in use
17
+ # or cannot be locked (concurrent clone race).
18
+ def vmid_conflict?
19
+ return false unless status_code == 400 || status_code == 500
20
+
21
+ response_body.match?(/already exists/i) ||
22
+ response_body.match?(/unable to create VM \d+/i) ||
23
+ response_body.match?(/can't lock file.*lock-\d+/i)
24
+ end
25
+
26
+ # Returns true when the error indicates another process owns the VM
27
+ # (lost a VMID race — VM was started or configured by another clone).
28
+ def vmid_race_lost?
29
+ return false unless status_code == 400 || status_code == 500
30
+
31
+ response_body.match?(/already running/i) ||
32
+ response_body.match?(/hotplug problem/i) ||
33
+ response_body.match?(/does not exist/i) ||
34
+ response_body.match?(/can't lock file.*lock-\d+/i)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'kitchen'
4
4
  require_relative 'proxmox_version'
5
+ require_relative 'proxmox/errors'
5
6
  require_relative 'proxmox/api_client'
6
7
 
7
8
  module Kitchen
@@ -16,6 +17,8 @@ module Kitchen
16
17
 
17
18
  plugin_version Kitchen::Driver::PROXMOX_VERSION
18
19
 
20
+ ApiError = Kitchen::Driver::ProxmoxErrors::ApiError
21
+
19
22
  required_config :proxmox_url
20
23
  required_config :proxmox_token_id
21
24
  required_config :proxmox_token_secret
@@ -32,6 +35,9 @@ module Kitchen
32
35
  default_config :clone_timeout, 300
33
36
  default_config :start_timeout, 300
34
37
  default_config :ip_wait_timeout, 120
38
+ default_config :clone_retries, 5
39
+ default_config :vmid_range_min, 900_000
40
+ default_config :vmid_range_max, 999_999
35
41
 
36
42
  def create(state)
37
43
  return if state[:vm_id]
@@ -63,23 +69,66 @@ module Kitchen
63
69
  end
64
70
 
65
71
  def clone_and_start(state)
66
- info("Creating Proxmox VM from template #{config[:template_id]}...")
72
+ retries = config[:clone_retries]
73
+ last_error = nil
74
+
75
+ retries.times do |attempt|
76
+ info("Creating Proxmox VM from template #{config[:template_id]}...")
77
+
78
+ vm_id, vm_name = allocate_and_clone
79
+ state[:vm_id] = vm_id
80
+ state[:vm_name] = vm_name
81
+
82
+ begin
83
+ configure_hardware(vm_id)
84
+ start_and_wait_for_ip(state, vm_id)
85
+ info("Proxmox VM #{vm_name} (#{vm_id}) created.")
86
+ return
87
+ rescue ApiError => e
88
+ raise unless e.vmid_race_lost?
89
+
90
+ last_error = e
91
+ warn("VMID #{vm_id} race lost (attempt #{attempt + 1}/#{retries}): #{e.message}")
92
+ warn("Another process owns VM #{vm_id} — abandoning and retrying...")
93
+ clear_state(state)
94
+ sleep backoff_delay(attempt)
95
+ end
96
+ end
97
+
98
+ raise last_error
99
+ end
67
100
 
68
- vm_id = allocate_vm_id
69
- vm_name = generate_vm_name(instance.name)
101
+ def allocate_and_clone
102
+ retries = config[:clone_retries]
103
+ last_error = nil
70
104
 
71
- clone_template(vm_id, vm_name)
72
- configure_hardware(vm_id)
73
- start_and_wait_for_ip(state, vm_id)
105
+ retries.times do |attempt|
106
+ vm_id = allocate_vm_id
107
+ vm_name = generate_vm_name(instance.name)
74
108
 
75
- state[:vm_id] = vm_id
76
- state[:vm_name] = vm_name
109
+ begin
110
+ clone_template(vm_id, vm_name)
111
+ return [vm_id, vm_name]
112
+ rescue ApiError => e
113
+ raise unless e.vmid_conflict?
114
+
115
+ last_error = e
116
+ warn("VMID #{vm_id} conflict (attempt #{attempt + 1}/#{retries}), retrying...")
117
+ sleep backoff_delay(attempt)
118
+ end
119
+ end
120
+
121
+ raise last_error
122
+ end
77
123
 
78
- info("Proxmox VM #{vm_name} (#{vm_id}) created.")
124
+ def backoff_delay(attempt)
125
+ (0.5 * (2**attempt)) + rand(0.0..1.0)
79
126
  end
80
127
 
81
128
  def allocate_vm_id
82
- Integer(api_client.next_vm_id)
129
+ vm_id = rand(config[:vmid_range_min]..config[:vmid_range_max])
130
+ api_client.validate_vmid(vm_id)
131
+ vm_id
83
132
  end
84
133
 
85
134
  def generate_vm_name(suite_name)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-proxmox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Nixon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-30 00:00:00.000000000 Z
11
+ date: 2026-05-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A Test Kitchen driver for Proxmox VE. Manages VM lifecycle (create, destroy)
14
14
  via the Proxmox REST API. Supports cloning from templates.
@@ -22,6 +22,7 @@ files:
22
22
  - README.md
23
23
  - lib/kitchen/driver/proxmox.rb
24
24
  - lib/kitchen/driver/proxmox/api_client.rb
25
+ - lib/kitchen/driver/proxmox/errors.rb
25
26
  - lib/kitchen/driver/proxmox_version.rb
26
27
  homepage: https://github.com/trickyearlobe-chef/kitchen-proxmox
27
28
  licenses: