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.
- The plugins and helpers have access to OnApp version via ‘onapp_version’.
- The plugins and helpers which are performed in a transaction can write to transaction log via user_facing_logger.log(“Something”).
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_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
- remove_virtual_server.rb
- install_agent.rb
- schedule_create.rb
- recovery_points_for_virtual_server.rb
- recovery_points_equal.rb
- file_entries.rb
- restore_file_entries.rb
- link_to_provider.rb
- space_used.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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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.
# 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
# adds a button to OnApp Control Panel UI that allows to access a provider backup console Backups::Plugin.hook do def call(virtual_server) 'http://urltoprovider' end end
# obtains a total disk size taken by all backups of a particular virtual server Backups::Plugin.hook helpers: :disk_safe_client_helper do def call(virtual_server) devices(virtual_server.metadata[:disk_safe_id]).sum { |d| d[:capacity].to_i } end private def devices(disk_safe_id) devices = disk_safe_client.call(:get_disk_safe_by_id, message: { id: disk_safe_id }).body[:get_disk_safe_by_id_response][:return][:device_list] devices.is_a?(Array) ? devices : [devices] 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:
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
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