foreman_setup 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES.md +4 -0
  3. data/LICENSE +619 -0
  4. data/README.md +47 -0
  5. data/app/assets/javascripts/foreman_setup/provisioner.js +13 -0
  6. data/app/controllers/foreman_setup/provisioners_controller.rb +206 -0
  7. data/app/helpers/concerns/foreman_setup/home_helper_ext.rb +15 -0
  8. data/app/helpers/foreman_setup/provisioner_helper.rb +14 -0
  9. data/app/models/foreman_setup/provisioner.rb +77 -0
  10. data/app/overrides/add_dashboard_provisioners_link.rb +12 -0
  11. data/app/overrides/remove_subnet_domains.rb +9 -0
  12. data/app/overrides/remove_subnet_proxies.rb +9 -0
  13. data/app/views/foreman_setup/provisioners/_step1.html.erb +45 -0
  14. data/app/views/foreman_setup/provisioners/_step2.html.erb +19 -0
  15. data/app/views/foreman_setup/provisioners/_step3.html.erb +58 -0
  16. data/app/views/foreman_setup/provisioners/_step4.html.erb +49 -0
  17. data/app/views/foreman_setup/provisioners/_step5.html.erb +22 -0
  18. data/app/views/foreman_setup/provisioners/index.html.erb +23 -0
  19. data/app/views/foreman_setup/provisioners/new.html.erb +3 -0
  20. data/app/views/foreman_setup/provisioners/step2.html.erb +3 -0
  21. data/app/views/foreman_setup/provisioners/step3.html.erb +3 -0
  22. data/app/views/foreman_setup/provisioners/step4.html.erb +3 -0
  23. data/app/views/foreman_setup/provisioners/step5.html.erb +3 -0
  24. data/config/routes.rb +15 -0
  25. data/db/migrate/20131023140100_add_provisioners.rb +11 -0
  26. data/db/migrate/20131025180600_add_provisioners_hostgroup.rb +5 -0
  27. data/db/migrate/20131028161000_add_provisioners_domain.rb +5 -0
  28. data/lib/foreman_setup.rb +7 -0
  29. data/lib/foreman_setup/engine.rb +37 -0
  30. data/lib/foreman_setup/version.rb +3 -0
  31. metadata +89 -0
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # foreman_setup
2
+
3
+ Setting up Foreman for provisioning can be daunting at first, as there are
4
+ lots of parameters to configure DHCP and DNS for the installer, plus for setup
5
+ of subnets, domains, installation media etc for Foreman.
6
+
7
+ # Installation
8
+
9
+ Please see the Foreman wiki for appropriate instructions:
10
+
11
+ * [Foreman: How to Install a Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
12
+
13
+ The gem name is "foreman_setup". Run `foreman-rake db:migrate` after
14
+ installation.
15
+
16
+ RPM users can install the "ruby193-rubygem-foreman_setup" or
17
+ "rubygem-foreman_setup" packages.
18
+
19
+ # Areas this should help
20
+
21
+ * take input of subnet and domain information
22
+ * output foreman-installer command with appropriate DHCP, DNS and TFTP parameters
23
+ * add foreman-installer modules to the Foreman host with appropriate parameters
24
+ * create a host group with appropriate parameters
25
+ * create hosts (proxies/nodes) using created host groups
26
+ * ensure provided templates and OSes are fully associated
27
+ * default templates should be properly associated in core
28
+ * when using Katello, its Foreman plugin helps associate
29
+ * add appropriate installation media
30
+ * add appropriate Spacewalk/redhat_register parameters
31
+
32
+ # Copyright
33
+
34
+ Copyright (c) 2013 Red Hat Inc.
35
+
36
+ This program is free software: you can redistribute it and/or modify
37
+ it under the terms of the GNU General Public License as published by
38
+ the Free Software Foundation, either version 3 of the License, or
39
+ (at your option) any later version.
40
+
41
+ This program is distributed in the hope that it will be useful,
42
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
43
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
44
+ GNU General Public License for more details.
45
+
46
+ You should have received a copy of the GNU General Public License
47
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,13 @@
1
+ function setup_media_change(ele) {
2
+ if ($(ele).val() == 'spacewalk') {
3
+ $('[id$="medium_path"]').prop('disabled', true)
4
+ $('#spacewalk_hostname').prop('disabled', false)
5
+ } else {
6
+ $('#spacewalk_hostname').prop('disabled', true)
7
+ $('[id$="medium_path"]').prop('disabled', false)
8
+ }
9
+ }
10
+
11
+ function setup_media_create_change(ele) {
12
+ $('[id$="medium_id"]').val('')
13
+ }
@@ -0,0 +1,206 @@
1
+ require 'facter'
2
+
3
+ module ForemanSetup
4
+ class ProvisionersController < ::ApplicationController
5
+ include Foreman::Renderer
6
+
7
+ before_filter :find_myself, :only => [:new, :create]
8
+ before_filter :find_resource, :except => [:index, :new, :create]
9
+
10
+ def index
11
+ @provisioners = Provisioner.all.paginate :page => params[:page]
12
+ redirect_to new_foreman_setup_provisioner_path unless @provisioners.any?
13
+ end
14
+
15
+ def destroy
16
+ @provisioner = Provisioner.find(params[:id])
17
+ if @provisioner.destroy
18
+ process_success :success_redirect => foreman_setup_provisioners_path
19
+ else
20
+ process_error
21
+ end
22
+ end
23
+
24
+ def new
25
+ @provisioner = Provisioner.new(:host => @host, :smart_proxy => @proxy)
26
+ end
27
+
28
+ def create
29
+ @provisioner = Provisioner.new(params['foreman_setup_provisioner'])
30
+ if @provisioner.save
31
+ redirect_to step2_foreman_setup_provisioner_path(@provisioner)
32
+ else
33
+ @provisioner.host = @host
34
+ @provisioner.smart_proxy = @proxy
35
+ process_error :render => 'foreman_setup/provisioners/new', :object => @provisioner
36
+ end
37
+ end
38
+
39
+ # Basic model created, now fill in nested subnet/domain info using selected interface
40
+ def step2
41
+ network = @provisioner.provision_interface_data
42
+ @provisioner.subnet ||= Subnet.find_by_network(network[:network])
43
+ @provisioner.subnet ||= Subnet.new(network.slice(:network, :mask).merge(
44
+ :dns_primary => @provisioner.provision_interface_data[:ip],
45
+ ))
46
+
47
+ @provisioner.domain ||= @provisioner.host.domain
48
+ @provisioner.domain ||= Domain.new(:name => 'example.com')
49
+ end
50
+
51
+ def step2_update
52
+ @provisioner.hostgroup ||= Hostgroup.find_or_create_by_name(_("Provision from %s") % @provisioner.fqdn)
53
+ @provisioner.subnet ||= Subnet.find_by_id(params['foreman_setup_provisioner']['subnet_attributes']['id'])
54
+ domain_name = params['foreman_setup_provisioner'].delete('domain_name')
55
+ @provisioner.domain = Domain.find_by_name(domain_name)
56
+ @provisioner.domain ||= Domain.new(:name => domain_name)
57
+
58
+ if @provisioner.update_attributes(params['foreman_setup_provisioner'])
59
+ @provisioner.subnet.domains << @provisioner.domain unless @provisioner.subnet.domains.include? @provisioner.domain
60
+ process_success :success_msg => _("Successfully updated subnet %s.") % @provisioner.subnet.name, :success_redirect => step3_foreman_setup_provisioner_path
61
+ else
62
+ process_error :render => 'foreman_setup/provisioners/step2', :object => @provisioner, :redirect => step2_foreman_setup_provisioner_path
63
+ end
64
+ end
65
+
66
+ # foreman-installer info screen
67
+ def step3
68
+ end
69
+
70
+ # Installer completed, start associating data, begin install media setup
71
+ def step4
72
+ # Refresh proxy features (1.4 versus 1.3 techniques)
73
+ proxy = @provisioner.smart_proxy
74
+ proxy.respond_to?(:refresh) ? proxy.refresh : proxy.ping
75
+ proxy.save!
76
+
77
+ # Associate as much as possible
78
+ if proxy.features.include? Feature.find_by_name('DNS')
79
+ @provisioner.domain.dns_id ||= proxy.id
80
+ @provisioner.subnet.dns_id ||= proxy.id
81
+ end
82
+ if proxy.features.include? Feature.find_by_name('DHCP')
83
+ @provisioner.subnet.dhcp_id ||= proxy.id
84
+ end
85
+ if proxy.features.include? Feature.find_by_name('TFTP')
86
+ @provisioner.subnet.tftp_id ||= proxy.id
87
+ end
88
+ @provisioner.save!
89
+
90
+ # Helpful fix to work around #3210
91
+ url = Setting.find_by_name('foreman_url')
92
+ url.value ||= Facter.fqdn
93
+ url.save!
94
+
95
+ # Build default PXE menu
96
+ status, msg = ConfigTemplate.build_pxe_default(self)
97
+ warning msg unless status == 200
98
+
99
+ @provisioner.hostgroup.medium ||= @provisioner.host.os.media.first
100
+ @medium = Medium.new(params['foreman_setup_provisioner'].try(:[], 'medium_attributes'))
101
+
102
+ parameters = @provisioner.hostgroup.group_parameters
103
+ @activation_key = parameters.where(:name => 'activation_key').first || parameters.new(:name => 'activation_key')
104
+ @satellite_type = parameters.where(:name => 'satellite_type').first || parameters.new(:name => 'satellite_type')
105
+ end
106
+
107
+ def step4_update
108
+ if params['medium_type'] == 'spacewalk'
109
+ spacewalk_hostname = params['spacewalk_hostname']
110
+ if spacewalk_hostname.blank?
111
+ @provisioner.errors.add(:base, _("Spacewalk hostname is missing"))
112
+ process_error :render => 'foreman_setup/provisioners/step4', :object => @provisioner, :redirect => step4_foreman_setup_provisioner_path
113
+ return
114
+ end
115
+ end
116
+
117
+ medium_id = params['foreman_setup_provisioner']['hostgroup_attributes']['medium_id']
118
+ if medium_id.to_i > 0
119
+ @medium = Medium.find(medium_id) || raise('unable to find medium')
120
+ else
121
+ @provisioner.hostgroup.medium_id = nil
122
+ @medium = Medium.new(params['foreman_setup_provisioner']['create_medium'].slice(:name, :path))
123
+ end
124
+ @medium.path = "http://#{spacewalk_hostname}/ks/dist/ks-rhel-$arch-server-$major-$version" unless spacewalk_hostname.blank?
125
+
126
+ parameters = @provisioner.hostgroup.group_parameters
127
+ @activation_key = parameters.where(:name => 'activation_key').first
128
+ if @activation_key
129
+ @activation_key.assign_attributes(params['foreman_setup_provisioner']['activation_key'])
130
+ elsif params['foreman_setup_provisioner']['activation_key']['value'].present?
131
+ @activation_key = parameters.new(params['foreman_setup_provisioner']['activation_key'].merge(:name => 'activation_key'))
132
+ end
133
+
134
+ @satellite_type = parameters.where(:name => 'satellite_type').first
135
+ if @satellite_type
136
+ @satellite_type.assign_attributes(params['foreman_setup_provisioner']['satellite_type'])
137
+ elsif params['foreman_setup_provisioner']['satellite_type']['value'].present?
138
+ @satellite_type = parameters.new(params['foreman_setup_provisioner']['satellite_type'].merge(:name => 'satellite_type'))
139
+ end
140
+
141
+ # Associate medium with the host OS
142
+ @medium.os_family ||= @provisioner.host.os.type
143
+ @medium.operatingsystems << @provisioner.host.os unless @medium.operatingsystems.include? @provisioner.host.os
144
+ unless @medium.save
145
+ process_error :render => 'foreman_setup/provisioners/step4', :object => @provisioner, :redirect => step4_foreman_setup_provisioner_path
146
+ return
147
+ end
148
+
149
+ # Associate templates with OS and vice-versa
150
+ if @provisioner.host.os.family == 'Redhat'
151
+ tmpl_name = 'Kickstart'
152
+ provision_tmpl_name = @provisioner.host.os.name == 'Redhat' ? 'RHEL Kickstart' : tmpl_name
153
+ gpxe_tmpl_name = 'Kickstart'
154
+ ptable_name = 'RedHat default'
155
+ elsif @provisioner.host.os.family == 'Debian'
156
+ tmpl_name = provision_tmpl_name = 'Preseed'
157
+ ptable_name = 'Ubuntu default'
158
+ end
159
+
160
+ {'provision' => provision_tmpl_name, 'PXELinux' => tmpl_name, 'gPXE' => gpxe_tmpl_name}.each do |kind_name, tmpl_name|
161
+ next if tmpl_name.blank?
162
+ kind = TemplateKind.find_by_name(kind_name)
163
+ tmpls = ConfigTemplate.where('name LIKE ?', "#{tmpl_name}%").where(:template_kind_id => kind.id)
164
+ tmpls.any? || raise("cannot find template for #{@provisioner.host.os}")
165
+
166
+ # prefer foreman_bootdisk templates
167
+ tmpl = tmpls.where('name LIKE "boot disk"').first || tmpls.first
168
+
169
+ tmpl.operatingsystems << @provisioner.host.os unless tmpl.operatingsystems.include? @provisioner.host.os
170
+ tmpl.save!
171
+
172
+ unless @provisioner.host.os.os_default_templates.where(:template_kind_id => kind.id).any?
173
+ @provisioner.host.os.os_default_templates.build(:template_kind_id => kind.id, :config_template_id => tmpl.id)
174
+ end
175
+ end
176
+
177
+ @provisioner.host.os.architectures << @provisioner.host.architecture unless @provisioner.host.os.architectures.include? @provisioner.host.architecture
178
+ @provisioner.host.os.save!
179
+
180
+ ptable = Ptable.where('name LIKE ?', "#{ptable_name}%").first || raise("cannot find ptable for #{@provisioner.host.os}")
181
+ ptable.operatingsystems << @provisioner.host.os unless ptable.operatingsystems.include? @provisioner.host.os
182
+ ptable.save!
183
+
184
+ @provisioner.hostgroup.medium_id = @medium.id
185
+ @provisioner.hostgroup.ptable_id ||= ptable.id
186
+ @provisioner.hostgroup.save!
187
+
188
+ process_success :success_msg => _("Successfully associated OS %s.") % @provisioner.host.os.to_s, :success_redirect => step5_foreman_setup_provisioner_path
189
+ end
190
+
191
+ private
192
+
193
+ # foreman_setup manages only itself at the moment, so ensure we always have a reference to
194
+ # the Host and SmartProxy on this server
195
+ def find_myself
196
+ fqdn = Facter.fqdn
197
+ @host = Host.find_by_name(fqdn)
198
+ @proxy = SmartProxy.all.find { |p| URI.parse(p.url).host == fqdn }
199
+ end
200
+
201
+ def find_resource
202
+ @provisioner = Provisioner.find(params[:id]) or raise('unknown id')
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,15 @@
1
+ module ForemanSetup
2
+ module HomeHelperExt
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method_chain :settings_menu_items, :provisioners_link
7
+ end
8
+
9
+ def settings_menu_items_with_provisioners_link
10
+ menu_items = settings_menu_items_without_provisioners_link
11
+ menu_items[2][2].insert(7, [_('Provisioning Setup'), :'foreman_setup/provisioners']) if menu_items[2]
12
+ menu_items
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module ForemanSetup
2
+ module ProvisionerHelper
3
+ def provisioner_wizard(step)
4
+ wizard_header(
5
+ step,
6
+ _("Pre-requisites"),
7
+ _("Network config"),
8
+ _("Foreman installer"),
9
+ _("Installation media"),
10
+ _("Completion")
11
+ )
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,77 @@
1
+ require 'ipaddr'
2
+
3
+ module ForemanSetup
4
+ class Provisioner < ActiveRecord::Base
5
+ include ::Authorization
6
+ include ::Host::Hostmix
7
+
8
+ before_save :populate_hostgroup
9
+
10
+ belongs_to_host
11
+ belongs_to :domain
12
+ belongs_to :hostgroup, :autosave => true
13
+ belongs_to :smart_proxy
14
+ belongs_to :subnet
15
+ has_one :architecture, :through => :host
16
+ has_one :medium, :through => :hostgroup
17
+ has_one :operatingsystem, :through => :host
18
+
19
+ accepts_nested_attributes_for :hostgroup
20
+ # TODO: further validation on the subnet's (usually optional) attributes
21
+ accepts_nested_attributes_for :subnet
22
+
23
+ validates :host_id, :presence => true, :uniqueness => true
24
+ validates :smart_proxy_id, :presence => true
25
+
26
+ def to_s
27
+ host.try(:to_s)
28
+ end
29
+
30
+ def fqdn
31
+ Facter.fqdn
32
+ end
33
+
34
+ def interfaces
35
+ facts = host.facts_hash
36
+ (facts['interfaces'] || '').split(',').reject { |i| i == 'lo' }.inject({}) do |ifaces,i|
37
+ ip = facts["ipaddress_#{i}"]
38
+ network = facts["network_#{i}"]
39
+ netmask = facts["netmask_#{i}"]
40
+ if ip && network && netmask
41
+ cidr = "#{network}/#{IPAddr.new(netmask).to_i.to_s(2).count("1")}"
42
+ ifaces[i] = {:ip => ip, :mask => netmask, :network => network, :cidr => cidr}
43
+ end
44
+ ifaces
45
+ end
46
+ end
47
+
48
+ def provision_interface_data
49
+ interfaces[provision_interface]
50
+ end
51
+
52
+ def rdns_zone
53
+ netmask_octets = subnet.mask.split('.').reverse
54
+ subnet.network.split('.').reverse.drop_while { |i| netmask_octets.shift == '0' }.join('.') + '.in-addr.arpa'
55
+ end
56
+
57
+ def dns_forwarders
58
+ File.open('/etc/resolv.conf', 'r').each_line.map do |r|
59
+ $1 if r =~ /^nameserver\s+(\S+)/
60
+ end.compact
61
+ end
62
+
63
+ private
64
+
65
+ # Ensures our nested hostgroup has as much data as possible
66
+ def populate_hostgroup
67
+ return unless hostgroup.present?
68
+ hostgroup.architecture_id ||= domain.id
69
+ hostgroup.domain_id ||= domain.id
70
+ hostgroup.operatingsystem_id ||= operatingsystem.id
71
+ hostgroup.puppet_ca_proxy_id = smart_proxy.id if smart_proxy.features.include? Feature.find_by_name('Puppet CA')
72
+ hostgroup.puppet_proxy_id = smart_proxy.id if smart_proxy.features.include? Feature.find_by_name('Puppet')
73
+ hostgroup.subnet_id ||= subnet.id
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,12 @@
1
+ Deface::Override.new(:virtual_path => 'dashboard/index',
2
+ :name => 'add_dashboard_provisioners_link',
3
+ :surround_contents => 'code[erb-loud]:contains("title_actions")',
4
+ :original => '35e276b7102eaa4feb8a4a84f66fd4812216fd48',
5
+ :text => <<EOS
6
+ if ForemanSetup::Provisioner.any?
7
+ <%= render_original %>
8
+ else
9
+ title_actions link_to_if_authorized(_("Set up provisioning"), hash_for_new_foreman_setup_provisioner_path)
10
+ end
11
+ EOS
12
+ )
@@ -0,0 +1,9 @@
1
+ Deface::Override.new(:virtual_path => 'subnets/_fields',
2
+ :name => 'remove_subnet_domains',
3
+ :surround_contents => 'code[erb-loud]:contains("multiple_checkboxes f, :domain")',
4
+ :text => <<EOS
5
+ unless controller.controller_name == 'provisioners'
6
+ <%= render_original %>
7
+ end
8
+ EOS
9
+ )
@@ -0,0 +1,9 @@
1
+ Deface::Override.new(:virtual_path => 'subnets/_fields',
2
+ :name => 'remove_subnet_proxies',
3
+ :surround_contents => 'code[erb-loud]:contains("SmartProxy.")',
4
+ :text => <<EOS
5
+ unless controller.controller_name == 'provisioners'
6
+ <%= render_original %>
7
+ end
8
+ EOS
9
+ )
@@ -0,0 +1,45 @@
1
+
2
+ <%= form_for provisioner do |f| %>
3
+ <%= base_errors_for provisioner %>
4
+ <%= provisioner_wizard 1 %>
5
+
6
+ <p>
7
+ <%= _("This wizard will help set up Foreman for full host provisioning. Before we begin, a few requirements will be verified.") %>
8
+ </p>
9
+
10
+ <h4><%= _("Pre-requisites") %></h4>
11
+ <div>
12
+ <%= f.hidden_field :host_id %>
13
+ <% if f.object.host.present? %>
14
+ <%= checked_icon f.object.host.present? %> <%= _("Found registered host %s") % f.object.host.name %>
15
+ <% else %>
16
+ <%= image_tag 'false.png' %> <%= _("Missing registered host %s, please ensure it is checking in") % f.object.fqdn %>
17
+ <% end %>
18
+ </div>
19
+ <div>
20
+ <%= f.hidden_field :smart_proxy_id %>
21
+ <% if f.object.smart_proxy.present? %>
22
+ <%= checked_icon f.object.smart_proxy.present? %> <%= _("Found registered smart proxy %s") % f.object.smart_proxy.name %>
23
+ <% else %>
24
+ <%= image_tag 'false.png' %> <%= _("Missing registered smart proxy %s, please ensure it is registered") % f.object.fqdn %>
25
+ <% end %>
26
+ </div>
27
+ <div>
28
+ <% if f.object.host.present? && f.object.interfaces.any? %>
29
+ <%= checked_icon f.object.host.present? %> <%= _("Host %s has at least one network interface") % f.object.host.name %>
30
+ <% else %>
31
+ <%= image_tag 'false.png' %> <%= _("No network interfaces listed in $interfaces fact") %>
32
+ <% end %>
33
+ </div>
34
+
35
+ <h4><%= _("Network selection") %></h4>
36
+ <div>
37
+ <% if f.object.host.present? && f.object.interfaces.any? %>
38
+ <%= selectable_f f, :provision_interface, f.object.interfaces.map { |i| ["#{i[1][:cidr]} (#{i[0]})", i[0]] }, {}, :label => _("Provisioning network") %>
39
+ <% else %>
40
+ <%= _("Not available until pre-requisites satisified.") %>
41
+ <% end %>
42
+ </div>
43
+
44
+ <%= submit_or_cancel f, false, {:cancel_path => foreman_setup_provisioners_path, :disabled => !(f.object.host.present? && f.object.smart_proxy.present?)} %>
45
+ <% end %>