fuel-astute/lib/astute/reporter.rb

294 lines
11 KiB
Ruby

# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
require 'set'
STATES = {
'offline' => 0,
'discover' => 10,
'provisioning' => 30,
'provisioned' => 40,
'deploying' => 50,
'ready' => 60,
'error' => 70
}
module Astute
module ProxyReporter
class DeploymentProxyReporter
attr_accessor :deploy
alias_method :deploy?, :deploy
def initialize(up_reporter, deployment_info=[])
@up_reporter = up_reporter
@nodes = deployment_info.inject([]) do |nodes, di|
nodes << {'uid' => di['uid'], 'role' => di['role'], 'fail_if_error' => di['fail_if_error']}
end
@deploy = deployment_info.present?
end
def report(data)
Astute.logger.debug("Data received by DeploymentProxyReporter to report it up: #{data.inspect}")
report_new_data(data)
end
private
def report_new_data(data)
if data['nodes']
nodes_to_report = get_nodes_to_report(data['nodes'])
return if nodes_to_report.empty? # Let's report only if nodes updated
# Update nodes attributes in @nodes.
update_saved_nodes(nodes_to_report)
data['nodes'] = nodes_to_report
end
data.merge!(get_overall_status(data))
Astute.logger.debug("Data send by DeploymentProxyReporter to report it up: #{data.inspect}")
@up_reporter.report(data)
end
def get_overall_status(data)
status = data['status']
error_nodes = @nodes.select { |n| n['status'] == 'error' }.map{ |n| n['uid'] }
msg = data['error']
if status == 'ready' && error_nodes.any?
status = 'error'
msg = "Some error occured on nodes #{error_nodes.inspect}"
end
progress = data['progress']
{'status' => status, 'error' => msg, 'progress' => progress}.reject{|k,v| v.nil?}
end
def get_nodes_to_report(nodes)
nodes.map{ |node| node_validate(node) }.compact
end
def update_saved_nodes(new_nodes)
# Update nodes attributes in @nodes.
new_nodes.each do |node|
saved_node = @nodes.find { |n| n['uid'] == node['uid'] && n['role'] == node['role'] }
if saved_node
node.each {|k, v| saved_node[k] = v}
else
@nodes << node
end
end
end
def node_validate(node)
validates_basic_fields(node)
# Ignore hooks report for multiroles progress
calculate_multiroles_node_progress(node) if deploy? && node['role'] != 'hook'
normalization_progress(node)
compare_with_previous_state(node)
end
# Validate of basic fields in message about nodes
def validates_basic_fields(node)
err = []
case
when node['status']
err << "Status provided #{node['status']} is not supported" unless STATES[node['status']]
when node['progress']
err << "progress value provided, but no status"
end
err << "Node role is not provided" if deploy? && !node['role']
err << "Node uid is not provided" unless node['uid']
if err.any?
msg = "Validation of node: #{node.inspect} for report failed: #{err.join('; ')}."
Astute.logger.error(msg)
raise msg
end
end
# Proportionally reduce the progress on the number of roles. Based on the
# fact that each part makes the same contribution to the progress we divide
# 100 to number of roles for this node. Also we prevent send final status for
# node before all roles will be deployed. Final result for node:
# * any error — error;
# * without error — succes.
# Example:
# Node have 3 roles and already success deploy first role and now deploying
# second(50%). Overall progress of the operation for node is
# 50 / 3 + 1 * 100 / 3 = 49
# We calculate it as 100/3 = 33% for every finished(success or fail) role
# Exception: node which have fail_if_error status equal true for some
# assigned node role. If this node role fail, we send error state for
# the entire node immediately.
def calculate_multiroles_node_progress(node)
@finish_roles_for_nodes ||= []
roles_of_node = @nodes.select { |n| n['uid'] == node['uid'] }
all_roles_amount = roles_of_node.size
return if all_roles_amount == 1 # calculation should only be done for multi roles
finish_roles_amount = @finish_roles_for_nodes.select do |n|
n['uid'] == node['uid'] && ['ready', 'error'].include?(n['status'])
end.size
return if finish_roles_amount == all_roles_amount # already done all work
# recalculate progress for node
node['progress'] = node['progress'].to_i/all_roles_amount + 100 * finish_roles_amount/all_roles_amount
# save final state if present
if ['ready', 'error'].include? node['status']
@finish_roles_for_nodes << { 'uid' => node['uid'], 'role' => node['role'], 'status' => node['status'] }
node['progress'] = 100 * (finish_roles_amount + 1)/all_roles_amount
end
# No more status update will be for node which failed and have fail_if_error as true
fail_if_error = @nodes.find { |n| n['uid'] == node['uid'] && n['role'] == node['role'] }['fail_if_error']
fail_now = fail_if_error && node['status'] == 'error'
if all_roles_amount - finish_roles_amount != 1 && !fail_now
# block 'ready' or 'error' final status for node if not all roles yet deployed
node['status'] = 'deploying'
node.delete('error_type') # Additional field for error response
elsif ['ready', 'error'].include? node['status']
node['status'] = @finish_roles_for_nodes.select { |n| n['uid'] == node['uid'] }
.select { |n| n['status'] == 'error' }
.empty? ? 'ready' : 'error'
node['progress'] = 100
node['error_type'] = 'deploy' if node['status'] == 'error'
end
end
# Normalization of progress field: ensures that the scaling progress was
# in range from 0 to 100 and has a value of 100 fot the final node status
def normalization_progress(node)
if node['progress']
if node['progress'] > 100
Astute.logger.warn("Passed report for node with progress > 100: "\
"#{node.inspect}. Adjusting progress to 100.")
node['progress'] = 100
elsif node['progress'] < 0
Astute.logger.warn("Passed report for node with progress < 0: "\
"#{node.inspect}. Adjusting progress to 0.")
node['progress'] = 0
end
end
if node['status'] && ['provisioned', 'ready'].include?(node['status']) && node['progress'] != 100
Astute.logger.warn("In #{node['status']} state node should have progress 100, "\
"but node passed: #{node.inspect}. Setting it to 100")
node['progress'] = 100
end
end
# Comparison information about node with previous state.
def compare_with_previous_state(node)
saved_node = @nodes.find { |x| x['uid'] == node['uid'] && x['role'] == node['role'] }
if saved_node
saved_status = STATES[saved_node['status']].to_i
node_status = STATES[node['status']] || saved_status
saved_progress = saved_node['progress'].to_i
node_progress = node['progress'] || saved_progress
if node_status < saved_status
Astute.logger.warn("Attempt to assign lower status detected: "\
"Status was: #{saved_node['status']}, attempted to "\
"assign: #{node['status']}. Skipping this node (id=#{node['uid']})")
return
end
if node_progress < saved_progress && node_status == saved_status
Astute.logger.warn("Attempt to assign lesser progress detected: "\
"Progress was: #{saved_node['status']}, attempted to "\
"assign: #{node['progress']}. Skipping this node (id=#{node['uid']})")
return
end
# We need to update node here only if progress is greater, or status changed
return if node.select{|k, v| saved_node[k] != v }.empty?
end
node
end
end # DeploymentProxyReporter
class ProvisiningProxyReporter < DeploymentProxyReporter
def initialize(up_reporter, provisioning_info=[])
@up_reporter = up_reporter
@nodes = provisioning_info['nodes'].inject([]) do |nodes, di|
nodes << {'uid' => di['uid']}
end
end
def report(data)
Astute.logger.debug("Data received by ProvisiningProxyReporter to report it up: #{data.inspect}")
report_new_data(data)
end
private
def report_new_data(data)
if data['nodes']
nodes_to_report = get_nodes_to_report(data['nodes'])
return if nodes_to_report.empty? # Let's report only if nodes updated
# Update nodes attributes in @nodes.
update_saved_nodes(nodes_to_report)
data['nodes'] = nodes_to_report
end
Astute.logger.debug("Data send by DeploymentProxyReporter to report it up: #{data.inspect}")
@up_reporter.report(data)
end
def node_validate(node)
validates_basic_fields(node)
normalization_progress(node)
compare_with_previous_state(node)
end
# Comparison information about node with previous state.
def compare_with_previous_state(node)
saved_node = @nodes.find { |x| x['uid'] == node['uid'] }
if saved_node
saved_status = STATES[saved_node['status']].to_i
node_status = STATES[node['status']] || saved_status
saved_progress = saved_node['progress'].to_i
node_progress = node['progress'] || saved_progress
if node_status < saved_status
Astute.logger.warn("Attempt to assign lower status detected: "\
"Status was: #{saved_node['status']}, attempted to "\
"assign: #{node['status']}. Skipping this node (id=#{node['uid']})")
return
end
if node_progress < saved_progress && node_status == saved_status
Astute.logger.warn("Attempt to assign lesser progress detected: "\
"Progress was: #{saved_node['status']}, attempted to "\
"assign: #{node['progress']}. Skipping this node (id=#{node['uid']})")
return
end
# We need to update node here only if progress is greater, or status changed
return if node.select{|k, v| saved_node[k] != v }.empty?
end
node
end
end # ProvisiningProxyReporter
end # ProxyReporter
end # Astute