smart_proxy_dhcp_kea_api 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60b69b716ae58881effcacb442e0357e0d92a2b30b4c5be29e24439e07c9e047
4
- data.tar.gz: c698f7bdb99f3965eb1f7eb02afe908559b29c2145e3c8870b87038c3552c101
3
+ metadata.gz: 42b1eec7524d54aa43e9f202c85cb88a92fbfcdf3dbf91a42df43e20d000ac1c
4
+ data.tar.gz: 66ae862538c0fc90345eb7a32e487ef63ef536c37cac572a07adfff9d39bb978
5
5
  SHA512:
6
- metadata.gz: bd83ca3c705b98af40b57b11b9d9d10386f43277a07884d537eda07f28ef433e3939aeb5d7ab90d351e0f1f429560ea642a9a8ce0b4a98eb60867cb037c6b1e5
7
- data.tar.gz: e965386db272058aa1504d089cc69e3e39ada4610a2fe96ff4e2f1a2c5ee7632577430f781e0bffc16239f7243d94f5acb536ef0e6ebcefd0558541b080a018d
6
+ metadata.gz: 5cf2dfcf2d1b5b52efa98f274b9abd4d777b5b62182f048401040a01baf3f7c32e70cf38921d7e596090a993b684ac19748257e8e2d67d9f35479ef0cc6f823c
7
+ data.tar.gz: 1c54018b7438505c0d8de97337a0363435498c29682e93d7699ffacb2933e7f4af57037c52bbe180b6273066b2c48e6712b06bf3484a83902715e585931d656b
data/lib/.DS_Store ADDED
Binary file
@@ -23,8 +23,12 @@ module Proxy
23
23
 
24
24
  # Defines the default settings for this provider. These values are used if
25
25
  # they are not explicitly overridden in a user's settings file
26
+ # The `kea_api_username` and `kea_api_password` can be used to enable
27
+ # HTTP Basic Authentication if required by the Kea control agent.
26
28
  # (e.g. `/etc/foreman-proxy/settings.d/dhcp_kea_api.yml`).
27
29
  default_settings kea_api_url: 'http://127.0.0.1:8000/',
30
+ kea_api_username: nil,
31
+ kea_api_password: nil,
28
32
  blacklist_duration_minutes: 5,
29
33
  open_timeout: 5,
30
34
  read_timeout: 10
@@ -31,7 +31,7 @@ module Proxy
31
31
  # @param reservations_by_ip [Proxy::MemoryStore] A memory store for reservations, passed to the parent class.
32
32
  # @param reservations_by_mac [Proxy::MemoryStore] A memory store for reservations, passed to the parent class.
33
33
  # @param reservations_by_name [Proxy::MemoryStore] A memory store for reservations, passed to the parent class.
34
-
34
+ # @return [void]
35
35
  # rubocop:disable Metrics/ParameterLists
36
36
  def initialize(client, leases_by_ip, leases_by_mac, reservations_by_ip, reservations_by_mac, reservations_by_name)
37
37
  @client = client
@@ -47,6 +47,8 @@ module Proxy
47
47
  #
48
48
  # @return [true] on success.
49
49
  # @raise [Proxy::DHCP::Error] if any part of the loading process fails.
50
+ # @see #load_subnets_and_reservations_from_kea
51
+ # @see #load_leases_from_kea
50
52
  # rubocop:disable Naming/PredicateMethod
51
53
  def load!
52
54
  subnets.clear
@@ -60,6 +62,9 @@ module Proxy
60
62
 
61
63
  # Fetches all subnets and their associated reservations from the Kea API.
62
64
  # This single `config-get` call is the most efficient way to get all static configuration.
65
+ # @return [void]
66
+ # @raise [Proxy::DHCP::Error] if the API call fails.
67
+ # @raise [IPAddr::InvalidAddressError] if a subnet address from Kea is invalid.
63
68
  # @see Proxy::DHCP::KeaApi::Client#post_command
64
69
  def load_subnets_and_reservations_from_kea
65
70
  config = @client.post_command('dhcp4', 'config-get')
@@ -80,6 +85,8 @@ module Proxy
80
85
  end
81
86
 
82
87
  # Fetches all active leases from the Kea API for the subnets currently in the cache.
88
+ # @return [void]
89
+ # @raise [Proxy::DHCP::Error] if the API call fails.
83
90
  # @see Proxy::DHCP::KeaApi::Client#post_command
84
91
  def load_leases_from_kea
85
92
  # This guard is necessary because the `lease4-get-all` command requires
@@ -114,6 +121,8 @@ module Proxy
114
121
  # Foreman Subnet and Reservation objects, and adds them to the cache.
115
122
  #
116
123
  # @param subnet_data [Hash] The hash representing a single subnet from Kea's `config-get` response.
124
+ # @return [void]
125
+ # @raise [IPAddr::InvalidAddressError] if the subnet string is not a valid IP address.
117
126
  def process_subnet(subnet_data)
118
127
  ip_object = IPAddr.new(subnet_data['subnet'])
119
128
  subnet_addr = ip_object.to_s
@@ -142,6 +151,7 @@ module Proxy
142
151
  #
143
152
  # @param subnet_data [Hash] The hash representing a single subnet.
144
153
  # @return [Array<String>, nil] An array of router IP addresses, or nil if none are found.
154
+ # @note The router data in Kea is a single comma-separated string which must be split into an array.
145
155
  def extract_routers(subnet_data)
146
156
  # The router data is a comma-separated string which must be split into an array.
147
157
  subnet_data['option-data']&.find { |opt| opt['name'] == 'routers' }&.[]('data')&.split(',')
@@ -151,6 +161,7 @@ module Proxy
151
161
  #
152
162
  # @param subnet_data [Hash] The hash representing a single subnet.
153
163
  # @return [Array<String>, nil] A two-element array containing the start and end of the range, or nil.
164
+ # @note The pool range is a hyphen-separated string (e.g. "10.0.0.10-10.0.0.20").
154
165
  def extract_range(subnet_data)
155
166
  # The pool range is a hyphen-separated string (e.g. "10.0.0.10-10.0.0.20").
156
167
  pool_string = subnet_data.dig('pools', 0, 'pool')
@@ -161,6 +172,7 @@ module Proxy
161
172
  #
162
173
  # @param res_data [Hash] The hash representing a single reservation.
163
174
  # @param subnet [Proxy::DHCP::Subnet] The subnet object this reservation belongs to.
175
+ # @return [void]
164
176
  def process_reservation(res_data, subnet)
165
177
  record = ::Proxy::DHCP::Reservation.new(res_data['hostname'], res_data['ip-address'], res_data['hw-address'], subnet)
166
178
  # Add the reservation to the parent class's cache, making it searchable.
@@ -3,7 +3,8 @@
3
3
  module Proxy
4
4
  module DHCP
5
5
  module KeaApi
6
- VERSION = '1.0.1'
6
+ # The current version of the smart_proxy_dhcp_kea_api gem.
7
+ VERSION = '1.1.0'
7
8
  end
8
9
  end
9
10
  end
@@ -16,6 +16,8 @@ module Proxy
16
16
  # Initialises a new Kea API client.
17
17
  #
18
18
  # @param url [String] The base URL of the Kea API endpoint (e.g. 'http://127.0.0.1:8000/').
19
+ # @param username [String, nil] The username for HTTP Basic Authentication.
20
+ # @param password [String, nil] The password for HTTP Basic Authentication.
19
21
  # @param open_timeout [Integer] Time in seconds to wait for the initial TCP connection to be established (defaults to 5).
20
22
  # @param read_timeout [Integer] Time in seconds to wait for a response from the server after the connection is made (defaults to 10).
21
23
  # @raise [ArgumentError] if the URL is blank, malformed, or not a valid HTTP/S URL.
@@ -26,10 +28,12 @@ module Proxy
26
28
  # @example Initialization with Custom Timeouts
27
29
  # client = Proxy::DHCP::KeaApi::Client.new(
28
30
  # url: 'http://127.0.0.1:8000',
31
+ # username: 'myuser',
32
+ # password: 'mypassword'
29
33
  # open_timeout: 2,
30
34
  # read_timeout: 5
31
35
  # )
32
- def initialize(url:, open_timeout: 5, read_timeout: 10)
36
+ def initialize(url:, username: nil, password: nil, open_timeout: 5, read_timeout: 10)
33
37
  raise ArgumentError, 'Kea API URL cannot be nil or empty' if url.to_s.empty?
34
38
 
35
39
  @uri = URI.parse(url)
@@ -38,6 +42,8 @@ module Proxy
38
42
 
39
43
  raise ArgumentError, "Invalid Kea API URL: '#{url}' is missing a host" unless @uri.host
40
44
 
45
+ @username = username
46
+ @password = password
41
47
  @open_timeout = open_timeout
42
48
  @read_timeout = read_timeout
43
49
  logger.info "Initializing Kea API client for URL: #{@uri} with timeouts (open: #{@open_timeout}s, read: #{@read_timeout}s)"
@@ -49,9 +55,10 @@ module Proxy
49
55
  # @param service [String] The Kea service to target (e.g. 'dhcp4').
50
56
  # @param command [String] The command to execute (e.g. 'config-get', 'reservation-add').
51
57
  # @param arguments [Hash] A hash of arguments required by the command. Defaults to an empty hash.
52
- #
53
58
  # @return [Hash] The 'arguments' hash from the Kea API response on success.
54
59
  # @raise [Proxy::DHCP::Error] if the API returns an error or if there's a communication issue.
60
+ # This can be caused by underlying errors like `Net::ReadTimeout`, `Net::OpenTimeout`,
61
+ # `Errno::ECONNREFUSED`, or `JSON::ParserError`.
55
62
  #
56
63
  # @example Get the current DHCPv4 configuration
57
64
  # client = Proxy::DHCP::KeaApi::Client.new(url: 'http://localhost:8000')
@@ -69,6 +76,9 @@ module Proxy
69
76
  # }
70
77
  # })
71
78
  # # => {"text"=>"Reservation added successfully."}
79
+ #
80
+ # @see https://kea.readthedocs.io/en/latest/api.html General Kea Management API documentation.
81
+ # @see https://kea.readthedocs.io/en/latest/api.html#ref-reservation-add For the `reservation-add` command.
72
82
  def post_command(service, command, arguments = {})
73
83
  header = { 'Content-Type' => 'application/json' }
74
84
  payload = {
@@ -87,16 +97,16 @@ module Proxy
87
97
  http.read_timeout = @read_timeout
88
98
  request = Net::HTTP::Post.new(@uri.request_uri, header)
89
99
  request.body = payload.to_json
100
+ request.basic_auth(@username, @password) if @username && @password
90
101
 
91
102
  logger.debug "Sending command to Kea: #{payload.inspect}"
92
103
  response = http.request(request)
93
104
 
94
105
  handle_response(response, command)
95
- # This rescue block catches all standard communication errors (e.g. connection refused,
96
- # timeouts, DNS failures) and wraps them in a Foreman-specific error type. This ensures
97
- # consistent error handling and reporting up to the Foreman UI.
98
- rescue StandardError => e
99
- logger.error "Failed to send command to Kea API: #{e.message}"
106
+ # This rescue block catches specific, expected network and parsing errors,
107
+ # wrapping them in a Foreman-specific error type for consistent handling.
108
+ rescue Net::ReadTimeout, Net::OpenTimeout, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, JSON::ParserError => e
109
+ logger.error "Failed to send command to Kea API: #{e.class.name} - #{e.message}"
100
110
  raise Proxy::DHCP::Error, "Kea API communication error: #{e.message}"
101
111
  end
102
112
 
@@ -106,9 +116,9 @@ module Proxy
106
116
  #
107
117
  # @param response [Net::HTTPResponse] The raw response object from the HTTP request.
108
118
  # @param command [String] The original command that was sent, used for context-specific handling.
109
- #
110
119
  # @return [Hash] The 'arguments' hash from the response on success.
111
- # @raise [Proxy::DHCP::Error] if the response indicates a failure or is malformed.
120
+ # @raise [Proxy::DHCP::Error] if the response indicates a failure, is malformed, or is empty.
121
+ # @raise [JSON::ParserError] if the response body is not valid JSON.
112
122
  # @private
113
123
  def handle_response(response, command)
114
124
  body = JSON.parse(response.body)
@@ -119,7 +129,7 @@ module Proxy
119
129
 
120
130
  # If the response is successful, return its arguments. Otherwise, raise an error.
121
131
  if response_successful?(result, command)
122
- # Provide a fallback of '{}' to prevent returning nil if 'arguments' key is missing.
132
+ # Provide a fallback of '{}' to prevent returning nil if the 'arguments' key is missing.
123
133
  result['arguments'] || {}
124
134
  else
125
135
  error_message = result['text'] || 'Unknown error from Kea API'
@@ -131,16 +141,16 @@ module Proxy
131
141
  #
132
142
  # @param result [Hash] The parsed result hash from the Kea response body.
133
143
  # @param command [String] The original command sent, needed for special case handling.
134
- #
135
144
  # @return [Boolean] `true` if the response is considered a success, `false` otherwise.
145
+ #
146
+ # @see https://kea.readthedocs.io/en/stable/api.html For documentation on Kea API result codes.
136
147
  # @private
137
148
  def response_successful?(result, command)
138
149
  # Universal success is result code 0.
139
150
  return true if result['result'].zero?
140
- # Special case: 'lease4-get-all' is successful even with result code 3 (no leases found).
141
- return true if command == 'lease4-get-all' && result['result'] == 3
142
151
 
143
- false
152
+ # Special case: 'lease4-get-all' is successful even with result code 3 (no leases found).
153
+ command == 'lease4-get-all' && result['result'] == 3
144
154
  end
145
155
  end
146
156
  end
@@ -42,6 +42,8 @@ module Proxy
42
42
  container.singleton_dependency :kea_client, (lambda do
43
43
  ::Proxy::DHCP::KeaApi::Client.new(
44
44
  url: settings[:kea_api_url],
45
+ username: settings[:kea_api_username],
46
+ password: settings[:kea_api_password],
45
47
  open_timeout: settings[:open_timeout],
46
48
  read_timeout: settings[:read_timeout]
47
49
  )
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The top-level namespace for the Foreman Smart Proxy application.
4
+ # All core Smart Proxy modules and plugins are defined within this namespace.
3
5
  module Proxy
6
+ # The namespace for DHCP-related functionality within the Foreman Smart Proxy.
7
+ # All DHCP providers and their associated services are defined here.
4
8
  module DHCP
5
9
  # The top-level namespace for this DHCP provider. All classes, modules,
6
10
  # and services related to the Kea API integration will be defined within
7
11
  # this KeaApi module to prevent naming conflicts with other plugins.
12
+ # @see Proxy::DHCP::KeaApi::Client
13
+ # @see Proxy::DHCP::KeaApi::SubnetService
8
14
  module KeaApi; end
9
15
  end
10
16
  end
@@ -14,11 +14,25 @@ Gem::Specification.new do |s|
14
14
  s.description = 'Provides DHCP management for Foreman via the ISC Kea API, requiring the host_cmds and lease_cmds hooks.'
15
15
  s.license = 'GPL-3.0-only'
16
16
  s.required_ruby_version = '>= 3.0', '< 4'
17
+ s.metadata = {
18
+ 'bug_tracker_uri' => 'https://gitlab.surrey.ac.uk/sm0049/smart-proxy-dhcp-kea-api/-/issues',
19
+ 'homepage_uri' => 'https://gitlab.surrey.ac.uk/sm0049/smart-proxy-dhcp-kea-api',
20
+ 'rubygems_mfa_required' => 'true'
21
+ }
17
22
 
18
23
  s.files = Dir.glob('{lib}/**/*', File::FNM_DOTMATCH).reject { |f| File.directory?(f) }
19
24
  s.files += %w[LICENSE README.md smart_proxy_dhcp_kea_api.gemspec]
20
25
 
21
26
  s.require_paths = ['lib']
22
27
 
23
- s.metadata['rubygems_mfa_required'] = 'true'
28
+ s.add_development_dependency "bundler"
29
+ s.add_development_dependency "concurrent-ruby"
30
+ s.add_development_dependency "rack-test"
31
+ s.add_development_dependency "rake"
32
+ s.add_development_dependency "rspec"
33
+ s.add_development_dependency "rubocop"
34
+ s.add_development_dependency "rubocop-rspec"
35
+ s.add_development_dependency "yard"
36
+ s.add_development_dependency "yard-rspec"
37
+ s.add_development_dependency "webmock"
24
38
  end
metadata CHANGED
@@ -1,15 +1,155 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_dhcp_kea_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam McCarthy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-17 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2025-07-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: concurrent-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack-test
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: yard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: webmock
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
13
153
  description: Provides DHCP management for Foreman via the ISC Kea API, requiring the
14
154
  host_cmds and lease_cmds hooks.
15
155
  email:
@@ -19,6 +159,7 @@ extra_rdoc_files: []
19
159
  files:
20
160
  - LICENSE
21
161
  - README.md
162
+ - lib/.DS_Store
22
163
  - lib/smart_proxy_dhcp_kea_api.rb
23
164
  - lib/smart_proxy_dhcp_kea_api/dhcp_kea_api_main.rb
24
165
  - lib/smart_proxy_dhcp_kea_api/dhcp_kea_api_plugin.rb
@@ -31,6 +172,8 @@ homepage: https://gitlab.surrey.ac.uk/sm0049/smart-proxy-dhcp-kea-api
31
172
  licenses:
32
173
  - GPL-3.0-only
33
174
  metadata:
175
+ bug_tracker_uri: https://gitlab.surrey.ac.uk/sm0049/smart-proxy-dhcp-kea-api/-/issues
176
+ homepage_uri: https://gitlab.surrey.ac.uk/sm0049/smart-proxy-dhcp-kea-api
34
177
  rubygems_mfa_required: 'true'
35
178
  post_install_message:
36
179
  rdoc_options: []