Project

General

Profile

Actions

How to Create a Smart-Proxy Plugin » History » Revision 16

« Previous | Revision 16/30 (diff) | Next »
Anonymous, 12/02/2015 07:09 AM


How to Create a Smart-Proxy Plugin

This guide outlines main components of a plugin, but assumes some degree of familiarity with ruby gems, bundler, rack, and Sinatra. You'll find links to useful documentation in each of the sections below.

Plugin Organization

Smart-Proxy plugins are normal ruby gems, please follow documentation at http://guides.rubygems.org/make-your-own-gem/ for guidance on gem creation and packaging. It is strongly recommended to follow smart_proxy_<your plugin name here> naming convention for your plugin.

We have some templates for creating your plugin:

Also, smart_proxy_pulp plugin is an example for a fully functional, yet easy to understand Smart-Proxy plugin.

Making your plugin official

Once you're ready to release the first version, please see How_to_Create_a_Plugin for info on making your plugin part of the Foreman project.

Plugin definition

A plugin definition is used to define plugin's name, version, location of rackup configuration, and other parameters. At a minimum, Plugin Descriptor must define plugin name and version. Note the base class of the descriptor is ::Proxy::Plugin:

module Proxy::Example
  class Plugin &lt; ::Proxy::Plugin
    plugin :example, "0.0.1" 
    http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
    https_rackup_path File.expand_path("https_config.ru", File.expand_path("../", __FILE__))
    default_settings :hello_greeting =&gt; 'Hello there!', :important_path =&gt; '/must/exist'
    validate_readable :optional_path, :important_path
  end
end

Here we defined a plugin called "example", with version "0.0.1", that is going to listen on both http and https ports. Following is the full list of parameters that can be defined by the Plugin Descriptor.

  • plugin :example, "1.2.3": required. Sets plugin name to "example" and version to "0.0.1".
  • http_rackup_path "path/to/http_config.ru": optional. Sets path to http rackup configuration. If omitted, the plugin is not going to listen on the http port. Please see below for information on rackup configuration.
  • https_rackup_path "path/to/https_config.ru": optional. Sets path to https rackup configuration. If omitted, the plugin is not going to listen on the https port. Please see below for information on rackup configuration.
  • requires :another_plugin, '~> 1.2.0': optional. Specifies plugin dependencies, where ":another_plugin" is another plugin name, and '~> 1.2.0' is version specification (pls. see http://guides.rubygems.org/patterns/#pessimistic_version_constraint for details on version specification).
  • default_settings :first => 'my first setting', :another => 'my other setting': optional. Defines default values for plugin parameters. These parameters can be overridden in plugin settings file. Setting any of the parameters in default_settings to nil will trigger a validation error.
  • validate_readable :optional_path, :important_path: optional. Verifies that settings listed here contain paths to files that exist and are readable. Optional settings (not listed under default_settings) will be skipped if left uninitialized.
  • after_activation { do_something }: optional. Supplied block is going to be executed after the plugin has been loaded and enabled. Note that the block is going to be executed in the context of the Plugin Descriptor class.
  • bundler_group :my_plugin_group: optional. Sets the name of the bundler group for plugin dependencies. If omitted the plugin name is used.

Provider definition

Some plugins are providers for an existing plugin or module in the Smart Proxy, e.g. a DNS provider.

These are registered almost identically, but use Proxy::Provider instead of Proxy::Plugin. No rackup_paths are used for providers, since they don't add any new REST API, they only add functionality to an existing module.

module Proxy::Dns::PluginTemplate
  class Plugin < ::Proxy::Provider
    plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION

    requires :dns, '>= 1.11'

    after_activation do
      require 'smart_proxy_dns_plugin_template/dns_plugin_template_main'
      require 'smart_proxy_dns_plugin_template/dns_plugin_template_dependencies'
    end
  end
end

Additionally, each provider must specify which class implements interface expected by the main plugin. This is done by declaring an association for module's dependency injection container.

require 'dns_common/dependency_injection/dependencies'

class Proxy::Dns::DependencyInjection::Dependencies
  dependency :dns_provider, Proxy::Dns::PluginTemplate::Record
end

API

Modular Sinatra app is used to define plugin API. Note the base class Sinatra::Base and inclusion of ::Proxy::Helpers:

module Proxy::Example
 class Api &lt; Sinatra::Base
  helpers ::Proxy::Helpers

  get "/hello" do
    Proxy::Example::Plugin.settings.hello_greeting
  end
end

Here we return a string defined in 'hello_greeting' parameter (see Plugin Descriptor above and settings file below) when a client performs a GET /hello. Please refer to Sinatra documentation on details about routing, template rendering, available helpers, etc.

Rackup Configuration

During startup Smart-Proxy assembles web applications listening on http and https ports using rackup files of enabled plugins. Plugin rackup files define mounting points of plugin API:

require 'example_plugin/example_api'

map "/example" do
  run Proxy::Example::Api
end

The example above should be sufficient for the majority of plugins. Please refer to Sinatra+Rack documentation for additional information.

Plugin Settings

On startup Smart-Proxy will load and parse plugin configuration files located in its settings.d/ directory. Each plugin config file is named after the plugin and is a yaml-encoded collection of key-value pairs and used to override default values of plugin parameters.

---
:enabled: true
:hello_greeting: "O hai!" 

This settings file enables the plugin (by default all plugins are disabled), and overrides :hello_greeting parameter. Plugin settings can be accessed through .settings method of the Plugin class, for example: ExamplePlugin.settings.hello_greeting. Global Smart-Proxy parameters are accessible through Proxy::SETTINGS, for example Proxy::SETTINGS.foreman_url returns Foreman url configured for this Smart-Proxy.

Bundler Configuration

Smart-Proxy relies on bundler to load its dependencies and plugins. We recommend to create a dedicated bundler config file for your plugin, and name it after the plugin. For example:

  gem 'smart_proxy_example'
  group :example do
    gem 'json'
  end

You'll need to create a dedicated bundler group for additional dependencies of your plugin. By default the group shares the name with the plugin, but you can override it using bundler_group parameter in Plugin Descriptor. Please refer to How_to_Install_a_Smart-Proxy_Plugin for additional details on "from source" plugin installations.

Adding a DNS provider

Requires Smart Proxy 1.11 or higher.

When extending the 'dns' smart proxy module, the plugin needs to create a new Proxy::Dns::Record class with create_a_record, create_ptr_record, remove_a_record, and remove_ptr_record methods for adding and removing of DNS records.

The easiest way to do this is using the Smart Proxy DNS plugin template which can get you up and running with a new DNS provider plugin in minutes.

DNS Provider classes are instantiated by DNS module's dependency injection container.

plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION

And then in the main file of the plugin:

require 'dns_common/dns_common'

module Proxy::Dns::PluginTemplate
  class Record < ::Proxy::Dns::Record
    include Proxy::Log
    include Proxy::Util

    def initialize
      super('localhost', ::Proxy::Dns::Plugin.settings.dns_ttl)
    end

    def create_a_record(fqdn, ip)
      # adds a forward 'A' record with fqdn, ip
    end

    def create_ptr_record(fqdn, ip)
      # adds a reverse 'PTR' record with ip, fqdn
    end

    def remove_a_record(fqdn)
      # removes the forward 'A' record with fqdn
    end

    def remove_ptr_record(ip)
      # removes the reverse 'PTR' record with ip
    end
  end
end

DNS provider support was first added in version 1.10, but the interface was updated between 1.10 and 1.11. Please see the history of this page for 1.10-compatible recommendations and the 1.10-stable branch of the example DNS plugin instead of master.

Adding a DHCP provider

Requires Smart Proxy 1.11 or higher.

When creating a new 'dhcp' provider smart proxy module, the plugin needs to create a new Proxy::DHCP::Server class that implements load_subnets, load_subnet_data, find_subnet, subnets, all_hosts, unused_ip, find_record, add_record, and del_record methods.

Provider classes are instantiated by DHCP module's dependency injection container.

plugin :example_dhcp_provider, ::ExampleDhcpPlugin::Provider::VERSION

And then in the main file of the plugin:

require 'dhcp_common/server'

module ::ExampleDhcpPlugin
  class Provider < ::Proxy::DHCP::Server
    include Proxy::Log
    include Proxy::Util

    def initialize
      super('localhost')
    end

    def load_subnets
      # loads subnet data into memory
    end

    def find_subnet(network_address)
      # returns Proxy::DHCP::Subnet that has network_address or nil if none was found
    end

    def load_subnet_data(a_subnet)
      # loads lease- and host-records for a Proxy::DHCP::Subnet
    end

    def subnets
      # returns all available subnets (instances of Proxy::DHCP::Subnet)
    end

    def all_hosts(network_address)
      # returns all reservations in a subnet with network_address
    end

    def unused_ip(network_address, mac_address, from_ip_address, to_ip_address)
      # returns first available ip address in a subnet with network_address, for a host with mac_address, in the range of ip addresses: from_ip_address, to_ip_address
    end

    def find_record(network_address, ip_or_mac_address)
      # returns a Proxy::DHCP::Record from a subnet with network_address that has ip- or mac-address specified in ip_or_mac_address, or nil of none was found 
    end

    def add_record(params)
      # creates a record
    end

    def del_record(network_address,a_record)
      # removes a Proxy::DHCP::Record from a subnet with network_address
    end
  end
end

DHCP provider support was first added in version 1.11.

Testing

Make sure that Gemfile includes "smart-proxy" gem as a development dependency:

group :development do
  gem 'smart_proxy', :git => "https://github.com/theforeman/smart-proxy.git" 
end

Load 'smart_proxy_for_testing' in your tests:


$: << File.join(File.dirname(__FILE__), '..', 'lib')

require 'smart_proxy_for_testing'
require 'test/unit'
require 'webmock/test_unit'
require 'mocha/test_unit'
require "rack/test" 

require 'smart_proxy_pulp_plugin/pulp_plugin'
require 'smart_proxy_pulp_plugin/pulp_api'

class PulpApiTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    PulpProxy::Api.new
  end

  def test_returns_pulp_status_on_200
    stub_request(:get, "#{::PulpProxy::Plugin.settings.pulp_url.to_s}/api/v2/status/").to_return(:body => "{\"api_version\":\"2\"}")
    get '/status'

    assert last_response.ok?, "Last response was not ok: #{last_response.body}" 
    assert_equal("{\"api_version\":\"2\"}", last_response.body)
  end
end

To execute all tests

bundle exec rake test
. To save time during development it is possible to execute tests in a single file:
bundle exec rake test TEST=path/to/test/file

Please refer to Sinatra documention for detailed information on testing of Sinatra applications.

Once you have tests, see Jenkins for info on setting up tests under Jenkins.

Updated by Anonymous over 8 years ago · 16 revisions