arangamani/jenkins_api_client

View on GitHub
lib/jenkins_api_client/node.rb

Summary

Maintainability
A
2 hrs
Test Coverage
#
# Copyright (c) 2012-2013 Kannan Manickam <arangamani.kannan@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

require 'jenkins_api_client/urihelper'

module JenkinsApi
  class Client
    # This class communicates with Jenkins "/computer" API to obtain details
    # about nodes or slaves connected to the Jenkins.
    #
    class Node
      include JenkinsApi::UriHelper

      # General attributes of a node.
      # This allows the following methods to be called on this node object.
      # These methods are defined using define_method and are prefixed
      # with get_.
      #
      #   def get_busyExecutors
      #   def get_displayName
      #   def get_totalExecutors
      #
      GENERAL_ATTRIBUTES = [
        "busyExecutors",
        "displayName",
        "totalExecutors"
      ].freeze

      # Properties of a node.
      # The following methods are defined to be called on the node object
      # and are prefixed with is_ and end with ? as they return true or false.
      #
      #   def is_idle?(node_name)
      #   def is_jnlpAgent?(node_name)
      #   def is_launchSupported?(node_name)
      #   def is_manualLaunchAllowed?(node_name)
      #   def is_offline?(node_name)
      #   def is_temporarilyOffline?(node_name)
      #
      NODE_PROPERTIES = [
        "idle",
        "jnlpAgent",
        "launchSupported",
        "manualLaunchAllowed",
        "offline",
        "temporarilyOffline"
      ].freeze

      # Node specific attributes.
      # The following methods are defined using define_method.
      # These methods are prefixed with get_node_.
      #
      #   def get_node_numExecutors(node_name)
      #   def get_node_icon(node_name)
      #   def get_node_displayName(node_name)
      #   def get_node_loadStatistics(node_name)
      #   def get_node_monitorData(node_name)
      #   def get_node_offlineCause(node_name)
      #   def get_node_oneOffExecutors(node_name)
      #
      NODE_ATTRIBUTES = [
        "numExecutors",
        "icon",
        "displayName",
        "loadStatistics",
        "monitorData",
        "offlineCause",
        "oneOffExecutors"
      ].freeze

      # Initializes a new node object
      #
      # @param client [Client] the client object
      #
      # @return [Node] the node object
      #
      def initialize(client)
        @client = client
        @logger = @client.logger
      end

      # Gives the string representation of the Object
      #
      def to_s
        "#<JenkinsApi::Client::Node>"
      end

      # Creates a new node with the specified parameters
      #
      # @param [Hash] params parameters for creating a dumb slave
      #  * +:name+ name of the slave
      #  * +:description+ description of the new slave
      #  * +:executors+ number of executors
      #  * +:remote_fs+ Remote FS root
      #  * +:labels+ comma separated list of labels
      #  * +:mode+ mode of the slave: normal, exclusive
      #  * +:slave_host+ Hostname/IP of the slave
      #  * +:slave_port+ Slave port
      #  * +:private_key_file+ Private key file of master
      #  * +:credentials_id+ Id for credential in Jenkins
      #
      # @example Create a Dumb Slave
      #   create_dumb_slave(
      #     :name => "slave1",
      #     :slave_host => "10.10.10.10",
      #     :private_key_file => "/root/.ssh/id_rsa",
      #     :executors => 10,
      #     :labels => "slave, ruby"
      #   )
      #
      def create_dumb_slave(params)
        unless params[:name] && params[:slave_host] && params[:private_key_file]
          raise ArgumentError, "Name, slave host, and private key file are" +
            " required for creating a slave."
        end

        @logger.info "Creating a dumb slave '#{params[:name]}'"
        @logger.debug "Creating a dumb slave with params: #{params.inspect}"
        default_params = {
          :description => "Automatically created through jenkins_api_client",
          :executors => 2,
          :remote_fs => "/var/jenkins",
          :labels => params[:name],
          :slave_port => 22,
          :mode => "normal",
          :private_key_file => "",
          :credentials_id => ""
        }

        params = default_params.merge(params)
        labels = params[:labels].split(/\s*,\s*/).join(" ")
        mode = params[:mode].upcase

        post_params = {
          "name" => params[:name],
          "type" => "hudson.slaves.DumbSlave$DescriptorImpl",
          "json" => {
            "name" => params[:name],
            "nodeDescription" => params[:description],
            "numExecutors" => params[:executors],
            "remoteFS" => params[:remote_fs],
            "labelString" => labels,
            "mode" => mode,
            "type" => "hudson.slaves.DumbSlave$DescriptorImpl",
            "retentionStrategy" => {
              "stapler-class" => "hudson.slaves.RetentionStrategy$Always"
            },
            "nodeProperties" => {
              "stapler-class-bag" => "true"
            },
            "launcher" => {
              "stapler-class" => "hudson.plugins.sshslaves.SSHLauncher",
              "host" => params[:slave_host],
              "port" => params[:slave_port],
              "username" => params[:slave_user],
              "privatekey" => params[:private_key_file],
              "credentialsId" => params[:credentials_id]
            }
          }.to_json
        }
        @logger.debug "Modified params posted to create slave:" +
          " #{post_params.inspect}"
        @client.api_post_request("/computer/doCreateItem", post_params)
      end

      def create_dump_slave(params)
        @logger.warn '[DEPRECATED] Please use create_dumb_slave instead.'
        create_dumb_slave(params)
      end

      # Deletes the specified node
      #
      # @param [String] node_name Name of the node to delete
      #
      def delete(node_name)
        @logger.info "Deleting node '#{node_name}'"
        if list.include?(node_name)
          @client.api_post_request("/computer/#{path_encode node_name}/doDelete")
        else
          raise "The specified node '#{node_name}' doesn't exist in Jenkins."
        end
      end

      # Deletes all slaves from Jenkins. The master will be the only node alive
      # after the exection of this call.
      #
      # @note This method will remove all slaves from Jenkins. Please use with
      #       caution.
      #
      def delete_all!
        @logger.info "Deleting all nodes (except master) from jenkins"
        list.each { |node| delete(node) unless node == "master" }
      end

      # This method returns two lists 1) nodes online 2) nodes offline
      #
      # @param [String] filter a regex to filter node names
      # @param [Bool] ignorecase whether to be case sensitive or not
      #
      def online_offline_lists(filter = nil, ignorecase = true)
        @logger.info "Obtaining nodes from jenkins matching filter '#{filter}'"
        offline_node_names = []
        online_node_names = []
        response_json = @client.api_get_request("/computer")
        response_json["computer"].each do |computer|
            if computer["displayName"] =~ /#{filter}/i
              if computer["offline"] == true
                offline_node_names << computer["displayName"]
              else
                online_node_names << computer["displayName"]
              end
            end
        end
        return online_node_names, offline_node_names
      end

      # This method lists all nodes
      #
      # @param [String] filter a regex to filter node names
      # @param [Bool] ignorecase whether to be case sensitive or not
      #
      def list(filter = nil, ignorecase = true, slaveonly = false)
        @logger.info "Obtaining nodes from jenkins matching filter '#{filter}'"
        node_names = []
        response_json = @client.api_get_request("/computer")
        response_json["computer"].each do |computer|
            if computer["displayName"] =~ /#{filter}/i
              unless slaveonly && computer["displayName"] == "master"
                node_names << computer["displayName"]
              end
            end
        end
        node_names
      end

      # Identifies the index of a node name in the array node nodes
      #
      # @param [String] node_name name of the node
      #
      def index(node_name)
        response_json = @client.api_get_request("/computer")
        response_json["computer"].each_with_index do |computer, index|
          return index if computer["displayName"] == node_name
        end
      end

      # Defines methods for general node attributes.
      #
      GENERAL_ATTRIBUTES.each do |meth_suffix|
        define_method("get_#{meth_suffix}") do
          @logger.info "Obtaining '#{meth_suffix}' attribute from jenkins"
          response_json = @client.api_get_request("/computer", "tree=#{path_encode meth_suffix}[*[*[*]]]")
          response_json["#{meth_suffix}"]
        end
      end

      # Defines methods for node properties.
      #
      NODE_PROPERTIES.each do |meth_suffix|
        define_method("is_#{meth_suffix}?") do |node_name|
          @logger.info "Obtaining '#{meth_suffix}' property of '#{node_name}'"
          node_name = "(master)" if node_name == "master"
          response_json = @client.api_get_request("/computer/#{path_encode node_name}", "tree=#{path_encode meth_suffix}")
          resp = response_json["#{meth_suffix}"].to_s
          resp =~ /False/i ? false : true
        end
      end

      # Defines methods for node specific attributes.
      NODE_ATTRIBUTES.each do |meth_suffix|
        define_method("get_node_#{meth_suffix}") do |node_name|
          @logger.info "Obtaining '#{meth_suffix}' attribute of '#{node_name}'"
          node_name = "(master)" if node_name == "master"
          response_json = @client.api_get_request("/computer/#{path_encode node_name}", "tree=#{path_encode meth_suffix}[*[*[*]]]")
          response_json["#{meth_suffix}"]
        end
      end

      # Changes the mode of a slave node in Jenkins
      #
      # @param [String] node_name name of the node to change mode for
      # @param [String] mode mode to change to
      #
      def change_mode(node_name, mode)
        @logger.info "Changing the mode of '#{node_name}' to '#{mode}'"
        mode = mode.upcase
        xml = get_config(node_name)
        n_xml = Nokogiri::XML(xml)
        desc = n_xml.xpath("//mode").first
        desc.content = "#{mode.upcase}"
        xml_modified = n_xml.to_xml
        post_config(node_name, xml_modified)
      end

      # Obtains the configuration of node from Jenkins server
      #
      # @param [String] node_name name of the node
      #
      def get_config(node_name)
        @logger.info "Obtaining the config.xml of node '#{node_name}'"
        node_name = "(master)" if node_name == "master"
        @client.get_config("/computer/#{path_encode node_name}")
      end

      # Posts the given config.xml to the Jenkins node
      #
      # @param [String] node_name name of the node
      # @param [String] xml Config.xml of the node
      #
      def post_config(node_name, xml)
        @logger.info "Posting the config.xml of node '#{node_name}'"
        node_name = "(master)" if node_name == "master"
        @client.post_config("/computer/#{path_encode node_name}/config.xml", xml)
      end

      # Toggles the temporarily offline state of the Jenkins node
      #
      # @param [String] node_name name of the node
      # @param [String] reason Offline reason why the node is offline
      #
      def toggle_temporarilyOffline(node_name, reason="")
        @logger.info "Toggling the temporarily offline status of of node '#{node_name}' with reason '#{reason}'"
        node_name = "(master)" if node_name == "master"
        previous_state = is_temporarilyOffline?(node_name)
        @client.api_post_request("/computer/#{path_encode node_name}/toggleOffline?offlineMessage=#{path_encode reason}")
        new_state = is_temporarilyOffline?(node_name)
        if new_state == previous_state
          raise "The specified node '#{node_name}' was unable to change offline state."
        end
        new_state
      end
    end
  end
end