Project

General

Profile

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

Anonymous, 04/29/2016 09:02 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 17 Anonymous
h2. Plugin Initialization
8
9
The initialization process can be thought of as consisting of two phases: loading and validation of settings and runtime initialization -- selection of classes, their parameters, and how they will be instantiated.
10
11
During the first phase of the process, all modules are gathered into groups consisting of the main plugin and one or more providers. If any of the members of the group fail at any time during initialization, the rest of the modules in the group will be failed as well. Initialization starts with all loaded and enabled plugin (main modules) classes being collected, then for each:
12
* configuration file is loaded
13
* dependencies are loaded (also see load_classes)
14
* configuration-related code executed, and configuration updated (also see load_runtime_configuration)
15
* validators executed (also see load_validators)
16
* provider names resolved to provider classes
17
18
At this point the steps above are repeated for all providers, one module group at a time. During the second phase, for each of the modules:
19
* dependency injection wirings are resolved (also see load_dependency_injection_wirings)
20
* services started (also see start_services)
21
* module's versions are checked against other modules stated requirements (also see)
22
23 1 Anonymous
h2. Plugin Organization
24
25 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.
26 1 Anonymous
27 11 Dominic Cleal
We have some templates for creating your plugin:
28
29
* "smart_proxy_example plugin":https://github.com/theforeman/smart_proxy_example is a minimal example plugin that can be used as a skeleton
30
* "smart_proxy_dns_plugin_template":https://github.com/theforeman/smart_proxy_dns_plugin_template is a template for creating new DNS provider plugins
31
32
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.
33
34 6 Dominic Cleal
h2. Making your plugin official
35
36 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.
37
38 11 Dominic Cleal
h2. Plugin definition
39 2 Anonymous
40 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:
41 3 Anonymous
42 1 Anonymous
<pre><code class='ruby'>
43 8 Anonymous
module Proxy::Example
44
  class Plugin < ::Proxy::Plugin
45
    plugin :example, "0.0.1"
46
    http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
47 13 Anonymous
    https_rackup_path File.expand_path("https_config.ru", File.expand_path("../", __FILE__))
48 8 Anonymous
    default_settings :hello_greeting => 'Hello there!', :important_path => '/must/exist'
49 17 Anonymous
    load_classes ::Proxy::Example::ClassLoader
50 18 Anonymous
    load_programmable_settings "::Proxy::Example::ProgrammableSettings"
51 17 Anonymous
    load_dependency_injection_wirings "::Proxy::Example::DIConfiguration"
52
    load_validators "Proxy::Example::CustomValidators"
53 3 Anonymous
    validate_readable :optional_path, :important_path
54 17 Anonymous
    validate :a_setting, :my_validator => true, :if => lambda {|settings| !settings[:a_setting].nil?}
55
    validate :another_setting, :my_other_validator => true
56
    start_services :service_a, :service_b
57 3 Anonymous
  end
58
end
59
</code></pre>
60
61
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.
62
63 1 Anonymous
 * plugin :example, "1.2.3": *required*. Sets plugin name to "example" and version to "0.0.1".
64
 * 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.
65 13 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.
66
 * 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).
67 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. Setting any of the parameters in default_settings to nil will trigger a validation error.
68
 * 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. 
69
 * 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.
70 17 Anonymous
 * bundler_group :my_plugin_group: *optional*.  Sets the name of the bundler group for plugin dependencies. If omitted the plugin name is used.
71 18 Anonymous
 * load_classes: must be a class or a block. Specified class must implement "load_classes" instance method that loads module's dependencies.
72
 * load_programmable_settings: can be a class or a class name. Specified class must implement load_programmable_settings(settings_hash) instance method that returns new or updated settings.
73 17 Anonymous
 * load_validators: can be a class or a class name. Specified class must implement load_validators instance method that returns a hash of validator name to validator class mappings.
74
 * load_dependency_injection_wirings: can be a class or a class name. The class must implement load_dependency_injection_wirings(di_container, settings_hash) instance method.
75
 * start_services: list of dependency injection wiring labels. Services that perform work independently (asynchroniously) of http requests should implement #start method.
76
 * validate: validate :setting_one, :setting_two, ..., :setting_n, :validator_name => { :validator_param_one => 'value one', ...,}, :if => lambda {|settings| ... }, alternatively use :validator_name => true if validator has no parameters. If predicate is specified, the validator will be called only if the lambda evaluates to true. Predicate's lambda expects module's settings passed as a parameter.
77 1 Anonymous
h3. Provider definition
78
79 11 Dominic Cleal
Some plugins are *providers* for an existing plugin or module in the Smart Proxy, e.g. a DNS provider.
80
81 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.
82 11 Dominic Cleal
83
<pre>
84
module Proxy::Dns::PluginTemplate
85
  class Plugin < ::Proxy::Provider
86 13 Anonymous
    plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION
87 1 Anonymous
88 15 Dominic Cleal
    requires :dns, '>= 1.11'
89 11 Dominic Cleal
90 1 Anonymous
    after_activation do
91
      require 'smart_proxy_dns_plugin_template/dns_plugin_template_main'
92 13 Anonymous
      require 'smart_proxy_dns_plugin_template/dns_plugin_template_dependencies'
93 1 Anonymous
    end
94
  end
95 13 Anonymous
end
96
</pre>
97
98
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.
99
100
<pre>
101
require 'dns_common/dependency_injection/dependencies'
102
103
class Proxy::Dns::DependencyInjection::Dependencies
104
  dependency :dns_provider, Proxy::Dns::PluginTemplate::Record
105 1 Anonymous
end
106
</pre>
107 17 Anonymous
108
h2. How to Load Dependencies
109
110
The class loader must implement load_classes instance method:
111
112
<pre>
113
class ::Proxy::Example::ClassLoader
114
  def load_classes
115
    require 'example/class_a'
116
    require 'example/class_b'
117
  end
118 1 Anonymous
end
119
</pre>
120
121 18 Anonymous
alternatively, a block can be used to load dependencies:
122
123
<pre>
124
module Proxy::Example
125
  class Plugin < ::Proxy::Plugin
126
    ...
127
    load_classes do
128
      require 'example/class_a'
129
      require 'example/class_b'
130
    end
131
    ...
132
  end
133
end
134
</pre>
135
136 17 Anonymous
h2. How to Programmatically Update Settings
137 1 Anonymous
138 18 Anonymous
The class must implement load_programmable_settings(settings_hash) instance method that returns new or updated settings:
139 17 Anonymous
140
<pre>
141
class ::Proxy::Example::RuntimeConfiguration
142 18 Anonymous
  def load_programmable_settings(settings_hash)
143 17 Anonymous
    settings_hash[:a_setting] = "Hello, world"
144
    settings_hash
145
  end
146
end
147
</pre>
148
149
h2. How to Define Dependency Injection Wirings
150
151
The class must implement load_dependency_injection_wirings instance method that has dependency injection container and settings hash as its parameters:
152
153
<pre>
154
class ::Proxy::Example::DIConfiguration
155
  def load_dependency_injection_wirings(container_instance, settings)
156
    container_instance.dependency :depedency_a, ::Proxy::Example::ClassA
157
    container_instance.dependency :dependency_b, ::Proxy::Example::ClassB
158
    container_instance.singleton_dependency :service_a, lambda {|container| ::Proxy::Example::ServiceA.new(settings[:example_setting], container_instance.get_dependency(:dependency_a))}
159
  end
160
end
161
</pre>
162
163
When Proxy::DependencyInjection::Container#dependency is used to define a dependency, a new instance of a class will be returned, or lambda executed every time the dependency is requested.
164
If only a single instance of a class is ever required, use Proxy::DependencyInjection::Container#singleton_dependency: the class will be instantiated first time the dependency is requested, and then reused on all subsequent requests. 
165
166
h2. How to Create and Load Custom Validators
167
168
The class used for loading of custom validators must implement load_validators that returns a hash of validator name to validator class mappings. A validator must use ::Proxy::PluginValidators::Base as its base class and implement validate!(settings) instance method that accepts a hash containing module settings. validate! should raise an exception if the check it's making fails.
169
170
<pre>
171
class Proxy::Example::CustomValidators
172
  class MyValidator < ::Proxy::PluginValidators::Base
173
    def validate!(settings)
174
      raise ::Proxy::Error::ConfigurationError("Unsupported greeting") if settings[@setting_name] != "Hello, world"
175
    end
176
  end
177
178
  def load_validators
179
    {:my_validator => MyValidator}
180
  end
181
end
182
</pre>
183
184 11 Dominic Cleal
185 4 Anonymous
h2. API
186 1 Anonymous
187 4 Anonymous
Modular Sinatra app is used to define plugin API. Note the base class Sinatra::Base and inclusion of ::Proxy::Helpers:
188
<pre><code class='ruby'>
189 8 Anonymous
module Proxy::Example
190
 class Api < Sinatra::Base
191 4 Anonymous
  helpers ::Proxy::Helpers
192
193
  get "/hello" do
194 8 Anonymous
    Proxy::Example::Plugin.settings.hello_greeting
195 4 Anonymous
  end
196
end
197
</code></pre>
198
199 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.
200 4 Anonymous
201
h2. Rackup Configuration
202
203
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:
204
<pre><code class="ruby">
205
require 'example_plugin/example_api'
206
207
map "/example" do
208 8 Anonymous
  run Proxy::Example::Api
209 4 Anonymous
end
210
</code></pre>
211
212
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. 
213
214
h2. Plugin Settings
215
216
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. 
217
<pre>
218
---
219
:enabled: true
220
:hello_greeting: "O hai!"
221
</pre>
222
223
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. 
224
225
h2. Bundler Configuration
226
227
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:
228
<pre><code class="ruby">
229
  gem 'smart_proxy_example'
230
  group :example do
231
    gem 'json'
232
  end
233 1 Anonymous
</code></pre>
234
 
235
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.
236 11 Dominic Cleal
237
h2. Adding a DNS provider
238
239 15 Dominic Cleal
*Requires Smart Proxy 1.11 or higher.*
240 12 Dominic Cleal
241 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.
242 11 Dominic Cleal
243
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.
244
245 16 Anonymous
DNS Provider classes are instantiated by DNS module's dependency injection container.
246 11 Dominic Cleal
247
<pre>
248 14 Anonymous
plugin :dns_plugin_template, ::Proxy::Dns::PluginTemplate::VERSION
249 11 Dominic Cleal
</pre>
250
251
And then in the main file of the plugin:
252 1 Anonymous
253
<pre>
254 14 Anonymous
require 'dns_common/dns_common'
255
256 11 Dominic Cleal
module Proxy::Dns::PluginTemplate
257
  class Record < ::Proxy::Dns::Record
258 1 Anonymous
    include Proxy::Log
259 11 Dominic Cleal
    include Proxy::Util
260 1 Anonymous
261 14 Anonymous
    def initialize
262
      super('localhost', ::Proxy::Dns::Plugin.settings.dns_ttl)
263 1 Anonymous
    end
264
265 14 Anonymous
    def create_a_record(fqdn, ip)
266
      # adds a forward 'A' record with fqdn, ip
267 1 Anonymous
    end
268
269 14 Anonymous
    def create_ptr_record(fqdn, ip)
270
      # adds a reverse 'PTR' record with ip, fqdn
271
    end
272
273
    def remove_a_record(fqdn)
274
      # removes the forward 'A' record with fqdn
275
    end
276
277
    def remove_ptr_record(ip)
278
      # removes the reverse 'PTR' record with ip
279 11 Dominic Cleal
    end
280
  end
281 1 Anonymous
end
282
</pre>
283
284
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.
285 16 Anonymous
286
h2. Adding a DHCP provider
287
288
*Requires Smart Proxy 1.11 or higher.*
289
290
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.
291
292
Provider classes are instantiated by DHCP module's dependency injection container.
293
294
<pre>
295
plugin :example_dhcp_provider, ::ExampleDhcpPlugin::Provider::VERSION
296
</pre>
297
298
And then in the main file of the plugin:
299
300
<pre>
301
require 'dhcp_common/server'
302
303
module ::ExampleDhcpPlugin
304
  class Provider < ::Proxy::DHCP::Server
305
    include Proxy::Log
306
    include Proxy::Util
307
308
    def initialize
309
      super('localhost')
310
    end
311
312
    def load_subnets
313
      # loads subnet data into memory
314
    end
315
316
    def find_subnet(network_address)
317
      # returns Proxy::DHCP::Subnet that has network_address or nil if none was found
318
    end
319
320
    def load_subnet_data(a_subnet)
321
      # loads lease- and host-records for a Proxy::DHCP::Subnet
322
    end
323
324
    def subnets
325
      # returns all available subnets (instances of Proxy::DHCP::Subnet)
326
    end
327
328
    def all_hosts(network_address)
329
      # returns all reservations in a subnet with network_address
330
    end
331
332
    def unused_ip(network_address, mac_address, from_ip_address, to_ip_address)
333
      # 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
334
    end
335
336
    def find_record(network_address, ip_or_mac_address)
337
      # 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 
338
    end
339
340
    def add_record(params)
341
      # creates a record
342
    end
343
344
    def del_record(network_address,a_record)
345
      # removes a Proxy::DHCP::Record from a subnet with network_address
346
    end
347
  end
348
end
349
</pre>
350
351
DHCP provider support was first added in version 1.11.
352 5 Anonymous
353
h2. Testing
354 1 Anonymous
355 9 Dominic Cleal
Make sure that Gemfile includes "smart-proxy" gem as a development dependency:
356 7 Anonymous
357
<pre><code class="ruby">
358
group :development do
359
  gem 'smart_proxy', :git => "https://github.com/theforeman/smart-proxy.git"
360
end
361
</code></pre>
362
363 5 Anonymous
Load 'smart_proxy_for_testing' in your tests:
364 1 Anonymous
365 12 Dominic Cleal
<pre><code>
366 5 Anonymous
$: << File.join(File.dirname(__FILE__), '..', 'lib')
367
368
require 'smart_proxy_for_testing'
369
require 'test/unit'
370
require 'webmock/test_unit'
371
require 'mocha/test_unit'
372
require "rack/test"
373
374
require 'smart_proxy_pulp_plugin/pulp_plugin'
375
require 'smart_proxy_pulp_plugin/pulp_api'
376
377
class PulpApiTest < Test::Unit::TestCase
378
  include Rack::Test::Methods
379
380
  def app
381
    PulpProxy::Api.new
382
  end
383
384
  def test_returns_pulp_status_on_200
385
    stub_request(:get, "#{::PulpProxy::Plugin.settings.pulp_url.to_s}/api/v2/status/").to_return(:body => "{\"api_version\":\"2\"}")
386
    get '/status'
387
388
    assert last_response.ok?, "Last response was not ok: #{last_response.body}"
389
    assert_equal("{\"api_version\":\"2\"}", last_response.body)
390
  end
391
end
392
</code></pre>
393
394 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>
395
396 1 Anonymous
Please refer to "Sinatra documention":http://www.sinatrarb.com/testing.html for detailed information on testing of Sinatra applications.
397 9 Dominic Cleal
398
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.