diff --git a/install.sh b/install.sh index 7631aded8e..70838455c5 100755 --- a/install.sh +++ b/install.sh @@ -1945,7 +1945,9 @@ ONE_CLI_LIB_FILES="src/cli/one_helper/onegroup_helper.rb \ src/cli/one_helper/onevcenter_helper.rb \ src/cli/one_helper/onemarket_helper.rb \ src/cli/one_helper/onevntemplate_helper.rb \ - src/cli/one_helper/onehook_helper.rb" + src/cli/one_helper/onehook_helper.rb \ + src/cli/one_helper/oneflow_helper.rb \ + src/cli/one_helper/oneflowtemplate_helper.rb" CLI_BIN_FILES="src/cli/onevm \ src/cli/onehost \ @@ -2250,7 +2252,8 @@ ONEFLOW_LIB_FILES="src/flow/lib/grammar.rb \ src/flow/lib/log.rb \ src/flow/lib/models.rb \ src/flow/lib/strategy.rb \ - src/flow/lib/validator.rb" + src/flow/lib/validator.rb \ + src/flow/lib/EventManager.rb" ONEFLOW_LIB_STRATEGY_FILES="src/flow/lib/strategy/straight.rb" diff --git a/share/linters/.rubocop.yml b/share/linters/.rubocop.yml index 90bc618322..5abbb0362f 100644 --- a/share/linters/.rubocop.yml +++ b/share/linters/.rubocop.yml @@ -557,8 +557,7 @@ AllCops: - src/flow/lib/models.rb - src/flow/lib/validator.rb - src/flow/lib/strategy/straight.rb - - src/flow/oneflow-server.rb - + - src/flow/lib/EventManager.rb ######## # LAYOUT diff --git a/src/cli/one_helper/oneflow_helper.rb b/src/cli/one_helper/oneflow_helper.rb new file mode 100644 index 0000000000..e1482b4d72 --- /dev/null +++ b/src/cli/one_helper/oneflow_helper.rb @@ -0,0 +1,317 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2019, OpenNebula Project, OpenNebula Systems # +# # +# 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 'one_helper' + +# Oneflow command helper +class OneFlowHelper < OpenNebulaHelper::OneHelper + + # Get client to make request + # + # @options [Hash] CLI options + def client(options) + Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT + ) + end + + # Get service pool table + def format_service_pool + # TODO: config file + CLIHelper::ShowTable.new(nil, self) do + column :ID, 'ID', :size => 10 do |d| + d['ID'] + end + + column :USER, 'Username', :left, :size => 15 do |d| + d['UNAME'] + end + + column :GROUP, 'Group', :left, :size => 15 do |d| + d['GNAME'] + end + + column :NAME, 'Name', :size => 25, :left => true do |d| + d['NAME'] + end + + column :STATE, 'State', :size => 11, :left => true do |d| + Service.state_str(d['TEMPLATE']['BODY']['state']) + end + + default :ID, :USER, :GROUP, :NAME, :STATE + end + end + + # List service pool + # + # @param client [Service::Client] Petition client + # @param options [Hash] CLI options + def list_service_pool(client, options) + response = client.get(RESOURCE_PATH) + + if CloudClient.is_error?(response) + [response.code.to_i, response.to_s] + else + array_list = JSON.parse(response.body) + array_list = array_list['DOCUMENT_POOL']['DOCUMENT'] + + array_list = [] if array_list.nil? + + unless options.key? :done + # remove from list flows in DONE state + array_list.reject! do |value| + value['TEMPLATE']['BODY']['state'] == 5 + end + end + + if options[:json] + if array_list.empty? + 0 + else + [0, JSON.pretty_generate(array_list)] + end + else + format_service_pool.show(array_list) + + 0 + end + end + end + + # List service pool continiously + # + # @param client [Service::Client] Petition client + # @param options [Hash] CLI options + def top_service_pool(client, options) + # TODO: make default delay configurable + options[:delay] ? delay = options[:delay] : delay = 4 + + begin + loop do + CLIHelper.scr_cls + CLIHelper.scr_move(0, 0) + + list_service_pool(client, options) + + sleep delay + end + rescue StandardError => e + STDERR.puts e.message + exit(-1) + end + + 0 + end + + # Show service detailed information + # + # @param client [Service::Client] Petition client + # @param service [Integer] Service ID + # @param options [Hash] CLI options + def format_resource(client, service, options) + response = client.get("#{RESOURCE_PATH}/#{service}") + + if CloudClient.is_error?(response) + [response.code.to_i, response.to_s] + else + if options[:json] + [0, response.body] + else + str_h1 = '%-80s' + document = JSON.parse(response.body)['DOCUMENT'] + template = document['TEMPLATE']['BODY'] + + CLIHelper.print_header( + str_h1 % "SERVICE #{document['ID']} INFORMATION" + ) + + print_service_info(document) + + print_roles_info(template['roles']) + + return 0 unless template['log'] + + CLIHelper.print_header(str_h1 % 'LOG MESSAGES', false) + + template['log'].each do |log| + t = Time.at(log['timestamp']).strftime('%m/%d/%y %H:%M') + puts "#{t} [#{log['severity']}] #{log['message']}" + end + + 0 + end + end + end + + private + + # Get nodes pool table + def format_node_pool + # TODO: config file + CLIHelper::ShowTable.new(nil, self) do + column :VM_ID, + 'ONE identifier for Virtual Machine', + :size => 6 do |d| + st = '' + + if d['scale_up'] + st << '\u2191 ' + elsif d['disposed'] + st << '\u2193 ' + end + + if d['vm_info'].nil? + st << d['deploy_id'].to_s + else + st << d['vm_info']['VM']['ID'] + end + + st + end + + column :NAME, + 'Name of the Virtual Machine', + :left, + :size => 24 do |d| + if !d['vm_info'].nil? + if d['vm_info']['VM']['RESCHED'] == '1' + "*#{d['NAME']}" + else + d['vm_info']['VM']['NAME'] + end + else + '' + end + end + + column :USER, + 'Username of the Virtual Machine owner', + :left, + :size => 15 do |d| + if !d['vm_info'].nil? + d['vm_info']['VM']['UNAME'] + else + '' + end + end + + column :GROUP, + 'Group of the Virtual Machine', + :left, + :size => 15 do |d| + if !d['vm_info'].nil? + d['vm_info']['VM']['GNAME'] + else + '' + end + end + + default :VM_ID, :NAME, :USER, :GROUP + end + end + + # Print service information + # + # @param document [Hash] Service document information + def print_service_info(document) + str = '%-20s: %-20s' + str_h1 = '%-80s' + template = document['TEMPLATE']['BODY'] + + puts Kernel.format(str, 'ID', document['ID']) + puts Kernel.format(str, 'NAME', document['NAME']) + puts Kernel.format(str, 'USER', document['UNAME']) + puts Kernel.format(str, 'GROUP', document['GNAME']) + + puts Kernel.format(str, 'STRATEGY', template['deployment']) + puts Kernel.format(str, + 'SERVICE STATE', + Service.state_str(template['state'])) + + if template['shutdown_action'] + puts Kernel.format(str, 'SHUTDOWN', template['shutdown_action']) + end + + puts + + CLIHelper.print_header(str_h1 % 'PERMISSIONS', false) + + %w[OWNER GROUP OTHER].each do |e| + mask = '---' + permissions_hash = document['PERMISSIONS'] + mask[0] = 'u' if permissions_hash["#{e}_U"] == '1' + mask[1] = 'm' if permissions_hash["#{e}_M"] == '1' + mask[2] = 'a' if permissions_hash["#{e}_A"] == '1' + + puts Kernel.format(str, e, mask) + end + + puts + end + + # Print service roles information + # + # @param roles [Array] Service roles information + def print_roles_info(roles) + str = '%-20s: %-20s' + + roles.each do |role| + CLIHelper.print_header("ROLE #{role['name']}", false) + + puts Kernel.format(str, + 'ROLE STATE', + Role.state_str(role['state'])) + + if role['parents'] + puts Kernel.format(str, + 'PARENTS', + role['parents'].join(', ')) + end + + puts Kernel.format(str, 'VM TEMPLATE', role['vm_template']) + puts Kernel.format(str, 'CARDINALITY', role['cardinality']) + + if role['min_vms'] + puts Kernel.format(str, 'MIN VMS', role['min_vms']) + end + + if role['max_vms'] + puts Kernel.format(str, 'MAX VMS', role['max_vms']) + end + + if role['coolddown'] + puts Kernel.format(str, 'COOLDOWN', "#{role['cooldown']}s") + end + + if role['shutdown_action'] + puts Kernel.format(str, 'SHUTDOWN', role['shutdown_action']) + end + + CLIHelper.print_header('NODES INFORMATION', false) + + format_node_pool.show(role['nodes']) + + puts + end + + puts + end + +end diff --git a/src/cli/one_helper/oneflowtemplate_helper.rb b/src/cli/one_helper/oneflowtemplate_helper.rb new file mode 100644 index 0000000000..aa9ee9375c --- /dev/null +++ b/src/cli/one_helper/oneflowtemplate_helper.rb @@ -0,0 +1,145 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2019, OpenNebula Project, OpenNebula Systems # +# # +# 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 'one_helper' + +# Oneflow Template command helper +class OneFlowTemplateHelper < OpenNebulaHelper::OneHelper + + # Get service template pool + def format_service_template_pool + # TODO: config file + CLIHelper::ShowTable.new(nil, self) do + column :ID, 'ID', :size => 10 do |d| + d['ID'] + end + + column :USER, 'Username', :left, :size => 15 do |d| + d['UNAME'] + end + + column :GROUP, 'Group', :left, :size => 15 do |d| + d['GNAME'] + end + + column :NAME, 'Name', :left, :size => 37 do |d| + d['NAME'] + end + + default :ID, :USER, :GROUP, :NAME + end + end + + # List service template pool + # + # @param client [Service::Client] Petition client + # @param options [Hash] CLI options + def list_service_template_pool(client, options) + response = client.get(RESOURCE_PATH) + + if CloudClient.is_error?(response) + [response.code.to_i, response.to_s] + else + if options[:json] + [0, response.body] + else + documents = JSON.parse(response.body)['DOCUMENT_POOL'] + format_service_template_pool.show(documents['DOCUMENT']) + + 0 + end + end + end + + # List service template pool continiously + # + # @param client [Service::Client] Petition client + # @param options [Hash] CLI options + def top_service_template_pool(client, options) + # TODO: make default delay configurable + options[:delay] ? delay = options[:delay] : delay = 4 + + begin + loop do + CLIHelper.scr_cls + CLIHelper.scr_move(0, 0) + + list_service_template_pool(client, options) + + sleep delay + end + rescue StandardError => e + STDERR.puts e.message + exit(-1) + end + + 0 + end + + # Show service template detailed information + # + # @param client [Service::Client] Petition client + # @param service_template [Integer] Service template ID + # @param options [Hash] CLI options + def format_resource(client, service_template, options) + response = client.get("#{RESOURCE_PATH}/#{service_template}") + + if CloudClient.is_error?(response) + [response.code.to_i, response.to_s] + else + if options[:json] + [0, response.body] + else + str = '%-20s: %-20s' + str_h1 = '%-80s' + + document = JSON.parse(response.body)['DOCUMENT'] + template = document['TEMPLATE']['BODY'] + + CLIHelper.print_header( + str_h1 % "SERVICE TEMPLATE #{document['ID']} INFORMATION" + ) + + puts Kernel.format str, 'ID', document['ID'] + puts Kernel.format str, 'NAME', document['NAME'] + puts Kernel.format str, 'USER', document['UNAME'] + puts Kernel.format str, 'GROUP', document['GNAME'] + + puts + + CLIHelper.print_header(str_h1 % 'PERMISSIONS', false) + + %w[OWNER GROUP OTHER].each do |e| + mask = '---' + permissions_hash = document['PERMISSIONS'] + mask[0] = 'u' if permissions_hash["#{e}_U"] == '1' + mask[1] = 'm' if permissions_hash["#{e}_M"] == '1' + mask[2] = 'a' if permissions_hash["#{e}_A"] == '1' + + puts Kernel.format str, e, mask + end + + puts + + CLIHelper.print_header(str_h1 % 'TEMPLATE CONTENTS', false) + puts JSON.pretty_generate(template) + + 0 + end + end + end + +end diff --git a/src/cli/oneflow b/src/cli/oneflow index 8b3e42ba2d..781a3f35fc 100755 --- a/src/cli/oneflow +++ b/src/cli/oneflow @@ -33,370 +33,17 @@ end $LOAD_PATH << RUBY_LIB_LOCATION $LOAD_PATH << RUBY_LIB_LOCATION + '/cli' +require 'json' + require 'command_parser' require 'opennebula/oneflow_client' - -require 'cli_helper' -require 'one_helper/onevm_helper' - -require 'json' +require 'one_helper/oneflow_helper' USER_AGENT = 'CLI' # Base Path representing the resource to be used in the requests RESOURCE_PATH = '/service' -# -# Table -# - -SERVICE_TABLE = CLIHelper::ShowTable.new(nil, self) do - column :ID, 'ID', :size => 10 do |d| - d['ID'] - end - - column :USER, 'Username', :left, :size => 15 do |d| - d['UNAME'] - end - - column :GROUP, 'Group', :left, :size => 15 do |d| - d['GNAME'] - end - - column :NAME, 'Name', :size => 25, :left => true do |d| - d['NAME'] - end - - column :STATE, 'State', :size => 11, :left => true do |d| - Service.state_str(d['TEMPLATE']['BODY']['state']) - end - - default :ID, :USER, :GROUP, :NAME, :STATE -end - -NODE_TABLE = CLIHelper::ShowTable.new(nil, self) do - column :VM_ID, 'ONE identifier for Virtual Machine', :size => 6 do |d| - st = '' - if d['scale_up'] - st << '\u2191 ' - elsif d['disposed'] - st << '\u2193 ' - end - - if d['vm_info'].nil? - st << d['deploy_id'].to_s - else - st << d['vm_info']['VM']['ID'] - end - - st - end - - column :NAME, 'Name of the Virtual Machine', :left, - :size => 23 do |d| - if !d['vm_info'].nil? - if d['vm_info']['VM']['RESCHED'] == '1' - "*#{d['NAME']}" - else - d['vm_info']['VM']['NAME'] - end - else - '' - end - end - - column :USER, 'Username of the Virtual Machine owner', :left, - :size => 8 do |d| - if !d['vm_info'].nil? - d['vm_info']['VM']['UNAME'] - else - '' - end - end - - column :GROUP, 'Group of the Virtual Machine', :left, - :size => 8 do |d| - if !d['vm_info'].nil? - d['vm_info']['VM']['GNAME'] - else - '' - end - end - - column :STAT, 'Actual status', :size => 4 do |d, _| - if !d['vm_info'].nil? - OneVMHelper.state_to_str(d['vm_info']['VM']['STATE'], - d['vm_info']['VM']['LCM_STATE']) - else - '' - end - end - - column :UCPU, 'CPU percentage used by the VM', :size => 4 do |d| - if !d['vm_info'].nil? - d['vm_info']['VM']['CPU'] - else - '' - end - end - - column :UMEM, 'Memory used by the VM', :size => 7 do |d| - if !d['vm_info'].nil? - OpenNebulaHelper.unit_to_str(d['vm_info']['VM']['MEMORY'].to_i, {}) - else - '' - end - end - - column :HOST, 'Host where the VM is running', :left, :size => 20 do |d| - if !d['vm_info'].nil? - if d['vm_info']['VM']['HISTORY_RECORDS'] && - d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'] - state_str = - VirtualMachine::VM_STATE[d['vm_info']['VM']['STATE'].to_i] - history = d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'] - if %w[ACTIVE SUSPENDED].include? state_str - history = history.last if history.instance_of?(Array) - history['HOSTNAME'] - end - end - else - '' - end - end - - column :TIME, 'Time since the VM was submitted', :size => 10 do |d| - if !d['vm_info'].nil? - stime = d['vm_info']['VM']['STIME'].to_i - if d['vm_info']['VM']['ETIME'] == '0' - etime = Time.now.to_i - else - etime = d['vm_info']['VM']['ETIME'].to_i - end - dtime = etime - stime - OpenNebulaHelper.period_to_str(dtime, false) - else - '' - end - end - - default :VM_ID, :NAME, :STAT, :UCPU, :UMEM, :HOST, :TIME -end - -# List the services. This method is used in top and list commands -# @param [Service::Client] client -# @param [Hash] options -# @return [[Integer, String], Integer] Returns the exit_code and optionally -# a String to be printed -def list_services(client, options) - response = client.get(RESOURCE_PATH) - - if CloudClient.is_error?(response) - [response.code.to_i, response.to_s] - else - # [0,response.body] - if options[:json] - [0, response.body] - else - array_list = JSON.parse(response.body) - SERVICE_TABLE.show(array_list['DOCUMENT_POOL']['DOCUMENT']) - 0 - end - end -end - -# Show the service information. This method is used in top and show commands -# @param [Service::Client] client -# @param [Array] args -# @param [Hash] options -# @return [[Integer, String], Integer] Returns the exit_code and optionally -# a String to be printed -def show_service(client, args, options) - response = client.get("#{RESOURCE_PATH}/#{args[0]}") - - if CloudClient.is_error?(response) - [response.code.to_i, response.to_s] - else - # [0,response.body] - if options[:json] - [0, response.body] - else - str = '%-20s: %-20s' - str_h1 = '%-80s' - - document_hash = JSON.parse(response.body) - template = document_hash['DOCUMENT']['TEMPLATE']['BODY'] - str_header = "SERVICE #{document_hash['DOCUMENT']['ID']} "\ - 'INFORMATION' - CLIHelper.print_header(str_h1 % str_header) - - puts Kernel.format(str, 'ID', document_hash['DOCUMENT']['ID']) - puts Kernel.format(str, 'NAME', document_hash['DOCUMENT']['NAME']) - puts Kernel.format(str, 'USER', document_hash['DOCUMENT']['UNAME']) - puts Kernel.format(str, 'GROUP', document_hash['DOCUMENT']['GNAME']) - - puts Kernel.format(str, 'STRATEGY', template['deployment']) - puts Kernel.format(str, - 'SERVICE STATE', - Service.state_str(template['state'])) - if template['shutdown_action'] - puts Kernel.format(str, 'SHUTDOWN', template['shutdown_action']) - end - - puts - - CLIHelper.print_header(str_h1 % 'PERMISSIONS', false) - - %w[OWNER GROUP OTHER].each do |e| - mask = '---' - permissions_hash = document_hash['DOCUMENT']['PERMISSIONS'] - mask[0] = 'u' if permissions_hash["#{e}_U"] == '1' - mask[1] = 'm' if permissions_hash["#{e}_M"] == '1' - mask[2] = 'a' if permissions_hash["#{e}_A"] == '1' - - puts Kernel.format(str, e, mask) - end - - puts - - template['roles'].each do |role| - CLIHelper.print_header("ROLE #{role['name']}", false) - - puts Kernel.format(str, - 'ROLE STATE', - Role.state_str(role['state'])) - if role['parents'] - puts Kernel.format(str, - 'PARENTS', - role['parents'].join(', ')) - end - puts Kernel.format(str, 'VM TEMPLATE', role['vm_template']) - puts Kernel.format(str, 'CARDINALITY', role['cardinality']) - if role['min_vms'] - puts Kernel.format(str, 'MIN VMS', role['min_vms']) - end - if role['max_vms'] - puts Kernel.format(str, 'MAX VMS', role['max_vms']) - end - if role['coolddown'] - puts Kernel.format(str, 'COOLDOWN', "#{role['cooldown']}s") - end - if role['shutdown_action'] - puts Kernel.format(str, 'SHUTDOWN', role['shutdown_action']) - end - - puts 'NODES INFORMATION' - NODE_TABLE.show(role['nodes']) - - if !role['elasticity_policies'].nil? && - !role['elasticity_policies'].empty? || - !role['scheduled_policies'].nil? && - !role['scheduled_policies'].empty? - puts - puts 'ELASTICITY RULES' - - if role['elasticity_policies'] && - !role['elasticity_policies'].empty? - puts - # puts 'ELASTICITY POLICIES' - CLIHelper::ShowTable.new(nil, self) do - column :ADJUST, '', :left, :size => 12 do |d| - adjust_str(d) - end - - column :EXPRESSION, '', :left, :size => 48 do |d| - if !d['expression_evaluated'].nil? - d['expression_evaluated'] - else - d['expression'] - end - end - - column :EVALS, '', :right, :size => 5 do |d| - if d['period_number'] - "#{d['true_evals'].to_i}/"\ - "#{d['period_number']}" - else - '-' - end - end - - column :PERIOD, '', :size => 6 do |d| - d['period'] ? "#{d['period']}s" : '-' - end - - column :COOL, '', :size => 5 do |d| - d['cooldown'] ? "#{d['cooldown']}s" : '-' - end - - default :ADJUST, :EXPRESSION, :EVALS, :PERIOD, :COOL - end.show([role['elasticity_policies']].flatten, {}) - end - - if role['scheduled_policies'] && - !role['scheduled_policies'].empty? - puts - # puts 'SCHEDULED POLICIES' - CLIHelper::ShowTable.new(nil, self) do - column :ADJUST, '', :left, :size => 12 do |d| - adjust_str(d) - end - - column :TIME, '', :left, :size => 67 do |d| - if d['start_time'] - Time.parse(d['start_time']).to_s - else - d['recurrence'] - end - end - - default :ADJUST, :TIME - end.show([role['scheduled_policies']].flatten, {}) - end - end - - puts - end - - puts - - CLIHelper.print_header(str_h1 % 'LOG MESSAGES', false) - - if template['log'] - template['log'].each do |log| - t = Time.at(log['timestamp']).strftime('%m/%d/%y %H:%M') - puts "#{t} [#{log['severity']}] #{log['message']}" - end - end - - 0 - end - end -end - -def adjust_str(policy) - policy['adjust'].to_i >= 0 ? sign = '+' : sign = '-' - adjust = policy['adjust'].to_i.abs - - case policy['type'] - when 'CARDINALITY' - "= #{adjust}" - when 'PERCENTAGE_CHANGE' - st = "#{sign} #{adjust} %" - if policy['min_adjust_step'] - st << " (#{policy['min_adjust_step']})" - end - - st - else - "#{sign} #{adjust}" - end -end - -# -# Commands -# - CommandParser::CmdParser.new(ARGV) do usage '`oneflow` [] []' version OpenNebulaHelper::ONE_VERSION @@ -405,9 +52,19 @@ CommandParser::CmdParser.new(ARGV) do set :option, CommandParser::VERSION set :option, CommandParser::HELP - # + DONE = { + :name => 'done', + :large => '--done', + :description => 'Show services in DONE state' + } + + # create helper object + helper = OneFlowHelper.new + + ############################################################################ # Formatters for arguments - # + ############################################################################ + set :format, :groupid, OpenNebulaHelper.rname_to_id_desc('GROUP') do |arg| OpenNebulaHelper.rname_to_id(arg, 'GROUP') end @@ -424,326 +81,213 @@ CommandParser::CmdParser.new(ARGV) do Service.list_to_id(arg, 'SERVICE') end - set :format, :vm_action, + set :format, + :vm_action, 'Actions supported: #{Role::SCHEDULE_ACTIONS.join(', ')}' do |arg| if Role::SCHEDULE_ACTIONS.include?(arg) [0, arg] else - [-1, "Action #{arg} is not supported. Actions supported: "\ + [-1, "Action '#{arg}' is not supported. Supported actions: " \ "#{Role::SCHEDULE_ACTIONS.join(', ')}"] end end - # - # List - # + ### list_desc = <<-EOT.unindent List the available services EOT - command :list, list_desc, :options => Service::JSON_FORMAT do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - - list_services(client, options) + command :list, list_desc, :options => [Service::JSON_FORMAT, DONE] do + helper.list_service_pool(helper.client(options), options) end - # - # Show - # + ### + + top_desc = <<-EOT.unindent + Top the available services + EOT + + command :top, top_desc, :options => [CLIHelper::DELAY, DONE] do + Signal.trap('INT') { exit(-1) } + + helper.top_service_pool(helper.client(options), options) + + 0 + end + + ### show_desc = <<-EOT.unindent Show detailed information of a given service EOT command :show, show_desc, :service_id, :options => Service::JSON_FORMAT do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - - show_service(client, args, options) + helper.format_resource(helper.client(options), args[0], options) end - # - # Top - # - - top_desc = <<-EOT.unindent - Top the services or the extended information of the target service if a - id is specified - EOT - - command :top, top_desc, [:service_id, nil], - :options => [Service::JSON_FORMAT, - Service::TOP, - CLIHelper::DELAY] do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - - options[:delay] ? delay = options[:delay] : delay = 3 - - begin - loop do - CLIHelper.scr_cls - CLIHelper.scr_move(0, 0) - - if args[0] - rc, message = show_service(client, args, options) - else - rc, message = list_services(client, options) - end - - raise message if rc - - sleep delay - end - rescue StandardError => e - puts e.message - -1 - end - end - - # - # Delete - # + ### delete_desc = <<-EOT.unindent Delete a given service EOT command :delete, delete_desc, [:range, :service_id_list] do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - Service.perform_actions(args[0]) do |service_id| - client.delete("#{RESOURCE_PATH}/#{service_id}") + helper.client(options).delete("#{RESOURCE_PATH}/#{service_id}") end end - # - # Shutdown - # - - shutdown_desc = <<-EOT.unindent - Shutdown a service. - From RUNNING or WARNING shuts down the Service - EOT - - command :shutdown, shutdown_desc, [:range, :service_id_list] do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - - Service.perform_actions(args[0]) do |service_id| - json_action = Service.build_json_action('shutdown') - - client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) - end - end - - # - # Recover - # + ### recover_desc = <<-EOT.unindent Recover a failed service, cleaning the failed VMs. From FAILED_DEPLOYING continues deploying the Service From FAILED_SCALING continues scaling the Service From FAILED_UNDEPLOYING continues shutting down the Service - From COOLDOWN the Service is set to running ignoring the cooldown duration + From COOLDOWN the Service is set to running ignoring the cooldown From WARNING failed VMs are deleted, and new VMs are instantiated EOT command :recover, recover_desc, [:range, :service_id_list] do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - Service.perform_actions(args[0]) do |service_id| - json_action = Service.build_json_action('recover') - - client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + helper.client(options).post("#{RESOURCE_PATH}/#{service_id}/action", + Service.build_json_action('recover')) end end - # - # Scale - # + ### scale_desc = <<-EOT.unindent Scale a role to the given cardinality EOT - command :scale, scale_desc, :service_id, :role_name, - :cardinality, :options => [Service::FORCE] do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - + command :scale, + scale_desc, + :service_id, + :role_name, + :cardinality, + :options => [Service::FORCE] do if args[2] !~ /^\d+$/ - puts 'Cardinality must be an integer number' + STDERR.puts 'Cardinality must be an integer number' exit(-1) end - exit_code = 0 + json = "{ \"cardinality\" : #{args[2]},\n" \ + " \"force\" : #{options[:force] == true}, " \ + " \"role_name\" : \"#{args[1]}\"}" - json = "{ \"cardinality\" : #{args[2]},\n" \ - " \"force\" : #{options[:force] == true} }" - - response = client - .put("#{RESOURCE_PATH}/#{args[0]}/role/#{args[1]}", json) - - if CloudClient.is_error?(response) - puts response.to_s - exit_code = response.code.to_i + Service.perform_action(args[0]) do |service_id| + helper.client(options).post("#{RESOURCE_PATH}/#{service_id}/scale", + json) end - - exit_code end + ### + chgrp_desc = <<-EOT.unindent Changes the service group EOT command :chgrp, chgrp_desc, [:range, :service_id_list], :groupid do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - Service.perform_actions(args[0]) do |service_id| params = {} params['group_id'] = args[1].to_i - json_action = Service.build_json_action('chgrp', params) + json = Service.build_json_action('chgrp', params) - client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + helper.client(options).post("#{RESOURCE_PATH}/#{service_id}/action", + json) end end + ### + chown_desc = <<-EOT.unindent Changes the service owner and group EOT - command :chown, chown_desc, - [:range, :service_id_list], :userid, [:groupid, nil] do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - + command :chown, + chown_desc, + [:range, :service_id_list], + :userid, + [:groupid, nil] do Service.perform_actions(args[0]) do |service_id| params = {} params['owner_id'] = args[1] params['group_id'] = args[2] if args[2] - json_action = Service.build_json_action('chown', params) + json = Service.build_json_action('chown', params) - client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + helper.client(options).post("#{RESOURCE_PATH}/#{service_id}/action", + json) end end + ### + chmod_desc = <<-EOT.unindent Changes the service permissions EOT command :chmod, chmod_desc, [:range, :service_id_list], :octet do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) + if !/\A\d+\z/.match(args[1]) + STDERR.puts "Invalid '#{args[1]}' octed permissions" + exit(-1) + end Service.perform_actions(args[0]) do |service_id| params = {} params['octet'] = args[1] - json_action = Service.build_json_action('chmod', params) + json = Service.build_json_action('chmod', params) - client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + helper.client(options).post("#{RESOURCE_PATH}/#{service_id}/action", + json) end end + ### + rename_desc = <<-EOT.unindent Renames the Service EOT command :rename, rename_desc, :service_id, :name do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) + Service.perform_action(args[0]) do |service_id| + params = {} + params['name'] = args[1] - params = {} - params['name'] = args[1] + json = Service.build_json_action('rename', params) - json_action = Service.build_json_action('rename', params) - - response = client - .post("#{RESOURCE_PATH}/#{args[0]}/action", json_action) - - if CloudClient.is_error?(response) - [response.code.to_i, response.to_s] - else - response.code.to_i + helper.client(options).post("#{RESOURCE_PATH}/#{service_id}/action", + json) end end + ### + action_desc = <<-EOT.unindent Perform an action on all the Virtual Machines of a given role. Actions supported: #{Role::SCHEDULE_ACTIONS.join(',')} EOT - command :action, action_desc, :service_id, :role_name, :vm_action, + command :action, + action_desc, + :service_id, + :role_name, + :vm_action, :options => [Service::PERIOD, Service::NUMBER] do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - - Service.perform_actions([args[0]]) do |service_id| + Service.perform_action(args[0]) do |service_id| params = {} params[:period] = options[:period].to_i if options[:period] params[:number] = options[:number].to_i if options[:number] - json_action = Service.build_json_action(args[2], params) + json = Service.build_json_action(args[2], params) + client = helper.client(options) client.post("#{RESOURCE_PATH}/#{service_id}/role/#{args[1]}/action", - json_action) + json) end end end diff --git a/src/cli/oneflow-template b/src/cli/oneflow-template index ab037c79bb..de2ea68cf9 100755 --- a/src/cli/oneflow-template +++ b/src/cli/oneflow-template @@ -33,125 +33,19 @@ end $LOAD_PATH << RUBY_LIB_LOCATION $LOAD_PATH << RUBY_LIB_LOCATION + '/cli' +require 'json' +require 'English' + require 'command_parser' require 'opennebula/oneflow_client' -require 'English' require 'cli_helper' -require 'one_helper' - -require 'json' +require 'one_helper/oneflowtemplate_helper' USER_AGENT = 'CLI' # Base Path representing the resource to be used in the requests RESOURCE_PATH = '/service_template' -# -# Table -# - -TABLE = CLIHelper::ShowTable.new(nil, self) do - column :ID, 'ID', :size => 10 do |d| - d['ID'] - end - - column :USER, 'Username', :left, :size => 15 do |d| - d['UNAME'] - end - - column :GROUP, 'Group', :left, :size => 15 do |d| - d['GNAME'] - end - - column :NAME, 'Name', :left, :size => 37 do |d| - d['NAME'] - end - - default :ID, :USER, :GROUP, :NAME -end - -# Show the service template information. This method is used in top and -# show commands -# @param [Service::Client] client -# @param [Array] args -# @param [Hash] options -# @return [[Integer, String], Integer] Returns the exit_code and optionally -# a String to be printed -def show_service_template(client, args, options) - response = client.get("#{RESOURCE_PATH}/#{args[0]}") - - if CloudClient.is_error?(response) - [response.code.to_i, response.to_s] - else - # [0,response.body] - if options[:json] - [0, response.body] - else - str = '%-20s: %-20s' - str_h1 = '%-80s' - - document_hash = JSON.parse(response.body) - template = document_hash['DOCUMENT']['TEMPLATE']['BODY'] - - CLIHelper.print_header(str_h1 % - "SERVICE TEMPLATE #{document_hash['DOCUMENT']['ID']} "\ - 'INFORMATION') - - puts Kernel.format str, 'ID', document_hash['DOCUMENT']['ID'] - puts Kernel.format str, 'NAME', document_hash['DOCUMENT']['NAME'] - puts Kernel.format str, 'USER', document_hash['DOCUMENT']['UNAME'] - puts Kernel.format str, 'GROUP', document_hash['DOCUMENT']['GNAME'] - - puts - - CLIHelper.print_header(str_h1 % 'PERMISSIONS', false) - - %w[OWNER GROUP OTHER].each do |e| - mask = '---' - permissions_hash = document_hash['DOCUMENT']['PERMISSIONS'] - mask[0] = 'u' if permissions_hash["#{e}_U"] == '1' - mask[1] = 'm' if permissions_hash["#{e}_M"] == '1' - mask[2] = 'a' if permissions_hash["#{e}_A"] == '1' - - puts Kernel.format str, e, mask - end - - puts - - CLIHelper.print_header(str_h1 % 'TEMPLATE CONTENTS', false) - puts JSON.pretty_generate(template) - - 0 - end - end -end - -# List the services. This method is used in top and list commands -# @param [Service::Client] client -# @param [Hash] options -# @return [[Integer, String], Integer] Returns the exit_code and optionally -# a String to be printed -def list_service_templates(client, options) - response = client.get(RESOURCE_PATH) - - if CloudClient.is_error?(response) - [response.code.to_i, response.to_s] - else - # [0,response.body] - if options[:json] - [0, response.body] - else - array_list = JSON.parse(response.body) - TABLE.show(array_list['DOCUMENT_POOL']['DOCUMENT']) - 0 - end - end -end - -# -# Commands -# - CommandParser::CmdParser.new(ARGV) do usage '`oneflow-template` [] []' version OpenNebulaHelper::ONE_VERSION @@ -160,9 +54,12 @@ CommandParser::CmdParser.new(ARGV) do set :option, CommandParser::VERSION set :option, CommandParser::HELP - # + # create helper object + helper = OneFlowTemplateHelper.new + + ############################################################################ # Formatters for arguments - # + ############################################################################ set :format, :groupid, OpenNebulaHelper.rname_to_id_desc('GROUP') do |arg| OpenNebulaHelper.rname_to_id(arg, 'GROUP') end @@ -181,9 +78,7 @@ CommandParser::CmdParser.new(ARGV) do Service.list_to_id(arg, 'SERVICE TEMPLATE') end - # - # List - # + ### list_desc = <<-EOT.unindent List the available Service Templates @@ -197,12 +92,10 @@ CommandParser::CmdParser.new(ARGV) do :user_agent => USER_AGENT ) - list_service_templates(client, options) + helper.list_service_template_pool(client, options) end - # - # Top - # + ### top_desc = <<-EOT.unindent List the available Service Templates continuously @@ -219,30 +112,29 @@ CommandParser::CmdParser.new(ARGV) do :user_agent => USER_AGENT ) - options[:delay] ? delay = options[:delay] : delay = 3 + Signal.trap('INT') { exit(-1) } - begin - loop do - CLIHelper.scr_cls - CLIHelper.scr_move(0, 0) - - rc, message = list_service_templates(client, options) - - if rc != 0 - raise message - end - - sleep delay - end - rescue StandardError => e - puts e.message - -1 - end + helper.top_service_template_pool(client, options) end - # - # Create - # + ### + + show_desc = <<-EOT.unindent + Show detailed information of a given Service Template + EOT + + command :show, show_desc, :templateid, :options => Service::JSON_FORMAT do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT + ) + + helper.format_resource(client, args[0], options) + end + + ### create_desc = <<-EOT.unindent Create a new Service Template @@ -271,28 +163,7 @@ CommandParser::CmdParser.new(ARGV) do end end - # - # Show - # - - show_desc = <<-EOT.unindent - Show detailed information of a given Service Template - EOT - - command :show, show_desc, :templateid, :options => Service::JSON_FORMAT do - client = Service::Client.new( - :username => options[:username], - :password => options[:password], - :url => options[:server], - :user_agent => USER_AGENT - ) - - show_service_template(client, args, options) - end - - # - # Delete - # + ### delete_desc = <<-EOT.unindent Delete a given Service Template @@ -311,9 +182,7 @@ CommandParser::CmdParser.new(ARGV) do end end - # - # Instantiate - # + ### instantiate_desc = <<-EOT.unindent Instantiate a Service Template @@ -351,6 +220,8 @@ CommandParser::CmdParser.new(ARGV) do end end + ### + chgrp_desc = <<-EOT.unindent Changes the service template group EOT @@ -373,6 +244,8 @@ CommandParser::CmdParser.new(ARGV) do end end + ### + chown_desc = <<-EOT.unindent Changes the service template owner and group EOT @@ -397,6 +270,8 @@ CommandParser::CmdParser.new(ARGV) do end end + ### + chmod_desc = <<-EOT.unindent Changes the service template permissions EOT @@ -419,6 +294,8 @@ CommandParser::CmdParser.new(ARGV) do end end + ### + clone_desc = <<-EOT.unindent Creates a new Service Template from an existing one EOT @@ -452,6 +329,8 @@ CommandParser::CmdParser.new(ARGV) do end end + ### + rename_desc = <<-EOT.unindent Renames the Service Template EOT @@ -479,6 +358,8 @@ CommandParser::CmdParser.new(ARGV) do end end + ### + update_desc = <<-EOT.unindent Update the template contents. If a path is not provided the editor will be launched to modify the current content. diff --git a/src/flow/etc/oneflow-server.conf b/src/flow/etc/oneflow-server.conf index 4b8be87bb5..2d75fd6398 100644 --- a/src/flow/etc/oneflow-server.conf +++ b/src/flow/etc/oneflow-server.conf @@ -49,15 +49,15 @@ :action_number: 1 :action_period: 60 -# Default name for the Virtual Machines created by oneflow. You can use any +# Default name for the Virtual Machines and Virtual Networks created by oneflow. You can use any # of the following placeholders: # $SERVICE_ID # $SERVICE_NAME # $ROLE_NAME -# $VM_NUMBER +# $VM_NUMBER (onely for VM names) :vm_name_template: '$ROLE_NAME_$VM_NUMBER_(service_$SERVICE_ID)' - +#:vn_name_template: '$ROLE_NAME(service_$SERVICE_ID)' ############################################################# # Auth ############################################################# diff --git a/src/flow/lib/EventManager.rb b/src/flow/lib/EventManager.rb new file mode 100644 index 0000000000..03c78299a7 --- /dev/null +++ b/src/flow/lib/EventManager.rb @@ -0,0 +1,322 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2019, OpenNebula Project, OpenNebula Systems # +# # +# 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 'ActionManager' +require 'ffi-rzmq' + +# OneFlow Event Manager +class EventManager + + attr_writer :lcm + attr_reader :am + + LOG_COMP = 'EM' + + ACTIONS = { + 'WAIT_DEPLOY' => :wait_deploy, + 'WAIT_UNDEPLOY' => :wait_undeploy, + 'WAIT_SCALEUP' => :wait_scaleup, + 'WAIT_SCALEDOWN' => :wait_scaledown, + 'WAIT_COOLDOWN' => :wait_cooldown + } + + FAILURE_STATES = %w[ + BOOT_FAILURE + BOOT_MIGRATE_FAILURE + PROLOG_MIGRATE_FAILURE + PROLOG_FAILURE + EPILOG_FAILURE + EPILOG_STOP_FAILURE + EPILOG_UNDEPLOY_FAILURE + PROLOG_MIGRATE_POWEROFF_FAILURE + PROLOG_MIGRATE_SUSPEND_FAILURE + PROLOG_MIGRATE_UNKNOWN_FAILURE + BOOT_UNDEPLOY_FAILURE + BOOT_STOPPED_FAILURE + PROLOG_RESUME_FAILURE + PROLOG_UNDEPLOY_FAILURE + ] + + # -------------------------------------------------------------------------- + # Default configuration options for the module + # -------------------------------------------------------------------------- + DEFAULT_CONF = { + :subscriber_endpoint => 'tcp://localhost:2101', + :timeout_s => 30, + :concurrency => 10, + :cloud_auth => nil, + :am => nil + } + + def initialize(options) + @conf = DEFAULT_CONF.merge(options) + + @lcm = options[:lcm] + @am = ActionManager.new(@conf[:concurrency], true) + + @context = ZMQ::Context.new(1) + @cloud_auth = @conf[:cloud_auth] + + # Register Action Manager actions + @am.register_action(ACTIONS['WAIT_DEPLOY'], method('wait_deploy_action')) + @am.register_action(ACTIONS['WAIT_UNDEPLOY'], method('wait_undeploy_action')) + @am.register_action(ACTIONS['WAIT_COOLDOWN'], method('wait_cooldown')) + @am.register_action(ACTIONS['WAIT_SCALEUP'], method('wait_scaleup_action')) + @am.register_action(ACTIONS['WAIT_SCALEDOWN'], method('wait_scaledown_action')) + Thread.new { @am.start_listener } + end + + ############################################################################ + # Actions + ############################################################################ + + # Wait for nodes to be in RUNNING if OneGate check required it will trigger + # another action after VMs are RUNNING + # @param [Service] service the service + # @param [Role] the role which contains the VMs + # @param [Node] nodes the list of nodes (VMs) to wait for + def wait_deploy_action(client, service_id, role_name, nodes) + Log.info LOG_COMP, "Waiting #{nodes} to be (ACTIVE, RUNNING)" + rc = wait(nodes, 'ACTIVE', 'RUNNING') + + # Todo, check if OneGate confirmation is needed (trigger another action) + if rc[0] + @lcm.trigger_action(:deploy_cb, + service_id, + client, + service_id, + role_name) + else + @lcm.trigger_action(:deploy_failure_cb, + service_id, + client, + service_id, + role_name) + end + end + + # Wait for nodes to be in DONE + # @param [service_id] the service id + # @param [role_name] the role name of the role which contains the VMs + # @param [nodes] the list of nodes (VMs) to wait for + def wait_undeploy_action(client, service_id, role_name, nodes) + Log.info LOG_COMP, "Waiting #{nodes} to be (DONE, LCM_INIT)" + rc = wait(nodes, 'DONE', 'LCM_INIT') + + if rc[0] + @lcm.trigger_action(:undeploy_cb, + service_id, + client, + service_id, + role_name, + rc[1]) + else + @lcm.trigger_action(:undeploy_failure_cb, + service_id, + client, + service_id, + role_name, + rc[1]) + end + end + + # Wait for nodes to be in RUNNING if OneGate check required it will trigger + # another action after VMs are RUNNING + # @param [Service] service the service + # @param [Role] the role which contains the VMs + # @param [Node] nodes the list of nodes (VMs) to wait for + # @param [Bool] up true if scalling up false otherwise + def wait_scaleup_action(client, service_id, role_name, nodes) + Log.info LOG_COMP, "Waiting #{nodes} to be (ACTIVE, RUNNING)" + + rc = wait(nodes, 'ACTIVE', 'RUNNING') + + # Todo, check if OneGate confirmation is needed (trigger another action) + if rc[0] + @lcm.trigger_action(:scaleup_cb, + service_id, + client, + service_id, + role_name) + else + @lcm.trigger_action(:scaleup_failure_cb, + service_id, + client, + service_id, + role_name) + end + end + + def wait_scaledown_action(client, service_id, role_name, nodes) + Log.info LOG_COMP, "Waiting #{nodes} to be (DONE, LCM_INIT)" + + rc = wait(nodes, 'DONE', 'LCM_INIT') + + # Todo, check if OneGate confirmation is needed (trigger another action) + if rc[0] + @lcm.trigger_action(:scaledown_cb, + service_id, + client, + service_id, + role_name, + rc[1]) + else + @lcm.trigger_action(:scaledown_failure_cb, + service_id, + client, + service_id, + role_name, + rc[1]) + end + end + + # Wait for nodes to be in DONE + # @param [service_id] the service id + # @param [role_name] the role name of the role which contains the VMs + # @param [nodes] the list of nodes (VMs) to wait for + def wait_cooldown(client, service_id, role_name, cooldown_time) + Log.info LOG_COMP, "Waiting #{cooldown_time}s for cooldown for " \ + "service #{service_id} and role #{role_name}." + + sleep cooldown_time + + @lcm.trigger_action(:cooldown_cb, + service_id, + client, + service_id, + role_name) + end + + private + + ############################################################################ + # Helpers + ############################################################################ + + def retrieve_id(key) + key.split('/')[-1].to_i + end + + def wait(nodes, state, lcm_state) + subscriber = gen_subscriber + + rc_nodes = { :successful => [], :failure => [] } + + return [true, rc_nodes] if nodes.empty? + + nodes.each do |node| + subscribe(node, state, lcm_state, subscriber) + end + + key = '' + content = '' + + until nodes.empty? + rc = subscriber.recv_string(key) + rc = subscriber.recv_string(content) if rc == 0 + + if rc == -1 && ZMQ::Util.errno != ZMQ::EAGAIN + Log.error LOG_COMP, 'Error reading from subscriber.' + elsif rc == -1 + Log.info LOG_COMP, "Timeout reached for VM #{nodes} =>"\ + " (#{state}, #{lcm_state})" + + rc = check_nodes(nodes, state, lcm_state, subscriber) + + rc_nodes[:successful].concat(rc[:successful]) + rc_nodes[:failure].concat(rc[:failure]) + + next if !nodes.empty? && rc_nodes[:failure].empty? + + # If any node is in error wait action will fails + return [false, rc_nodes] unless rc_nodes[:failure].empty? + + return [true, rc_nodes] # (nodes.empty? && fail_nodes.empty?) + end + + id = retrieve_id(key) + Log.info LOG_COMP, "Node #{id} reached (#{state},#{lcm_state})" + + nodes.delete(id) + unsubscribe(id, state, lcm_state, subscriber) + rc_nodes[:successful] << id + end + + [true, rc_nodes] + end + + def check_nodes(nodes, state, lcm_state, subscriber) + rc_nodes = { :successful => [], :failure => [] } + + nodes.delete_if do |node| + vm = OpenNebula::VirtualMachine + .new_with_id(node, @cloud_auth.client) + + vm.info + + vm_state = OpenNebula::VirtualMachine::VM_STATE[vm.state] + vm_lcm_state = OpenNebula::VirtualMachine::LCM_STATE[vm.lcm_state] + + if vm_state == 'DONE' || + (vm_state == state && vm_lcm_state == lcm_state) + unsubscribe(node, state, lcm_state, subscriber) + + rc_nodes[:successful] << node + next true + end + + if FAILURE_STATES.include? vm_lcm_state + Log.error LOG_COMP, "Node #{node} is in FAILURE state" + + rc_nodes[:failure] << node + + next true + end + + false + end + + rc_nodes + end + + ############################################################################ + # Functionns to subscribe/unsuscribe to event changes on VM + ############################################################################ + + def gen_subscriber + subscriber = @context.socket(ZMQ::SUB) + # Set timeout (TODO add option for customize timeout) + subscriber.setsockopt(ZMQ::RCVTIMEO, @conf[:timeout_s] * 10**3) + subscriber.connect(@conf[:subscriber_endpoint]) + + subscriber + end + + def subscribe(vm_id, state, lcm_state, subscriber) + subscriber.setsockopt(ZMQ::SUBSCRIBE, + gen_filter(vm_id, state, lcm_state)) + end + + def unsubscribe(vm_id, state, lcm_state, subscriber) + subscriber.setsockopt(ZMQ::UNSUBSCRIBE, + gen_filter(vm_id, state, lcm_state)) + end + + def gen_filter(vm_id, state, lcm_state) + "EVENT STATE VM/#{state}/#{lcm_state}/#{vm_id}" + end + +end diff --git a/src/flow/lib/LifeCycleManager.rb b/src/flow/lib/LifeCycleManager.rb index 6bc015cd03..5d572997b1 100644 --- a/src/flow/lib/LifeCycleManager.rb +++ b/src/flow/lib/LifeCycleManager.rb @@ -15,163 +15,686 @@ #--------------------------------------------------------------------------- # require 'strategy' +require 'ActionManager' +# Service Life Cycle Manager class ServiceLCM - LOG_COMP = "LCM" + attr_writer :event_manager + attr_reader :am - def initialize(sleep_time, cloud_auth) - @sleep_time = sleep_time + LOG_COMP = 'LCM' + + ACTIONS = { + # Callbacks + 'DEPLOY_CB' => :deploy_cb, + 'DEPLOY_FAILURE_CB' => :deploy_failure_cb, + 'UNDEPLOY_CB' => :undeploy_cb, + 'UNDEPLOY_FAILURE_CB' => :undeploy_failure_cb, + 'COOLDOWN_CB' => :cooldown_cb, + 'SCALEUP_CB' => :scaleup_cb, + 'SCALEUP_FAILURE_CB' => :scaleup_failure_cb, + 'SCALEDOWN_CB' => :scaledown_cb, + 'SCALEDOWN_FAILURE_CB' => :scaledown_failure_cb + } + + def initialize(client, concurrency, cloud_auth) @cloud_auth = cloud_auth + @am = ActionManager.new(concurrency, true) + @srv_pool = ServicePool.new(@cloud_auth, nil) + + em_conf = { + :cloud_auth => @cloud_auth, + :concurrency => 10, + :lcm => @am + } + + @event_manager = EventManager.new(em_conf).am + + # Register Action Manager actions + @am.register_action(ACTIONS['DEPLOY_CB'], + method('deploy_cb')) + @am.register_action(ACTIONS['DEPLOY_FAILURE_CB'], + method('deploy_failure_cb')) + @am.register_action(ACTIONS['UNDEPLOY_CB'], + method('undeploy_cb')) + @am.register_action(ACTIONS['UNDEPLOY_FAILURE_CB'], + method('undeploy_failure_cb')) + @am.register_action(ACTIONS['SCALEUP_CB'], + method('scaleup_cb')) + @am.register_action(ACTIONS['SCALEUP_FAILURE_CB'], + method('scaleup_failure_cb')) + @am.register_action(ACTIONS['SCALEDOWN_CB'], + method('scaledown_cb')) + @am.register_action(ACTIONS['SCALEDOWN_FAILURE_CB'], + method('scaledown_failure_cb')) + @am.register_action(ACTIONS['COOLDOWN_CB'], + method('cooldown_cb')) + + Thread.new { @am.start_listener } + + Thread.new { catch_up(client) } end - def loop() - Log.info LOG_COMP, "Starting Life Cycle Manager" + # Change service ownership + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # @param u_id [Integer] User ID + # @param g_id [Integer] Group ID + # + # @return [OpenNebula::Error] Error if any + def chown_action(client, service_id, u_id, g_id) + rc = @srv_pool.get(service_id, client) do |service| + service.chown(u_id, g_id) + end - while true - srv_pool = ServicePool.new(@cloud_auth.client) + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) - rc = srv_pool.info_all() + rc + end - if OpenNebula.is_error?(rc) - Log.error LOG_COMP, "Error retrieving the Service Pool: #{rc.message}" - else - srv_pool.each_xpath('DOCUMENT/ID') { |id| - rc_get = srv_pool.get(id.to_i) { |service| - owner_client = @cloud_auth.client(service.owner_name) - service.replace_client(owner_client) + # Change service permissions + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # @param octet [Integer] Permissions in octet format + # + # @return [OpenNebula::Error] Error if any + def chmod_action(client, service_id, octet) + rc = @srv_pool.get(service_id, client) do |service| + service.chmod_octet(octet) + end - Log.debug LOG_COMP, "Loop for service #{service.id()} #{service.name()}" \ - " #{service.state_str()} #{service.strategy()}" + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) - strategy = get_deploy_strategy(service) + rc + end - case service.state() - when Service::STATE['PENDING'] - service.set_state(Service::STATE['DEPLOYING']) + # Change service name + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # @param new_name [String] New service name + # + # @return [OpenNebula::Error] Error if any + def rename_action(client, service_id, new_name) + rc = @srv_pool.get(service_id, client) do |service| + service.rename(new_name) + end - rc = strategy.boot_step(service) - if !rc[0] - service.set_state(Service::STATE['FAILED_DEPLOYING']) - end - when Service::STATE['DEPLOYING'] - strategy.monitor_step(service) + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) - if service.all_roles_running? - service.set_state(Service::STATE['RUNNING']) - elsif service.any_role_failed? - service.set_state(Service::STATE['FAILED_DEPLOYING']) - else - rc = strategy.boot_step(service) - if !rc[0] - service.set_state(Service::STATE['FAILED_DEPLOYING']) - end - end - when Service::STATE['RUNNING'], Service::STATE['WARNING'] - strategy.monitor_step(service) + rc + end - if service.all_roles_running? - if service.state() == Service::STATE['WARNING'] - service.set_state(Service::STATE['RUNNING']) - end - else - if service.state() == Service::STATE['RUNNING'] - service.set_state(Service::STATE['WARNING']) - end - end + # Add shced action to service role + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # @param role_name [String] Role to add action + # @param action [String] Action to perform + # @param period [Integer] When to execute the action + # @param number [Integer] How many VMs per period + # + # @return [OpenNebula::Error] Error if any + def sched_action(client, service_id, role_name, action, period, number) + rc = @srv_pool.get(service_id, client) do |service| + role = service.roles[role_name] - if strategy.apply_scaling_policies(service) - service.set_state(Service::STATE['SCALING']) - - rc = strategy.scale_step(service) - if !rc[0] - service.set_state(Service::STATE['FAILED_SCALING']) - end - end - when Service::STATE['SCALING'] - strategy.monitor_step(service) - - if service.any_role_failed_scaling? - service.set_state(Service::STATE['FAILED_SCALING']) - elsif service.any_role_cooldown? - service.set_state(Service::STATE['COOLDOWN']) - elsif !service.any_role_scaling? - service.set_state(Service::STATE['RUNNING']) - else - rc = strategy.scale_step(service) - if !rc[0] - service.set_state(Service::STATE['FAILED_SCALING']) - end - end - when Service::STATE['COOLDOWN'] - strategy.monitor_step(service) - - if !service.any_role_cooldown? - service.set_state(Service::STATE['RUNNING']) - end - when Service::STATE['FAILED_SCALING'] - strategy.monitor_step(service) - - if !service.any_role_failed_scaling? - service.set_state(Service::STATE['SCALING']) - end - when Service::STATE['UNDEPLOYING'] - strategy.monitor_step(service) - - if service.all_roles_done? - service.set_state(Service::STATE['DONE']) - elsif service.any_role_failed? - service.set_state(Service::STATE['FAILED_UNDEPLOYING']) - else - rc = strategy.shutdown_step(service) - if !rc[0] - service.set_state(Service::STATE['FAILED_UNDEPLOYING']) - end - end - when Service::STATE['FAILED_DEPLOYING'] - strategy.monitor_step(service) - - if !service.any_role_failed? - service.set_state(Service::STATE['DEPLOYING']) - end - when Service::STATE['FAILED_UNDEPLOYING'] - strategy.monitor_step(service) - - if !service.any_role_failed? - service.set_state(Service::STATE['UNDEPLOYING']) - end - end - - rc = service.update() - if OpenNebula.is_error?(rc) - Log.error LOG_COMP, "Error trying to update " << - "Service #{service.id()} : #{rc.message}" - end - } - - if OpenNebula.is_error?(rc_get) - Log.error LOG_COMP, "Error getting Service " << - "#{id}: #{rc_get.message}" - end - } + if role.nil? + break OpenNebula::Error.new("Role '#{role_name}' not found") end - sleep @sleep_time + role.batch_action(action, period, number) + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + + # Create new service + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # + # @return [OpenNebula::Error] Error if any + def deploy_action(client, service_id) + rc = @srv_pool.get(service_id, client) do |service| + # Create vnets only first time action is called + if service.state == Service::STATE['PENDING'] + rc = service.deploy_networks + + if OpenNebula.is_error?(rc) + service.set_state(Service::STATE['FAILED_DEPLOYING']) + service.update + + break rc + end + end + + set_deploy_strategy(service) + + roles = service.roles_deploy + + # Maybe roles.empty? because are being deploying in other threads + if roles.empty? + if service.all_roles_running? + service.set_state(Service::STATE['RUNNING']) + service.update + end + + # If there is no node in PENDING the service is not modified. + break + end + + rc = deploy_roles(client, + roles, + 'DEPLOYING', + 'FAILED_DEPLOYING', + false) + + if !OpenNebula.is_error?(rc) + service.set_state(Service::STATE['DEPLOYING']) + else + service.set_state(Service::STATE['FAILED_DEPLOYING']) + end + + service.update + + rc + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + + # Delete service + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # + # @return [OpenNebula::Error] Error if any + def undeploy_action(client, service_id) + rc = @srv_pool.get(service_id, client) do |service| + unless service.can_undeploy? + break OpenNebula::Error.new( + 'Service cannot be undeployed in state: ' \ + "#{service.state_str}" + ) + end + + set_deploy_strategy(service) + + roles = service.roles_shutdown + + # If shutdown roles is empty, asume the service is in DONE and exit + if roles.empty? + if service.all_roles_done? + service.set_state(Service::STATE['DONE']) + service.update + end + + break + end + + rc = undeploy_roles(client, + roles, + 'UNDEPLOYING', + 'FAILED_UNDEPLOYING', + false) + + if !OpenNebula.is_error?(rc) + service.set_state(Service::STATE['UNDEPLOYING']) + else + service.set_state(Service::STATE['FAILED_UNDEPLOYING']) + end + + service.update + + rc + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + + # Scale service + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # @param role_name [String] Role to scale + # @param cardinality [Integer] Number of VMs to scale + # @param force [Boolean] True to force scaling + # + # @return [OpenNebula::Error] Error if any + def scale_action(client, service_id, role_name, cardinality, force) + rc = @srv_pool.get(service_id, client) do |service| + unless service.can_scale? + break OpenNebula::Error.new( + "Service cannot be scaled in state: #{service.state_str}" + ) + end + + role = service.roles[role_name] + + if role.nil? + break OpenNebula::Error.new("Role #{role_name} not found") + end + + rc = nil + cardinality_diff = cardinality - role.cardinality + + set_cardinality(role, cardinality, force) + + if cardinality_diff > 0 + role.scale_way('UP') + + rc = deploy_roles(client, + { role_name => role }, + 'SCALING', + 'FAILED_SCALING', + true) + elsif cardinality_diff < 0 + role.scale_way('DOWN') + + rc = undeploy_roles(client, + { role_name => role }, + 'SCALING', + 'FAILED_SCALING', + true) + else + break OpenNebula::Error.new( + "Cardinality of #{role_name} is already at #{cardinality}" + ) + end + + if !OpenNebula.is_error?(rc) + service.set_state(Service::STATE['SCALING']) + else + service.set_state(Service::STATE['FAILED_SCALING']) + end + + service.update + + rc + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + + # Recover service + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # + # @return [OpenNebula::Error] Error if any + def recover_action(client, service_id) + # TODO, kill other proceses? (other recovers) + rc = @srv_pool.get(service_id, client) do |service| + if service.can_recover_deploy? + recover_deploy(client, service) + elsif service.can_recover_undeploy? + recover_undeploy(client, service) + elsif service.can_recover_scale? + recover_scale(client, service) + else + break OpenNebula::Error.new( + 'Service cannot be recovered in state: ' \ + "#{service.state_str}" + ) + end + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + + private + + ############################################################################ + # Callbacks + ############################################################################ + + def deploy_cb(client, service_id, role_name) + rc = @srv_pool.get(service_id, client) do |service| + service.roles[role_name].set_state(Role::STATE['RUNNING']) + + if service.all_roles_running? + service.set_state(Service::STATE['RUNNING']) + elsif service.strategy == 'straight' + set_deploy_strategy(service) + + deploy_roles(client, + service.roles_deploy, + 'DEPLOYING', + 'FAILED_DEPLOYING', + false) + end + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def deploy_failure_cb(client, service_id, role_name) + rc = @srv_pool.get(service_id, client) do |service| + # stop actions for the service if deploy fails + @event_manager.cancel_action(service_id) + + service.set_state(Service::STATE['FAILED_DEPLOYING']) + service.roles[role_name].set_state(Role::STATE['FAILED_DEPLOYING']) + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def undeploy_cb(client, service_id, role_name, nodes) + rc = @srv_pool.get(service_id, client) do |service| + service.roles[role_name].set_state(Role::STATE['DONE']) + + service.roles[role_name].nodes.delete_if do |node| + !nodes[:failure].include?(node['deploy_id']) && + nodes[:successful].include?(node['deploy_id']) + end + + if service.all_roles_done? + rc = service.delete_networks + + if rc && !rc.empty? + Log.info LOG_COMP, 'Error trying to delete '\ + "Virtual Networks #{rc}" + end + + service.set_state(Service::STATE['DONE']) + elsif service.strategy == 'straight' + set_deploy_strategy(service) + + undeploy_roles(client, + service.roles_shutdown, + 'UNDEPLOYING', + 'FAILED_UNDEPLOYING', + false) + end + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def undeploy_failure_cb(client, service_id, role_name, nodes) + rc = @srv_pool.get(service_id, client) do |service| + # stop actions for the service if deploy fails + @event_manager.cancel_action(service_id) + + service.set_state(Service::STATE['FAILED_UNDEPLOYING']) + service.roles[role_name].set_state(Role::STATE['FAILED_UNDEPLOYING']) + + service.roles[role_name].nodes.delete_if do |node| + !nodes[:failure].include?(node['deploy_id']) && + nodes[:successful].include?(node['deploy_id']) + end + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def scaleup_cb(client, service_id, role_name) + rc = @srv_pool.get(service_id, client) do |service| + service.set_state(Service::STATE['COOLDOWN']) + service.roles[role_name].set_state(Role::STATE['COOLDOWN']) + @event_manager.trigger_action(:wait_cooldown, + service.id, + client, + service.id, + role_name, + 10) # TODO, config time + + service.roles[role_name].clean_scale_way + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def scaledown_cb(client, service_id, role_name, nodes) + rc = @srv_pool.get(service_id, client) do |service| + service.set_state(Service::STATE['COOLDOWN']) + service.roles[role_name].set_state(Role::STATE['COOLDOWN']) + + service.roles[role_name].nodes.delete_if do |node| + !nodes[:failure].include?(node['deploy_id']) && + nodes[:successful].include?(node['deploy_id']) + end + + @event_manager.trigger_action(:wait_cooldown, + service.id, + client, + service.id, + role_name, + 10) # TODO, config time + + service.roles[role_name].clean_scale_way + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def scaleup_failure_cb(client, service_id, role_name) + rc = @srv_pool.get(service_id, client) do |service| + # stop actions for the service if deploy fails + @event_manager.cancel_action(service_id) + + service.set_state(Service::STATE['FAILED_SCALING']) + service.roles[role_name].set_state(Role::STATE['FAILED_SCALING']) + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def scaledown_failure_cb(client, service_id, role_name, nodes) + rc = @srv_pool.get(service_id, client) do |service| + # stop actions for the service if deploy fails + @event_manager.cancel_action(service_id) + + role = service.roles[role_name] + + service.set_state(Service::STATE['FAILED_SCALING']) + role.set_state(Role::STATE['FAILED_SCALING']) + + role.nodes.delete_if do |node| + !nodes[:failure].include?(node['deploy_id']) && + nodes[:successful].include?(node['deploy_id']) + end + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def cooldown_cb(client, service_id, role_name) + rc = @srv_pool.get(service_id, client) do |service| + service.set_state(Service::STATE['RUNNING']) + service.roles[role_name].set_state(Role::STATE['RUNNING']) + + service.update + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + ############################################################################ + # Helpers + ############################################################################ + + # Iterate through the services for catching up with the state of each servic + # used when the LCM starts + def catch_up(client) + Log.error LOG_COMP, 'Catching up...' + + @srv_pool.info + + @srv_pool.each do |service| + recover_action(client, service.id) if service.transient_state? end end -private # Returns the deployment strategy for the given Service # @param [Service] service the service - # @return [Strategy] the deployment Strategy - def get_deploy_strategy(service) - strategy = Strategy.new - + # rubocop:disable Naming/AccessorMethodName + def set_deploy_strategy(service) + # rubocop:enable Naming/AccessorMethodName case service.strategy when 'straight' - strategy.extend(Straight) + service.extend(Straight) + else + service.extend(Strategy) + end + end + + # Returns true if the deployments of all roles was fine and + # update their state consequently + # @param [Array] roles to be deployed + # @param [Role::STATE] success_state new state of the role + # if deployed successfuly + # @param [Role::STATE] error_state new state of the role + # if deployed unsuccessfuly + def deploy_roles(client, roles, success_state, error_state, scale) + if scale + action = :wait_scaleup + else + action = :wait_deploy end - return strategy + rc = roles.each do |name, role| + rc = role.deploy + + if !rc[0] + role.set_state(Role::STATE[error_state]) + break OpenNebula::Error.new("Error deploying role #{name}") + end + + role.set_state(Role::STATE[success_state]) + + @event_manager.trigger_action(action, + role.service.id, + client, + role.service.id, + role.name, + rc[0]) + end + + rc end + + def undeploy_roles(client, roles, success_state, error_state, scale) + if scale + action = :wait_scaledown + else + action = :wait_undeploy + end + + rc = roles.each do |name, role| + rc = role.shutdown(false) + + if !rc[0] + role.set_state(Role::STATE[error_state]) + break OpenNebula::Error.new("Error undeploying role #{name}") + end + + role.set_state(Role::STATE[success_state]) + + # TODO, take only subset of nodes which needs to be undeployed (new role.nodes_undeployed_ids ?) + @event_manager.trigger_action(action, + role.service.id, + client, + role.service.id, + role.name, + rc[0]) + end + + rc + end + + def set_cardinality(role, cardinality, force) + tmpl_json = "{ \"cardinality\" : #{cardinality},\n" \ + " \"force\" : #{force} }" + + rc = role.update(JSON.parse(tmpl_json)) + + return rc if OpenNebula.is_error?(rc) + + nil + end + + def recover_deploy(client, service) + service.roles.each do |name, role| + next unless role.can_recover_deploy? + + nodes = role.recover_deploy + + @event_manager.trigger_action(:wait_deploy, + service.id, + client, + service.id, + name, + nodes) + end + end + + def recover_undeploy(client, service) + service.roles.each do |name, role| + next unless role.can_recover_undeploy? + + nodes = role.recover_undeploy + + @event_manager.trigger_action(:wait_undeploy, + service.id, + client, + service.id, + name, + nodes) + end + end + + def recover_scale(client, service) + service.roles.each do |name, role| + next unless role.can_recover_scale? + + nodes, up = role.recover_scale + + if up + action = :wait_scaleup + else + action = :wait_scaledown + end + + @event_manager.trigger_action(action, + service.id, + client, + service.id, + name, + nodes) + end + end + end diff --git a/src/flow/lib/models/role.rb b/src/flow/lib/models/role.rb index 437899f52c..d798375cc4 100644 --- a/src/flow/lib/models/role.rb +++ b/src/flow/lib/models/role.rb @@ -19,29 +19,33 @@ require 'treetop/version' require 'grammar' require 'parse-cron' -if !(Gem::Version.create(Treetop::VERSION::STRING) >= Gem::Version.create('1.6.3')) - raise "treetop gem version must be >= 1.6.3. Current version is #{Treetop::VERSION::STRING}" +if !(Gem::Version.create('1.6.3') < Gem.loaded_specs['treetop'].version) + raise 'treetop gem version must be >= 1.6.3.'\ + "Current version is #{Treetop::VERSION::STRING}" end module OpenNebula + + # Service Role class class Role + attr_reader :service # Actions that can be performed on the VMs of a given Role - SCHEDULE_ACTIONS = [ - 'terminate', - 'terminate-hard', - 'undeploy', - 'undeploy-hard', - 'hold', - 'release', - 'stop', - 'suspend', - 'resume', - 'reboot', - 'reboot-hard', - 'poweroff', - 'poweroff-hard', - 'snapshot-create' + SCHEDULE_ACTIONS = %w[ + terminate + terminate-hard + undeploy + undeploy-hard + hold + release + stop + suspend + resume + reboot + reboot-hard + poweroff + poweroff-hard + snapshot-create ] STATE = { @@ -58,21 +62,45 @@ module OpenNebula 'COOLDOWN' => 10 } - STATE_STR = [ - 'PENDING', - 'DEPLOYING', - 'RUNNING', - 'UNDEPLOYING', - 'WARNING', - 'DONE', - 'FAILED_UNDEPLOYING', - 'FAILED_DEPLOYING', - 'SCALING', - 'FAILED_SCALING', - 'COOLDOWN' + STATE_STR = %w[ + PENDING + DEPLOYING + RUNNING + UNDEPLOYING + WARNING + DONE + FAILED_UNDEPLOYING + FAILED_DEPLOYING + SCALING + FAILED_SCALING + COOLDOWN ] - LOG_COMP = "ROL" + RECOVER_DEPLOY_STATES = %w[ + FAILED_DEPLOYING + DEPLOYING + PENDING + ] + + RECOVER_UNDEPLOY_STATES = %w[ + FAILED_UNDEPLOYING + UNDEPLOYING + ] + + RECOVER_SCALE_STATES = %w[ + FAILED_SCALING + SCALING + ] + + SCALE_WAYS = { + 'UP' => 0, + 'DOWN' => 1 + } + + # VM information to save in document + VM_INFO = %w[ID UID GID UNAME GNAME NAME] + + LOG_COMP = 'ROL' def initialize(body, service) @body = body @@ -83,33 +111,73 @@ module OpenNebula end def name - return @body['name'] + @body['name'] end # Returns the role state # @return [Integer] the role state def state - return @body['state'].to_i + @body['state'].to_i + end + + def can_recover_deploy? + return RECOVER_DEPLOY_STATES.include? STATE_STR[state] if state != STATE['PENDING'] + + parents.each do |parent| + return false if @service.roles[parent].state != STATE['RUNNING'] + end + + true + end + + def can_recover_undeploy? + if !RECOVER_UNDEPLOY_STATES.include? STATE_STR[state] + # TODO, check childs if !empty? check if can be undeployed + @service.roles.each do |role_name, role| + next if role_name == name + + if role.parents.include? name + return false if role.state != STATE['DONE'] + end + end + end + + true + end + + def can_recover_scale? + return false unless RECOVER_SCALE_STATES.include? STATE_STR[state] + + true end # Returns the role parents # @return [Array] the role parents def parents - return @body['parents'] || [] + @body['parents'] || [] end # Returns the role cardinality # @return [Integer] the role cardinality def cardinality - return @body['cardinality'].to_i + @body['cardinality'].to_i end # Sets a new cardinality for this role # @param [Integer] the new cardinality + # rubocop:disable Naming/AccessorMethodName def set_cardinality(target_cardinality) - dir = target_cardinality > cardinality ? "up" : "down" - msg = "Role #{name} scaling #{dir} from #{cardinality} to #{target_cardinality} nodes" - Log.info LOG_COMP, msg, @service.id() + # rubocop:enable Naming/AccessorMethodName + if target_cardinality > cardinality + dir = 'up' + else + dir = 'down' + end + + msg = "Role #{name} scaling #{dir} from #{cardinality} to "\ + "#{target_cardinality} nodes" + + Log.info LOG_COMP, msg, @service.id @service.log_info(msg) @body['cardinality'] = target_cardinality.to_i @@ -117,7 +185,7 @@ module OpenNebula # Updates the cardinality with the current number of nodes def update_cardinality() - @body['cardinality'] = @body['nodes'].size() + @body['cardinality'] = @body['nodes'].size end # Returns the role max cardinality @@ -125,9 +193,9 @@ module OpenNebula def max_cardinality max = @body['max_vms'] - return nil if max.nil? + return if max.nil? - return max.to_i + max.to_i end # Returns the role min cardinality @@ -135,27 +203,33 @@ module OpenNebula def min_cardinality min = @body['min_vms'] - return nil if min.nil? + return if min.nil? - return min.to_i + min.to_i end # Returns the string representation of the service state # @return [String] the state string def state_str - return STATE_STR[state] + STATE_STR[state] end # Returns the nodes of the role # @return [Array] the nodes - def get_nodes + def nodes @body['nodes'] end + def nodes_ids + @body['nodes'].map {|node| node['deploy_id'] } + end + # Sets a new state # @param [Integer] the new state # @return [true, false] true if the value was changed + # rubocop:disable Naming/AccessorMethodName def set_state(state) + # rubocop:enable Naming/AccessorMethodName if state < 0 || state > STATE_STR.size return false end @@ -173,9 +247,19 @@ module OpenNebula end end - Log.info LOG_COMP, "Role #{name} new state: #{STATE_STR[state]}", @service.id() + Log.info LOG_COMP, + "Role #{name} new state: #{STATE_STR[state]}", + @service.id - return true + true + end + + def scale_way(way) + @body['scale_way'] = SCALE_WAYS[way] + end + + def clean_scale_way + @body.delete('scale_way') end # Retrieves the VM information for each Node in this Role. If a Node @@ -196,8 +280,9 @@ module OpenNebula rc = vm.info if OpenNebula.is_error?(rc) - msg = "Role #{name} : VM #{vm_id} monitorization failed; #{rc.message}" - Log.error LOG_COMP, msg, @service.id() + msg = "Role #{name} : VM #{vm_id} "\ + "monitorization failed; #{rc.message}" + Log.error LOG_COMP, msg, @service.id @service.log_error(msg) success = false @@ -215,7 +300,7 @@ module OpenNebula if running && @service.ready_status_gate running_status = node['vm_info']['VM']['USER_TEMPLATE']['READY'] || "" - running = running_status.upcase == "YES" + running = running_status.upcase == 'YES' end node['running'] = running @@ -238,7 +323,7 @@ module OpenNebula @body['nodes'] = new_nodes if !success - return OpenNebula::Error.new() + return OpenNebula::Error.new end return nil @@ -248,8 +333,11 @@ module OpenNebula # @return [Array, Array] true if all the VMs # were created, false and the error reason if there was a problem # creating the VMs - def deploy(scale_up=false) - n_nodes = cardinality() - get_nodes.size + def deploy + deployed_nodes = [] + n_nodes = cardinality - nodes.size + + return [deployed_nodes, nil] if n_nodes == 0 @body['last_vmname'] ||= 0 @@ -262,79 +350,90 @@ module OpenNebula # If the extra_template contains APPEND=",", it # will add the attributes that already exist in the template, # instead of replacing them. - append = extra_template.match(/^\s*APPEND=\"?(.*?)\"?\s*$/)[1].split(",") rescue nil + append = extra_template + .match(/^\s*APPEND=\"?(.*?)\"?\s*$/)[1] + .split(',') rescue nil if append && !append.empty? rc = template.info if OpenNebula.is_error?(rc) - msg = "Role #{name} : Info template #{template_id}; #{rc.message}" + msg = "Role #{name} : Info template #{template_id};"\ + " #{rc.message}" Log.error LOG_COMP, msg, @service.id() @service.log_error(msg) - return [false, "Error fetching Info to instantiate the VM Template" \ - " #{template_id} in Role #{self.name}: #{rc.message}"] + return [false, 'Error fetching Info to instantiate the'\ + " VM Template #{template_id} in Role "\ + "#{name}: #{rc.message}"] end - et = template.template_like_str("TEMPLATE", - true, append.join("|")) + et = template.template_like_str('TEMPLATE', + true, + append.join('|')) et = et << "\n" << extra_template extra_template = et end else - extra_template = "" + extra_template = '' end extra_template << - "\nSERVICE_ID = #{@service.id()}" << + "\nSERVICE_ID = #{@service.id}" \ "\nROLE_NAME = \"#{@body['name']}\"" - n_nodes.times { |i| - vm_name = @@vm_name_template. - gsub("$SERVICE_ID", @service.id().to_s). - gsub("$SERVICE_NAME", @service.name().to_s). - gsub("$ROLE_NAME", name().to_s). - gsub("$VM_NUMBER", @body['last_vmname'].to_s) + n_nodes.times do + vm_name = @@vm_name_template + .gsub('$SERVICE_ID', @service.id.to_s) + .gsub('$SERVICE_NAME', @service.name.to_s) + .gsub('$ROLE_NAME', name.to_s) + .gsub('$VM_NUMBER', @body['last_vmname'].to_s) @body['last_vmname'] += 1 - Log.debug LOG_COMP, "Role #{name} : Trying to instantiate template "\ - "#{template_id}, with name #{vm_name}", @service.id() + Log.debug LOG_COMP, "Role #{name} : Trying to instantiate "\ + "template #{template_id}, with name #{vm_name}", @service.id vm_id = template.instantiate(vm_name, false, extra_template) + deployed_nodes << vm_id + if OpenNebula.is_error?(vm_id) - msg = "Role #{name} : Instantiate failed for template #{template_id}; #{vm_id.message}" - Log.error LOG_COMP, msg, @service.id() + msg = "Role #{name} : Instantiate failed for template "\ + "#{template_id}; #{vm_id.message}" + Log.error LOG_COMP, msg, @service.id @service.log_error(msg) - return [false, "Error trying to instantiate the VM Template" \ - " #{template_id} in Role #{self.name}: #{vm_id.message}"] + return [false, 'Error trying to instantiate the VM ' \ + "Template #{template_id} in Role " \ + "#{name}: #{vm_id.message}"] end - Log.debug LOG_COMP, "Role #{name} : Instantiate success, VM ID #{vm_id}", @service.id() + Log.debug LOG_COMP, "Role #{name} : Instantiate success,"\ + " VM ID #{vm_id}", @service.id node = { - 'deploy_id' => vm_id, + 'deploy_id' => vm_id } - vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, + @service.client) rc = vm.info if OpenNebula.is_error?(rc) node['vm_info'] = nil else - node['vm_info'] = vm.to_hash - end + hash_vm = vm.to_hash['VM'] + vm_info = {} + vm_info['VM'] = hash_vm.select {|v| VM_INFO.include?(v) } - if scale_up - node['scale_up'] = '1' + node['vm_info'] = vm_info end @body['nodes'] << node - } + end - return [true, nil] + [deployed_nodes, nil] end # Terminate all the nodes in this role @@ -344,27 +443,25 @@ module OpenNebula # @return [Array, Array] true if all the VMs # were terminated, false and the error reason if there was a problem # shutting down the VMs - def shutdown(scale_down=false) - success = true - - nodes = get_nodes - - if scale_down - n_nodes = nodes.size - cardinality() + def shutdown(recover) + if nodes.size != cardinality + n_nodes = nodes.size - cardinality else n_nodes = nodes.size end - shutdown_nodes(nodes[0..n_nodes-1], scale_down) + rc = shutdown_nodes(nodes, n_nodes, recover) - return [success, nil] + return [false, "Error undeploying nodes for role #{id}"] unless rc[0] + + [rc[1], nil] end # Delete all the nodes in this role # @return [Array] All the VMs are deleted, and the return # ignored def delete - get_nodes.each { |node| + nodes.each do |node| vm_id = node['deploy_id'] Log.debug LOG_COMP, "Role #{name} : Deleting VM #{vm_id}", @service.id() @@ -382,12 +479,13 @@ module OpenNebula msg = "Role #{name} : Delete failed for VM #{vm_id}; #{rc.message}" Log.error LOG_COMP, msg, @service.id() @service.log_error(msg) + set_state(Role::STATE['FAILED_DELETING']) else Log.debug LOG_COMP, "Role #{name} : Delete success for VM #{vm_id}", @service.id() end - } + end - return [true, nil] + [true, nil] end # Changes the owner/group of all the nodes in this role @@ -399,7 +497,7 @@ module OpenNebula # were updated, false and the error reason if there was a problem # updating the VMs def chown(uid, gid) - get_nodes.each { |node| + nodes.each { |node| vm_id = node['deploy_id'] Log.debug LOG_COMP, "Role #{name} : Chown for VM #{vm_id}", @service.id() @@ -418,7 +516,7 @@ module OpenNebula end } - return [true, nil] + [true, nil] end # Schedule the given action on all the VMs that belong to the Role @@ -426,34 +524,41 @@ module OpenNebula # @param [Integer] period # @param [Integer] vm_per_period def batch_action(action, period, vms_per_period) - vms_id = [] - - # TODO: check action is a valid string, period vm_per_period integer - - error_msgs = [] - nodes = @body['nodes'] - now = Time.now.to_i - - do_offset = ( !period.nil? && period.to_i > 0 && - !vms_per_period.nil? && vms_per_period.to_i > 0 ) - + vms_id = [] + error_msgs = [] + nodes = @body['nodes'] + now = Time.now.to_i time_offset = 0 + # if role is done, return error + if state == 5 + return OpenNebula::Error.new("Role #{name} is in DONE state") + end + + do_offset = (!period.nil? && period.to_i > 0 && + !vms_per_period.nil? && vms_per_period.to_i > 0) + nodes.each_with_index do |node, index| vm_id = node['deploy_id'] - vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, + @service.client) + rc = vm.info if OpenNebula.is_error?(rc) - msg = "Role #{name} : VM #{vm_id} monitorization failed; #{rc.message}" + msg = "Role #{name} : VM #{vm_id} monitorization failed;"\ + " #{rc.message}" + error_msgs << msg - Log.error LOG_COMP, msg, @service.id() + + Log.error LOG_COMP, msg, @service.id + @service.log_error(msg) else ids = vm.retrieve_elements('USER_TEMPLATE/SCHED_ACTION/ID') id = 0 - if (!ids.nil? && !ids.empty?) + if !ids.nil? && !ids.empty? ids.map! {|e| e.to_i } id = ids.max + 1 end @@ -461,17 +566,22 @@ module OpenNebula tmp_str = vm.user_template_str if do_offset - time_offset = (index / vms_per_period.to_i).floor * period.to_i + offset = (index / vms_per_period.to_i).floor + time_offset = offset * period.to_i end - tmp_str << "\nSCHED_ACTION = "<< - "[ID = #{id}, ACTION = #{action}, TIME = #{now + time_offset}]" + tmp_str << "\nSCHED_ACTION = [ID = #{id},ACTION = "\ + "#{action}, TIME = #{now + time_offset}]" rc = vm.update(tmp_str) if OpenNebula.is_error?(rc) - msg = "Role #{name} : VM #{vm_id} error scheduling action; #{rc.message}" + msg = "Role #{name} : VM #{vm_id} error scheduling "\ + "action; #{rc.message}" + error_msgs << msg - Log.error LOG_COMP, msg, @service.id() + + Log.error LOG_COMP, msg, @service.id + @service.log_error(msg) else vms_id << vm.id @@ -479,15 +589,16 @@ module OpenNebula end end - log_msg = "Action:#{action} scheduled on Role:#{self.name} VMs:#{vms_id.join(',')}" - Log.info LOG_COMP, log_msg, @service.id() + log_msg = "Action:#{action} scheduled on Role:#{name}"\ + "VMs:#{vms_id.join(',')}" - if error_msgs.empty? - return [true, log_msg] - else - error_msgs << log_msg - return [false, error_msgs.join('\n')] - end + Log.info LOG_COMP, log_msg, @service.id + + return [true, log_msg] if error_msgs.empty? + + error_msgs << log_msg + + [false, error_msgs.join('\n')] end # Returns true if the VM state is failure @@ -559,29 +670,6 @@ module OpenNebula return [0, 0] end - # Scales up or down the number of nodes needed to match the current - # cardinality - # - # @return [Array, Array] true if all the VMs - # were created/shut down, false and the error reason if there - # was a problem - def scale() - n_nodes = 0 - - get_nodes.each do |node| - n_nodes += 1 if node['disposed'] != "1" - end - - diff = cardinality - n_nodes - - if diff > 0 - return deploy(true) - elsif diff < 0 - return shutdown(true) - end - - return [true, nil] - end # Updates the duration for the next cooldown # @param cooldown_duration [Integer] duration for the next cooldown @@ -639,39 +727,98 @@ module OpenNebula # @return [nil, OpenNebula::Error] nil in case of success, Error # otherwise def update(template) - force = template['force'] == true - new_cardinality = template["cardinality"] + new_cardinality = template['cardinality'] - if new_cardinality.nil? - return nil - end + return if new_cardinality.nil? new_cardinality = new_cardinality.to_i if !force - if new_cardinality < min_cardinality().to_i + if new_cardinality < min_cardinality.to_i return OpenNebula::Error.new( - "Minimum cardinality is #{min_cardinality()}") + "Minimum cardinality is #{min_cardinality}" + ) - elsif !max_cardinality().nil? && new_cardinality > max_cardinality().to_i + elsif !max_cardinality.nil? && + new_cardinality > max_cardinality.to_i return OpenNebula::Error.new( - "Maximum cardinality is #{max_cardinality()}") + "Maximum cardinality is #{max_cardinality}" + ) end end set_cardinality(new_cardinality) - return nil + nil end ######################################################################## # Recover ######################################################################## - def recover_deployment() - recover() + def recover_deploy + nodes = @body['nodes'] + deployed_nodes = [] + + nodes.each do |node| + vm_id = node['deploy_id'] + + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, + @service.client) + + rc = vm.info + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : Retry failed for VM "\ + "#{vm_id}; #{rc.message}" + Log.error LOG_COMP, msg, @service.id + + next true + end + + vm_state = vm.state + lcm_state = vm.lcm_state + + next false if vm_state == 3 && lcm_state == 3 # ACTIVE/RUNNING + + next true if vm_state == '6' # Delete DONE nodes + + if Role.vm_failure?(vm_state, lcm_state) + rc = vm.recover(2) + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : Retry failed for VM "\ + "#{vm_id}; #{rc.message}" + + Log.error LOG_COMP, msg, @service.id + @service.log_error(msg) + else + deployed_nodes << vm_id + end + else + vm.resume + + deployed_nodes << vm_id + end + end + + rc = deploy + + deployed_nodes.concat(rc[0]) if rc[1].nil? + + deployed_nodes + end + + def recover_undeploy + undeployed_nodes = [] + + rc = shutdown(true) + + undeployed_nodes.concat(rc[0]) if rc[1].nil? + + undeployed_nodes end def recover_warning() @@ -679,16 +826,120 @@ module OpenNebula deploy() end - def recover_scale() - recover() - retry_scale() + def recover_scale + rc = nil + + if @body['scale_way'] == SCALE_WAYS['UP'] + rc = [recover_deploy, true] + elsif @body['scale_way'] == SCALE_WAYS['DOWN'] + rc = [recover_undeploy, false] + end + + rc end + ######################################################################## + # Nodes info + ######################################################################## + + # Determine if the role nodes are running + # @return [true|false] + def nodes_running? + if nodes.size != cardinality + return false + end + + nodes.each do |node| + return false unless node && node['running'] + end + + true + end + + # Returns true if any VM is in UNKNOWN or FAILED + # @return [true|false] + def nodes_warning? + nodes.each do |node| + next unless node && node['vm_info'] + + vm_state = node['vm_info']['VM']['STATE'] + lcm_state = node['vm_info']['VM']['LCM_STATE'] + + # Failure or UNKNOWN + if vm_failure?(node) || (vm_state == '3' && lcm_state == '16') + return true + end + end + + false + end + + def nodes_done? + nodes.each do |node| + if node && node['vm_info'] + vm_state = node['vm_info']['VM']['STATE'] + + if vm_state != '6' # DONE + return false + end + else + return false + end + end + + true + end + + # Determine if any of the role nodes failed + # @param [Role] role + # @return [true|false] + def any_node_failed? + nodes.each do |node| + if vm_failure?(node) + return true + end + end + + false + end + + # Determine if any of the role nodes failed to scale + # @return [true|false] + def any_node_failed_scaling? + nodes.each do |node| + if node && node['vm_info'] && + (node['disposed'] == '1' || node['scale_up'] == '1') && + vm_failure?(node) + + return true + end + end + + false + end + + def role_finished_scaling? + nodes.each { |node| + # For scale up, check new nodes are running, or past running + if node + if node['scale_up'] == '1' + return false if !node['running'] + end + else + return false + end + } + + # TODO: If a shutdown ends in running again (VM doesn't have acpi), + # the role/service will stay in SCALING + + # For scale down, it will finish when scaling nodes are deleted + return nodes.size() == cardinality() + end ######################################################################## ######################################################################## - private # Returns a positive, 0, or negative number of nodes to adjust, @@ -880,13 +1131,13 @@ module OpenNebula # For a failed scale up, the cardinality is updated to the actual value # For a failed scale down, the shutdown actions are retried def retry_scale() - nodes_dispose = get_nodes.select { |node| + nodes_dispose = nodes.select { |node| node['disposed'] == "1" } shutdown_nodes(nodes_dispose, true) - set_cardinality( get_nodes.size() - nodes_dispose.size() ) + set_cardinality(nodes.size - nodes_dispose.size) end # Deletes VMs in DONE or FAILED, and sends a resume action to VMs in UNKNOWN @@ -943,41 +1194,53 @@ module OpenNebula @body['nodes'] = new_nodes end - # Shuts down all the given nodes # @param scale_down [true,false] True to set the 'disposed' node flag - def shutdown_nodes(nodes, scale_down) + def shutdown_nodes(nodes, n_nodes, recover) + success = true + undeployed_nodes = [] action = @body['shutdown_action'] if action.nil? - action = @service.get_shutdown_action() + action = @service.shutdown_action end if action.nil? action = @@default_shutdown end - nodes.each { |node| + nodes[0..n_nodes - 1].each do |node| vm_id = node['deploy_id'] - Log.debug LOG_COMP, "Role #{name} : Terminating VM #{vm_id}", @service.id() + Log.debug(LOG_COMP, + "Role #{name} : Terminating VM #{vm_id}", + @service.id) - vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, + @service.client) - if action == 'terminate-hard' + vm_state = nil + lcm_state = nil + + if recover + vm.info + + vm_state = vm.state + lcm_state = vm.lcm_state + end + + if recover && Role.vm_failure?(vm_state, lcm_state) + rc = vm.recover(2) + elsif action == 'terminate-hard' rc = vm.terminate(true) else rc = vm.terminate end - if scale_down - node['disposed'] = '1' - end - if OpenNebula.is_error?(rc) msg = "Role #{name} : Terminate failed for VM #{vm_id}, will perform a Delete; #{rc.message}" - Log.error LOG_COMP, msg, @service.id() + Log.error LOG_COMP, msg, @service.id @service.log_error(msg) if action != 'terminate-hard' @@ -990,19 +1253,38 @@ module OpenNebula if OpenNebula.is_error?(rc) msg = "Role #{name} : Delete failed for VM #{vm_id}; #{rc.message}" - Log.error LOG_COMP, msg, @service.id() + Log.error LOG_COMP, msg, @service.id @service.log_error(msg) success = false - #return [false, rc.message] else - Log.debug LOG_COMP, "Role #{name} : Delete success for VM #{vm_id}", @service.id() + Log.debug(LOG_COMP, + "Role #{name} : Delete success for VM #{vm_id}", + @service.id) + + undeployed_nodes << vm_id end else - Log.debug LOG_COMP, "Role #{name} : Terminate success for VM #{vm_id}", @service.id() + Log.debug(LOG_COMP, + "Role #{name}: Terminate success for VM #{vm_id}", + @service.id) + undeployed_nodes << vm_id end - } + end + + [success, undeployed_nodes] + end + + def vm_failure?(node) + if node && node['vm_info'] + return Role.vm_failure?( + vm_state = node['vm_info']['VM']['STATE'], + lcm_state = node['vm_info']['VM']['LCM_STATE']) + end + + false end end + end diff --git a/src/flow/lib/models/service.rb b/src/flow/lib/models/service.rb index 524213ca96..842aed0d40 100644 --- a/src/flow/lib/models/service.rb +++ b/src/flow/lib/models/service.rb @@ -15,8 +15,12 @@ #--------------------------------------------------------------------------- # module OpenNebula + + # Service class as wrapper of DocumentJSON class Service < DocumentJSON + attr_reader :roles, :client + DOCUMENT_TYPE = 100 STATE = { @@ -33,44 +37,108 @@ module OpenNebula 'COOLDOWN' => 10 } - STATE_STR = [ - 'PENDING', - 'DEPLOYING', - 'RUNNING', - 'UNDEPLOYING', - 'WARNING', - 'DONE', - 'FAILED_UNDEPLOYING', - 'FAILED_DEPLOYING', - 'SCALING', - 'FAILED_SCALING', - 'COOLDOWN' + STATE_STR = %w[ + PENDING + DEPLOYING + RUNNING + UNDEPLOYING + WARNING + DONE + FAILED_UNDEPLOYING + FAILED_DEPLOYING + SCALING + FAILED_SCALING + COOLDOWN ] - LOG_COMP = "SER" + TRANSIENT_STATES = %w[ + DEPLOYING + UNDEPLOYING + SCALING + ] + + FAILED_STATES = %w[ + FAILED_DEPLOYING + FAILED_UNDEPLOYING + FAILED_SCALING + ] + + RECOVER_DEPLOY_STATES = %w[ + FAILED_DEPLOYING + DEPLOYING + PENDING + ] + + RECOVER_UNDEPLOY_STATES = %w[ + FAILED_UNDEPLOYING + UNDEPLOYING + ] + + RECOVER_SCALE_STATES = %w[ + FAILED_SCALING + SCALING + ] + + LOG_COMP = 'SER' # Returns the service state # @return [Integer] the service state def state - return @body['state'].to_i + @body['state'].to_i end # Returns the service strategy # @return [String] the service strategy def strategy - return @body['deployment'] + @body['deployment'] end # Returns the string representation of the service state # @return the state string def state_str - return STATE_STR[state] + STATE_STR[state] + end + + # Returns true if the service is in transient state + # @return true if the service is in transient state, false otherwise + def transient_state? + TRANSIENT_STATES.include? STATE_STR[state] + end + + # Return true if the service is in failed state + # @return true if the service is in failed state, false otherwise + def failed_state? + FAILED_STATES.include? STATE_STR[state] + end + + # Return true if the service can be undeployed + # @return true if the service can be undeployed, false otherwise + def can_undeploy? + if transient_state? + state != Service::STATE['UNDEPLOYING'] + else + state != Service::STATE['DONE'] && !failed_state? + end + end + + def can_recover_deploy? + RECOVER_DEPLOY_STATES.include? STATE_STR[state] + end + + def can_recover_undeploy? + RECOVER_UNDEPLOY_STATES.include? STATE_STR[state] + end + + def can_recover_scale? + RECOVER_SCALE_STATES.include? STATE_STR[state] end # Sets a new state # @param [Integer] the new state # @return [true, false] true if the value was changed + # rubocop:disable Naming/AccessorMethodName def set_state(state) + # rubocop:enable Naming/AccessorMethodName if state < 0 || state > STATE_STR.size return false end @@ -78,16 +146,16 @@ module OpenNebula @body['state'] = state msg = "New state: #{STATE_STR[state]}" - Log.info LOG_COMP, msg, self.id() - self.log_info(msg) + Log.info LOG_COMP, msg, id + log_info(msg) - return true + true end # Returns the owner username # @return [String] the service's owner username - def owner_name() - return self['UNAME'] + def owner_name + self['UNAME'] end # Replaces this object's client with a new one @@ -96,86 +164,82 @@ module OpenNebula @client = owner_client end - # Returns all the node Roles - # @return [Hash] all the node Roles - def get_roles - return @roles - end - # Returns true if all the nodes are correctly deployed # @return [true, false] true if all the nodes are correctly deployed - def all_roles_running?() - @roles.each { |name, role| + def all_roles_running? + @roles.each do |_name, role| if role.state != Role::STATE['RUNNING'] return false end - } + end - return true + true end # Returns true if all the nodes are in done state # @return [true, false] true if all the nodes are correctly deployed - def all_roles_done?() - @roles.each { |name, role| + def all_roles_done? + @roles.each do |_name, role| if role.state != Role::STATE['DONE'] return false end - } + end - return true + true end # Returns true if any of the roles is in failed state # @return [true, false] true if any of the roles is in failed state - def any_role_failed?() + def any_role_failed? failed_states = [ Role::STATE['FAILED_DEPLOYING'], - Role::STATE['FAILED_UNDEPLOYING']] + Role::STATE['FAILED_UNDEPLOYING'], + Role::STATE['FAILED_DELETING'] + ] - @roles.each { |name, role| + @roles.each do |_name, role| if failed_states.include?(role.state) return true end - } + end - return false + false end # Returns the running_status_vm option # @return [true, false] true if the running_status_vm option is enabled def ready_status_gate - return @body['ready_status_gate'] + @body['ready_status_gate'] end - def any_role_scaling?() - @roles.each do |name, role| + def any_role_scaling? + @roles.each do |_name, role| if role.state == Role::STATE['SCALING'] return true end end - return false + false end - def any_role_failed_scaling?() - @roles.each do |name, role| + def any_role_failed_scaling? + @roles.each do |_name, role| if role.state == Role::STATE['FAILED_SCALING'] return true end end - return false + false end - def any_role_cooldown?() - @roles.each do |name, role| + def any_role_cooldown? + @roles.each do |_name, role| if role.state == Role::STATE['COOLDOWN'] return true end end - return false + false end # Create a new service based on the template provided @@ -187,95 +251,94 @@ module OpenNebula template['state'] = STATE['PENDING'] if template['roles'] - template['roles'].each { |elem| + template['roles'].each do |elem| elem['state'] ||= Role::STATE['PENDING'] - } + end end super(template.to_json, template['name']) end - # Shutdown the service. This action is called when user wants to shutdwon - # the Service - # @return [nil, OpenNebula::Error] nil in case of success, Error - # otherwise - def shutdown - if ![Service::STATE['FAILED_SCALING'], - Service::STATE['DONE']].include?(self.state) - self.set_state(Service::STATE['UNDEPLOYING']) - return self.update - else - return OpenNebula::Error.new("Action shutdown: Wrong state" \ - " #{self.state_str()}") - end - end - # Recover a failed service. # @return [nil, OpenNebula::Error] nil in case of success, Error # otherwise def recover - if [Service::STATE['FAILED_DEPLOYING']].include?(self.state) - @roles.each do |name, role| + if [Service::STATE['FAILED_DEPLOYING']].include?(state) + @roles.each do |_name, role| if role.state == Role::STATE['FAILED_DEPLOYING'] role.set_state(Role::STATE['PENDING']) - role.recover_deployment() end end - self.set_state(Service::STATE['DEPLOYING']) + set_state(Service::STATE['DEPLOYING']) - elsif self.state == Service::STATE['FAILED_SCALING'] - @roles.each do |name, role| + elsif state == Service::STATE['FAILED_SCALING'] + @roles.each do |_name, role| if role.state == Role::STATE['FAILED_SCALING'] - role.recover_scale() role.set_state(Role::STATE['SCALING']) end end - self.set_state(Service::STATE['SCALING']) + set_state(Service::STATE['SCALING']) - elsif self.state == Service::STATE['FAILED_UNDEPLOYING'] - @roles.each do |name, role| + elsif state == Service::STATE['FAILED_UNDEPLOYING'] + @roles.each do |_name, role| if role.state == Role::STATE['FAILED_UNDEPLOYING'] role.set_state(Role::STATE['RUNNING']) end end - self.set_state(Service::STATE['UNDEPLOYING']) + set_state(Service::STATE['UNDEPLOYING']) - elsif self.state == Service::STATE['COOLDOWN'] - @roles.each do |name, role| + elsif state == Service::STATE['COOLDOWN'] + @roles.each do |_name, role| if role.state == Role::STATE['COOLDOWN'] role.set_state(Role::STATE['RUNNING']) end end - self.set_state(Service::STATE['RUNNING']) + set_state(Service::STATE['RUNNING']) - elsif self.state == Service::STATE['WARNING'] - @roles.each do |name, role| + elsif state == Service::STATE['WARNING'] + @roles.each do |_name, role| if role.state == Role::STATE['WARNING'] - role.recover_warning() + role.recover_warning end end - else - return OpenNebula::Error.new("Action recover: Wrong state" \ - " #{self.state_str()}") + OpenNebula::Error.new('Action recover: Wrong state' \ + " #{state_str}") end - - return self.update end # Delete the service. All the VMs are also deleted from OpenNebula. # @return [nil, OpenNebula::Error] nil in case of success, Error # otherwise - def delete - @roles.each { |name, role| - role.delete() - } - return super() + def delete + networks = JSON.parse(self['TEMPLATE/BODY'])['networks_values'] + + networks.each do |net| + next unless net[net.keys[0]].key? 'template_id' + + net_id = net[net.keys[0]]['id'].to_i + + rc = OpenNebula::VirtualNetwork + .new_with_id(net_id, @client).delete + + if OpenNebula.is_error?(rc) + log_info("Error deleting vnet #{net_id}: #{rc}") + end + end + + super() + end + + def delete_roles + @roles.each do |_name, role| + role.set_state(Role::STATE['DELETING']) + role.delete + end end # Retrieves the information of the Service and all its Nodes. @@ -291,14 +354,14 @@ module OpenNebula @roles = {} if @body['roles'] - @body['roles'].each { |elem| + @body['roles'].each do |elem| elem['state'] ||= Role::STATE['PENDING'] role = Role.new(elem, self) @roles[role.name] = role - } + end end - return nil + nil end # Add an info message in the service information that will be stored @@ -315,15 +378,10 @@ module OpenNebula add_log(Logger::ERROR, message) end - # Retrieve the service client - def client - @client - end - # Changes the owner/group # - # @param [Integer] uid the new owner id. Set to -1 to leave the current one - # @param [Integer] gid the new group id. Set to -1 to leave the current one + # @param [Integer] uid the new owner id. Use -1 to leave the current one + # @param [Integer] gid the new group id. Use -1 to leave the current one # # @return [nil, OpenNebula::Error] nil in case of success, Error # otherwise @@ -337,26 +395,28 @@ module OpenNebula return rc end - @roles.each { |name, role| + @roles.each do |_name, role| rc = role.chown(uid, gid) break if rc[0] == false - } + end if rc[0] == false - self.log_error("Chown operation failed, will try to rollback all VMs to the old user and group") - update() + log_error('Chown operation failed, will try to rollback ' \ + 'all VMs to the old user and group') + + update super(old_uid, old_gid) - @roles.each { |name, role| + @roles.each do |_name, role| role.chown(old_uid, old_gid) - } + end return OpenNebula::Error.new(rc[1]) end - return nil + nil end # Updates a role @@ -365,10 +425,11 @@ module OpenNebula # @return [nil, OpenNebula::Error] nil in case of success, Error # otherwise def update_role(role_name, template_json) + if ![Service::STATE['RUNNING'], Service::STATE['WARNING']] + .include?(state) - if ![Service::STATE['RUNNING'], Service::STATE['WARNING']].include?(self.state) - return OpenNebula::Error.new("Update role: Wrong state" \ - " #{self.state_str()}") + return OpenNebula::Error.new('Update role: Wrong state' \ + " #{state_str}") end template = JSON.parse(template_json) @@ -378,7 +439,8 @@ module OpenNebula role = @roles[role_name] if role.nil? - return OpenNebula::Error.new("ROLE \"#{role_name}\" does not exist") + return OpenNebula::Error.new("ROLE \"#{role_name}\" " \ + 'does not exist') end rc = role.update(template) @@ -392,15 +454,15 @@ module OpenNebula role.set_state(Role::STATE['SCALING']) - role.set_default_cooldown_duration() + role.set_default_cooldown_duration - self.set_state(Service::STATE['SCALING']) + set_state(Service::STATE['SCALING']) - return self.update + update end - def get_shutdown_action() - return @body['shutdown_action'] + def shutdown_action + @body['shutdown_action'] end # Replaces the template contents @@ -411,7 +473,7 @@ module OpenNebula # # @return [nil, OpenNebula::Error] nil in case of success, Error # otherwise - def update(template_json=nil, append=false) + def update(template_json = nil, append = false) if template_json template = JSON.parse(template_json) @@ -439,22 +501,93 @@ module OpenNebula # # @return [nil, OpenNebula::Error] nil in case of success, Error # otherwise - def update_raw(template_raw, append=false) + def update_raw(template_raw, append = false) super(template_raw, append) end + def deploy_networks + body = JSON.parse(self['TEMPLATE/BODY']) + + return if body['networks_values'].nil? + + body['networks_values'].each do |net| + rc = create_vnet(net) if net[net.keys[0]].key?('template_id') + + if OpenNebula.is_error?(rc) + return rc + end + + rc = reserve(net) if net[net.keys[0]].key?('reserve_from') + + if OpenNebula.is_error?(rc) + return rc + end + end + + # Replace $attibute by the corresponding value + resolve_attributes(body) + + # @body = template.to_hash + + update_body(body) + end + + def delete_networks + vnets = @body['networks_values'] + vnets_failed = [] + + return if vnets.nil? + + vnets.each do |vnet| + next unless vnet[vnet.keys[0]].key?('template_id') || + vnet[vnet.keys[0]].key?('reserve_from') + + vnet_id = vnet[vnet.keys[0]]['id'].to_i + + rc = OpenNebula::VirtualNetwork + .new_with_id(vnet_id, @client).delete + + if OpenNebula.is_error?(rc) + vnets_failed << vnet_id + end + end + + vnets_failed + end + + def can_scale? + state == Service::STATE['RUNNING'] + end + private # Maximum number of log entries per service # TODO: Make this value configurable MAX_LOG = 50 + def update_body(body) + @body = body + + # Update @roles attribute with the new @body content + @roles = {} + if @body['roles'] + @body['roles'].each do |elem| + elem['state'] ||= Role::STATE['PENDING'] + role = Role.new(elem, self) + @roles[role.name] = role + end + end + + # Update @xml attribute with the new body content + @xml.at_xpath('/DOCUMENT/TEMPLATE/BODY').children[0].content = @body + end + # @param [Logger::Severity] severity # @param [String] message def add_log(severity, message) severity_str = Logger::SEV_LABEL[severity][0..0] - @body['log'] ||= Array.new + @body['log'] ||= [] @body['log'] << { :timestamp => Time.now.to_i, :severity => severity_str, @@ -464,5 +597,82 @@ module OpenNebula # Truncate the number of log entries @body['log'] = @body['log'].last(MAX_LOG) end + + def create_vnet(net) + extra = '' + + extra = net[net.keys[0]]['extra'] if net[net.keys[0]].key? 'extra' + + vntmpl_id = OpenNebula::VNTemplate + .new_with_id(net[net.keys[0]]['template_id'] + .to_i, @client).instantiate(get_vnet_name(net), extra) + + # TODO, check which error should be returned + return vntmpl_id if OpenNebula.is_error?(vntmpl_id) + + net[net.keys[0]]['id'] = vntmpl_id + + true + end + + def reserve(net) + get_vnet_name(net) + extra = net[net.keys[0]]['extra'] if net[net.keys[0]].key? 'extra' + + return false if extra.empty? + + extra.concat("\nNAME=\"#{get_vnet_name(net)}\"\n") + + reserve_id = OpenNebula::VirtualNetwork + .new_with_id(net[net.keys[0]]['reserve_from'] + .to_i, @client).reserve_with_extra(extra) + + return reserve_id if OpenNebula.is_error?(reserve_id) + + net[net.keys[0]]['id'] = reserve_id + + true + end + + def get_vnet_name(net) + "#{net.keys[0]}-#{id}" + end + + def resolve_attributes(template) + template['roles'].each do |role| + if role['vm_template_contents'] + # $CUSTOM1_VAR Any word character (letter, number, underscore) + role['vm_template_contents'].scan(/\$(\w+)/).each do |key| + # Check if $ var value is in custom_attrs_values + if template['custom_attrs_values'].key?(key[0]) + role['vm_template_contents'].gsub!( + '$'+key[0], + template['custom_attrs_values'][key[0]]) + next + end + + # Check if $ var value is in networks + net = template['networks_values'] + .find {|att| att.key? key[0] } + + next if net.nil? + + role['vm_template_contents'].gsub!( + '$'+key[0], + net[net.keys[0]]['id'].to_s + ) + end + end + + next unless role['user_inputs_values'] + + role['vm_template_contents'] ||= '' + role['user_inputs_values'].each do |key, value| + role['vm_template_contents'] += "\n#{key}=\"#{value}\"" + end + end + end + end + end diff --git a/src/flow/lib/models/service_pool.rb b/src/flow/lib/models/service_pool.rb index c65dc93829..137f721a96 100644 --- a/src/flow/lib/models/service_pool.rb +++ b/src/flow/lib/models/service_pool.rb @@ -15,12 +15,29 @@ #--------------------------------------------------------------------------- # module OpenNebula - class ServicePool < DocumentPoolJSON + + # ServicePool class + class OpenNebulaServicePool < DocumentPoolJSON DOCUMENT_TYPE = 100 + def initialize(client, user_id = -1) + super(client, user_id) + end + + def factory(element_xml) + service = OpenNebula::Service.new(element_xml, @client) + service.load_body + service + end + + end + + # ServicePool class + class ServicePool + @@mutex = Mutex.new - @@mutex_hash = Hash.new + @@mutex_hash = {} # Class constructor # @@ -29,14 +46,38 @@ module OpenNebula # http://opennebula.org/documentation:rel3.6:api # # @return [DocumentPool] the new object - def initialize(client, user_id=-1) - super(client, user_id) + def initialize(cloud_auth, client) + # TODO, what if cloud_auth is nil? + @cloud_auth = cloud_auth + @client = client + @one_pool = nil end - def factory(element_xml) - service = OpenNebula::Service.new(element_xml, @client) - service.load_body - service + def client + # If there's a client defined use it + return @client unless @client.nil? + + # If not, get one via cloud_auth + @cloud_auth.client + end + + def info + osp = OpenNebulaServicePool.new(client) + rc = osp.info + + @one_pool = osp + + rc + end + + def to_json + @one_pool.to_json + end + + def each(&block) + return if @one_pool.nil? + + @one_pool.each(&block) end # Retrieves a Service element from OpenNebula. The Service::info() @@ -47,51 +88,66 @@ module OpenNebula # The mutex will be unlocked after the block execution. # # @return [Service, OpenNebula::Error] The Service in case of success - def get(service_id, &block) + def get(service_id, external_client = nil, &block) service_id = service_id.to_i if service_id - service = Service.new_with_id(service_id, @client) + aux_client = nil - rc = service.info - - if OpenNebula.is_error?(rc) - return rc + if external_client.nil? + aux_client = client else - if block_given? - obj_mutex = nil - entry = nil + aux_client = external_client + end - @@mutex.synchronize { - # entry is an array of [Mutex, waiting] - # waiting is the number of threads waiting on this mutex - entry = @@mutex_hash[service_id] + service = Service.new_with_id(service_id, aux_client) - if entry.nil? - entry = [Mutex.new, 0] - @@mutex_hash[service_id] = entry + if block_given? + obj_mutex = nil + entry = nil + + @@mutex.synchronize do + # entry is an array of [Mutex, waiting] + # waiting is the number of threads waiting on this mutex + entry = @@mutex_hash[service_id] + + if entry.nil? + entry = [Mutex.new, 0] + @@mutex_hash[service_id] = entry + end + + obj_mutex = entry[0] + entry[1] = entry[1] + 1 + + if @@mutex_hash.size > 10000 + @@mutex_hash.delete_if do |_s_id, entry_loop| + entry_loop[1] == 0 end - - obj_mutex = entry[0] - entry[1] = entry[1] + 1 - - if @@mutex_hash.size > 10000 - @@mutex_hash.delete_if { |s_id, entry| - entry[1] == 0 - } - end - } - - obj_mutex.synchronize { - block.call(service) - } - - @@mutex.synchronize { - entry[1] = entry[1] - 1 - } + end end - return service + rc = obj_mutex.synchronize do + rc = service.info + + if OpenNebula.is_error?(rc) + return rc + end + + block.call(service) + end + + @@mutex.synchronize do + entry[1] = entry[1] - 1 + end + + if OpenNebula.is_error?(rc) + return rc + end + else + service.info end + + service end end + end diff --git a/src/flow/lib/models/service_template.rb b/src/flow/lib/models/service_template.rb index d257895280..d08220bbee 100644 --- a/src/flow/lib/models/service_template.rb +++ b/src/flow/lib/models/service_template.rb @@ -152,9 +152,13 @@ module OpenNebula :required => true }, 'deployment' => { - :type => :string, - :enum => %w{none straight}, - :default => 'none' + :type => :string, + :enum => %w{none straight}, + :default => 'none' + }, + 'description' => { + :type => :string, + :default => '' }, 'shutdown_action' => { :type => :string, @@ -168,18 +172,37 @@ module OpenNebula }, 'custom_attrs' => { :type => :object, - :properties => { - }, + :properties => { }, + :required => false + }, + 'custom_attrs_values' => { + :type => :object, + :properties => { }, :required => false }, 'ready_status_gate' => { :type => :boolean, :required => false + }, + 'networks' => { + :type => :object, + :properties => { }, + :required => false + }, + 'networks_values' => { + :type => :array, + :items => { + :type => :object, + :properties => { } + }, + :required => false } } } - + def self.init_default_vn_name_template(vn_name_template) + @@vn_name_template = vn_name_template + end DOCUMENT_TYPE = 101 @@ -212,9 +235,7 @@ module OpenNebula if append rc = info - if OpenNebula.is_error? rc - return rc - end + return rc if OpenNebula.is_error?(rc) template = @body.merge(template) end @@ -248,7 +269,32 @@ module OpenNebula validate_values(template) end - private + def instantiate(merge_template) + rc = nil + + if merge_template.nil? + instantiate_template = JSON.parse(@body.to_json) + else + instantiate_template = JSON.parse(@body.to_json) + .merge(merge_template) + end + + begin + ServiceTemplate.validate(instantiate_template) + + xml = OpenNebula::Service.build_xml + service = OpenNebula::Service.new(xml, @client) + + rc = service.allocate(instantiate_template.to_json) + rescue Validator::ParseException, JSON::ParserError => e + return e + end + + return rc if OpenNebula.is_error?(rc) + + service.info + service + end def self.validate_values(template) parser = ElasticityGrammarParser.new diff --git a/src/flow/lib/strategy.rb b/src/flow/lib/strategy.rb index 6784224abc..33a4a92585 100644 --- a/src/flow/lib/strategy.rb +++ b/src/flow/lib/strategy.rb @@ -16,216 +16,22 @@ require 'strategy/straight' -class Strategy +# Strategy class (module none?) +module Strategy - LOG_COMP = "STR" + LOG_COMP = 'STR' - # Performs a boot step, deploying all nodes that meet the requirements - # @param [Service] service service to boot - # @return [Array, Array] true if all the nodes - # were created, false and the error reason if there was a problem - # creating the VMs - def boot_step(service) - Log.debug LOG_COMP, "Boot step", service.id() - - roles_deploy = get_roles_deploy(service) - - roles_deploy.each { |name, role| - Log.debug LOG_COMP, "Deploying role #{name}", service.id() - - rc = role.deploy - - if !rc[0] - role.set_state(Role::STATE['FAILED_DEPLOYING']) - - return rc - else - role.set_state(Role::STATE['DEPLOYING']) - end - } - - return [true, nil] - end - - # Performs a shutdown step, shutting down all nodes that meet the requirements - # @param [Service] service service to boot - # @return [Array, Array] true if all the nodes - # were created, false and the error reason if there was a problem - # creating the VMs - def shutdown_step(service) - Log.debug LOG_COMP, "Shutdown step", service.id() - - roles_shutdown = get_roles_shutdown(service) - - roles_shutdown.each { |name, role| - Log.debug LOG_COMP, "Shutting down role #{name}", service.id() - - rc = role.shutdown - - if !rc[0] - role.set_state(Role::STATE['FAILED_UNDEPLOYING']) - - return rc - else - role.set_state(Role::STATE['UNDEPLOYING']) - end - } - - return [true, nil] - end - - # If a role needs to scale, its cardinality is updated, and its state is set - # to SCALING. Only one role is set to scale. - # @param [Service] service - # @return [true|false] true if any role needs to scale - def apply_scaling_policies(service) - Log.debug LOG_COMP, "Apply scaling policies", service.id() - - service.get_roles.each do |name, role| - diff, cooldown_duration = role.scale? - - if diff != 0 - Log.debug LOG_COMP, "Role #{name} needs to scale #{diff} nodes", service.id() - - role.set_cardinality(role.cardinality() + diff) - - role.set_state(Role::STATE['SCALING']) - role.set_cooldown_duration(cooldown_duration) - - return true - end - end - - return false - end - - # If a role is scaling, the nodes are created/destroyed to match the current - # cardinality - # @return [Array, Array] true if the action was - # performed, false and the error reason if there was a problem - def scale_step(service) - Log.debug LOG_COMP, "Scale step", service.id() - - service.get_roles.each do |name, role| - - if role.state == Role::STATE['SCALING'] - rc = role.scale() - - if !rc[0] - role.set_state(Role::STATE['FAILED_SCALING']) - - return rc - end - end - end - - return [true, nil] - end - - # Performs a monitor step, check if the roles already deployed are running - # @param [Service] service service to monitor - # @return [nil] - def monitor_step(service) - Log.debug LOG_COMP, "Monitor step", service.id() - - roles_monitor = get_roles_monitor(service) - - roles_monitor.each { |name, role| - Log.debug LOG_COMP, "Monitoring role #{name}", service.id() - - rc = role.info - - case role.state() - when Role::STATE['RUNNING'] - if OpenNebula.is_error?(rc) || role_nodes_warning?(role) - role.set_state(Role::STATE['WARNING']) - end - - role.update_cardinality() - when Role::STATE['WARNING'] - if !OpenNebula.is_error?(rc) && !role_nodes_warning?(role) - role.set_state(Role::STATE['RUNNING']) - end - - role.update_cardinality() - when Role::STATE['DEPLOYING'] - if OpenNebula.is_error?(rc) - role.set_state(Role::STATE['FAILED_DEPLOYING']) - elsif role_nodes_running?(role) - role.set_state(Role::STATE['RUNNING']) - elsif any_node_failed?(role) - role.set_state(Role::STATE['FAILED_DEPLOYING']) - end - when Role::STATE['SCALING'] - if OpenNebula.is_error?(rc) - role.set_state(Role::STATE['FAILED_SCALING']) - elsif role_finished_scaling?(role) - if role.apply_cooldown_duration() - role.set_state(Role::STATE['COOLDOWN']) - else - role.set_state(Role::STATE['RUNNING']) - end - elsif any_node_failed_scaling?(role) - role.set_state(Role::STATE['FAILED_SCALING']) - end - when Role::STATE['COOLDOWN'] - if role.cooldown_over? - role.set_state(Role::STATE['RUNNING']) - end - - role.update_cardinality() - when Role::STATE['UNDEPLOYING'] - if OpenNebula.is_error?(rc) - role.set_state(Role::STATE['FAILED_UNDEPLOYING']) - elsif role_nodes_done?(role) - role.set_state(Role::STATE['DONE']) - elsif any_node_failed?(role) - role.set_state(Role::STATE['FAILED_UNDEPLOYING']) - end - when Role::STATE['FAILED_DEPLOYING'] - if !OpenNebula.is_error?(rc) && role_nodes_running?(role) - role.set_state(Role::STATE['RUNNING']) - end - when Role::STATE['FAILED_UNDEPLOYING'] - if !OpenNebula.is_error?(rc) && role_nodes_done?(role) - role.set_state(Role::STATE['DONE']) - end - when Role::STATE['FAILED_SCALING'] - if !OpenNebula.is_error?(rc) && role_finished_scaling?(role) - role.set_state(Role::STATE['SCALING']) - end - end - } - end - -protected # All subclasses must define these methods # Returns all node Roles ready to be deployed # @param [Service] service # @return [Hash] Roles - def get_roles_deploy(service) - result = service.get_roles.select {|name, role| + def roles_deploy + result = roles.select do |_name, role| role.state == Role::STATE['PENDING'] || - role.state == Role::STATE['DEPLOYING'] - } - - # Ruby 1.8 compatibility - if result.instance_of?(Array) - result = Hash[result] + role.state == Role::STATE['SCALING'] end - result - end - - # Returns all node Roles be monitored - # @param [Service] service - # @return [Hash] Roles - def get_roles_monitor(service) - result = service.get_roles.select {|name, role| - ![Role::STATE['PENDING'], Role::STATE['DONE']].include?(role.state) - } - # Ruby 1.8 compatibility if result.instance_of?(Array) result = Hash[result] @@ -237,12 +43,12 @@ protected # Returns all node Roles ready to be shutdown # @param [Service] service # @return [Hash] Roles - def get_roles_shutdown(service) - result = service.get_roles.select {|name, role| - ![Role::STATE['UNDEPLOYING'], - Role::STATE['DONE'], - Role::STATE['FAILED_UNDEPLOYING']].include?(role.state) - } + def roles_shutdown + result = roles.reject do |_name, role| + [Role::STATE['UNDEPLOYING'], + Role::STATE['DONE'], + Role::STATE['FAILED_UNDEPLOYING']].include?(role.state) + end # Ruby 1.8 compatibility if result.instance_of?(Array) @@ -252,114 +58,4 @@ protected result end - # Determine if the role nodes are running - # @param [Role] role - # @return [true|false] - def role_nodes_running?(role) - if role.get_nodes.size() != role.cardinality() - return false - end - - role.get_nodes.each { |node| - return false if !(node && node['running']) - } - - return true - end - - # Returns true if any VM is in UNKNOWN or FAILED - # @param [Role] role - # @return [true|false] - def role_nodes_warning?(role) - role.get_nodes.each do |node| - if node && node['vm_info'] - vm_state = node['vm_info']['VM']['STATE'] - lcm_state = node['vm_info']['VM']['LCM_STATE'] - - # Failure or UNKNOWN - if vm_failure?(node) || (vm_state == '3' && lcm_state == '16') - return true - end - end - end - - return false - end - - # Determine if any of the role nodes failed - # @param [Role] role - # @return [true|false] - def any_node_failed?(role) - role.get_nodes.each { |node| - if vm_failure?(node) - return true - end - } - - return false - end - - # Determine if the role nodes are in done state - # @param [Role] role - # @return [true|false] - def role_nodes_done?(role) - role.get_nodes.each { |node| - if node && node['vm_info'] - vm_state = node['vm_info']['VM']['STATE'] - - if vm_state != '6' # DONE - return false - end - else - return false - end - } - - return true - end - - # Determine if any of the role nodes failed to scale - # @param [Role] role - # @return [true|false] - def any_node_failed_scaling?(role) - role.get_nodes.each { |node| - if node && node['vm_info'] && - (node['disposed'] == '1' || node['scale_up'] == '1') && - vm_failure?(node) - - return true - end - } - - return false - end - - def role_finished_scaling?(role) - role.get_nodes.each { |node| - # For scale up, check new nodes are running, or past running - if node - if node['scale_up'] == '1' - return false if !node['running'] - end - else - return false - end - } - - # TODO: If a shutdown ends in running again (VM doesn't have acpi), - # the role/service will stay in SCALING - - # For scale down, it will finish when scaling nodes are deleted - return role.get_nodes.size() == role.cardinality() - end - - def vm_failure?(node) - if node && node['vm_info'] - return Role.vm_failure?( - vm_state = node['vm_info']['VM']['STATE'], - lcm_state = node['vm_info']['VM']['LCM_STATE']) - end - - return false - end end diff --git a/src/flow/lib/strategy/straight.rb b/src/flow/lib/strategy/straight.rb index 350201d756..e44443ec67 100644 --- a/src/flow/lib/strategy/straight.rb +++ b/src/flow/lib/strategy/straight.rb @@ -14,11 +14,13 @@ # limitations under the License. # #--------------------------------------------------------------------------- # +# Straight strategy module module Straight + # Using this strategy the service is deployed based on a directed # acyclic graph where each node defines its parents. # - # For example: + # For example: # # mysql nfs # | | \ @@ -66,41 +68,34 @@ module Straight # 2. kvm & myslq # 3. nfs - - # Returns all node Roles ready to be deployed - # @param [Service] service # @return [Hash] Roles - def get_roles_deploy(service) - roles = service.get_roles - - running_roles = roles.select {|name, role| + def roles_deploy + running_roles = roles.select do |_name, role| role.state == Role::STATE['RUNNING'] - } + end # Ruby 1.8 compatibility if running_roles.instance_of?(Array) running_roles = Hash[running_roles] end - result = roles.select {|name, role| + result = roles.select do |_name, role| check = true if role.state == Role::STATE['PENDING'] - role.parents.each { |parent| + role.parents.each do |parent| if !running_roles.include?(parent) check = false break end - } - elsif role.state == Role::STATE['DEPLOYING'] - check = true + end else check = false end check - } + end # Ruby 1.8 compatibility if result.instance_of?(Array) @@ -111,34 +106,31 @@ module Straight end # Returns all node Roles ready to be shutdown - # @param [Service] service # @return [Hash] Roles - def get_roles_shutdown(service) - roles = service.get_roles - + def roles_shutdown # Get all the parents from running roles parents = [] running_roles = {} - roles.each { |name, role| + roles.each do |name, role| # All roles can be shutdown, except the ones in these states - if (![Role::STATE['UNDEPLOYING'], - Role::STATE['DONE'], - Role::STATE['FAILED_UNDEPLOYING']].include?(role.state) ) + if ![Role::STATE['UNDEPLOYING'], + Role::STATE['DONE'], + Role::STATE['FAILED_UNDEPLOYING']].include?(role.state) running_roles[name]= role end # Only the parents of DONE roles can be shutdown - if (role.state != Role::STATE['DONE'] ) + if role.state != Role::STATE['DONE'] parents += role.parents end - } + end # Select the nodes that are not parent from any node - result = running_roles.select {|name, role| - !parents.include?(name) - } + result = running_roles.reject do |name, _role| + parents.include?(name) + end # Ruby 1.8 compatibility if result.instance_of?(Array) @@ -148,4 +140,4 @@ module Straight result end -end \ No newline at end of file +end diff --git a/src/flow/oneflow-server.rb b/src/flow/oneflow-server.rb index fbecb5a2fe..04b09e20bf 100644 --- a/src/flow/oneflow-server.rb +++ b/src/flow/oneflow-server.rb @@ -1,3 +1,4 @@ +# rubocop:disable Naming/FileName # -------------------------------------------------------------------------- # # Copyright 2002-2019, OpenNebula Project, OpenNebula Systems # # # @@ -54,6 +55,9 @@ require 'CloudServer' require 'models' require 'log' +require 'LifeCycleManager' +require 'EventManager' + DEFAULT_VM_NAME_TEMPLATE = '$ROLE_NAME_$VM_NUMBER_(service_$SERVICE_ID)' ############################################################################## @@ -62,26 +66,28 @@ DEFAULT_VM_NAME_TEMPLATE = '$ROLE_NAME_$VM_NUMBER_(service_$SERVICE_ID)' begin conf = YAML.load_file(CONFIGURATION_FILE) -rescue Exception => e +rescue StandardError => e STDERR.puts "Error parsing config file #{CONFIGURATION_FILE}: #{e.message}" exit 1 end -conf[:debug_level] ||= 2 -conf[:lcm_interval] ||= 30 +conf[:debug_level] ||= 2 +conf[:lcm_interval] ||= 30 conf[:default_cooldown] ||= 300 -conf[:shutdown_action] ||= 'terminate' -conf[:action_number] ||= 1 -conf[:action_period] ||= 60 - -conf[:auth] = 'opennebula' +conf[:shutdown_action] ||= 'terminate' +conf[:action_number] ||= 1 +conf[:action_period] ||= 60 +conf[:vm_name_template] ||= DEFAULT_VM_NAME_TEMPLATE +conf[:auth] = 'opennebula' set :bind, conf[:host] set :port, conf[:port] - set :config, conf +# rubocop:disable Style/MixinUsage include CloudLogger +# rubocop:enable Style/MixinUsage + logger = enable_logging ONEFLOW_LOG, conf[:debug_level].to_i use Rack::Session::Pool, :key => 'oneflow' @@ -89,19 +95,18 @@ use Rack::Session::Pool, :key => 'oneflow' Log.logger = logger Log.level = conf[:debug_level].to_i +LOG_COMP = 'ONEFLOW' -LOG_COMP = "ONEFLOW" - -Log.info LOG_COMP, "Starting server" +Log.info LOG_COMP, 'Starting server' begin - ENV["ONE_CIPHER_AUTH"] = ONEFLOW_AUTH + ENV['ONE_CIPHER_AUTH'] = ONEFLOW_AUTH cloud_auth = CloudAuth.new(conf) -rescue => e +rescue StandardError => e message = "Error initializing authentication system : #{e.message}" Log.error LOG_COMP, message STDERR.puts message - exit -1 + exit(-1) end set :cloud_auth, cloud_auth @@ -110,16 +115,43 @@ set :cloud_auth, cloud_auth # Helpers ############################################################################## - before do auth = Rack::Auth::Basic::Request.new(request.env) if auth.provided? && auth.basic? username, password = auth.credentials - @client = OpenNebula::Client.new("#{username}:#{password}", conf[:one_xmlrpc]) + @client = OpenNebula::Client.new("#{username}:#{password}", + conf[:one_xmlrpc]) else - error 401, "A username and password must be provided" + error 401, 'A username and password must be provided' + end +end + +# Set status error and return the error msg +# +# @param error_msg [String] Error message +# @param error_code [Integer] Http error code +def internal_error(error_msg, error_code) + status error_code + body error_msg +end + +# Get HTTP error code based on OpenNebula eror code +# +# @param error [Integer] OpenNebula error code +def one_error_to_http(error) + case error + when OpenNebula::Error::ESUCCESS + 200 + when OpenNebula::Error::EAUTHORIZATION + 401 + when OpenNebula::Error::EAUTHENTICATION + 403 + when OpenNebula::Error::ENO_EXISTS + 404 + else + 500 end end @@ -130,32 +162,36 @@ end Role.init_default_cooldown(conf[:default_cooldown]) Role.init_default_shutdown(conf[:shutdown_action]) Role.init_force_deletion(conf[:force_deletion]) - -conf[:vm_name_template] ||= DEFAULT_VM_NAME_TEMPLATE Role.init_default_vm_name_template(conf[:vm_name_template]) +ServiceTemplate.init_default_vn_name_template(conf[:vn_name_template]) + ############################################################################## -# LCM thread +# HTTP error codes ############################################################################## -t = Thread.new { - require 'LifeCycleManager' +VALIDATION_EC = 400 # bad request by the client +OPERATION_EC = 405 # operation not allowed (e.g: in current state) +GENERAL_EC = 500 # general error - ServiceLCM.new(conf[:lcm_interval], cloud_auth).loop -} -t.abort_on_exception = true +############################################################################## +# LCM and Event Manager +############################################################################## +# TODO: make thread number configurable? +lcm = ServiceLCM.new(@client, 10, cloud_auth) ############################################################################## # Service ############################################################################## get '/service' do - service_pool = OpenNebula::ServicePool.new(@client, OpenNebula::Pool::INFO_ALL) + # Read-only object + service_pool = OpenNebula::ServicePool.new(nil, @client) rc = service_pool.info if OpenNebula.is_error?(rc) - error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + return internal_error(rc.message, one_error_to_http(rc.errno)) end status 200 @@ -164,12 +200,11 @@ get '/service' do end get '/service/:id' do - service_pool = OpenNebula::ServicePool.new(@client) + service = Service.new_with_id(params[:id], @client) - service = service_pool.get(params[:id]) - - if OpenNebula.is_error?(service) - error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + rc = service.info + if OpenNebula.is_error?(rc) + return internal_error(rc.message, one_error_to_http(rc.errno)) end status 200 @@ -178,174 +213,157 @@ get '/service/:id' do end delete '/service/:id' do - service_pool = OpenNebula::ServicePool.new(@client) - - rc = nil - service = service_pool.get(params[:id]) { |service| - rc = service.delete - } - - if OpenNebula.is_error?(service) - error CloudServer::HTTP_ERROR_CODE[service.errno], service.message - end + # Read-only object + service = OpenNebula::Service.new_with_id(params[:id], @client) + rc = service.info if OpenNebula.is_error?(rc) error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message end + # Starts service undeploying async + rc = lcm.undeploy_action(@client, service.id) + + if OpenNebula.is_error?(rc) + return internal_error(rc.message, one_error_to_http(rc.errno)) + end + status 204 end post '/service/:id/action' do - service_pool = OpenNebula::ServicePool.new(@client) action = JSON.parse(request.body.read)['action'] opts = action['params'] - rc = nil - service = service_pool.get(params[:id]) { |service| - rc = case action['perform'] - when 'shutdown' - service.shutdown - when 'recover', 'deploy' - service.recover - when 'chown' - if opts && opts['owner_id'] - args = Array.new - args << opts['owner_id'].to_i - args << (opts['group_id'] || -1).to_i + case action['perform'] + when 'recover' + rc = lcm.recover_action(@client, params[:id]) + when 'chown' + if opts && opts['owner_id'] + u_id = opts['owner_id'].to_i + g_id = (opts['group_id'] || -1).to_i - ret = service.chown(*args) - - if !OpenNebula.is_error?(ret) - Log.info(LOG_COMP, "Service owner changed to #{args[0]}:#{args[1]}", params[:id]) - end - - ret - else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to specify a UID") - end - when 'chgrp' - if opts && opts['group_id'] - ret = service.chown(-1, opts['group_id'].to_i) - - if !OpenNebula.is_error?(ret) - Log.info(LOG_COMP, "Service group changed to #{opts['group_id']}", params[:id]) - end - - ret - else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to specify a GID") - end - when 'chmod' - if opts && opts['octet'] - ret = service.chmod_octet(opts['octet']) - - if !OpenNebula.is_error?(ret) - Log.info(LOG_COMP, "Service permissions changed to #{opts['octet']}", params[:id]) - end - - ret - else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to specify an OCTET") - end - when 'rename' - service.rename(opts['name']) - when 'update' - if opts && opts['append'] - if opts['template_json'] - begin - rc = service.update(opts['template_json'], true) - status 204 - rescue Validator::ParseException, JSON::ParserError - OpenNebula::Error.new($!.message) - end - elsif opts['template_raw'] - rc = service.update_raw(opts['template_raw'], true) - status 204 - else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to provide a template") - end - else - OpenNebula::Error.new("Action #{action['perform']}: " << - "Only supported for append") - end + rc = lcm.chown_action(@client, params[:id], u_id, g_id) else - OpenNebula::Error.new("Action #{action['perform']} not supported") + rc = OpenNebula::Error.new("Action #{action['perform']}: " \ + 'You have to specify a UID') end - } + when 'chgrp' + if opts && opts['group_id'] + g_id = opts['group_id'].to_i - if OpenNebula.is_error?(service) - error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + rc = lcm.chown_action(@client, params[:id], -1, g_id) + else + rc = OpenNebula::Error.new("Action #{action['perform']}: " \ + 'You have to specify a GID') + end + when 'chmod' + if opts && opts['octet'] + rc = lcm.chmod_action(@client, params[:id], opts['octet']) + else + rc = OpenNebula::Error.new("Action #{action['perform']}: " \ + 'You have to specify an OCTET') + end + when 'rename' + if opts && opts['name'] + rc = lcm.rename_action(@client, params[:id], opts['name']) + else + rc = OpenNebula::Error.new("Action #{action['perform']}: " \ + 'You have to specify a name') + end + # when 'update' + # if opts && opts['append'] + # if opts['template_json'] + # begin + # service.update(opts['template_json'], true) + # status 204 + # rescue Validator::ParseException, JSON::ParserError => e + # OpenNebula::Error.new(e.message) + # end + # elsif opts['template_raw'] + # service.update_raw(opts['template_raw'], true) + # status 204 + # else + # OpenNebula::Error.new("Action #{action['perform']}: " \ + # 'You have to provide a template') + # end + # else + # OpenNebula::Error.new("Action #{action['perform']}: " \ + # 'Only supported for append') + # end + else + rc = OpenNebula::Error.new("Action #{action['perform']} not supported") end if OpenNebula.is_error?(rc) - error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + return internal_error(rc.message, one_error_to_http(rc.errno)) end status 204 end -put '/service/:id/role/:name' do - - service_pool = OpenNebula::ServicePool.new(@client) - - rc = nil - service = service_pool.get(params[:id]) do |service| - begin - rc = service.update_role(params[:name], request.body.read) - rescue Validator::ParseException, JSON::ParserError - return error 400, $!.message - end - end - - if OpenNebula.is_error?(service) - error CloudServer::HTTP_ERROR_CODE[service.errno], service.message - end - - if OpenNebula.is_error?(rc) - error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message - end - - status 204 -end +# put '/service/:id/role/:name' do +# service_pool = nil # OpenNebula::ServicePool.new(@client) +# +# rc = nil +# service_rc = service_pool.get(params[:id]) do |service| +# begin +# rc = service.update_role(params[:name], request.body.read) +# rescue Validator::ParseException, JSON::ParserError => e +# return internal_error(e.message, VALIDATION_EC) +# end +# end +# +# if OpenNebula.is_error?(service_rc) +# error CloudServer::HTTP_ERROR_CODE[service_rc.errno], service_rc.message +# end +# +# if OpenNebula.is_error?(rc) +# error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message +# end +# +# status 204 +# end post '/service/:id/role/:role_name/action' do - service_pool = OpenNebula::ServicePool.new(@client) action = JSON.parse(request.body.read)['action'] opts = action['params'] - rc = nil - service = service_pool.get(params[:id]) { |service| - roles = service.get_roles - - role = roles[params[:role_name]] - if role.nil? - rc = OpenNebula::Error.new("Role '#{params[:role_name]}' not found") - else - # Use defaults only if one of the options is supplied - if opts['period'].nil? ^ opts['number'].nil? - opts['period'] = conf[:action_period] if opts['period'].nil? - opts['number'] = conf[:action_number] if opts['number'].nil? - end - - rc = role.batch_action(action['perform'], opts['period'], opts['number']) - end - } - - if OpenNebula.is_error?(service) - error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + # Use defaults only if one of the options is supplied + if opts['period'].nil? && opts['number'].nil? + opts['period'] = conf[:action_period] if opts['period'].nil? + opts['number'] = conf[:action_number] if opts['number'].nil? end + rc = lcm.sched_action(@client, + params[:id], + params[:role_name], + action['perform'], + opts['period'], + opts['number']) + if OpenNebula.is_error?(rc) - error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + return internal_error(rc.message, one_error_to_http(rc.errno)) end status 201 - body rc.to_json +end + +post '/service/:id/scale' do + call_body = JSON.parse(request.body.read) + + rc = lcm.scale_action(@client, + params[:id], + call_body['role_name'], + call_body['cardinality'].to_i, + call_body['force']) + + if OpenNebula.is_error?(rc) + return internal_error(rc.message, one_error_to_http(rc.errno)) + end + + status 201 + body end ############################################################################## @@ -353,7 +371,8 @@ end ############################################################################## get '/service_template' do - s_template_pool = OpenNebula::ServiceTemplatePool.new(@client, OpenNebula::Pool::INFO_ALL) + s_template_pool = OpenNebula::ServiceTemplatePool + .new(@client, OpenNebula::Pool::INFO_ALL) rc = s_template_pool.info if OpenNebula.is_error?(rc) @@ -366,7 +385,8 @@ get '/service_template' do end get '/service_template/:id' do - service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], + @client) rc = service_template.info if OpenNebula.is_error?(rc) @@ -379,7 +399,8 @@ get '/service_template/:id' do end delete '/service_template/:id' do - service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], + @client) rc = service_template.delete if OpenNebula.is_error?(rc) @@ -390,12 +411,13 @@ delete '/service_template/:id' do end put '/service_template/:id' do - service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], + @client) begin rc = service_template.update(request.body.read) - rescue Validator::ParseException, JSON::ParserError - error 400, $!.message + rescue Validator::ParseException, JSON::ParserError => e + return internal_error(e.message, VALIDATION_EC) end if OpenNebula.is_error?(rc) @@ -405,18 +427,18 @@ put '/service_template/:id' do service_template.info status 200 + body service_template.to_json end post '/service_template' do - s_template = OpenNebula::ServiceTemplate.new( - OpenNebula::ServiceTemplate.build_xml, - @client) + xml = OpenNebula::ServiceTemplate.build_xml + s_template = OpenNebula::ServiceTemplate.new(xml, @client) begin rc = s_template.allocate(request.body.read) - rescue Validator::ParseException, JSON::ParserError - error 400, $!.message + rescue Validator::ParseException, JSON::ParserError => e + return internal_error(e.message, VALIDATION_EC) end if OpenNebula.is_error?(rc) @@ -426,122 +448,142 @@ post '/service_template' do s_template.info status 201 - #body Parser.render(rc) + + # body Parser.render(rc) body s_template.to_json end post '/service_template/:id/action' do - service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) - + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], + @client) action = JSON.parse(request.body.read)['action'] - opts = action['params'] opts = {} if opts.nil? - rc = case action['perform'] + # rubocop:disable Style/ConditionalAssignment + case action['perform'] when 'instantiate' rc = service_template.info + if OpenNebula.is_error?(rc) - error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + return internal_error(rc.message, one_error_to_http(rc.errno)) end merge_template = opts['merge_template'] + service_json = JSON.parse(service_template.to_json) - if !merge_template.nil? - begin - orig_template = JSON.parse(service_template.template) + # Check custom_attrs + body = service_json['DOCUMENT']['TEMPLATE']['BODY'] + custom_attrs = body['custom_attrs'] - instantiate_template = orig_template.merge(merge_template) + if merge_template + custom_attrs_values = merge_template['custom_attrs_values'] + end - ServiceTemplate.validate(instantiate_template) + if custom_attrs && !(custom_attrs.is_a? Hash) + return internal_error('Wrong custom_attrs format', + VALIDATION_EC) + end - instantiate_template["roles"].each { |role| - if role["vm_template_contents"] - # $CUSTOM1_VAR Any word character (letter, number, underscore) - role["vm_template_contents"].scan(/\$(\w+)/).each { |key| - if instantiate_template["custom_attrs_values"].has_key?(key[0]) - role["vm_template_contents"].gsub!( - "$"+key[0], - instantiate_template["custom_attrs_values"][key[0]]) - end - } - end + if custom_attrs_values && !(custom_attrs_values.is_a? Hash) + return internal_error('Wrong custom_attrs_values format', + VALIDATION_EC) + end - if role["user_inputs_values"] - role["vm_template_contents"] ||= "" - role["user_inputs_values"].each{ |key, value| - role["vm_template_contents"] += "\n#{key}=\"#{value}\"" - } - end - } + if custom_attrs && + custom_attrs_values && + !(custom_attrs.keys - custom_attrs_values.keys).empty? + return internal_error('Every custom_attrs key must have its ' \ + 'value defined at custom_attrs_value', + VALIDATION_EC) + end - instantiate_template_json = instantiate_template.to_json + # Check networks + networks = body['networks'] + networks_values = merge_template['networks_values'] if merge_template - rescue Validator::ParseException, JSON::ParserError - error 400, $!.message - end + if networks && !(networks.is_a? Hash) + return internal_error('Wrong networks format', VALIDATION_EC) + end + + if networks_values && networks_values.find {|v| !v.is_a? Hash } + return internal_error('Wrong networks_values format', VALIDATION_EC) + end + + if networks && networks_values && !(networks.keys - + networks_values.collect {|i| i.keys }.flatten).empty? + return internal_error('Every network key must have its value ' \ + 'defined at networks_value', VALIDATION_EC) + end + + # Creates service document + service = service_template.instantiate(merge_template) + + if OpenNebula.is_error?(service) + return internal_error(service.message, + one_error_to_http(service.errno)) + elsif service.is_a? StandardError + # there was a JSON validation error + return internal_error(service.message, GENERAL_EC) else - instantiate_template_json = service_template.template + # Starts service deployment async + rc = lcm.deploy_action(@client, service.id) + + if OpenNebula.is_error?(rc) + return internal_error(rc.message, one_error_to_http(rc.errno)) + end + + service_json = service.nil? ? '' : service.to_json + + status 201 + body service_json end - - service = OpenNebula::Service.new(OpenNebula::Service.build_xml, @client) - rc = service.allocate(instantiate_template_json) - if OpenNebula.is_error?(rc) - error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message - end - - service.info - - status 201 - body service.to_json when 'chown' if opts && opts['owner_id'] - args = Array.new + args = [] args << opts['owner_id'].to_i args << (opts['group_id'].to_i || -1) status 204 service_template.chown(*args) else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to specify a UID") + OpenNebula::Error.new("Action #{action['perform']}: "\ + 'You have to specify a UID') end when 'chgrp' if opts && opts['group_id'] status 204 service_template.chown(-1, opts['group_id'].to_i) else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to specify a GID") + OpenNebula::Error.new("Action #{action['perform']}: "\ + 'You have to specify a GID') end when 'chmod' if opts && opts['octet'] status 204 service_template.chmod_octet(opts['octet']) else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to specify an OCTET") + OpenNebula::Error.new("Action #{action['perform']}: "\ + 'You have to specify an OCTET') end when 'update' + append = opts['append'] == true + if opts && opts['template_json'] begin - rc = service_template.update( - opts['template_json'], - (opts['append'] == true)) + service_template.update(opts['template_json'], append) status 204 - rescue Validator::ParseException, JSON::ParserError - OpenNebula::Error.new($!.message) + rescue Validator::ParseException, JSON::ParserError => e + return internal_error(e.message, VALIDATION_EC) end elsif opts && opts['template_raw'] - rc = service_template.update_raw( - opts['template_raw'], - (opts['append'] == true)) + service_template.update_raw(opts['template_raw'], append) status 204 else - OpenNebula::Error.new("Action #{action['perform']}: " << - "You have to provide a template") + OpenNebula::Error.new("Action #{action['perform']}: "\ + 'You have to provide a template') end when 'rename' status 204 @@ -555,7 +597,8 @@ post '/service_template/:id/action' do new_stemplate = OpenNebula::ServiceTemplate.new_with_id(rc, @client) new_stemplate.info if OpenNebula.is_error?(new_stemplate) - error CloudServer::HTTP_ERROR_CODE[new_stemplate.errno], new_stemplate.message + error CloudServer::HTTP_ERROR_CODE[new_stemplate.errno], + new_stemplate.message end status 201 @@ -563,8 +606,10 @@ post '/service_template/:id/action' do else OpenNebula::Error.new("Action #{action['perform']} not supported") end + # rubocop:enable Style/ConditionalAssignment if OpenNebula.is_error?(rc) error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message end end +# rubocop:enable Naming/FileName diff --git a/src/oca/ruby/opennebula/oneflow_client.rb b/src/oca/ruby/opennebula/oneflow_client.rb index 9f649f9187..455016d446 100644 --- a/src/oca/ruby/opennebula/oneflow_client.rb +++ b/src/oca/ruby/opennebula/oneflow_client.rb @@ -285,7 +285,7 @@ module Service exit_code = 0 ids.each do |id| - response = block.call(id) + response = block.call(id) if block_given? if CloudClient::is_error?(response) puts response.to_s @@ -296,6 +296,22 @@ module Service exit_code end + # Perform an action on a resource + # @param [Integer] id resource id + # @param [Block] block action to be performed + # @return [Integer] exit_code + def self.perform_action(id, &block) + exit_code = 0 + response = block.call(id) if block_given? + + if CloudClient::is_error?(response) + puts response.to_s + exit_code = response.code.to_i + end + + exit_code + end + class Client def initialize(opts={}) @username = opts[:username] || ENV['ONEFLOW_USER'] diff --git a/src/oca/ruby/opennebula/virtual_network.rb b/src/oca/ruby/opennebula/virtual_network.rb index 449effa7f4..beb9206752 100644 --- a/src/oca/ruby/opennebula/virtual_network.rb +++ b/src/oca/ruby/opennebula/virtual_network.rb @@ -252,6 +252,12 @@ module OpenNebula return @client.call(VN_METHODS[:reserve], @pe_id, rtmpl) end + def reserve_with_extra(extra) + return Error.new('ID not defined') unless @pe_id + + @client.call(VN_METHODS[:reserve], @pe_id, extra) + end + # Removes an Address Range from the VirtualNetwork def free(ar_id) return Error.new('ID not defined') if !@pe_id diff --git a/src/sunstone/etc/sunstone-server.conf b/src/sunstone/etc/sunstone-server.conf index 23d30579b0..9c56d331a9 100644 --- a/src/sunstone/etc/sunstone-server.conf +++ b/src/sunstone/etc/sunstone-server.conf @@ -200,3 +200,18 @@ - vcenter - support - nsx + +# this display button and clock icon in table of vm +:leases: + suspense: + time: "+1209600" + color: "#000000" + warning: + time: "-86400" + color: "#085aef" + terminate: + time: "+1209600" + color: "#e1ef08" + warning: + time: "-86400" + color: "#ef2808" \ No newline at end of file diff --git a/src/sunstone/public/app/tabs/oneflow-services-tab/form-panels/create.js b/src/sunstone/public/app/tabs/oneflow-services-tab/form-panels/create.js index 8186e153d9..1e42f46085 100644 --- a/src/sunstone/public/app/tabs/oneflow-services-tab/form-panels/create.js +++ b/src/sunstone/public/app/tabs/oneflow-services-tab/form-panels/create.js @@ -74,9 +74,8 @@ define(function(require) { $(".instantiate_wrapper", context).hide(); - this.templatesTable.idInput().on("change", function(){ + this.templatesTable.idInput().off("change").on("change", function(){ $(".instantiate_wrapper", context).show(); - var template_id = $(this).val(); that.setTemplateId(context, template_id); }); diff --git a/src/sunstone/public/app/tabs/oneflow-templates-tab/form-panels/instantiate.js b/src/sunstone/public/app/tabs/oneflow-templates-tab/form-panels/instantiate.js index b800ab4850..7d70aa890b 100644 --- a/src/sunstone/public/app/tabs/oneflow-templates-tab/form-panels/instantiate.js +++ b/src/sunstone/public/app/tabs/oneflow-templates-tab/form-panels/instantiate.js @@ -113,16 +113,16 @@ define(function(require) { $(".name", context).text(template_json.DOCUMENT.NAME); $("#instantiate_service_user_inputs", context).empty(); - UserInputs.serviceTemplateInsert( $("#instantiate_service_user_inputs", context), - template_json); + template_json, { + select_networks: true + }); n_roles = template_json.DOCUMENT.TEMPLATE.BODY.roles.length; n_roles_done = 0; var total_cost = 0; - $.each(template_json.DOCUMENT.TEMPLATE.BODY.roles, function(index, role){ var div_id = "user_input_role_"+index; @@ -140,30 +140,31 @@ define(function(require) { success: function (request, vm_template_json){ that.vm_template_json = vm_template_json; $("#"+div_id, context).empty(); - - if (role.vm_template_contents){ + //if (role.vm_template_contents){ roleTemplate = TemplateUtils.stringToTemplate(role.vm_template_contents); - var append = roleTemplate.APPEND.split(","); - $.each(append, function(key, value){ - if (!that.vm_template_json.VMTEMPLATE.TEMPLATE[value]){ - that.vm_template_json.VMTEMPLATE.TEMPLATE[value] = roleTemplate[value]; - } else { - if (!Array.isArray(that.vm_template_json.VMTEMPLATE.TEMPLATE[value])){ - that.vm_template_json.VMTEMPLATE.TEMPLATE[value] = [that.vm_template_json.VMTEMPLATE.TEMPLATE[value]]; - } - if (Array.isArray(roleTemplate[value])){ - $.each(roleTemplate[value], function(rkey, rvalue){ - that.vm_template_json.VMTEMPLATE.TEMPLATE[value].push(rvalue); - }); + if(roleTemplate && roleTemplate.APPEND){ + var append = roleTemplate.APPEND.split(","); + $.each(append, function(key, value){ + if (!that.vm_template_json.VMTEMPLATE.TEMPLATE[value]){ + that.vm_template_json.VMTEMPLATE.TEMPLATE[value] = roleTemplate[value]; } else { - that.vm_template_json.VMTEMPLATE.TEMPLATE[value].push(roleTemplate[value]); + if (!Array.isArray(that.vm_template_json.VMTEMPLATE.TEMPLATE[value])){ + that.vm_template_json.VMTEMPLATE.TEMPLATE[value] = [that.vm_template_json.VMTEMPLATE.TEMPLATE[value]]; + } + if (Array.isArray(roleTemplate[value])){ + $.each(roleTemplate[value], function(rkey, rvalue){ + that.vm_template_json.VMTEMPLATE.TEMPLATE[value].push(rvalue); + }); + } else { + that.vm_template_json.VMTEMPLATE.TEMPLATE[value].push(roleTemplate[value]); + } } - } - delete roleTemplate[value]; - }); - delete roleTemplate.APPEND; + delete roleTemplate[value]; + }); + delete roleTemplate.APPEND; + } $.extend(true, that.vm_template_json.VMTEMPLATE.TEMPLATE, roleTemplate); - } + //} if (vm_template_json.VMTEMPLATE.TEMPLATE["MEMORY_COST"] && vm_template_json.VMTEMPLATE.TEMPLATE["MEMORY_UNIT_COST"] && vm_template_json.VMTEMPLATE.TEMPLATE["MEMORY_UNIT_COST"] === "GB") { vm_template_json.VMTEMPLATE.TEMPLATE["MEMORY_COST"] = vm_template_json.VMTEMPLATE.TEMPLATE["MEMORY_COST"]*1024; } @@ -227,11 +228,43 @@ define(function(require) { var extra_info = { "merge_template": {} }; - var tmp_json = WizardFields.retrieve($("#instantiate_service_user_inputs", context)); + - extra_info.merge_template.custom_attrs_values = tmp_json; + var network_values = []; + var prefix = "type_"; + var networks = Object.keys(tmp_json).filter(function(k) { + return k.indexOf('type_') == 0; + }).reduce(function(newData, k) { + var key = "id"; + switch (tmp_json[k]) { + case "create": + key = "template_id"; + break; + case "reserve": + key = "reserve_from"; + break; + default: + break; + } + var internal = {}; + internal[k.replace(prefix,"")] = {}; + internal[k.replace(prefix,"")][key] = tmp_json[k.replace(prefix,"")]; + if(tmp_json[k] === "create" || tmp_json[k] === "reserve"){ + internal[k.replace(prefix,"")].extra = tmp_json["extra_"+k.replace(prefix,"")]; + } + newData[k.replace(prefix,"")] = internal; + return newData; + }, {}); + //parse to array + Object.keys(networks).map(function(key_network){ + network_values.push(networks[key_network]); + }); + + //extra_info.merge_template.custom_attrs_values = tmp_json; //OLD + extra_info.merge_template.custom_attrs_values = {}; + extra_info.merge_template.networks_values = network_values; extra_info.merge_template.roles = []; $.each(that.service_template_json.DOCUMENT.TEMPLATE.BODY.roles, function(index, role){ diff --git a/src/sunstone/public/app/tabs/provision-tab.js b/src/sunstone/public/app/tabs/provision-tab.js index 6a3552ed79..61ef9840e9 100644 --- a/src/sunstone/public/app/tabs/provision-tab.js +++ b/src/sunstone/public/app/tabs/provision-tab.js @@ -1157,7 +1157,7 @@ define(function(require) { }); - tab.on("click", ".provision_select_flow_template .provision-pricing-table.only-one" , function(){ + tab.off("click").on("click", ".provision_select_flow_template .provision-pricing-table.only-one" , function(){ var context = $("#provision_create_flow"); if ($(this).hasClass("selected")){ diff --git a/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general.js b/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general.js index dfac394452..e67695d026 100644 --- a/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general.js +++ b/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general.js @@ -30,6 +30,7 @@ define(function(require) { var UsersTable = require("tabs/users-tab/datatable"); var GroupTable = require("tabs/groups-tab/datatable"); var OpenNebulaHost = require("opennebula/host"); + var Leases = require("utils/leases"); /* TEMPLATES @@ -89,7 +90,8 @@ define(function(require) { 'capacityCreateHTML': CapacityCreate.html(), 'logos': Config.vmLogos, 'usersDatatable': this.usersTable.dataTableHTML, - 'groupDatatable': this.groupTable.dataTableHTML + 'groupDatatable': this.groupTable.dataTableHTML, + 'leases': Leases.html() }); } @@ -106,6 +108,8 @@ define(function(require) { .prop('wizard_field_disabled', true); } + Leases.actions(panelForm); + if (panelForm.resource == "VirtualRouterTemplate"){ $("input[wizard_field=VROUTER]", context).attr("checked", "checked"); } @@ -114,11 +118,11 @@ define(function(require) { } function convertCostNumber(number){ if(number >= 1000000){ - number = (number/1000000).toFixed(6) + number = (number/1000000).toFixed(6); return number.toString()+"M"; } else if(number >= 1000){ - number = (number/1000).toFixed(6) + number = (number/1000).toFixed(6); return number.toString()+"K"; } return number.toFixed(6); diff --git a/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general/html.hbs b/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general/html.hbs index c3866c894d..1a34542906 100644 --- a/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general/html.hbs +++ b/src/sunstone/public/app/tabs/templates-tab/form-panels/create/wizard-tabs/general/html.hbs @@ -14,7 +14,12 @@ {{! limitations under the License. }} {{! -------------------------------------------------------------------------- }}
-
+
+ {{{leases}}} +
+
+
+
+
+ {{{leases}}} +