Project

General

Profile

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

Anonymous, 12/02/2015 07:09 AM

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 13 Anonymous
    default_settings :hello_greeting => 'Hello there!', :important_path => '/must/exist'
33
    validate_readable :optional_path, :important_path
34 8 Anonymous
  end
35 3 Anonymous
end
36
</code></pre>
37
38
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.
39
40
 * plugin :example, "1.2.3": *required*. Sets plugin name to "example" and version to "0.0.1".
41
 * 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.
42 1 Anonymous
 * 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.
43
 * 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).
44 13 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. Setting any of the parameters in default_settings to nil will trigger a validation error.
45
 * 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. 
46 1 Anonymous
 * 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.
47 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. 
48 1 Anonymous
49
h3. Provider definition
50
51 11 Dominic Cleal
Some plugins are *providers* for an existing plugin or module in the Smart Proxy, e.g. a DNS provider.
52
53 13 Anonymous
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.
54 11 Dominic Cleal
55
<pre>
56
module Proxy::Dns::PluginTemplate
57
  class Plugin < ::Proxy::Provider
58 13 Anonymous
    plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION
59 1 Anonymous
60 15 Dominic Cleal
    requires :dns, '>= 1.11'
61 11 Dominic Cleal
62 1 Anonymous
    after_activation do
63
      require 'smart_proxy_dns_plugin_template/dns_plugin_template_main'
64 13 Anonymous
      require 'smart_proxy_dns_plugin_template/dns_plugin_template_dependencies'
65 1 Anonymous
    end
66
  end
67 13 Anonymous
end
68
</pre>
69
70
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.
71
72
<pre>
73
require 'dns_common/dependency_injection/dependencies'
74
75
class Proxy::Dns::DependencyInjection::Dependencies
76
  dependency :dns_provider, Proxy::Dns::PluginTemplate::Record
77 11 Dominic Cleal
end
78
</pre>
79
80 4 Anonymous
h2. API
81 1 Anonymous
82 4 Anonymous
Modular Sinatra app is used to define plugin API. Note the base class Sinatra::Base and inclusion of ::Proxy::Helpers:
83
<pre><code class='ruby'>
84 8 Anonymous
module Proxy::Example
85
 class Api < Sinatra::Base
86 4 Anonymous
  helpers ::Proxy::Helpers
87
88
  get "/hello" do
89 8 Anonymous
    Proxy::Example::Plugin.settings.hello_greeting
90 4 Anonymous
  end
91
end
92
</code></pre>
93
94 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.
95 4 Anonymous
96
h2. Rackup Configuration
97
98
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:
99
<pre><code class="ruby">
100
require 'example_plugin/example_api'
101
102
map "/example" do
103 8 Anonymous
  run Proxy::Example::Api
104 4 Anonymous
end
105
</code></pre>
106
107
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. 
108
109
h2. Plugin Settings
110
111
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. 
112
<pre>
113
---
114
:enabled: true
115
:hello_greeting: "O hai!"
116
</pre>
117
118
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. 
119
120
h2. Bundler Configuration
121
122
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:
123
<pre><code class="ruby">
124
  gem 'smart_proxy_example'
125
  group :example do
126
    gem 'json'
127
  end
128 1 Anonymous
</code></pre>
129
 
130
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.
131 11 Dominic Cleal
132
h2. Adding a DNS provider
133
134 15 Dominic Cleal
*Requires Smart Proxy 1.11 or higher.*
135 12 Dominic Cleal
136 16 Anonymous
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.
137 11 Dominic Cleal
138
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.
139
140 16 Anonymous
DNS Provider classes are instantiated by DNS module's dependency injection container.
141 11 Dominic Cleal
142
<pre>
143 14 Anonymous
plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION
144 11 Dominic Cleal
</pre>
145
146
And then in the main file of the plugin:
147 1 Anonymous
148
<pre>
149 14 Anonymous
require 'dns_common/dns_common'
150
151 11 Dominic Cleal
module Proxy::Dns::PluginTemplate
152
  class Record < ::Proxy::Dns::Record
153 1 Anonymous
    include Proxy::Log
154 11 Dominic Cleal
    include Proxy::Util
155 1 Anonymous
156 14 Anonymous
    def initialize
157
      super('localhost', ::Proxy::Dns::Plugin.settings.dns_ttl)
158 1 Anonymous
    end
159
160 14 Anonymous
    def create_a_record(fqdn, ip)
161
      # adds a forward 'A' record with fqdn, ip
162 1 Anonymous
    end
163
164 14 Anonymous
    def create_ptr_record(fqdn, ip)
165
      # adds a reverse 'PTR' record with ip, fqdn
166
    end
167
168
    def remove_a_record(fqdn)
169
      # removes the forward 'A' record with fqdn
170
    end
171
172
    def remove_ptr_record(ip)
173
      # removes the reverse 'PTR' record with ip
174 11 Dominic Cleal
    end
175
  end
176 1 Anonymous
end
177
</pre>
178
179
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.
180 16 Anonymous
181
h2. Adding a DHCP provider
182
183
*Requires Smart Proxy 1.11 or higher.*
184
185
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.
186
187
Provider classes are instantiated by DHCP module's dependency injection container.
188
189
<pre>
190
plugin :example_dhcp_provider, ::ExampleDhcpPlugin::Provider::VERSION
191
</pre>
192
193
And then in the main file of the plugin:
194
195
<pre>
196
require 'dhcp_common/server'
197
198
module ::ExampleDhcpPlugin
199
  class Provider < ::Proxy::DHCP::Server
200
    include Proxy::Log
201
    include Proxy::Util
202
203
    def initialize
204
      super('localhost')
205
    end
206
207
    def load_subnets
208
      # loads subnet data into memory
209
    end
210
211
    def find_subnet(network_address)
212
      # returns Proxy::DHCP::Subnet that has network_address or nil if none was found
213
    end
214
215
    def load_subnet_data(a_subnet)
216
      # loads lease- and host-records for a Proxy::DHCP::Subnet
217
    end
218
219
    def subnets
220
      # returns all available subnets (instances of Proxy::DHCP::Subnet)
221
    end
222
223
    def all_hosts(network_address)
224
      # returns all reservations in a subnet with network_address
225
    end
226
227
    def unused_ip(network_address, mac_address, from_ip_address, to_ip_address)
228
      # 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
229
    end
230
231
    def find_record(network_address, ip_or_mac_address)
232
      # 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 
233
    end
234
235
    def add_record(params)
236
      # creates a record
237
    end
238
239
    def del_record(network_address,a_record)
240
      # removes a Proxy::DHCP::Record from a subnet with network_address
241
    end
242
  end
243
end
244
</pre>
245
246
DHCP provider support was first added in version 1.11.
247 5 Anonymous
248
h2. Testing
249 1 Anonymous
250 9 Dominic Cleal
Make sure that Gemfile includes "smart-proxy" gem as a development dependency:
251 7 Anonymous
252
<pre><code class="ruby">
253
group :development do
254
  gem 'smart_proxy', :git => "https://github.com/theforeman/smart-proxy.git"
255
end
256
</code></pre>
257
258 5 Anonymous
Load 'smart_proxy_for_testing' in your tests:
259 1 Anonymous
260 12 Dominic Cleal
<pre><code>
261 5 Anonymous
$: << File.join(File.dirname(__FILE__), '..', 'lib')
262
263
require 'smart_proxy_for_testing'
264
require 'test/unit'
265
require 'webmock/test_unit'
266
require 'mocha/test_unit'
267
require "rack/test"
268
269
require 'smart_proxy_pulp_plugin/pulp_plugin'
270
require 'smart_proxy_pulp_plugin/pulp_api'
271
272
class PulpApiTest < Test::Unit::TestCase
273
  include Rack::Test::Methods
274
275
  def app
276
    PulpProxy::Api.new
277
  end
278
279
  def test_returns_pulp_status_on_200
280
    stub_request(:get, "#{::PulpProxy::Plugin.settings.pulp_url.to_s}/api/v2/status/").to_return(:body => "{\"api_version\":\"2\"}")
281
    get '/status'
282
283
    assert last_response.ok?, "Last response was not ok: #{last_response.body}"
284
    assert_equal("{\"api_version\":\"2\"}", last_response.body)
285
  end
286
end
287
</code></pre>
288
289 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>
290
291 1 Anonymous
Please refer to "Sinatra documention":http://www.sinatrarb.com/testing.html for detailed information on testing of Sinatra applications.
292 9 Dominic Cleal
293
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.