durable-proxmox-ruby 0.1.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 +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +21 -0
- data/README.md +572 -0
- data/durable-proxmox-ruby.gemspec +47 -0
- data/exe/proxmox +218 -0
- data/lib/durable_proxmox/client.rb +262 -0
- data/lib/durable_proxmox/dsl.rb +374 -0
- data/lib/durable_proxmox/errors.rb +44 -0
- data/lib/durable_proxmox/ip_address_group.rb +99 -0
- data/lib/durable_proxmox/node.rb +126 -0
- data/lib/durable_proxmox/version.rb +5 -0
- data/lib/durable_proxmox/vm.rb +200 -0
- data/lib/durable_proxmox.rb +31 -0
- metadata +201 -0
data/exe/proxmox
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Only require bundler/setup when not running as a packaged tebako executable
|
|
5
|
+
# Check if we're running from the tebako memfs
|
|
6
|
+
unless $LOAD_PATH.first&.start_with?('/__tebako_memfs__')
|
|
7
|
+
require "bundler/setup"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require "durable_proxmox"
|
|
11
|
+
require "optparse"
|
|
12
|
+
|
|
13
|
+
class ProxmoxCLI
|
|
14
|
+
def initialize
|
|
15
|
+
@options = {}
|
|
16
|
+
@parser = create_parser
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run(args)
|
|
20
|
+
@parser.parse!(args)
|
|
21
|
+
command = args.shift
|
|
22
|
+
|
|
23
|
+
case command
|
|
24
|
+
when "nodes"
|
|
25
|
+
list_nodes
|
|
26
|
+
when "vms"
|
|
27
|
+
list_vms(args.shift)
|
|
28
|
+
when "start"
|
|
29
|
+
start_vm(args)
|
|
30
|
+
when "stop"
|
|
31
|
+
stop_vm(args)
|
|
32
|
+
when "delete"
|
|
33
|
+
delete_vm(args)
|
|
34
|
+
when "spawn"
|
|
35
|
+
spawn_vm(args)
|
|
36
|
+
when "ips"
|
|
37
|
+
show_ips(args)
|
|
38
|
+
when "export"
|
|
39
|
+
export_dsl(args)
|
|
40
|
+
when "version"
|
|
41
|
+
puts "durable-proxmox-ruby v#{DurableProxmox::VERSION}"
|
|
42
|
+
else
|
|
43
|
+
puts @parser.help
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
puts "Error: #{e.message}"
|
|
48
|
+
puts e.backtrace if @options[:debug]
|
|
49
|
+
exit 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def create_parser
|
|
55
|
+
OptionParser.new do |opts|
|
|
56
|
+
opts.banner = "Usage: proxmox [options] COMMAND [args]"
|
|
57
|
+
opts.separator ""
|
|
58
|
+
opts.separator "Commands:"
|
|
59
|
+
opts.separator " nodes List all Proxmox nodes"
|
|
60
|
+
opts.separator " vms [NODE] List VMs (optionally filter by node name)"
|
|
61
|
+
opts.separator " start NODE VMID Start a VM"
|
|
62
|
+
opts.separator " stop NODE VMID Stop a VM"
|
|
63
|
+
opts.separator " delete NODE VMID Delete a VM"
|
|
64
|
+
opts.separator " spawn NODE HOSTNAME Spawn a new VM from template"
|
|
65
|
+
opts.separator " ips NODE VMID Show IP addresses for a VM"
|
|
66
|
+
opts.separator " export [NODE] [FILE] Export infrastructure as DSL script"
|
|
67
|
+
opts.separator " version Show version"
|
|
68
|
+
opts.separator ""
|
|
69
|
+
opts.separator "Options:"
|
|
70
|
+
|
|
71
|
+
opts.on("-d", "--debug", "Enable debug mode") do
|
|
72
|
+
@options[:debug] = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
opts.on("--insecure", "Skip SSL certificate validation and expiry checks") do
|
|
76
|
+
@options[:insecure] = true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
80
|
+
puts opts
|
|
81
|
+
exit
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def client
|
|
87
|
+
@client ||= DurableProxmox.client(nil, insecure: @options[:insecure])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def list_nodes
|
|
91
|
+
nodes = client.nodes
|
|
92
|
+
puts "Nodes:"
|
|
93
|
+
nodes.each do |node|
|
|
94
|
+
puts " #{node.node_id} (#{node.status}) - CPU: #{(node.cpu * 100).round(2)}%, Memory: #{node.memory}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def list_vms(node_filter = nil)
|
|
99
|
+
nodes = client.nodes
|
|
100
|
+
nodes = nodes.select { |n| n.node_id == node_filter } if node_filter
|
|
101
|
+
|
|
102
|
+
nodes.each do |node|
|
|
103
|
+
puts "Node: #{node.node_id}"
|
|
104
|
+
vms = node.vms
|
|
105
|
+
if vms.empty?
|
|
106
|
+
puts " No VMs"
|
|
107
|
+
else
|
|
108
|
+
vms.each do |vm|
|
|
109
|
+
puts " [#{vm.vm_id}] #{vm.name} (#{vm.status})"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
puts
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def find_vm(node_name, vmid)
|
|
117
|
+
node = client.nodes.find { |n| n.node_id == node_name }
|
|
118
|
+
raise "Node '#{node_name}' not found" unless node
|
|
119
|
+
|
|
120
|
+
vm = node.vms.find { |v| v.vm_id.to_s == vmid.to_s }
|
|
121
|
+
raise "VM '#{vmid}' not found on node '#{node_name}'" unless vm
|
|
122
|
+
|
|
123
|
+
[node, vm]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def start_vm(args)
|
|
127
|
+
raise "Usage: proxmox start NODE VMID" if args.length < 2
|
|
128
|
+
|
|
129
|
+
node_name, vmid = args
|
|
130
|
+
_node, vm = find_vm(node_name, vmid)
|
|
131
|
+
|
|
132
|
+
puts "Starting VM #{vmid} on node #{node_name}..."
|
|
133
|
+
vm.start
|
|
134
|
+
puts "VM started successfully"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def stop_vm(args)
|
|
138
|
+
raise "Usage: proxmox stop NODE VMID" if args.length < 2
|
|
139
|
+
|
|
140
|
+
node_name, vmid = args
|
|
141
|
+
_node, vm = find_vm(node_name, vmid)
|
|
142
|
+
|
|
143
|
+
puts "Stopping VM #{vmid} on node #{node_name}..."
|
|
144
|
+
vm.stop
|
|
145
|
+
puts "VM stopped successfully"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def delete_vm(args)
|
|
149
|
+
raise "Usage: proxmox delete NODE VMID" if args.length < 2
|
|
150
|
+
|
|
151
|
+
node_name, vmid = args
|
|
152
|
+
node, vm = find_vm(node_name, vmid)
|
|
153
|
+
|
|
154
|
+
puts "Deleting VM #{vmid} on node #{node_name}..."
|
|
155
|
+
node.delete_vm(vm)
|
|
156
|
+
puts "VM deleted successfully"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def spawn_vm(args)
|
|
160
|
+
raise "Usage: proxmox spawn NODE HOSTNAME" if args.length < 2
|
|
161
|
+
|
|
162
|
+
node_name, hostname = args
|
|
163
|
+
node = client.nodes.find { |n| n.node_id == node_name }
|
|
164
|
+
raise "Node '#{node_name}' not found" unless node
|
|
165
|
+
|
|
166
|
+
puts "Spawning VM '#{hostname}' on node #{node_name}..."
|
|
167
|
+
new_vmid = node.spawn_vm(hostname)
|
|
168
|
+
puts "VM spawned successfully with ID: #{new_vmid}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def show_ips(args)
|
|
172
|
+
raise "Usage: proxmox ips NODE VMID" if args.length < 2
|
|
173
|
+
|
|
174
|
+
node_name, vmid = args
|
|
175
|
+
_node, vm = find_vm(node_name, vmid)
|
|
176
|
+
|
|
177
|
+
puts "IP addresses for VM #{vmid} (#{vm.name}):"
|
|
178
|
+
vm.network_interfaces.each do |iface|
|
|
179
|
+
next if iface["name"] == "lo"
|
|
180
|
+
|
|
181
|
+
puts " Interface: #{iface['name']}"
|
|
182
|
+
if iface["ip-addresses"]
|
|
183
|
+
iface["ip-addresses"].each do |ip|
|
|
184
|
+
puts " #{ip['ip-address']}/#{ip['prefix']}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def export_dsl(args)
|
|
191
|
+
node_filter = args.shift
|
|
192
|
+
output_file = args.shift
|
|
193
|
+
|
|
194
|
+
# Find the node if specified
|
|
195
|
+
target_node = nil
|
|
196
|
+
if node_filter
|
|
197
|
+
target_node = client.nodes.find { |n| n.node_id == node_filter }
|
|
198
|
+
raise "Node '#{node_filter}' not found" unless target_node
|
|
199
|
+
puts "Exporting configuration for node: #{node_filter}..."
|
|
200
|
+
else
|
|
201
|
+
puts "Exporting configuration for all nodes..."
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Generate the DSL script
|
|
205
|
+
script = client.export_configuration(target_node)
|
|
206
|
+
|
|
207
|
+
# Output to file or stdout
|
|
208
|
+
if output_file
|
|
209
|
+
File.write(output_file, script)
|
|
210
|
+
puts "Configuration exported to: #{output_file}"
|
|
211
|
+
puts "To apply: ruby #{output_file}"
|
|
212
|
+
else
|
|
213
|
+
puts script
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
ProxmoxCLI.new.run(ARGV)
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module DurableProxmox
|
|
7
|
+
# Main client for interacting with Proxmox API.
|
|
8
|
+
#
|
|
9
|
+
# Handles authentication, HTTP requests, and provides access to nodes and VMs.
|
|
10
|
+
class Client
|
|
11
|
+
# @return [String] The base URL for the Proxmox API
|
|
12
|
+
attr_reader :url_base
|
|
13
|
+
|
|
14
|
+
# @return [Faraday::Connection] The HTTP connection
|
|
15
|
+
attr_reader :connection
|
|
16
|
+
|
|
17
|
+
# @return [Hash] Authentication parameters
|
|
18
|
+
attr_reader :auth_params
|
|
19
|
+
|
|
20
|
+
# Creates a new Proxmox client with automatic authentication.
|
|
21
|
+
#
|
|
22
|
+
# @param url_base [String, nil] The base URL for the Proxmox API.
|
|
23
|
+
# Defaults to ENV['PROXMOX_HOSTNAME'] or a default URL.
|
|
24
|
+
# @param insecure [Boolean] Skip SSL certificate validation and expiry checks (default: false).
|
|
25
|
+
# Set to true for self-signed or expired certificates.
|
|
26
|
+
def initialize(url_base = nil, insecure: false)
|
|
27
|
+
hostname = ENV['PROXMOX_HOSTNAME'] || 'fuzzyfireball.durablelan'
|
|
28
|
+
@url_base = url_base || "https://#{hostname}:8006/api2/json/"
|
|
29
|
+
@insecure = insecure
|
|
30
|
+
|
|
31
|
+
@connection = create_connection
|
|
32
|
+
@auth_params = create_auth_header
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Creates a new Faraday connection with appropriate SSL settings.
|
|
36
|
+
#
|
|
37
|
+
# @return [Faraday::Connection] A new connection instance
|
|
38
|
+
def create_connection
|
|
39
|
+
ssl_options = if @insecure
|
|
40
|
+
{
|
|
41
|
+
verify: false,
|
|
42
|
+
version: :TLSv1_2
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
{ verify: true }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Faraday.new(@url_base, headers: { "user_agent": 'DurableProxmox - As Suave As They Come', "Accept-Encoding": 'identity' }, ssl: ssl_options) do |f|
|
|
49
|
+
f.request :url_encoded
|
|
50
|
+
f.response :json, content_type: /\bjson$/
|
|
51
|
+
f.adapter :net_http do |client|
|
|
52
|
+
client.keep_alive_timeout = 5
|
|
53
|
+
client.read_timeout = 60
|
|
54
|
+
client.write_timeout = 60
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates an authentication header for API token access.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] Authentication parameters with Authorization header
|
|
62
|
+
# @raise [RuntimeError] If required environment variables are missing
|
|
63
|
+
def create_auth_header
|
|
64
|
+
username = ENV['PROXMOX_USERNAME']
|
|
65
|
+
token_name = ENV['PROXMOX_API_TOKEN_NAME']
|
|
66
|
+
token_secret = ENV['PROXMOX_API_TOKEN_SECRET']
|
|
67
|
+
|
|
68
|
+
raise AuthenticationError, 'PROXMOX_USERNAME environment variable is required' unless username
|
|
69
|
+
raise AuthenticationError, 'PROXMOX_API_TOKEN_NAME environment variable is required' unless token_name
|
|
70
|
+
raise AuthenticationError, 'PROXMOX_API_TOKEN_SECRET environment variable is required' unless token_secret
|
|
71
|
+
|
|
72
|
+
token = "#{username}!#{token_name}=#{token_secret}"
|
|
73
|
+
{
|
|
74
|
+
'Authorization' => "PVEAPIToken=#{token}"
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Performs an authenticated POST request.
|
|
79
|
+
#
|
|
80
|
+
# @param path [String] The API endpoint path
|
|
81
|
+
# @param params [Hash] Request parameters
|
|
82
|
+
# @return [Faraday::Response] The response
|
|
83
|
+
def post(path, params = {})
|
|
84
|
+
@connection.post(path, params) do |req|
|
|
85
|
+
req.headers.merge!(@auth_params)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Performs an authenticated DELETE request.
|
|
90
|
+
#
|
|
91
|
+
# @param path [String] The API endpoint path
|
|
92
|
+
# @param params [Hash] Request parameters
|
|
93
|
+
# @return [Faraday::Response] The response
|
|
94
|
+
def delete(path, params = {})
|
|
95
|
+
@connection.delete(path, params) do |req|
|
|
96
|
+
req.headers.merge!(@auth_params)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Performs an authenticated GET request.
|
|
101
|
+
#
|
|
102
|
+
# @param path [String] The API endpoint path
|
|
103
|
+
# @param params [Hash] Request parameters
|
|
104
|
+
# @return [Faraday::Response] The response
|
|
105
|
+
def get(path, params = {})
|
|
106
|
+
retries = 0
|
|
107
|
+
max_retries = 3
|
|
108
|
+
begin
|
|
109
|
+
# Create a new connection for each request to avoid connection reuse issues
|
|
110
|
+
conn = create_connection
|
|
111
|
+
conn.get(path, params) do |req|
|
|
112
|
+
req.headers.merge!(@auth_params)
|
|
113
|
+
end
|
|
114
|
+
rescue EOFError, OpenSSL::SSL::SSLError, Faraday::ConnectionFailed, Faraday::SSLError => e
|
|
115
|
+
retries += 1
|
|
116
|
+
$stderr.puts "Request failed (attempt #{retries}/#{max_retries}): #{e.class} - #{e.message}" if ENV['DEBUG']
|
|
117
|
+
if retries <= max_retries
|
|
118
|
+
sleep 0.5
|
|
119
|
+
retry
|
|
120
|
+
else
|
|
121
|
+
raise
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Retrieves all nodes from the Proxmox cluster.
|
|
127
|
+
#
|
|
128
|
+
# @return [Array<DurableProxmox::Node>] Array of node objects
|
|
129
|
+
def nodes
|
|
130
|
+
get('nodes').body['data'].map { |data| Node.new(data, self) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Waits for an asynchronous Proxmox task to complete.
|
|
134
|
+
#
|
|
135
|
+
# @param node_or_node_id [DurableProxmox::Node, String] The node or node ID
|
|
136
|
+
# @param upid [String] The task UPID
|
|
137
|
+
# @param show_progress [Boolean] Show a progress bar while waiting (default: true)
|
|
138
|
+
def wait_for_upid(node_or_node_id, upid, show_progress: true)
|
|
139
|
+
node_id = node_or_node_id.respond_to?(:node_id) ? node_or_node_id.node_id : node_or_node_id
|
|
140
|
+
elapsed = 0
|
|
141
|
+
|
|
142
|
+
if show_progress
|
|
143
|
+
print "Waiting for task to complete "
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
loop do
|
|
147
|
+
task_status = get("nodes/#{node_id}/tasks/#{upid}/status").body
|
|
148
|
+
status = task_status.dig('data', 'status')
|
|
149
|
+
break unless status == 'running'
|
|
150
|
+
|
|
151
|
+
if show_progress
|
|
152
|
+
print "."
|
|
153
|
+
$stdout.flush
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
sleep 1
|
|
157
|
+
elapsed += 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if show_progress
|
|
161
|
+
puts " done (#{elapsed}s)"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check if task succeeded or failed
|
|
165
|
+
task_status = get("nodes/#{node_id}/tasks/#{upid}/status").body
|
|
166
|
+
exitstatus = task_status.dig('data', 'exitstatus')
|
|
167
|
+
|
|
168
|
+
if exitstatus && exitstatus != 'OK'
|
|
169
|
+
$stderr.puts "Warning: Task completed with status: #{exitstatus}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Exports the current Proxmox infrastructure as a DSL script.
|
|
174
|
+
#
|
|
175
|
+
# @param node [DurableProxmox::Node, nil] Optional specific node to export (exports all if nil)
|
|
176
|
+
# @param options [Hash] Export options
|
|
177
|
+
# @option options [Boolean] :include_stopped Include stopped VMs (default: true)
|
|
178
|
+
# @option options [Boolean] :include_templates Include template VMs (default: false)
|
|
179
|
+
# @return [String] The generated DSL script
|
|
180
|
+
def export_configuration(node = nil, options = {})
|
|
181
|
+
include_stopped = options.fetch(:include_stopped, true)
|
|
182
|
+
include_templates = options.fetch(:include_templates, false)
|
|
183
|
+
|
|
184
|
+
target_nodes = node ? [node] : nodes
|
|
185
|
+
|
|
186
|
+
script_lines = []
|
|
187
|
+
script_lines << '#!/usr/bin/env ruby'
|
|
188
|
+
script_lines << '# frozen_string_literal: true'
|
|
189
|
+
script_lines << ''
|
|
190
|
+
script_lines << "# Generated by durable-proxmox-ruby on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
191
|
+
script_lines << '# This script defines the current Proxmox infrastructure'
|
|
192
|
+
script_lines << ''
|
|
193
|
+
script_lines << "require 'durable_proxmox'"
|
|
194
|
+
script_lines << ''
|
|
195
|
+
script_lines << '# Apply this configuration with: ruby <filename>.rb'
|
|
196
|
+
script_lines << ''
|
|
197
|
+
script_lines << 'DurableProxmox::DSL.configure do'
|
|
198
|
+
|
|
199
|
+
target_nodes.each do |target_node|
|
|
200
|
+
vms = target_node.vms
|
|
201
|
+
|
|
202
|
+
# Filter VMs based on options
|
|
203
|
+
vms = vms.reject { |vm| vm.status == 'stopped' } unless include_stopped
|
|
204
|
+
vms = vms.reject { |vm| vm.data['template'] == 1 } unless include_templates
|
|
205
|
+
|
|
206
|
+
vms.each do |vm|
|
|
207
|
+
script_lines << generate_vm_dsl(vm, target_node)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
script_lines << 'end.apply'
|
|
212
|
+
script_lines << ''
|
|
213
|
+
|
|
214
|
+
script_lines.join("\n")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
# Generates DSL code for a single VM.
|
|
220
|
+
#
|
|
221
|
+
# @param vm [DurableProxmox::VM] The VM to export
|
|
222
|
+
# @param node [DurableProxmox::Node] The node the VM is on
|
|
223
|
+
# @return [String] The DSL code for the VM
|
|
224
|
+
def generate_vm_dsl(vm, node)
|
|
225
|
+
config = vm.config
|
|
226
|
+
lines = []
|
|
227
|
+
|
|
228
|
+
lines << ''
|
|
229
|
+
lines << " # Node: #{node.node_id}"
|
|
230
|
+
lines << " # Status: #{vm.status}"
|
|
231
|
+
lines << " vm '#{vm.name}' do"
|
|
232
|
+
lines << " vmid #{vm.vm_id}"
|
|
233
|
+
|
|
234
|
+
# Extract cores
|
|
235
|
+
lines << " cores #{config['cores']}" if config['cores']
|
|
236
|
+
|
|
237
|
+
# Extract memory (convert from bytes to MB if needed)
|
|
238
|
+
if config['memory']
|
|
239
|
+
memory_mb = config['memory']
|
|
240
|
+
lines << " memory #{memory_mb}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Extract IP address if available
|
|
244
|
+
begin
|
|
245
|
+
ips = vm.ip_addresses
|
|
246
|
+
if ips && !ips.octets.empty?
|
|
247
|
+
primary_ip = ips.octets.first
|
|
248
|
+
lines << " ip '#{primary_ip}'" if primary_ip
|
|
249
|
+
end
|
|
250
|
+
rescue StandardError
|
|
251
|
+
# IP address may not be available, skip
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Note about template
|
|
255
|
+
lines << ' # This is a template VM' if config['template'] == 1
|
|
256
|
+
|
|
257
|
+
lines << ' end'
|
|
258
|
+
|
|
259
|
+
lines.join("\n")
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|