fuel-astute/lib/fuel_deployment/node.rb

290 lines
9.0 KiB
Ruby

# Copyright 2015 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.
module Deployment
# The Node class should work with a deployed nodes
# it should be able to somehow run tasks on them and
# query their status. It should also manage it's status
# attribute and the status of the currently running task.
# A Node has a Graph object assigned and can run all methods
# of the Graph object.
#
# @attr [Symbol] status The node's status
# @attr [String] name The node's name
# @attr [Deployment::Task] task The currently running task of this node
# @attr [Deployment::Cluster] cluster The cluster this node is assigned to
# @attr [Deployment::Graph] graph The Graph assigned to this node
# @attr [Numeric, String] id Misc id that can be used by this node
# @attr [true, false] critical This node is critical for the deployment
# and the deployment is considered failed if this node is failed
class Node
# A node can have one of these statuses
ALLOWED_STATUSES = [:online, :busy, :offline, :failed, :successful, :skipped]
# A node is considered finished with one of these statuses
FINISHED_STATUSES = [:failed, :successful, :skipped]
# A node is considered failed with these statuses
FAILED_STATUSES = [:failed]
# @param [String, Symbol] name
# @param [Deployment::Cluster] cluster
# @param [Object] id
def initialize(name, cluster, id = nil)
@name = name
@status = :online
@task = nil
@critical = false
@uid = id || self.name
self.cluster = cluster
cluster.node_add self
create_new_graph
end
include Enumerable
include Deployment::Log
attr_reader :status
attr_reader :name
attr_reader :task
attr_reader :cluster
alias :current_task :task
attr_reader :graph
attr_accessor :uid
attr_reader :critical
alias :critical? :critical
attr_reader :sync_point
alias :sync_point? :sync_point
# Set a new status of this node
# @param [Symbol, String] value
# @raise [Deployment::InvalidArgument] if the status is not valid
# @return [Symbol]
def status=(value)
value = value.to_sym
raise Deployment::InvalidArgument.new self, 'Invalid node status!', value unless ALLOWED_STATUSES.include? value
status_changes_concurrency @status, value
@status = value
end
# Set the critical property of this node
# @param [true, false] value
# @return [true, false]
def critical=(value)
@critical = !!value
end
# Set this node to be a critical node
# @return [true]
def set_critical
debug "Setup #{self} as critical node"
self.critical = true
end
# Set this node to be a normal node
# @return [false]
def set_normal
debug "Setup #{self} as normal node"
self.critical = false
end
# Set this node as sync point node
# @return [true]
def set_as_sync_point
self.sync_point = true
end
# Set this node as normal point node
# @return [false]
def unset_as_sync_point
self.sync_point = false
end
# Set the sync point property of this node
# @param [true, false] value
# @return [true, false]
def sync_point=(value)
@sync_point = !!value
end
# Set this node's Cluster Object
# @param [Deployment::Cluster] cluster The new cluster object
# @raise [Deployment::InvalidArgument] if the object is not a Node
# @return [Deployment::Node]
def cluster=(cluster)
raise Deployment::InvalidArgument.new self, 'Not a cluster used instead of the cluster object!', cluster unless cluster.is_a? Deployment::Cluster
@cluster = cluster
end
# Check if this node has a Concurrency::Counter set
# and it has a defined maximum value
# @return [true,false]
def concurrency_present?
return false unless cluster.is_a? Deployment::Cluster
return false unless cluster.node_concurrency.is_a? Deployment::Concurrency::Counter
cluster.node_concurrency.maximum_set?
end
# Check if this node has a free concurrency slot to run a new task
# @return [true,false]
def concurrency_available?
return true unless concurrency_present?
cluster.node_concurrency.available?
end
# Increase or decrease the node concurrency value
# when the node's status is changed.
# @param [Symbol] status_from
# @param [Symbol] status_to
# @return [void]
def status_changes_concurrency(status_from, status_to)
return unless concurrency_present?
if status_to == :busy
cluster.node_concurrency.increment
debug "Increasing node concurrency to: #{cluster.node_concurrency.current}"
elsif status_from == :busy
cluster.node_concurrency.decrement
debug "Decreasing node concurrency to: #{cluster.node_concurrency.current}"
end
end
# The node have finished all its tasks
# or has one of finished statuses
# @return [true, false]
def finished?
FINISHED_STATUSES.include? status or tasks_are_finished?
end
# Check if this node is ready to receive a task: it's online and
# concurrency slots are available.
# @return [true, false]
def ready?
online? and concurrency_available?
end
# The node is online and can accept new tasks
# @return [true, false]
def online?
status == :online
end
# The node is busy running a task
# @return [true, false]
def busy?
status == :busy
end
# The node is offline and cannot accept tasks
# @return [true, false]
def offline?
status == :offline
end
# The node has several failed tasks
# or has the failed status
# @return [true, false]
def failed?
FAILED_STATUSES.include? status or tasks_have_failed?
end
# The node has all tasks successful
# or has the successful status
# @return [true, false]
def successful?
status == :successful or tasks_are_successful?
end
# The node is skipped and will not get any tasks
def skipped?
status == :skipped #or tasks_have_only_dep_failed?
end
ALLOWED_STATUSES.each do |status|
method_name = "set_status_#{status}".to_sym
define_method(method_name) do
self.status = status
end
end
# Set the new name of this node
# @param [String, Symbol] name
def name=(name)
@name = name.to_s
end
# Set the new current task of this node
# @param [Deployment::Task, nil] task
# @raise [Deployment::InvalidArgument] if the object is not a task or nil or the task is not in this graph
# @return [Deployment::Task]
def task=(task)
unless task.nil?
raise Deployment::InvalidArgument.new self, 'Task should be a task object or nil!', task unless task.is_a? Deployment::Task
raise Deployment::InvalidArgument.new self, 'Task is not found in the graph!', task unless graph.task_present? task
end
@task = task
end
alias :current_task= :task=
# Set a new graph object
# @param [Deployment::Graph] graph
# @return [Deployment::Graph]
def graph=(graph)
raise Deployment::InvalidArgument.new self, 'Graph should be a graph object!', graph unless graph.is_a? Deployment::Graph
graph.node = self
@graph = graph
end
# Create a new empty graph object for this node
# @return [Deployment::Graph]
def create_new_graph
self.graph = Deployment::Graph.new(self)
end
# @return [String]
def to_s
return "Node[#{uid}]" if uid == name
"Node[#{uid}/#{name}]"
end
# @return [String]
def inspect
message = "#{self}{Status: #{status}"
message += " Tasks: #{tasks_finished_count}/#{tasks_total_count}"
message += " CurrentTask: #{task.name}, task status: #{task.status}" if task
message + '}'
end
# Sends all unknown methods to the graph object
def method_missing(method, *args, &block)
graph.send method, *args, &block
end
# Run the task on this node
# @param [Deployment::Task] task
# @abstract Should be implemented in a subclass
def run(task)
debug "Run task: #{task}"
raise Deployment::NotImplemented, 'This method is abstract and should be implemented in a subclass'
end
# Polls the status of the node
# should update the node's status
# and the status of the current task
# @abstract Should be implemented in a subclass
def poll
raise Deployment::NotImplemented, 'This method is abstract and should be implemented in a subclass'
end
end
end