diff --git a/src/cli/oneflow b/src/cli/oneflow index 192a6c05a9..89caa98663 100755 --- a/src/cli/oneflow +++ b/src/cli/oneflow @@ -428,4 +428,76 @@ CommandParser::CmdParser.new(ARGV) do 0 end end + + ### + + add_role_desc = <<-EOT.unindent + Add new role to running service + EOT + + command :'add-role', add_role_desc, :service_id, [:file, nil] do + service_id = args[0] + client = helper.client(options) + + if args[1] + path = args[1] + else + tmp = Tempfile.new(service_id.to_s) + path = tmp.path + + if ENV['EDITOR'] + editor_path = ENV['EDITOR'] + else + editor_path = OpenNebulaHelper::EDITOR_PATH + end + + system("#{editor_path} #{path}") + + unless $CHILD_STATUS.exitstatus.zero? + STDERR.puts 'Editor not defined' + exit(-1) + end + + tmp.close + end + + params = {} + params[:role] = File.read(path) + params[:add] = true + json = Service.build_json_action('add_role', params) + + response = client.post("#{RESOURCE_PATH}/#{service_id}/role_action", + json) + + if CloudClient.is_error?(response) + [response.code.to_i, response.to_s] + else + 0 + end + end + + ### + + remove_role_desc = <<-EOT.unindent + Remove role from running service + EOT + + command :'remove-role', remove_role_desc, :service_id, :role_name do + service_id = args[0] + client = helper.client(options) + + params = {} + params[:role] = args[1] + params[:add] = false + json = Service.build_json_action('remove_role', params) + + response = client.post("#{RESOURCE_PATH}/#{service_id}/role_action", + json) + + if CloudClient.is_error?(response) + [response.code.to_i, response.to_s] + else + 0 + end + end end diff --git a/src/flow/lib/EventManager.rb b/src/flow/lib/EventManager.rb index 216d066e8a..c319a222e5 100644 --- a/src/flow/lib/EventManager.rb +++ b/src/flow/lib/EventManager.rb @@ -31,6 +31,8 @@ class EventManager 'WAIT_UNDEPLOY' => :wait_undeploy, 'WAIT_SCALEUP' => :wait_scaleup, 'WAIT_SCALEDOWN' => :wait_scaledown, + 'WAIT_ADD' => :wait_add, + 'WAIT_REMOVE' => :wait_remove, 'WAIT_COOLDOWN' => :wait_cooldown } @@ -86,6 +88,10 @@ class EventManager method('wait_cooldown')) @am.register_action(ACTIONS['WAIT_SCALEUP'], method('wait_scaleup_action')) + @am.register_action(ACTIONS['WAIT_ADD'], + method('wait_add_action')) + @am.register_action(ACTIONS['WAIT_REMOVE'], + method('wait_remove_action')) @am.register_action(ACTIONS['WAIT_SCALEDOWN'], method('wait_scaledown_action')) @@ -204,6 +210,56 @@ class EventManager end end + def wait_add_action(client, service_id, role_name, nodes, report) + if report + Log.info LOG_COMP, "Waiting #{nodes} to report ready" + rc = wait_report_ready(nodes) + else + Log.info LOG_COMP, "Waiting #{nodes} to be (ACTIVE, RUNNING)" + rc = wait(nodes, 'ACTIVE', 'RUNNING') + end + + if rc[0] + @lcm.trigger_action(:add_cb, + service_id, + client, + service_id, + role_name, + rc[1]) + else + @lcm.trigger_action(:add_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_remove_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(:remove_cb, + service_id, + client, + service_id, + role_name, + rc[1]) + else + @lcm.trigger_action(:remove_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 diff --git a/src/flow/lib/LifeCycleManager.rb b/src/flow/lib/LifeCycleManager.rb index 44687ef12a..b06a0fd319 100644 --- a/src/flow/lib/LifeCycleManager.rb +++ b/src/flow/lib/LifeCycleManager.rb @@ -39,6 +39,10 @@ class ServiceLCM 'SCALEUP_FAILURE_CB' => :scaleup_failure_cb, 'SCALEDOWN_CB' => :scaledown_cb, 'SCALEDOWN_FAILURE_CB' => :scaledown_failure_cb, + 'ADD_CB' => :add_cb, + 'ADD_FAILURE_CB' => :add_failure_cb, + 'REMOVE_CB' => :remove_cb, + 'REMOVE_FAILURE_CB' => :remove_failure_cb, # WD callbacks 'ERROR_WD_CB' => :error_wd_cb, @@ -79,6 +83,14 @@ class ServiceLCM method('scaledown_failure_cb')) @am.register_action(ACTIONS['COOLDOWN_CB'], method('cooldown_cb')) + @am.register_action(ACTIONS['ADD_CB'], + method('add_cb')) + @am.register_action(ACTIONS['add_FAILURE_CB'], + method('add_failure_cb')) + @am.register_action(ACTIONS['REMOVE_CB'], + method('remove_cb')) + @am.register_action(ACTIONS['REMOVE_FAILURE_CB'], + method('remove_failure_cb')) @am.register_action(ACTIONS['ERROR_WD_CB'], method('error_wd_cb')) @am.register_action(ACTIONS['DONE_WD_CB'], @@ -252,7 +264,7 @@ class ServiceLCM roles, 'DEPLOYING', 'FAILED_DEPLOYING', - false, + :wait_deploy, service.report_ready?) if !OpenNebula.is_error?(rc) @@ -304,7 +316,7 @@ class ServiceLCM roles, 'UNDEPLOYING', 'FAILED_UNDEPLOYING', - false) + :wait_undeploy) if !OpenNebula.is_error?(rc) service.set_state(Service::STATE['UNDEPLOYING']) @@ -371,7 +383,7 @@ class ServiceLCM { role_name => role }, 'SCALING', 'FAILED_SCALING', - true, + :wait_scaleup, service.report_ready?) elsif cardinality_diff < 0 role.scale_way('DOWN') @@ -380,7 +392,7 @@ class ServiceLCM { role_name => role }, 'SCALING', 'FAILED_SCALING', - true) + :wait_scaledown) else break OpenNebula::Error.new( "Cardinality of #{role_name} is already at #{cardinality}" @@ -515,6 +527,69 @@ class ServiceLCM rc end + # Add role from running service + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # @param role [Hash] Role information + def add_role_action(client, service_id, role) + rc = @srv_pool.get(service_id, client) do |service| + unless service.running? + break OpenNebula::Error.new( + "Cannot modify roles in state: #{service.state_str}" + ) + end + + role = service.add_role(role) + + break role if OpenNebula.is_error?(role) + + service.update + + rc = service.deploy_networks(false) + + if OpenNebula.is_error?(rc) + service.set_state(Service::STATE['FAILED_DEPLOYING']) + service.update + + break rc + end + + service.update + + add_role(client, service, role) + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + + # Remove role from running service + # + # @param client [OpenNebula::Client] Client executing action + # @param service_id [Integer] Service ID + # @param role [Hash] Role information + def remove_role_action(client, service_id, role) + rc = @srv_pool.get(service_id, client) do |service| + unless service.running? + break OpenNebula::Error.new( + "Cannot modify roles in state: #{service.state_str}" + ) + end + + unless service.roles[role] + break OpenNebula::Error.new("Role #{role} does not exist") + end + + remove_role(client, service, service.roles[role]) + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + private ############################################################################ @@ -547,7 +622,7 @@ class ServiceLCM service.roles_deploy, 'DEPLOYING', 'FAILED_DEPLOYING', - false, + :wait_deploy, service.report_ready?) end @@ -608,7 +683,7 @@ class ServiceLCM service.roles_shutdown, 'UNDEPLOYING', 'FAILED_UNDEPLOYING', - false) + :wait_undeploy) end service.update @@ -749,6 +824,72 @@ class ServiceLCM undeploy_action(client, service_id) end + def add_cb(client, service_id, role_name, _) + rc = @srv_pool.get(service_id, client) do |service| + service.roles[role_name].set_state(Role::STATE['RUNNING']) + + service.set_state(Service::STATE['RUNNING']) + + rc = service.update + + return rc if OpenNebula.is_error?(rc) + + @wd.add_service(service) if service.all_roles_running? + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def add_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 remove_cb(client, service_id, role_name, _) + rc = @srv_pool.get(service_id, client) do |service| + service.remove_role(role_name) + + service.set_state(Service::STATE['RUNNING']) + + rc = service.update + + return rc if OpenNebula.is_error?(rc) + + @wd.add_service(service) if service.all_roles_running? + end + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + end + + def remove_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 + ############################################################################ # WatchDog Callbacks ############################################################################ @@ -773,7 +914,10 @@ class ServiceLCM undeploy = false rc = @srv_pool.get(service_id, client) do |service| - role = service.roles[role_name] + role = service.roles[role_name] + + next unless role + cardinality = role.cardinality - 1 next unless role.nodes.find {|n| n['deploy_id'] == node } @@ -857,14 +1001,8 @@ class ServiceLCM # @param [Role::STATE] error_state new state of the role # if deployed unsuccessfuly # rubocop:disable Metrics/ParameterLists - def deploy_roles(client, roles, success_state, error_state, scale, report) + def deploy_roles(client, roles, success_state, error_state, action, report) # rubocop:enable Metrics/ParameterLists - if scale - action = :wait_scaleup - else - action = :wait_deploy - end - roles.each do |name, role| rc = role.deploy @@ -887,13 +1025,7 @@ class ServiceLCM end end - def undeploy_roles(client, roles, success_state, error_state, scale) - if scale - action = :wait_scaledown - else - action = :wait_undeploy - end - + def undeploy_roles(client, roles, success_state, error_state, action) roles.each do |name, role| rc = role.shutdown(false) @@ -981,5 +1113,54 @@ class ServiceLCM end end + def add_role(client, service, role) + @wd.remove_service(service.id) + + set_deploy_strategy(service) + + rc = deploy_roles(client, + { role.name => role }, + 'DEPLOYING', + 'FAILED_DEPLOYING', + :wait_add, + service.report_ready?) + + if !OpenNebula.is_error?(rc) + service.set_state(Service::STATE['DEPLOYING']) + else + service.set_state(Service::STATE['FAILED_DEPLOYING']) + end + + service.update + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + + def remove_role(client, service, role) + @wd.remove_service(service.id) + + set_deploy_strategy(service) + + rc = undeploy_roles(client, + { role.name => role }, + 'UNDEPLOYING', + 'FAILED_UNDEPLOYING', + :wait_remove) + + if !OpenNebula.is_error?(rc) + service.set_state(Service::STATE['UNDEPLOYING']) + else + service.set_state(Service::STATE['FAILED_UNDEPLOYING']) + end + + service.update + + Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc) + + rc + end + end # rubocop:enable Naming/FileName diff --git a/src/flow/lib/ServiceAutoScaler.rb b/src/flow/lib/ServiceAutoScaler.rb index 8e29b79663..1dfdf3c41b 100644 --- a/src/flow/lib/ServiceAutoScaler.rb +++ b/src/flow/lib/ServiceAutoScaler.rb @@ -43,7 +43,8 @@ class ServiceAutoScaler monitoring = vm_pool.monitoring_xml(-2, 0) monitoring = XMLElement.new(XMLElement.build_xml(monitoring, 'MONITORING_DATA')) - monitoring = monitoring.to_hash['MONITORING_DATA']['MONITORING'] + monitoring = monitoring.to_hash['MONITORING_DATA'] + monitoring = monitoring['MONITORING'] if monitoring monitoring = [monitoring].flatten vm_pool.info_all_extended diff --git a/src/flow/lib/models/service.rb b/src/flow/lib/models/service.rb index 776c3ad4d2..2ade5b0829 100644 --- a/src/flow/lib/models/service.rb +++ b/src/flow/lib/models/service.rb @@ -141,7 +141,7 @@ module OpenNebula # @return true if the service can be undeployed, false otherwise def can_undeploy? if (transient_state? && state != Service::STATE['UNDEPLOYING']) || - state == Service::STATE['DONE'] || failed_state? + state == Service::STATE['DONE'] || failed_state? false else true @@ -166,6 +166,12 @@ module OpenNebula RECOVER_SCALE_STATES.include? STATE_STR[state] end + # Return true if the service is running + # @return true if the service is runnning, false otherwise + def running? + state_str == 'RUNNING' + end + # Returns the running_status_vm option # @return [true, false] true if the running_status_vm option is enabled def report_ready? @@ -323,6 +329,36 @@ module OpenNebula nil end + # Adds a role to the service + # + # @param template [Hash] Role information + # + # @return [OpenNebula::Role] New role + def add_role(template) + template['state'] ||= Role::STATE['PENDING'] + role = Role.new(template, self) + + if @roles[role.name] + return OpenNebula::Error.new("Role #{role.name} already exists") + end + + @roles[role.name] = role + @body['roles'] << template if @body && @body['roles'] + + role + end + + # Removes a role from the service + # + # @param name [String] Role name to delete + def remove_role(name) + @roles.delete(name) + + @body['roles'].delete_if do |role| + role['name'] == name + end + end + # Retrieves the information of the Service and all its Nodes. # # @return [nil, OpenNebula::Error] nil in case of success, Error @@ -514,8 +550,12 @@ module OpenNebula [true, nil] end - def deploy_networks - body = JSON.parse(self['TEMPLATE/BODY']) + def deploy_networks(deploy = true) + if deploy + body = JSON.parse(self['TEMPLATE/BODY']) + else + body = @body + end return if body['networks_values'].nil? @@ -531,7 +571,7 @@ module OpenNebula if OpenNebula.is_error?(rc) return rc end - end + end if deploy # Replace $attibute by the corresponding value resolve_attributes(body) diff --git a/src/flow/oneflow-server.rb b/src/flow/oneflow-server.rb index e21c50f873..a36c7a774c 100644 --- a/src/flow/oneflow-server.rb +++ b/src/flow/oneflow-server.rb @@ -403,6 +403,38 @@ post '/service/:id/scale' do status 204 end +post '/service/:id/role_action' do + action = JSON.parse(request.body.read)['action'] + opts = action['params'] + + case action['perform'] + when 'add_role' + begin + # Check that the JSON is valid + json_template = JSON.parse(opts['role']) + + # Check the schema of the new template + ServiceTemplate.validate_role(json_template) + rescue Validator::ParseException, JSON::ParserError => e + return internal_error(e.message, VALIDATION_EC) + end + + rc = lcm.add_role_action(@client, params[:id], json_template) + when 'remove_role' + rc = lcm.remove_role_action(@client, params[:id], opts['role']) + else + rc = OpenNebula::Error.new( + "Action #{action['perform']} not supported" + ) + end + + if OpenNebula.is_error?(rc) + return internal_error(rc.message, one_error_to_http(rc.errno)) + end + + status 204 +end + ############################################################################## # Service Pool ############################################################################## diff --git a/src/oca/ruby/opennebula/flow/service_template.rb b/src/oca/ruby/opennebula/flow/service_template.rb index 4c05f3d532..86cea714fb 100644 --- a/src/oca/ruby/opennebula/flow/service_template.rb +++ b/src/oca/ruby/opennebula/flow/service_template.rb @@ -451,6 +451,16 @@ module OpenNebula validate_values(template) end + def self.validate_role(template) + validator = Validator::Validator.new( + :default_values => true, + :delete_extra_properties => false, + :allow_extra_properties => true + ) + + validator.validate!(template, ROLE_SCHEMA) + end + def instantiate(merge_template) rc = nil