|
[[how-to-create-a-plugin]]
|
|
= How to Create a Plugin
|
|
:toc: right
|
|
:toclevels: 5
|
|
|
|
The goal of this tutorial is to help users quickly and easily create
|
|
Foreman plugins. This is not an exhaustive tutorial of
|
|
http://rubyonrails.org/[Ruby on Rails] or an explanation of how
|
|
http://guides.rubyonrails.org/engines.html[rails engines] work.
|
|
|
|
[[naming-your-plugin]]
|
|
== 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]]
|
|
== 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:
|
|
|
|
[source, bash]
|
|
....
|
|
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:
|
|
|
|
[source, bash]
|
|
....
|
|
cd my_plugin; ./rename.rb my_plugin
|
|
....
|
|
|
|
[[installing-the-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. http://theforeman.org/contribute.html[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
|
|
[source, ruby]
|
|
----
|
|
gem 'my_plugin', :path => 'path_to/my_plugin'
|
|
----
|
|
Install the 'preface' bundle by running from foreman core directory:
|
|
|
|
[source, bash]
|
|
----
|
|
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
|
|
[source, ruby]
|
|
----
|
|
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
|
|
|
|
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:
|
|
[source, ruby]
|
|
----
|
|
gem 'my_plugin'
|
|
----
|
|
|
|
Then restart httpd to load it.
|
|
|
|
[[initial-edits]]
|
|
=== 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]]
|
|
== 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 http://semver.org[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 https://projects.theforeman.org/projects/foreman/wiki/List_of_Plugins[List_of_Plugins]
|
|
5. Add some tests and enable testing in
|
|
http://projects.theforeman.org/projects/foreman/wiki/Jenkins#Foreman-plugin-testing[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 https://github.com/theforeman/[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 https://projects.theforeman.org/projects/foreman/wiki/GitHub[GitHub].
|
|
8. Have an issue tracker on
|
|
http://projects.theforeman.org/projects[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]]
|
|
== 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 (http://semver.org/)[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]]
|
|
=== 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]]
|
|
=== 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
|
|
https://github.com/theforeman/foreman-packaging[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]]
|
|
== 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]]
|
|
=== 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
|
|
http://bundler.io/v1.3/gemfile.html[bundler.io]
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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-foremans-roles]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
----
|
|
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
|
|
|
|
[source, ruby]
|
|
----
|
|
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:
|
|
|
|
[source, ruby]
|
|
----
|
|
add_permissions_to_default_roles 'Manager' => [:first_permission, :second_permission], 'Viewer' => [:third_permission]
|
|
----
|
|
|
|
Or alternatively:
|
|
|
|
[source, ruby]
|
|
----
|
|
add_resource_permissions_to_default_roles ['MyPlugin::FirstResource', 'MyPlugin::SecondResource'], :except => [:skip_this_permission]
|
|
----
|
|
|
|
[[adding-roles]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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
|
|
|
|
[source, ruby]
|
|
----
|
|
# Example: delete the hosts menu item
|
|
delete_menu_item :top_menu, :hosts
|
|
----
|
|
Adding a divider:
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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
|
|
----
|
|
|
|
https://github.com/theforeman/foreman/blob/0a39d23f088ae42995910f4b6d9898e2e13f7a02/app/registries/menu/loader.rb[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]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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:
|
|
|
|
[source, bash]
|
|
....
|
|
app/views/dashboard/_<widget_name>.html.erb
|
|
....
|
|
|
|
[[adding-a-pagelet]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
==== Extending hosts table with pagelets
|
|
|
|
Hosts table on the index page can be extended using two predefined pagelets:
|
|
[source, ruby]
|
|
----
|
|
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 https://github.com/theforeman/foreman/blob/fd2d3f160ca7db0d56cac1e61ce6a474b5cf7def/config/initializers/foreman_register.rb#L8[core repository]
|
|
|
|
[[using-react-in-plugins]]
|
|
=== 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]]
|
|
==== 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:
|
|
|
|
[source, javascript]
|
|
----
|
|
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]]
|
|
===== 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]]
|
|
===== 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.
|
|
|
|
[source, javascript]
|
|
----
|
|
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:
|
|
|
|
[source, ERB]
|
|
....
|
|
<%= webpacked_plugins_js_for :foreman_plugin, :foreman_other_plugin %>
|
|
<%= react_component('MyComponent', :id => '5', :name => 'whatever') %>
|
|
....
|
|
|
|
[[adding-3rd-party-js-libraries]]
|
|
=== 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]]
|
|
=== 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]]
|
|
==== 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]]
|
|
==== How to register a facet
|
|
|
|
Facet registration is done via the initializers mechanism: add a new
|
|
initializer with the following code:
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
==== 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]]
|
|
==== #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]]
|
|
==== #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]]
|
|
==== #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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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*
|
|
|
|
[source, ruby]
|
|
----
|
|
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
|
|
[source, ruby]
|
|
----
|
|
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
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
==== #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]]
|
|
==== #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]]
|
|
==== #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]]
|
|
=== 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
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
----
|
|
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
|
|
https://github.com/theforeman/foreman_salt/blob/84bc9cb9d8c6cb9748c14e7634b8e1a062558a3d/app/controllers/foreman_salt/application_controller.rb[foreman_salt]
|
|
for an example.
|
|
|
|
For more information on controllers, see
|
|
http://guides.rubyonrails.org/action_controller_overview.html
|
|
|
|
[[extending-a-controller]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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-controllers-query]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :my_plugin do
|
|
add_controller_action_scope(HostsController, :index) { |base_scope| base_scope.includes(:my_table) }
|
|
end
|
|
----
|
|
[[adding-a-smart-proxy]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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]]
|
|
=== Authenticating a Smart Proxy
|
|
|
|
If you have controller actions that SSL-authenticated Smart Proxies
|
|
should be able to access, add this to your controller:
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
=== 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
|
|
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html[ActiveSupport::Concern].
|
|
Below is an example from from
|
|
https://github.com/isratrade/foreman_plugin_template/blob/master/app/models/foreman_plugin_template/host_extensions.rb[host_extensions.rb].
|
|
|
|
[source, ruby]
|
|
----
|
|
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:
|
|
|
|
[source, ruby]
|
|
----
|
|
module ForemanPluginTemplate
|
|
class Engine < ::Rails::Engine
|
|
|
|
config.to_prepare do
|
|
Host.send :include, ForemanPluginTemplate::HostExtensions
|
|
end
|
|
end
|
|
----
|
|
[[add-new-view]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
=== 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]]
|
|
=== Add new migration
|
|
|
|
[[prerequisites]]
|
|
==== 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
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
==== Generating a new migration file
|
|
|
|
As of Foreman 1.16 migration files could be generated by invoking
|
|
[source, bash]
|
|
....
|
|
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
|
|
http://guides.rubyonrails.org/active_record_migrations.html[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]]
|
|
==== 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]]
|
|
==== 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]]
|
|
=== 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:
|
|
https://github.com/theforeman/foreman_bootdisk/blob/master/db/seeds.d/50-bootdisk_templates.rb[50-bootdisk_templates.rb]
|
|
|
|
[[adding-new-model-classes]]
|
|
=== 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):
|
|
|
|
[source, ruby]
|
|
----
|
|
class MyModel < ApplicationRecord
|
|
...
|
|
end
|
|
----
|
|
[[add-new-database-seeds]]
|
|
=== 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]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :sample_plugin do
|
|
parameter_filter Host::Managed, :sample_attribute
|
|
end
|
|
----
|
|
More information is available on the https://projects.theforeman.org/projects/foreman/wiki/Strong_parameters[Strong parameters] page.
|
|
|
|
[[modify-existing-foreman-view-using-deface]]
|
|
=== 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
|
|
https://github.com/spree/deface/blob/master/README.markdown[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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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 https://github.com/spree/deface/blob/master/README.markdown[deface
|
|
manual] shows further examples and an alternative way of modifying
|
|
existing views, i.e., using .deface files.
|
|
|
|
[[extend-safemode-access]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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.
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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]]
|
|
=== 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.
|
|
|
|
[source, bash]
|
|
----
|
|
$ 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):
|
|
|
|
[source, bash]
|
|
----
|
|
$ rake plugin:assets:precompile[foreman_plugin]
|
|
----
|
|
[[logging]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
....
|
|
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]]
|
|
==== 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:
|
|
|
|
[source, ruby]
|
|
....
|
|
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:
|
|
|
|
[source, bash]
|
|
....
|
|
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:
|
|
|
|
[source, yaml]
|
|
....
|
|
: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]]
|
|
==== 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]]
|
|
=== Extending host model
|
|
|
|
|
|
[[add-custom-host-status]]
|
|
==== 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
|
|
|
|
[source, ruby]
|
|
----
|
|
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 https://github.com/theforeman/foreman/blob/ceb276fbe96b97770f5d292b1eadca0205a34a0a/app/models/host_status/global.rb#L3[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 https://github.com/theforeman/foreman/blob/ceb276fbe96b97770f5d292b1eadca0205a34a0a/app/models/host_status/status.rb#L2[`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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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
|
|
|
|
[source, ruby]
|
|
----
|
|
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
|
|
|
|
[source, ruby]
|
|
----
|
|
class HostStatus::RandomStatus
|
|
----
|
|
avoid definition like this
|
|
|
|
[source, ruby]
|
|
----
|
|
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.
|
|
|
|
[source, ruby]
|
|
----
|
|
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.
|
|
|
|
[source, ruby]
|
|
----
|
|
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.
|
|
|
|
[source, ruby]
|
|
----
|
|
my_host.refresh_statuses([HostStatus.find_status_by_humanized_name("statusname")])
|
|
# or:
|
|
my_host.refresh_statuses([MyHostStatus])
|
|
----
|
|
[[selecting-properties-to-clone]]
|
|
==== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
==== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
=== 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.
|
|
|
|
[[extending-host-ui-overview-fields]]
|
|
==== 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`):
|
|
[source, ruby]
|
|
----
|
|
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`:
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :my_plugin do
|
|
describe_host do
|
|
overview_fields_provider :my_plugin_host_overview_fields
|
|
end
|
|
end
|
|
----
|
|
|
|
[[extending-host-ui-overview-buttons]]
|
|
==== 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`):
|
|
[source, ruby]
|
|
----
|
|
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`:
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :my_plugin do
|
|
describe_host do
|
|
overview_buttons_provider :my_plugin_host_overview_buttons
|
|
end
|
|
end
|
|
----
|
|
|
|
[[extending-host-ui-title-actions]]
|
|
==== 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`):
|
|
[source, ruby]
|
|
----
|
|
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`:
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :my_plugin do
|
|
describe_host do
|
|
title_actions_provider :my_plugin_host_title_actions
|
|
end
|
|
end
|
|
----
|
|
|
|
[[extending-host-ui-multiple-actions]]
|
|
==== 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`):
|
|
[source, ruby]
|
|
----
|
|
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`:
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :my_plugin do
|
|
describe_host do
|
|
multiple_actions_provider :my_plugin_multiple_actions
|
|
end
|
|
end
|
|
----
|
|
|
|
|
|
[[extending-hostgroup-ui]]
|
|
=== Extending hostgroup UI
|
|
|
|
[[extending-hostgroup-ui-index-actions]]
|
|
==== 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`):
|
|
[source, ruby]
|
|
----
|
|
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`:
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :my_plugin do
|
|
describe_hostgroup do
|
|
hostgroup_actions_provider :my_plugin_hostgroups_actions
|
|
end
|
|
end
|
|
----
|
|
|
|
|
|
[[settings]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
....
|
|
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.
|
|
|
|
[source, ruby]
|
|
```
|
|
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.
|
|
|
|
[source, ruby]
|
|
....
|
|
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
|
|
|
|
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.
|
|
|
|
[source, yaml]
|
|
----
|
|
: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]]
|
|
=== 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
|
|
|
|
[source, ruby]
|
|
....
|
|
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]]
|
|
=== 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]]
|
|
==== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
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]]
|
|
==== Registering medium provider
|
|
|
|
Once medium provider is created we will need to register it in plugin
|
|
declaration:
|
|
|
|
[source, ruby]
|
|
----
|
|
Foreman::Plugin.register :my_plugin do
|
|
medium_providers_registry.register(MyPlugin::ManagedContentMediumProvider)
|
|
end
|
|
----
|
|
[[compute-resources]]
|
|
=== 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`:
|
|
|
|
[source, ruby]
|
|
....
|
|
module ForemanExample
|
|
class MyService < ComputeResource
|
|
# ...
|
|
end
|
|
end
|
|
....
|
|
|
|
and register it:
|
|
|
|
[source, ruby]
|
|
....
|
|
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]]
|
|
==== Fog provider
|
|
|
|
This requires support in http://fog.io/[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
|
|
https://github.com/fog/fog/wiki/Create-New-Provider-from-Scratch[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]]
|
|
==== 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]]
|
|
==== 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]]
|
|
=== 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
|
|
|
|
[source, ruby]
|
|
....
|
|
date_time_absolute(Time.zone.now)
|
|
date_time_absolute(@user.last_login_at, :long)
|
|
date_time_relative(@host.last_report_at)
|
|
....
|
|
|
|
[[extending-rabl-templates]]
|
|
=== 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).
|
|
|
|
[source, ruby]
|
|
....
|
|
# 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
|
|
....
|
|
|
|
[source, ruby]
|
|
....
|
|
# app/views/api/v2/hosts/expiration.json.rabl
|
|
attribute :expired_on
|
|
....
|
|
|
|
[[making-use-of-reports-origin]]
|
|
=== 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]]
|
|
==== 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
|
|
https://github.com/theforeman/foreman_ansible/blob/2d2f23b5400d7300c0f42a30dbbbcdd7d3089293/lib/foreman_ansible/register.rb#L56[foreman-ansible]
|
|
`register.rb`:
|
|
|
|
[source, ruby]
|
|
....
|
|
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]]
|
|
==== 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`.
|
|
|
|
https://github.com/theforeman/foreman_ansible/blob/2d2f23b5400d7300c0f42a30dbbbcdd7d3089293/lib/foreman_ansible/register.rb#L56[foreman-ansible]
|
|
for example provides one:
|
|
|
|
[source, ruby]
|
|
....
|
|
register_report_scanner ForemanAnsible::AnsibleReportScanner
|
|
....
|
|
|
|
https://github.com/theforeman/foreman_ansible/blob/2d2f23b5400d7300c0f42a30dbbbcdd7d3089293/app/services/foreman_ansible/ansible_report_scanner.rb[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]]
|
|
==== 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
|
|
https://github.com/theforeman/foreman_ansible/blob/2d2f23b5400d7300c0f42a30dbbbcdd7d3089293/app/helpers/foreman_ansible/ansible_reports_helper.rb#L28[foreman_ansible]
|
|
plugin.
|
|
|
|
[[origin-based-settings]]
|
|
==== 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]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
....
|
|
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.
|
|
|
|
[source, ruby]
|
|
....
|
|
extend_graphql_type type: Types::SmartProxy, with_module: ForemanOpenscap::SmartProxyTypeExtensions
|
|
....
|
|
|
|
[[adding-graphql-types]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
....
|
|
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:
|
|
|
|
[source, ruby]
|
|
....
|
|
register_graphql_mutation_field :delete_duck, '::Mutations::Ducks::Delete'
|
|
....
|
|
|
|
where `::Mutations::Ducks::Delete` is your delete mutation class inheriting from `::Mutations::DeleteMutation`.
|
|
|
|
[[adding-subscribers]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
....
|
|
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`:
|
|
|
|
[source, ruby]
|
|
....
|
|
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:
|
|
|
|
[source, ruby]
|
|
....
|
|
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]]
|
|
== 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. https://www.transifex.com/projects/p/foreman/[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]]
|
|
=== Extracting strings
|
|
|
|
Read the https://projects.theforeman.org/projects/foreman/wiki/Translating[Translating]
|
|
guide and extract all strings in the codebase itself. Then in *foreman* folder
|
|
enable plugin:
|
|
|
|
[source, bash]
|
|
----
|
|
$ 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):
|
|
|
|
[source, bash]
|
|
----
|
|
$ 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]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
....
|
|
# Duplicates foreman_plugin.gemspec
|
|
_("My great plugin for Foreman adds missile control support")
|
|
....
|
|
|
|
foreman_plugin.gemspec:
|
|
|
|
[source, ruby]
|
|
....
|
|
# Keep locale/gemspec.rb in sync
|
|
s.description = "My great plugin for Foreman adds missile control support"
|
|
....
|
|
|
|
[[pulling-translations-from-transifex]]
|
|
=== Pulling translations from Transifex
|
|
|
|
To find more info about our Transifex project visit https://projects.theforeman.org/projects/foreman/wiki/Translating[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":
|
|
|
|
[source, bash]
|
|
....
|
|
$ 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
|
|
https://github.com/theforeman/foreman_plugin_template/blob/master/locale/Makefile[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]]
|
|
=== 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.
|
|
|
|
[source, ruby]
|
|
....
|
|
Foreman::Plugin.register :sample_plugin do
|
|
# other code here
|
|
register_gettext domain: "sample_plugin"
|
|
end
|
|
....
|
|
|
|
[[translating-template-kind]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
....
|
|
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]]
|
|
== 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.
|
|
|
|
https://github.com/theforeman/foreman_plugin_template/blob/master/lib/tasks/foreman_plugin_template_tasks.rake
|
|
|
|
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]]
|
|
=== Jenkins
|
|
|
|
Plugins can, and should, be tested on Jenkins! See
|
|
https://projects.theforeman.org/projects/foreman/wiki/Jenkins#Foreman-plugin-testing[Jenkins].
|
|
|
|
[[support-file-for-test-setups]]
|
|
==== 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]]
|
|
=== 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:
|
|
|
|
[source, ruby]
|
|
----
|
|
# 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]]
|
|
=== 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
|
|
https://rubygems.org/gems/as_deprecation_tracker[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:
|
|
|
|
|
|
[source, bash]
|
|
----
|
|
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).
|