Project

General

Profile

How to Create a Smart-Proxy Plugin » History » Version 12

Dominic Cleal, 07/10/2015 06:46 AM
1.10

1 1 Anonymous
h1. How to Create a Smart-Proxy Plugin
2
3 4 Anonymous
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.
4 1 Anonymous
5 9 Dominic Cleal
{{toc}}
6
7 1 Anonymous
h2. Plugin Organization
8
9 11 Dominic Cleal
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.
10 1 Anonymous
11 11 Dominic Cleal
We have some templates for creating your plugin:
12
13
* "smart_proxy_example plugin":https://github.com/theforeman/smart_proxy_example is a minimal example plugin that can be used as a skeleton
14
* "smart_proxy_dns_plugin_template":https://github.com/theforeman/smart_proxy_dns_plugin_template is a template for creating new DNS provider plugins
15
16
Also, "smart_proxy_pulp plugin":https://github.com/theforeman/smart-proxy-pulp is an example for a fully functional, yet easy to understand Smart-Proxy plugin.
17
18 6 Dominic Cleal
h2. Making your plugin official
19
20 1 Anonymous
Once you're ready to release the first version, please see [[How_to_Create_a_Plugin#Making-your-plugin-official]] for info on making your plugin part of the Foreman project.
21
22 11 Dominic Cleal
h2. Plugin definition
23 2 Anonymous
24 11 Dominic Cleal
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:
25 3 Anonymous
26 1 Anonymous
<pre><code class='ruby'>
27 8 Anonymous
module Proxy::Example
28
  class Plugin < ::Proxy::Plugin
29
    plugin :example, "0.0.1"
30
    http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
31
    https_rackup_path File.expand_path("https_config.ru", File.expand_path("../", __FILE__))
32
    default_settings :hello_greeting => 'Hello there!'
33
  end
34 3 Anonymous
end
35
</code></pre>
36
37
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.
38
39
 * plugin :example, "1.2.3": *required*. Sets plugin name to "example" and version to "0.0.1".
40
 * 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.
41
 * 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.
42
 * 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).
43 1 Anonymous
 * 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.
44
 * 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.
45 4 Anonymous
 * bundler_group :my_plugin_group: *optional*.  Sets the name of the bundler group for plugin dependencies. If omitted the plugin name is used. 
46 1 Anonymous
47 11 Dominic Cleal
h3. Provider definition
48 1 Anonymous
49 11 Dominic Cleal
Some plugins are *providers* for an existing plugin or module in the Smart Proxy, e.g. a DNS provider.
50
51
These are registered almost identically, but use Proxy::Provider instead of Proxy::Plugin and additionally define a factory for initialization by the main module.  No rackup_paths are used for providers, since they don't add any new REST API, they only add to an existing module.
52
53
<pre>
54
module Proxy::Dns::PluginTemplate
55
  class Plugin < ::Proxy::Provider
56
    plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION,
57
           :factory => proc { |attrs| ::Proxy::Dns::PluginTemplate::Record.record(attrs) }
58
59
    requires :dns, '>= 1.10'
60
61
    after_activation do
62
      require 'smart_proxy_dns_plugin_template/dns_plugin_template_main'
63
    end
64
  end
65
end
66
</pre>
67
68 4 Anonymous
h2. API
69 1 Anonymous
70 4 Anonymous
Modular Sinatra app is used to define plugin API. Note the base class Sinatra::Base and inclusion of ::Proxy::Helpers:
71
<pre><code class='ruby'>
72 8 Anonymous
module Proxy::Example
73
 class Api < Sinatra::Base
74 4 Anonymous
  helpers ::Proxy::Helpers
75
76
  get "/hello" do
77 8 Anonymous
    Proxy::Example::Plugin.settings.hello_greeting
78 4 Anonymous
  end
79
end
80
</code></pre>
81
82 1 Anonymous
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":http://www.sinatrarb.com/intro.html on details about routing, template rendering, available helpers, etc.
83 4 Anonymous
84
h2. Rackup Configuration
85
86
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:
87
<pre><code class="ruby">
88
require 'example_plugin/example_api'
89
90
map "/example" do
91 8 Anonymous
  run Proxy::Example::Api
92 4 Anonymous
end
93
</code></pre>
94
95
The example above should be sufficient for the majority of plugins. Please refer to "Sinatra+Rack":http://www.sinatrarb.com/intro.html documentation for additional information. 
96
97
h2. Plugin Settings
98
99
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. 
100
<pre>
101
---
102
:enabled: true
103
:hello_greeting: "O hai!"
104
</pre>
105
106
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. 
107
108
h2. Bundler Configuration
109
110
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:
111
<pre><code class="ruby">
112
  gem 'smart_proxy_example'
113
  group :example do
114
    gem 'json'
115
  end
116 1 Anonymous
</code></pre>
117
 
118
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.
119 11 Dominic Cleal
120
h2. Adding a DNS provider
121
122 12 Dominic Cleal
*Requires Smart Proxy 1.10 or higher.*
123
124 11 Dominic Cleal
When extending the 'dns' smart proxy module, the plugin needs to create a new Proxy::Dns::Record class with @create@ and @remove@ methods for adding and removing the DNS record.
125
126
The easiest way to do this is using the "Smart Proxy DNS plugin template":https://github.com/theforeman/smart_proxy_dns_plugin_template which can get you up and running with a new DNS provider plugin in minutes.
127
128
New record classes are instantiated from the :factory proc in the plugin definition.
129
130
<pre>
131
plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION,
132
       :factory => proc { |attrs| ::Proxy::Dns::PluginTemplate::Record.record(attrs) }
133
</pre>
134
135
And then in the main file of the plugin:
136
137
<pre>
138
module Proxy::Dns::PluginTemplate
139
  class Record < ::Proxy::Dns::Record
140
    include Proxy::Log
141
    include Proxy::Util
142
143
    def self.record(attrs = {})
144
      new(attrs)
145
    end
146
147
    def create
148
      # use @fqdn, @value, @ttl, @type
149
    end
150
151
    def remove
152
      # use @fqdn, @value, @ttl, @type
153
    end
154
  end
155
end
156
</pre>
157 5 Anonymous
158
h2. Testing
159 1 Anonymous
160 9 Dominic Cleal
Make sure that Gemfile includes "smart-proxy" gem as a development dependency:
161 7 Anonymous
162
<pre><code class="ruby">
163
group :development do
164
  gem 'smart_proxy', :git => "https://github.com/theforeman/smart-proxy.git"
165
end
166
</code></pre>
167
168 5 Anonymous
Load 'smart_proxy_for_testing' in your tests:
169 1 Anonymous
170 12 Dominic Cleal
<pre><code>
171 5 Anonymous
$: << File.join(File.dirname(__FILE__), '..', 'lib')
172
173
require 'smart_proxy_for_testing'
174
require 'test/unit'
175
require 'webmock/test_unit'
176
require 'mocha/test_unit'
177
require "rack/test"
178
179
require 'smart_proxy_pulp_plugin/pulp_plugin'
180
require 'smart_proxy_pulp_plugin/pulp_api'
181
182
class PulpApiTest < Test::Unit::TestCase
183
  include Rack::Test::Methods
184
185
  def app
186
    PulpProxy::Api.new
187
  end
188
189
  def test_returns_pulp_status_on_200
190
    stub_request(:get, "#{::PulpProxy::Plugin.settings.pulp_url.to_s}/api/v2/status/").to_return(:body => "{\"api_version\":\"2\"}")
191
    get '/status'
192
193
    assert last_response.ok?, "Last response was not ok: #{last_response.body}"
194
    assert_equal("{\"api_version\":\"2\"}", last_response.body)
195
  end
196
end
197
</code></pre>
198
199 10 Anonymous
To execute all tests <code><pre>bundle exec rake test</code></pre>.  To save time during development it is possible to execute tests in a single file: <code><pre>bundle exec rake test TEST=path/to/test/file</pre></code>
200
201 1 Anonymous
Please refer to "Sinatra documention":http://www.sinatrarb.com/testing.html for detailed information on testing of Sinatra applications.
202 9 Dominic Cleal
203
Once you have tests, see "Jenkins":http://projects.theforeman.org/projects/foreman/wiki/Jenkins#Smart-proxy-plugin-testing for info on setting up tests under Jenkins.