diff --git a/app/assets/javascripts/hosts.js b/app/assets/javascripts/hosts.js new file mode 100644 index 000000000000..6a87f749402e --- /dev/null +++ b/app/assets/javascripts/hosts.js @@ -0,0 +1,23 @@ +$(function () { + $("#build-review").click(function() { + $("#review_before_build .modal-body #build_status").html(''); + $('.loading').addClass('visible'); + $.ajax({ + type:'get', + url: $(this).attr('data-url'), + success: function(result){ + $("#review_before_build .modal-body #build_status").html(result); + }, + complete: function(){ + $('.loading').removeClass('visible'); + } + }) + }); + $('#review_before_build').on('change', "#host_build", function () { + $('#build_form').find('input.submit').val((this.checked) ? (__("Reboot and build")) : (__("Build"))); + }); + + $('#review_before_build').on('click', "#recheck_review", function () { + $("#build-review").click(); + }); +}); diff --git a/app/assets/stylesheets/hosts.scss b/app/assets/stylesheets/hosts.scss new file mode 100644 index 000000000000..a615b9475a18 --- /dev/null +++ b/app/assets/stylesheets/hosts.scss @@ -0,0 +1,38 @@ +#review_before_build { + #build_form { + padding: 0px; + border: none; + .modal-footer { + border-top: none; + padding: 0px; + } + } + .list-group { + float: left; + clear: both; + width: 100%; + .list-group-item { + background: transparent; + border: none; + font-size: 90%; + width: 90%; + } + } + + .build_state { + max-height: 400px; + overflow: auto; + } + + .loading { + display: none; + width: 100%; + text-align: center; + &.visible { + display: block; + img { + width: 22px; + } + } + } +} \ No newline at end of file diff --git a/app/controllers/hosts_controller.rb b/app/controllers/hosts_controller.rb index 60a255b5bbc5..df6b32491aa5 100644 --- a/app/controllers/hosts_controller.rb +++ b/app/controllers/hosts_controller.rb @@ -6,7 +6,7 @@ class HostsController < ApplicationController PUPPETMASTER_ACTIONS=[ :externalNodes, :lookup ] SEARCHABLE_ACTIONS= %w[index active errors out_of_sync pending disabled ] - AJAX_REQUESTS=%w{compute_resource_selected hostgroup_or_environment_selected current_parameters puppetclass_parameters process_hostgroup process_taxonomy} + AJAX_REQUESTS=%w{compute_resource_selected hostgroup_or_environment_selected current_parameters puppetclass_parameters process_hostgroup process_taxonomy review_before_build} BOOT_DEVICES={ :disk => N_('Disk'), :cdrom => N_('CDROM'), :pxe => N_('PXE'), :bios => N_('BIOS') } MULTIPLE_ACTIONS = %w(multiple_parameters update_multiple_parameters select_multiple_hostgroup update_multiple_hostgroup select_multiple_environment update_multiple_environment @@ -17,10 +17,11 @@ class HostsController < ApplicationController add_puppetmaster_filters PUPPETMASTER_ACTIONS before_filter :ajax_request, :only => AJAX_REQUESTS - before_filter :find_resource, :only => [:show, :clone, :edit, :update, :destroy, :puppetrun, + before_filter :find_resource, :only => [:show, :clone, :edit, :update, :destroy, :puppetrun, :review_before_build, :setBuild, :cancelBuild, :power, :overview, :bmc, :vm, :runtime, :resources, :templates, :ipmi_boot, :console, :toggle_manage, :pxe_config, :storeconfig_klasses, :disassociate] + before_filter :taxonomy_scope, :only => [:new, :edit] + AJAX_REQUESTS before_filter :set_host_type, :only => [:update] before_filter :find_multiple, :only => MULTIPLE_ACTIONS @@ -187,10 +188,26 @@ def puppetrun redirect_to host_path(@host) end + def review_before_build + @build = @host.build_status + render :layout => false + end + def setBuild forward_url_options if @host.setBuild - process_success :success_msg => _("Enabled %s for rebuild on next boot") % (@host), :success_redirect => :back + if (params[:host] && params[:host][:build] == '1') + begin + process_success :success_msg => _("Enabled %s for reboot and rebuild") % (@host), :success_redirect => :back if @host.power.reset + rescue => error + logger.warn(error.to_s) + logger.debug error.backtrace.join("\n") + warning(_('Failed to reboot %s.') % @host) + process_success :success_msg => _("Enabled %s for rebuild on next boot") % (@host), :success_redirect => :back + end + else + process_success :success_msg => _("Enabled %s for rebuild on next boot") % (@host), :success_redirect => :back + end else process_error :redirect => :back, :error_msg => _("Failed to enable %{host} for installation: %{errors}") % { :host => @host, :errors => @host.errors.full_messages } end @@ -523,30 +540,10 @@ def process_taxonomy end def template_used - host = params[:host] - kinds = if params[:provisioning] == 'image' - cr = ComputeResource.find_by_id(host[:compute_resource_id]) - images = cr.try(:images) - if images.nil? - [TemplateKind.find('finish')] - else - uuid = host[:compute_attributes][cr.image_param_name] - image_kind = images.find_by_uuid(uuid).try(:user_data) ? 'user_data' : 'finish' - [TemplateKind.find(image_kind)] - end - else - TemplateKind.all - end - - templates = kinds.map do |kind| - ConfigTemplate.find_template({:kind => kind.name, - :operatingsystem_id => host[:operatingsystem_id], - :hostgroup_id => host[:hostgroup_id], - :environment_id => host[:environment_id] - }) - end.compact + host = Host.new(params[:host]) + templates = host.available_template_kinds(params[:provisioning]) return not_found if templates.empty? - render :partial => "provisioning", :locals => {:templates => templates} + render :partial => 'provisioning', :locals => { :templates => templates } end private @@ -562,7 +559,7 @@ def action_permission :view when 'puppetrun', 'multiple_puppetrun', 'update_multiple_puppetrun' :puppetrun - when 'setBuild', 'cancelBuild', 'multiple_build', 'submit_multiple_build' + when 'setBuild', 'cancelBuild', 'multiple_build', 'submit_multiple_build', 'review_before_build' :build when 'power' :power diff --git a/app/helpers/hosts_helper.rb b/app/helpers/hosts_helper.rb index ae7acbb9f424..03366fbfba60 100644 --- a/app/helpers/hosts_helper.rb +++ b/app/helpers/hosts_helper.rb @@ -237,10 +237,16 @@ def host_title_actions(host) :disabled => host.can_be_built?, :title => _("Cancel build request for this host")) else - link_to_if_authorized(_("Build"), hash_for_setBuild_host_path(:id => host).merge(:auth_object => host, :permission => 'build_hosts'), + link_to_if_authorized(_("Build"), hash_for_host_path(:id => host).merge(:auth_object => host, :permission => 'build_hosts', :anchor => "review_before_build"), :disabled => !host.can_be_built?, :title => _("Enable rebuild on next host boot"), - :confirm => _("Rebuild %s on next reboot?\nThis would also delete all of its current facts and reports") % host) + :class => "btn", + :id => "build-review", + :data => { :toggle => 'modal', + :target => '#review_before_build', + :url => review_before_build_host_path(:id => host) + } + ) end ), if host.compute_resource_id || host.bmc_available? @@ -329,4 +335,30 @@ def link_status(f) return '' if f.object.new_record? '(' + (f.object.link ? _('Up') : _('Down')) + ')' end + + def build_state(build) + build.state ? 'warning' : 'danger' + end + + def review_build_button(form, status) + form.submit(_("Build"), + :class => "btn btn-#{status} submit", + :title => (status == 'warning') ? _('Build') : _('Errors occurred, build may fail') + ) + end + + def supports_power_and_running(host) + return false unless host.compute_resource_id || host.bmc_available? + host.power.ready? + rescue ProxyAPI::ProxyException + false + end + + def build_error_link(type, id) + case type + when :templates + link_to_if_authorized(_("Edit"), hash_for_edit_config_template_path(:id => id).merge(:auth_object => id), + :class => "btn btn-default btn-xs pull-right", :title => _("Edit %s" % type) ) + end + end end diff --git a/app/helpers/layout_helper.rb b/app/helpers/layout_helper.rb index c9496291adb9..ad41bfdb4fa1 100644 --- a/app/helpers/layout_helper.rb +++ b/app/helpers/layout_helper.rb @@ -160,9 +160,9 @@ def field(f, attr, options = {}) label = label.present? ? label_tag(attr, "#{label}#{required_mark}".html_safe, :class => "col-md-2 control-label") : '' label.html_safe + - content_tag(:div, :class => size_class) do - yield.html_safe + help_block.html_safe - end.html_safe + help_inline.html_safe + content_tag(:div, :class => size_class) do + yield.html_safe + help_block.html_safe + end.html_safe + help_inline.html_safe end.html_safe end end @@ -297,8 +297,8 @@ def alert_header(text) "

#{text}

".html_safe end - def alert_close - ''.html_safe + def alert_close(data_dismiss = 'alert') + "".html_safe end def trunc(text, length = 32) @@ -307,4 +307,7 @@ def trunc(text, length = 32) content_tag(:span, truncate(text, :length => length), options).html_safe end + def modal_close(data_dismiss = 'modal', text = _('Close')) + button_tag(text, :class => 'btn btn-default', :data => { :dismiss => data_dismiss }) + end end diff --git a/app/models/concerns/fog_extensions/aws/server.rb b/app/models/concerns/fog_extensions/aws/server.rb index c52a6b14a877..b9b5e8890da1 100644 --- a/app/models/concerns/fog_extensions/aws/server.rb +++ b/app/models/concerns/fog_extensions/aws/server.rb @@ -26,7 +26,7 @@ def poweroff end def reset - poweroff && start + poweroff && start end def vm_description diff --git a/app/models/concerns/fog_extensions/libvirt/server.rb b/app/models/concerns/fog_extensions/libvirt/server.rb index fc66a3a3d721..7fbe992778da 100644 --- a/app/models/concerns/fog_extensions/libvirt/server.rb +++ b/app/models/concerns/fog_extensions/libvirt/server.rb @@ -25,8 +25,8 @@ def memory=(mem) end def reset - poweroff - start + # @TODO: change to poweroff && start upon fix for LibVirt on Fog gem. + service.vm_action(uuid, :reset) end def vm_description diff --git a/app/models/host/managed.rb b/app/models/host/managed.rb index c6ac9df7324d..e8cc3d4c2931 100644 --- a/app/models/host/managed.rb +++ b/app/models/host/managed.rb @@ -778,6 +778,41 @@ def validate_media? managed && pxe_build? && build? end + def available_template_kinds(provisioning = nil) + kinds = if provisioning == 'image' + cr = ComputeResource.find_by_id(self.compute_resource_id) + images = cr.try(:images) + if images.blank? + [TemplateKind.find('finish')] + else + uuid = self.compute_attributes.cr.image_param_name + image_kind = images.find_by_uuid(uuid).try(:user_data) ? 'user_data' : 'finish' + [TemplateKind.find(image_kind)] + end + else + TemplateKind.all + end + + kinds.map do |kind| + ConfigTemplate.find_template({ :kind => kind.name, + :operatingsystem_id => operatingsystem_id, + :hostgroup_id => hostgroup_id, + :environment_id => environment_id + }) + end.compact + end + + def render_template(template) + @host = self + unattended_render(template) + end + + def build_status + build_status = HostBuildStatus.new(self) + build_status.check_all_statuses + build_status + end + private def lookup_value_match diff --git a/app/services/foreman/access_permissions.rb b/app/services/foreman/access_permissions.rb index 131990bae9c6..dab4eeab52df 100644 --- a/app/services/foreman/access_permissions.rb +++ b/app/services/foreman/access_permissions.rb @@ -357,7 +357,7 @@ :"api/v2/hosts" => [:destroy], :"api/v2/interfaces" => [:destroy] } - map.permission :build_hosts, {:hosts => [:setBuild, :cancelBuild, :multiple_build, :submit_multiple_build], + map.permission :build_hosts, {:hosts => [:setBuild, :cancelBuild, :multiple_build, :submit_multiple_build, :review_before_build], :tasks => tasks_ajax_actions, :"api/v2/tasks" => [:index] } map.permission :power_hosts, {:hosts => [:power], diff --git a/app/services/host_build_status.rb b/app/services/host_build_status.rb new file mode 100644 index 000000000000..147f4305c129 --- /dev/null +++ b/app/services/host_build_status.rb @@ -0,0 +1,60 @@ +class HostBuildStatus + attr_reader :host, :state, :errors + delegate :available_template_kinds, :smart_proxies, :to => :host + VALIDATION_TYPES = [:host, :templates, :proxies] + + def initialize(host) + @host = host + @errors = {} + @state = true # default to true state + VALIDATION_TYPES.each {|type| @errors[type] = []} + end + + def check_all_statuses + host_status + templates_status + smart_proxies_status + end + + private + + def host_status + return if host.valid? + host.errors.full_messages.each do |error| + fail!(:host, error.to_s, host.to_label) + end + rescue => error + fail!(:host, _('Failed to validate %s: %s') % [host, error.to_s], host.to_label) + end + + def templates_status + fail!(:templates, _('No templates found for this host.')) if available_template_kinds.empty? + + available_template_kinds.each do |template| + begin + valid_template = host.render_template(template.template) + fail!(:templates, _('Template %s is empty.') % template.name, template.name) if valid_template.blank? + rescue => exception + fail!(:templates, (_('Failure parsing %s: %s.') % [template.name, exception]), template.name) + end + end + end + + def smart_proxies_status + fail!(:proxies, _('No smart proxies found.')) if smart_proxies.empty? + + smart_proxies.each do |proxy| + begin + errors = proxy.refresh.messages.any? + fail!(:proxies, _('Failure deploying via smart proxy %s: %s.') % [proxy, errors.to_sentence], proxy.id) if errors + rescue => error + fail!(:proxies, _('Error connecting to %s: %s.') % [proxy, error], proxy.id) + end + end + end + + def fail!(type, message, id = nil) + @state = false + @errors[type] << {:message => message, :edit_id => id} + end +end diff --git a/app/services/power_manager.rb b/app/services/power_manager.rb index 9f31dee94197..8174d15226a6 100644 --- a/app/services/power_manager.rb +++ b/app/services/power_manager.rb @@ -1,4 +1,4 @@ module PowerManager SUPPORTED_ACTIONS = [N_('start'), N_('stop'), N_('poweroff'), N_('reboot'), N_('reset'), N_('state'), - N_('on'), N_('off'), N_('soft'), N_('cycle'), N_('status')] + N_('on'), N_('off'), N_('soft'), N_('cycle'), N_('status'), N_('ready?')] end diff --git a/app/services/power_manager/bmc.rb b/app/services/power_manager/bmc.rb index b7ba65079d1e..61625ef69fa8 100644 --- a/app/services/power_manager/bmc.rb +++ b/app/services/power_manager/bmc.rb @@ -12,6 +12,10 @@ def initialize(opts = {}) end # end end + def ready? + status == 'on' + end + private attr_reader :proxy diff --git a/app/services/power_manager/virt.rb b/app/services/power_manager/virt.rb index 06d43fa7e91e..97338f72a04a 100644 --- a/app/services/power_manager/virt.rb +++ b/app/services/power_manager/virt.rb @@ -42,7 +42,8 @@ def action_map :stop => 'stop', :poweroff => 'poweroff', :reset => 'reset', - :state => 'state' + :state => 'state', + :ready? => 'ready?' } end end diff --git a/app/views/hosts/_build_review_status.html.erb b/app/views/hosts/_build_review_status.html.erb new file mode 100644 index 000000000000..2ad59c99b9c3 --- /dev/null +++ b/app/views/hosts/_build_review_status.html.erb @@ -0,0 +1,4 @@ +<%= build_error_link(type, error[:edit_id]) if error[:edit_id] %> +
  • + <%= icon_text('minus') %><%= error[:message] %> +
  • diff --git a/app/views/hosts/review_before_build.html.erb b/app/views/hosts/review_before_build.html.erb new file mode 100644 index 000000000000..2552fd4415a1 --- /dev/null +++ b/app/views/hosts/review_before_build.html.erb @@ -0,0 +1,36 @@ +<% unless @build.state %> +
    +

    + <%= icon_text('remove', _('The following errors may prevent a successful build:')) %> + <%= button_tag(icon_text('refresh'), :title => _('Check again'), :class => 'btn btn-default btn-sm pull-right', + :id => 'recheck_review', :data => { :url => review_before_build_host_path(:id => @host) }) %> +

    + + <%= link_to_if_authorized(_("Edit"), hash_for_edit_host_path(:id => @host.id).merge(:auth_object => @host.id), + :class => "btn btn-default btn-xs pull-right", :title => _('Edit Host')) unless @build.errors[:host].blank? %> + + <% HostBuildStatus::VALIDATION_TYPES.each do |type| %> + <% if @build.errors[type].any? %> +

    <%= _(type.to_s.capitalize) %>

    + + <% end %> + <% end %> +
    +<% end %> +<%= form_for :host, + :method => :put, + :url => hash_for_setBuild_host_path(:id => @host).merge(:auth_object => @host, :permission => 'build_hosts'), + :html => { :id => 'build_form' } do |form| +%> + <%= checkbox_f(form, :build, :help_text => _('Reboot now') % @host) if supports_power_and_running(@host) %> + +<% end %> + + diff --git a/app/views/hosts/show.html.erb b/app/views/hosts/show.html.erb index e29bcae0261e..b0aaee7eb365 100644 --- a/app/views/hosts/show.html.erb +++ b/app/views/hosts/show.html.erb @@ -1,4 +1,5 @@ -<% javascript 'charts' %> +<% stylesheet 'hosts' %> +<% javascript 'charts', 'hosts' %> <% title @host.to_label, icon(@host.os) + @host.to_label %> <%= host_title_actions(@host) %> @@ -92,3 +93,22 @@ + diff --git a/config/environments/production.rb b/config/environments/production.rb index 23de0857c622..42f747ba6886 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -83,6 +83,7 @@ ace/keybinding-emacs diff host_edit + hosts jquery.cookie host_checkbox nfs_visibility diff --git a/config/routes.rb b/config/routes.rb index c22558a9a585..af2413a06b94 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,7 +17,8 @@ get 'clone' get 'storeconfig_klasses' get 'externalNodes' - get 'setBuild' + get 'review_before_build' + put 'setBuild' get 'cancelBuild' get 'puppetrun' get 'pxe_config' diff --git a/test/functional/hosts_controller_test.rb b/test/functional/hosts_controller_test.rb index 83378ddc6914..866055ac3d42 100644 --- a/test/functional/hosts_controller_test.rb +++ b/test/functional/hosts_controller_test.rb @@ -107,28 +107,66 @@ def test_update_valid assert_template :text => @host.info.to_yaml end - test "when host is saved after setBuild, the flash should inform it" do - Host.any_instance.stubs(:setBuild).returns(true) - @request.env['HTTP_REFERER'] = hosts_path - - get :setBuild, {:id => @host.name}, set_session_user - assert_response :found - assert_redirected_to hosts_path - assert_not_nil flash[:notice] - assert flash[:notice] == "Enabled #{@host} for rebuild on next boot" - end - test "when host is not saved after setBuild, the flash should inform it" do Host.any_instance.stubs(:setBuild).returns(false) @request.env['HTTP_REFERER'] = hosts_path - get :setBuild, {:id => @host.name}, set_session_user + put :setBuild, {:id => @host.name}, set_session_user assert_response :found assert_redirected_to hosts_path assert_not_nil flash[:error] assert flash[:error] =~ /Failed to enable #{@host} for installation/ end + context "when host is saved after setBuild" do + setup do + @request.env['HTTP_REFERER'] = hosts_path + end + + teardown do + Host::Managed.any_instance.unstub(:setBuild) + @request.env['HTTP_REFERER'] = '' + end + + test "the flash should inform it" do + Host::Managed.any_instance.stubs(:setBuild).returns(true) + put :setBuild, {:id => @host.name}, set_session_user + assert_response :found + assert_redirected_to hosts_path + assert_not_nil flash[:notice] + assert flash[:notice] == "Enabled #{@host} for rebuild on next boot" + end + + test 'and reboot was requested, the flash should inform it' do + Host::Managed.any_instance.stubs(:setBuild).returns(true) + # Setup a power mockup + class PowerShmocker + def reset + true + end + end + Host::Managed.any_instance.stubs(:power).returns(PowerShmocker.new()) + + put :setBuild, {:id => @host.name, :host => {:build => '1'}}, set_session_user + assert_response :found + assert_redirected_to hosts_path + assert_not_nil flash[:notice] + assert_equal(flash[:notice], "Enabled #{@host} for reboot and rebuild") + end + + test 'and reboot requested and reboot failed, the flash should inform it' do + Host::Managed.any_instance.stubs(:setBuild).returns(true) + put :setBuild, {:id => @host.name, :host => {:build => '1'}}, set_session_user + assert_raise Foreman::Exception do + @host.power.reset + end + assert_response :found + assert_redirected_to hosts_path + assert_not_nil flash[:notice] + assert_equal(flash[:notice], "Enabled #{@host} for rebuild on next boot") + end + end + def test_clone ComputeResource.any_instance.stubs(:vm_compute_attributes_for).returns({}) get :clone, {:id => Host.first.name}, set_session_user @@ -788,6 +826,13 @@ class Host::Valid < Host::Base; end refute host.compute_resource_id end + test '#review_before_build' do + HostBuildStatus.any_instance.stubs(:host_status).returns(true) + xhr :get, :review_before_build, {:id => @host.name}, set_session_user + assert_response :success + assert_template 'review_before_build' + end + private def initialize_host User.current = users(:admin) diff --git a/test/integration/host_test.rb b/test/integration/host_test.rb index 10dd8fa72170..c0c2ad884c16 100644 --- a/test/integration/host_test.rb +++ b/test/integration/host_test.rb @@ -37,7 +37,7 @@ def setup assert page.has_link?("Metrics", :href => "#metrics") assert page.has_link?("Templates", :href => "#template") assert page.has_link?("Edit", :href => "/hosts/#{@host.fqdn}/edit") - assert page.has_link?("Build", :href => "/hosts/#{@host.fqdn}/setBuild") + assert page.has_link?("Build", :href => "/hosts/#{@host.fqdn}#review_before_build") assert page.has_link?("Run puppet", :href => "/hosts/#{@host.fqdn}/puppetrun") assert page.has_link?("Delete", :href => "/hosts/#{@host.fqdn}") end diff --git a/test/unit/host_build_status_test.rb b/test/unit/host_build_status_test.rb new file mode 100644 index 000000000000..6bef6786df44 --- /dev/null +++ b/test/unit/host_build_status_test.rb @@ -0,0 +1,32 @@ +require 'test_helper' + +class HostBuildStatusTest < ActiveSupport::TestCase + attr_reader :build + + setup do + User.current = users(:admin) + host = Host.new(:name => "myfullhost", :mac => "aabbecddeeff", :ip => "2.3.4.03", :ptable => ptables(:one), :medium => media(:one), + :domain => domains(:mydomain), :operatingsystem => operatingsystems(:redhat), :subnet => subnets(:one), :puppet_proxy => smart_proxies(:puppetmaster), + :subnet => subnets(:one), :architecture => architectures(:x86_64), :environment => environments(:production), :managed => true, + :owner_type => "User", :root_pass => "xybxa6JUkz63w") + @build = host.build_status + # bypass host.valid? + HostBuildStatus.any_instance.stubs(:host_status).returns(true) + end + + test "should be able to render a template" do + assert_blank build.errors[:templates] + end + + test "should fail rendering a template" do + host = FactoryGirl.create(:host, :managed) + kind = FactoryGirl.create(:template_kind) + config_template = FactoryGirl.create(:config_template, :template => "provision script <%= @foreman.server.status %>",:name => "My Failed Template", :template_kind => kind, :operatingsystem_ids => [host.operatingsystem_id], :environment_ids => [host.environment_id], :hostgroup_ids => [host.hostgroup_id] ) + @build = host.build_status + refute_empty @build.errors[:templates] + end + + test "should be able to refresh a smart proxy" do + assert_empty build.errors[:proxies] + end +end