Implementing Plugin

To integrate your backup plugin with OnApp, you need to provide the following files:

  • A plugin.rb file where you define a name of your backup plugin.
  • A separate file for each hook that is required to be used in integration with OnApp. You also pass models with metadata objects to hooks in these files. 
  • A helper file with code that you can include into any hook-specific file. 

In this document, you can find the examples of the required files with the descriptions and inline comments.  

Before You Begin 


Before you proceed to working with files, please note that: 

  • Every call of a hook is idempotent.
  • Every hook returns a Backups::Plugin::Response instance that is built using the 'success' and 'error' helpers. If none of the helpers were used, the successful response is returned implicitly.
  • The plugins and helpers have access to the backup resource attributes that are primary_host, secondary_host, username and password.
  • The plugins and helpers possess logging capabilities, i.e. errors with 'logger.error("Error message goes here")'.
  • The plugins and helpers have access to configuration via 'config'.
  • The plugins and helpers have access to configuration parameters via 'config.advanced_options' that is a hash, where key is a name of an option and value is a value of an option set for this resource in OnApp.


Use Polling 


You can use polling in the hook-specific and helper files to run a periodic check on results of specific operations. The following code snippet is an example of a polled operation that you can use in a hook-specific or helper file.

# an interval is how often to call a block (in seconds)
res = poller.setup(interval: 10) do |p|
  result = do_action # whatever is need to call here

  if result == 'progress'
    p.progress # means that it is not ready to finish and needs another run after `interval`
  elsif result == 'success'
    p.success(some_result)
  else
    p.failure("Fail message")
  end
end.on_success do |result|
  # the result here is equal to some_result
  1
end.on_failure do |message|
  # the message here is "Fail message"
  # on_failure should be used if you can recover from an error. If not, ignore on_failure and the poller will raise an error.
  2
end.run

# the result now equals to 1 onsuccess and 2 onfailure

You can simplify polling operations with the help of STATUSES. See the task_helper.rb file for an example.



Create Plugin File


The plugin.rb is a main file where you define a name of your backup plugin. Below you can find an example of plugin.rb with inline comments that help to understand the requirements to the file. 

plugin.rb
# plugin_name is a name of a plugin that will be stored as a `plugin` attribute of a backup resource
# OnApp doesn't allow two plugins with the same name
Backups::Plugin.plugin :plugin_name



Create Hook-Specific Files


There is a set of hooks that you can implement for your backup plugin depending on its configuration. Each hook requires a separate file with a name that corresponds to a particular event triggered by a hook. Every hook should contain a method 'call' with the corresponding arguments.

Below you can find the examples of hook-specific files with inline comments that help to understand the structure of the files and their essential components: 

add_virtual_server.rb
# registers a virtual server on the provider side
# IMPORTANT: this hook shouldn't deal with installation of the agent
Backups::Plugin.hook helpers: :agent_client_helper ) do
  def call(virtual_server)
    # creates agent but doesn't deploy it
    res = make_call(virtual_server)
    if res.result
      remote_id = res.remote_id
      # remote_id is an example of a metadata object that can be passed via models. OnApp saves it automatically.
      virtual_server.metadata[:remote_id] = remote_id
      success
    else
      error('Error key' => res.error)
    end
    success
  end

  private

  # private methods are encouraged to be written to make code more readable
  def make_call(virtual_server)
    client.call(:add_virtual_server, message: {ip: virtual_server.ip_addresses.first.address})
  end
end

remove_virtual_server.rb
# removes a virtual server on the provider side
Backups::Plugin.hook helpers: :agent_client_helper do
  def call(virtual_server)
    client.call(:remove_virtual_server, message: {remote_id: virtual_server.metadata[:remote_id]})
  end
end

The  install_agent and uninstall_agent hooks are required if at least one of the  agent elements is set to true.
install_agent.rb
# OnApp determines whether to call this hook based on the config.xml file. You may not use this hook if a provider doesn't support agents at all.
# installs the agent on a virtual server
# %i() is used for an array of helpers
Backups::Plugin.hook helpers: %i( agent_client_helper task_helper ) do
  def call(virtual_server)
    # the logic on how to install the agent goes here
    # use ssh_caller.call('ssh command') if SSH access is required

    # assuming that the agent installation can be scheduled with the service API

    task_id = client.call(:install_agent, message: {id: virtual_server.metdata[:remote_id]})

    wait_for_task!(task_id)
  end
end

uninstall_agent.rb
# works in the same way as install_agent
Backups::Plugin.hook helpers: %i( agent_client_helper task_helper ) do
  def call(virtual_server)
    # the logic on how to uninstall the agent goes here
    # use ssh_caller.call('ssh command') if SSH access is required

    # assuming that uninstall of the agent can be scheduled with the service API

    task_id = client.call(:uninstall_agent, message: {id: virtual_server.metdata[:remote_id]})

    wait_for_task!(task_id)
  end
end

schedule_create.rb
# creates a schedule on the provider side
Backups::Plugin.hook helpers: :agent_client_helper do
  def call(schedule, virtual_server)
    client.call(:create_schedule, message: {
      remote_id: virtual_server.metadata[:remote_id],
      enabled: true,
      name: schedule.id,
      description: schedule.period,
      replicationScheduleFrequencyType: schedule.period.upcase,
    })

    success
  end
end

recovery_points_for_virtual_server.rb
# gets recovery points from a provider for a particular virtual server
# must return an array of recovery points or a blank array
# a helper build_recovery_point can be used to ease the process
Backups::Plugin.hook helpers: :agent_client_helper do
  def call(virtual_server)
    client.call(:get_recovery_points, message: {remote_id: virtual_server.metadata[:remote_id]}).result.map |rp|
      build_recovery_point(size: rp[:size],
                           created_at: Time.at(rp[:created_on_timestamp_in_millis].to_i / 1000),
                           updated_at: Time.at(rp[:created_on_timestamp_in_millis].to_i / 1000),
                           state: rp[:recovery_point_state].downcase).tap do |r|
        r.metadata[:remote_id] = rp[:recovery_point_id]
      end
  end

recovery_points_equal.rb
# used by OnApp to determine whether a recovery point record stored in OnApp is the same as on the provider side
# this is required to sync recovery points regularly
Backups::Plugin.hook helpers: :agent_client_helper do
  def call(local_recovery_point, remote_recovery_point)
    local_recovery_point.metadata[:remote_id] == remote_recovery_point.metadata[:remote_id] ? success : error
  end

restore_recovery_point.rb
# restores a recovery point
# this hook waits until recovery point is restored
Backups::Plugin.hook helpers: %i( agent_client_helper task_helper ) do
  def call(recovery_point, virtual_server)
    ssh_caller.call("some ssh setups if needed")

    task_id = client.call(:schedule_restore, message: {remote_id: recovery_point.metadata[:remote_id]}).result

    wait_for_task!(task_id)
  end

file_entries.rb
# lists file entries of some directory
Backups::Plugin.hook helpers: :agent_client_helper do
  DEFAULT_ROOT_PATH = '/'

  def call(recovery_point, virtual_server, root_dir)
    root_dir ||= DEFAULT_ROOT_PATH # root_dir might be nil

    agent.call(:get_file_entries, message: {vs_id: virtual_server.metadata[:remote_id], recovery_point_id: recvoery_point.metadata[:remote_id]}).map do |f|
      build_file_entry(path: f[:full_path], # UNIX separator
                       file_name: f[:file_path],
                       dir: f[:is_directory],
                       size: f[:is_directory] ? nil : f[:file_size].to_i, # for directory size shoulde be `nil`
                       last_modified: Time.at(f[:modify_time].to_i / 1000))
    end
  end
end

OnApp Backup Plugin System supports only UNIX file separators for both UNIX-like operating systems and Windows. It is up to a plugin to convert one type of a file separator into another.

restore_file_entries.rb
# restores the given file entries
Backups::Plugin.hook helpers: %i( agent_client_helper task_helper ) do
  # paths are an array of paths of files that are to be restored
  def call(recovery_point, virtual_server, paths)
    wait_for_task!(restore_file_paths(recovery_point.metadata[:id], virtual_server.metadata[:remote_id], paths))
  end

  private

  def restore_file_paths(recovery_point_id, vs_id, paths)
    agent.call(:restore_file_entries, message: {
      recvoery_point_id: recvoery_point_id,
      virtual_server_id: vs_id,
      paths_to_restore: paths
    }).result[:task_id]
  end
end



Create Helper Files 


The helper files or helpers contain code that you can include into any hook-specific file. The usage of helpers allows you to apply the Don’t Repeat Yourself (DRY) principle to your backup plugin. The helper files reside in the helpers directory of a backup plugin. 

Below you can find the examples of helpers with inline comments that help to understand the structure of the files and their essential components: 

helpers/client_helper.rb
Backups::Plugin.helper do
  # defines a method that can be included into any hook
  def client
    @client ||= Savon.client(wsdl: "#{ primary_host }/Agent?wsdl", basic_auth: [username, password])
  end
end

helpers/task_helper.rb
Backups::Plugin.helper do
  # values here are remote statues that can be returned by a service
  STATUSES = {
    success: 'FINISHED',
    progress: 'RUNNING',
    failure: 'ERROR'
  }.freeze

  def wait_for_task!(task_id)
    poller.setup(interval: 10, statuses: STATUSES) do |p|
      p.handle_status(task_status(task_id)) # handle_status does all the job
    end.run
  end

  def task_status(task_id)
    # returns a status that is one of the values in the STATUSES constant
  end
end