class Rake::Proxmox::RakeTasks

This class provides rake tasks to control proxmox cluster through api.

Attributes

lxc_status[RW]

@return [Hash] from Proxmox.lxc_get

proxmox[R]

@return [Proxmox] a Proxmox::Proxmox

Public Class Methods

new(ssl_options = {}, logger = nil) { |self| ... } click to toggle source

@yield [self] gives itself to the block

# File lib/rake/proxmox.rb, line 11
def initialize(ssl_options = {}, logger = nil)
  unless ENV.include?('PROXMOX_PVE_CLUSTER') &&
         ENV.include?('PROXMOX_NODE')
    puts ''
    puts '# Proxmox Tasks are not available without correct environment'
    puts '#'
    puts '# Please set following variables to enable that feature:'
    puts '#'
    puts '# export PROXMOX_PVE_CLUSTER='\
      'https://pve1.example.com:8006/api2/json/'
    puts '# export PROXMOX_NODE=pve1'
    puts '#'
    puts '# optional add following variables if needed'
    puts '# export PROXMOX_REALM=pve'
    puts '# export PROXMOX_USERNAME=vagrant'
    puts '# export PROXMOX_PASSWORD=vagrant'
    puts ''
    return false
  end

  @proxmox = Rake::Proxmox::ProxmoxApi.new(
    ENV['PROXMOX_PVE_CLUSTER'],
    ENV['PROXMOX_NODE'],
    ENV['PROXMOX_USERNAME'] ? ENV['PROXMOX_USERNAME'] : 'vagrant',
    ENV['PROXMOX_PASSWORD'] ? ENV['PROXMOX_PASSWORD'] : 'vagrant',
    ENV['PROXMOX_REALM'] ? ENV['PROXMOX_REALM'] : 'pve',
    ssl_options,
    logger
  )

  # container for current lxc status
  @lxc_status = {}

  yield self if block_given?
  define
end

Private Instance Methods

define() click to toggle source

define Rake tasks

# File lib/rake/proxmox.rb, line 187
def define
  desc 'Proxmox'
  update_lxc_status
  namespace 'proxmox' do
    desc 'get proxmox cluster status'
    task 'cluster:status' do
      status = { nodes: 0, cluster_name: nil }
      proxmox.cluster_status.each do |entry|
        case entry['type']
        when 'cluster'
          unless entry['quorate'] == 1
            raise "Proxmox Cluster #{entry} is not quorate"
          end
          status[:cluster_name] = entry['name']
        when 'node'
          if entry['level'].nil?
            node_name = entry.sort.map { |k, v| "#{k}=#{v}" }.join(',')
            raise "Proxmox Node #{node_name} has errors"
          end
          status[:nodes] += 1
        else
          raise "unknown entry: #{entry}"
        end
      end
      puts "Proxmox Cluster #{status[:cluster_name]} online with"\
           " #{status[:nodes]} nodes"
    end

    desc 'upload template to proxmox storage'
    task 'storage:upload:template', %i[filename
                                       node
                                       storage
                                       force] do |_t, args|
      args.with_defaults(storage: 'local')
      args.with_defaults(node: ENV['PROXMOX_NODE'])
      args.with_defaults(force: 'false')
      # get upload filename
      upload_file_split = File.split(args.filename)
      upload_filename = upload_file_split[1]
      upload_size_b = File.size?(args.filename)
      # validate file does not already exist
      should_i_upload = true
      proxmox.list_storage(args.node, args.storage).each do |c|
        (_c_storage, c_path) = c['volid'].split(':')
        (_c_content, c_name) = c_path.split('/')
        next unless c_name == upload_filename
        # check filesize
        if c['size'].to_i >= upload_size_b.to_i
          puts "Template #{upload_filename} already on server"\
               " (#{c['size'].to_i / 1024 / 1024}MB)"
          should_i_upload = false
        else
          puts 'Template on Proxmox node has wrong size.'\
               " Local: #{upload_size_b.to_i / 1024 / 1024}MB"\
               " Proxmox: #{c['size'].to_i / 1024 / 1024}MB"
        end
        # found image leave loop
        break
      end
      if should_i_upload || args.force.to_s != 'false'
        puts "upload template: #{args.filename} to "\
             "#{args.storage}@#{args.node}:\n"
        r = proxmox.upload_template(args.filename, args.node,
                                    args.storage)
        puts "upload result: #{r}"
      end
    end

    desc 'list proxmox storage'
    task 'storage:list', %i[node storage] do |_t, args|
      args.with_defaults(storage: 'local')
      args.with_defaults(node: ENV['PROXMOX_NODE'])
      print "list_storage: #{args.storage}@#{args.node}:\n"
      proxmox.list_storage(args.node, args.storage).each do |c|
        print " content:#{c['content']} volid:#{c['volid']}\n"
      end
    end

    desc 'delete proxmox storage volume'
    task 'storage:delete:volume', %i[volume node storage] do |_t, args|
      args.with_defaults(storage: 'local')
      args.with_defaults(node: ENV['PROXMOX_NODE'])
      taskid = proxmox.storage_volume_delete(args.volume,
                                             args.node,
                                             args.storage)
      wait_for_task(taskid, args.node)
    end

    desc 'list all backup jobs'
    task 'cluster:backupjob:list', %i[json] do |_t, args|
      # handle arguments
      args.with_defaults(json: 'false')
      print_json = /true|1|j|y/ =~ args.json ? true : false
      # fetch jobs
      jobs = proxmox.fetch_backup_jobs
      # handle errors
      raise 'Error while fetching backup jobs' unless jobs.is_a?(Array)
      # produce output
      if print_json
        puts jobs.to_json
      else
        puts 'list all backup jobs: '
        jobs.each_with_index do |c, i|
          puts "#{i + 1}.  id: #{c['id']}; starttime: #{c['starttime']}"
        end
      end
    end

    desc 'show details of backup job identified by id'
    task 'cluster:backupjob:show', %i[jobid json] do |_t, args|
      # handle arguments
      args.with_defaults(json: 'false')
      print_json = /true|1|j|y/ =~ args.json ? true : false
      raise 'Please provide backup jobid' unless args.jobid
      # fetch job details
      job = proxmox.fetch_backup_job(args.jobid)
      # handle errors
      unless job.is_a?(Hash)
        raise "There is no backupjob with id #{args.jobid}"
      end
      # produce output
      if print_json
        puts job.to_json
      else
        puts 'Backup Job Parameter:'
        job.sort.each do |k, v|
          puts format "%-18s: %s\n", k.to_s, v.to_s
        end
      end
    end

    desc 'exclude VM id ranges from all backup jobs'
    task 'cluster:backupjob:exclude_range', %i[range_min range_max] \
      do |_t, args|
      args.with_defaults(range_min: 900)
      args.with_defaults(range_max: 999)
      exclude_list = []
      lxc_status.each do |vmid, _|
        exclude_list.push(vmid.to_s) if vmid >= args.range_min.to_i && \
                                        vmid < args.range_max.to_i + 1
      end
      if exclude_list.empty?
        puts "No VM Ids found in range #{args.range_min}"\
             " to #{args.range_max}. Nothing to exclude."
        next
      end
      puts "Found following VM Ids in range #{args.range_min}"\
           " to #{args.range_max}: "
      puts exclude_list.join(',')
      proxmox.fetch_backup_jobs.each do |c|
        bjob_list = c['exclude'].split(',')
        unless (exclude_list - bjob_list).any?
          puts 'Nothing to exclude. All VMs in given range are in'\
                ' current exclude list in backup job '\
                "with id #{c['id']} and starttime #{c['starttime']}."
          next
        end
        puts 'Add following IDs to exclude list in backup job '\
             "with id #{c['id']} and starttime #{c['starttime']}:"
        add_to_bjob = exclude_list - bjob_list
        puts add_to_bjob.join(',')
        new_settings = {
          starttime: c['starttime'],
          exclude: (exclude_list | bjob_list).join(',')
        }
        response = proxmox.update_backup_job(c['id'], new_settings)
        next if response.nil?
        if response.include? 'NOK: error code'
          raise "Update of Backup Job with id #{c['id']} "\
                "failed: #{response}"
        else
          puts response
        end
      end
    end

    desc 'list proxmox backups'
    task 'backup:list', %i[vmid node storage] do |_t, args|
      args.with_defaults(storage: 'local')
      args.with_defaults(node: ENV['PROXMOX_NODE'])
      warn "backup list vmid:#{args.vmid} #{args.storage}@"\
                     "#{args.node}:"
      proxmox.list_storage(args.node, args.storage).each do |c|
        # print " content:#{c['content']} volid:#{c['volid']}\n"
        filename_parts = c['volid'].split('/')[1].split('.')[0].split('-')
        next if filename_parts.count < 4
        next if filename_parts[1].empty?
        next unless filename_parts[1] <=> 'lxc' # must be lxc type
        # get vmid of backup
        file_vmid = filename_parts[2].to_i
        next unless file_vmid.to_s == filename_parts[2]
        next unless file_vmid == args.vmid.to_i
        print "#{c['volid']}\n"
      end
    end

    desc 'restore from vzdump [:vmid, :node'\
         " :storage => 'local',"\
         " :backup_storage => 'local',"\
         ' :file]'
    task 'backup:restore', %i[vmid
                              node
                              storage
                              backup_storage
                              file] do |_t, args|
      print "restore args: #{args}\n"
      id = args.vmid.to_i
      args.with_defaults(storage: 'local')
      args.with_defaults(backup_storage: 'local')
      unless lxc_restore(id, args.storage, args.backup_storage,
                         args.file, args.node)
        raise "failed to restore #{id} from "\
                "#{args.storage}:#{args.file} to #{args.storage}\n"
      end
    end

    desc 'destroy all but exclude_ids (defaulting to: 6002) separated by'\
         ' colon(:)'
    task 'destroy:all', %i[exclude_ids delete_low_ids] do |t, args|
      args.with_defaults(exclude_ids: '6002')
      args.with_defaults(delete_low_ids: 'false')
      exclude_ids = args.exclude_ids.split(':').map(&:to_i)
      b = { 'true' => true,
            true => true,
            'false' => false,
            false => false }
      low_ids = b[args.delete_low_ids.downcase]
      print "exclude_ids: #{exclude_ids}\n"

      return false unless lxc_status

      lxc_status.each do |id, prop|
        next if exclude_ids.include?(id.to_i)
        if !low_ids && id.to_i < 6000
          raise "not allowed to destroy id: #{id} < low_ids (6000)"\
               " enable delete_low_ids deletion by executing \n"\
               "# rake #{t}[#{args.exclude_ids},true]"
        end
        unless lxc_stop(id)
          raise "failed to stop #{id} (#{prop['name']})\n"
        end
        unless lxc_destroy(id)
          raise "failed to destroy #{id} (#{prop['name']})\n"
        end
      end
    end
    # create snapshot of every container
    desc 'snapshot all but exclude_ids (defaulting to: 6002) separated'\
         ' by colon(:)'
    task 'snapshot:create:all', %i[exclude_ids name desc] do |_t, args|
      args.with_defaults(exclude_ids: '6002')
      args.with_defaults(name: 'rakesnap1')
      args.with_defaults(desc: 'snapshot taken by rake task')
      exclude_ids = args.exclude_ids.split(':').map(&:to_i)
      print "exclude_ids: #{exclude_ids}\n"

      return false unless lxc_status

      lxc_status.each do |id, prop|
        next if exclude_ids.include?(id.to_i)
        unless lxc_snapshot(id, args.name, args.desc)
          raise "failed to snapshot #{id} (#{prop['name']})\n"
        end
      end
    end
    # delete all snapshots with specific name
    desc 'delete all snapshots with :name'
    task 'snapshot:delete:all', %i[exclude_ids name] do |_t, args|
      args.with_defaults(exclude_ids: '6002')
      args.with_defaults(name: 'rakesnap1')
      exclude_ids = args.exclude_ids.split(':').map(&:to_i)
      print "exclude_ids: #{exclude_ids}\n"

      return false unless lxc_status

      lxc_status.each do |id, prop|
        next if exclude_ids.include?(id.to_i)
        unless lxc_snapshot_delete(id, args.name)
          raise "failed to delete snapshot #{snap['name']} from #{id} "\
               "(#{prop['name']})\n"
        end
      end
    end
    # add task for each lxc container
    lxc_status.each do |id, prop|
      desc "destroy #{prop['name']}"
      task "destroy:#{prop['name']}" do
        unless lxc_stop(id)
          raise "failed to stop #{id} (#{prop['name']})\n"
        end
        unless lxc_destroy(id)
          raise "failed to destroy #{id} (#{prop['name']})\n"
        end
      end
      desc "backup #{prop['name']} [:storage => 'local',"\
           " :mode => 'snapshot']"
      task "backup:create:#{prop['name']}", %i[storage mode] do |_t, args|
        args.with_defaults(storage: 'local')
        args.with_defaults(mode: 'snapshot')
        unless lxc_backup(id, args.storage, args.mode)
          raise "failed to backup #{id} (#{prop['name']})\n"
        end
      end
      desc "restore #{prop['name']} [:storage => 'local',"\
           ' :file]'
      task "backup:restore:#{prop['name']}", %i[storage
                                                backup_storage
                                                file] do |_t, args|
        args.with_defaults(storage: 'local')
        args.with_defaults(backup_storage: 'local')
        unless lxc_restore(id, args.storage, args.backup_storage,
                           args.file)
          raise "failed to restore #{id} (#{prop['name']}) from "\
                  "#{args.storage}:#{args.file} to #{args.storage}\n"
        end
      end
      desc "snapshot #{prop['name']}"
      task "snapshot:create:#{prop['name']}", %i[name desc] do |_t, args|
        args.with_defaults(name: 'rakesnap1')
        args.with_defaults(desc: 'snapshot taken by rake task')
        unless lxc_snapshot(id, args.name, args.desc)
          raise "failed to snapshot #{id} (#{prop['name']})\n"
        end
      end
      lxc_snap_list = lxc_snapshot_list(id)
      next if lxc_snap_list == false
      lxc_snap_list.each do |snap|
        next unless snap.include?('name')
        next if snap['name'] == 'current'
        desc "snapshot #{snap['name']} from #{prop['name']} "\
             "(#{snap['description'].tr("\n", ' ').gsub(/ $/, '')})"
        task "snapshot:delete:#{prop['name']}:#{snap['name']}"\
          do |_t, _args|
          unless lxc_snapshot_delete(id, snap['name'])
            raise "failed to delete snapshot #{snap['name']} of #{id}"\
                 " (#{prop['name']})\n"
          end
        end
      end
    end
  end
end
lxc_backup(vmid, storage, mode = 'snapshot') click to toggle source
# File lib/rake/proxmox.rb, line 154
def lxc_backup(vmid, storage, mode = 'snapshot')
  prop = lxc_status[vmid]
  return false unless prop.include?('status')
  return false unless prop.include?('node')
  print "vzdump to #{storage} vmid: #{vmid} (#{prop['name']})\n"
  taskid = proxmox.vzdump_single(vmid, prop['node'], storage, mode)
  wait_for_task(taskid, prop['node'])
end
lxc_destroy(vmid) click to toggle source
# File lib/rake/proxmox.rb, line 117
def lxc_destroy(vmid)
  prop = lxc_status[vmid]
  return false unless prop.include?('name')
  return false unless prop.include?('node')
  print "destroy CT #{vmid} (#{prop['name']})\n"
  taskid = proxmox.lxc_delete(vmid, prop['node'])
  wait_for_task(taskid, prop['node'])
end
lxc_restore(vmid, storage, backup_storage, backup, node = nil) click to toggle source
# File lib/rake/proxmox.rb, line 163
def lxc_restore(vmid, storage, backup_storage, backup, node = nil)
  options = {
    force: 1,
    restore: 1,
    storage: storage
  }
  prop = if node
           p = {}
           p['status'] = 'to-be-recovered'
           p['node'] = node.to_s
           # return p into prop
           p
         else
           lxc_status[vmid]
         end
  return false unless prop.include?('status')
  return false unless prop.include?('node')
  put "vzrestore of #{storage}:#{file} to vmid: #{vmid} (#{prop['name']})"
  taskid = proxmox.lxc_post("#{backup_storage}:#{backup}", vmid, options,
                            prop['node'])
  wait_for_task(taskid, prop['node'])
end
lxc_snapshot(vmid, name, desc) click to toggle source
# File lib/rake/proxmox.rb, line 126
def lxc_snapshot(vmid, name, desc)
  prop = lxc_status[vmid]
  return false unless prop.include?('status')
  return false unless prop.include?('node')
  print "snapshot vmid: #{vmid} (#{prop['name']})\n"
  taskid = proxmox.lxc_snapshot(vmid, name, desc, prop['node'])
  wait_for_task(taskid, prop['node'])
end
lxc_snapshot_delete(vmid, snapname) click to toggle source
# File lib/rake/proxmox.rb, line 145
def lxc_snapshot_delete(vmid, snapname)
  prop = lxc_status[vmid]
  return false unless prop.include?('status')
  return false unless prop.include?('node')
  print "snapshot delete #{snapname} vmid: #{vmid} (#{prop['name']})\n"
  taskid = proxmox.lxc_snapshot_delete(vmid, snapname, prop['node'])
  wait_for_task(taskid, prop['node'])
end
lxc_snapshot_list(vmid) click to toggle source
# File lib/rake/proxmox.rb, line 135
def lxc_snapshot_list(vmid)
  prop = lxc_status[vmid]
  return false unless prop.include?('status')
  return false unless prop.include?('node')
  # print "snapshot list vmid: #{vmid} (#{prop['name']})\n"
  l = proxmox.lxc_snapshot_list(vmid, prop['node'])
  return false if l.is_a?(String)
  l
end
lxc_stop(vmid) click to toggle source
# File lib/rake/proxmox.rb, line 107
def lxc_stop(vmid)
  prop = lxc_status[vmid]
  return false unless prop.include?('status')
  return false unless prop.include?('node')
  return true if prop['status'].include?('stopped')
  print "stopping CT #{vmid} (#{prop['name']})\n"
  taskid = proxmox.lxc_stop(vmid, prop['node'])
  wait_for_task(taskid, prop['node'])
end
update_lxc_status() click to toggle source
# File lib/rake/proxmox.rb, line 56
def update_lxc_status
  # self.lxc_status = proxmox.lxc_get
  proxmox.cluster_resources_get('vm').each do |vm|
    # print "vm: #{vm}\n"
    # print "vm: #{vm['name']} on #{vm['node']}\n"
    next unless vm.include?('type')
    next unless vm.include?('vmid')
    next unless vm['type'] <=> 'lxc'
    lxc_status[vm['vmid']] = vm
  end
  # print lxc_status
end
wait_for_task(upid, node = nil) click to toggle source
# File lib/rake/proxmox.rb, line 69
def wait_for_task(upid, node = nil)
  max_n = 0
  if upid.include?('NOK: error code')
    print "Proxmox task failed #{upid}"
    l = proxmox.task_log(upid, node, max_n)
    return false unless l.is_a?(Array)
    l.each do |log_entry|
      next if log_entry['n'].to_i <= max_n
      print "[#{log_entry['n']}] #{log_entry['t']}\n"
      max_n = log_entry['n'].to_i
    end
    return false
  end
  status = proxmox.task_status(upid, node)
  until status.include? ':'
    sleep 1
    print "waiting for task: '#{upid}' [#{status}]\n"
    proxmox.task_log(upid, node, max_n).each do |log_entry|
      next if log_entry['n'].to_i <= max_n
      print "[#{log_entry['n']}] #{log_entry['t']}\n"
      max_n = log_entry['n'].to_i
    end
    status = proxmox.task_status(upid, node)
  end
  (_final_status, exitstatus) = status.split(':')
  if exitstatus.include?('OK')
    print "waiting for task: '#{upid}' [#{exitstatus}]\n"
    return true
  end
  proxmox.task_log(upid, node, max_n).each do |log_entry|
    next if log_entry['n'].to_i <= max_n
    print "[#{log_entry['n']}] #{log_entry['t']}\n"
    max_n = log_entry['n'].to_i
  end
  print "waiting for task failed: '#{upid}' [#{exitstatus}]\n"
  false
end