theforeman/foreman

View on GitHub
app/services/proxy_api/external_ipam.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'uri'

module ProxyAPI
  class ExternalIPAM < ProxyAPI::Resource
    def initialize(args)
      @url = args[:url] + "/ipam"
      super args
    end

    # Queries External IPAM and retrieves the next available IP address for the given subnet.
    # The IP returned is NOT reserved in External IPAM database. It is however written to an in-memory,
    # thread safe cache, on the proxy side, with the subnet CIDR/mac address as the key(see
    # IP cache structure below). This will prevent the same IP being suggested for different mac addresses,
    # and will handle race condition scenarios where multiple hosts are being provisioned simultaneously.
    #
    # Groups of subnets are cached under the External IPAM Group name. "IPAM Group" in the example below is
    # the group name. For subnets that have an External IPAM group specified(e.g. "IPAM Group"),
    # the IP's/mac are cached under the "IPAM Group" key. If External IPAM group is not defined, then they
    # are cached under a "" key.
    #
    # The IP address is only actually reserved in the External IPAM database upon successful host
    # and/or interface creation.
    #
    # In-memory IP cache structure(IPv4 and IPv6):
    # ===============================
    # {
    #   "": {
    #     "100.55.55.0/24":{
    #       "00:0a:95:9d:68:10": {"ip": "100.55.55.1", "timestamp": "2019-09-17 12:03:43 -D400"}
    #     },
    #   },
    #   "IPAM Group": {
    #     "123.11.33.0/24":{
    #       "00:0a:95:9d:68:33": {"ip": "123.11.33.1", "timestamp": "2019-09-17 12:04:43 -0400"},
    #       "00:0a:95:9d:68:34": {"ip": "123.11.33.2", "timestamp": "2019-09-17 12:05:48 -0400"},
    #       "00:0a:95:9d:68:35": {"ip": "123.11.33.3", "timestamp:: "2019-09-17 12:06:50 -0400"}
    #     }
    #   },
    #   "Another IPAM Group": {
    #     "185.45.39.0/24":{
    #       "00:0a:95:9d:68:55": {"ip": "185.45.39.1", "timestamp": "2019-09-17 12:04:43 -0400"},
    #       "00:0a:95:9d:68:56": {"ip": "185.45.39.2", "timestamp": "2019-09-17 12:05:48 -0400"}
    #     }
    #   }
    # }
    #
    # Params: 1. subnet:           The IPv4 or IPv6 subnet CIDR. (Examples: IPv4 - "100.10.10.0/24", IPv6 - "2001:db8:abcd:12::/124")
    #         2. mac:              The mac address of the interface obtaining the IP address for
    #         3. group(optional):  The group in External IPAM that the subnet belongs to (e.g. 'Section' in
    #                              in phpIPAM, or 'VRF' in Netbox)
    #
    # Responses:
    #   Responses if success:
    #     IPv4: {"data": "100.55.55.3"}
    #     IPv6: {"data": "2001:db8:abcd:12::1"}
    #   Response if missing required params:
    #     {"error": ["A 'cidr' parameter for the subnet must be provided(e.g. 100.10.10.0/24)","A 'mac' address must be provided(e.g. 00:0a:95:9d:68:10)"]}
    #   Response if subnet does not exist:
    #     {"error": "Subnet not found in External IPAM."}
    #   Response if there are no free addresses:
    #     {"error": "No free addresses found"}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM server"}
    def next_ip(subnet, mac, group = "")
      raise "subnet cannot be nil" if subnet.nil?
      response = parse get("/subnet/#{subnet}/next_ip", query: { mac: mac, group: group })
      raise(response['error']) if response['error'].present?
      response['data']
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to retrieve the next available IP for subnet %{subnet} External IPAM."), subnet: subnet)
    end

    # Adds an IP address to the specified subnet in External IPAM. This will reserve the IP in the
    # External IPAM database. If group is specified, the IP will be added to the subnet within the
    # given group.
    #
    # Params: 1. ip:               IP address to be added
    #         2. subnet:           The IPv4 or IPv6 subnet CIDR. (Examples: IPv4 - "100.10.10.0/24",
    #                              IPv6 - "2001:db8:abcd:12::/124")
    #         3. group(optional):  The name of the External IPAM group containing the subnet.
    #
    # Returns: true if IP was added successfully to External IPAM, otherwise false
    #
    # Responses:"
    #   Response if success:
    #     201
    #   Response if IP already reserved:
    #     {"error": "IP address already exists"}
    #   Response if subnet error:
    #     {"error": "Subnet not found in External IPAM"}
    #   Response if missing required params:
    #     {"error": ["A 'cidr' parameter for the subnet must be provided(e.g. 100.10.10.0/24)","Missing 'ip' parameter. An IPv4 address must be provided(e.g. 100.10.10.22)"]}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM server"}
    def add_ip_to_subnet(ip, subnet, group = "")
      raise "subnet cannot be nil" if subnet.nil?
      raise "ip cannot be nil" if ip.nil?
      response = parse post({}, "/subnet/#{subnet}/#{ip}?group=#{CGI.escape(group.to_s)}")
      raise(response['error']) if response.is_a?(Hash) && response['error'].present?
      response
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to add IP %{ip} to the subnet %{subnet} in External IPAM."), ip: ip, subnet: subnet)
    end

    # Get a list of groups from External IPAM. A group is a logical grouping of subnets/ips.
    #
    # Params:  None
    #
    # Returns: An array of groups on success, or a hash with a "error" key
    #          containing error on failure.
    #
    # Responses:
    #   Response if success:
    #     {[
    #       {name":"Test Group","description": "A Test Group"},
    #       {name":"Awesome Group","description": "A totally awesome Group"}
    #     ]}
    #   Response if no groups exist:
    #     []
    #   Response if groups are not supported:
    #     {"error": "Groups are not supported"}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM server"}
    def get_groups
      response = parse get("/groups")
      raise(response['error']) if response.is_a?(Hash) && response['error'].present?
      response
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to obtain groups from External IPAM."))
    end

    # Get a group from External IPAM. A group is a logical grouping of subnets/ips (e.g. 'Section' in
    # in phpIPAM, or 'VRF' in Netbox)
    #
    # Params:   1. group:     The name of the External IPAM group
    #
    # Responses:
    #   Response if success:
    #     {"name":"Awesome Group", "description": "Awesome Group"}
    #   Response if group doesn't exist:
    #     {"error": "Group not found in External IPAM"}
    #   Response if groups are not supported:
    #     {"error": "Groups are not supported"}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM server"}
    def get_group(group)
      raise "group must be provided" if group.blank?
      response = parse get("/groups/#{CGI.escape(group)}")
      raise(response['error']) if response['error'].present?
      response
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to obtain group %{group} from External IPAM."), group: group)
    end

    # Get a list of subnets for the given External IPAM group.
    #
    # Params:  1. group:     The name of the External IPAM group containing the subnet.
    #
    # Responses:
    #   Response if success:
    #   {[
    #     {subnet":"100.10.10.0","mask":"24","description":"Test Subnet 1"},
    #     {subnet":"100.20.20.0","mask":"24","description":"Test Subnet 2"}
    #   ]}
    #   Response if groups not supported:
    #     {"error": "Groups are not supported"}
    #   Response if no subnets exist in group.
    #     {"error": "Subnet not found in External IPAM"}
    #   Response if group not found:
    #     {"error": "Group not found in External IPAM"}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM"}
    def get_subnets_by_group(group)
      raise "group must be provided" if group.blank?
      response = parse get("/groups/#{CGI.escape(group)}/subnets")
      raise(response['error']) if response.is_a?(Hash) && response['error'].present?
      response
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to obtain subnets in group %{group} from External IPAM."), group: group)
    end

    # Returns an array of subnets from External IPAM matching the given subnet.
    #
    # Params:  1. subnet:           The IPv4 or IPv6 subnet CIDR. (Examples: IPv4 - "100.10.10.0/24",
    #                               IPv6 - "2001:db8:abcd:12::/124")
    #          2. group(optional):  The name of the External IPAM group containing the subnet.
    #
    # Responses:
    #   Response if subnet(s) exists:
    #     {"subnet": "44.44.44.0", "description": "", "mask":"29"}
    #   Response if subnet not exists:
    #     {"error": "No subnets found"}
    #   Response if groups not supported(only checked if a group is specified):
    #     {"error": "Groups are not supported"}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM server"}
    def get_subnet(subnet, group = "")
      raise "subnet cannot be nil" if subnet.nil?
      response = parse get("/subnet/#{subnet}", query: {group: group})
      raise(response['error']) if response['error'].present?
      response
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to obtain subnet %{subnet} from External IPAM."), subnet: subnet)
    end

    # Checks whether an IP address has been reserved in External IPAM.
    #
    # Inputs: 1. ip:               IP address to be checked
    #         2. subnet:           The IPv4 or IPv6 subnet CIDR. (Examples: IPv4 - "100.10.10.0/24",
    #                              IPv6 - "2001:db8:abcd:12::/124")
    #         3. group(optional):  The name of the External IPAM group containing the subnet to pull IP from
    #
    # Responses:
    #   Response if IP is reserved:
    #     {"result": true}
    #   Response if IP address is available
    #     {"result": false}
    #   Response if missing required parameters:
    #     {"error": ["A 'cidr' parameter for the subnet must be provided(e.g. 100.10.10.0/24)", "Missing 'ip' parameter. An IPv4 address must be provided(e.g. 100.10.10.22)"]}
    #   Response if subnet not exists:
    #     {"error": "Subnet not found in External IPAM"}
    #   Response if groups not supported(only checked if a group is specified):
    #     {"error": "Groups are not supported"}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM server"}
    def ip_exists(ip, subnet, group = "")
      raise "subnet cannot be nil" if subnet.nil?
      raise "ip cannot be nil" if ip.nil?
      response = parse get("/subnet/#{subnet}/#{ip}", query: { group: group })
      raise(response['error']) if response.is_a?(Hash) && response['error'].present?
      if response.is_a?(Hash)
        raise(response['error']) if response['error'].present?
        response['result']
      else # Foreman Proxy < 3.3 support
        response
      end
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to obtain IP address for subnet %{subnet} in External IPAM."), subnet: subnet)
    end

    # Deletes an IP address from a given subnet in External IPAM.
    #
    # Inputs: 1. ip:               IP address to be freed up
    #         2. subnet:           The IPv4 or IPv6 subnet CIDR. (Examples: IPv4 - "100.10.10.0/24",
    #                              IPv6 - "2001:db8:abcd:12::/124")
    #         3. group(optional):  The name of the External IPAM group containing the subnet.
    #
    # Responses:
    #   Response if success:
    #     200
    #   Response if subnet error:
    #     {"error": "The specified subnet does not exist in External IPAM."}
    #   Response if IP already deleted:
    #     {"error": "Unable to delete IP from External IPAM"}
    #   Response if groups not supported(only checked if a group is specified):
    #     {"error": "Groups are not supported"}
    #   Response if can't connect to External IPAM server
    #     {"error": "Unable to connect to External IPAM server"}
    def delete_ip_from_subnet(ip, subnet, group = "")
      raise "subnet cannot be nil" if subnet.nil?
      raise "ip cannot be nil" if ip.nil?
      response = parse delete("/subnet/#{subnet}/#{ip}?group=#{CGI.escape(group.to_s)}")
      raise(response['error']) if response.is_a?(Hash) && response['error'].present?
      response
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to delete IP %{ip} from the subnet %{subnet} in External IPAM."), ip: ip, subnet: subnet)
    end
  end
end