foreman_acd 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_acd/ansible_playbooks_controller.rb +122 -0
  3. data/app/controllers/foreman_acd/api/v2/ansible_playbooks_controller.rb +54 -0
  4. data/app/controllers/foreman_acd/api/v2/app_instances_controller.rb +54 -0
  5. data/app/controllers/foreman_acd/api/v2/app_playbooks_controller.rb +0 -0
  6. data/app/controllers/foreman_acd/app_definitions_controller.rb +7 -4
  7. data/app/controllers/foreman_acd/app_instances_controller.rb +33 -126
  8. data/app/controllers/foreman_acd/concerns/ansible_playbook_parameters.rb +23 -0
  9. data/app/controllers/foreman_acd/concerns/app_definition_parameters.rb +1 -1
  10. data/app/controllers/foreman_acd/concerns/app_instance_parameters.rb +1 -1
  11. data/app/controllers/ui_acd_controller.rb +11 -3
  12. data/app/models/foreman_acd/ansible_playbook.rb +50 -0
  13. data/app/models/foreman_acd/app_definition.rb +2 -0
  14. data/app/models/foreman_acd/app_instance.rb +7 -0
  15. data/app/services/foreman_acd/app_configurator.rb +70 -0
  16. data/app/services/foreman_acd/app_deployer.rb +143 -0
  17. data/app/services/foreman_acd/inventory_creator.rb +67 -0
  18. data/app/views/foreman_acd/ansible_playbooks/_form.html.erb +21 -0
  19. data/app/views/foreman_acd/ansible_playbooks/edit.html.erb +3 -0
  20. data/app/views/foreman_acd/ansible_playbooks/index.html.erb +30 -0
  21. data/app/views/foreman_acd/ansible_playbooks/new.html.erb +3 -0
  22. data/app/views/foreman_acd/api/v2/ansible_playbooks/base.json.rabl +3 -0
  23. data/app/views/foreman_acd/api/v2/ansible_playbooks/index.json.rabl +3 -0
  24. data/app/views/foreman_acd/api/v2/ansible_playbooks/show.json.rabl +3 -0
  25. data/app/views/foreman_acd/api/v2/app_definitions/base.json.rabl +3 -0
  26. data/app/views/foreman_acd/api/v2/app_definitions/index.json.rabl +3 -0
  27. data/app/views/foreman_acd/api/v2/app_definitions/show.json.rabl +3 -0
  28. data/app/views/foreman_acd/api/v2/app_instances/base.json.rabl +3 -0
  29. data/app/views/foreman_acd/api/v2/app_instances/index.json.rabl +3 -0
  30. data/app/views/foreman_acd/api/v2/app_instances/show.json.rabl +3 -0
  31. data/app/views/foreman_acd/app_definitions/_form.html.erb +24 -10
  32. data/app/views/foreman_acd/app_definitions/edit.html.erb +5 -0
  33. data/app/views/foreman_acd/app_instances/_form.html.erb +7 -5
  34. data/app/views/foreman_acd/app_instances/index.html.erb +5 -1
  35. data/app/views/templates/job/run_acd_ansible_playbook.erb +49 -0
  36. data/app/views/ui_acd/ansible_data.json.rabl +6 -0
  37. data/app/views/ui_acd/app.json.rabl +6 -2
  38. data/app/views/ui_acd/app_definition.json.rabl +1 -1
  39. data/app/views/ui_acd/{fdata.json.rabl → foreman_data.json.rabl} +1 -1
  40. data/config/routes.rb +24 -1
  41. data/db/migrate/20200916091018_create_ansible_playbooks.rb +20 -0
  42. data/db/migrate/20200917120220_add_ansible_playbook_id.rb +14 -0
  43. data/db/migrate/20201016002819_add_ansible_vars_all_to_app_definitions.rb +5 -0
  44. data/db/migrate/20201016104338_add_ansible_vars_all_to_app_instances.rb +5 -0
  45. data/db/seeds.d/75-job_templates.rb +8 -0
  46. data/lib/foreman_acd/engine.rb +3 -0
  47. data/lib/foreman_acd/plugin.rb +53 -2
  48. data/lib/foreman_acd/version.rb +1 -1
  49. data/package.json +1 -1
  50. data/webpack/components/ApplicationDefinition/ApplicationDefinition.js +137 -22
  51. data/webpack/components/ApplicationDefinition/ApplicationDefinitionActions.js +95 -11
  52. data/webpack/components/ApplicationDefinition/ApplicationDefinitionConstants.js +7 -2
  53. data/webpack/components/ApplicationDefinition/ApplicationDefinitionHelper.js +26 -0
  54. data/webpack/components/ApplicationDefinition/ApplicationDefinitionReducer.js +117 -21
  55. data/webpack/components/ApplicationDefinition/ApplicationDefinitionSelectors.js +2 -0
  56. data/webpack/components/ApplicationDefinition/components/AnsiblePlaybookSelector.js +49 -0
  57. data/webpack/components/ApplicationDefinition/index.js +4 -0
  58. data/webpack/components/ApplicationInstance/ApplicationInstance.js +92 -22
  59. data/webpack/components/ApplicationInstance/ApplicationInstanceActions.js +37 -8
  60. data/webpack/components/ApplicationInstance/ApplicationInstanceConstants.js +4 -2
  61. data/webpack/components/ApplicationInstance/ApplicationInstanceReducer.js +98 -26
  62. data/webpack/components/ApplicationInstance/ApplicationInstanceSelectors.js +2 -1
  63. data/webpack/components/ApplicationInstance/index.js +2 -0
  64. data/webpack/components/ParameterSelection/ParameterSelection.js +46 -37
  65. data/webpack/components/ParameterSelection/ParameterSelectionActions.js +49 -52
  66. data/webpack/components/ParameterSelection/ParameterSelectionConstants.js +5 -3
  67. data/webpack/components/ParameterSelection/ParameterSelectionHelper.js +0 -32
  68. data/webpack/components/ParameterSelection/ParameterSelectionReducer.js +32 -16
  69. data/webpack/components/ParameterSelection/ParameterSelectionSelectors.js +2 -2
  70. data/webpack/components/ParameterSelection/index.js +4 -4
  71. data/webpack/components/common/DeleteTableEntry.js +1 -1
  72. data/webpack/reducer.js +14 -11
  73. metadata +48 -3
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanAcd
4
+ module Concerns
5
+ # Parameters for AnsiblePlaybooks
6
+ module AnsiblePlaybookParameters
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def ansible_playbook_params_filter
11
+ Foreman::ParameterFilter.new(::ForemanAcd::AnsiblePlaybook).tap do |filter|
12
+ filter.permit(:name, :description, :scm_type, :path, :playfile)
13
+ end
14
+ end
15
+ end
16
+
17
+ def ansible_playbook_params
18
+ param_name = parameter_filter_context.api? ? 'ansible_playbook' : 'foreman_acd_ansible_playbook'
19
+ self.class.ansible_playbook_params_filter.filter_params(params, parameter_filter_context, param_name)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -9,7 +9,7 @@ module ForemanAcd
9
9
  class_methods do
10
10
  def app_definition_params_filter
11
11
  Foreman::ParameterFilter.new(::ForemanAcd::AppDefinition).tap do |filter|
12
- filter.permit(:name, :description, :services)
12
+ filter.permit(:name, :description, :acd_ansible_playbook_id, :services, :ansible_vars_all)
13
13
  end
14
14
  end
15
15
  end
@@ -9,7 +9,7 @@ module ForemanAcd
9
9
  class_methods do
10
10
  def app_instance_params_filter
11
11
  Foreman::ParameterFilter.new(::ForemanAcd::AppInstance).tap do |filter|
12
- filter.permit(:name, :app_definition_id, :description, :hosts)
12
+ filter.permit(:name, :app_definition_id, :description, :hosts, :ansible_vars_all)
13
13
  end
14
14
  end
15
15
  end
@@ -8,13 +8,17 @@ class UiAcdController < ::Api::V2::BaseController
8
8
  @app_data['app_definition'] = app_definition
9
9
  end
10
10
 
11
- def fdata
12
- @fdata = collect_fdata(params['id'])
11
+ def foreman_data
12
+ @foreman_data = collect_foreman_data(params['id'])
13
+ end
14
+
15
+ def ansible_data
16
+ @ansible_data = collect_ansible_data(params['id'])
13
17
  end
14
18
 
15
19
  private
16
20
 
17
- def collect_fdata(hostgroup_id)
21
+ def collect_foreman_data(hostgroup_id)
18
22
  hg = Hostgroup.find(hostgroup_id)
19
23
  fdata = OpenStruct.new(
20
24
  :environments => Environment.all,
@@ -26,4 +30,8 @@ class UiAcdController < ::Api::V2::BaseController
26
30
  )
27
31
  fdata
28
32
  end
33
+
34
+ def collect_ansible_data(playbook_id)
35
+ ForemanAcd::AnsiblePlaybook.find(playbook_id).as_unified_structobj
36
+ end
29
37
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanAcd
4
+ # Ansible playbook
5
+ class AnsiblePlaybook < ApplicationRecord
6
+ include Authorizable
7
+ extend FriendlyId
8
+ friendly_id :name
9
+
10
+ self.table_name = 'acd_ansible_playbooks'
11
+ has_many :app_definitions, :inverse_of => :ansible_playbook, :dependent => :destroy
12
+ validates :name, :presence => true, :uniqueness => true
13
+ scoped_search :on => :name
14
+ default_scope -> { order("acd_ansible_playbooks.name") }
15
+
16
+ def self.humanize_class_name(_name = nil)
17
+ _('Ansible playbook')
18
+ end
19
+
20
+ def self.permission_name
21
+ 'ansible_playbooks'
22
+ end
23
+
24
+ def content
25
+ case scm_type
26
+ when 'directory'
27
+ File.read(File.join(path, playfile))
28
+ else
29
+ raise NotImplementedError.new "scm_type #{scm_type.inspect} not supported!"
30
+ end
31
+ end
32
+
33
+ def as_unified_structobj
34
+ groups = []
35
+
36
+ JSON.load(self.vars).each do |group_name, vars|
37
+ groups << OpenStruct.new(
38
+ :name => group_name,
39
+ :vars => vars.map { |k,v| OpenStruct.new(:name => k, :value => v) })
40
+ end
41
+
42
+ adata = OpenStruct.new(
43
+ :id => self.id,
44
+ :name => self.name,
45
+ :groups => JSON.load(self.vars)
46
+ )
47
+ adata
48
+ end
49
+ end
50
+ end
@@ -9,7 +9,9 @@ module ForemanAcd
9
9
 
10
10
  validates :name, :presence => true, :uniqueness => true
11
11
  has_many :app_instances, :inverse_of => :app_definition, :dependent => :destroy
12
+ belongs_to :ansible_playbook, :inverse_of => :app_definitions, :foreign_key => :acd_ansible_playbook_id
12
13
  scoped_search :on => :name
14
+ default_scope -> { order("app_definitions.name") }
13
15
 
14
16
  def self.humanize_class_name(_name = nil)
15
17
  _('App Definition')
@@ -9,6 +9,7 @@ module ForemanAcd
9
9
  validates :name, :presence => true, :uniqueness => true
10
10
  belongs_to :app_definition, :inverse_of => :app_instances
11
11
  scoped_search :on => :name
12
+ default_scope -> { order("app_instances.name") }
12
13
 
13
14
  def self.humanize_class_name(_name = nil)
14
15
  _('App Instance')
@@ -17,5 +18,11 @@ module ForemanAcd
17
18
  def self.permission_name
18
19
  'app_instances'
19
20
  end
21
+
22
+ def foreman_hosts
23
+ app_hosts = JSON.parse(self.hosts)
24
+ host_ids = app_hosts.select{ |h| h&.key?('foreman_host_id') }.map{ |h| h['foreman_host_id'] }
25
+ ::Host.find(host_ids)
26
+ end
20
27
  end
21
28
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanAcd
4
+ # application instances configurator
5
+ class AppConfigurator
6
+
7
+ delegate :logger, :to => :Rails
8
+
9
+ def initialize(app_instance)
10
+ @app_instance = app_instance
11
+ end
12
+
13
+ def configure
14
+ job_input = {}
15
+ job_input['application_name'] = @app_instance.name
16
+ job_input['playbook_name'] = @app_instance.app_definition.ansible_playbook.name
17
+ job_input['playbook_path'] = File.join(@app_instance.app_definition.ansible_playbook.path,
18
+ @app_instance.app_definition.ansible_playbook.playfile)
19
+
20
+ # TODO should or do we really need it to a file?
21
+ #inventory_file = File.new("/tmp/acd_inventory_file", "w") # we can also use Tempfile.new() but a tempfile will be deleted soon (after transaction finished)
22
+ #inventory_file << inventory.to_yaml
23
+ #inventory_file.close
24
+
25
+ logger.info("Use inventory to configure #{@app_instance.name} with ansible playbook #{@app_instance.app_definition.ansible_playbook.name}")
26
+
27
+ begin
28
+ proxy_hosts = {}
29
+ jobs = []
30
+
31
+ hosts = @app_instance.foreman_hosts
32
+ proxy_selector = RemoteExecutionProxySelector.new
33
+ hosts.each do |h|
34
+ proxy = proxy_selector.determine_proxy(h, 'SSH')
35
+ unless proxy_hosts.has_key?(proxy.name)
36
+ proxy_hosts[proxy.name] = Array.new
37
+ end
38
+ proxy_hosts[proxy.name] << h.id
39
+ end
40
+
41
+ # TODO just for testing...
42
+ #proxy_hosts = { Host.first.name => [ Host.first.id] }
43
+
44
+ # we need to compose multiple jobs. for each proxy one job.
45
+ proxy_hosts.each do |proxy_name, host_names|
46
+ # create the inventory file
47
+ inventory = ForemanAcd::InventoryCreator.new(@app_instance, host_names).create_inventory
48
+ job_input['inventory'] = YAML.dump(inventory)
49
+
50
+ # Unfortunately, we can not use "JobInvocationComposer.for_feature" method
51
+ # because then its not possible to the set effective_user
52
+ job = JobTemplate.find(RemoteExecutionFeature.feature('run_acd_ansible_playbook').job_template_id)
53
+ params = {
54
+ :job_category => job.job_category,
55
+ :job_template_id => job.id,
56
+ :targeting_type => 'static_query',
57
+ :search_query => "name = #{proxy_name}",
58
+ :effective_user => 'foreman-proxy',
59
+ :inputs => job_input.to_hash
60
+ }
61
+ composer = JobInvocationComposer.from_api_params(params)
62
+ jobs << composer
63
+ end
64
+ rescue StandardError => e
65
+ logger.error("Failed to configure hosts: #{e.class}: #{e.message}\n#{e.backtrace.join($INPUT_RECORD_SEPARATOR)}")
66
+ end
67
+ jobs
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanAcd
4
+ # application instances deployer
5
+ class AppDeployer
6
+
7
+ delegate :logger, :to => :Rails
8
+ attr_reader :deploy_hosts
9
+
10
+ def initialize(app_instance)
11
+ @app_instance = app_instance
12
+ @deploy_hosts = []
13
+ end
14
+
15
+ def deploy
16
+ services = JSON.parse(@app_instance.app_definition.services)
17
+ app_hosts = JSON.parse(@app_instance.hosts)
18
+
19
+ app_hosts.each do |host_data|
20
+ begin
21
+ service_data = services.select { |k| k['id'] == host_data['service'].to_i }.first
22
+ host_params = set_host_params(host_data, service_data)
23
+
24
+ host = nil
25
+ if host_data.has_key?('foreman_host_id')
26
+ logger.debug("Try to find host with id #{host_data['foreman_host_id']}")
27
+ begin
28
+ host = Host.find(host_data['foreman_host_id'])
29
+ rescue ActiveRecord::RecordNotFound
30
+ logger.info("Host with id #{host_data['foreman_host_id']} couldn\'t be found, create a new one!")
31
+ host = nil
32
+ end
33
+ end
34
+
35
+ if host.nil?
36
+ params = host_attributes(host_params)
37
+ logger.info("Host creation parameters for #{host_data['hostname']}:\n#{params}\n")
38
+ host = Host.new(params)
39
+ else
40
+ logger.info("Update parameters and re-deploy host #{host_data['hostname']}")
41
+ host.attributes = host_attributes(host_params, host)
42
+ host.setBuild
43
+ host.power.reset
44
+ end
45
+
46
+ # REMOVE ME (but very nice for testing)
47
+ #host.mac = "00:11:22:33:44:55"
48
+
49
+ apply_compute_profile(host)
50
+ host.suggest_default_pxe_loader
51
+ host.save
52
+
53
+ # save the foreman host id
54
+ host_data['foreman_host_id'] = host.id
55
+
56
+ @deploy_hosts.push({ id: host.id, name: host_data['hostname'], hostname: host.hostname, hostUrl: host_path(h), progress_report_id: host.progress_report_id})
57
+ rescue StandardError => e
58
+ logger.error("Failed to initiate host creation: #{e.class}: #{e.message}\n#{e.backtrace.join($INPUT_RECORD_SEPARATOR)}")
59
+ end
60
+ end
61
+
62
+ return app_hosts
63
+ end
64
+
65
+ private
66
+
67
+ # Copied from foreman/app/controllers/api/v2/hosts_controller.rb
68
+ def apply_compute_profile(host)
69
+ host.apply_compute_profile(InterfaceMerge.new(:merge_compute_attributes => true))
70
+ host.apply_compute_profile(ComputeAttributeMerge.new)
71
+ end
72
+
73
+ # Copied from foreman/app/controllers/api/v2/hosts_controller.rb
74
+ def host_attributes(params, host = nil)
75
+ return {} if params.nil?
76
+
77
+ params = params.deep_clone
78
+ if params[:interfaces_attributes]
79
+ # handle both hash and array styles of nested attributes
80
+ params[:interfaces_attributes] = params[:interfaces_attributes].values if params[:interfaces_attributes].is_a?(Hash) || params[:interfaces_attributes].is_a?(ActionController::Parameters)
81
+ # map interface types
82
+ params[:interfaces_attributes] = params[:interfaces_attributes].map do |nic_attr|
83
+ interface_attributes(nic_attr, :allow_nil_type => host.nil?)
84
+ end
85
+ end
86
+ params = host.apply_inherited_attributes(params) if host
87
+ params
88
+ end
89
+
90
+ # Copied from foreman/app/controllers/api/v2/hosts_controller.rb
91
+ def interface_attributes(params, allow_nil_type: false)
92
+ params[:type] = InterfaceTypeMapper.map(params[:type]) if params.key?(:type) || allow_nil_type
93
+ params
94
+ end
95
+
96
+ def hardcoded_params
97
+ result = {}
98
+ result['managed'] = true
99
+ result['enabled'] = true
100
+ result['build'] = true
101
+ result['compute_attributes'] = { 'start' => '1' }
102
+ result['host_parameters_attributes'] = []
103
+ result
104
+ end
105
+
106
+ def set_host_params(host_data, service_data)
107
+ result = hardcoded_params
108
+ result['name'] = host_data['hostname']
109
+ result['hostgroup_id'] = service_data['hostgroup']
110
+
111
+ host_data['foremanParameters'].each do |param|
112
+ case param['type']
113
+
114
+ when 'computeprofile'
115
+ result['compute_profile_id'] = param['value']
116
+
117
+ when 'domain'
118
+ result['domain_id'] = param['value']
119
+
120
+ when 'hostparam'
121
+ result['host_parameters_attributes'].push(:name => param['name'], :value => param['value'])
122
+
123
+ when 'ip'
124
+ result['ip'] = param['value']
125
+
126
+ when 'lifecycleenv'
127
+ result['content_facet_attributes'] = { 'lifecycle_environment_id' => param['value'] }
128
+
129
+ when 'ptable'
130
+ result['ptable_id'] = param['value']
131
+
132
+ when 'puppetenv'
133
+ result['environment_id'] = param['value']
134
+
135
+ when 'password'
136
+ result['root_pass'] = param['value']
137
+
138
+ end
139
+ end
140
+ result
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanAcd
4
+ # inventory creator for application instances
5
+ class InventoryCreator
6
+
7
+ delegate :logger, :to => :Rails
8
+
9
+ def initialize(app_instance, host_ids)
10
+ @app_instance = app_instance
11
+ @host_ids = host_ids
12
+ end
13
+
14
+ # TODO: this might be part of the smart proxy plugin.
15
+ def create_inventory
16
+ inventory = {}
17
+ inventory['all'] = {}
18
+
19
+ inventory['all'] = { 'vars' => inventory_all_vars } unless @app_instance.ansible_vars_all.nil? || @app_instance.ansible_vars_all.empty?
20
+
21
+ services = JSON.parse(@app_instance.app_definition.services)
22
+ app_hosts = filtered_hosts
23
+
24
+ children = {}
25
+ app_hosts.each do |host_data|
26
+ if host_data['foreman_host_id'].nil?
27
+ logger.warn "Ignore host #{host_data['hostname']} because no foreman host id could be found. Is the host not provisioned yet?"
28
+ next
29
+ end
30
+
31
+ service_id = host_data['service'].to_i
32
+ host_service = services.select { |s| s['id'] == service_id }.first
33
+ ansible_group = host_service['ansibleGroup']
34
+
35
+ unless children.has_key?(host_service['ansibleGroup'])
36
+ children[ansible_group] = { 'hosts' => {} }
37
+ end
38
+
39
+ ansible_vars = host_data['ansibleParameters'].map { |v| { v['name'] => v['value'] } }.reduce({}, :merge!)
40
+
41
+ # in case there is no ansible_user defined, set "root" as default.
42
+ unless ansible_vars.has_key?('ansible_user')
43
+ ansible_vars['ansible_user'] = 'root'
44
+ end
45
+
46
+ children[ansible_group]['hosts'][get_fqdn(host_data['foreman_host_id'])] = ansible_vars
47
+ end
48
+ inventory['all']['children'] = children
49
+ inventory
50
+ end
51
+
52
+ private
53
+ def inventory_all_vars
54
+ JSON.parse(@app_instance.ansible_vars_all).map do |a|
55
+ { a['name'] => a['value'] }
56
+ end.reduce({}, :merge!)
57
+ end
58
+
59
+ def get_fqdn(host_id)
60
+ Host.find(host_id)&.name
61
+ end
62
+
63
+ def filtered_hosts
64
+ JSON.parse(@app_instance.hosts).select{ |h| h&.key?('foreman_host_id') && @host_ids.include?(h['foreman_host_id']) }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,21 @@
1
+ <%= form_for @ansible_playbook, :url => (@ansible_playbook.new_record? ? ansible_playbooks_path : ansible_playbook_path(:id => @ansible_playbook.id)) do |f| %>
2
+ <%= base_errors_for @ansible_playbook %>
3
+
4
+ <ul class="nav nav-tabs" data-tabs="tabs">
5
+ <li class="active"><a href="#primary" data-toggle="tab"><%= _('Ansible playbooks') %></a></li>
6
+ </ul>
7
+
8
+ <div class="tab-content">
9
+ <div class="tab-pane active" id="primary">
10
+ <%= text_f f, :name %>
11
+ <%= text_f f, :description %>
12
+ <%= f.hidden_field :scm_type, :value => 'directory' %>
13
+ <% # TODO:
14
+ # select_f(f, :scm_type, %w[Directory GIT], :downcase, :to_s)
15
+ %>
16
+ <%= text_f f, :path %>
17
+ <%= text_f f, :playfile %>
18
+ </div>
19
+
20
+ <%= submit_or_cancel f %>
21
+ <% end %>