Task base deployment
* async task: puppet, shell, cobbler, sync, noop and reboot; * not async: upload file, upload files, copy files; * new report format based on task progress instead of log; * support union graph deployment: pre/post tasks included in main graph; * plugins tasks also included in main graph; * support previous failt tollerance strategy (percent of failed nodes). * add tests. Implements: blueprint task-based-deployment-astute Closes-Bug: #1506962 Closes-Bug: #1430470 Change-Id: Ibe24d047ab502b88ea0ae9f8c77f94440000d289
This commit is contained in:
parent
e8c753d6ce
commit
583d68b1cb
@ -45,14 +45,18 @@ require 'astute/task_manager'
|
|||||||
require 'astute/pre_delete'
|
require 'astute/pre_delete'
|
||||||
require 'astute/version'
|
require 'astute/version'
|
||||||
require 'astute/server/async_logger'
|
require 'astute/server/async_logger'
|
||||||
|
require 'astute/task'
|
||||||
|
require 'astute/task_deployment'
|
||||||
|
require 'astute/task_node'
|
||||||
|
require 'fuel_deployment'
|
||||||
|
|
||||||
['/astute/pre_deployment_actions/*.rb',
|
['/astute/pre_deployment_actions/*.rb',
|
||||||
'/astute/pre_deploy_actions/*.rb',
|
'/astute/pre_deploy_actions/*.rb',
|
||||||
'/astute/pre_node_actions/*.rb',
|
'/astute/pre_node_actions/*.rb',
|
||||||
'/astute/post_deploy_actions/*.rb',
|
'/astute/post_deploy_actions/*.rb',
|
||||||
'/astute/post_deployment_actions/*.rb',
|
'/astute/post_deployment_actions/*.rb',
|
||||||
'/astute/common_actions/*.rb'
|
'/astute/common_actions/*.rb',
|
||||||
|
'/astute/tasks/*.rb'
|
||||||
].each do |path|
|
].each do |path|
|
||||||
Dir[File.dirname(__FILE__) + path].each{ |f| require f }
|
Dir[File.dirname(__FILE__) + path].each{ |f| require f }
|
||||||
end
|
end
|
||||||
|
@ -59,6 +59,7 @@ module Astute
|
|||||||
conf[:puppet_deploy_interval] = 2 # sleep for ## sec, then check puppet status again
|
conf[:puppet_deploy_interval] = 2 # sleep for ## sec, then check puppet status again
|
||||||
conf[:puppet_fade_timeout] = 120 # how long it can take for puppet to exit after dumping to last_run_summary
|
conf[:puppet_fade_timeout] = 120 # how long it can take for puppet to exit after dumping to last_run_summary
|
||||||
conf[:puppet_retries] = 2 # how many times astute will try to run puppet
|
conf[:puppet_retries] = 2 # how many times astute will try to run puppet
|
||||||
|
conf[:puppet_module_path] = '/etc/puppet/modules' # where we should find basic modules for puppet
|
||||||
conf[:mc_retries] = 10 # MClient tries to call mcagent before failure
|
conf[:mc_retries] = 10 # MClient tries to call mcagent before failure
|
||||||
conf[:mc_retry_interval] = 1 # MClient sleeps for ## sec between retries
|
conf[:mc_retry_interval] = 1 # MClient sleeps for ## sec between retries
|
||||||
conf[:puppet_fade_interval] = 30 # retry every ## seconds to check puppet state if it was running
|
conf[:puppet_fade_interval] = 30 # retry every ## seconds to check puppet state if it was running
|
||||||
@ -66,7 +67,9 @@ module Astute
|
|||||||
conf[:reboot_timeout] = 240 # how long it can take for node to reboot
|
conf[:reboot_timeout] = 240 # how long it can take for node to reboot
|
||||||
conf[:dump_timeout] = 3600 # maximum time it waits for the dump (meaningles to be larger
|
conf[:dump_timeout] = 3600 # maximum time it waits for the dump (meaningles to be larger
|
||||||
# than the specified in timeout of execute_shell_command mcagent
|
# than the specified in timeout of execute_shell_command mcagent
|
||||||
|
conf[:shell_timeout] = 300 # default timeout for shell task
|
||||||
|
conf[:shell_cwd] = '/' # default cwd for shell task
|
||||||
|
conf[:rsync_options] = '-c -r --delete' # default rsync options
|
||||||
conf[:keys_src_dir] = '/var/lib/fuel/keys' # path where ssh and openssl keys will be created
|
conf[:keys_src_dir] = '/var/lib/fuel/keys' # path where ssh and openssl keys will be created
|
||||||
conf[:puppet_ssh_keys] = [
|
conf[:puppet_ssh_keys] = [
|
||||||
'neutron',
|
'neutron',
|
||||||
@ -96,6 +99,8 @@ module Astute
|
|||||||
# Please increase if nodes could not provisioning
|
# Please increase if nodes could not provisioning
|
||||||
conf[:agent_nodiscover_file] = '/etc/nailgun-agent/nodiscover' # if this file in place, nailgun-agent will do nothing
|
conf[:agent_nodiscover_file] = '/etc/nailgun-agent/nodiscover' # if this file in place, nailgun-agent will do nothing
|
||||||
conf[:bootstrap_profile] = 'bootstrap' # use the CentOS based bootstrap by default
|
conf[:bootstrap_profile] = 'bootstrap' # use the CentOS based bootstrap by default
|
||||||
|
conf[:graph_dot_dir] = "/tmp/" # default dir patch for debug graph file
|
||||||
|
conf[:enable_graph_file] = false # enable debug graph records to file
|
||||||
|
|
||||||
# Server settings
|
# Server settings
|
||||||
conf[:broker_host] = 'localhost'
|
conf[:broker_host] = 'localhost'
|
||||||
|
@ -35,5 +35,9 @@ module Astute
|
|||||||
reporter.report(data)
|
reporter.report(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def report(msg)
|
||||||
|
@reporter.report msg
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -131,7 +131,21 @@ module Astute
|
|||||||
super
|
super
|
||||||
@actions = [
|
@actions = [
|
||||||
EnablePuppetDeploy.new,
|
EnablePuppetDeploy.new,
|
||||||
UploadFacts.new
|
UploadFacts.new,
|
||||||
|
InitialConnectFacts.new
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
class TaskPreDeploymentActions < DeployActions
|
||||||
|
|
||||||
|
def initialize(deployment_info, context)
|
||||||
|
super
|
||||||
|
@actions = [
|
||||||
|
EnablePuppetDeploy.new,
|
||||||
|
UploadFacts.new,
|
||||||
|
InitialConnectFacts.new
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -28,5 +28,7 @@ module Astute
|
|||||||
class MClientError < AstuteError; end
|
class MClientError < AstuteError; end
|
||||||
# MClient timeout error
|
# MClient timeout error
|
||||||
class MClientTimeout < Timeout::Error; end
|
class MClientTimeout < Timeout::Error; end
|
||||||
|
# Task validation error
|
||||||
|
class TaskValidationError < AstuteError; end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -91,7 +91,9 @@ module Astute
|
|||||||
if @on_respond_timeout
|
if @on_respond_timeout
|
||||||
@on_respond_timeout.call not_responded
|
@on_respond_timeout.call not_responded
|
||||||
else
|
else
|
||||||
err_msg += "MCollective agents '#{not_responded.join(',')}' didn't respond within the allotted time.\n"
|
err_msg += "MCollective agents '#{@agent}' " \
|
||||||
|
"'#{not_responded.join(',')}' didn't respond within the " \
|
||||||
|
"allotted time.\n"
|
||||||
timeout_nodes_count += not_responded.size
|
timeout_nodes_count += not_responded.size
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -30,6 +30,9 @@ module Astute
|
|||||||
report_result({}, up_reporter)
|
report_result({}, up_reporter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Deprecated deploy method. Use monolitic site.pp. Do not use from 7.1.
|
||||||
|
# Report progress based on puppet logs
|
||||||
def deploy(up_reporter, task_id, deployment_info, pre_deployment=[], post_deployment=[])
|
def deploy(up_reporter, task_id, deployment_info, pre_deployment=[], post_deployment=[])
|
||||||
deploy_cluster(
|
deploy_cluster(
|
||||||
up_reporter,
|
up_reporter,
|
||||||
@ -41,6 +44,9 @@ module Astute
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Deploy method which use small tasks, but run block of tasks for role
|
||||||
|
# instead of run it using full graph. Use from 7.1 to 8.0. Report progress
|
||||||
|
# based on puppet logs
|
||||||
def granular_deploy(up_reporter, task_id, deployment_info, pre_deployment=[], post_deployment=[])
|
def granular_deploy(up_reporter, task_id, deployment_info, pre_deployment=[], post_deployment=[])
|
||||||
deploy_cluster(
|
deploy_cluster(
|
||||||
up_reporter,
|
up_reporter,
|
||||||
@ -52,6 +58,16 @@ module Astute
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Deploy method which use small tasks in full graph.
|
||||||
|
# Use from 8.0 (experimental). Report progress based on tasks
|
||||||
|
def task_deploy(up_reporter, task_id, deployment_info, deployment_tasks)
|
||||||
|
context = Context.new(task_id, up_reporter)
|
||||||
|
Astute.logger.info "Task based deployment will be used"
|
||||||
|
|
||||||
|
deployment_engine = TaskDeployment.new(context)
|
||||||
|
deployment_engine.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
|
||||||
def provision(up_reporter, task_id, provisioning_info, provision_method)
|
def provision(up_reporter, task_id, provisioning_info, provision_method)
|
||||||
proxy_reporter = ProxyReporter::ProvisiningProxyReporter.new(
|
proxy_reporter = ProxyReporter::ProvisiningProxyReporter.new(
|
||||||
up_reporter,
|
up_reporter,
|
||||||
|
@ -61,8 +61,8 @@ module Astute
|
|||||||
|
|
||||||
# ready, error or deploying
|
# ready, error or deploying
|
||||||
result.fetch('status', 'deploying')
|
result.fetch('status', 'deploying')
|
||||||
rescue MClientTimeout
|
rescue MClientTimeout, Timeout::Error
|
||||||
Astute.logger.warn "MCollective agents #{@node['uid']} " \
|
Astute.logger.warn "Puppet agent #{@node['uid']} " \
|
||||||
"didn't respond within the allotted time"
|
"didn't respond within the allotted time"
|
||||||
'error'
|
'error'
|
||||||
end
|
end
|
||||||
|
@ -104,6 +104,25 @@ module Astute
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def task_deploy(data)
|
||||||
|
Astute.logger.info("'task_deploy' method called with data:\n"\
|
||||||
|
"#{data.pretty_inspect}")
|
||||||
|
|
||||||
|
reporter = create_reporter(data)
|
||||||
|
begin
|
||||||
|
@orchestrator.task_deploy(
|
||||||
|
reporter,
|
||||||
|
data['args']['task_uuid'],
|
||||||
|
data['args']['deployment_info'],
|
||||||
|
data['args']['deployment_tasks']
|
||||||
|
)
|
||||||
|
rescue Timeout::Error
|
||||||
|
msg = "Timeout of deployment is exceeded."
|
||||||
|
Astute.logger.error(msg)
|
||||||
|
reporter.report('status' => 'error', 'error' => msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def verify_networks(data)
|
def verify_networks(data)
|
||||||
data.fetch('subtasks', []).each do |subtask|
|
data.fetch('subtasks', []).each do |subtask|
|
||||||
if self.respond_to?(subtask['method'])
|
if self.respond_to?(subtask['method'])
|
||||||
|
205
lib/astute/task.rb
Normal file
205
lib/astute/task.rb
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class Task
|
||||||
|
|
||||||
|
ALLOWED_STATUSES = [:successful, :failed, :running, :pending]
|
||||||
|
|
||||||
|
def initialize(task, context)
|
||||||
|
# WARNING: this code expect that only one node will be send
|
||||||
|
# on one hook.
|
||||||
|
@task = task
|
||||||
|
@status = :pending
|
||||||
|
@ctx = context
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run current task on node, specified in task
|
||||||
|
def run
|
||||||
|
validation
|
||||||
|
setup_default
|
||||||
|
running!
|
||||||
|
process
|
||||||
|
rescue => e
|
||||||
|
Astute.logger.warn("Fail to run task #{@task['type']} #{task_name}" \
|
||||||
|
" with error #{e.message} trace: #{e.format_backtrace}")
|
||||||
|
failed!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Polls the status of the task
|
||||||
|
def status
|
||||||
|
calculate_status unless finished?
|
||||||
|
@status
|
||||||
|
rescue => e
|
||||||
|
Astute.logger.warn("Fail to detect status of the task #{@task['type']}" \
|
||||||
|
" #{task_name} with error #{e.message} trace: #{e.format_backtrace}")
|
||||||
|
failed!
|
||||||
|
end
|
||||||
|
|
||||||
|
def status=(value)
|
||||||
|
value = value.to_sym
|
||||||
|
unless ALLOWED_STATUSES.include?(value)
|
||||||
|
raise AstuteError::InvalidArgument,
|
||||||
|
"#{self}: Invalid task status: #{value}"
|
||||||
|
end
|
||||||
|
@status = value
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Run current task on node, specified in task
|
||||||
|
# should be fast and async and do not raise exceptions
|
||||||
|
# @abstract Should be implemented in a subclass
|
||||||
|
def process
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Polls the status of the task
|
||||||
|
# should update the task status and do not raise exceptions
|
||||||
|
# @abstract Should be implemented in a subclass
|
||||||
|
def calculate_status
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_presence(data, key)
|
||||||
|
raise TaskValidationError,
|
||||||
|
"Missing a required parameter #{key}" unless data[key].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pre validation of the task
|
||||||
|
# should check task and raise error if something went wrong
|
||||||
|
# @raise [TaskValidationError] if the object is not a task or has missing fields
|
||||||
|
def validation
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# Setup default value for hook
|
||||||
|
# should not raise any exception
|
||||||
|
def setup_default
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run short shell commands
|
||||||
|
# should use only in case of short run command
|
||||||
|
# In other case please use shell task
|
||||||
|
# Synchronous (blocking) call
|
||||||
|
def run_shell_without_check(node_uid, cmd, timeout=2)
|
||||||
|
shell = MClient.new(
|
||||||
|
@ctx,
|
||||||
|
'execute_shell_command',
|
||||||
|
Array(node_uid),
|
||||||
|
check_result=false,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
results = shell.execute(:cmd => cmd)
|
||||||
|
#FIXME(vsharshov): remove after find problem with
|
||||||
|
# undefined method `results' for nil:NilClas
|
||||||
|
Astute.logger.debug("Mcollective shell result: #{results}")
|
||||||
|
if results
|
||||||
|
result = results.first
|
||||||
|
Astute.logger.debug(
|
||||||
|
"#{@ctx.task_id}: cmd: #{cmd}\n" \
|
||||||
|
"stdout: #{result.results[:data][:stdout]}\n" \
|
||||||
|
"stderr: #{result.results[:data][:stderr]}\n" \
|
||||||
|
"exit code: #{result.results[:data][:exit_code]}")
|
||||||
|
{
|
||||||
|
:stdout =>result.results[:data][:stdout].chomp,
|
||||||
|
:stderr => result.results[:data][:stderr].chomp,
|
||||||
|
:exit_code => result.results[:data][:exit_code]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Astute.logger.warn("#{@ctx.task_id}: Failed to run shell #{cmd} on " \
|
||||||
|
"node #{node_uid}. Error will not raise because shell was run " \
|
||||||
|
"without check")
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Create file with content on selected node
|
||||||
|
# should use only for small file
|
||||||
|
# In other case please use separate thread or
|
||||||
|
# use upload file task.
|
||||||
|
# Synchronous (blocking) call
|
||||||
|
def upload_file(node_uid, mco_params={})
|
||||||
|
upload_mclient = Astute::MClient.new(
|
||||||
|
@ctx,
|
||||||
|
"uploadfile",
|
||||||
|
Array(node_uid)
|
||||||
|
)
|
||||||
|
|
||||||
|
mco_params['overwrite'] = true if mco_params['overwrite'].nil?
|
||||||
|
mco_params['parents'] = true if mco_params['parents'].nil?
|
||||||
|
mco_params['permissions'] ||= '0644'
|
||||||
|
mco_params['user_owner'] ||= 'root'
|
||||||
|
mco_params['group_owner'] ||= 'root'
|
||||||
|
mco_params['dir_permissions'] ||= '0755'
|
||||||
|
|
||||||
|
upload_mclient.upload(
|
||||||
|
:path => mco_params['path'],
|
||||||
|
:content => mco_params['content'],
|
||||||
|
:overwrite => mco_params['overwrite'],
|
||||||
|
:parents => mco_params['parents'],
|
||||||
|
:permissions => mco_params['permissions'],
|
||||||
|
:user_owner => mco_params['user_owner'],
|
||||||
|
:group_owner => mco_params['group_owner'],
|
||||||
|
:dir_permissions => mco_params['dir_permissions']
|
||||||
|
)
|
||||||
|
Astute.logger.debug("#{@ctx.task_id}: file was uploaded " \
|
||||||
|
"#{mco_params['path']} on node #{node_uid} successfully")
|
||||||
|
true
|
||||||
|
rescue MClientTimeout, MClientError => e
|
||||||
|
Astute.logger.error("#{@ctx.task_id}: file was not uploaded "\
|
||||||
|
"#{mco_params['path']} on node #{node_uid}: #{e.message}")
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def finished?
|
||||||
|
[:successful, :failed].include? @status
|
||||||
|
end
|
||||||
|
|
||||||
|
def failed!
|
||||||
|
self.status = :failed
|
||||||
|
end
|
||||||
|
|
||||||
|
def failed?
|
||||||
|
@status == :failed
|
||||||
|
end
|
||||||
|
|
||||||
|
def running!
|
||||||
|
self.status = :running
|
||||||
|
end
|
||||||
|
|
||||||
|
def running?
|
||||||
|
@status == :running
|
||||||
|
end
|
||||||
|
|
||||||
|
def succeed!
|
||||||
|
self.status = :successful
|
||||||
|
end
|
||||||
|
|
||||||
|
def successful?
|
||||||
|
@status == :successful
|
||||||
|
end
|
||||||
|
|
||||||
|
def pending?
|
||||||
|
@status == :pending
|
||||||
|
end
|
||||||
|
|
||||||
|
def task_name
|
||||||
|
@task['id'] || @task['diagnostic_name']
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
246
lib/astute/task_deployment.rb
Normal file
246
lib/astute/task_deployment.rb
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# 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 'fuel_deployment'
|
||||||
|
|
||||||
|
module Astute
|
||||||
|
class TaskDeployment
|
||||||
|
|
||||||
|
def initialize(context)
|
||||||
|
@ctx = context
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy(deployment_info, deployment_tasks)
|
||||||
|
raise DeploymentEngineError, "Deployment info are not provided!" if
|
||||||
|
deployment_info.blank? || deployment_tasks.blank?
|
||||||
|
|
||||||
|
deployment_info, offline_uids = remove_failed_nodes(deployment_info)
|
||||||
|
Astute::TaskPreDeploymentActions.new(deployment_info, @ctx).process
|
||||||
|
|
||||||
|
deployment_tasks = support_virtual_node(deployment_tasks)
|
||||||
|
|
||||||
|
nodes = {}
|
||||||
|
deployment_tasks.keys.each do |node_id|
|
||||||
|
node = TaskNode.new(node_id)
|
||||||
|
node.context = @ctx
|
||||||
|
node.set_critical if critical_node_uids(deployment_info).include?(node_id)
|
||||||
|
node.set_status_failed if offline_uids.include? node_id
|
||||||
|
nodes[node_id] = node
|
||||||
|
end
|
||||||
|
|
||||||
|
deployment_tasks.each do |node_id, tasks|
|
||||||
|
tasks.each do |task|
|
||||||
|
nodes[node_id].graph.create_task(
|
||||||
|
task['id'],
|
||||||
|
task.merge({'node_id' => node_id})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
deployment_tasks.each do |node_id, tasks|
|
||||||
|
tasks.each do |task|
|
||||||
|
task['requires'].each do |d_t|
|
||||||
|
nodes[node_id][task['id']].depends nodes[d_t['node_id']][d_t['name']]
|
||||||
|
end
|
||||||
|
|
||||||
|
task['required_for'].each do |d_t|
|
||||||
|
nodes[node_id][task['id']].depended_on nodes[d_t['node_id']][d_t['name']]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
deployment = Deployment::Process.new(nodes.values)
|
||||||
|
write_graph_to_file(deployment)
|
||||||
|
result = deployment.run
|
||||||
|
report_deploy_result(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def report_deploy_result(result)
|
||||||
|
if result[:success]
|
||||||
|
@ctx.report('status' => 'ready', 'progress' => 100)
|
||||||
|
else
|
||||||
|
result[:failed_nodes].each do |node|
|
||||||
|
report_status = {
|
||||||
|
'uid' => node.id,
|
||||||
|
'status' => 'error',
|
||||||
|
'error_type' => 'deploy',
|
||||||
|
'error_msg' => result[:status]
|
||||||
|
}
|
||||||
|
task = result[:failed_tasks].find{ |t| t.node == node }
|
||||||
|
if task
|
||||||
|
report_status.merge!({
|
||||||
|
'task' => task.name,
|
||||||
|
'task_status' => task.status.to_s
|
||||||
|
})
|
||||||
|
end
|
||||||
|
@ctx.report(report_status)
|
||||||
|
end
|
||||||
|
@ctx.report(
|
||||||
|
'status' => 'error',
|
||||||
|
'progress' => 100,
|
||||||
|
'error' => result[:status]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def write_graph_to_file(deployment)
|
||||||
|
return unless Astute.config.enable_graph_file
|
||||||
|
graph_file = File.join(
|
||||||
|
Astute.config.graph_dot_dir,
|
||||||
|
"graph-#{@ctx.task_id}.dot"
|
||||||
|
)
|
||||||
|
File.open(graph_file, 'w') { |f| f.write(deployment.to_dot) }
|
||||||
|
Astute.logger.info("Check graph into file #{graph_file}")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Astute use special virtual node for deployment tasks, because
|
||||||
|
# any task must be connected to node. For task, which play
|
||||||
|
# synchronization role, we create virtual_sync_node
|
||||||
|
def support_virtual_node(deployment_tasks)
|
||||||
|
deployment_tasks['virtual_sync_node'] = deployment_tasks['null']
|
||||||
|
deployment_tasks.delete('null')
|
||||||
|
|
||||||
|
deployment_tasks.each do |node_id, tasks|
|
||||||
|
tasks.each do |task|
|
||||||
|
task['requires'].each do |d_t|
|
||||||
|
d_t['node_id'] = 'virtual_sync_node' if d_t['node_id'].nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
task['required_for'].each do |d_t|
|
||||||
|
d_t['node_id'] = 'virtual_sync_node' if d_t['node_id'].nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
deployment_tasks
|
||||||
|
end
|
||||||
|
|
||||||
|
def critical_node_uids(deployment_info)
|
||||||
|
@critcial_nodes ||= deployment_info.select{ |n| n['fail_if_error'] }
|
||||||
|
.map{ |n| n['uid'] }.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
# Removes nodes which failed to provision
|
||||||
|
def remove_failed_nodes(deployment_info)
|
||||||
|
uids = get_uids_from_deployment_info deployment_info
|
||||||
|
required_uids = critical_node_uids(deployment_info)
|
||||||
|
|
||||||
|
available_uids = detect_available_nodes(uids)
|
||||||
|
offline_uids = uids - available_uids
|
||||||
|
if offline_uids.present?
|
||||||
|
# set status for all failed nodes to error
|
||||||
|
nodes = (uids - available_uids).map do |uid|
|
||||||
|
{'uid' => uid,
|
||||||
|
'status' => 'error',
|
||||||
|
'error_type' => 'provision',
|
||||||
|
'error_msg' => 'Node is not ready for deployment: '\
|
||||||
|
'mcollective has not answered'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@ctx.report_and_update_status(
|
||||||
|
'nodes' => nodes,
|
||||||
|
'error' => 'Node is not ready for deployment'
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if all required nodes are online
|
||||||
|
# if not, raise error
|
||||||
|
missing_required = required_uids - available_uids
|
||||||
|
if missing_required.present?
|
||||||
|
error_message = "Critical nodes are not available for deployment: " \
|
||||||
|
"#{missing_required}"
|
||||||
|
raise Astute::DeploymentEngineError, error_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return remove_offline_nodes(
|
||||||
|
uids,
|
||||||
|
available_uids,
|
||||||
|
deployment_info,
|
||||||
|
offline_uids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_offline_nodes(uids, available_uids, deployment_info, offline_uids)
|
||||||
|
if offline_uids.blank?
|
||||||
|
return [deployment_info, offline_uids]
|
||||||
|
end
|
||||||
|
|
||||||
|
Astute.logger.info "Removing nodes which failed to provision: " \
|
||||||
|
"#{offline_uids}"
|
||||||
|
deployment_info = cleanup_nodes_block(deployment_info, offline_uids)
|
||||||
|
deployment_info = deployment_info.select do |node|
|
||||||
|
available_uids.include? node['uid']
|
||||||
|
end
|
||||||
|
|
||||||
|
[deployment_info, offline_uids]
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup_nodes_block(deployment_info, offline_uids)
|
||||||
|
return deployment_info if offline_uids.blank?
|
||||||
|
|
||||||
|
nodes = deployment_info.first['nodes']
|
||||||
|
|
||||||
|
# In case of deploy in already existing cluster in nodes block
|
||||||
|
# we will have all cluster nodes. We should remove only missing
|
||||||
|
# nodes instead of stay only avaliable.
|
||||||
|
# Example: deploy 3 nodes, after it deploy 2 nodes.
|
||||||
|
# In 1 of 2 seconds nodes missing, in nodes block we should
|
||||||
|
# contain only 4 nodes.
|
||||||
|
nodes_wthout_missing = nodes.select do |node|
|
||||||
|
!offline_uids.include?(node['uid'])
|
||||||
|
end
|
||||||
|
deployment_info.each { |node| node['nodes'] = nodes_wthout_missing }
|
||||||
|
deployment_info
|
||||||
|
end
|
||||||
|
|
||||||
|
def detect_available_nodes(uids)
|
||||||
|
all_uids = uids.clone
|
||||||
|
available_uids = []
|
||||||
|
|
||||||
|
# In case of big amount of nodes we should do several calls to be sure
|
||||||
|
# about node status
|
||||||
|
Astute.config[:mc_retries].times.each do
|
||||||
|
systemtype = Astute::MClient.new(
|
||||||
|
@ctx,
|
||||||
|
"systemtype",
|
||||||
|
all_uids,
|
||||||
|
check_result=false,
|
||||||
|
10
|
||||||
|
)
|
||||||
|
available_nodes = systemtype.get_type.select do |node|
|
||||||
|
node.results[:data][:node_type].chomp == "target"
|
||||||
|
end
|
||||||
|
|
||||||
|
available_uids += available_nodes.map { |node| node.results[:sender] }
|
||||||
|
all_uids -= available_uids
|
||||||
|
break if all_uids.empty?
|
||||||
|
|
||||||
|
sleep Astute.config[:mc_retry_interval]
|
||||||
|
end
|
||||||
|
|
||||||
|
available_uids
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_uids_from_deployment_info(deployment_info)
|
||||||
|
top_level_uids = deployment_info.map{ |node| node["uid"] }
|
||||||
|
|
||||||
|
inside_uids = deployment_info.inject([]) do |uids, node|
|
||||||
|
uids += node.fetch('nodes', []).map{ |n| n['uid'] }
|
||||||
|
end
|
||||||
|
top_level_uids | inside_uids
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
95
lib/astute/task_node.rb
Normal file
95
lib/astute/task_node.rb
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
require 'fuel_deployment'
|
||||||
|
|
||||||
|
module Astute
|
||||||
|
class TaskNode < Deployment::Node
|
||||||
|
def context=(context)
|
||||||
|
@ctx = context
|
||||||
|
end
|
||||||
|
|
||||||
|
def run(inbox_task)
|
||||||
|
self.task = inbox_task
|
||||||
|
@task_engine = select_task_engine(task.data)
|
||||||
|
@task_engine.run
|
||||||
|
task.set_status_running
|
||||||
|
set_status_busy
|
||||||
|
end
|
||||||
|
|
||||||
|
def poll
|
||||||
|
return unless busy?
|
||||||
|
|
||||||
|
debug("Node #{id}: task #{task.name}, task status #{task.status}")
|
||||||
|
|
||||||
|
# Please be informed that this code define special method
|
||||||
|
# of Deployment::Node class. We use special method `task`
|
||||||
|
# to manage task status, graph of tasks and nodes.
|
||||||
|
task.status = @task_engine.status
|
||||||
|
if @task.running?
|
||||||
|
@ctx.report({
|
||||||
|
'uid' => id,
|
||||||
|
'status' => 'deploying',
|
||||||
|
'task' => task.name,
|
||||||
|
'progress' => current_progress_bar
|
||||||
|
})
|
||||||
|
else
|
||||||
|
set_status_online
|
||||||
|
|
||||||
|
deploy_status = if !finished?
|
||||||
|
'deploying'
|
||||||
|
elsif successful?
|
||||||
|
'ready'
|
||||||
|
else
|
||||||
|
'error'
|
||||||
|
end
|
||||||
|
|
||||||
|
report_status = {
|
||||||
|
'uid' => id,
|
||||||
|
'status' => deploy_status,
|
||||||
|
'task' => task.name,
|
||||||
|
'task_status' => task.status.to_s,
|
||||||
|
'progress' => current_progress_bar
|
||||||
|
}
|
||||||
|
report_status.merge!('error_type' => 'deploy') if
|
||||||
|
deploy_status == 'error'
|
||||||
|
@ctx.report(report_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def current_progress_bar
|
||||||
|
100 * tasks_finished_count / tasks_total_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_task_engine(data)
|
||||||
|
# TODO: replace by Object.const_get(type.split('_').collect(&:capitalize).join)
|
||||||
|
case data['type']
|
||||||
|
when 'shell' then Shell.new(data, @ctx)
|
||||||
|
when 'puppet' then Puppet.new(data, @ctx)
|
||||||
|
when 'upload_file' then UploadFile.new(data, @ctx)
|
||||||
|
when 'upload_files' then UploadFiles.new(data, @ctx)
|
||||||
|
when 'reboot' then Reboot.new(data, @ctx)
|
||||||
|
when 'sync' then Sync.new(data, @ctx)
|
||||||
|
when 'cobbler_sync' then CobblerSync.new(data, @ctx)
|
||||||
|
when 'copy_files' then CopyFiles.new(data, @ctx)
|
||||||
|
when 'noop' then Noop.new(data, @ctx)
|
||||||
|
when 'stage' then Noop.new(data, @ctx)
|
||||||
|
when 'skipped' then Noop.new(data, @ctx)
|
||||||
|
else raise TaskValidationError, "Unknown task type '#{data['type']}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
42
lib/astute/tasks/cobbler_sync.rb
Normal file
42
lib/astute/tasks/cobbler_sync.rb
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class CobblerSync < Task
|
||||||
|
|
||||||
|
def initialize(task, context)
|
||||||
|
super
|
||||||
|
@work_thread = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
cobbler = CobblerManager.new(
|
||||||
|
@task['parameters']['provisioning_info']['engine'],
|
||||||
|
@ctx.reporter
|
||||||
|
)
|
||||||
|
@work_thread = Thread.new { cobbler.sync }
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
@work_thread.join and succeed! unless @work_thread.alive?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task['parameters'], 'provisioning_info')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
59
lib/astute/tasks/copy_files.rb
Normal file
59
lib/astute/tasks/copy_files.rb
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class CopyFiles < Task
|
||||||
|
|
||||||
|
def initialize(task, context)
|
||||||
|
super
|
||||||
|
@work_thread = nil
|
||||||
|
@files_status = @task['parameters']['files'].inject({}) do |f_s, n|
|
||||||
|
f_s.merge({ n['src']+n['dst'] => :pending })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
@task['parameters']['files'].each do |file|
|
||||||
|
if File.file?(file['src']) && File.readable?(file['src'])
|
||||||
|
parameters = {
|
||||||
|
'content' => File.binread(file['src']),
|
||||||
|
'path' => file['dst'],
|
||||||
|
'permissions' => file['permissions'] || @task['parameters']['permissions'],
|
||||||
|
'dir_permissions' => file['dir_permissions'] || @task['parameters']['dir_permissions'],
|
||||||
|
}
|
||||||
|
@files_status[file['src']+file['dst']] =
|
||||||
|
upload_file(@task['node_id'], parameters)
|
||||||
|
else
|
||||||
|
@files_status[file['src']+file['dst']] = false
|
||||||
|
end
|
||||||
|
end # files
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
if @files_status.values.all?{ |s| s != :pending }
|
||||||
|
failed! if @files_status.values.include?(false)
|
||||||
|
succeed! if @files_status.values.all?{ |s| s == true }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task, 'node_id')
|
||||||
|
validate_presence(@task['parameters'], 'files')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
29
lib/astute/tasks/noop.rb
Normal file
29
lib/astute/tasks/noop.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class Noop < Task
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
succeed!
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
72
lib/astute/tasks/puppet.rb
Normal file
72
lib/astute/tasks/puppet.rb
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Copyright 2014 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 'timeout'
|
||||||
|
|
||||||
|
module Astute
|
||||||
|
class Puppet < Task
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
@puppet_task = create_puppet_task
|
||||||
|
@puppet_task.run
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
case @puppet_task.status
|
||||||
|
when 'ready' then succeed!
|
||||||
|
when 'error' then failed!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task, 'node_id')
|
||||||
|
validate_presence(@task['parameters'], 'puppet_manifest')
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_default
|
||||||
|
@task['parameters']['cwd'] ||= '/'
|
||||||
|
@task['parameters']['timeout'] ||= Astute.config.puppet_timeout
|
||||||
|
@task['parameters']['retries'] ||= Astute.config.puppet_retries
|
||||||
|
@task['parameters']['debug'] = false unless @task['parameters']['debug'].present?
|
||||||
|
@task['parameters']['puppet_modules'] ||= Astute.config.puppet_module_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_puppet_task
|
||||||
|
PuppetTask.new(
|
||||||
|
Context.new(
|
||||||
|
@ctx.task_id,
|
||||||
|
PuppetLoggerReporter.new,
|
||||||
|
LogParser::NoParsing.new
|
||||||
|
),
|
||||||
|
{'uid' => @task['node_id'].to_s, 'role' => task_name},
|
||||||
|
@task['parameters']['retries'],
|
||||||
|
@task['parameters']['puppet_manifest'],
|
||||||
|
@task['parameters']['puppet_modules'],
|
||||||
|
@task['parameters']['cwd'],
|
||||||
|
@task['parameters']['timeout'],
|
||||||
|
@task['parameters']['debug']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end # class
|
||||||
|
|
||||||
|
class PuppetLoggerReporter
|
||||||
|
def report(msg)
|
||||||
|
Astute.logger.debug msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
84
lib/astute/tasks/reboot.rb
Normal file
84
lib/astute/tasks/reboot.rb
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class Reboot < Task
|
||||||
|
|
||||||
|
def initialize(task, context)
|
||||||
|
super
|
||||||
|
@control_time = nil
|
||||||
|
@time_start = nil
|
||||||
|
@already_rebooted = false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
@control_time = boot_time
|
||||||
|
@time_start = Time.now.to_i
|
||||||
|
unless @control_time
|
||||||
|
failed!
|
||||||
|
Astute.logger.warn("#{@ctx.task_id}: #{task_name} failed because" \
|
||||||
|
"task could not get valid info about boot time")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
reboot
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
if Time.now.to_i - @time_start > @task['parameters']['timeout']
|
||||||
|
failed!
|
||||||
|
Astute.logger.warn("#{@ctx.task_id}: #{task_name} failed because" \
|
||||||
|
"reboot timeout #{@task['parameters']['timeout']} expired")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
current_bt = boot_time
|
||||||
|
succeed! if current_bt != @control_time && !current_bt.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task, 'node_id')
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_default
|
||||||
|
@task['parameters']['timeout'] ||= Astute.config.reboot_timeout
|
||||||
|
end
|
||||||
|
|
||||||
|
def reboot
|
||||||
|
run_shell_without_check(
|
||||||
|
Array(@task['node_id']),
|
||||||
|
'reboot',
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
rescue Astute::MClientTimeout, Astute::MClientError => e
|
||||||
|
Astute.logger.error("#{@ctx.task_id}: #{task_name} mcollective " \
|
||||||
|
"reboot command failed with error #{e.message}")
|
||||||
|
failed!
|
||||||
|
end
|
||||||
|
|
||||||
|
def boot_time
|
||||||
|
run_shell_without_check(
|
||||||
|
Array(@task['node_id']),
|
||||||
|
"stat --printf='%Y' /proc/1",
|
||||||
|
timeout=2
|
||||||
|
)[:stdout].to_i
|
||||||
|
rescue Astute::MClientTimeout, Astute::MClientError => e
|
||||||
|
Astute.logger.debug("#{@ctx.task_id}: #{task_name} mcollective " \
|
||||||
|
"boot time command failed with error #{e.message}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
137
lib/astute/tasks/shell.rb
Normal file
137
lib/astute/tasks/shell.rb
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# 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.
|
||||||
|
require 'erb'
|
||||||
|
|
||||||
|
module Astute
|
||||||
|
class Shell < Task
|
||||||
|
|
||||||
|
# Accept to run shell tasks using existing puppet asynchronous
|
||||||
|
# mechanism. It create and upload 2 files: shell script and
|
||||||
|
# puppet manifest. Then run puppet manifest
|
||||||
|
|
||||||
|
def initialize(task, context)
|
||||||
|
super
|
||||||
|
@puppet_task = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
SHELL_MANIFEST_DIR = '/etc/puppet/shell_manifests'
|
||||||
|
|
||||||
|
def process
|
||||||
|
run_shell_without_check(
|
||||||
|
@task['node_id'],
|
||||||
|
"mkdir -p #{SHELL_MANIFEST_DIR}",
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
upload_shell_manifest
|
||||||
|
@puppet_task = Puppet.new(
|
||||||
|
generate_puppet_hook,
|
||||||
|
@ctx
|
||||||
|
)
|
||||||
|
@puppet_task.run
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
self.status = @puppet_task.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task, 'node_id')
|
||||||
|
validate_presence(@task['parameters'], 'cmd')
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_default
|
||||||
|
@task['parameters']['timeout'] ||= Astute.config.shell_timeout
|
||||||
|
@task['parameters']['cwd'] ||= Astute.config.shell_cwd
|
||||||
|
@task['parameters']['retries'] ||= Astute.config.mc_retries
|
||||||
|
@task['parameters']['interval'] ||= Astute.config.mc_retry_interval
|
||||||
|
end
|
||||||
|
|
||||||
|
def puppet_exec_template
|
||||||
|
template = <<-eos
|
||||||
|
# Puppet manifest wrapper for task: <%= task_name %>
|
||||||
|
notice('MODULAR: <%= task_name %>')
|
||||||
|
|
||||||
|
exec { '<%= task_name %>_shell' :
|
||||||
|
path => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||||
|
command => '/bin/bash "<%= shell_exec_file_path %>"',
|
||||||
|
logoutput => true,
|
||||||
|
}
|
||||||
|
eos
|
||||||
|
ERB.new(template, nil, '-').result(binding)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shell_exec_template
|
||||||
|
command = "cd #{@task['parameters']['cwd']} &&" \
|
||||||
|
" #{@task['parameters']['cmd']}"
|
||||||
|
template = <<-eos
|
||||||
|
#!/bin/bash
|
||||||
|
# Puppet shell wrapper for task: <%= task_name %>
|
||||||
|
# Manifest: <%= puppet_exec_file_path %>
|
||||||
|
|
||||||
|
<%= command %>
|
||||||
|
eos
|
||||||
|
ERB.new(template, nil, '-').result(binding)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shell_exec_file_path
|
||||||
|
File.join(SHELL_MANIFEST_DIR, "#{task_name}_command.sh")
|
||||||
|
end
|
||||||
|
|
||||||
|
def puppet_exec_file_path
|
||||||
|
File.join(SHELL_MANIFEST_DIR, manifest_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload_puppet_manifest
|
||||||
|
upload_file(@task['node_id'], {
|
||||||
|
'path' => puppet_exec_file_path,
|
||||||
|
'content' => puppet_exec_template,
|
||||||
|
'permissions' => '0755'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload_shell_file
|
||||||
|
upload_file(@task['node_id'], {
|
||||||
|
'path' => shell_exec_file_path,
|
||||||
|
'content' => shell_exec_template,
|
||||||
|
'permissions' => '0755'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload_shell_manifest
|
||||||
|
upload_puppet_manifest
|
||||||
|
upload_shell_file
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def manifest_name
|
||||||
|
"#{task_name}_manifest.pp"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_puppet_hook
|
||||||
|
{
|
||||||
|
'node_id' => @task['node_id'],
|
||||||
|
'id' => @task['id'],
|
||||||
|
'parameters' => {
|
||||||
|
"puppet_manifest" => manifest_name,
|
||||||
|
"cwd" => SHELL_MANIFEST_DIR,
|
||||||
|
"timeout" => @task['parameters']['timeout'],
|
||||||
|
"retries" => @task['parameters']['retries']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
60
lib/astute/tasks/sync.rb
Normal file
60
lib/astute/tasks/sync.rb
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class Sync < Task
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
@shell_task = Shell.new(
|
||||||
|
generate_shell_hook,
|
||||||
|
@ctx
|
||||||
|
)
|
||||||
|
@shell_task.run
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
self.status = @shell_task.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task, 'node_id')
|
||||||
|
validate_presence(@task['parameters'], 'dst')
|
||||||
|
validate_presence(@task['parameters'], 'src')
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_default
|
||||||
|
@task['parameters']['timeout'] ||= 300
|
||||||
|
@task['parameters']['retries'] ||= 10
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_shell_hook
|
||||||
|
path = @task['parameters']['dst']
|
||||||
|
rsync_cmd = "mkdir -p #{path} && rsync #{Astute.config.rsync_options}" \
|
||||||
|
" #{@task['parameters']['src']} #{path}"
|
||||||
|
{
|
||||||
|
"node_id" => @task['node_id'],
|
||||||
|
"id" => @task['id'],
|
||||||
|
"parameters" => {
|
||||||
|
"cmd" => rsync_cmd,
|
||||||
|
"cwd" => "/",
|
||||||
|
"timeout" => @task['parameters']['timeout'],
|
||||||
|
"retries" => @task['parameters']['retries']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
47
lib/astute/tasks/upload_file.rb
Normal file
47
lib/astute/tasks/upload_file.rb
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class UploadFile < Task
|
||||||
|
|
||||||
|
def initialize(task, context)
|
||||||
|
super
|
||||||
|
@upload_status = :pending
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
@upload_status = upload_file(@task['node_id'], @task['parameters'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
if [true, false].include? @upload_status
|
||||||
|
@upload_status ? succeed! : failed!
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task, 'node_id')
|
||||||
|
validate_presence(@task['parameters'], 'path')
|
||||||
|
validate_presence(@task['parameters'], 'data')
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_default
|
||||||
|
@task['parameters']['content'] = @task['parameters']['data']
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
56
lib/astute/tasks/upload_files.rb
Normal file
56
lib/astute/tasks/upload_files.rb
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 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 Astute
|
||||||
|
class UploadFiles < Task
|
||||||
|
|
||||||
|
def initialize(task, context)
|
||||||
|
super
|
||||||
|
@nodes_status = @task['parameters']['nodes'].inject({}) do |n_s, n|
|
||||||
|
n_s.merge({ n['uid'] => :pending })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process
|
||||||
|
hook['parameters']['nodes'].each do |node|
|
||||||
|
node['files'].each do |file|
|
||||||
|
parameters = {
|
||||||
|
'content' => file['data'],
|
||||||
|
'path' => file['dst'],
|
||||||
|
'permissions' => file['permissions'] || '0644',
|
||||||
|
'dir_permissions' => file['dir_permissions'] || '0755',
|
||||||
|
}
|
||||||
|
if @nodes_status[node['uid']]
|
||||||
|
@nodes_status[node['uid']] = upload_file(node['uid'], parameters)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_status
|
||||||
|
if @nodes_status.values.all? { |v| v != :pending }
|
||||||
|
failed! if @nodes_status.values.include?(false)
|
||||||
|
succeed! if @nodes_status.values.all?{ |s| s == true }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation
|
||||||
|
validate_presence(@task['parameters'], 'nodes')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
@ -68,7 +68,7 @@ describe MClient do
|
|||||||
|
|
||||||
mclient = MClient.new(@ctx, "faketest", nodes.map {|x| x['uid']}, check_result=true, timeout=nil, retries=1)
|
mclient = MClient.new(@ctx, "faketest", nodes.map {|x| x['uid']}, check_result=true, timeout=nil, retries=1)
|
||||||
expect { mclient.echo(:msg => 'hello world') }.to \
|
expect { mclient.echo(:msg => 'hello world') }.to \
|
||||||
raise_error(Astute::MClientTimeout, /MCollective agents '3' didn't respond./)
|
raise_error(Astute::MClientTimeout, /MCollective agents 'faketest' '3' didn't respond./)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should raise error if agent returns statuscode != 0" do
|
it "should raise error if agent returns statuscode != 0" do
|
||||||
|
@ -24,6 +24,25 @@ describe Astute::Orchestrator do
|
|||||||
@reporter.stub_everything
|
@reporter.stub_everything
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#task_deployment' do
|
||||||
|
it 'should run task deployment' do
|
||||||
|
deployment_info = []
|
||||||
|
deployment_tasks = {'1' => []}
|
||||||
|
|
||||||
|
Astute::TaskDeployment.any_instance.expects(:deploy).with(
|
||||||
|
deployment_info,
|
||||||
|
deployment_tasks
|
||||||
|
)
|
||||||
|
|
||||||
|
@orchestrator.task_deploy(
|
||||||
|
@reporter,
|
||||||
|
'task_id',
|
||||||
|
deployment_info,
|
||||||
|
deployment_tasks
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#verify_networks' do
|
describe '#verify_networks' do
|
||||||
it 'should validate nodes availability before check' do
|
it 'should validate nodes availability before check' do
|
||||||
nodes = [{'uid' => '1'}, {'uid' => '2'}]
|
nodes = [{'uid' => '1'}, {'uid' => '2'}]
|
||||||
|
181
spec/unit/task_deployment_spec.rb
Normal file
181
spec/unit/task_deployment_spec.rb
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
require File.join(File.dirname(__FILE__), '../spec_helper')
|
||||||
|
|
||||||
|
describe Astute::TaskDeployment do
|
||||||
|
include SpecHelpers
|
||||||
|
|
||||||
|
let(:ctx) do
|
||||||
|
ctx = mock('context')
|
||||||
|
ctx.stubs(:task_id)
|
||||||
|
ctx
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:deployment_info) do
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'uid' => '1',
|
||||||
|
'fail_if_error' => false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:deployment_tasks) do
|
||||||
|
{
|
||||||
|
"1"=>
|
||||||
|
[{
|
||||||
|
"type"=>"noop",
|
||||||
|
"fail_on_error"=>true,
|
||||||
|
"required_for"=>[],
|
||||||
|
"requires"=> [],
|
||||||
|
"id"=>"ironic_post_swift_key"
|
||||||
|
}],
|
||||||
|
"null"=> [{
|
||||||
|
"skipped"=>true,
|
||||||
|
"type"=>"skipped",
|
||||||
|
"fail_on_error"=>false,
|
||||||
|
"required_for"=>[],
|
||||||
|
"requires"=>[],
|
||||||
|
"id"=>"post_deployment_start"}]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:task_deployment) { Astute::TaskDeployment.new(ctx) }
|
||||||
|
|
||||||
|
describe '#deploy' do
|
||||||
|
it 'should run deploy' do
|
||||||
|
task_deployment.stubs(:remove_failed_nodes).returns([deployment_info, []])
|
||||||
|
Astute::TaskPreDeploymentActions.any_instance.stubs(:process)
|
||||||
|
task_deployment.stubs(:write_graph_to_file)
|
||||||
|
ctx.stubs(:report)
|
||||||
|
|
||||||
|
Deployment::Process.any_instance.expects(:run).returns({:success => true})
|
||||||
|
task_deployment.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise error if deployment info not provided' do
|
||||||
|
expect{task_deployment.deploy([],{})}.to raise_error(
|
||||||
|
Astute::DeploymentEngineError,
|
||||||
|
"Deployment info are not provided!"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should run pre deployment task' do
|
||||||
|
task_deployment.stubs(:remove_failed_nodes).returns([deployment_info, []])
|
||||||
|
task_deployment.stubs(:write_graph_to_file)
|
||||||
|
ctx.stubs(:report)
|
||||||
|
Deployment::Process.any_instance.stubs(:run).returns({:success => true})
|
||||||
|
|
||||||
|
pre_deployment = Astute::TaskPreDeploymentActions.new(deployment_info, ctx)
|
||||||
|
Astute::TaskPreDeploymentActions.expects(:new)
|
||||||
|
.with(deployment_info, ctx)
|
||||||
|
.returns(pre_deployment)
|
||||||
|
Astute::TaskPreDeploymentActions.any_instance.expects(:process)
|
||||||
|
task_deployment.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should support virtual node' do
|
||||||
|
d_t = task_deployment.send(:support_virtual_node, deployment_tasks)
|
||||||
|
expect(d_t.keys.include?('virtual_sync_node')).to be_true
|
||||||
|
expect(d_t.keys.include?('null')).to be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should remove failed nodes' do
|
||||||
|
#TODO(vsharshov): improve remove failed nodes check. Check mcollective
|
||||||
|
Astute::TaskPreDeploymentActions.any_instance.stubs(:process)
|
||||||
|
task_deployment.stubs(:write_graph_to_file)
|
||||||
|
ctx.stubs(:report)
|
||||||
|
|
||||||
|
task_deployment.expects(:remove_failed_nodes).returns([deployment_info, []])
|
||||||
|
|
||||||
|
Deployment::Process.any_instance.stubs(:run).returns({:success => true})
|
||||||
|
task_deployment.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'should report final status' do
|
||||||
|
|
||||||
|
it 'succeed status' do
|
||||||
|
Astute::TaskPreDeploymentActions.any_instance.stubs(:process)
|
||||||
|
Deployment::Process.any_instance.stubs(:run).returns({:success => true})
|
||||||
|
task_deployment.stubs(:remove_failed_nodes).returns([deployment_info, []])
|
||||||
|
task_deployment.stubs(:write_graph_to_file)
|
||||||
|
ctx.expects(:report).with({'status' => 'ready', 'progress' => 100})
|
||||||
|
|
||||||
|
task_deployment.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'failed status' do
|
||||||
|
Astute::TaskPreDeploymentActions.any_instance.stubs(:process)
|
||||||
|
Deployment::Process.any_instance.stubs(:run).returns({
|
||||||
|
:success => false,
|
||||||
|
:failed_nodes => [],
|
||||||
|
:failed_tasks => [],
|
||||||
|
:status => 'Failed because of'})
|
||||||
|
task_deployment.stubs(:remove_failed_nodes).returns([deployment_info, []])
|
||||||
|
task_deployment.stubs(:write_graph_to_file)
|
||||||
|
ctx.expects(:report).with({
|
||||||
|
'status' => 'error',
|
||||||
|
'progress' => 100,
|
||||||
|
'error' => 'Failed because of'})
|
||||||
|
|
||||||
|
task_deployment.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'graph file' do
|
||||||
|
|
||||||
|
around(:each) do |example|
|
||||||
|
old_value = Astute.config.enable_graph_file
|
||||||
|
example.run
|
||||||
|
Astute.config.enable_graph_file = old_value
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should write if disable' do
|
||||||
|
Astute.config.enable_graph_file = false
|
||||||
|
|
||||||
|
task_deployment.stubs(:remove_failed_nodes).returns([deployment_info, []])
|
||||||
|
Astute::TaskPreDeploymentActions.any_instance.stubs(:process)
|
||||||
|
ctx.stubs(:report)
|
||||||
|
Deployment::Process.any_instance.stubs(:run).returns({:success => true})
|
||||||
|
|
||||||
|
file_handle = mock
|
||||||
|
file_handle.expects(:write).with(regexp_matches(/digraph/)).never
|
||||||
|
File.expects(:open).with("/tmp/graph-#{ctx.task_id}.dot", 'w')
|
||||||
|
.yields(file_handle).never
|
||||||
|
|
||||||
|
task_deployment.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should write graph if enable' do
|
||||||
|
Astute.config.enable_graph_file = true
|
||||||
|
|
||||||
|
task_deployment.stubs(:remove_failed_nodes).returns([deployment_info, []])
|
||||||
|
Astute::TaskPreDeploymentActions.any_instance.stubs(:process)
|
||||||
|
ctx.stubs(:report)
|
||||||
|
Deployment::Process.any_instance.stubs(:run).returns({:success => true})
|
||||||
|
|
||||||
|
file_handle = mock
|
||||||
|
file_handle.expects(:write).with(regexp_matches(/digraph/)).once
|
||||||
|
File.expects(:open).with("/tmp/graph-#{ctx.task_id}.dot", 'w')
|
||||||
|
.yields(file_handle).once
|
||||||
|
|
||||||
|
task_deployment.deploy(deployment_info, deployment_tasks)
|
||||||
|
end
|
||||||
|
end # 'graph file'
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
295
spec/unit/task_node_spec.rb
Normal file
295
spec/unit/task_node_spec.rb
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
require File.join(File.dirname(__FILE__), '../spec_helper')
|
||||||
|
|
||||||
|
describe Astute::TaskNode do
|
||||||
|
include SpecHelpers
|
||||||
|
|
||||||
|
let(:ctx) do
|
||||||
|
ctx = mock('context')
|
||||||
|
ctx.stubs(:task_id)
|
||||||
|
ctx
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:task_node) do
|
||||||
|
node = Astute::TaskNode.new('node_id')
|
||||||
|
node.context = ctx
|
||||||
|
node
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:task) do
|
||||||
|
task_node.graph.create_task(
|
||||||
|
task_data['id'],
|
||||||
|
task_data.merge({'node_id' => 'node_id'})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context '#run' do
|
||||||
|
|
||||||
|
let(:task_data) do
|
||||||
|
{
|
||||||
|
"parameters" => {
|
||||||
|
"puppet_modules" => "/etc/puppet/modules",
|
||||||
|
"puppet_manifest" => "/etc/puppet/modules/osnailyfacter/modular" \
|
||||||
|
"/openstack-haproxy/openstack-haproxy-mysqld.pp",
|
||||||
|
"timeout" => 300,
|
||||||
|
"cwd" => "/"
|
||||||
|
},
|
||||||
|
"type" => "puppet",
|
||||||
|
"fail_on_error" => true,
|
||||||
|
"required_for" => [],
|
||||||
|
"requires" => [],
|
||||||
|
"id" => "openstack-haproxy-mysqld"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should run task' do
|
||||||
|
Astute::Puppet.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should mark node as busy' do
|
||||||
|
Astute::Puppet.any_instance.stubs(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
expect(task_node.status).to eql(:busy)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should mark task as running' do
|
||||||
|
Astute::Puppet.any_instance.stubs(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
expect(task.status).to eql(:running)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'support different task type' do
|
||||||
|
|
||||||
|
let(:task_data) do
|
||||||
|
{
|
||||||
|
"parameters" => {},
|
||||||
|
"type" => "noop",
|
||||||
|
"fail_on_error" => false,
|
||||||
|
"required_for" => [],
|
||||||
|
"requires" => [],
|
||||||
|
"id" => "test-task"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shell' do
|
||||||
|
task_data['type'] = "shell"
|
||||||
|
Astute::Shell.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'puppet' do
|
||||||
|
task_data['type'] = "puppet"
|
||||||
|
Astute::Puppet.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sync' do
|
||||||
|
task_data['type'] = "sync"
|
||||||
|
Astute::Sync.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'cobbler_sync' do
|
||||||
|
task_data['type'] = "cobbler_sync"
|
||||||
|
Astute::CobblerSync.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'noop' do
|
||||||
|
task_data['type'] = "noop"
|
||||||
|
Astute::Noop.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skipped' do
|
||||||
|
task_data['type'] = "skipped"
|
||||||
|
Astute::Noop.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stage' do
|
||||||
|
task_data['type'] = "stage"
|
||||||
|
Astute::Noop.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reboot' do
|
||||||
|
task_data['type'] = "reboot"
|
||||||
|
Astute::Reboot.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'upload_file' do
|
||||||
|
task_data['type'] = "upload_file"
|
||||||
|
Astute::UploadFile.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'upload_files' do
|
||||||
|
task_data['type'] = "upload_files"
|
||||||
|
task_data['parameters']['nodes'] = []
|
||||||
|
Astute::UploadFiles.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'copy_files' do
|
||||||
|
task_data['type'] = "copy_files"
|
||||||
|
task_data['parameters']['files'] = []
|
||||||
|
Astute::CopyFiles.any_instance.expects(:run)
|
||||||
|
task_node.run(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unkown type' do
|
||||||
|
task_data['type'] = "unknown"
|
||||||
|
expect{task_node.run(task)}.to raise_error(
|
||||||
|
Astute::TaskValidationError,
|
||||||
|
"Unknown task type 'unknown'")
|
||||||
|
end
|
||||||
|
end # support task type
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
context '#poll' do
|
||||||
|
|
||||||
|
context 'not busy' do
|
||||||
|
it 'should not raise any error' do
|
||||||
|
expect{task_node.poll}.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not change node status' do
|
||||||
|
old_status = task_node.status
|
||||||
|
task_node.poll
|
||||||
|
expect(task_node.status).to eql(old_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'busy' do
|
||||||
|
let(:task_data) do
|
||||||
|
{
|
||||||
|
"parameters" => {},
|
||||||
|
"type" => "puppet",
|
||||||
|
"fail_on_error" => false,
|
||||||
|
"required_for" => [],
|
||||||
|
"requires" => [],
|
||||||
|
"id" => "test-task"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
Astute::Puppet.any_instance.stubs(:run)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'mark online' do
|
||||||
|
it 'if task successful' do
|
||||||
|
Astute::Puppet.any_instance.stubs(:status).returns(:successful)
|
||||||
|
ctx.stubs(:report)
|
||||||
|
task_node.run(task)
|
||||||
|
task_node.poll
|
||||||
|
expect(task_node.status).to eql(:online)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'if task failed' do
|
||||||
|
Astute::Puppet.any_instance.stubs(:status).returns(:failed)
|
||||||
|
ctx.stubs(:report)
|
||||||
|
task_node.run(task)
|
||||||
|
task_node.poll
|
||||||
|
expect(task_node.status).to eql(:online)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should report progress if task running' do
|
||||||
|
Astute::Puppet.any_instance.expects(:status).returns(:running)
|
||||||
|
task_node.run(task)
|
||||||
|
ctx.expects(:report).with({
|
||||||
|
'uid' => 'node_id',
|
||||||
|
'status' => 'deploying',
|
||||||
|
'task' => task.name,
|
||||||
|
'progress' => 0
|
||||||
|
})
|
||||||
|
task_node.poll
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should report ready if task successful and no more task' do
|
||||||
|
Astute::Puppet.any_instance.expects(:status).returns(:successful)
|
||||||
|
task_node.run(task)
|
||||||
|
ctx.expects(:report).with({
|
||||||
|
'uid' => 'node_id',
|
||||||
|
'status' => 'ready',
|
||||||
|
'task' => task.name,
|
||||||
|
'task_status' => 'successful',
|
||||||
|
'progress' => 100
|
||||||
|
})
|
||||||
|
task_node.poll
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should report error if task failed and no more task' do
|
||||||
|
Astute::Puppet.any_instance.expects(:status).returns(:failed)
|
||||||
|
task_node.run(task)
|
||||||
|
ctx.expects(:report).with({
|
||||||
|
'uid' => 'node_id',
|
||||||
|
'status' => 'error',
|
||||||
|
'task' => task.name,
|
||||||
|
'task_status' => 'failed',
|
||||||
|
'error_type' => 'deploy',
|
||||||
|
'progress' => 100,
|
||||||
|
})
|
||||||
|
task_node.poll
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should report deploy progress if task successful and another tasks exists' do
|
||||||
|
Astute::Puppet.any_instance.expects(:status).returns(:successful)
|
||||||
|
task_node.graph.create_task(
|
||||||
|
'second_task',
|
||||||
|
task_data.merge({'node_id' => 'node_id'})
|
||||||
|
)
|
||||||
|
|
||||||
|
task_node.run(task)
|
||||||
|
ctx.expects(:report).with({
|
||||||
|
'uid' => 'node_id',
|
||||||
|
'status' => 'deploying',
|
||||||
|
'task' => task.name,
|
||||||
|
'task_status' => 'successful',
|
||||||
|
'progress' => 50
|
||||||
|
})
|
||||||
|
task_node.poll
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should report deploy progress if task failed and another tasks exists' do
|
||||||
|
Astute::Puppet.any_instance.expects(:status).returns(:failed)
|
||||||
|
task_node.graph.create_task(
|
||||||
|
'second_task',
|
||||||
|
task_data.merge({'node_id' => 'node_id'})
|
||||||
|
)
|
||||||
|
|
||||||
|
task_node.run(task)
|
||||||
|
ctx.expects(:report).with({
|
||||||
|
'uid' => 'node_id',
|
||||||
|
'status' => 'deploying',
|
||||||
|
'task' => task.name,
|
||||||
|
'task_status' => 'failed',
|
||||||
|
'progress' => 50
|
||||||
|
})
|
||||||
|
task_node.poll
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user