Class: Pvectl::Services::CloneVm

Inherits:
Object
  • Object
show all
Defined in:
lib/pvectl/services/clone_vm.rb,
sig/pvectl/services/clone_vm.rbs

Overview

Orchestrates VM clone operations.

Handles validation, auto-generation of VMID/name, and sync/async modes. Supports both full clones and linked clones (templates only).

Examples:

Full clone with auto-generated VMID

service = CloneVm.new(vm_repository: vm_repo, task_repository: task_repo)
result = service.execute(vmid: 100)

Linked clone to specific node

service = CloneVm.new(vm_repository: vm_repo, task_repository: task_repo)
result = service.execute(vmid: 100, linked: true, target_node: "pve2")

Async clone with custom timeout

service = CloneVm.new(vm_repository: vm_repo, task_repository: task_repo, options: { async: true })
result = service.execute(vmid: 100, new_vmid: 200, name: "web-clone")

Constant Summary collapse

DEFAULT_TIMEOUT =

Returns:

  • (Integer)
300
START_TIMEOUT =

Returns Default timeout for start operations (seconds).

Returns:

  • (Integer)

    Default timeout for start operations (seconds)

60

Instance Method Summary collapse

Constructor Details

#initialize(vm_repository:, task_repository:, options: {}) ⇒ CloneVm

Creates a new CloneVm service.

Parameters:

  • vm_repository (Repositories::Vm)

    VM repository

  • task_repository (Repositories::Task)

    Task repository

  • options (Hash) (defaults to: {})

    Options (timeout, async)

  • vm_repository: (Object)
  • task_repository: (Object)
  • options: (Hash[Symbol, untyped]) (defaults to: {})


33
34
35
36
37
# File 'lib/pvectl/services/clone_vm.rb', line 33

def initialize(vm_repository:, task_repository:, options: {})
  @vm_repository = vm_repository
  @task_repository = task_repository
  @options = options
end

Instance Method Details

#add_disk_params(params, disks) ⇒ void

This method returns an undefined value.

Adds disk parameters mapped to scsi0, scsi1, etc.

Parameters:

  • params (Hash)

    Parameters hash to modify

  • disks (Array<Hash>)

    Disk configurations



206
207
208
209
210
# File 'lib/pvectl/services/clone_vm.rb', line 206

def add_disk_params(params, disks)
  disks.each_with_index do |disk, index|
    params[:"scsi#{index}"] = Parsers::DiskConfig.to_proxmox(disk)
  end
end

#add_net_params(params, nets) ⇒ void

This method returns an undefined value.

Adds network parameters mapped to net0, net1, etc.

Parameters:

  • params (Hash)

    Parameters hash to modify

  • nets (Array<Hash>)

    Network configurations



217
218
219
220
221
# File 'lib/pvectl/services/clone_vm.rb', line 217

def add_net_params(params, nets)
  nets.each_with_index do |net, index|
    params[:"net#{index}"] = Parsers::NetConfig.to_proxmox(net)
  end
end

#apply_config_update(source_vm, new_vmid, node, config_params, resource_info) ⇒ Models::VmOperationResult

Applies config update to the cloned VM.

Converts user-friendly params to Proxmox API format and calls the repository update method. Returns partial result on failure.

Parameters:

  • source_vm (Models::Vm)

    Source VM

  • new_vmid (Integer)

    Cloned VM identifier

  • node (String)

    Target node for the cloned VM

  • config_params (Hash)

    Config parameters to apply

  • resource_info (Hash)

    Resource info for result

Returns:



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/pvectl/services/clone_vm.rb', line 155

def apply_config_update(source_vm, new_vmid, node, config_params, resource_info)
  api_params = build_config_api_params(config_params)
  @vm_repository.update(new_vmid, node, api_params)
  start_vm(new_vmid, node) if @options[:start]
  Models::VmOperationResult.new(
    vm: source_vm, operation: :clone,
    success: true, resource: resource_info
  )
rescue StandardError => e
  Models::VmOperationResult.new(
    vm: source_vm, operation: :clone,
    success: :partial, resource: resource_info,
    error: "Cloned successfully, but config update failed: #{e.message}"
  )
end

#build_clone_options(name:, target_node:, storage:, linked:, pool:, description:) ⇒ Hash

Builds clone options hash for repository call.

Parameters:

  • name (String)

    Clone name

  • target_node (String, nil)

    Target node

  • storage (String, nil)

    Target storage

  • linked (Boolean)

    Linked clone flag

  • pool (String, nil)

    Resource pool

  • description (String, nil)

    Description

  • name: (String)
  • target_node: (String, nil)
  • storage: (String, nil)
  • linked: (Boolean)
  • pool: (String, nil)
  • description: (String, nil)

Returns:

  • (Hash)

    Clone options



135
136
137
138
139
140
141
142
# File 'lib/pvectl/services/clone_vm.rb', line 135

def build_clone_options(name:, target_node:, storage:, linked:, pool:, description:)
  opts = { name: name, full: !linked }
  opts[:target] = target_node if target_node
  opts[:storage] = storage if storage
  opts[:pool] = pool if pool
  opts[:description] = description if description
  opts
end

#build_config_api_params(config_params) ⇒ Hash

Builds Proxmox API parameters from user-friendly config options.

Maps config keys to their Proxmox API equivalents. Does not include name, description, or pool (those belong to the clone step).

Parameters:

  • config_params (Hash)

    User-friendly config parameters

Returns:

  • (Hash)

    Proxmox API parameters



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/pvectl/services/clone_vm.rb', line 178

def build_config_api_params(config_params)
  params = {}
  params[:cores] = config_params[:cores] if config_params[:cores]
  params[:sockets] = config_params[:sockets] if config_params[:sockets]
  params[:cpu] = config_params[:cpu_type] if config_params[:cpu_type]
  params[:numa] = config_params[:numa] ? 1 : 0 unless config_params[:numa].nil?
  params[:memory] = config_params[:memory] if config_params[:memory]
  params[:balloon] = config_params[:balloon] if config_params[:balloon]
  add_disk_params(params, config_params[:disks]) if config_params[:disks]
  params[:scsihw] = config_params[:scsihw] if config_params[:scsihw]
  params[:cdrom] = config_params[:cdrom] if config_params[:cdrom]
  add_net_params(params, config_params[:nets]) if config_params[:nets]
  params[:bios] = config_params[:bios] if config_params[:bios]
  params[:boot] = "order=#{config_params[:boot_order]}" if config_params[:boot_order]
  params[:machine] = config_params[:machine] if config_params[:machine]
  params[:efidisk0] = config_params[:efidisk] if config_params[:efidisk]
  params.merge!(config_params[:cloud_init]) if config_params[:cloud_init]
  params[:agent] = config_params[:agent] ? "1" : "0" unless config_params[:agent].nil?
  params[:ostype] = config_params[:ostype] if config_params[:ostype]
  params[:tags] = config_params[:tags] if config_params[:tags]
  params
end

#execute(vmid:, node: nil, new_vmid: nil, name: nil, target_node: nil, storage: nil, linked: false, pool: nil, description: nil, config_params: {}) ⇒ Models::OperationResult

Executes clone operation.

Performs a two-step flow: clone the VM first, then optionally apply config updates via PUT /nodes/node/qemu/vmid/config.

Parameters:

  • vmid (Integer)

    Source VM identifier

  • node (String, nil) (defaults to: nil)

    Source node (auto-detected from VM if nil)

  • new_vmid (Integer, nil) (defaults to: nil)

    New VMID (auto-selected if nil)

  • name (String, nil) (defaults to: nil)

    Name for clone (auto-generated if nil)

  • target_node (String, nil) (defaults to: nil)

    Target node for clone

  • storage (String, nil) (defaults to: nil)

    Target storage

  • linked (Boolean) (defaults to: false)

    Linked clone (default: false, requires template)

  • pool (String, nil) (defaults to: nil)

    Resource pool

  • description (String, nil) (defaults to: nil)

    Description

  • config_params (Hash) (defaults to: {})

    VM config parameters to apply after clone

  • vmid: (Integer)
  • node: (String, nil) (defaults to: nil)
  • new_vmid: (Integer, nil) (defaults to: nil)
  • name: (String, nil) (defaults to: nil)
  • target_node: (String, nil) (defaults to: nil)
  • storage: (String, nil) (defaults to: nil)
  • linked: (Boolean) (defaults to: false)
  • pool: (String, nil) (defaults to: nil)
  • description: (String, nil) (defaults to: nil)
  • config_params: (Hash[Symbol, untyped]) (defaults to: {})

Returns:



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/pvectl/services/clone_vm.rb', line 55

def execute(vmid:, node: nil, new_vmid: nil, name: nil, target_node: nil,
            storage: nil, linked: false, pool: nil, description: nil,
            config_params: {})
  source_vm = @vm_repository.get(vmid)
  return vm_not_found_error(vmid) unless source_vm

  if linked && !source_vm.template?
    return linked_clone_error(source_vm)
  end

  node ||= source_vm.node
  new_vmid ||= @vm_repository.next_available_vmid
  name ||= generate_name(source_vm)

  clone_options = build_clone_options(
    name: name, target_node: target_node, storage: storage,
    linked: linked, pool: pool, description: description
  )

  upid = @vm_repository.clone(vmid, node, new_vmid, clone_options)
  resource_info = { new_vmid: new_vmid, name: name, node: target_node || node }

  if @options[:async]
    Models::VmOperationResult.new(
      vm: source_vm, operation: :clone,
      task_upid: upid, success: :pending,
      resource: resource_info
    )
  else
    task = @task_repository.wait(upid, timeout: timeout)

    unless task.successful?
      return Models::VmOperationResult.new(
        vm: source_vm, operation: :clone,
        task: task, success: task.successful?,
        resource: resource_info
      )
    end

    if config_params.any?
      apply_config_update(source_vm, new_vmid, resource_info[:node], config_params, resource_info)
    else
      start_vm(new_vmid, resource_info[:node]) if @options[:start]
      Models::VmOperationResult.new(
        vm: source_vm, operation: :clone,
        task: task, success: true,
        resource: resource_info
      )
    end
  end
rescue StandardError => e
  Models::VmOperationResult.new(
    vm: source_vm, operation: :clone,
    success: false, error: e.message
  )
end

#generate_name(source_vm) ⇒ String

Generates clone name from source VM.

Parameters:

Returns:

  • (String)

    Generated name



118
119
120
121
122
123
124
# File 'lib/pvectl/services/clone_vm.rb', line 118

def generate_name(source_vm)
  if source_vm.name && !source_vm.name.empty?
    "#{source_vm.name}-clone"
  else
    "vm-#{source_vm.vmid}-clone"
  end
end

#linked_clone_error(source_vm) ⇒ Models::OperationResult

Returns error for linked clone of non-template VM.

Parameters:

Returns:



256
257
258
259
260
261
262
# File 'lib/pvectl/services/clone_vm.rb', line 256

def linked_clone_error(source_vm)
  Models::VmOperationResult.new(
    vm: source_vm, operation: :clone,
    success: false,
    error: "Linked clone requires VM to be a template. VM #{source_vm.vmid} is not a template"
  )
end

#start_vm(vmid, node) ⇒ void

This method returns an undefined value.

Starts a VM after successful clone and config update.

Parameters:

  • vmid (Integer)

    VM identifier

  • node (String)

    Node name



228
229
230
231
# File 'lib/pvectl/services/clone_vm.rb', line 228

def start_vm(vmid, node)
  upid = @vm_repository.start(vmid, node)
  @task_repository.wait(upid, timeout: START_TIMEOUT)
end

#timeoutInteger

Returns configured timeout.

Returns:

  • (Integer)

    Timeout in seconds



236
237
238
# File 'lib/pvectl/services/clone_vm.rb', line 236

def timeout
  @options[:timeout] || DEFAULT_TIMEOUT
end

#vm_not_found_error(vmid) ⇒ Models::OperationResult

Returns error for VM not found.

Parameters:

  • vmid (Integer)

    VM identifier

Returns:



244
245
246
247
248
249
250
# File 'lib/pvectl/services/clone_vm.rb', line 244

def vm_not_found_error(vmid)
  Models::VmOperationResult.new(
    operation: :clone,
    success: false,
    error: "VM #{vmid} not found"
  )
end