├── .gitignore ├── proxmox_tools.gemspec ├── config.example.json ├── lib ├── proxmox_api.rb └── proxmox_tools.rb └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | *.gem 3 | proxmox.json 4 | -------------------------------------------------------------------------------- /proxmox_tools.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'proxmox_tools' 3 | s.version = '0.0.1' 4 | s.date = '2018-06-25' 5 | s.summary = "Proxmox Tools" 6 | s.description = "Tools that externalis the configuration and clones virtual machine in Proxmox, allowing virtual machines to be cloned with one command. These tools are Idempotent allowing them to be intergrated in Conguration mangers such as Chef or Ansible" 7 | s.authors = ["Jonahtan Weems", "The Server Guys Ltd"] 8 | s.email = 'jweems@serverdevs.com' 9 | s.files = ["lib/proxmox_tools.rb"] 10 | s.homepage = 11 | 'http://rubygems.org/gems/proxmox_tools' 12 | s.license = 'MIT' 13 | end -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mount": { 3 | "local": false, 4 | "host": "172.16.0.1", 5 | "username": "username" 6 | }, 7 | "proxmox_api": { 8 | "address": "127.0.0.1", 9 | "port": "8006", 10 | "version": "api2", 11 | "type": "json", 12 | "username": "username", 13 | "password": "password" 14 | }, 15 | "instance": { 16 | "name": "test", 17 | "description": "this is a test instance", 18 | "storage": "", 19 | "vmid": "125", 20 | "node": "h2", 21 | "format": "qcow2", 22 | "vgname" : "centos", 23 | "full": true, 24 | "ips": [ 25 | "172.16.1.21", 26 | "192.168.1.21" 27 | ], 28 | "config" : { 29 | "autostart" : "yes", 30 | "cores" : "2", 31 | "balloon" : "256", 32 | "memory" : "512", 33 | "onboot" : "true", 34 | "sockets" : "1" 35 | }, 36 | "replacements": { 37 | "/etc/sysconfig/network-scripts/ifcfg-ens18": { 38 | "TYPE": "Ethernet", 39 | "BOOTPROTO": "static", 40 | "DEFROUTE": "yes", 41 | "IPV4_FAILURE_FATAL": "no", 42 | "IPV6INIT": "yes", 43 | "IPV6_AUTOCONF": "yes", 44 | "IPV6_DEFROUTE": "yes", 45 | "IPV6_FAILURE_FATAL": "no", 46 | "NAME": "ens18", 47 | "UUID": "", 48 | "DEVICE": "ens18", 49 | "ONBOOT": "yes", 50 | "IPADDR": "172.16.1.4", 51 | "PREFIX": "12", 52 | "BROADCAST": "172.31.255.255", 53 | "IPV6_PEERDNS": "yes", 54 | "IPV6_PEERROUTES": "yes", 55 | "IPV6_PRIVACY": "no", 56 | "IPADDR0": "91.134.251.148", 57 | "PREFIX0": "28", 58 | "NETMASK0": "255.255.255.240", 59 | "GATEWAY0": "91.134.251.158" 60 | }, 61 | "/etc/sysconfig/network-scripts/ifcfg-ens19": { 62 | "TYPE": "Ethernet", 63 | "BOOTPROTO": "static", 64 | "NAME": "ens19", 65 | "UUID": "", 66 | "DEVICE": "ens19", 67 | "ONBOOT": "yes", 68 | "IPADDR": "192.168.1.20", 69 | "NETMASK": "255.255.0.0", 70 | "BROADCAST": "192.168.255.255", 71 | "GATEWAY": "192.168.0.254" 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /lib/proxmox_api.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'httparty' 4 | require 'json' 5 | require 'cgi' 6 | 7 | class ProxmoxApi 8 | @base_address 9 | @username 10 | @password 11 | @verify 12 | @authenticated = false 13 | @ticket 14 | @headers 15 | 16 | def initialize(address, port, version, type, verify, username, password) 17 | @base_address = "https://#{address}:#{port}/#{version}/#{type}" 18 | @username = username 19 | @password = password 20 | @verify = verify 21 | end 22 | 23 | def do_authentication 24 | response = HTTParty.post("#{@base_address}/access/ticket?username=#{@username}@pam&password=#{@password}", :verify => @verify ) 25 | if response.code == 401 26 | puts "Authenication Failed, please check username and password. exiting" 27 | exit(1) 28 | return false; 29 | end 30 | if response.code != 200 31 | puts "Failed login to api. error code " + response.code 32 | return false 33 | end 34 | data = JSON.parse(response.response.body)["data"] 35 | @headers = { 36 | "CSRFPreventionToken" => data['CSRFPreventionToken'] 37 | } 38 | @ticket = data['ticket'] 39 | @authenticated = true; 40 | end 41 | 42 | def authenticated 43 | unless @authenticated 44 | do_authentication 45 | end 46 | end 47 | 48 | def next_vmid 49 | authenticated 50 | response = JSON.parse HTTParty.get("#{@base_address}/cluster/nextid", :verify => @verify, :cookies => { PVEAuthCookie: @ticket } ).response.body 51 | response['data'] 52 | end 53 | 54 | def clone_vm(node, vmid, name, description, storage, format, full, ips) 55 | authenticated 56 | nvmid = next_vmid 57 | url = "#{@base_address}/nodes/#{node}/qemu/#{vmid}/clone?newid=#{nvmid}" 58 | if name != '' 59 | url += '&name=' + CGI.escape(name) 60 | end 61 | if description != '' 62 | url += '&description=' + CGI.escape(description) 63 | url += CGI.escape("\nIP Addresses") 64 | ips.each do |ip| 65 | url += CGI.escape("\n" + ip) 66 | end 67 | end 68 | if storage != '' 69 | url += '&storage=' + CGI.escape(storage) 70 | end 71 | if format != '' 72 | url += '&format=' + CGI.escape(format) 73 | end 74 | if full 75 | url += '&full=1' 76 | end 77 | response = HTTParty.post(url, :verify => @verify, :cookies => { PVEAuthCookie: @ticket }, :headers => @headers ) 78 | if response.code != 200 79 | puts 'Failed to clone VM, Response ' + response.code.to_s 80 | p response.body 81 | exit(2) 82 | end 83 | response = JSON.parse response.response.body 84 | clone_reponse = CloneResponse.new 85 | clone_reponse.vmid = nvmid 86 | clone_reponse.task_id = response['data'] 87 | clone_reponse 88 | end 89 | 90 | def task_log (node, task_id, start) 91 | JSON.parse HTTParty.get("#{@base_address}/nodes/#{node}/tasks/#{task_id}/log?start=#{start}", :verify => @verify, :cookies => { PVEAuthCookie: @ticket } ).response.body 92 | end 93 | 94 | def update_vm_config (node, vmid, configs) 95 | request = HTTParty.post("#{@base_address}/nodes/#{node}/qemu/#{vmid}/config", 96 | :verify => @verify, 97 | :cookies => { PVEAuthCookie: @ticket }, 98 | :headers => @headers, 99 | :query => configs 100 | ) 101 | if request.code != 200 102 | puts 'Failed to set configs, code ' + request.code.to_s + " returned. Error details " + request.response.body 103 | exit(2) 104 | end 105 | end 106 | 107 | def start_vm (node, vmid) 108 | response = HTTParty.post("#{@base_address}/nodes/#{node}/qemu/#{vmid}/status/start", :verify => @verify, :cookies => { PVEAuthCookie: @ticket }, :headers => @headers ) 109 | response = JSON.parse response.response.body 110 | response['data'] 111 | end 112 | end 113 | 114 | class CloneResponse 115 | attr_accessor :task_id 116 | attr_accessor :vmid 117 | end -------------------------------------------------------------------------------- /lib/proxmox_tools.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'json' 5 | require 'ostruct' 6 | require 'cgi' 7 | require 'net/ping' 8 | require 'net/ssh' 9 | require_relative 'proxmox_api' 10 | 11 | options = OpenStruct.new 12 | 13 | outputlevel = 0; 14 | quietRun = 0; 15 | OptionParser.new do |opt| 16 | opt.banner = "Usage: #{$PROGRAM_NAME} node vmid [options]" 17 | opt.on('-m', '--vmid VMID', 'Virtual machine id to clone from.') { |o| options.vmid = o } 18 | opt.on('-e', '--node NODE', 'Proxmox deployment node.') { |o| options.node = o } 19 | opt.on('-n', '--name NAME', 'Name for new VM.') { |o| options.name = o } 20 | opt.on('-d', '--description DESCRIPTION', 'New VM description.') { |o| options.description = o } 21 | opt.on('-s', '--storage STORAGE', 'Target storage for new VM.') { |o| options.storage = o } 22 | opt.on('-u', '--username USERNAME', 'Username for proxmox API.') { |o| options.username = o } 23 | opt.on('-p', '--password PASSWORD', 'Password for proxmox API.') { |o| options.password = o } 24 | opt.on('-c', '--config CONFIG', 'Location of config (default config.json)') { |o| options.config = o } 25 | opt.on('-g', '--vgname NAME', 'Name of volume group.') { |o| options.vgname = o } 26 | opt.on('-v', '--verbose', 'Explains what is being done') { outputlevel = 1 } 27 | opt.on('-q', '--quiet', 'Silent all output except error, even if verbose') { quietRun = 1 } 28 | opt.on('--address ADDRESS', 'Proxmox api address (default 127.0.0.1)') { |o| options.address = o } 29 | opt.on('--port POST', 'Proxmox api port (default 8006)') { |o| options.port = o } 30 | opt.on('--version VERSION', 'Proxmox api version (default api2)') { |o| options.version = o } 31 | opt.on('--type TYPE', 'Proxmox api type (default json)') { |o| options.type = o } 32 | opt.on("-h", "--help", "Prints this help") do 33 | puts opt 34 | exit 35 | end 36 | end.parse! 37 | 38 | address = '127.0.0.1' 39 | port = '8006' 40 | version = 'api2' 41 | type = 'json' 42 | username = '' 43 | password = '' 44 | 45 | name = 'proxmox-tools' 46 | description = '' 47 | storage = '' 48 | ips = [] 49 | vmid = '' 50 | node = '' 51 | format = 'qcow2' 52 | full = true 53 | 54 | local = true 55 | sshHost = "127.0.0.1" 56 | sshPassword = "password" 57 | sshUsername = "username" 58 | vgname = "" 59 | 60 | 61 | 62 | configFilename = 'config.json' 63 | 64 | if options.config.to_s != '' 65 | configFilename = options.config.to_s 66 | end 67 | configContents = '' 68 | if File.file?(configFilename) 69 | configContents = JSON.parse(File.read(File.join(Dir.pwd,configFilename))) 70 | if configContents.include?("mount") 71 | if configContents["mount"].include?("local") 72 | local = configContents["mount"]["local"] 73 | end 74 | if configContents["mount"].include?("host") 75 | sshHost = configContents["mount"]["host"] 76 | end 77 | if configContents["mount"].include?("password") 78 | sshPassword = configContents["mount"]["password"] 79 | end 80 | if configContents["mount"].include?("username") 81 | sshUsername = configContents["mount"]["username"] 82 | end 83 | end 84 | if configContents.include?("proxmox_api") 85 | if configContents["proxmox_api"].include?("address") 86 | address = configContents["proxmox_api"]["address"] 87 | end 88 | if configContents["proxmox_api"].include?("port") 89 | port = configContents["proxmox_api"]["port"] 90 | end 91 | if configContents["proxmox_api"].include?("version") 92 | version = configContents["proxmox_api"]["version"] 93 | end 94 | if configContents["proxmox_api"].include?("type") 95 | type = configContents["proxmox_api"]["type"] 96 | end 97 | if configContents["proxmox_api"].include?("username") 98 | username = configContents["proxmox_api"]["username"] 99 | end 100 | if configContents["proxmox_api"].include?("password") 101 | password = configContents["proxmox_api"]["password"] 102 | end 103 | end 104 | if configContents.include?("instance") 105 | if configContents["instance"].include?("name") 106 | name = configContents["instance"]["name"] 107 | end 108 | if configContents["instance"].include?("description") 109 | description = configContents["instance"]["description"] 110 | end 111 | if configContents["instance"].include?("storage") 112 | storage = configContents["instance"]["storage"] 113 | end 114 | if configContents["instance"].include?("vmid") 115 | vmid = configContents["instance"]["vmid"] 116 | end 117 | if configContents["instance"].include?("node") 118 | node = configContents["instance"]["node"] 119 | end 120 | if configContents["instance"].include?("format") 121 | format = configContents["instance"]["format"] 122 | end 123 | if configContents["instance"].include?("vgname") 124 | vgname = configContents["instance"]["vgname"] 125 | end 126 | if configContents["instance"].include?("full") 127 | full = configContents["instance"]["full"] 128 | end 129 | if configContents["instance"].include?("ips") 130 | ips = configContents["instance"]["ips"] 131 | ips.each do |ip| 132 | check = Net::Ping::External.new(ip) 133 | if check.ping? 134 | puts "#{ip} is already in use. please try a different ip" 135 | exit(0) 136 | end 137 | end 138 | end 139 | end 140 | else 141 | puts 'Configuration file not found. exiting' 142 | exit(1) 143 | end 144 | 145 | if options.address.to_s != '' 146 | address = options.address.to_s 147 | end 148 | if options.port.to_s != '' 149 | port = options.port.to_s 150 | end 151 | if options.version.to_s != '' 152 | version = options.version.to_s 153 | end 154 | if options.type.to_s != '' 155 | type = options.type.to_s 156 | end 157 | if options.username.to_s != '' 158 | username = options.username.to_s 159 | end 160 | if options.password.to_s != '' 161 | password = options.password.to_s 162 | end 163 | if options.name.to_s != '' 164 | name = options.name.to_s 165 | end 166 | if options.description.to_s != '' 167 | description = options.description.to_s 168 | end 169 | if options.storage.to_s != '' 170 | storage = options.storage.to_s 171 | end 172 | if options.vmid.to_s != '' 173 | vmid = options.vmid.to_s 174 | end 175 | if options.node.to_s != '' 176 | node = options.node.to_s 177 | end 178 | if options.vgname.to_s != '' 179 | node = options.vgname.to_s 180 | end 181 | 182 | 183 | 184 | proxmoxUrl = "https://#{address}:#{port}/#{version}/#{type}" 185 | 186 | if node.to_s == '' || vmid.to_s == '' || username.to_s == '' || password.to_s == '' || vgname.to_s == '' 187 | puts 'Missing some or all of the required arguments (vmid, node, username, password, vgname)' 188 | puts 'These can be passed as options or placed in the configuration file.' 189 | exit(1) 190 | end 191 | 192 | proxmox_api = ProxmoxApi.new(address, port, version, type, false, username, password) 193 | clone_response = proxmox_api.clone_vm(node, vmid, name, description, storage, format, full, ips) 194 | 195 | #login to the api and get ticket and csrf token 196 | # response = HTTParty.post("#{proxmoxUrl}/access/ticket?username=#{username}@pam&password=#{password}", :verify => false ) 197 | # if response.code == 401 198 | # puts "Authenication Failed, please check username and password. exiting" 199 | # exit(1) 200 | # end 201 | # if response.code != 200 202 | # puts "Failed login to api. error code " + response.code 203 | # end 204 | # data = JSON.parse(response.response.body)["data"] 205 | # crsf = data['CSRFPreventionToken'] 206 | # ticket = data['ticket'] 207 | # response = JSON.parse HTTParty.get("#{proxmoxUrl}/cluster/nextid", :verify => false, :cookies => { PVEAuthCookie: ticket } ).response.body 208 | # nextVmid = response['data'] 209 | # headers = { 210 | # "CSRFPreventionToken" => crsf 211 | # } 212 | # # Create url for instance clone with optional parameters 213 | # url = "#{proxmoxUrl}/nodes/#{node}/qemu/#{vmid}/clone?newid=#{nextVmid}" 214 | # if name != '' 215 | # url += "&name=" + CGI.escape(name) 216 | # end 217 | # if description != '' 218 | # url += "&description=" + CGI.escape(description) 219 | # url += CGI.escape("\nIP Addresses") 220 | # ips.each do |ip| 221 | # url += CGI.escape("\n" + ip) 222 | # end 223 | # end 224 | # if storage != '' 225 | # url += "&storage=" + CGI.escape(storage) 226 | # end 227 | # if format != '' 228 | # url += "&format=" + CGI.escape(format) 229 | # end 230 | # if full 231 | # url += "&full=1" 232 | # end 233 | # # Call the api to do the clone 234 | # response = HTTParty.post(url, :verify => false, :cookies => { PVEAuthCookie: ticket }, :headers => headers ) 235 | # if response.code != 200 236 | # puts 'Failed to clone VM, Response ' + response.code.to_s 237 | # p response.body 238 | # exit(2) 239 | # end 240 | 241 | # Check the status of instance, print logs and export when complete 242 | loop = true 243 | current_log = 0 244 | image_location = ""; 245 | storage_directory = ""; 246 | while loop 247 | proxmox_api.task_log(node, clone_response.task_id, current_log)['data'].each do |log| 248 | if quietRun == 0 && outputlevel == 1 249 | p log["t"] 250 | end 251 | current_log = log["n"].to_i 252 | # noinspection RubyUnusedLocalVariable 253 | if current_log == 2 254 | image_location = log["t"].split('\'')[1] 255 | storage_directory = image_location.split('/')[0...-1].join('/') 256 | end 257 | if log["t"] == "TASK OK" 258 | if quietRun == 0 259 | puts "New VM successfully created, image stored at #{image_location}" 260 | end 261 | loop = false 262 | end 263 | end 264 | sleep(0.2) 265 | end 266 | 267 | #mount vm image and change ip address 268 | if configContents.key?("replacements") 269 | Net::SSH.start(sshHost, sshUsername) do |session| 270 | if quietRun == 0 271 | puts "connected to host" 272 | end 273 | 274 | #Mount disc 275 | session.exec! "modprobe nbd max_part=8" 276 | session.exec! "qemu-nbd --connect=/dev/nbd0 #{image_location}" 277 | session.exec! "vgchange -ay centos" 278 | session.exec! "mkdir #{storage_directory}/disk" 279 | session.exec! "mount /dev/mapper/centos-root #{storage_directory}/disk" 280 | replacements = configContents["replacements"] 281 | replacements.each do |file, parts| 282 | parts.each do |field, value| 283 | session.exec! "sed -i s/^#{field}.*/#{field}=#{value}/ #{storage_directory}/disk#{file}" 284 | if quietRun == 0 && outputlevel == 1 285 | puts "replacing #{field}=#{value} in file #{storage_directory}/disk#{file}" 286 | end 287 | end 288 | end 289 | 290 | # unmount disc 291 | session.exec! "umount #{storage_directory}/disk" 292 | session.exec! "vgchange -an #{vgname}" 293 | session.exec! "qemu-nbd -d /dev/nbd0" 294 | session.exec! "rm -r #{storage_directory}/disk" 295 | if quietRun == 0 296 | puts "disk unmounted" 297 | end 298 | end 299 | end 300 | 301 | #Set the configs 302 | if configContents.key?("config") 303 | if quietRun == 0 304 | puts "Sending configs" 305 | end 306 | if quietRun == 0 && outputlevel == 1 307 | configContents["config"].each do |config, value| 308 | puts config + " " + value 309 | end 310 | end 311 | proxmox_api.update_vm_config(node, clone_response.vmid, configContents["config"]) 312 | end 313 | 314 | #Start the machine 315 | if quietRun == 0 316 | puts "Starting Virtual Machine" 317 | end 318 | task_id = proxmox_api.start_vm(node, clone_response.vmid) 319 | loop = 0 320 | current_log = 0 321 | while loop < 100 322 | loop += 1 323 | proxmox_api.task_log(node, task_id, current_log)['data'].each do |log| 324 | if quietRun == 0 && outputlevel == 1 325 | puts log["t"] 326 | end 327 | current_log = log["n"].to_i 328 | if log["t"] == "TASK OK" 329 | if quietRun == 0 330 | puts "New VM successfully started." 331 | end 332 | loop = 100 333 | end 334 | end 335 | sleep(0.2) 336 | end 337 | 338 | #Validating new instance is responding to ip 339 | if quietRun == 0 340 | puts 'Checking host is up and responding to pings' 341 | end 342 | loop = 0 343 | while loop < 110 344 | if loop == 100 345 | puts 'Host not up after 100 seconds, something probably went wrong.' 346 | exit(1) 347 | end 348 | check = Net::Ping::External.new(ips[0]) 349 | if check.ping? 350 | if quietRun == 0 351 | puts 'Host is responding to pings. Instance created successfully.' 352 | end 353 | exit(0) 354 | else 355 | if quietRun == 0 && outputlevel == 1 356 | puts 'Not up yet, retrying' 357 | end 358 | end 359 | sleep(1) 360 | end 361 | 362 | 363 | 364 | 365 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Proxmox Tools 2 | 3 | ## Description 4 | 5 | The main reason I developed this is because I wanted a way to automatically provision a new virtual machine with Ansible. 6 | 7 | The main issue I was facing when using templates with Proxmox is they retain their ip address and there isn't a way (as far as I am aware) to change the ip address before creating a new vm from a template. 8 | 9 | I did figure out that I was able to clone a template then mount the kvm image and change the network configuration on the host machine. 10 | 11 | From this proxmox tools was born, not really a comprehensive tool set at the moment, but a big first step. 12 | 13 | The script uses both the Proxmox API and ssh to achieve a quick and automated provisioning and you can use it to not only change the network configuration before lauching a new vm but also change any files on a system before starting for the first time. 14 | 15 | For some reason when I started this project I though it was a good time to start learning ruby..... 16 | 17 | So if you are a ruby expert you will probably find some very noob errors and unfortunately I haven't got time to refactor into something that looks nice. So pull requests to expand the functionality or coding standards are welcome. 18 | 19 | ## Installation 20 | 21 | ### Centos 7 22 | 23 | Clone the repository to any server that is able to ssh to the host, the server could even be a virtual machine of the host. 24 | 25 | #yum update 26 | #yum install epel-release ruby 27 | #gem install httparty net-ping net-ssh 28 | 29 | ### Optional Install 30 | 31 | Install globally 32 | 33 | #cp lib/proxmox_tools.rb /usr/local/bin/clone 34 | 35 | ## Configuration 36 | 37 | #cp config.example.json config.json 38 | 39 | ### mount 40 | 41 | To be able to change the ip address within the file system before starting the new virtual machine. The script needs to mount the filesystem on the hosting machine. So this section is the ssh details for the host machine. There will be the option to run it locally in the future by changing local to true. But this hasn't been implemented as I don't need it. 42 | 43 | Password login isn't supported at the moment so you need to make sure your user can ssh to the host with keys and it can mount, unmount and access the var/lib/vz/ directory on the host machine. 44 | 45 | "mount": { 46 | "local": false, 47 | "host": "172.16.0.1", 48 | "username": "username" 49 | }, 50 | 51 | ### proxmox api 52 | 53 | These are the credentials and location of the Proxmox api the user will need permissions to clone existing templates/vms and start vms. 54 | 55 | 56 | "proxmox_api": { 57 | "address": "172.16.0.1", 58 | "port": "8006", 59 | "version": "api2", 60 | "type": "json", 61 | "username": "username", 62 | "password": "password" 63 | }, 64 | 65 | ### instance 66 | 67 | These are the details for the new vm 68 | 69 | "instance": { 70 | "name": "test", //vm name 71 | "description": "this is a test instance", //New VM description. 72 | "storage": "", //Target storage for new VM. 73 | "vmid": "125", //Virtual machine id to clone from 74 | "node": "h2", //Proxmox deployment node 75 | "format": "qcow2", //Storage format 76 | "full": true, //Is this a full clone 77 | "ips": [ //New ip address. These are checked first to make sure we dont get conflicts 78 | "172.16.1.21", 79 | "192.168.1.21" 80 | ], 81 | 82 | The next section is for file replacements, ie changing the ip address etc. This example shows for a centos 7 virtual machine, but could be changed to work for ubuntu etc 83 | 84 | "replacements": { 85 | "/etc/sysconfig/network-scripts/ifcfg-ens18": { 86 | "TYPE": "Ethernet", 87 | "BOOTPROTO": "static", 88 | "DEFROUTE": "yes", 89 | "IPV4_FAILURE_FATAL": "no", 90 | "IPV6INIT": "yes", 91 | "IPV6_AUTOCONF": "yes", 92 | "IPV6_DEFROUTE": "yes", 93 | "IPV6_FAILURE_FATAL": "no", 94 | "NAME": "ens18", 95 | "UUID": "", 96 | "DEVICE": "ens18", 97 | "ONBOOT": "yes", 98 | "IPADDR": "172.16.1.21", 99 | "PREFIX": "12", 100 | "BROADCAST": "172.31.255.255", 101 | "IPV6_PEERDNS": "yes", 102 | "IPV6_PEERROUTES": "yes", 103 | "IPV6_PRIVACY": "no", 104 | "IPADDR0": "91.0.0.1", //External IP address 105 | "PREFIX0": "28", 106 | "NETMASK0": "255.255.255.240", 107 | "GATEWAY0": "91.0.0.1" 108 | }, 109 | "/etc/sysconfig/network-scripts/ifcfg-ens19": { 110 | "TYPE": "Ethernet", 111 | "BOOTPROTO": "static", 112 | "NAME": "ens19", 113 | "UUID": "", 114 | "DEVICE": "ens19", 115 | "ONBOOT": "yes", 116 | "IPADDR": "192.168.1.20", 117 | "NETMASK": "255.255.0.0", 118 | "BROADCAST": "192.168.255.255", 119 | "GATEWAY": "192.168.0.254" 120 | } 121 | } 122 | 123 | ## Config 124 | 125 | By including this section additional configuration options can be set on the cloned vm. If this section isn't present no configuration changes will take place. 126 | For further details on option please see https://pve.proxmox.com/pve-docs/api-viewer/index.html nodes >> {node} >> qemu >> {vmid} >> config >> POST 127 | 128 | "config" : { 129 | "autostart" : "yes", 130 | "cores" : "2", 131 | "balloon" : "256", 132 | "memory" : "512", 133 | "onboot" : "yes", 134 | "sockets" : "1" 135 | }, 136 | ## Running 137 | 138 | clone --help 139 | Usage: clone node vmid [options] 140 | -m, --vmid VMID Virtual machine id to clone from 141 | -e, --node NODE Proxmox deployment node 142 | -n, --name NAME Name for new VM. 143 | -d, --description DESCRIPTION New VM description. 144 | -s, --storage STORAGE Target storage for new VM. 145 | -u, --username USERNAME Username for proxmox API 146 | -p, --password PASSWORD Password for proxmox API 147 | -c, --config CONFIG Location of config (default config.json) 148 | --address ADDRESS Promox api address (default 127.0.0.1) 149 | --port POST Proxmox api port (default 8006) 150 | --version VERSION Proxmox api version (default api2) 151 | --type TYPE Proxmox api type (default json) 152 | -h, --help Prints this help 153 | 154 | Most of the configuration can be overwritten when running. 155 | 156 | The default config name is config.json but you can pass in a configuration file with the -c option. 157 | 158 | clone -c config.json -v 159 | 160 | create full clone of drive ide0 (local:125/vm-125-disk-1.qcow2) 161 | Formatting '/var/lib/vz/images/137/vm-137-disk-1.qcow2', fmt=qcow2 size=10737418240 encryption=off cluster_size=65536 preallocation=metadata lazy_refcounts=off refcount_bits=16 162 | drive mirror is starting (scanning bitmap) : this step can take some minutes/hours, depend of disk size and storage speed 163 | transferred: 146800640 bytes remaining: 10590617600 bytes total: 10737418240 bytes progression: 1.37 % busy: true ready: false 164 | transferred: 618659840 bytes remaining: 10118758400 bytes total: 10737418240 bytes progression: 5.76 % busy: true ready: false 165 | transferred: 1142947840 bytes remaining: 9594470400 bytes total: 10737418240 bytes progression: 10.64 % busy: true ready: false 166 | transferred: 1331691520 bytes remaining: 9405726720 bytes total: 10737418240 bytes progression: 12.40 % busy: true ready: false 167 | transferred: 1520435200 bytes remaining: 9216983040 bytes total: 10737418240 bytes progression: 14.16 % busy: true ready: false 168 | transferred: 1698693120 bytes remaining: 9038725120 bytes total: 10737418240 bytes progression: 15.82 % busy: true ready: false 169 | transferred: 1887436800 bytes remaining: 8849981440 bytes total: 10737418240 bytes progression: 17.58 % busy: true ready: false 170 | transferred: 2065694720 bytes remaining: 8671723520 bytes total: 10737418240 bytes progression: 19.24 % busy: true ready: false 171 | transferred: 2254438400 bytes remaining: 8482979840 bytes total: 10737418240 bytes progression: 21.00 % busy: true ready: false 172 | transferred: 2443182080 bytes remaining: 8294236160 bytes total: 10737418240 bytes progression: 22.75 % busy: true ready: false 173 | transferred: 2631925760 bytes remaining: 8105492480 bytes total: 10737418240 bytes progression: 24.51 % busy: true ready: false 174 | transferred: 2820669440 bytes remaining: 7916748800 bytes total: 10737418240 bytes progression: 26.27 % busy: true ready: false 175 | transferred: 3009413120 bytes remaining: 7728005120 bytes total: 10737418240 bytes progression: 28.03 % busy: true ready: false 176 | transferred: 3187671040 bytes remaining: 7549747200 bytes total: 10737418240 bytes progression: 29.69 % busy: true ready: false 177 | transferred: 3386900480 bytes remaining: 7350517760 bytes total: 10737418240 bytes progression: 31.54 % busy: true ready: false 178 | transferred: 3565158400 bytes remaining: 7172259840 bytes total: 10737418240 bytes progression: 33.20 % busy: true ready: false 179 | transferred: 3743416320 bytes remaining: 6994001920 bytes total: 10737418240 bytes progression: 34.86 % busy: true ready: false 180 | transferred: 3932160000 bytes remaining: 6805258240 bytes total: 10737418240 bytes progression: 36.62 % busy: true ready: false 181 | transferred: 4099932160 bytes remaining: 6637486080 bytes total: 10737418240 bytes progression: 38.18 % busy: true ready: false 182 | transferred: 4288675840 bytes remaining: 6448742400 bytes total: 10737418240 bytes progression: 39.94 % busy: true ready: false 183 | transferred: 4466933760 bytes remaining: 6270484480 bytes total: 10737418240 bytes progression: 41.60 % busy: true ready: false 184 | transferred: 4634705920 bytes remaining: 6102712320 bytes total: 10737418240 bytes progression: 43.16 % busy: true ready: false 185 | transferred: 4812963840 bytes remaining: 5924454400 bytes total: 10737418240 bytes progression: 44.82 % busy: true ready: false 186 | transferred: 5001707520 bytes remaining: 5735710720 bytes total: 10737418240 bytes progression: 46.58 % busy: true ready: false 187 | transferred: 5169479680 bytes remaining: 5567938560 bytes total: 10737418240 bytes progression: 48.14 % busy: true ready: false 188 | transferred: 5337251840 bytes remaining: 5400166400 bytes total: 10737418240 bytes progression: 49.71 % busy: true ready: false 189 | transferred: 5525995520 bytes remaining: 5211422720 bytes total: 10737418240 bytes progression: 51.46 % busy: true ready: false 190 | transferred: 5693767680 bytes remaining: 5043650560 bytes total: 10737418240 bytes progression: 53.03 % busy: true ready: false 191 | transferred: 5872025600 bytes remaining: 4865392640 bytes total: 10737418240 bytes progression: 54.69 % busy: true ready: false 192 | transferred: 6417285120 bytes remaining: 4320133120 bytes total: 10737418240 bytes progression: 59.77 % busy: true ready: false 193 | transferred: 6585057280 bytes remaining: 4152360960 bytes total: 10737418240 bytes progression: 61.33 % busy: true ready: false 194 | transferred: 7455375360 bytes remaining: 3282042880 bytes total: 10737418240 bytes progression: 69.43 % busy: true ready: false 195 | transferred: 8053063680 bytes remaining: 2684354560 bytes total: 10737418240 bytes progression: 75.00 % busy: true ready: false 196 | transferred: 9122611200 bytes remaining: 1614807040 bytes total: 10737418240 bytes progression: 84.96 % busy: true ready: false 197 | transferred: 9688842240 bytes remaining: 1048576000 bytes total: 10737418240 bytes progression: 90.23 % busy: true ready: false 198 | transferred: 10643046400 bytes remaining: 94371840 bytes total: 10737418240 bytes progression: 99.12 % busy: true ready: false 199 | transferred: 10737418240 bytes remaining: 0 bytes total: 10737418240 bytes progression: 100.00 % busy: false ready: true 200 | TASK OK 201 | New VM successfully created, image stored at /var/lib/vz/images/137/vm-137-disk-1.qcow2 202 | attempting connection to host 203 | connected to host 204 | /var/lib/vz/images/137 205 | /var/lib/vz/images/137/vm-137-disk-1.qcow2 206 | replacing TYPE=Ethernet in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 207 | replacing BOOTPROTO=static in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 208 | replacing DEFROUTE=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 209 | replacing IPV4_FAILURE_FATAL=no in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 210 | replacing IPV6INIT=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 211 | replacing IPV6_AUTOCONF=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 212 | replacing IPV6_DEFROUTE=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 213 | replacing IPV6_FAILURE_FATAL=no in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 214 | replacing NAME=ens18 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 215 | replacing UUID= in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 216 | replacing DEVICE=ens18 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 217 | replacing ONBOOT=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 218 | replacing IPADDR=172.16.1.12 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 219 | replacing PREFIX=12 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 220 | replacing BROADCAST=172.31.255.255 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 221 | replacing IPV6_PEERDNS=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 222 | replacing IPV6_PEERROUTES=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 223 | replacing IPV6_PRIVACY=no in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens18 224 | replacing TYPE=Ethernet in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 225 | replacing BOOTPROTO=static in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 226 | replacing NAME=ens19 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 227 | replacing UUID= in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 228 | replacing DEVICE=ens19 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 229 | replacing ONBOOT=yes in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 230 | replacing IPADDR=192.168.1.12 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 231 | replacing NETMASK=255.255.0.0 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 232 | replacing BROADCAST=192.168.255.255 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 233 | replacing GATEWAY=192.168.0.254 in file /var/lib/vz/images/137/disk/etc/sysconfig/network-scripts/ifcfg-ens19 234 | disk unmounted 235 | no content 236 | TASK OK 237 | New VM successfully started. 238 | Not up yet, retrying 239 | Not up yet, retrying 240 | Not up yet, retrying 241 | Not up yet, retrying 242 | Host is responding to pings. Instance created successfully. 243 | 244 | ## Notices 245 | 246 | Please use this at your own risk, I'm not responsible for any damages that may arise from using this. You should read the code first and understand it. --------------------------------------------------------------------------------