lib/service/api/insightvm.rb in avs-0.1.0 vs lib/service/api/insightvm.rb in avs-0.1.1

- old
+ new

@@ -1,159 +1,189 @@ -#!/usr/bin/env ruby # frozen_string_literal: true -require 'net/http' +require 'typhoeus' require 'json' -require 'uri' require 'base64' -require 'openssl' require_relative '../../domain/api' +# InsightVMApi class provides an interface for interacting with the InsightVM API. +# It handles request building, and response parsing for various API endpoints. +# +# @attr_reader [String] base_url The base URL for the InsightVM API +# @attr_reader [String] base_auth The Basic Authentication string used for requests +# +# @example Initializing the API client +# api = InsightVMApi.new( +# base_url: 'https://your-insightvm-instance.com', +# username: 'your_username', +# password: 'your_password' +# ) +# +# @example Fetching all sites +# api.fetch_all('/sites') do |site| +# puts site['name'] +# end +# +# @example Getting a specific site +# site = api.get('/sites/1') +# puts site.inspect class InsightVMApi - attr_reader :http, :base_auth, :base_url + attr_reader :base_url, :base_auth - # Fetchs all resources + # Initializes a new InsightVMApi instance. # - # @param [String] - # @param [Hash] optional parameters: page, size, type, name, sort + # @param base_url [String] The base URL for the InsightVM API + # @param username [String] The username for API authentication + # @param password [String] The password for API authentication + def initialize(base_url:, username:, password:) + @base_url = base_url + @base_auth = ['Basic', Base64.strict_encode64("#{username}:#{password}")].join(' ') + @options = { + headers: { + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Authorization' => @base_auth + } + } + @cached_tags = nil + @shared_credentials = nil + end + + # Fetches all resources from a paginated endpoint. + # + # @param endpoint [String] The API endpoint to fetch resources from + # @param opts [Hash] Additional options for the request (e.g., filters) + # @yield [resource] Gives each resource to the block def fetch_all(endpoint, opts = {}, &block) params = { page: 0, - size: 100, - read_timeout: 5 - }.merge opts + size: 100 + }.merge(opts) + loop do - full_url = @base_url.dup - full_url.path += endpoint - full_url.query = URI.encode_www_form( - params - ) - request = Net::HTTP::Get.new(full_url) - request['Authorization'] = @base_auth - @http.read_timeout = params[:read_timeout] - response = @http.request(request) - unless response.is_a?(Net::HTTPSuccess) - puts "Error with code #{response.code}" - break - end + response = get("#{endpoint}?#{URI.encode_www_form(params)}") + break if response.is_a?(Hash) && response[:error] - json_response = JSON.parse(response.body) - resources = json_response['resources'] + resources = response['resources'] break if resources.nil? - resources.each(&block) # equivalent to resources.each {|resource| yield resource} + resources.each(&block) - # Check if this is the last page - current_page = json_response['page']&.[]('number') || 0 - pages = json_response['page']&.[]('totalPages') || 0 - break if params[:page] >= pages - break if current_page + 1 >= pages + current_page = response['page']&.[]('number') || 0 + pages = response['page']&.[]('totalPages') || 0 + break if params[:page] >= pages || current_page + 1 >= pages params[:page] += 1 end - rescue Net::ReadTimeout - opts[:read_timeout] = 2 * params[:read_timeout] || 30 - raise 'Fail after multiple attempts' if opts[:read_timeout] > 300 + end - puts "Increase the timeout to #{opts[:read_timeout]}" - fetch_all(endpoint, opts, &block) + # Performs a GET request to the specified path. + # + # @param path [String] The API path to send the GET request to + # @param params [Hash] Query parameters to include in the request + # @return [Hash] The parsed JSON response or an error hash + def get(path, params = {}) + run_request(:get, path, params:) end - def initialize(base_url, username, password) - @base_url = URI(base_url) - @http = Net::HTTP.new(@base_url.host, @base_url.port) - @http.use_ssl = true - @http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Reminder: Adjust for production use - @base_auth = ['Basic', Base64.strict_encode64("#{username}:#{password}")].join(' ') + # Performs a POST request to the specified endpoint. + # + # @param endpoint [String] The API endpoint to send the POST request to + # @param body [Hash] The request body to be sent as JSON + # @return [Hash] The parsed JSON response or an error hash + def post(endpoint, body) + run_request(:post, endpoint, body: body.to_json) end - private + # Performs a DELETE request to the specified endpoint. + # + # @param endpoint [String] The base API endpoint + # @param id [String, Integer] The ID of the resource to delete + # @param attempts [Integer] The number of delete attempts (for internal use) + def delete(endpoint, id, _attempts = 0) + response = run_request(:delete, "#{endpoint}/#{id}") - # return the response.body.to_json if Success - # else response,message and response.status if failure - def post(endpoint, params) - # Construct the request - uri = URI("#{@base_url}#{endpoint}") - request = Net::HTTP::Post.new(uri) - request['Content-Type'] = 'application/json' - request['Authorization'] = @base_auth - request.body = params.to_json - # Send the request - response = @http.request(request) - - if response.is_a?(Net::HTTPSuccess) - # puts 'Success!' - # You can parse the response body if needed - JSON.parse(response.body) + if response.is_a?(Hash) && response[:error] + puts "Error deleting #{endpoint}/#{id}: #{response[:error]}" else - puts "Error with status code: #{response.code}, Response body: #{response.body}" - nil - end - end - - def delete(endpoint, id, attempts = 0) - uri = URI("#{@base_url}#{endpoint}/#{id}") - request = Net::HTTP::Delete.new(uri) - request['Content-Type'] = 'application/json' - request['Authorization'] = @base_auth - # Send the requesbody - response = @http.request(request) - case response - when Net::HTTPSuccess puts "#{endpoint}/#{id} deleted successfully." - when Net::ReadTimeout - delete(endpoint, id, attempts + 1) if attempts < 5 - else - puts "Error deleting #{endpoint}/#{id} Status code: #{response.code}, Response body: #{response.body}" end end + # Performs a PATCH request to the specified endpoint. + # + # @param endpoint [String] The API endpoint to send the PATCH request to + # @param body [Hash] The request body to be sent as JSON def patch(endpoint, body) - # Construct the request - uri = URI("#{@base_url}#{endpoint}") - request = Net::HTTP::Patch.new(uri) - request['Content-Type'] = 'application/json' - request['Authorization'] = @base_auth - request.body = JSON.generate(body) - # Send the requesbody - response = @http.request(request) + response = run_request(:patch, endpoint, body: body.to_json) + return unless response.is_a?(Hash) && response[:error] - return if response.is_a?(Net::HTTPSuccess) - - puts "Error PATCH #{endpoint}. Status code: #{response.code}\n Response body: #{response.body}" + puts "Error PATCH #{endpoint}: #{response[:error]}" end + # Performs a PUT request to the specified endpoint. + # + # @param endpoint [String] The API endpoint to send the PUT request to + # @param body [Hash] The request body to be sent as JSON def put(endpoint, body) - # Construct the request - uri = URI("#{@base_url}#{endpoint}") - request = Net::HTTP::Put.new(uri) - request['Content-Type'] = 'application/json' - request['Authorization'] = @base_auth - request.body = JSON.generate(body) + json = JSON.generate(body) + response = run_request(:put, endpoint, body: json) + return response unless response.is_a?(Hash) && response[:error] - # Send the requesbody - response = @http.request(request) - - return if response.is_a?(Net::HTTPSuccess) - - puts "Error PUT #{endpoint}. Status code: #{response.code}, Response body: #{response.body}" + puts "Error PUT #{endpoint}: #{response[:error]}" end + # Fetches data from an endpoint with retry logic. + # + # @param endpoint [String] The API endpoint to fetch data from + # @param attempts [Integer] The number of fetch attempts (for internal use) + # @yield [response] Gives the successful response to the block + # @raise [RuntimeError] If the maximum number of retries is exceeded def fetch(endpoint, attempts = 0) max_retries = 5 - uri = URI("#{@base_url}#{endpoint}") - request = Net::HTTP::Get.new(uri) - request['Authorization'] = @base_auth - response = @http.request(request) - raise "HTTP Error #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess) + response = get(endpoint) - yield JSON.parse(response.body) - rescue Net::ReadTimeout => e - puts "Attempts #{attempts}" - raise "Network error after #{max_retries} #{e.message}" unless attempts < max_retries + if response.is_a?(Hash) && response[:error] + raise "Network error after #{max_retries} attempts: #{response[:error]}" unless attempts < max_retries - sleep 2**attempts - fetch(endpoint, attempts + 1) - else - # TODO + sleep 2**attempts + fetch(endpoint, attempts + 1) + + else + yield response + end + end + + private + + # Runs an HTTP request using Typhoeus. + # + # @param method [Symbol] The HTTP method (:get, :post, etc.) + # @param path [String] The API path for the request + # @param options [Hash] Additional options for the request + # @return [Hash] The parsed JSON response or an error hash + def run_request(method, path, options = {}) + request = Typhoeus::Request.new( + "#{@base_url}#{path}", + method:, + headers: @options[:headers], + **options + ) + + response = request.run + + if response.success? + JSON.parse(response.body) + elsif response.timed_out? + { error: 'Request timed out' } + elsif response.code.zero? + { error: response.return_message } + else + { error: "HTTP request failed: #{response.code}" } + end + end + + def confirm_action(prompt) + print "#{prompt} (y/n): " + gets.chomp.downcase == 'y' end end