gandi_v5 0.3.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +56 -2
  4. data/.travis.yml +6 -1
  5. data/CHANGELOG.md +61 -0
  6. data/LICENSE.md +2 -6
  7. data/README.md +15 -9
  8. data/gandi_v5.gemspec +2 -2
  9. data/lib/gandi_v5.rb +40 -14
  10. data/lib/gandi_v5/data.rb +1 -2
  11. data/lib/gandi_v5/data/converter/integer.rb +26 -0
  12. data/lib/gandi_v5/domain.rb +13 -14
  13. data/lib/gandi_v5/email/forward.rb +2 -8
  14. data/lib/gandi_v5/email/mailbox.rb +57 -11
  15. data/lib/gandi_v5/email/slot.rb +11 -3
  16. data/lib/gandi_v5/live_dns.rb +0 -12
  17. data/lib/gandi_v5/live_dns/domain.rb +313 -29
  18. data/lib/gandi_v5/live_dns/domain/dnssec_key.rb +115 -0
  19. data/lib/gandi_v5/live_dns/domain/record.rb +81 -0
  20. data/lib/gandi_v5/live_dns/domain/snapshot.rb +107 -0
  21. data/lib/gandi_v5/live_dns/domain/tsig_key.rb +71 -0
  22. data/lib/gandi_v5/organization.rb +10 -0
  23. data/lib/gandi_v5/organization/customer.rb +90 -0
  24. data/lib/gandi_v5/sharing_space.rb +27 -0
  25. data/lib/gandi_v5/simple_hosting.rb +12 -0
  26. data/lib/gandi_v5/simple_hosting/instance.rb +242 -0
  27. data/lib/gandi_v5/simple_hosting/instance/application.rb +44 -0
  28. data/lib/gandi_v5/simple_hosting/instance/database.rb +19 -0
  29. data/lib/gandi_v5/simple_hosting/instance/language.rb +22 -0
  30. data/lib/gandi_v5/simple_hosting/instance/upgrade.rb +21 -0
  31. data/lib/gandi_v5/simple_hosting/instance/virtual_host.rb +187 -0
  32. data/lib/gandi_v5/simple_hosting/instance/virtual_host/linked_dns_zone.rb +74 -0
  33. data/lib/gandi_v5/version.rb +1 -1
  34. data/spec/.rubocop.yml +56 -2
  35. data/spec/fixtures/bodies/GandiV5_Domain/fetch.yml +8 -0
  36. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain/fetch.yml +1 -2
  37. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain/list_tsig.yml +3 -0
  38. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain/nameservers.yml +3 -0
  39. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain_DnssecKey/fetch.yml +12 -0
  40. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain_DnssecKey/list.yml +9 -0
  41. data/spec/fixtures/bodies/{GandiV5_LiveDNS_Zone_Snapshot → GandiV5_LiveDNS_Domain_Snapshot}/fetch.yml +4 -3
  42. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain_Snapshot/list.yml +5 -0
  43. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain_TsigKey/fetch.yml +9 -0
  44. data/spec/fixtures/bodies/GandiV5_LiveDNS_Domain_TsigKey/list.yml +4 -0
  45. data/spec/fixtures/bodies/GandiV5_Organization_Customer/list.yml +8 -0
  46. data/spec/fixtures/bodies/GandiV5_SimpleHosting_Instance/fetch.yml +80 -0
  47. data/spec/fixtures/bodies/GandiV5_SimpleHosting_Instance/list.yml +38 -0
  48. data/spec/fixtures/bodies/GandiV5_SimpleHosting_Instance_VirtualHost/fetch.yml +26 -0
  49. data/spec/fixtures/bodies/GandiV5_SimpleHosting_Instance_VirtualHost/list.yml +18 -0
  50. data/spec/units/gandi_v5/data/converter/integer_spec.rb +16 -0
  51. data/spec/units/gandi_v5/domain_spec.rb +53 -43
  52. data/spec/units/gandi_v5/email/forward_spec.rb +5 -34
  53. data/spec/units/gandi_v5/email/mailbox_spec.rb +119 -37
  54. data/spec/units/gandi_v5/email/slot_spec.rb +10 -2
  55. data/spec/units/gandi_v5/live_dns/domain/dnssec_key_spec.rb +128 -0
  56. data/spec/units/gandi_v5/live_dns/{record_set_spec.rb → domain/record_spec.rb} +1 -1
  57. data/spec/units/gandi_v5/live_dns/domain/snapshot_spec.rb +101 -0
  58. data/spec/units/gandi_v5/live_dns/domain/tsig_key_spec.rb +78 -0
  59. data/spec/units/gandi_v5/live_dns/domain_spec.rb +297 -118
  60. data/spec/units/gandi_v5/live_dns_spec.rb +0 -12
  61. data/spec/units/gandi_v5/organization/customer_spec.rb +81 -0
  62. data/spec/units/gandi_v5/organization_spec.rb +14 -0
  63. data/spec/units/gandi_v5/sharing_space_spec.rb +4 -0
  64. data/spec/units/gandi_v5/simple_hosting/instance/application_spec.rb +37 -0
  65. data/spec/units/gandi_v5/simple_hosting/instance/database_spec.rb +4 -0
  66. data/spec/units/gandi_v5/simple_hosting/instance/language_spec.rb +4 -0
  67. data/spec/units/gandi_v5/simple_hosting/instance/upgrade_spec.rb +4 -0
  68. data/spec/units/gandi_v5/simple_hosting/instance/virtual_host/linked_dns_zone_spec.rb +50 -0
  69. data/spec/units/gandi_v5/simple_hosting/instance/virtual_host_spec.rb +199 -0
  70. data/spec/units/gandi_v5/simple_hosting/instance_spec.rb +182 -0
  71. data/spec/units/gandi_v5/simple_hosting_spec.rb +9 -0
  72. data/spec/units/gandi_v5_spec.rb +139 -30
  73. metadata +50 -31
  74. data/lib/gandi_v5/domain/sharing_space.rb +0 -21
  75. data/lib/gandi_v5/live_dns/has_zone_records.rb +0 -153
  76. data/lib/gandi_v5/live_dns/record_set.rb +0 -79
  77. data/lib/gandi_v5/live_dns/zone.rb +0 -160
  78. data/lib/gandi_v5/live_dns/zone/snapshot.rb +0 -81
  79. data/spec/features/domain_spec.rb +0 -45
  80. data/spec/features/livedns_domain_spec.rb +0 -8
  81. data/spec/features/livedns_zone_spec.rb +0 -45
  82. data/spec/features/mailbox_spec.rb +0 -18
  83. data/spec/fixtures/bodies/GandiV5_LiveDNS_Zone/fetch.yml +0 -11
  84. data/spec/fixtures/bodies/GandiV5_LiveDNS_Zone/list.yml +0 -11
  85. data/spec/fixtures/bodies/GandiV5_LiveDNS_Zone_Snapshot/list.yml +0 -3
  86. data/spec/fixtures/vcr/Domain_features/List_domains.yml +0 -55
  87. data/spec/fixtures/vcr/Domain_features/Renew_domain.yml +0 -133
  88. data/spec/fixtures/vcr/LiveDNS_Domain_features/List_domains.yml +0 -32
  89. data/spec/fixtures/vcr/LiveDNS_Zone_features/List_zones.yml +0 -42
  90. data/spec/fixtures/vcr/LiveDNS_Zone_features/Make_and_save_snapshot.yml +0 -72
  91. data/spec/fixtures/vcr/LiveDNS_Zone_features/Save_zone_to_file.yml +0 -28
  92. data/spec/fixtures/vcr/Mailbox_features/List_mailboxes.yml +0 -39
  93. data/spec/units/gandi_v5/domain/sharing_space_spec.rb +0 -4
  94. data/spec/units/gandi_v5/live_dns/zone/snapshot_spec.rb +0 -66
  95. data/spec/units/gandi_v5/live_dns/zone_spec.rb +0 -347
@@ -76,18 +76,12 @@ class GandiV5
76
76
  # e.g. ("alice" "*lice", "alic*").
77
77
  # @return [Array<GandiV5::Email::Forward>]
78
78
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
79
- def self.list(fqdn, page: (1..), **params)
80
- page = [page.to_i] unless page.respond_to?(:each)
81
-
79
+ def self.list(fqdn, page: (1..), per_page: 100, **params)
82
80
  params.reject! { |_k, v| v.nil? }
83
81
 
84
82
  mailboxes = []
85
- page.each do |page_number|
86
- _response, data = GandiV5.get url(fqdn), params: params.merge(page: page_number)
87
- break if data.empty?
88
-
83
+ GandiV5.paginated_get(url(fqdn), page, per_page, params: params) do |data|
89
84
  mailboxes += data.map { |mailbox| from_gandi mailbox.merge(fqdn: fqdn) }
90
- break if data.count < params.fetch(:per_page, 100)
91
85
  end
92
86
  mailboxes
93
87
  end
@@ -105,6 +105,39 @@ class GandiV5
105
105
  data['message']
106
106
  end
107
107
 
108
+ # Upgrade a standard mailbox to premium.
109
+ # If the current slot is a free one, a new premium slot is created and
110
+ # used for the mailbox. Otherwise, the slot is upgraded to premium.
111
+ # @see https://api.gandi.net/docs/email#patch-v5-email-mailboxes-domain-mailbox_id-type
112
+ # @param sharing_id [String, #to_s, nil] (optional)
113
+ # the organisation ID to bill for the mailbox.
114
+ # @param dry_run [Boolean] whether the details should be checked instead
115
+ # of actually upgrading the mailbox.
116
+ # @return [true] if the mailbox was upgraded
117
+ # @return [false] if the mailbox was not upgraded (it's already premium)
118
+ # @return [Hash] if doing a dry run, you get what Gandi returns
119
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error
120
+ def upgrade(sharing_id: nil, dry_run: false)
121
+ patch_type :premium, sharing_id, dry_run
122
+ end
123
+
124
+ # Downgrade a premium mailbox to standard.
125
+ # If a free slot is available, the premium slot is destroyed
126
+ # (and refunded) and the free one is used for the mailbox.
127
+ # Otherwise, the slot is downgraded to standard.
128
+ # @see https://api.gandi.net/docs/email#patch-v5-email-mailboxes-domain-mailbox_id-type
129
+ # @param sharing_id [String, #to_s, nil] (optional)
130
+ # the organisation ID to bill for the mailbox.
131
+ # @param dry_run [Boolean] whether the details should be checked instead
132
+ # of actually downgrading the mailbox.
133
+ # @return [true] if the mailbox was downgraded
134
+ # @return [false] if the mailbox was not downgraded (it's already standard)
135
+ # @return [Hash] if doing a dry run, you get what Gandi returns
136
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error
137
+ def downgrade(sharing_id: nil, dry_run: false)
138
+ patch_type :standard, sharing_id, dry_run
139
+ end
140
+
108
141
  # Create a new mailbox.
109
142
  # Note that before you can create a mailbox, you must have a slot available.
110
143
  # @see https://api.gandi.net/docs/email#post-v5-email-mailboxes-domain
@@ -113,10 +146,13 @@ class GandiV5
113
146
  # @param password [String, #to_s] the password to use.
114
147
  # @param aliases [Array<String, #to_s>] any alternative email address to be used.
115
148
  # @param type [:standard, :premium] the type of mailbox slot to use.
149
+ # @param dry_run [Boolean] whether the details should be checked instead
150
+ # of actually creating the mailbox.
116
151
  # @return [GandiV5::Email::Mailbox] The created mailbox.
117
152
  # @raise [GandiV5::Error] if no slots are available.
118
153
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
119
- def self.create(fqdn, login, password, aliases: [], type: :standard)
154
+ # rubocop:disable Metrics/AbcSize
155
+ def self.create(fqdn, login, password, aliases: [], type: :standard, dry_run: false)
120
156
  fail ArgumentError, "#{type.inspect} is not a valid type" unless TYPES.include?(type)
121
157
  if GandiV5::Email::Slot.list.none? { |slot| slot.mailbox_type == type && slot.inactive? }
122
158
  fail GandiV5::Error, "no available #{type} slots"
@@ -131,9 +167,11 @@ class GandiV5
131
167
  aliases: aliases.push
132
168
  }.to_json
133
169
 
134
- response, _data = GandiV5.post url(fqdn), body
135
- fetch fqdn, response.headers[:location].split('/').last
170
+ response, data = GandiV5.post(url(fqdn), body, 'Dry-Run': dry_run ? 1 : 0)
171
+
172
+ dry_run ? data : fetch(fqdn, response.headers[:location].split('/').last)
136
173
  end
174
+ # rubocop:enable Metrics/AbcSize
137
175
 
138
176
  # Get information for a mailbox.
139
177
  # @see https://api.gandi.net/docs/email#get-v5-email-mailboxes-domain-mailbox_id
@@ -159,19 +197,13 @@ class GandiV5
159
197
  # e.g. ("alice" "*lice", "alic*").
160
198
  # @return [Array<GandiV5::Email::Mailbox>]
161
199
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
162
- def self.list(fqdn, page: (1..), **params)
163
- page = [page.to_i] unless page.respond_to?(:each)
164
-
200
+ def self.list(fqdn, page: (1..), per_page: 100, **params)
165
201
  params['~login'] = params.delete(:login)
166
202
  params.reject! { |_k, v| v.nil? }
167
203
 
168
204
  mailboxes = []
169
- page.each do |page_number|
170
- _response, data = GandiV5.get url(fqdn), params: params.merge(page: page_number)
171
- break if data.empty?
172
-
205
+ GandiV5.paginated_get(url(fqdn), page, per_page, params: params) do |data|
173
206
  mailboxes += data.map { |mailbox| from_gandi mailbox }
174
- break if data.count < params.fetch(:per_page, 100)
175
207
  end
176
208
  mailboxes
177
209
  end
@@ -240,6 +272,20 @@ class GandiV5
240
272
  def crypt_password(password)
241
273
  self.class.send :crypt_password, password
242
274
  end
275
+
276
+ def patch_type(new_type, sharing_id, dry_run)
277
+ fail ArgumentError unless TYPES.include?(new_type)
278
+ return false if type == new_type
279
+
280
+ url_ = "#{url}/type"
281
+ url_ = sharing_id ? "#{url_}?sharing_id=#{sharing_id}" : url_
282
+ body = { mailbox_type: new_type }
283
+
284
+ _response, data = GandiV5.patch(url_, body.to_json, 'Dry-Run': dry_run ? 1 : 0)
285
+
286
+ @type = new_type unless dry_run
287
+ dry_run ? data : true
288
+ end
243
289
  end
244
290
  end
245
291
  end
@@ -78,15 +78,23 @@ class GandiV5
78
78
  # but require more mailboxes on that domain, you must first purchase additional slots.
79
79
  # @see https://api.gandi.net/docs/email#post-v5-email-slots-domain
80
80
  # @param fqdn [String, #to_s] the fully qualified domain name to add the slot to.
81
- # @param type [:standard, :premium] Tyhe type of slot to add.
81
+ # @param type [:standard, :premium] The type of slot to add.
82
+ # @param sharing_id [nil, String, #to_s] either:
83
+ # * nil (default) - nothing special happens
84
+ # * an organization ID - pay using another organization
85
+ # (you need to have billing permissions on the organization
86
+ # and use the same organization name for the domain name's owner).
87
+ # The invoice will be edited using this organization's information.
82
88
  # @return [String] The confirmation message from Gandi.
83
89
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
84
- def self.create(fqdn, type = :standard)
90
+ def self.create(fqdn, type: :standard, sharing_id: nil)
85
91
  body = {
86
92
  mailbox_type: type
87
93
  }.to_json
88
94
 
89
- response, _data = GandiV5.post url(fqdn), body
95
+ url_ = url(fqdn)
96
+ url_ += "?sharing_id=#{sharing_id}" if sharing_id
97
+ response, _data = GandiV5.post url_, body
90
98
  fetch fqdn, response.headers[:location].split('/').last
91
99
  end
92
100
 
@@ -4,8 +4,6 @@
4
4
  class GandiV5
5
5
  # Gandi LiveDNS Management API.
6
6
  class LiveDNS
7
- BASE = 'https://dns.api.gandi.net/api/v5/'
8
-
9
7
  RECORD_TYPES = %w[
10
8
  A AAAA CNAME MX NS TXT ALIAS
11
9
  WKS SRV LOC SPF CAA DS SSHFP PTR KEY DNAME TLSA OPENPGPKEY CDS
@@ -21,16 +19,6 @@ class GandiV5
21
19
  GandiV5::LiveDNS::Domain.list
22
20
  end
23
21
 
24
- # @see GandiV5::LiveDNS::Zone.fetch
25
- def self.zone(uuid)
26
- GandiV5::LiveDNS::Zone.fetch(uuid)
27
- end
28
-
29
- # @see GandiV5::LiveDNS::Zone.list
30
- def self.zones
31
- GandiV5::LiveDNS::Zone.list
32
- end
33
-
34
22
  # Raise an error if passed type is invalid.
35
23
  # @param type [String] the record type to check.
36
24
  # @return [nil]
@@ -5,19 +5,12 @@ class GandiV5
5
5
  # A domain name within the LiveDNS system.
6
6
  # @!attribute [r] fqdn
7
7
  # @return [String]
8
- # @!attribute [r] zone_uuid
9
- # @return [String]
8
+ # @!attribute [r] automatic_snapshots
9
+ # @return [Boolean]
10
10
  class Domain
11
11
  include GandiV5::Data
12
- include GandiV5::LiveDNS::HasZoneRecords
13
-
14
- members :fqdn
15
12
 
16
- member(
17
- :zone_uuid,
18
- gandi_key: 'zone',
19
- converter: GandiV5::Data::Converter.new(from_gandi: ->(zone) { zone&.split('/')&.last })
20
- )
13
+ members :fqdn, :automatic_snapshots
21
14
 
22
15
  # Refetch the information for this domain from Gandi.
23
16
  # @return [GandiV5::LiveDNS::Domain]
@@ -27,35 +20,283 @@ class GandiV5
27
20
  from_gandi data
28
21
  end
29
22
 
30
- # Change the zone used by this domain.
31
- # @param uuid [String, #uuid, #to_s] the UUID of the zone this domain should now use.
23
+ # Update this domain's settings.
24
+ # @param automatic_snapshots [String, #to_s]
25
+ # Enable or disable the automatic creation of new snapshots when records are changed.
32
26
  # @return [String] The confirmation message from Gandi.
33
27
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
34
- def change_zone(uuid)
35
- uuid = uuid.uuid if uuid.respond_to?(:uuid)
36
- _response, data = GandiV5.patch url, { zone_uuid: uuid }.to_json
37
- self.zone_uuid = uuid
28
+ def update(automatic_snapshots:)
29
+ _response, data = GandiV5.patch url, { automatic_snapshots: automatic_snapshots }.to_json
30
+ self.automatic_snapshots = automatic_snapshots
38
31
  data['message']
39
32
  end
40
33
 
41
- # @see GandiV5::LiveDNS::Zone.fetch
42
- def fetch_zone
43
- GandiV5::LiveDNS::Zone.fetch zone_uuid
34
+ # @overload fetch_records()
35
+ # Fetch all records for this domain.
36
+ # @param page [Integer, #each<Integer>] which page(s) of results to get.
37
+ # If page is not provided keep querying until an empty list is returned.
38
+ # If page responds to .each then iterate until an empty list is returned.
39
+ # @param per_page [Integer, #to_s] (optional default 100) how many results ot get per page.
40
+ # @overload fetch_records(name)
41
+ # Fetch records for a name.
42
+ # @param name [String] the name to fetch records for.
43
+ # @param page [Integer, #each<Integer>] which page(s) of results to get.
44
+ # If page is not provided keep querying until an empty list is returned.
45
+ # If page responds to .each then iterate until an empty list is returned.
46
+ # @param per_page [Integer, #to_s] (optional default 100) how many results ot get per page.
47
+ # @overload fetch_records(name, type)
48
+ # Fetch records of a type for a name.
49
+ # @param name [String] the name to fetch records for.
50
+ # @param type [String] the record type to fetch.
51
+ # @param page [Integer, #each<Integer>] which page(s) of results to get.
52
+ # If page is not provided keep querying until an empty list is returned.
53
+ # If page responds to .each then iterate until an empty list is returned.
54
+ # @param per_page [Integer, #to_s] (optional default 100) how many results ot get per page.
55
+ # @return [Array<GandiV5::LiveDNS::Domain::Record>]
56
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
57
+ def fetch_records(name = nil, type = nil, page: (1..), per_page: 100)
58
+ GandiV5::LiveDNS.require_valid_record_type type if type
59
+
60
+ url_ = "#{url}/records"
61
+ url_ += "/#{CGI.escape name}" if name
62
+ url_ += "/#{CGI.escape type}" if type
63
+
64
+ all = []
65
+ GandiV5.paginated_get(url_, page, per_page) do |data|
66
+ all += [*data].map { |item| GandiV5::LiveDNS::Domain::Record.from_gandi item }
67
+ end
68
+ all
44
69
  end
45
70
 
46
- # The domain's zone (fetching from Gandi if required).
47
- # @return [GandiV5::LiveDNS::Zone]
71
+ # @overload fetch_zone_lines()
72
+ # Fetch all records for this domain.
73
+ # @overload fetch_zone_lines(name)
74
+ # Fetch records for a name.
75
+ # @param name [String] the name to fetch records for.
76
+ # @overload fetch_zone_lines(name, type)
77
+ # Fetch records of a type for a name.
78
+ # @param name [String] the name to fetch records for.
79
+ # @param type [String] the record type to fetch.
80
+ # @return [String]
48
81
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
49
- def zone
50
- @zone ||= fetch_zone
82
+ def fetch_zone_lines(name = nil, type = nil)
83
+ GandiV5::LiveDNS.require_valid_record_type type if type
84
+
85
+ url_ = "#{url}/records"
86
+ url_ += "/#{CGI.escape name}" if name
87
+ url_ += "/#{CGI.escape type}" if type
88
+
89
+ GandiV5.get(url_, accept: 'text/plain').last
90
+ end
91
+
92
+ # Add record to this domain.
93
+ # @param name [String]
94
+ # @param type [String]
95
+ # @param ttl [Integer]
96
+ # @param values [Array<String>]
97
+ # @return [String] The confirmation message from Gandi.
98
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
99
+ def add_record(name, type, ttl, *values)
100
+ GandiV5::LiveDNS.require_valid_record_type type
101
+ fail ArgumentError, 'ttl must be positive and non-zero' unless ttl.positive?
102
+ fail ArgumentError, 'there must be at least one value' if values.none?
103
+
104
+ body = {
105
+ rrset_name: name,
106
+ rrset_type: type,
107
+ rrset_ttl: ttl,
108
+ rrset_values: values
109
+ }.to_json
110
+ _response, data = GandiV5.post "#{url}/records", body
111
+ data['message']
112
+ end
113
+
114
+ # @overload delete_records()
115
+ # Delete all records for this domain.
116
+ # @overload delete_records(name)
117
+ # Delete records for a name.
118
+ # @param name [String] the name to delete records for.
119
+ # @overload delete_records(name, type)
120
+ # Delete records of a type for a name.
121
+ # @param name [String] the name to delete records for.
122
+ # @param type [String] the record type to delete.
123
+ # @return [nil]
124
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
125
+ def delete_records(name = nil, type = nil)
126
+ GandiV5::LiveDNS.require_valid_record_type(type) if type
127
+
128
+ url_ = "#{url}/records"
129
+ url_ += "/#{CGI.escape name}" if name
130
+ url_ += "/#{CGI.escape type}" if type
131
+ GandiV5.delete(url_).last
132
+ end
133
+
134
+ # Replace records for the domain.
135
+ # @param name [String, nil] only replaces records for this name.
136
+ # @param type [String, nil] only replaces record of this type (requires name).
137
+ # @param values [Array<String>] the values to set for the record.
138
+ # @raise [ArgumentError] if ttl is present and type is absent.
139
+ # @return [String] The confirmation message from Gandi.
140
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
141
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
142
+ def replace_records(records, name: nil, type: nil)
143
+ if type
144
+ GandiV5::LiveDNS.require_valid_record_type(type) if type
145
+ fail ArgumentError, 'missing keyword: name' if name.nil?
146
+ end
147
+
148
+ url_ = "#{url}/records"
149
+ url_ += "/#{CGI.escape name}" if name
150
+ url_ += "/#{CGI.escape type}" if type
151
+
152
+ body = if type && name
153
+ { rrset_values: records }
154
+ else
155
+ { items: records.map { |r| r.transform_keys { |k| "rrset_#{k}" } } }
156
+ end
157
+
158
+ _response, data = GandiV5.put url_, body.to_json
159
+ data['message']
160
+ end
161
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
162
+
163
+ # Replace all records for this domain.
164
+ # @param text [String] zone file lines to replace the records with.
165
+ # @return [String] The confirmation message from Gandi.
166
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
167
+ def replace_zone_lines(text)
168
+ _response, data = GandiV5.put "#{url}/records", text, 'content-type': 'text/plain'
169
+ data['message']
170
+ end
171
+
172
+ # The list of nameservers that this domain is using according to LiveDNS' systems.
173
+ # * Either there are no NS records on @ and the 3 hashed nameservers are returned
174
+ # (ns-{123}-{abc}.gandi.net)
175
+ # * Or some NS records exist on @ and it will return those
176
+ # @see https://api.gandi.net/docs/domains#get-v5-domain-domains-domain-name$
177
+ # @return [Array<String>]
178
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
179
+ def name_servers
180
+ @name_servers ||= fetch_name_servers
181
+ end
182
+
183
+ # Requery Gandi for the domain's name servers.
184
+ # @see https://api.gandi.net/docs/domains#get-v5-domain-domains-domain-name$
185
+ # @return [Array<String>]
186
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
187
+ def fetch_name_servers
188
+ _response, data = GandiV5.get "#{url}/nameservers"
189
+ @name_servers = data
190
+ end
191
+
192
+ # The list of DNSSEC keys for the domain.
193
+ # If you need the fingerprint, public_key or tag attributes you'll need
194
+ # use GandiV5::LiveDNS::Domain::DnssecKey.fetch on each item.
195
+ # @return [Array<GandiV5::LiveDNS::Domain::DnssecKey>]
196
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
197
+ def dnssec_keys
198
+ @dnssec_keys ||= fetch_dnssec_keys
199
+ end
200
+
201
+ # Requery Gandi for the domain's DNSSEC keys.
202
+ # @return [Array<GandiV5::LiveDNS::Domain::DnssecKey>]
203
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
204
+ def fetch_dnssec_keys
205
+ @dnssec_keys = GandiV5::LiveDNS::Domain::DnssecKey.list(fqdn)
206
+ end
207
+
208
+ # The list of TSIG keys for the domain.
209
+ # If you need the secret, fingerprint, public_key or tag attributes you'll need
210
+ # to use GandiV5::LiveDNS::Domain::DnssecKey.fetch on each item.
211
+ # @return [Array<GandiV5::LiveDNS::Domain::TsigKey>]
212
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
213
+ def tsig_keys
214
+ @tsig_keys ||= fetch_tsig_keys
215
+ end
216
+
217
+ # Requery Gandi for the domain's TSIG keys.
218
+ # @return [Array<GandiV5::LiveDNS::Domain::TsigKey>]
219
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
220
+ def fetch_tsig_keys
221
+ _response, data = GandiV5.get "#{url}/axfr/tsig"
222
+ data.map { |item| GandiV5::LiveDNS::Domain::TsigKey.from_gandi item }
223
+ end
224
+
225
+ # Add a Tsig key to this domain.
226
+ # @param key [GandiV5::LiveDNS::Domain::TsigKey, #uuid, String, #to_s]
227
+ # the key to add.
228
+ # @param sharing_id [nil, String, #to_s]
229
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
230
+ def add_tsig_key(key, sharing_id: nil)
231
+ key = key.uuid if key.respond_to?(:uuid)
232
+ url_ = "#{url}/axfr/tsig/#{key}"
233
+ url_ += "?sharing_id=#{CGI.escape sharing_id}" if sharing_id
234
+ _response, _data = GandiV5.put url_
235
+ end
236
+
237
+ # Remove a Tsig key from this domain.
238
+ # @param key [GandiV5::LiveDNS::Domain::TsigKey, #uuid, String, #to_s]
239
+ # the key to remove.
240
+ # @param sharing_id [nil, String, #to_s]
241
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
242
+ def remove_tsig_key(key, sharing_id: nil)
243
+ key = key.uuid if key.respond_to?(:uuid)
244
+ url_ = "#{url}/axfr/tsig/#{key}"
245
+ url_ += "?sharing_id=#{CGI.escape sharing_id}" if sharing_id
246
+ _response, _data = GandiV5.delete url_
247
+ end
248
+
249
+ # The list of AXFR clients for the domain.
250
+ # @return [Array<String>] list of IP addresses.
251
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
252
+ def axfr_clients
253
+ @axfr_clients ||= fetch_axfr_clients
254
+ end
255
+
256
+ # Requery Gandi for the domain's AXFR clients.
257
+ # @return [Array<String>] list of IP addresses.
258
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
259
+ def fetch_axfr_clients
260
+ _response, data = GandiV5.get "#{url}/axfr/slaves"
261
+ data
262
+ end
263
+
264
+ # Add an AXFR client to this domain.
265
+ # @param ip [String, #to_s] the IP address to add.
266
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
267
+ def add_axfr_client(ip)
268
+ _response, _data = GandiV5.put "#{url}/axfr/slaves/#{ip}"
269
+ end
270
+
271
+ # Remove and AXFR client from this domain.
272
+ # @param ip [String, #to_s] the IP address to remove.
273
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
274
+ def remove_axfr_client(ip)
275
+ _response, _data = GandiV5.delete "#{url}/axfr/slaves/#{ip}"
51
276
  end
52
277
 
53
278
  # List the domains.
54
- # @return [Array<GandiV5::LiveDNS::Domain>]
279
+ # @param page [#each<Integer, #to_s>] the page(s) of results to retrieve.
280
+ # If page is not provided keep querying until an empty list is returned.
281
+ # If page responds to .each then iterate until an empty list is returned.
282
+ # @param per_page [Integer, #to_s] (optional default 100) how many results to get per page.
283
+ # @param sharing_id [String, #to_s] (optional) filter by domains assigned to a given user.
284
+ # @return [Array<String>]
55
285
  # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
56
- def self.list
57
- _response, data = GandiV5.get url
58
- data.map { |item| from_gandi item }
286
+ def self.list(page: (1..), per_page: 100, sharing_id: nil)
287
+ page = [page.to_i] unless page.respond_to?(:each)
288
+ params = { per_page: per_page }
289
+ params[:sharing_id] = sharing_id unless sharing_id.nil?
290
+
291
+ domains = []
292
+ page.each do |page_number|
293
+ _resp, data = GandiV5.get url, params: params.merge(page: page_number)
294
+ break if data.empty?
295
+
296
+ domains += data.map { |item| item['fqdn'] }
297
+ break if data.count < per_page
298
+ end
299
+ domains
59
300
  end
60
301
 
61
302
  # Get a domain.
@@ -67,14 +308,57 @@ class GandiV5
67
308
  from_gandi data
68
309
  end
69
310
 
311
+ # Create a new domain in the LiveDNS system.
312
+ # You must have sufficent permission to manage the domain to do this.
313
+ # @param fqdn [String, #to_s] the fully qualified domain to add to LiveDNS.
314
+ # @param records [Array<Hash, GandiV5::LiveDNS::Domain::Record, #to_h, nil>]
315
+ # @param ttl [Integer, #to_s, nil] the TTL of the SOA record.
316
+ # Note that this is not a default TTL that will be used for the records in the zone.
317
+ # the records (if any) to add to the created zone.
318
+ # @param sharing_id [nil, String, #to_s]
319
+ # @return [GandiV5::LiveDNS::Domain]
320
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
321
+ def self.create(fqdn, records = nil, soa_ttl: nil, sharing_id: nil)
322
+ body = { fqdn: fqdn, zone: {} }
323
+ body[:zone][:ttl] = soa_ttl if soa_ttl
324
+ if records
325
+ body[:zone][:items] = records.map do |r|
326
+ r.to_h.transform_keys { |k| "rrset_#{k}" }
327
+ end
328
+ end
329
+
330
+ url_ = url
331
+ url_ += "?sharing_id=#{CGI.escape sharing_id}" if sharing_id
332
+
333
+ GandiV5.post url_, body.to_json
334
+ fetch(fqdn)
335
+ end
336
+
337
+ # Fetch the list of known record types (A, CNAME, etc.)
338
+ # @return [Array<String>]
339
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
340
+ def self.record_types
341
+ GandiV5.get("#{BASE}livedns/dns/rrtypes").last
342
+ end
343
+
344
+ # Get the LiveDNS servers to use for a domain.
345
+ # Does not take into account any NS records that exist in the zone.
346
+ # @param fqdn [String, #to_s] the fully qualified domain to hash in
347
+ # in order to get the LiveDNS servers to use.
348
+ # @return [Array<String>]
349
+ # @raise [GandiV5::Error::GandiError] if Gandi returns an error.
350
+ def self.generic_name_servers(fqdn)
351
+ GandiV5.get("#{BASE}livedns/nameservers/#{CGI.escape fqdn}").last
352
+ end
353
+
70
354
  private
71
355
 
72
356
  def url
73
- "#{BASE}domains/#{CGI.escape(fqdn)}"
357
+ "#{BASE}livedns/domains/#{CGI.escape fqdn}"
74
358
  end
75
359
 
76
360
  def self.url(fqdn = nil)
77
- "#{BASE}domains" + (fqdn ? "/#{CGI.escape(fqdn)}" : '')
361
+ "#{BASE}livedns/domains" + (fqdn ? "/#{CGI.escape fqdn}" : '')
78
362
  end
79
363
  private_class_method :url
80
364
  end