Skip to content

Latest commit

 

History

History
2667 lines (2147 loc) · 94.9 KB

how_to_create_a_plugin.asciidoc

File metadata and controls

2667 lines (2147 loc) · 94.9 KB

How to Create a Plugin

Table of Contents

The goal of this tutorial is to help users quickly and easily create Foreman plugins. This is not an exhaustive tutorial of Ruby on Rails or an explanation of how rails engines work.

Naming your plugin

It’s strongly recommended that your plugin begins with the string "foreman" to help people identify its relationship with the project. Plugins are published as gems to rubygems.org, so check that the name you wish to use is free - again, using a standard prefix helps. The prefix is also assumed in the installer, so it makes adding support there easier.

Multiple words in the gem name should be separated with underscores ("_"), although some plugins use underscores for the gem name and hyphens for git repo names. The git repo name isn’t as important, but consistency in naming is recommended as it will make life easier for both users and developers.

A good example is foreman_hooks because the name clearly states it’s a foreman plugin that adds hooks.

Using the example plugin

There is a fully working example plugin which you can clone to get quickly started. It contains examples of many of the types of behaviour that you might want to do in a plugin, such as adding new models, overriding views, extending controllers, adding permissions and menu items, and so on. The README contains a list of it’s current behaviour. To get started building your first Foreman Plugin run the following command:

git clone https://github.com/theforeman/foreman_plugin_template my_plugin

A new directory my_plugin is created for the plugin. Now go into this directory and use the rename script to change all references to ForemanPluginTemplate to MyPlugin:

cd my_plugin; ./rename.rb my_plugin

Installing the plugin

It’s best to test a plugin on a development installation of Foreman, as it loads code on the fly and doesn’t require building and installing your plugin as a gem. Foreman’s contribution guide describes setting up a small test instance.

You can enable the plugin right away, and see what it’s default behavior is, by editing foreman Gemfile.local.rb file (or creating this file under the folder bundler.d) and adding the following line

Gemfile.local.rb
gem 'my_plugin', :path => 'path_to/my_plugin'

Install the 'preface' bundle by running from foreman core directory:

bundle install

Restart (or start if it wasn’t up) foreman (type 'rails server') and the new foreman plugin should be listed in the about page plugin tab. If it isn’t, check your gem name and the symbol you passed to Foreman::Plugin.register match. Watch out for hyphens - e.g. gem 'foreman-tasks' would need to be registered as

Foreman::Plugin.register :"foreman-tasks"

Since hyphens are less intuitive, the policy for naming plugins is to use underscores, like foreman_salt.

Note that Debian or other "production" installations need to be restarted after code changes, as they won’t reload on the fly.

RPM installations

RPM installations use bundler_ext and are unable to load plugins from a path, they need the plugin to be built as a .gem file, installed and then reloaded. Development setups as described above are much better.

In the plugin directory, run gem build my_plugin.gemspec which will build a file such as my_plugin-0.0.1.gem. Copy to the Foreman server and run scl enable tfm "gem install --ignore-dependencies /tmp/my_plugin-0.0.1.gem"

Add to /usr/share/foreman/bundler.d/Gemfile.local.rb:

gem 'my_plugin'

Then restart httpd to load it.

Initial edits

First edit the my_plugin.gemspec file, you can specify here the name, authors, description homepage and version of your plugin, by simply replacing the appropriate strings with your content.

Making your plugin official

Once you’ve written the first version of your plugin, what comes next? We’d recommend plugin authors to consider the following:

  1. Tag releases in git - ideally, following semver for versioning

  2. Use gem compare -b foo 0.1 0.2 -k tool to identify content changes (you need separate gem-compare gem to be installed)

  3. Push a gem of each release to rubygems.org

  4. Add it to List_of_Plugins

  5. Add some tests and enable testing in Jenkins

  6. Create an RPM and Debian package for the plugin - submitted to the foreman-packaging repo, we’re also happy to do this and publish to our official plugin repos

  7. Move git repo to theforeman organization - in case you move on, this lets us help with maintenance or delegate permissions to somebody else and keep the project alive. It also makes it easier for people to find. See also GitHub.

  8. Have an issue tracker on projects.theforeman.org - a common location for users for any Foreman-related issue

  9. Ensure other maintainers can push to rubygems.org - again in case you should move on

Please get in touch via foreman-dev (IRC or e-mail) to arrange for repo transfers, packages, issue trackers etc.

Release strategies

The big advantage of developing a plugin is that it’s not tied to Foreman’s quarterly release process, so you can get features and bug fixes out to meet your own users' expectations, even for Foreman versions that are already released. We’d encourage plugin authors to release early, release often.

When versioning your plugin, we’d recommend using a semantic versioning scheme (semver.org where the major digit is incremented for each incompatible change (e.g. only works with Foreman X, not Y), the second for backwards compatible releases (new features) and the third for fixes.

When preparing to release, consider which versions of Foreman it’s compatible with (ensure you set the minimum Foreman version, see Requiring Foreman version) and also which should receive the update. Our package repositories for plugins are separate per major Foreman release, so you may only want to release an update to nightlies and the last stable release, or just to nightlies for instance.

If your plugin is only compatible with certain versions of Foreman, a small compatibility table in the README or documentation can be very useful to users to check they’re on the right version. If you make a change to support the current Foreman nightlies, you should then change the minimum version, bump the major version (e.g. 3.x becomes 4.0.0) and add a line to the table to say for Foreman X, you now need 4.x.

Foreman compatibility

We know from experience that Foreman plugins can be fragile and it’s common for some plugins to need small tweaks on most major Foreman releases.

Foreman will always strive to make no incompatible changes in a minor release, but be prepared to make updates on major releases. Where possible, deprecation warnings will be added for old public methods before their removal. Warnings will be issued for two major releases and then the old method removed in the third release, giving plenty of time to update plugins.

Plugin package repos

Foreman operates a set of plugin repos that are enabled by default, in addition to our core repos. We package lots of plugins for Foreman, the smart proxy and Hammer in these through foreman-packaging so they’re easily installable for end users.

If you’d like to get your plugin packaged, first release it to rubygems.org, sticking to the recommended naming conventions as closely as possible. Next, send a pull request to foreman-packaging’s deb/develop and/or rpm/develop branches creating the package - see the README.md files in each branch, and other plugins for examples.

There’s a separate repo per major version of Foreman (nightly, 1.11, 1.10 etc.) and we update nightly plus the last three stable releases at any one time. When packaging a plugin update, it can go to any of these repos that you’d like it in - just tell the maintainers when opening a packaging PR. Make sure that you’re comfortable with the compatibility level of the update, knowing which releases it can safely be run on and which it should be updated in. Users on the very old stable releases might not expect to receive a new major version of a plugin with significant changes, even if it runs OK.

Lastly, it’s helpful for maintainers to open up pull requests for packaging updates when making a release to share the workload with the regular packaging maintainers. (The regular packagers are also likely to be unfamiliar with the plugin and which releases it’s appropriate for.)

Code examples

What follows are an assorted collection of code snippets that may be useful. We try and document all of the official plugin APIs with examples here.

Requiring Foreman version

To require a specific foreman version use the bundler require syntax. Most of the version specifiers, like >= 1.4 are self-explanatory, the specifier ~ has a special meaning, best shown by example: ~> 2.1 is identical to >= 2.1 and < 3.0.

To read the full specification visit bundler.io

requires_foreman '>= 1.4'

Avoid using > 1.7, stick to >= 1.8. Greater than 1.7 would include 1.7.1, when the intention is probably only 1.8 and above.

Adding permission

Whether adding a new actions to existing controller or adding a new controller, every action must be mapped to a foreman permission.
See a typical structure of the security section of the registered plugin method:

security_block :security_block_name do
          permission :view_something, {:controller_name => [:index, :show, :auto_complete_search] }
          permission :new_something, {:controller_name => [:new, :create] }
          permission :edit_something, {:controller_name => [:edit, :update] }
          permission :delete_something, {:controller_name => [:destroy] }
end

Adding your permissions to Foreman’s roles

Requires Foreman 1.15 or higher, set requires_foreman '>= 1.15' in engine.rb

Plugins should merge seamlessly with the rest of the application. Foreman provides you with several DSL methods to add your permissions to existing Foreman’s roles.
That way, users with these roles have access to your plugin’s functionality without a need to change anything.

security_block :security_block_name do
  # define your permissions
end

# add permissions to Manager and Viewer roles
add_all_permissions_to_default_roles

Alternatively, one can exclude specific permissions from being added to the default roles by using the following form instead

add_all_permissions_to_default_roles(except: [:first_permission, :second_permission])

If you need more control over what needs to be added you can use the following:

add_permissions_to_default_roles 'Manager' => [:first_permission, :second_permission], 'Viewer' => [:third_permission]

Or alternatively:

add_resource_permissions_to_default_roles ['MyPlugin::FirstResource', 'MyPlugin::SecondResource'], :except => [:skip_this_permission]

Adding roles

The register plugin method allows adding a predefined role, the following sample show how to add a role that includes the set of permissions from the previous section.

  # Add a new role called 'New Role Name' if it doesn't exist
  role "New Role Name", [:view_something, :provision_something, :edit_something, :destroy_something]

Specifying alternate auto-complete path for Role Filters

Requires Foreman 1.6 or higher, set requires_foreman '>= 1.6' in engine.rb

Use search_path_override method with the namespace of your plugin as the parameter to define overrides. Usage example:

search_path_override("Katello") do |resource|
  case resource
    when 'Katello::Content_View'
      '/katello/content_views/auto_complete_path'
    else
      "katello/#{resource.deconstantise.pluralise}/another_search_path"
  end
end

Altering the menu

A plugin can add menu items, entire sub menus and even delete a menu item, here are a few examples:

Adding an item to existing menu:

 # menu(menu_name, item_id, options)
 # menu_name can be one of :user_menu, :top_menu or :admin_menu
 # options can include
 #    :url_hash => {:controller=> :example, :action=>:index}
 #    :caption
 #    :html - set html options for the menu item
 #    :parent, :first, :last, :before, :after - are positions statements
 #    :if => code_block is for conditional menus
 #    :children => code_block is for dynamically creating a list of sub menu items.
 #
 # Example: adding a menu item for new host at the top menu under the hosts sub menu:
 menu :top_menu, :new_host, :url_hash => {:controller=> :hosts, :action=>:new},
      :caption=> N_('New host'),
      :parent => :hosts_menu,
      :first => true

Deleting a menu item

 # Example: delete the hosts menu item
 delete_menu_item :top_menu, :hosts

Adding a divider:

 # Example: add a divider after an entry, same position statements as adding menu items (above) apply
 divider :top_menu, :parent => :monitor_menu, :after => :reports

Adding a sub menu:

 # Adding a sub menu after hosts menu
 sub_menu :top_menu, :example, :caption=> N_('Example'), :after=> :hosts_menu do
   menu :top_menu, :level1, :caption=>N_('the first level'), :url_hash => {:controller=> :example, :action=>:index}
   menu :top_menu, :level2, :url_hash => {:controller=> :example, :action=>:index}
   menu :top_menu, :level3, :url_hash => {:controller=> :example, :action=>:index}
   sub_menu :top_menu, :inner_level, :caption=> N_('Inner level') do
     menu :top_menu, :level41, :url_hash => {:controller=> :example, :action=>:index}
     menu :top_menu, :level42, :url_hash => {:controller=> :example, :action=>:index}
   end
   menu :top_menu, :level5, :url_hash => {:controller=> :example, :action=>:index}
 end

Here is the code in foreman that builds basic menu. You can use it for reference, and for understanding which :parent values will always be there.

Adding a dashboard widget

Requires Foreman 1.6 or higher, set requires_foreman '>= 1.6' in engine.rb

The register plugin method allows adding a widget to the dashboard, the following sample show how to add a widget.

  # Add a new widget <widget_name>
  # options:
  # sizex should be in the range of 1..12, sizey will typically be 1 (defaults are 4 and 1 respectively)
  # The widget can be hidden by default by adding the :hide => true option,
  # The name option will be used to list the widget, in the restore-widget list, after hiding it.
  widget <widget_name>, :name => 'awesome widget', :sizey => 1, :sizex => 4

When the dashboard is displayed, the dashboard page will call "render widget_name". The content of the widget should be in the path:

  app/views/dashboard/_<widget_name>.html.erb

Adding a Pagelet

Requires Foreman 1.11 or higher, set requires_foreman '>= 1.11' in engine.rb

Arbitrary content can be put on specific places in the Foreman Web UI (called "mount points"). To add a pagelet on a specific mount point, use this syntax in the engine.rb file’s plugin registration:

extend_page "smart_proxies/show" do |cx|
  cx.add_pagelet :main_tabs, :name => "New tab", :partial => "smart_proxies/show/mypage_contents"
end

If the mount point does not exist, it can be added in Foreman core by calling the render_pagelets_for method. The first argument is the name of the mount point that should be used when the pagelet is registered. Other arguments are optional. If data needs to be processed by the pagelet, it can be passed as second argument:

render_pagelets_for(:smart_proxy_title_actions, :subject => proxy)

Possible mount points:

  • smart_proxy_title_actions

  • details_content

  • overview_content

  • subnet_index_action_buttons

  • main_tab_fields

  • main_tabs

  • hosts_table_column_header

  • hosts_table_column_content

  • tab_headers

  • tab_content

Extending hosts table with pagelets

Hosts table on the index page can be extended using two predefined pagelets:

add_pagelet :hosts_table_column_header, key: :name, label: _('Name'), sortable: true, width: '25%'
add_pagelet :hosts_table_column_content, key: :name, class: 'ellipsis', callback: ->(host) { name_column(host) }

notice that key is mandatory, since we will present it to the user when selecting columns.

hosts_table_column_header supports the following attributes:

  • key - the name that will be used to choose the column

  • label - a string that will be shown in the header row

  • sortable - true/false value that indicates whether the column should support sorting

  • width - width percentage of the column

  • class - additional html classes to put on the <th> element

  • attr_callbacks - a hash where the key is the name of html attribute, and the value is a function in the form →(host) { attribute_value }

  • callback - a function that receives the host model and returns html content for the <th> element: →(host) { "<span>…​</span>".html_safe }

  • export_key - a string that is used to derive exported column’s name and value from. Passing reported_data.sockets results into header called Reported Data - Sockets and value corresponding to the result of calling host&.reported_data&.sockets. Alternatively, for cases where WebUI columns aggregate multiple values, this can be an array of strings to split the values into their own columns in the export.

  • export_data - An instance (or an array of instances) of CsvExporter::ExportDefinition to be used when the data to be exported cannot be retrieved by a series of calls from the exported object. A lambda can be passed into it with the callback keyword argument. First positional argument is the key, a label can be derived from it unless provided explicitly with the label keyword argument.

hosts_table_column_content supports the following attributes:

  • key - the name that will be used to choose the column

  • width - width percentage of the column

  • class - additional html classes to put on the <th> element

  • attr_callbacks - a hash where the key is the name of html attribute, and the value is a function in the form →(host) { attribute_value }

  • callback - a function that receives the host model and returns html content for the <th> element: →(host) { "<span>…​</span>".html_safe }

You can find usage of those pagelets in the core repository

Using React in plugins

Requires Foreman 1.18 or higher, set requires_foreman '>= 1.18' in engine.rb

Adding columns to the React hosts index page

Similar to the way the legacy hosts index page can be extended via pagelets, columns can also be added to the React hosts index page, or any other page that uses the ColumnSelector component and user TablePreferences. These columns will then be available in the ColumnSelector so that users can customize which columns are displayed in the table. Instead of pagelets, column data is defined in the plugin’s webpack/global_index.js file. The following example demonstrates how to add a new column to the React hosts index page:

import React from 'react';
import { RelativeDateTime } from 'foremanReact/components/RelativeDateTime';
import { registerColumns } from 'foremanReact/components/HostsIndex/Columns/core';
import { __ as translate } from 'foremanReact/common/i18n';

const hostsIndexColumnExtensions = [
  {
    columnName: 'last_checkin',
    title: __('Last seen'),
    wrapper: (hostDetails) => {
      const lastCheckin =
        hostDetails?.subscription_facet_attributes?.last_checkin;
      return <RelativeDateTime defaultValue={__('Never')} date={lastCheckin} />;
    },
    weight: 400,
    tableName: 'hosts',
    categoryName: __('Content'),
    categoryKey: 'content',
    isSorted: false,
  },
];

registerColumns(hostsIndexColumnExtensions);

Each column extension object must contain the following properties:

  • columnName - the name of the column, which must match the column name in the API response.

  • title - the title of the column to be displayed on screen in the <th> element. Should be translated.

  • wrapper - a function that returns the content (as JSX) to be displayed in the table cell. The function receives the host details as an argument.

  • weight - the weight of the column, which determines the order in which columns are displayed. Lower weights are displayed first.

  • tableName - the name of the table. Should match the name of the user’s TablePreference.

  • categoryName - the name of the category to which the column belongs. Displayed on screen in the ColumnSelector. Should be translated.

  • categoryKey - the key of the category to which the column belongs. Used to group columns in the ColumnSelector. Should not be translated.

  • isSorted - whether the column is sortable. Sortable columns must have a columnName that matches a sortable column in the API response.

New structure for assets.

Create 'webpack' directory in the root folder of your plugin and place 'index.js' inside. It will be automatically picked up by webpack.

Registering React components

Any components that a plugin might want to add and use must be registered first. Registering a component is necessary so that component mounter is aware of it and is able to mount it on page.
In your webpack/index.js

  • import component registry

  • import your custom components

  • register components

store attribute determines whether the component will be connected to the Redux store and data attribute whether to pass data from mounting service to a component.

import componentRegistry from 'foremanReact/components/componentRegistry';
import MyComponent from './components/MyComponent';
import MyOtherComponent from './components/MyOtherComponent';

/* name and type is required */
componentRegistry.register({ name: 'MyComponent', type: MyComponent });
/* store and data attributes are true by default */
componentRegistry.register({ name: 'MyOtherComponent', type: MyOtherComponent, store: false, data: false });

/* or to register multiple components: */
componentRegistry.registerMultiple([
  { name: 'MyComponent', type: MyComponent },
  { name: 'MyOtherComponent', type: MyOtherComponent, store: false, data: false }
]);

If you want your component mounted, you must first make sure the assets are loaded in the page. All you have to do is call a helper in your view and then you can mount your component in the same fashion as you would in core:

 <%= webpacked_plugins_js_for :foreman_plugin, :foreman_other_plugin %>
 <%= react_component('MyComponent', :id => '5', :name => 'whatever') %>

Adding 3rd party js libraries

Create package.json in the root of your plugin (you can use npm init). Add dependencies into your plugin’s package.json. Run npm install from the foreman directory to install the dependencies.

Facets

Requires Foreman 1.11 or higher, set requires_foreman '>= 1.11' in engine.rb

Facets is a mechanism for extending a host model and adding new properties to it. For example puppet facet will add environment and puppet_proxy properties.
Every plugin can add one or more facets to a host. Facet is a model that has a one-to-one relationship with the host that is maintained by the framework. It enables us to encapsulate all properties and logic that is related to a specific subject (such as puppet management of a host) to a single model. This enables the user to use mix and match approach to determine which facets of host’s lifetime will be managed by Foreman. Each host can turn facets on or off according to which parts of host’s lifetime should be managed.

How to build a facet

  1. [mandatory] Create a rails model with host_id column for connecting it later to a host

  2. [mandatory] Add a folder with your facet name plural to app/views folder (requires #13873)

  3. [mandatory] Add _your_facet_name.html.erb template file in order to show your new facet as a tab in host’s view. (requires #13873)

  4. [optional] Create a module that will add additional services to a host model. This module will be included in hosts.

  5. [optional] Add helper module to be included in host’s views.

  6. [optional] Add API RABL templates for displaying properties on host list and show API calls. Assume that these templates are in context of host object in both cases.

How to register a facet

Facet registration is done via the initializers mechanism: add a new initializer with the following code:

Rails.application.config.to_prepare do
  Facets.register(PuppetFacet) do
    extend_model PuppetHostExtensions
    add_helper PuppetFacetHelper
    add_tabs :puppet_tabs
    api_view :list => 'api/v2/puppet_facets/base', :single => 'api/v2/puppet_facets/single_host_view'
    template_compatibility_properties :environment_id, :puppet_proxy_id, :puppet_ca_proxy_id
    set_dependent_action :destroy # requires #21657, Foreman >= 1.19
  end
end

This is being re-worked into a proper plugin API via #13417, it’s highly recommended to use that when available and not use internal APIs.

Facets.register method

this method takes two parameters and an initialization block:

  • facet_model A class that will be used as a model.

  • facet_name (optional) a new name for the relation in the host model.

The initialization block exposes the following DSL:

#extend_model

  • extension_module Module to be included in the host model

Use this extension point if you want to add functionality to the Host::Managed object. Be aware that not every host will contain a valid instance of your facet.

#add_helper

  • facet_helper Helper module to be included in host’s view.

Use this extension point to add methods that will be available to the View phase. You will be able to use those methods in your facet’s related templates.

#add_tabs

  • tabs The parameter can be either a hash or a symbol that points to a method in helper.

In addition to the main facet’s tab (that is declared by app/views/my_facets/_my_facet.html.erb) each facet can declare additional tabs to be shown in the UI. The declaration can be either static - a static hash of keys and tab templates, or dynamic - the hash will be generated for each host.

The hash should contain the following information:

  • key should be an identifier that will be used by the UI framework to identify the new tab

  • value should be a value that will be passed to render method - it can be a string representing a template or an object. The render call will set f parameter to the value of host’s form, if you want to add parameters to be passed at the submit method.
    Example:

tabs_hash = {
  :puppetclasses => 'puppet_facets/puppetclasses_tab', #will call puppetclasses_tab.html.erb template
  :facet_tab_example => SomeModel.first, #will try to match a template for SomeModel.
}

static declaration

Rails.application.config.to_prepare do
  Facets.register(PuppetFacet) do
    tabs_hash = {
      :puppetclasses => 'puppet_facets/puppetclasses_tab', #will call puppetclasses_tab.html.erb template
      :facet_tab_example => SomeModel.first, #will try to match a template for SomeModel.
    }

    add_tabs tabs_hash #will generate two more tabs for each host.
  end
end

dynamic declaration

my_facet_helper.rb
def my_additional_tabs(host)
  tabs = {}

  if SmartProxy.with_features("Puppet").count > 0 # add a tab only if this condition evaluates to true
    tabs[:puppetclasses] = 'puppet_facets/puppetclasses_tab'
  end

  tabs
end
my_facet_initializer.rb
Rails.application.config.to_prepare do
  Facets.register(MyFacet) do
    add_helper MyFacetHelper # specify that the facet has a helper
    add_tabs :my_additional_tabs # specify that #my_additional_tabs should be called when deciding which tabs to show for a host.
  end
end

As you can see, the method that you specify will receive a single parameter - the host model that is about to be shown.
The method should return a hash in the same format that was specified earlier.

#api_view

  • views_hash a hash of views and template strings to invoke for each view.

    • :list: this template will be invoked on host list API call.

    • :single: this template will be invoked on single host view API call.

Both templates will be called in a host’s node context - that means you can add properties on the host level itself.

#template_compatibility_properties

  • property_symbols Symbols of properties that need to be maintained at a host level although they moved to a facet.

This method adds the ability to create a compatibility with older templates. Let’s take for example puppet facet refactoring. As a part of this refactoring process environment property has been moved from host.environment to host.puppet_facet.environment. In order to maintain compatibility with foreman templates that were written before the refactoring, the framework will maintain host.environment property and forward the call to the puppet facet.

#api_docs

  • param_group Symbol of the param group that describes properties defined by the facet.

  • controller API controller class that defines the param_group

  • description (optional) Description of the facet attributes param group.

Facets framework is taking advantage of api_pie’s ability to define param group on a different controller. The param group that is defined for a host will be extended with parameters defined by the facet’s controller. Each call to host will be able to set properties on the new facet, using new_facet_attributes main property. The definition of what is inside that property is described by the param_group property of this method.

Add New URL (Route)

If your plugin is adding a new URL to foreman, then you must add a route to the routes.rb file.

config/routes.rb
match 'new_action', :to => 'foreman_plugin_template/hosts#new_action'

For more information on routes, see http://guides.rubyonrails.org/routing.html

Add New Controller Action

If you added a new URL, then you must add a new corresponding controller and action. In the example above, the new URL http://yourforeman/new_action maps to the plugin’s controller named hosts_controller.rb and calls the action named ‘new_action’.

A new plugin controller may inherit from any existing Foreman controller by prefacing the name with two colons (::). See example code below. A plugin’s controller also gives you the option to render a different layout/template than Foreman’s standard template. To do so, just add the word "layout" and it’s path as shown in the example code below.

class HostsController < ::HostsController
layout 'foreman_plugin_template/layouts/new_layout'

In Foreman 1.7+, if you want to use Foreman’s find_resource method as a before_filter in your plugin, you will need to extend Foreman’s ApplicationController and override resource_class, see foreman_salt for an example.

For more information on controllers, see http://guides.rubyonrails.org/action_controller_overview.html

Extending a Controller

If you are extending the app/controllers/application_controller.rb, then within the "config.to_prepare do" block, in the lib/yourplugin/engine.rb of your plugin, add the following:

    ApplicationController.send(:include, YourPlugin::ApplicationControllerExt)

That is, you are attaching your extension class called ApplicationControllerExt to the original ApplicationController.
Then, in your plugin folder, under app/controllers/concerns/yourplugin/application_controller_ext.rb, you can write your own extension.
For instance, if you want to change the Content-Security-Policy HTTP header, then add the following:

module YourPlugin::ApplicationControllerExt
    extend ActiveSupport::Concern

    included do
        before_filter :set_csp
    end

    def set_csp
            response.headers['Content-Security-Policy'] = "default-src 'self';"
    end
end

Modifying controller’s query

Requires Foreman 1.14 or higher, set requires_foreman '>= 1.14' in engine.rb

Every controller’s GET action should fetch its data before rendering a template.
You can modify the scope used for this query by adding a declaration to the plugin definition:

For example, if your plugin extends a view for :index and shows more columns from related tables.

Foreman::Plugin.register :my_plugin do
  add_controller_action_scope(HostsController, :index) { |base_scope| base_scope.includes(:my_table) }
end

Adding a Smart Proxy

Requires Foreman 1.14 or higher, set requires_foreman '>= 1.14' in engine.rb

You can add smart proxies to the Subnet, Host, Hostgroup, Domain and Realm models.
This :if parameter is optional. You can define whether the field should be hidden in the UI.

# add discovery smart proxy to subnet
smart_proxy_for Subnet, :discovery,
  :feature => 'Discovery',
  :label => N_('Discovery Proxy'),
  :description => N_('Discovery Proxy to use within this subnet for managing connection to discovered hosts'),
  :api_description => N_('ID of Discovery Proxy'),
  :if => ->(subnet) { subnet.supports_ipam_mode?(:dhcp) }

Authenticating a Smart Proxy

If you have controller actions that SSL-authenticated Smart Proxies should be able to access, add this to your controller:

class MyController < ApplicationController
  include Foreman::Controller::SmartProxyAuth

  add_smart_proxy_filters :my_method, :features => 'My Feature'

  def my_method
    # do stuff
  end
end

Extend Foreman Model (Add instance or class methods)

Your plugin’s controller may call new instance, class methods, or callbacks on an existing Forman model (ex. Host). The recommended way to do this is to create a module (ex. host_extensions.rb) under the /models directory and use extend ActiveSupport::Concern. Below is an example from from host_extensions.rb.

module ForemanPluginTemplate
  module HostExtensions
    extend ActiveSupport::Concern

    included do
      # execute callbacks
    end

    # create or overwrite instance methods...
    def instance_method_name
    end

    module ClassMethods
      # create or overwrite class methods...
      def class_method_name
      end
    end
  end
end

Now within your engine.rb, simply tell rails to load that module:

module ForemanPluginTemplate
  class Engine < ::Rails::Engine

  config.to_prepare do
    Host.send :include, ForemanPluginTemplate::HostExtensions
  end
end

Add New View

By default, a controller action will render a view with the same name as its action. However, you can add multiple new views to your foreman plugin and specify in your controller when to render which view.

def new_action
  render 'hosts/different_view'
end

For more information on controllers, see http://guides.rubyonrails.org/layouts_and_rendering.html

Adding Rails helpers

Rails helpers are mixed-in all views and controllers, therefore the method names must be unique. When defining helper methods, include some kind of unique prefix for your plugin.

Add new migration

Prerequisites

You can use rails generate migration helper to create new migrations in you engine. However, to make the application see your migrations, you must add following code into your plugin initializer

module PluginTemplate
  class Engine < ::Rails::Engine
    initializer "foreman_chef.load_app_instance_data" do |app|
      app.config.paths['db/migrate'] += PluginTemplate::Engine.paths['db/migrate'].existent
    end
  end
end

Initializer is usually to be found at lib/foreman_plugin_template/engine.rb.

Generating a new migration file

As of Foreman 1.16 migration files could be generated by invoking

rails generate plugin:migration --plugin-name=my_plugin

that will create a migration file and put it into plugin’s migrations directory. You can use any parameters defined in Rails migrations guide in addition to two specialized parameters:

  • --plugin-name(required) Specify the name of your plugin. This name would be used to scope all your migrations.

  • --plugin-source(optional) Specify where your plugin source is located. If not specified, it assumes a typical developer’s directory structure:

root
  |
  +-- foreman   # foreman core directory
  |
  +-- my_plugin # plugin directory

Running your migrations

  • You can use rake db:migrate in your app directly to run all pending migrations (from all available plugins).

  • You can use rake db:migrate SCOPE=my_plugin to apply migrations from a single plugin only.

Advanced

Under the hood, migrations scope is implemented as a postfix to a migrations file name, i.e.: 000000_my_migration_name.my_plugin.rb.

If all your migrations were created using this scheme, the user will be able to remove every trace of the plugin from the database
by running rake db:migrate SCOPE=my_plugin VERSION=0 statement.

Adding new provisioning templates

Provisioning templates exist in Foreman as eRuby files under "views". To add new provisioning templates to a plugin, first create an eRuby file for each new template. Then, create a DB seed file so that your new templates will exist in the Foreman DB. A good example of this is available here: 50-bootdisk_templates.rb

Adding new model classes

New model classes should use ApplicationRecord parent class which is a Rails 5 practice (but implemented in Foreman versions on Rails 4):

class MyModel < ApplicationRecord
  ...
end

Add new database seeds

Requires Foreman 1.6 or higher, set requires_foreman '>= 1.6' in engine.rb

Inside your plugin, create a seeds directory at db/seeds.d/ and add .rb files inside. These should contain plain Ruby statements to add records in the application, and they will be run after the main Foreman DB seeding (so you can rely on things such as template kinds being available).

Ensure that your seed scripts are idempotent, otherwise when the db:seed task runs on upgrades etc, you may get multiple resources, errors etc.

Further, placing seeds in the above directory can then be interjected in between the Foreman seeds by using unix ordering (e.g. 06-my-plugin-seeds.rb)

Permitting new attributes on Foreman models

Requires Foreman 1.13 or higher, set requires_foreman '>= 1.13' in engine.rb

When a new attribute is added via a DB migration (or accessor) to a core Foreman model, if it’s going to be updated through an API or UI controller then it has to be added to the attribute whitelist. In the plugin registration, add:

Foreman::Plugin.register :sample_plugin do
  parameter_filter Host::Managed, :sample_attribute
end

More information is available on the Strong parameters page.

Modify Existing Foreman View (using Deface)

Several actions are allowed to edit the original Foreman views, from "replace" to "insert_after", as listed in the deface manual .

To use deface, first add the dependency to the plugin gemspec (e.g. foreman_example.gemspec):

s.add_dependency 'deface'

When instantiating the Deface::Override class, you need to specify one Target, one Action one Source parameter and any number of Optional parameters. All the supported values for each of them are in the manual.

For instance, in order to replace the line "<%= link_to "Foreman", main_app.root_path %>" from the file foreman/app/views/home/_topbar.html.erb:

Deface::Override.new(:virtual_path => "home/_topbar",
                     :name => "replace_title",
                     :replace => "erb[loud]:contains('link_to')",
                     :text => "<a href='/'>Hello</a>",
                     :original => "<%= link_to \"Foreman\", main_app.root_path %>")

Just copy and paste the code above as it is, within a file under app/overrides within your own plugin folder. The file name has to be the same as what specified by the parameter :name above, i.e., in this case, replace_title.rb.

The :original parameter enables the logging of eventual future changes to the original view, whenever those changes affect the line that is meant to be replaced by deface.

The deface manual shows further examples and an alternative way of modifying existing views, i.e., using .deface files.

Extend safemode access

Requires Foreman 1.5 or higher, set requires_foreman '>= 1.5' in engine.rb

When extending a template render (e.g. UnattendedHelper), then additional methods and variables will usually be blocked by safemode, but these can be permitted with the following plugin registration declarations:

allowed_template_helpers :subscription_manager_configuration_url
allowed_template_variables :subscription_manager_configuration_url

These would permit access to a helper named "subscription_manager_configuration_url" or to an instance variable named @subscription_manager_configuration_url. Note that you’d have to define the "subscription_manager_configuration_url" method in TemplatesController and its descendant as well as UnatendedHelper module to make it available for both previewing and rendering. The easiest way is to implement it as in a concern that you include in all of these classes.

Requires Foreman 1.12 or higher, set requires_foreman '>= 1.12' in engine.rb

You can instead use extend_template_helpers, all you have to do is give it a module which public methods will be made available.

# imagine we have module like this
module ForemanChef
  module ChefTemplateHelpers
    def chef_url
      protocol + 'example.tst'
    end

    private

    def protocol
      'https://'
    end
  end
end

# in plugin engine.rb:
initializer 'foreman_chef.register_plugin', :after => :finisher_hook do |app|
  Foreman::Plugin.register :foreman_chef do
    requires_foreman '>= 1.12'
    extend_template_helpers ForemanChef::ChefTemplateHelpers
  end
end

The example above will make "chef_url" helper available in templates and will also allow it for safemode rendering like you’d call allowed_template_helpers :chef_url. Note that the private method "protocol" will not be safemode whitelisted.

Generating plugin assets

Requires Foreman 1.5 or higher, set requires_foreman '>= 1.5' in engine.rb

In the foreman folder, enable the plugin. When doing this in package build script, you need to add Foreman as a build dependency.

$ cat bundler.d/Gemfile.local.rb
gem 'foreman_plugin', :path => "../foreman_plugin/"

To generate Rails pipeline assets, be sure to have the "foreman-assets" package installed and run (again in the foreman app folder):

$ rake plugin:assets:precompile[foreman_plugin]

Logging

Requires Foreman 1.9 or higher, set requires_foreman '>= 1.9' in engine.rb

Foreman provides support for plugins to log messages contextually so that when looking from the master log file it is easy to see where messages come from. For example, Foreman will log messages to the 'app' logger for Rails specific calls and foreman_docker can log custom messages to it’s own logger to give a better idea of where messages are coming from:

2015-05-13 13:28:22 [app] [D] Request for /foreman_docker/registry
2015-05-13 13:28:22 [foreman_docker] [D] Initializing docker registry for user admin

By default, loggers are generated for all plugins based upon their plugin ID when registering a plugin. Thus, a plugin registering itself as 'foreman_docker' would automatically have a logger made available by that same name. For that plugin to log messages, they need only request that logger and then use it similar to the default Rails logger:

Foreman::Logging.logger('foreman_docker').debug "Initializing docker registry for user #{User.current}"

Note that if plugins use the standard Rails logging (i.e. Rails.logger.debug), the log messages will go to the 'app' logger defined by Foreman core. Plugin developers must make a conscious choice to use the plugins logger throughout their code. Plugins can also create multiple, configurable loggers such as the Katello plugin that logs things like REST calls to backends to different loggers.

Custom Plugin Loggers

Besides the default logger generated automatically, plugins can create any number of custom loggers to log different concerns throughout their codebase. For example, the Katello plugin creates a 'pulp_rest' logger to log only REST calls to Pulp. This logger can be configured with it’s own log level and enabled or disabled. New loggers can be defined through the Plugin API or in the settings file for the plugin. The plugin settings file also serves as a way to re-configure predefined loggers.

Using the Plugin API:

Foreman::Plugin.register :foreman_docker do
  ....

  logger :rest, :enabled => true
  logger :registry, :enabled => false
end

This will create two new loggers for use by the foreman_docker plugin. The rest logger is enabled by default, the registry logger is disabled by default. These loggers can then be used within the plugin code as such:

Foreman::Logging.logger('foreman_docker/rest').debug 'REST call to /docker/registry'
Foreman::Logging.logger('foreman_docker/registry').info 'Created new registry'

In this case, the log file would only show:

2015-05-13 13:28:22 [foreman_docker/rest] [D] REST call to /docker/registry

Let’s now assume that a user wants to see registry logging. They would edit the foreman_docker settings file as such:

:foreman_docker:
  :loggers:
    :registry:
      :enabled: true

It’s recommended that the plugin ships an example config file with a full, commented out list of loggers and show the default enabled true/false value.

Note
Custom plugin loggers MUST be defined somewhere to be used. The logging system will throw a failure message if loggers that aren’t registered are attempted to be used. This is to prevent using unknown loggers or loggers that are not properly namespaced as enforced by the core logging code. See the next section to learn about namespacing.

Namespacing

In the 'Custom Plugin Loggers' section, a logger for foreman_docker was defined as 'rest'. However, to access the logger the call to get the logger included 'foreman_docker' preceding the 'rest' declaration. All plugin loggers (except the default since it already IS the namespace) are namespaced by the ID of the plugin that it registered with. This is to ensure that two loggers from multiple plugins do not clash and are
clearly denoted within the logs to identify where the message came from.

Extending host model

Add custom host status

In Foreman 1.10 and above you can affect a host status by your own custom, plugin-specific status. To do so, you must create a new class that represents the custom status and define mapping to global status. A simple example might be following status class

class RandomStatus < HostStatus::Status
  ODD = 0
  EVEN = 1

  # this method must return current status based on some data, in this case it's random
  def to_status
    result = rand(2).odd?
    if result
      ODD
    else
      EVEN
    end
  end

  # this method defines mapping to global status, see HostStatus::Global for all possible values,
  # at the moment there OK, ERROR and WARN global statuses
  # we map ODD result to ERROR while EVEN random number will be OK
  def to_global
    if to_status == ODD
      return HostStatus::Global::ERROR
    else
      return HostStatus::Global::OK
    end
  end

  # don't forget to give your status some name so it's nicely displayed
  def self.status_name
    N_('Random number')
  end

  # you probably want to represent numbers with some more descriptive messages
  def to_label
    case to_status
      when ODD
        N_('Random number was odd')
      when EVEN
        N_('Random number was even')
      else
        N_('The world has ended')
    end
  end
end

The status class must implement the followig methods:

  • to_label: this method will be called to determine the string that will be used while displaying the status value.

  • self.status_name: this method will be called to determine what label to display for the status.

It can also implement the following methods according to the specific needs:

  • to_global: This method will be used to determine global status according to this specific one. The mechanism here is "voting" - to_global is called for each status and the highest value from the list would be taken. The default is HostStatus::Global::OK.

  • to_status: This method is used to determine the status based on external values in the system. By default it will return the previous value the status had. This default is useful if the status could not be determined by examining the current state, for example if the status is changing by some external event.

For more information about possible customizations see the status.rb base class.

There are times when you may want to create a status that should not affect the host’s global status. One use case is when there exists a status which derives its own status from one or more sub-statuses. Implementing a sub-status is as simple as implementing the substatus? method in your code:

class MySubStatus < HostStatus::Status

# other status methods omitted for brevity

  def substatus?
    true
  end
end

Now when you have your class defined, you have to make Foreman know about it. In your plugin register call in engine.rb add following line

Foreman::Plugin.register :foreman_remote_execution do
  ...
  register_custom_status RandomStatus
  ...
end

If your custom status is under HostStatus namespace, make sure you define it as

class HostStatus::RandomStatus

avoid definition like this

module HostStatus
  class RandomStatus < HostStatus::Status
  end
end

otherwise you will encounter hard to debug loading issues on Foreman 1.10

When updating or refreshing a sub-status, be sure to call refresh_statuses, which will update all of the other statuses including the global status.

my_host.refresh_statuses

The method refreshes all statuses by default, this is usually not what you want so provide status for refresh.

For each status, the user should be able to search host by it’s possible values. Your plugin must extend the Host::Managed object. Here is an example of how such extension definition could look like.

module ForemanRemoteExecution
  module HostExtensions
    def self.prepended(base)
      base.instance_eval do
         # We need to make sure, there's a AR relation we'd be searching on, because the class name can't be determined by the
         # association name, it needs to be specified explicitly (as a string). Similarly for the foreign key.
         has_one :execution_status_object, :class_name => 'HostStatus::ExecutionStatus', :foreign_key => 'host_id'

         # Then we define the searching, the relation is the association name we define on a line above
         # :rename key indicates the term user will use to search hosts for a given state, the convention is $feature_status
         # :complete_value must be a hash with symbolized keys specifying all possible state values, otherwise the autocompletion
         # would only offer values that are already in the database, however it's not guaranteed that user have hosts covering
         # the whole set of values, that's why the explicit list is necessary. That way user can easily search of all hosts with
         # "error" even though no host has such status.
         scoped_search :relation => :execution_status_object, :on => :status,
                       :rename => :execution_status,
                       :complete_value => { :ok => HostStatus::ExecutionStatus::OK, :error => HostStatus::ExecutionStatus::ERROR }
      end
    end
  end
end

For more information about searching capabilities, check the [scoped_search](https://github.com/wvanbergen/scoped_search) gem documentation.

my_host.refresh_statuses([HostStatus.find_status_by_humanized_name("statusname")])
# or:
my_host.refresh_statuses([MyHostStatus])

Selecting properties to clone

Requires Foreman 1.11 or higher, set requires_foreman '>= 1.11' in engine.rb

If you extend the Host::Managed object and add attributes or associations to the model, you probably want those to be cloned with the rest of the host object.
In your concern you should add the following calls:

module ForemanPluginTemplate
  module HostExtensions
    extend ActiveSupport::Concern

    included do
      # specify which properties to include in clone
      include_in_clone :property1, :property2

      # specify which properties should not be cloned
      exclude_from_clone :property3, :property4
    end
  end
end

All attributes on the model will be cloned by default (therefore may be excluded), while associations to other models will not be cloned by default (therefore may be included).

Host info providers

Every host exposes Host#info method to provide a complete information hash about itself. This hash is mainly used as external node classifier in puppet.
Any plugin can extend this info by creating a class that inherits HostInfo::Provider and registering it in the plugin:

# In plugin declaration (engine.rb):
Foreman::Plugin.register :my_plugin do
  register_info_provider MyPlugin::InfoProvider
end

# Actual info provider class
module MyPlugin
  class InfoProvider < HostInfo::Provider # inherit the base class

    # override this method according to principles specified below
    def host_info
      { 'parameters' => host.params }
    end
  end
end

Info hash is structured in the following way:

host_info = Host.first.info

host_info['classes'] # set of puppet classes that are associated with this host including class parameters
host_info['parameters'] # list of foreman properties that are associated with this host i.e taxonomy, hostgroup, interfaces.
# This list also includes values of global parameters associated with the host.
host_info['environment'] # Host's environment

Extending host UI

A plugin can add fields displayed in Properties tab on host overview page, add buttons to Details area on host overview page, add actions to the right side of the title area on host overview page and add actions for multiple selected hosts.

Adding an item to one of those lists requires adding a helper with a method that returns relevant items to your plugin and registering that method in plugin description. The methods should return a list of hashes, where each hash will have two predefined fields: :priority and either :field, :action or :button according to the desired extension point. :priority value would be used by the system to define the order of the items to show. The lower the priority, the higher the item will show.

Adding overview fields

In this case, the method that will contribute overview fields will receive a host instance for generating the field. Here are a couple of examples of fields added by the core. Notice the :priority setting, it will determine the order in which the fields are shown.

In plugin helper (my_plugin_helper.rb):

def my_plugin_host_overview_fields(host)
  fields = []
  fields << { :field => [_("Build duration"), build_duration(host)], :priority => 90 } # call to other helper method
  fields << { :field => [_("Operating System"), link_to(host.operatingsystem.to_label, hosts_path(:search => "os_description = #{host.operatingsystem.description}"))], :priority => 800 } # creating a linkable item
  fields << { :field => [_("PXE Loader"), host.pxe_loader], :priority => 900 } # adding a simple value

  fields
end

Now we have to register our new helper in engine.rb:

Foreman::Plugin.register :my_plugin do
  describe_host do
    overview_fields_provider :my_plugin_host_overview_fields
  end
end

Adding overview details buttons

In this case, the method that will contribute buttons will also receive a host instance for generating the action. Here are a couple of examples of actions added by the core. Notice the :priority setting, it will determine the order in which the buttons are shown.

In plugin helper (my_plugin_helper.rb):

def my_plugin_host_overview_buttons(host)
  [
    { :button => link_to_if_authorized(_("Audits"), hash_for_host_audits_path(:host_id => host), :title => _("Host audit entries"), :class => 'btn btn-default'), :priority => 100 },
    { :button => link_to_if_authorized(_("Facts"), hash_for_host_facts_path(:host_id => host), :title => _("Browse host facts"), :class => 'btn btn-default'), :priority => 200 },
  ]
end

Now we have to register our new helper in engine.rb:

Foreman::Plugin.register :my_plugin do
  describe_host do
    overview_buttons_provider :my_plugin_host_overview_buttons
  end
end

Adding actions to title area

In this case, the method that will contribute actions will also receive a host instance for generating the action. Here are a couple of examples of actions added by the core. Notice the :priority setting, it will determine the order in which the actions are shown.

In plugin helper (my_plugin_helper.rb):

def my_plugin_host_title_actions(host)
  [
    {
      :action => button_group(
        link_to_if_authorized(_("Edit"), hash_for_edit_host_path(:id => host).merge(:auth_object => host),
                                :title    => _("Edit this host"), :id => "edit-button", :class => 'btn btn-default'),
        display_link_if_authorized(_("Clone"), hash_for_clone_host_path(:id => host).merge(:auth_object => host, :permission => 'create_hosts'),
                                :title    => _("Clone this host"), :id => "clone-button", :class => 'btn btn-default'),
      ),
      :priority => 100
    },
    {
      :action => button_group(
        link_to_if_authorized(_("Delete"), hash_for_host_path(:id => host).merge(:auth_object => host, :permission => 'destroy_hosts'),
                              :class => "btn btn-danger",
                              :id => "delete-button",
                              :data => { :message => delete_host_dialog(host) },
                              :method => :delete)
      ),
      :priority => 300,
    },
  ]
end

Now we have to register our new helper in engine.rb:

Foreman::Plugin.register :my_plugin do
  describe_host do
    title_actions_provider :my_plugin_host_title_actions
  end
end

Adding actions to multiple host select menu

In this case, the method that will contribute actions will not receive any parameters. Here are a couple of examples of actions added by the core. Notice the :priority setting, it will determine the order in which the actions are shown.

In plugin helper (my_plugin_helper.rb):

def my_plugin_multiple_actions
  [
    { :action => [_('Assign Organization'), select_multiple_organization_hosts_path], :priority => 800 },
    { :action => [_('Assign Location'), select_multiple_location_hosts_path], :priority => 900 }
  ]
end

Now we have to register our new helper in engine.rb:

Foreman::Plugin.register :my_plugin do
  describe_host do
    multiple_actions_provider :my_plugin_multiple_actions
  end
end

Extending hostgroup UI

Adding actions to the index page

A plugin can add items to the Actions dropdown in the table on the hostgroups overview page.

Adding an item to the actions dropdown requires adding a helper with a method that accepts a hostgroup as an argument and returns a list of hashes, where each hash will have two predefined fields: :action and :priority. The :action item should be an HTML element (probably a link) that will be embedded as an item in the dropdown. The :priority value would be used by the system to define the order of the items to show. The lower the priority, the higher the item will show.

There is also an option to add action with a disabled link by passing the :action as a hash. This hash has two values: :content which contains the HTML link as before, and :options which contains a hash with the HTML options to be added to the action’s li tag. See the second example below.

In plugin helper (my_plugin_helper.rb):

def my_plugin_hostgroups_actions(hostgroup)
  [
    { :action => display_link_if_authorized('new_action', {other_properties}), :priority => 20 }
    { :action => { :content => display_link_if_authorized('new_action', {other_properties}), :options => { :class => 'disabled' } }, :priority => 20 }
  ]
end

Now we have to register our new helper in register.rb:

Foreman::Plugin.register :my_plugin do
  describe_hostgroup do
    hostgroup_actions_provider :my_plugin_hostgroups_actions
  end
end

Settings

Plugins can store Foreman-wide settings either in the database or a config file. The DB should be preferred as it can be managed from the UI (under Administer > Settings), CLI and API. It also can be changed on the fly, while the config file is usually only used for settings that change behaviour during app startup and require a restart.

To add DB settings, the plugin should define them in it’s registration block:

Foreman::Plugin.register :my_plugin do
  # ....

  settings do
    # Following settings will be added to a General category
    category :general do
      setting 'example_setting',
        type: :string,
        default: 'default value',
        full_name: N_('Example of general setting'),
        description: N_('Example setting that controls something')
    end

    # Following settings will be added to category name 'cool' with label Cool
    category :cool, N_('Cool') do
      setting 'example_int',
        type: :integer,
        default: 42,
        full_name: N_('The answer'),
        description: N_('Answer to the life, universe, and everything')
    end

    # Following settings will be added to existing category named 'cfgmgmt'
    category :cfgmgmt do
      setting 'configure_everything',
        type: :boolean
        default: true,
        full_name: N_('Configure everything'),
        description: N_('Should configuration management tools configure everything for user, so user can go to the beach?')
    end
  end
end

To access the value of a setting, use Setting[:example_setting] from anywhere in your plugin.

The settings are strongly typed and you have to define it. The basic types supported by Foreman are: :boolean, :integer, :float, :string, :text, :hash, :array. The :text type supports markdown and usage of such setting should expect markdown syntax when using it.

Initial setting value

In most cases, a setting should only have a default defined via the DSL, not an initial value. If you really need the setting to have an initial value, please use a seed to set it.

Setting[:instance_id] = Foreman.uuid unless Setting.where(name: 'instance_id').exists?

Validate setting values

To validate setting value, you can use API, that tries to mimic the API of ActiveRecord. We can use most of the perks offered by ActiveRecord, only defining on setting name instead of attribute. We are adding just some shorthands like direct regexp validations. The attribute is always value, you can’t validate anything else as it is the only user input.

You have two ways to define the validations: * inline with setting definition by symbol matching ActiveRecord validator, RegExp on strings, or lambda function that gets value to validate as argument. * using validates of validates_with methods, that mimic [Rails validation methods](https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html), but are using setting names instead of attribute names, as the validated attribute is always value in this case.

settings do
  category(:cfgmgmt) do
    # Following definitions are missing full names for simplification

    setting(:blank_setting, type: :string,  default: '', description: 'Unnecessary setting')

    setting(:cool_setting, type: :string, default: 'cool' description: 'Setting with only cool values', validate: /^cool.*/)

    setting(:cooler_puppet, type: :integer, default: 5, description: 'Use Puppet that goes to 11', validate: ->(value) { value <= 11 })

    validates(:cooler_puppet, numericality: { greater_than: 10 }, if: -> { Setting[:cool_setting] == 'coolest' }, allow_blank: true)

    # the validator needs to be ActiveModel::Validator
    validates_with :cool_setting, MyCoolnessValidator
  end
end

Config files

Config files are in YAML format and can contain simple or complex data. They are read from config/settings.yaml and config/settings.plugins.d/ (aka /etc/foreman/plugins/) at startup and all contents are merged together and stored in the global SETTINGS hash.

It’s recommended to put all settings in a hash named after the plugin so they don’t conflict with others, e.g.

:foreman_example: +
:foo: bar

Then to access the value, use SETTINGS[:example][:foo] from the plugin.

Do keep an example config file in the repo at config/foreman_example.yaml.example or similar, and ensure it’s listed in the gemspec files list. This makes it easy to package and for users to see what the possible options are.

Tip: database settings can be overridden from a config file out of the box, making the value read-only in the UI. Just set :example_string: foo in settings.yaml or settings.plugins.d/.

Provision Method

Requires Foreman 1.11 or higher, set requires_foreman '>= 1.11' in engine.rb

In Foreman 1.11 or above you can add custom provision methods via a plugin.

Just extend the engine.rb

      Foreman::Plugin.register :foreman_bootdisk do
        requires_foreman '>= 1.11'
        provision_method 'bootdisk', 'Bootdisk Based'
      end

You can then extend the host edit / new host ui, e.g. add the file
app/views/hosts/provision_method/bootdisk/_form.html.erb

Controlling installation media

By default foreman comes with simple installation media management that could be accessed via "Hosts" → "Installation media" from the menu.
If a plugin introduces a different media management, it should register a new MediumProvider class in order to control medium’s URL and TFTP file naming scheme.

Creating medium provider

Medium provider is a class that inherits ::MediumProviders::Provider. This base class provides all utility methods and method signatures needed for creating your own media provider. Foreman’s core basic medium provider is implemented in ::MediumProviders::Default class.

Each time installation medium related information for a specific entity (host or hostgroup) would be requested, a new instance of installation medium class would be created and the entity passed to it in the constructor.

Medium provider has following key functions:

  • medium_uri: returns installation medium URI for a given host

  • unique_id: returns a unique string representing current medium, will be used to generate TFTP file names for example.

  • validate: Returns true if this medium provider can handle given entity. Mostly it will examine properties that are set on the entity to see if medium URI could be generated. This method will be used to determine if this is the correct medium provider for a given entity. It returns an array of errors, if a provider cannot handle the entity, or empty array if everything is OK.

Example:

module MyPlugin
  class ManagedContentMediumProvider < ::MediumProviders::Provider
    def validate
      errors = []

      kickstart_repo = entity.try(:content_facet).try(:kickstart_repository) || entity.try(:kickstart_repository)

      errors << N_("Kickstart repository was not set for host '%{host}'") % { :host => entity } if kickstart_repo.nil?
      errors << N_("Content source was not set for host '%{host}'") % { :host => entity } if entity.content_source.nil?
      errors
    end

    def medium_uri(path = "")
      kickstart_repo = entity.try(:content_facet).try(:kickstart_repository) || entity.try(:kickstart_repository)
      url = kickstart_repo.full_path(entity.content_source)
      url += '/' + path unless path.empty?
      URI.parse(url)
    end

    def unique_id
      @unique_id ||= begin
        "#{entity.kickstart_repository.name.parameterize}-#{entity.kickstart_repository_id}"
      end
    end
  end
end

Registering medium provider

Once medium provider is created we will need to register it in plugin declaration:

Foreman::Plugin.register :my_plugin do
  medium_providers_registry.register(MyPlugin::ManagedContentMediumProvider)
end

Compute resources

Requires Foreman 1.5 or higher, set requires_foreman '>= 1.5' in engine.rb

Plugins can add new compute resource types, allowing users to create hosts on new types of virtualisation or cloud providers. The plugin should create a new model that extends ComputeResource, e.g. ForemanExample::MyService:

module ForemanExample
  class MyService < ComputeResource
    # ...
  end
end

and register it:

Foreman::Plugin.register :foreman_bootdisk do
  requires_foreman '>= 1.5'
  compute_resource ForemanExample::MyService
end

In Foreman 1.12, a provider with the same name as a builtin Foreman compute resource type can be registered from a plugin. This allows a plugin to override the builtin one, making it easier to extract or update a builtin provider from Foreman to a plugin.

Fog provider

This requires support in Fog for the provider - usually with a fog-myservice gem, see the list of available repositories at https://rubygems.org/search?utf8=%E2%9C%93&query=fog%2D or https://github.com/fog. If the provider isn’t yet implemented, see Create New Provider from Scratch.

Some providers are in the main fog gem still, rather than a separate gem. It’s recommended that these are extracted to a gem before using them for a plugin, as Foreman may drop the dependency on the whole fog gem in future - it’s much easier for a plugin to depend only on the provider gem it needs.

Required interfaces

This section needs expanding, please edit as you find missing items. Look at existing compute resource plugins and classes in Foreman core to get an idea of what needs implementing on the main compute resource model.

  • #capabilities should return an array containing :build if it supports network/PXE installations, and/or :image if it supports image/template installations

  • #client should return a new Fog::Compute instance

  • #provided_attributes returns a hash of Foreman host attributes (:uuid, :ip, :ip6, :mac) to Fog server model methods. Foreman copies data from the Fog server model (see below) to these attributes. By default it returns :uuid ⇒ :identity, so the UUID of the host/VM is stored. Add MACs, IP and IPv6 addresses if available from the compute resource.

The Fog server model is used a lot to render views in Foreman, so this should respond to a variety of methods too. These aren’t usually in Fog itself so are extended with a concern in the plugin (e.g. https://github.com/theforeman/foreman-xen/blob/master/app/models/concerns/fog_extensions/xenserver/server.rb).

  • #identity must return a unique string identifier (UUID, number etc) for the VM on that compute resource, for non-string IDs add a different method and change :uuid in provided_attributes (see above)

  • #ip_addresses should return an array of every IP address assigned to the VM, including public, private, IPv4 and IPv6 addresses

  • #reboot should perform a soft reboot on the VM

  • #reset should perform a hard power reset on the VM

  • #start should power on or boot up the VM

  • #stop should power off or shut down the VM

  • #to_s should return the server’s name for display in confirmation dialog boxes

  • #vm_description should return a short piece of text shown on the compute profiles pages describing basic info about the server "hardware" (e.g. CPUs, memory)

Required views

  • app/views/compute_resources/form/_myservice.html.erb should contain form elements for creating/editing the compute resource

  • app/views/compute_resources/show/_myservice.html.erb should contain rows with extra attributes shown on the compute resource information page

  • app/views/compute_resources_vms/form/myservice/_base.html.erb should contain form elements for creating new hosts/VMs, e.g. CPU/memory information

  • app/views/compute_resources_vms/form/myservice/_network.html.erb should contain form elements for network interfaces when creating new hosts/VMs, e.g. which provider network the interface is connected to

  • app/views/compute_resources_vms/form/myservice/_storage.html.erb should contain form elements for storage volumes when creating new hosts/VMs, e.g. which storage pool the device is on

  • app/views/compute_resources_vms/index/_myservice.html.erb should contain a table of information about current virtual machines on the compute resource, shown under the CR page

  • app/views/compute_resources_vms/show/_myservice.html.erb should show a table of detailed information about an individual current virtual machine

Printing date and time

In order to keep consistency in format we use, Foreman 1.16+ provide helpers to print the date either in relative (3 days ago) or absolute (2017-05-01 08:12:11) way. It also adds a title with respective information, so after hovering e.g. on absolute date, the relative time information is displayed. Absolute date helper supports two formats, short and long

Examples

date_time_absolute(Time.zone.now)
date_time_absolute(@user.last_login_at, :long)
date_time_relative(@host.last_report_at)

Extending RABL templates

Requires Foreman 1.17 or higher, set requires_foreman '>= 1.17' in engine.rb

In order to extend APIv2 views with e.g. more attributes, you can extend the RABL templates.

Examples

This will extend the template "api/v2/hosts/main" (from core) by including "api/v2/hosts/expiration" (from our plugin).

# lib/foreman_expire_hosts/engine.rb
Foreman::Plugin.register :foreman_expire_hosts do
  [...]
  extend_rabl_template 'api/v2/hosts/main', 'api/v2/hosts/expiration'
end
# app/views/api/v2/hosts/expiration.json.rabl
attribute :expired_on

Making use of Reports "origin"

Reports have an attribute called origin, which can be used to set what submitted this report. Based on it Foreman allows a few customization for reports of that origin.

Registering an origin

To start using an origin for reports handled by a plugin it first needs to register it via register_report_origin, when it registers itself in Foreman.

Here an example from foreman-ansible register.rb:

  register_report_origin 'Ansible', 'ConfigReport'

The first argument is the origins name, which will be set as the reports origin attribute. The second optional argument is to specify a certain type Report that the origin can be applied to.

Registering a ReportScanner

In order to set the origin attribute on reports, they need to be identified. This can be done with a ReportScanner, which can be registered with register_report_scanner.

foreman-ansible for example provides one:

  register_report_scanner ForemanAnsible::AnsibleReportScanner

AnsibleReportScanner is a simple class that has a .scan method, which will be called when a report is imported. .scan will receive the report object and the raw logs to identify the report and make changes to the report based on this.

Provide a custom report view & icon for an origin

Via helpers it is possible for a plugin using an origin to provide a custom view template to be used for showing reports, as well as a custom icon to show for reports of that origin. This helpers must follow a certain naming schema and be available to ReportsHelper.

  • ORIGIN_report_origin_icon - should return a string with the path to an asset

  • ORIGIN_report_origin_partial - should return a string with the path to a view template.

For an example see the foreman_ansible plugin.

Origin based settings

To influence the out of sync behavior for host reports for a specific origin, it is possible for plugins to provide settings that will be recognized and used to determine whether hosts are out of sync or good. Out of sync can also be fully disabled for a certain origin. The settings must be named as follows and provide the right setting type.

  • ORIGIN_interval - A String/Integer of minutes for the interval that hosts of this origin need report.

  • ORIGIN_out_of_sync_disabled - A boolean setting to disable the out of sync status for hosts reporting with this origin.

Extending the graphql schema

Requires Foreman 1.23 or higher, set requires_foreman '>= 1.23' in engine.rb

To extend a graphl type with custom code, you can register the extension via extend_graphql_type in your plugin’s engine.rb. The plugin DSL allows to pass a code block that is run in the type’s class scope.

  extend_graphql_type type: Types::Host do
    belongs_to :openscap_proxy, Types::SmartProxy
  end

In order to extend a graphql type with code defined in a module, you can register an extension by passing the module name to extend_graphql_type. The module should extend ActiveSupport::Concern. Note that any code that is supposed to run in the class scope of the module needs to be in an included do …​ end block.

   extend_graphql_type type: Types::SmartProxy, with_module: ForemanOpenscap::SmartProxyTypeExtensions

Adding new graphql types

Requires Foreman 1.23 or higher, set requires_foreman '>= 1.23' in engine.rb

When you create a new graphql type in your plugin, you need to register it in your engine.rb so that Foreman knows how it should be used in a query.

  register_graphql_query_field :duck, '::Types::Duck', :record_field
  register_graphql_query_field :ducks, '::Types::Duck', :collection_field

With the example above, server will know how to respond to duck and ducks queries. The first argument of register_graphql_field is query name, second is the type class and the third is whether the query is for a single record or a collection.

Similarly for mutations:

  register_graphql_mutation_field :delete_duck, '::Mutations::Ducks::Delete'

where ::Mutations::Ducks::Delete is your delete mutation class inheriting from ::Mutations::DeleteMutation.

Adding subscribers

Requires Foreman 2.0 or higher, set requires_foreman '>= 2.0' in engine.rb

You can consume events from Foreman core by registering subscribers. To define a Subscriber class called MySubscriber, see the following example:

module MyPlugin
  class MySubscriber < ::Foreman::BaseSubscriber
    def call(*args)
      # ...
    end
  end
end

It is recommended to store subscribers under the /app/subscribers/my_plugin/ directory. If you have your Subscriber class defined, register it in the plugin. Example of your engine.rb:

Foreman::Plugin.register :my_plugin do
  # other code here
  subscribe 'my_event.foreman', MyPlugin::MySubscriber
end

where my_event.foreman is the name of the event you want to subscribe to. You may also subscribe to multiple events at once by using a regular expression, e.g. to subscribe to all events whose name ends with .foreman use:

  subscribe /.foreman$/, MyPlugin::MySubscriber

Example events emitted by creating, updating or deleting of selected records (subclasses of ApplicationRecord which are defined via set_hook method):

  • subnet_created.event.foreman

  • subnet_updated.event.foreman

  • subnet_destroyed.event.foreman

Payload for records is the record model object itself under key object and context with additional logging context. Keep in mind that the model classes are not subject of stable API, they will change in the future. It’s recommended not to publish full object but to strip down exposed information to bare minimum (e.g. host name and ID).

Example events emitted by performing background jobs (subclasses of ApplicationJob):

  • template_render_job_performed.event.foreman

  • create_rss_notifications_performed.event.foreman

Payload for background jobs is the serialized active job hash (see ActiveJob#serialize method) named job and context with additional logging context. Arguments are available via "arguments" key and hash keys are converted to strings. An example for SomeJob.new(1, 2, "third option", {"a_string" ⇒ 1, :a_symbol ⇒ 1}).perform_now:

{
  "context"=>{"user_login"=>"secret_admin", "user_admin"=>true},
  "job_class"=>nil,
  "job_id"=>"fbbf03d3-43a3-4466-9582-16825dd56334",
  "provider_job_id"=>nil,
  "queue_name"=>"default",
  "priority"=>nil,
  "arguments"=>[1, 2, "third option", {"a_string"=>1, "a_symbol"=>1, "_aj_symbol_keys"=>["a_symbol"]}],
  "executions"=>1,
  "exception_executions"=>{},
  "locale"=>"en",
  "timezone"=>"UTC",
  "enqueued_at"=>"2021-01-12T10:07:22Z"
}

Example events emitted by Remote Execution plugin:

  • actions.remote_execution.run_host_job_succeeded

Foreman Webhooks plugin ships with an example "Remote Execution Host Job" template.

You can find all observable events by calling Foreman::EventSubscribers.all_observable_events in the Rails console.

Translating

Translations of plugins work largely in the same way as Foreman. The basic steps are:

  1. Code is updated and maintained with _("Example") calls to gettext where translated text is required.

  2. The strings are extracted regularly by the maintainer and the file locale/foreman_plugin.pot is committed to the repository.

  3. Transifex regularly downloads the POT file from the git repository, and translators update the translations on the website

  4. Before making a release of the plugin, the maintainer pulls the translations and merges the translations into the per-language PO files, and generates binary MO translation files - these are committed to git and shipped in the gem.

Extracting strings

Read the Translating guide and extract all strings in the codebase itself. Then in foreman folder enable plugin:

$ cat bundler.d/Gemfile.local.rb
gem 'foreman_plugin', :path => "../foreman_plugin/"

And extract strings for the plugin easily (again in the foreman app folder):

$ mkdir ../foreman_plugin/locale
$ mkdir ../foreman_plugin/locale/en
$ rake plugin:gettext[foreman_plugin]
$ rake plugin:po_to_json[foreman_plugin]

This should create locale/foreman_plugin.pot file. Edit the header correctly (take locale/foreman.pot as a template) and submit to Transifex.com if you want.

Re-run this step on a regular basis when strings are changed in the plugin and once they’re not likely to change again. Make sure to run it early enough before planning to release the plugin to allow translators time to update the translations. Commit any changes to the POT file to the git repository and push it - Transifex should be configured to pull updates daily.

Translating plugin description

The description of your plugin (as set in your .gemspec) is shown to users on the About page. To get this translated, create a locale/gemspec.rb file which the rake task will extract the text from and copy the description there, then re-run the extraction above. Ensure they stay in sync!

locale/gemspec.rb:

# Duplicates foreman_plugin.gemspec
_("My great plugin for Foreman adds missile control support")

foreman_plugin.gemspec:

# Keep locale/gemspec.rb in sync
s.description = "My great plugin for Foreman adds missile control support"

Pulling translations from Transifex

To find more info about our Transifex project visit Translating guide. Configuration is easy once a resource for the plugin is created. It must have both SLUG and RESOURCE NAME set to "foreman_plugin":

$ cat .tx/config
[main]
host = https://www.transifex.com

[foreman.foreman_plugin]
file_filter = locale/<lang>/foreman_plugin.edit.po
source_file = locale/foreman_plugin.pot
source_lang = en
type = PO

Use this Makefile to pull translations (you need the Transifex client installed). Always re-run these steps before releasing the plugin to get the latest updates:

  1. In the plugin dir, pull updates into the .edit.po plain text files: make -C locale tx-update

  2. In the Foreman dir, merge the updates into the PO files: rake plugin:gettext[foreman_plugin]

  3. In the Foreman dir, generate files with translations for use in frontend: rake plugin:po_to_json[foreman_plugin]

  4. In the plugin dir, rebuild the MO files: make -C locale mo-files

These files should be .gitignored:

locale/*/*.edit.po +
locale/*/*.po.time_stamp

These files must be committed to git:

app/assets/javascripts/locale/**/*.js
locale/foreman_plugin.pot +
locale/*/foreman_plugin.po +
locale/*/LC_MESSAGES/foreman_plugin.mo

Ensure that the whole locale/ directory is included in the gem via the gemspec file list. The .po and .mo files are important in development and production environments respectively, so must both be shipped in the gem.

Registering Translations

Requires Foreman 3.7 or higher, set requires_foreman '>= 3.7' in engine.rb

If your plugin has translations, you can register its gettext domain to have the translations processed properly. The domain keyword argument can be omitted, in which case the domain will be derived from plugin name.

Foreman::Plugin.register :sample_plugin do
  # other code here
  register_gettext domain: "sample_plugin"
end

Translating Template Kind

Requires Foreman 1.12 or higher, set requires_foreman '>= 1.12' in engine.rb

If your plugin constains a new TemplateKind, you are encouraged to make its name available for translation. Since the actual name of the TemplateKind stored in DB may not be user-friendly, you can specify something more convenient. Example of your engine.rb:

Foreman::Plugin.register :sample_plugin do
  # other code here
  template_labels "my_template_kind_name" => N_("My pretty template kind name")
end

This will make sure there will be "My pretty template kind" on Foreman core pages and it can be translated.

Testing

Foreman plugins are tested by adding the plugin to a normal Foreman checkout and then running the whole test suite. The plugin should extend the Foreman test rake task(s) to add its own, e.g.

A couple of generic core Foreman tests will also be run against the plugin - one to test for permissions on all routes (non-isolated engines), and another to test seed scripts.

Jenkins

Plugins can, and should, be tested on Jenkins! See Jenkins.

Support file for test setups

To allow the Foreman unit tests to run in the presence of your plugin, you may add a support test file that is loaded by Foreman before any tests are run. In order to do this, within your plugin, add the following file:

test/support/foreman_test_helper_additions.rb

Any code placed in this file will be run at the end of the Foreman test_helper but before any individual tests.

Skipping tests

Requires Foreman 1.7 or higher, set requires_foreman '>= 1.7' in engine.rb

Sometimes a plugin changes core behaviour deliberately and replaces it with its own. In this case, the plugin can disable tests shipped in core from running by specifying their names, and should add tests of its own covering the expected behaviour.

To disable tests, give the full class name of the test class (left hand side of the output, split on '.'), and an array of test names (the right hand side of the '.') to skip. The custom test runner in Foreman uses substring matches, so you can ignore the "test_???" section of the output, and just use the name of the test direct from the test file. For example:

  # Skip some tests
  # Takes a hash of arrays, split on the '.' in the test output. For example, if you have:
  #     "DomainTest.test_0010_should update hosts_count on domain_id change" failed!
  #     "HostTest::import host and facts.test_0004_should find a host by certname not fqdn when provided" failed!
  # then you would use this to skip them
  tests_to_skip ({
                  "DomainTest" => ["should update hosts_count on domain_id change"],
                  "HostTest::import host and facts" => ["should find a host by certname not fqdn when provided"]
                })

Testing for deprecations

Requires Foreman 1.15 or higher

Plugins may call APIs in either Rails or Foreman that become deprecated and are either replaced with something different or are removed within a couple of releases, so it’s important to keep on top of any warnings issued. This ensures that the plugin will continue working against nightly and the next major release.

Foreman runs tests with as_deprecation_tracker which can be configured to raise errors (causing test failures) when any deprecated code is called, alerting you to any new dependency being introduced on deprecated features by maintaining a whitelist for known deprecation issues. By working through the whitelist and replacing deprecated code, you can then ensure the plugin works for the next version of Rails and Foreman.

By default it’s configured to be off for all plugins, but create an empty config/as_deprecation_whitelist.yaml file inside the plugin root to enable it. When tests run, any deprecation warnings called from your plugin will now raise exceptions.

You can automatically generate a whitelist by running:

AS_DEPRECATION_WHITELIST=~/plugin_path AS_DEPRECATION_RECORD=yes rake
test:foreman_your_plugin

Rails deprecations will typically be removed in the next minor release (e.g. 5.0 to 5.1) and Foreman deprecations will normally be removed after two major releases (e.g. warning in 1.10, 1.11 and removal in 1.12).