Calling Automation from the RESTful API

We can call any Automation Instance from the RESTful API, by issuing a POST call to /api/automation_requests, and enclosing a JSON-encoded parameter hash such as the following:

post_params = {
  :version => '1.1',
  :uri_parts => {
    :namespace => 'ACME/General',
    :class => 'Methods',
    :instance => 'HelloWorld'
  },
  :requester => {
    :auto_approve => true
  }
}.to_json

We can call the RESTful API from an external Ruby script by using the rest-client gem:

url = 'https://cloudforms_server'
query = '/api/automation_requests'
rest_return = RestClient::Request.execute(
                                method: :post,
                                url: url + query,
                                :user => username,
                                :password => password,
                                :headers => {:accept => :json},
                                :payload => post_params,
                                verify_ssl: false)
result = JSON.parse(rest_return)

The request ID is returned to us in the result from the initial call:

request_id = result['results'][0]['id']

We call poll this to check on status:

query = "/api/automation_requests/#{request_id}"
rest_return = RestClient::Request.execute(
                                method: :get, 
                                url: url + query, 
                                :user => username,
                                :password => password,
                                :headers => {:accept => :json},
                                verify_ssl: false)
result = JSON.parse(rest_return)
request_state = result['request_state']
until request_state == "finished"
  puts "Checking completion state..."
  rest_return = RestClient::Request.execute(
                                  method: :get,
                                  url: url + query,
                                  :user => username,
                                  :password => password,
                                  :headers => {:accept => :json},
                                  verify_ssl: false)
  result = JSON.parse(rest_return)
  request_state = result['request_state']
  sleep 3
end

Returning Results to the Caller

The request task's options hash is included in the return from the RestClient::Request call, and we can use this to our advantage, by using set_option to add return data in the form of key/value pairs to the options hash from our called Automation method.

For example from the called (Automate) method:

automation_request = $evm.root['automation_task'].automation_request
automation_request.set_option(:return, JSON.generate({:status => 'success', :return => some_data}))

From the calling (external) method:

puts "Results: #{result['options']['return'].inspect}"

Using this technique we can write our own pseudo-API calls for CloudForms to handle anything that the standard RESTful API doesn't support. We implement the "API" using a standard Automate method, call it using the RESTful automate call, and we can pass parameters to, and retrieve result back from the called method.

Authentication and auto_approve

When we make a RESTful call, we must authenticate using a valid username and password. This user must be an admin or equivalent however if we wish to specify :auto\_approve => true in our calling arguments (only admins can auto-approve Automation requests).

If we try making a RESTful call as a non-admin user, the Automation request will be blocked pending approval (as expected). If we want to submit an auto-approved automation request as a non-admin user, we would need to write our own approval workflow (see Automation Request Approval).

Zone Implications

When we submit an Automation Request via the API, by default the Automate Task is queued on the same appliance that the Web Service is running on. This will be de-queued to run by any appliance with the Automation Engine role set in the same zone. If we have separated out our UI/Web Service appliances into a separate zone, this may not necessarily be our desired behaviour.

We can add a parameter :miq_zone to the automation request to override this:

  :requester => {
    :auto_approve => true
  },
  :parameters => {
       :miq_zone => 'Zone Name'
  }

The behaviour of this parameter is as follows (from BZ #1162832):

  1. If the parameter is not passed the request should use the zone of the server that receives the request.
  2. If passed but empty (example 'parameters' => "miq_zone=",) the zone should be set to nil and any appliance can process the request.
  3. Passed a valid zone name parameter (example 'parameters' => "miq_zone=Test",) should process the work in the "Test" zone.
  4. Passing an invalid zone name should raise an error of unknown zone \ back to the caller.

Generic run_via_api Script Example

The following is a generic run_via_api script that can be used to call any Automation method, using arguments to pass server name, credentials, and URI parameters to the Instance to be called:

Usage: run_via_api.rb [options]
    -s, --server server              CloudForms server to connect to
    -u, --username username          Username to connect as
    -p, --password password          Password
    -d, --domain                     Domain
    -n, --namespace                  Namespace
    -c, --class                      Class
    -i, --instance                   Instance
    -P, --parameter <key,value>      Parameter (key => value pair) for the instance
    -h, --help
#!/usr/env ruby
#
# run_via_api
#
# Author:   Peter McGowan (pemcg@redhat.com)
#           Copyright 2015 Peter McGowan, Red Hat
#
# Revision History
#
require 'rest-client'
require 'json'
require 'optparse'

begin
  options = {
            :server      => nil,
            :username      => nil,
            :password      => nil,
            :domain      => nil,
            :namespace   => nil,
            :class          => nil,
            :instance      => nil,
            :parameters  => []
            }
  parser = OptionParser.new do|opts|
    opts.banner = "Usage: run_via_api.rb [options]"
    opts.on('-s', '--server server', 'CloudForms server to connect to') do |server|
      options[:server] = server
    end
    opts.on('-u', '--username username', 'Username to connect as') do |username|
      options[:username] = username
    end
    opts.on('-p', '--password password', 'Password') do |password|
      options[:password] = password
    end
    opts.on('-d', '--domain ', 'Domain') do |domain|
      options[:domain] = domain
    end
    opts.on('-n', '--namespace ', 'Namespace') do |namespace|
      options[:namespace] = namespace
    end
    opts.on('-c', '--class ', 'Class') do |klass|
      options[:class] = klass
    end
    opts.on('-i', '--instance ', 'Instance') do |instance|
      options[:instance] = instance
    end
    opts.on('-P', '--parameter <key,value>', Array, 'Parameter (key,value pair) for the instance') do |parameters|
      unless parameters.length == 2
        puts "Parameter argument must be key,value list"
        exit!
      end
      options[:parameters].push parameters
    end
    opts.on('-h', '--help', 'Displays Help') do
      puts opts
      exit!
    end
  end
  parser.parse!

  if options[:password] && options[:prompt]
    puts "Ambiguous: specify either --password or --prompt but not both"
    exit!
  end
  if options[:server].nil?
    server = "cloudforms_server"
  else
    server = options[:server]
  end
  if options[:username].nil?
    username = "rest_user"
  else
    username = options[:username]
  end
  if options[:password].nil?
    password = "secure"
  else
    password = options[:password]
  end
  if options[:domain].nil?
    puts "Domain must be specified"
    exit!
  end
  if options[:namespace].nil?
    puts "Namespace must be specified"
    exit!
  end
  if options[:class].nil?
    puts "Class must be specified"
    exit!
  end
  if options[:instance].nil?
    puts "Instance to run must be specified"
    exit!
  end

  url = "https://#{server}"
  #
  # Turn parameter list into hash
  #
  parameter_hash = {}
  options[:parameters].each do |parameter|
    parameter_hash[parameter[0]] = parameter[1]
  end

  message = "Running automation method "
  message += "#{options[:namespace]}/#{options[:class]}/#{options[:instance]}"
  message += " using parameters: "
  message += "#{parameter_hash.inspect}"
  puts message

  post_params = {
    :version => '1.1',
    :uri_parts => {
      :namespace => "#{options[:domain]}/#{options[:namespace]}",
      :class => options[:class],
      :instance => options[:instance]
    },
    :parameters => parameter_hash,
    :requester => {
      :auto_approve => true
    }
  }.to_json
  query = "/api/automation_requests"
  #
  # Issue the automation request
  #
  rest_return = RestClient::Request.execute(
                          method: :post,
                          url: url + query,
                          :user         => username,
                          :password     => password,
                          :headers     => {:accept => :json},
                          :payload     => post_params,
                          verify_ssl: false)
  result = JSON.parse(rest_return)
  #
  # get the request ID
  #
  request_id = result['results'][0]['id']
  query = "/api/automation_requests/#{request_id}"
  #
  # Now we have to poll the automate engine to see when the request_state has changed to 'finished'
  #
  rest_return = RestClient::Request.execute(
                          method: :get,
                          url: url + query,
                          :user         => username,
                          :password     => password,
                          :headers     => {:accept => :json},
                          verify_ssl: false)
  result = JSON.parse(rest_return)
  request_state = result['request_state']
  until request_state == "finished"
    puts "Checking completion state..."
    rest_return = RestClient::Request.execute(
                        method: :get, 
                        url: url + query,
                        :user         => username,
                        :password     => password,
                        :headers     => {:accept => :json},
                        verify_ssl: false)
    result = JSON.parse(rest_return)
    request_state = result['request_state']
    sleep 3
  end
  puts "Results: #{result['options']['return'].inspect}"
rescue => err
  puts "[#{err}]\n#{err.backtrace.join("\n")}"
  exit!
end

Edit the default values for server, username and password if required. Run the script as:

./run_via_api.rb -s 192.168.1.1 -u cfadmin -p password -d ACME -n General \
-c Methods -i AddNIC2VM -P vm_id,1000000000195 -P nic_name,nic1 -P nic_network,vlan_712

results matching ""

    No results matching ""