Module: Pvectl::Commands::FeatureCommand

Included in:
FeatureContainer, FeatureVm
Defined in:
lib/pvectl/commands/feature_command.rb,
sig/pvectl/commands/feature_command.rbs

Overview

Shared functionality for the feature query commands (feature vm, feature ct).

Queries whether a Proxmox feature (clone, snapshot, copy) is available for a given VM or container. Availability depends on storage type, snapshot state, and other server-side conditions.

Designed with the hybrid Template Method pattern (MoveDiskCommand / MigrateCommand) — specializations only set RESOURCE_TYPE and SUPPORTED_RESOURCES; the FeatureVm specialization additionally implements .register which wires the GLI command.

Examples:

Including in a command class

class FeatureVm
  include FeatureCommand
  RESOURCE_TYPE = :vm
  SUPPORTED_RESOURCES = %w[vm].freeze
end

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

VALID_FEATURES =

Valid feature names per Proxmox API (both QEMU and LXC endpoints).

Returns:

  • (Array[String])
%w[clone snapshot copy].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ void

This method returns an undefined value.

Hook called when the module is included.

Parameters:

  • base (Class)

    the class including this module



43
44
45
# File 'lib/pvectl/commands/feature_command.rb', line 43

def self.included(base)
  base.extend(ClassMethods)
end

Instance Method Details

#build_repository(connection) ⇒ Repositories::Vm, Repositories::Container

Builds the resource repository (VM or Container) for the current type.

Parameters:

Returns:



145
146
147
148
149
150
151
# File 'lib/pvectl/commands/feature_command.rb', line 145

def build_repository(connection)
  if resource_type_symbol == :vm
    Pvectl::Repositories::Vm.new(connection)
  else
    Pvectl::Repositories::Container.new(connection)
  end
end

#executeInteger

Executes the feature query.

Returns:

  • (Integer)

    exit code (0 if available, 1 if unavailable, 2 on usage)



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/pvectl/commands/feature_command.rb', line 61

def execute
  return usage_error("#{id_label} is required") if @args.empty?
  return usage_error("FEATURE is required") if @args.size < 2

  id_str = @args[0]
  feature = @args[1]

  unless id_str.to_s.match?(/\A\d+\z/)
    return usage_error("Invalid #{id_label}: #{id_str}")
  end

  unless VALID_FEATURES.include?(feature)
    return usage_error(
      "Invalid feature: #{feature} (allowed: #{VALID_FEATURES.join(', ')})"
    )
  end

  perform_operation(id_str.to_i, feature)
end

#id_labelString

Returns the human label for the resource ID ("VMID" or "CTID").

Returns:

  • (String)


93
94
95
# File 'lib/pvectl/commands/feature_command.rb', line 93

def id_label
  resource_type_symbol == :vm ? "VMID" : "CTID"
end

#initialize(args, options, global_options) ⇒ FeatureCommand

Initializes a feature query command.

Parameters:

  • args (Array<String>)

    command-positional args ([id, feature])

  • options (Hash)

    command options

  • global_options (Hash)

    global CLI options

Returns:



52
53
54
55
56
# File 'lib/pvectl/commands/feature_command.rb', line 52

def initialize(args, options, global_options)
  @args = Array(args).compact
  @options = options
  @global_options = global_options
end

#load_configvoid

This method returns an undefined value.

Loads configuration from file or environment.



156
157
158
159
160
# File 'lib/pvectl/commands/feature_command.rb', line 156

def load_config
  service = Pvectl::Config::Service.new
  service.load(config: @global_options[:config])
  @config = service.current_config
end

#output_result(result, feature) ⇒ void

This method returns an undefined value.

Outputs the result in the requested format.

Plain output: "available"/"unavailable" + optional list of capable nodes. JSON/YAML output: structured hash with feature metadata.

Parameters:

  • result (Hash)

    result from feature_available? (:available, :nodes)

  • feature (String)

    feature name



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/pvectl/commands/feature_command.rb', line 170

def output_result(result, feature)
  format = (@global_options[:output] || "plain").to_s

  case format
  when "json"
    require "json"
    $stdout.puts JSON.pretty_generate(payload(result, feature))
  when "yaml"
    require "yaml"
    $stdout.puts YAML.dump(stringify_keys(payload(result, feature)))
  else
    status = result[:available] ? "available" : "unavailable"
    $stdout.puts status
    if result[:available] && !result[:nodes].empty?
      $stdout.puts "nodes: #{result[:nodes].join(', ')}"
    end
  end
end

#payload(result, feature) ⇒ Hash

Builds the structured result payload for JSON/YAML output.

Parameters:

  • result (Hash)

    feature_available? result

  • feature (String)

    feature name

Returns:

  • (Hash)


194
195
196
197
198
199
200
201
# File 'lib/pvectl/commands/feature_command.rb', line 194

def payload(result, feature)
  {
    available: result[:available],
    feature: feature,
    snapname: @options[:snapname],
    nodes: result[:nodes]
  }
end

#perform_operation(id, feature) ⇒ Integer

Performs the feature availability check.

Parameters:

  • id (Integer)

    resource identifier

  • feature (String)

    feature name

Returns:

  • (Integer)

    exit code



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/pvectl/commands/feature_command.rb', line 109

def perform_operation(id, feature)
  load_config
  connection = Pvectl::Connection.new(@config)

  resource = resolve_resource(connection, id)
  return resource_not_found(id) if resource.nil?

  result = build_repository(connection)
    .feature_available?(id, resource.node, feature, snapname: @options[:snapname])

  output_result(result, feature)
  result[:available] ? ExitCodes::SUCCESS : ExitCodes::GENERAL_ERROR
rescue Pvectl::Config::ConfigNotFoundError,
       Pvectl::Config::InvalidConfigError,
       Pvectl::Config::ContextNotFoundError,
       Pvectl::Config::ClusterNotFoundError,
       Pvectl::Config::UserNotFoundError
  raise
rescue StandardError => e
  $stderr.puts "Error: #{e.message}"
  ExitCodes::GENERAL_ERROR
end

#resolve_resource(connection, id) ⇒ Models::Vm, ...

Resolves the resource (VM or container) by ID using the appropriate repository.

Parameters:

  • connection (Connection)

    API connection

  • id (Integer)

    resource ID

Returns:



137
138
139
# File 'lib/pvectl/commands/feature_command.rb', line 137

def resolve_resource(connection, id)
  build_repository(connection).get(id)
end

#resource_not_found(id) ⇒ Integer

Outputs "not found" error and returns the not-found exit code.

Parameters:

  • id (Integer)

Returns:

  • (Integer)


224
225
226
227
# File 'lib/pvectl/commands/feature_command.rb', line 224

def resource_not_found(id)
  $stderr.puts "Error: No #{type_name} found with ID #{id}"
  ExitCodes::NOT_FOUND
end

#resource_type_symbolSymbol

Returns the resource type symbol (:vm or :container).

Returns:

  • (Symbol)


86
87
88
# File 'lib/pvectl/commands/feature_command.rb', line 86

def resource_type_symbol
  self.class::RESOURCE_TYPE
end

#stringify_keys(hash) ⇒ Hash

Converts symbol keys to strings for YAML output.

Parameters:

  • hash (Hash)

Returns:

  • (Hash)


207
208
209
# File 'lib/pvectl/commands/feature_command.rb', line 207

def stringify_keys(hash)
  hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v }
end

#type_nameString

Returns the singular type name ("VM" or "container") used in error messages.

Returns:

  • (String)


100
101
102
# File 'lib/pvectl/commands/feature_command.rb', line 100

def type_name
  resource_type_symbol == :vm ? "VM" : "container"
end

#usage_error(message) ⇒ Integer

Outputs usage error and returns the usage exit code.

Parameters:

  • message (String)

Returns:

  • (Integer)


215
216
217
218
# File 'lib/pvectl/commands/feature_command.rb', line 215

def usage_error(message)
  $stderr.puts "Error: #{message}"
  ExitCodes::USAGE_ERROR
end