foreman_ansible 1.0 → 1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of foreman_ansible might be problematic. Click here for more details.

Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +6 -27
  3. data/app/controllers/ansible_roles_controller.rb +55 -0
  4. data/app/controllers/api/v2/ansible_roles_controller.rb +52 -0
  5. data/app/controllers/foreman_ansible/concerns/hosts_controller_extensions.rb +11 -10
  6. data/app/helpers/foreman_ansible/ansible_plugin_helper.rb +8 -0
  7. data/app/helpers/foreman_ansible/ansible_reports_helper.rb +6 -4
  8. data/app/helpers/foreman_ansible/ansible_roles_helper.rb +17 -3
  9. data/app/helpers/foreman_ansible/hosts_helper_extensions.rb +4 -3
  10. data/app/lib/actions/foreman_ansible/play_host_roles.rb +56 -0
  11. data/app/lib/actions/foreman_ansible/play_hosts_roles.rb +26 -0
  12. data/app/lib/proxy_api/ansible.rb +28 -0
  13. data/app/models/ansible_role.rb +8 -0
  14. data/app/models/concerns/foreman_ansible/has_many_ansible_roles.rb +13 -0
  15. data/app/models/concerns/foreman_ansible/host_managed_extensions.rb +5 -1
  16. data/app/models/concerns/foreman_ansible/hostgroup_extensions.rb +19 -0
  17. data/app/models/host_ansible_role.rb +0 -2
  18. data/app/models/hostgroup_ansible_role.rb +8 -0
  19. data/app/overrides/ansible_roles_tab.rb +2 -2
  20. data/app/overrides/hostgroup_ansible_roles_tab.rb +14 -0
  21. data/app/services/foreman_ansible/api_roles_importer.rb +16 -0
  22. data/app/services/foreman_ansible/fact_importer.rb +2 -1
  23. data/app/services/foreman_ansible/inventory_creator.rb +37 -5
  24. data/app/services/foreman_ansible/playbook_creator.rb +3 -4
  25. data/app/services/foreman_ansible/proxy_selector.rb +19 -0
  26. data/app/services/foreman_ansible/roles_importer.rb +42 -15
  27. data/app/services/foreman_ansible/ui_roles_importer.rb +26 -0
  28. data/app/views/ansible_roles/import.html.erb +51 -0
  29. data/app/views/ansible_roles/index.html.erb +30 -0
  30. data/app/views/ansible_roles/welcome.html.erb +14 -0
  31. data/app/views/api/v2/ansible_roles/import.json.rabl +3 -0
  32. data/app/views/api/v2/ansible_roles/index.json.rabl +3 -0
  33. data/app/views/api/v2/ansible_roles/obsolete.json.rabl +3 -0
  34. data/app/views/api/v2/ansible_roles/show.json.rabl +3 -0
  35. data/app/views/foreman_ansible/ansible_roles/_select_tab_content.html.erb +6 -0
  36. data/app/views/foreman_ansible/{hosts/_tab_title.html.erb → ansible_roles/_select_tab_title.html.erb} +0 -0
  37. data/config/routes.rb +25 -2
  38. data/db/migrate/20160705082036_create_ansible_role.rb +2 -1
  39. data/db/migrate/20160706074540_create_join_table_hosts_ansible_roles.rb +1 -0
  40. data/db/migrate/20160707195442_create_host_ansible_roles.rb +1 -0
  41. data/db/migrate/20160729094457_add_columns_to_ansible_role.rb +12 -0
  42. data/db/migrate/20160802153302_create_join_table_hostgroup_ansible_roles.rb +10 -0
  43. data/db/migrate/20160805094233_add_primary_key_hostgroup_ansible_roles.rb +6 -0
  44. data/db/seeds.d/{62-ansible_proxy_feature.rb → 62_ansible_proxy_feature.rb} +0 -0
  45. data/lib/foreman_ansible.rb +1 -0
  46. data/lib/foreman_ansible/engine.rb +54 -4
  47. data/lib/foreman_ansible/version.rb +1 -1
  48. data/locale/en/foreman_ansible.po +139 -2
  49. data/locale/foreman_ansible.pot +200 -8
  50. data/test/factories/ansible_proxy.rb +7 -0
  51. data/test/factories/ansible_roles.rb +2 -2
  52. data/test/fixtures/ansible_permissions.yml +11 -0
  53. data/test/functional/ansible_roles_controller_test.rb +16 -0
  54. data/test/functional/api/v2/ansible_roles_controller_test.rb +24 -0
  55. data/test/functional/hosts_controller_test.rb +65 -24
  56. data/test/support/fixture_support.rb +25 -0
  57. data/test/support/foreman_tasks/task.rb +47 -0
  58. data/test/support/foreman_test_helper_additions.rb +22 -0
  59. data/test/test_plugin_helper.rb +5 -2
  60. data/test/unit/ansible_role_test.rb +6 -2
  61. data/test/unit/concerns/host_managed_extensions_test.rb +37 -0
  62. data/test/unit/concerns/hostgroup_extensions_test.rb +36 -0
  63. data/test/unit/fact_importer_test.rb +11 -8
  64. data/test/unit/fact_parser_test.rb +2 -0
  65. data/test/unit/fact_sparser_test.rb +1 -0
  66. data/test/unit/helpers/foreman_ansible/ansible_reports_helper_test.rb +1 -0
  67. data/test/unit/host_ansible_role_test.rb +10 -0
  68. data/test/unit/hostgroup_ansible_role_test.rb +19 -0
  69. data/test/unit/lib/foreman_ansible_core/playbook_runner_test.rb +28 -0
  70. data/test/unit/lib/proxy_api/ansible_test.rb +23 -0
  71. data/test/unit/services/api_roles_importer_test.rb +28 -0
  72. data/test/unit/services/proxy_selector_test.rb +28 -0
  73. data/test/unit/services/roles_importer_test.rb +17 -0
  74. data/test/unit/services/ui_roles_importer_test.rb +30 -0
  75. metadata +108 -14
  76. data/app/jobs/foreman_ansible/run_playbook_job.rb +0 -28
  77. data/app/services/foreman_ansible/role_player.rb +0 -26
  78. data/app/views/foreman_ansible/hosts/_tab_content.html.erb +0 -4
  79. data/lib/tasks/foreman_ansible_tasks.rake +0 -40
@@ -1,8 +1,6 @@
1
1
  # Join model that hosts the connection between hosts and ansible_roles
2
2
  class HostAnsibleRole < ActiveRecord::Base
3
3
  audited :associated_with => :host, :allow_mass_assignment => true
4
- attr_accessible :host_id, :host, :ansible_role_id, :ansible_role
5
-
6
4
  belongs_to_host
7
5
  belongs_to :ansible_role
8
6
 
@@ -0,0 +1,8 @@
1
+ # Join model that hosts the connection between hostgroups and ansible_roles
2
+ class HostgroupAnsibleRole < ActiveRecord::Base
3
+ belongs_to :hostgroup
4
+ belongs_to :ansible_role
5
+
6
+ validates :ansible_role_id, :presence => true,
7
+ :uniqueness => { :scope => :hostgroup_id }
8
+ end
@@ -3,12 +3,12 @@ Deface::Override.new(
3
3
  :virtual_path => 'hosts/_form',
4
4
  :name => 'ansible_roles_tab',
5
5
  :insert_after => 'li.active',
6
- :partial => 'foreman_ansible/hosts/tab_title'
6
+ :partial => 'foreman_ansible/ansible_roles/select_tab_title'
7
7
  )
8
8
 
9
9
  Deface::Override.new(
10
10
  :virtual_path => 'hosts/_form',
11
11
  :name => 'ansible_roles_tab_content',
12
12
  :insert_after => 'div.tab-pane.active',
13
- :partial => 'foreman_ansible/hosts/tab_content'
13
+ :partial => 'foreman_ansible/ansible_roles/select_tab_content'
14
14
  )
@@ -0,0 +1,14 @@
1
+ # Displays Ansible roles tab on Hostgroup form
2
+ Deface::Override.new(
3
+ :virtual_path => 'hostgroups/_form',
4
+ :name => 'hostgroup_ansible_roles_tab',
5
+ :insert_after => 'li.active',
6
+ :partial => 'foreman_ansible/ansible_roles/select_tab_title'
7
+ )
8
+
9
+ Deface::Override.new(
10
+ :virtual_path => 'hostgroups/_form',
11
+ :name => 'hostgroup_ansible_roles_tab_content',
12
+ :insert_after => 'div.tab-pane.active',
13
+ :partial => 'foreman_ansible/ansible_roles/select_tab_content'
14
+ )
@@ -0,0 +1,16 @@
1
+ module ForemanAnsible
2
+ # imports Ansible roles through API
3
+ class ApiRolesImporter < RolesImporter
4
+ def import!
5
+ new_roles = import_role_names[:new]
6
+ new_roles.map(&:save)
7
+ new_roles
8
+ end
9
+
10
+ def obsolete!
11
+ obsolete_roles = import_role_names[:obsolete]
12
+ obsolete_roles.map(&:destroy)
13
+ obsolete_roles
14
+ end
15
+ end
16
+ end
@@ -20,7 +20,8 @@ module ForemanAnsible
20
20
  @counters[:added] = 0
21
21
  add_missing_facts(FactSparser.unsparse(@original_facts))
22
22
  logger.debug(
23
- "Merging facts for '#{host}': added #{@counters[:added]} facts")
23
+ "Merging facts for '#{host}': added #{@counters[:added]} facts"
24
+ )
24
25
  end
25
26
 
26
27
  def add_missing_facts(imported_facts, parent = nil, prefix = '')
@@ -8,11 +8,43 @@ module ForemanAnsible
8
8
  @hosts = hosts
9
9
  end
10
10
 
11
- def tempfile
12
- tempfile = Tempfile.new("foreman-#{SecureRandom.uuid}-inventory")
13
- tempfile.write(hosts.map(&:fqdn).join('\n'))
14
- tempfile.close
15
- tempfile
11
+ # It returns a hash in a format that Ansible understands.
12
+ # See http://docs.ansible.com/ansible/developing_inventory.html
13
+ # for more details.
14
+ # For now, we don't group the hosts based on different paramters
15
+ # (use https://github.com/theforeman/foreman_ansible_inventory for
16
+ # more advanced cases). Therefore we have only the 'all' group
17
+ # with all hosts.
18
+ def to_hash
19
+ { 'all' => { 'hosts' => hosts.map(&:fqdn) },
20
+ '_meta' => { 'hostvars' => host_vars } }
21
+ end
22
+
23
+ def host_vars
24
+ hosts.reduce({}) do |hash, host|
25
+ hash.update(host.fqdn =>
26
+ { 'foreman' => host_attributes(host),
27
+ 'foreman_params' => host_params(host),
28
+ 'foreman_ansible_roles' => host_roles(host) })
29
+ end
30
+ end
31
+
32
+ def host_roles(host)
33
+ host.all_ansible_roles.map(&:name)
34
+ end
35
+
36
+ def host_attributes(host)
37
+ render_rabl(host, 'api/v2/hosts/main')
38
+ end
39
+
40
+ def host_params(host)
41
+ host.host_params
42
+ end
43
+
44
+ private
45
+
46
+ def render_rabl(host, template)
47
+ Rabl.render(host, template, :format => 'hash')
16
48
  end
17
49
  end
18
50
  end
@@ -1,15 +1,14 @@
1
1
  module ForemanAnsible
2
2
  # Service to generate a playbook given roles and a list of hosts
3
3
  class PlaybookCreator
4
- attr_reader :fqdn, :role_names
4
+ attr_reader :role_names
5
5
 
6
- def initialize(fqdn, role_names)
7
- @fqdn = fqdn
6
+ def initialize(role_names)
8
7
  @role_names = role_names
9
8
  end
10
9
 
11
10
  def roles_playbook
12
- playbook = ['hosts' => fqdn, 'roles' => role_names]
11
+ playbook = ['hosts' => 'all', 'roles' => role_names]
13
12
  playbook.to_yaml
14
13
  end
15
14
 
@@ -0,0 +1,19 @@
1
+ module ForemanAnsible
2
+ # Contains proxy selection rules for a host playbook run
3
+ class ProxySelector < ::ForemanTasks::ProxySelector
4
+ def available_proxies(host)
5
+ proxies = {}
6
+ proxies[:fallback] = host.smart_proxies.with_features('Ansible')
7
+ proxies[:global] = proxy_scope(host).authorized.with_features('Ansible')
8
+ proxies
9
+ end
10
+
11
+ private
12
+
13
+ def proxy_scope(host)
14
+ return ::SmartProxy unless Taxonomy.enabled_taxonomies.any?
15
+ ::SmartProxy.with_taxonomy_scope_override(host.location,
16
+ host.organization)
17
+ end
18
+ end
19
+ end
@@ -1,24 +1,51 @@
1
1
  module ForemanAnsible
2
- # Service to import roles from the filesystem.
3
- # Should be extracted to a gem that both the proxy and Foreman can use.
2
+ # imports roles from smart proxy
4
3
  class RolesImporter
5
- class << self
6
- def import!
7
- list_roles_in_fs.each do |role_name|
8
- role = AnsibleRole.new(:name => role_name)
9
- next if role.save
10
- Rails.logger.debug("Failed to save role #{role_name}: "\
11
- "#{role.errors.full_messages.join(', ')}")
12
- end
4
+ attr_reader :ansible_proxy
5
+
6
+ def initialize(proxy = nil)
7
+ @ansible_proxy = proxy
8
+ end
9
+
10
+ def import_role_names
11
+ return import_roles remote_roles if ansible_proxy
12
+ import_roles local_roles
13
+ end
14
+
15
+ def import_roles(roles)
16
+ imported = roles.map do |role_name|
17
+ ::AnsibleRole.find_or_initialize_by(:name => role_name)
13
18
  end
19
+ detect_changes imported
20
+ end
21
+
22
+ def detect_changes(imported)
23
+ changes = {}.with_indifferent_access
24
+ old, changes[:new] = imported.partition { |role| role.id.present? }
25
+ changes[:obsolete] = ::AnsibleRole.where.not(:id => old.map(&:id))
26
+ changes
27
+ end
28
+
29
+ private
14
30
 
15
- private
31
+ def find_proxy_api
32
+ raise ::Foreman::Exception.new(N_('Proxy not found')) unless ansible_proxy
33
+ @proxy_api = ::ProxyAPI::Ansible.new(:url => ansible_proxy.url)
34
+ end
16
35
 
17
- def list_roles_in_fs
18
- Dir.glob('/etc/ansible/roles/*').map do |path|
19
- path.split('/').last
20
- end
36
+ def proxy_api
37
+ return @proxy_api if @proxy_api
38
+ find_proxy_api
39
+ end
40
+
41
+ def local_roles
42
+ Dir.glob('/etc/ansible/roles/*').map do |path|
43
+ path.split('/').last
21
44
  end
22
45
  end
46
+
47
+ def remote_roles
48
+ proxy_api.roles
49
+ end
23
50
  end
24
51
  end
@@ -0,0 +1,26 @@
1
+ module ForemanAnsible
2
+ # imports ansible roles through UI
3
+ class UiRolesImporter < RolesImporter
4
+ def import!
5
+ import_role_names
6
+ end
7
+
8
+ def finish_import(changes)
9
+ return unless changes.present?
10
+ create_new_roles changes['new'] if changes['new']
11
+ delete_old_roles changes['obsolete'] if changes['obsolete']
12
+ end
13
+
14
+ def create_new_roles(changes)
15
+ changes.values.each do |new_role|
16
+ ::AnsibleRole.create(JSON.parse(new_role))
17
+ end
18
+ end
19
+
20
+ def delete_old_roles(changes)
21
+ changes.values.each do |old_role|
22
+ ::AnsibleRole.find(JSON.parse(old_role)['id']).destroy
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ <% title _("Changed Ansible roles") %>
2
+ <%= form_tag confirm_import_ansible_roles_path do %>
3
+ <h4><%= _("Select the changes you want to realize in Foreman") %></h4>
4
+ <h6>
5
+ <%= _("Toggle") %>:
6
+ <%= link_to_function(icon_text("check", _("New")),
7
+ "toggleCheckboxesBySelector('.role_select_boxes_new')",
8
+ :title => _("Check/Uncheck new")) %> |
9
+ <%= link_to_function(icon_text("check", _("Obsolete")),
10
+ "toggleCheckboxesBySelector('.role_select_boxes_obsolete')",
11
+ :title => _("Check/Uncheck obsolete")) %>
12
+ </h6>
13
+ <table class="<%= table_css_classes %>">
14
+ <thead>
15
+ <tr>
16
+ <th class="ca">
17
+ <%= link_to_function(icon_text("check"),
18
+ "toggleCheckboxesBySelector('.role_select_boxes')",
19
+ :title => _("Check/Uncheck all")) %>
20
+ </th>
21
+ <th><%= _("Name") %></th>
22
+ <th class="col-md-2"><%= _("Hosts count") %></th>
23
+ <th class="col-md-2"><%= _("Hostgroups count") %></th>
24
+ <th><%= _("Operation") %></th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <% changed.each do |kind, roles| %>
29
+ <% roles.each do |role| %>
30
+ <tr>
31
+ <td>
32
+ <%= check_box_tag "changed[#{kind}][#{role}]", role.to_json, false, :class => "role_select_boxes role_select_boxes_#{kind} role_select_boxes_role_#{role}" %>
33
+ </td>
34
+ <td>
35
+ <%= link_to_function("#{role}", "toggleCheckboxesBySelector('.role_select_boxes_role_#{role}')", :title => _("Check/Uncheck all %s changes") % role) %>
36
+ </td>
37
+ <td><%= role.hosts.count %></td>
38
+ <td><%= role.hostgroups.count %></td>
39
+ <td>
40
+ <%= { "new" => _("Add"), "obsolete" => _("Remove") }[kind] %>
41
+ </td>
42
+ </tr>
43
+ <% end %>
44
+ <% end %>
45
+ </tbody>
46
+ </table>
47
+ <div>
48
+ <%= link_to _("Cancel"), ansible_roles_path, :class => "btn btn-default" %>
49
+ <%= submit_tag _("Update"), :class => "btn btn-primary" %>
50
+ </div>
51
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <% title _("Ansible Roles") %>
2
+
3
+ <% title_actions ansible_proxy_import(hash_for_import_ansible_roles_path) %>
4
+
5
+ <table class="<%= table_css_classes 'table-fixed' %>">
6
+ <thead>
7
+ <tr>
8
+ <th class="col-md-3"><%= sort :name, :as => s_("Role|Name") %></th>
9
+ <th class="col-md-3"><%= _("Imported at") %></th>
10
+ <th class="col-md-1"><%= _("Actions") %></th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% @ansible_roles.each do |role| %>
15
+ <tr>
16
+ <td class="ellipsis"><%= role.name %></td>
17
+ <td class="ellipsis"><%= import_time role %></td>
18
+ <td>
19
+ <%
20
+ links = [display_delete_if_authorized(hash_for_ansible_role_path(:id => role).merge(:auth_object => role, :authorizer => authorizer),
21
+ :data => { :confirm => _("Delete %s?") % role.name }, :action => :delete)]
22
+ %>
23
+ <%= action_buttons(*links) %>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+
30
+ <%= will_paginate_with_info @ansible_roles %>
@@ -0,0 +1,14 @@
1
+ <% content_for(:title, _("Ansible Roles")) %>
2
+ <div class="blank-slate-pf">
3
+ <div class="blank-slate-pf-icon">
4
+ <%= icon_text("play", "", :kind => "fa") %>
5
+ </div>
6
+ <h1><%= _('Ansible Roles') %></h1>
7
+ <p><%= _('No ansible roles were found in Foreman. If you want to assign roles to your hosts,
8
+ you have to import them first.').html_safe %>
9
+ </p>
10
+ <p><%= link_to(_('Learn more about this in the documentation.'), ansible_doc_url) %></p>
11
+ <div class="blank-slate-pf-secondary-action">
12
+ <%= ansible_proxy_import(hash_for_import_ansible_roles_path) %>
13
+ </div>
14
+ </div>
@@ -0,0 +1,3 @@
1
+ collection @imported => :imported
2
+
3
+ extends 'api/v2/ansible_roles/show'
@@ -0,0 +1,3 @@
1
+ collection @ansible_roles
2
+
3
+ extends 'api/v2/ansible_roles/show'
@@ -0,0 +1,3 @@
1
+ collection @obsoleted => :obsoleted
2
+
3
+ extends 'api/v2/ansible_roles/show'
@@ -0,0 +1,3 @@
1
+ object @ansible_role
2
+
3
+ attributes :name, :created_at, :updated_at
@@ -0,0 +1,6 @@
1
+ <div class='tab-pane' id='ansible_roles'>
2
+ <%= multiple_selects(f, :ansible_roles, AnsibleRole, f.object.all_ansible_roles.map(&:id),
3
+ {:disabled => f.object.inherited_ansible_roles.map(&:id),
4
+ :label => _('Available roles')},
5
+ { 'data-inheriteds' => f.object.inherited_ansible_roles.map(&:id).to_json }) %>
6
+ </div>
data/config/routes.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  Rails.application.routes.draw do
2
- scope :ansible, :path => '/ansible', :as => 'ansible' do
3
- constraints(:id => /[^\/]+/) do
2
+ scope '/ansible' do
3
+ constraints(:id => %r{[^\/]+}) do
4
4
  resources :hosts, :only => [] do
5
5
  member do
6
6
  get :play_roles
@@ -10,5 +10,28 @@ Rails.application.routes.draw do
10
10
  end
11
11
  end
12
12
  end
13
+
14
+ resources :ansible_roles, :only => [:index, :destroy] do
15
+ collection do
16
+ get :import
17
+ post :confirm_import
18
+ end
19
+ end
20
+
21
+ namespace :api do
22
+ scope '(:apiv)',
23
+ :module => :v2,
24
+ :defaults => { :apiv => 'v2' },
25
+ :apiv => /v1|v2/,
26
+ :constraints => ApiConstraints.new(:version => 2) do
27
+
28
+ resources :ansible_roles, :only => [:show, :index, :destroy] do
29
+ collection do
30
+ put :import
31
+ put :obsolete
32
+ end
33
+ end
34
+ end
35
+ end
13
36
  end
14
37
  end
@@ -1,7 +1,8 @@
1
+ # Creates simple model of Ansible Role
1
2
  class CreateAnsibleRole < ActiveRecord::Migration
2
3
  def change
3
4
  create_table :ansible_roles do |t|
4
- t.string :name, :null => false, :limit => 255
5
+ t.string :name, :null => false, :limit => 255, :unique => true
5
6
  end
6
7
  end
7
8
  end