backup vmware esxi with a rake (script)

Recently we built a small VMware ESXi cluster (3 nodes) for testing and developing purpose. After playing with it for a while (and spinning up a few dozen vms), we started looking around for a cheap and simple backup solution.

Step 0 - install the tools
  • install ruby
  • install bundler gem install bundler
  • create the project folder mkdir esxi_backup
  • create a file named Gemfile into the project folder with the following content:
source 'https://rubygems.org'

gem 'rbvmomi'

group :development, :test do  
  gem 'rake'
end
  • install all the dependencies cd esxi_backup && bundle install
  • create the project directories mkdir config && mkdir lib && mkdir scripts
Step 1 - build a small automation lib

vSphere Hypervisor it's fully (and easy) "scriptable" through its comprehensive vsphere api. Unfortunately the api isn't available for the single server free edition, so you need a fully licensed server even for testing it.
As always, if there is an api or library, there is some sort of ruby wrapper to make life easier... this time let me thank RbVmomi for his contribution to the ruby fellowship.

To keep things more clean (and "upgradable"), we will write a simple configuration file in config/vcenter.yml:

connection:  
  host: vsphere_host
  user: vsphere_api_user
  pwd: api_user_password
  datacenter: vsphere_datacenter
backup:  
  datastore: vsphere_datastore
  folder: backup_folder_inside_datastore

n.b. the file above stores sensitive informations. In a secure production environment it's better not to leave them around (even worse if written in files...), but that's another post...

A small helper class in lib/vcenter.rb:

# coding: utf-8
require 'rbvmomi'

class VCenter < Object  
  attr_accessor :config, :vim, :data_center

public  
  def init()
    @config = YAML.load_file("config/vcenter.yml")
    @vim = RbVmomi::VIM.connect host: @config["connection"]["host"], user: @config["connection"]["user"], password: @config["connection"]["pwd"], insecure: true
    @data_center = vim.serviceInstance.find_datacenter(@config["connection"]["datacenter"])
  end

  def backup_vm(vm_path, vm_snapshot='', vm_datastore='')
    vm_name = vm_path.split('/')[-1]
    timestamp = Time.now.utc.strftime('%y%m%d%H%M%S')
    vm = @data_center.find_vm("#{vm_path}")
    backup_folder = @data_center.vmFolder.children.find{|folder| folder.name==@config["backup"]["folder"]}
    iscsi_datastore = @data_center.find_datastore((vm_datastore.nil? || vm_datastore=='') ? @config["backup"]["datastore"] : vm_datastore)
    vm_relocate_spec = { datastore: iscsi_datastore, diskMoveType: :moveAllDiskBackingsAndDisallowSharing, transform: :sparse}
    vm_clone_spec = { location: vm_relocate_spec, powerOn: false, template: false }
    vm_clone_spec[:snapshot] = find_snapshot(vm, vm_snapshot, nil) unless (vm_snapshot.nil? || vm_snapshot=='')
    vm.CloneVM_Task(:folder => backup_folder, :name => "#{vm_name}#{(('_' + vm_snapshot) unless (vm_snapshot.nil? || vm_snapshot=='')).to_s}_#{timestamp}", :spec => vm_clone_spec).wait_for_completion
  end

  def find_snapshot(vm, snapshot_name, parent_snapshot)
    found_snapshot = nil
    if parent_snapshot.nil?
      snapshots = vm.snapshot.rootSnapshotList
    else
      snapshots = parent_snapshot.childSnapshotList
    end
    snapshots.each do |snapshot|
      if snapshot.name==snapshot_name
        found_snapshot = snapshot.snapshot
      else
        found_snapshot = find_snapshot(vm, snapshot_name, snapshot) unless snapshot.childSnapshotList.size==0
      end
    end
    return found_snapshot
  end
end

and finally a rake file in lib/task/setup.rake

require './lib/vcenter'

include RakeHelper

namespace :vcenter do

  desc 'backup vm'
  task :backup_vm, [:vm_name,:vm_snapshot,:vm_datastore] do |t, args|
    abort if args[:vm_name].nil?
    label = "#{args[:vm_name]}#{((' [' + args[:vm_snapshot] + ']') unless (args[:vm_snapshot].nil? || args[:vm_snapshot]=='')).to_s}#{((' [' + args[:vm_datastore] + ']') unless (args[:vm_datastore].nil? || args[:vm_datastore]=='')).to_s}"
    puts "vm: #{label} - starting backup..."
    api = VCenter.new
    api.init
    api.backup_vm args[:vm_name], args[:vm_snapshot], args[:vm_datastore]
    puts "vm: #{label} - backup completed!"
  end

end

With all this in place you can open a shell and

cd esxi_backup  
bundle exec rake vcenter:backup_vm[VM_NAME]  

if your VM it's inside a folder

bundle exec rake vcenter:backup_vm[FOLDER/VM_NAME]  

if you want to backup a specific VM snapshot

bundle exec rake vcenter:backup_vm[VM_NAME,SNAPSHOT_NAME]  

if any of the above parameters has spaces or special shell characters, just wrap the whole rake command inside single/double quotes (depending on your OS)

bundle exec "rake vcenter:backup_vm[FOLDER/VM_NAME,SNAPSHOT_NAME]"  

(as you can guess I'm a great rake fan. I think it's a very useful wrapper for shell scripting...)

Post Scriptum

All the files above are available in github