294 lines
11 KiB
Ruby
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
|