viaduct-gandi 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +229 -0
- data/lib/gandi.rb +44 -0
- data/lib/gandi/contact.rb +200 -0
- data/lib/gandi/domain.rb +157 -0
- data/lib/gandi/operation.rb +27 -0
- data/lib/gandi/price.rb +48 -0
- data/lib/gandi/version.rb +3 -0
- data/lib/gandi/zlib_parser_decorator.rb +15 -0
- metadata +55 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 Viaduct Hosting Limited.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
# Gandi API Library
|
2
|
+
|
3
|
+
This is a short library to facilitate working with the Gandi API for
|
4
|
+
domain registrations. While there are other libraries for this purpose
|
5
|
+
this offers a more satisfying syntax.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
As usual, just pop a reference to the gem into your `Gemfile` and run
|
10
|
+
`bundle`. Alternatively, just install the gem and require it manually.
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'viaduct-gandi', :require => 'gandi'
|
14
|
+
```
|
15
|
+
|
16
|
+
## Configuration
|
17
|
+
|
18
|
+
Before you can run commands, you'll need to configure the library.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
Gandi.apikey = 'abc123abc123abc123abc'
|
22
|
+
Gandi.mode = 'test' # or 'live'
|
23
|
+
```
|
24
|
+
|
25
|
+
## Pricing
|
26
|
+
|
27
|
+
You can obtain pricing for registrations very easily. This example is the
|
28
|
+
most basic form of obtaining a price. In this instance, it returns the price
|
29
|
+
for registering a .com domain.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
price = Gandi::Price.find('example.com')
|
33
|
+
price.action #=> 'create'
|
34
|
+
price.description #=> '.com'
|
35
|
+
price.price #=> 5.5
|
36
|
+
price.min_duration #=> 1
|
37
|
+
price.max_duration #=> 10
|
38
|
+
price.currency #=> 'GBP'
|
39
|
+
price.grid #=> 'E'
|
40
|
+
price.duration_unit #=> 'y'
|
41
|
+
```
|
42
|
+
|
43
|
+
There are a number of options which can be passed to method.
|
44
|
+
|
45
|
+
* `:action` - which service you will be using (create, renew, restore, transfer, plus others - create is default)
|
46
|
+
* `:currency` - which currency to return a price in (GBP, EUR or USD - GBP is default)
|
47
|
+
* `:grid` - your pricing grid (A,B,C,D,E - E is default)
|
48
|
+
* `:duration` - the duration of the registration (a positive interger - 1 is default)
|
49
|
+
|
50
|
+
## Contacts
|
51
|
+
|
52
|
+
At the root of all domain registrations is a contact. Each contact has a unique `handle`
|
53
|
+
which identifies the contact and is needed for all registrations.
|
54
|
+
|
55
|
+
### Creating a new contact
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
contact = Gandi::Contact.new
|
59
|
+
contact.type = 0
|
60
|
+
contact.first_name = 'Adam'
|
61
|
+
contact.last_name = 'Cooke'
|
62
|
+
contact.address = 'Unit 9 Winchester Place'
|
63
|
+
contact.city = 'Poole'
|
64
|
+
contact.country = 'GB'
|
65
|
+
contact.email_address = 'adam@example.com'
|
66
|
+
contact.phone = '+44.1202901101'
|
67
|
+
contact.password = 'randompasswordhere'
|
68
|
+
contact.save #=> true
|
69
|
+
contact.handle #=> 'AC1234-GANDI'
|
70
|
+
```
|
71
|
+
|
72
|
+
If an error occurs here there are two possible exceptions which may be raised (as the
|
73
|
+
same applies to updates). If a validation error is caught locally you will find a
|
74
|
+
`Gandi::ValidationError` is raised otherwise you'll receive `Gandi::DataError` with some
|
75
|
+
mostly uninteligiable garbage from the Gandi API.
|
76
|
+
|
77
|
+
### Finding a contact
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
contact = Gandi::Contact.find('AC1234')
|
81
|
+
contact.first_name #=> 'Adam'
|
82
|
+
```
|
83
|
+
|
84
|
+
### Updating a contact
|
85
|
+
|
86
|
+
To update a contact you need to find it and then make changes to its attributes followed
|
87
|
+
by a save operation.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
contact = Gandi::Contact.find('AC1234')
|
91
|
+
contact.phone = '+44.123451234'
|
92
|
+
contact.save
|
93
|
+
```
|
94
|
+
|
95
|
+
### Determine if a contact can be associated with a domain
|
96
|
+
|
97
|
+
If you wish to see whether a contact is suitable for registration with a given domain,
|
98
|
+
you can call this method on any contact.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
contact.can_associate?('yourdomain.com')
|
102
|
+
```
|
103
|
+
|
104
|
+
This will return true or an error string straight from the Gandi API.
|
105
|
+
|
106
|
+
### Return associated domains
|
107
|
+
|
108
|
+
If you wish to return all domains associated with a contact, you can call the `domains`
|
109
|
+
method.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
contact.domains #=> [Gandi::Domain, Gandi::Domains, ...]
|
113
|
+
```
|
114
|
+
|
115
|
+
## Domains
|
116
|
+
|
117
|
+
Once you have some contacts, you can easily manage domains through through the library.
|
118
|
+
|
119
|
+
### TLDs
|
120
|
+
|
121
|
+
You can get a full list of TLDs which can be registered using the Gandi API using the method
|
122
|
+
shown below. This will return an array of hashes with TLD information.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
Gandi::Domains.tlds
|
126
|
+
```
|
127
|
+
|
128
|
+
### Checking domain availability
|
129
|
+
|
130
|
+
Gandi provides a asyncronous method for checking domain availability. This library provides
|
131
|
+
methods for determining availability both asyncronously and syncronously.
|
132
|
+
|
133
|
+
The asyncronous method accepts a number of domains to check and will return a hash
|
134
|
+
of domains with their availability status. Any status which is 'pending' has not been determined
|
135
|
+
yet and another call should be made in the near future to check again.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
Gandi::Domain.check_availability('domain1.com', 'domain2.io') #=> {'domain1.com' => 'pending', 'domain2.io' => 'pending'}
|
139
|
+
```
|
140
|
+
|
141
|
+
The syncronous method will keep calling the API until it gets a non-pending status for all
|
142
|
+
requested domains. Pending will only be returned if the status cannot be determined within
|
143
|
+
20 requests to the Gandi API.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
Gandi::Domain.check_availability!('domain1.com', 'domain2.io') #=> {'domain1.com' => 'unavailable', 'domain2.io' => 'available'}
|
147
|
+
```
|
148
|
+
|
149
|
+
In addition to these methods, there is also a shorthand syncronous method for checking a
|
150
|
+
single domain's status.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
Gandi::Domain.available?('blahblahblah.com') #=> true or false
|
154
|
+
```
|
155
|
+
|
156
|
+
### Create/register a new domain
|
157
|
+
|
158
|
+
In order to register a domain you need to create a new domain registration object. Once
|
159
|
+
created the domain will be set up automatically. You should monitor the operation's 'step'
|
160
|
+
attribute to see how things are progressing. When this is 'DONE', the domain has been
|
161
|
+
registered.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
if Gandi::Domain.available?('somedomain.com')
|
165
|
+
# Create a domain registration request
|
166
|
+
operation = Gandi::Domain.create('blahblahblah.com', :owner => 'ABC123')
|
167
|
+
# Monitor the operation status
|
168
|
+
operation.step #=> 'BILL'
|
169
|
+
operation.reload
|
170
|
+
operation.step #=> 'RUN'
|
171
|
+
operation.reload
|
172
|
+
operation.step #=> 'DONE' or 'ERROR'
|
173
|
+
operation.last_error #=> 'An error message here if failed'
|
174
|
+
else
|
175
|
+
# Domain is not available for registration
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
### Looking up domain information
|
180
|
+
|
181
|
+
Once a domain has been registered, you can look it up using the following method.
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
domain = Gandi::Domain.find('blahblahblah.com')
|
185
|
+
domain.name #=> "blahblahblah.com"
|
186
|
+
domain.owner #=> A Gandi::Contact instance for the owner\
|
187
|
+
domain.partial? #=> false
|
188
|
+
```
|
189
|
+
|
190
|
+
In addition to these methods, there are a number of other attributes which are available.
|
191
|
+
Consult with the [Gandi docs](http://doc.rpc.gandi.net/domain/reference.html#DomainReturn)
|
192
|
+
for full details.
|
193
|
+
|
194
|
+
### Changing contacts
|
195
|
+
|
196
|
+
To change the contact associated with a domain you can use the method below. You must
|
197
|
+
pass the Gandi handle for the new contact.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
domain = Gandi::Domain.find('blahblahblah.com')
|
201
|
+
operation = domain.change_contacts('admin' => 'DEF123', 'bill' => 'DEF123', 'tech' => 'DEF123')
|
202
|
+
```
|
203
|
+
|
204
|
+
Note: you cannot change the owner with this method and you don't need to change all
|
205
|
+
the contact types. You should monitor the operation object to determine the status.
|
206
|
+
|
207
|
+
### Renewing a domain
|
208
|
+
|
209
|
+
If you wish to renew a domain, you can use the method below.
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
domain = Gandi::Domain.find('blahblahblah.com')
|
213
|
+
operation = domain.renew(2015, 2)
|
214
|
+
```
|
215
|
+
|
216
|
+
The first argument here is the current expiry year and the second argument is the duration
|
217
|
+
for the renewal. You should monitor the operation object to determine the status.
|
218
|
+
|
219
|
+
### Restoring a domain
|
220
|
+
|
221
|
+
If you wish to restore a domain, you can use the method below.
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
domain = Gandi::Domain.find('blahblahblah.com')
|
225
|
+
operation = domain.restore(2)
|
226
|
+
```
|
227
|
+
|
228
|
+
The first argument here is the duration you wish to renew the domain for.
|
229
|
+
You should monitor the operation object to determine the status.
|
data/lib/gandi.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'xmlrpc/client'
|
2
|
+
require 'gandi/zlib_parser_decorator'
|
3
|
+
require 'gandi/version'
|
4
|
+
require 'gandi/price'
|
5
|
+
require 'gandi/contact'
|
6
|
+
require 'gandi/domain'
|
7
|
+
require 'gandi/operation'
|
8
|
+
|
9
|
+
module Gandi
|
10
|
+
|
11
|
+
class Error < StandardError; end
|
12
|
+
class DataError < Error; end
|
13
|
+
class ServerError < Error; end
|
14
|
+
class ValidationError < Error; end
|
15
|
+
|
16
|
+
|
17
|
+
class << self
|
18
|
+
attr_accessor :apikey
|
19
|
+
attr_accessor :mode
|
20
|
+
|
21
|
+
def endpoint
|
22
|
+
mode == 'live' ? 'https://rpc.gandi.net/xmlrpc/' : 'https://rpc.ote.gandi.net/xmlrpc/'
|
23
|
+
end
|
24
|
+
|
25
|
+
def client
|
26
|
+
@client ||= begin
|
27
|
+
XMLRPC::Config.module_eval do
|
28
|
+
remove_const(:ENABLE_NIL_PARSER)
|
29
|
+
const_set(:ENABLE_NIL_PARSER, true)
|
30
|
+
end
|
31
|
+
client = XMLRPC::Client.new2(self.endpoint)
|
32
|
+
client.http_header_extra = {"Accept-Encoding" => "gzip"}
|
33
|
+
client.set_parser ZlibParserDecorator.new(client.send(:parser))
|
34
|
+
client
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(name, *args)
|
39
|
+
client.call(name, apikey, *args)
|
40
|
+
rescue XMLRPC::FaultException => e
|
41
|
+
raise(e.faultCode < 500000 ? ServerError : DataError, e.faultString)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module Gandi
|
2
|
+
class Contact
|
3
|
+
|
4
|
+
COUNTRIES = ['AF','ZA','AL','DZ','DE','AD','AO','AI','AG','AN','SA','AR','AM','AW','AC','AU','AT','AZ','BS','BH','BD','BB','BY','BE','BZ','BJ','BM','BT','BO','BA','BW','BR','BN','BG','BF','BI','KH','CM','CA','CV','KY','CF','CL','CN','CY','CO','KM','CG','CD','CK','KR','KP','CR','CI','HR','CU','DK','DJ','DO','DM','EG','SV','AE','EC','ER','ES','EE','US','ET','FK','FO','FJ','FI','FR','GA','GM','GE','GH','GI','GR','GD','GL','GU','GT','GN','GQ','GW','GY','HT','HI','HN','HK','HU','IN','ID','IR','IQ','IE','IS','IL','IT','JM','JP','JE','JO','KZ','KE','KG','KI','KW','LA','LS','LV','LB','LR','LY','LI','LT','LU','MO','MK','MG','MY','MW','MV','ML','MT','MP','MA','MH','MU','MR','MX','FM','MD','MC','MN','MS','MZ','MM','NA','NR','NP','NI','NE','NG','NU','NF','NO','NZ','OM','UG','UZ','PK','PW','PS','PA','PG','PY','NL','PE','PH','PL','PT','PR','QA','RO','GB','RU','RW','SH','LC','KN','SM','VC','SB','AS','WS','ST','SN','SC','SL','SG','SK','SI','SO','SD','LK','SE','CH','SR','SZ','SY','TJ','TW','TZ','TD','CZ','TH','TP','TG','TK','TO','TT','TN','TM','TC','TR','TV','UA','UY','VU','VE','VI','VG','VN','YE','ZM','ZW','RE','PF','AQ','GF','NC','MQ','GP','PM','TF','YT','RS','TL','CC','CX','HM','AX','BV','GS','GG','IM','ME','IO','PN','EH','BL','MF','VA','SJ','WF','EP']
|
5
|
+
TYPES = {0 => 'person', 1 => 'company', 2 => 'association', 3 => 'public_body'}
|
6
|
+
ATTRIBUTE_MAP = {
|
7
|
+
'type' => 'type',
|
8
|
+
'given' => 'first_name',
|
9
|
+
'family' => 'last_name',
|
10
|
+
'orgname' => 'organization',
|
11
|
+
'password' => 'password',
|
12
|
+
'streetaddr' => 'address',
|
13
|
+
'city' => 'city',
|
14
|
+
'zip' => 'post_code',
|
15
|
+
'country' => 'country',
|
16
|
+
'email' => 'email_address',
|
17
|
+
'phone' => 'phone',
|
18
|
+
'extra_parameters' => 'extra_parameters'
|
19
|
+
}
|
20
|
+
|
21
|
+
# Type of object
|
22
|
+
attr_reader :handle
|
23
|
+
attr_reader :attributes
|
24
|
+
attr_accessor :type
|
25
|
+
|
26
|
+
# Name
|
27
|
+
attr_accessor :first_name
|
28
|
+
attr_accessor :last_name
|
29
|
+
attr_accessor :organization
|
30
|
+
|
31
|
+
# Credentials
|
32
|
+
attr_accessor :password
|
33
|
+
|
34
|
+
# Address Fields
|
35
|
+
attr_accessor :address
|
36
|
+
attr_accessor :city
|
37
|
+
attr_accessor :post_code
|
38
|
+
attr_accessor :country
|
39
|
+
|
40
|
+
# Contact details
|
41
|
+
attr_accessor :email_address
|
42
|
+
attr_accessor :phone
|
43
|
+
attr_accessor :extra_parameters
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@attributes = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Find a contact based on it's handle
|
51
|
+
#
|
52
|
+
def self.find(handle)
|
53
|
+
info = Gandi.call("contact.info", handle)
|
54
|
+
contact = self.new
|
55
|
+
contact.set_attributes(info)
|
56
|
+
contact
|
57
|
+
rescue Gandi::DataError => e
|
58
|
+
e.message =~ /CAUSE_NOTFOUND/ ? nil : raise
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Return extra parameters for this contact
|
63
|
+
#
|
64
|
+
def extra_parameters
|
65
|
+
@extra_parameters ||= {}
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Save the contact's information
|
70
|
+
#
|
71
|
+
def save
|
72
|
+
new_record? ? create : update
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Is this a new record?
|
77
|
+
#
|
78
|
+
def new_record?
|
79
|
+
self.handle.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Create a contact
|
84
|
+
#
|
85
|
+
def create
|
86
|
+
return false unless new_record?
|
87
|
+
raise Gandi::ValidationError, self.errors unless self.errors.empty?
|
88
|
+
result = Gandi.call('contact.create', dirty_attributes_with_remote_keys)
|
89
|
+
set_attributes(result)
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Update a contact
|
95
|
+
#
|
96
|
+
def update
|
97
|
+
return false if new_record?
|
98
|
+
raise Gandi::ValidationError, self.errors unless self.errors.empty?
|
99
|
+
result = Gandi.call('contact.update', self.handle, dirty_attributes_with_remote_keys)
|
100
|
+
set_attributes(result)
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Is the current instance dirty? (I.e. need updating remotely)
|
106
|
+
#
|
107
|
+
def dirty?
|
108
|
+
dirty_attributes.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Returns a hash of all dirty attributes (with local keys)
|
113
|
+
#
|
114
|
+
def dirty_attributes
|
115
|
+
ATTRIBUTE_MAP.values.inject(Hash.new) do |hash, local|
|
116
|
+
if @attributes.keys.include?(local) && @attributes[local] == self.send(local)
|
117
|
+
next hash
|
118
|
+
else
|
119
|
+
hash[local] = self.send(local)
|
120
|
+
end
|
121
|
+
hash
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Returns a hash of all dirty_attributes (with remote keys)
|
127
|
+
#
|
128
|
+
def dirty_attributes_with_remote_keys
|
129
|
+
dirty_attributes.inject(Hash.new) do |hash, (local, value)|
|
130
|
+
remote = ATTRIBUTE_MAP.select { |k,v| v == local }.first[0]
|
131
|
+
hash[remote] = value || ''
|
132
|
+
hash
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# Set the attributes from a remote hash
|
138
|
+
#
|
139
|
+
def set_attributes(hash)
|
140
|
+
@handle = hash['handle']
|
141
|
+
@attributes = {}
|
142
|
+
ATTRIBUTE_MAP.each do |remote, local|
|
143
|
+
self.send(local + '=', hash[remote])
|
144
|
+
remote_value = hash[remote]
|
145
|
+
remote_value = remote_value.dup if remote_value && !remote_value.is_a?(Fixnum)
|
146
|
+
@attributes[local] = remote_value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
#
|
151
|
+
# Validate the contact looks OK to avoid getting ugly-ass and unhelpful messages
|
152
|
+
# from the Gandi endpoint.
|
153
|
+
#
|
154
|
+
# Returns an array of problems or empty if no issues.
|
155
|
+
#
|
156
|
+
def errors
|
157
|
+
Hash.new.tap do |a|
|
158
|
+
a[:type] = "must be one of #{TYPES.keys.join(',')}" unless TYPES.keys.include?(self.type)
|
159
|
+
a[:city] = 'is required' if is_blank?(self.city)
|
160
|
+
a[:first_name] = 'is required' if is_blank?(self.first_name)
|
161
|
+
a[:last_name] = 'is required' if is_blank?(self.last_name)
|
162
|
+
a[:organization] = 'is required' if is_blank?(self.organization) && [1,2,3].include?(self.type)
|
163
|
+
a[:country] = 'is invalid' unless COUNTRIES.include?(self.country)
|
164
|
+
a[:address] = 'is required' if is_blank?(self.address)
|
165
|
+
a[:phone] = 'is invalid' unless self.phone =~ /^\+\d{1,3}\.\d+$/
|
166
|
+
a[:password] = 'is required' if is_blank?(self.password) && new_record?
|
167
|
+
a[:email_address] = 'is invalid' unless self.email_address =~ /^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[-a-z0-9]{2,63})$/
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# Is the given variable blank?
|
173
|
+
#
|
174
|
+
def is_blank?(variable)
|
175
|
+
variable.nil? || variable.length == 0
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Return whether this contact is suitable for use with a certain domain
|
180
|
+
#
|
181
|
+
def can_associate?(domain)
|
182
|
+
spec = {'domain' => domain, 'owner' => true, 'admin' => true}
|
183
|
+
res = Gandi.call('contact.can_associate_domain', self.handle, spec)
|
184
|
+
if res == true
|
185
|
+
true
|
186
|
+
else
|
187
|
+
res
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Return an array of domains associated with this contact
|
193
|
+
#
|
194
|
+
def domains
|
195
|
+
return [] if new_record?
|
196
|
+
Gandi.call('domain.list', {'handle' => self.handle}).map { |d| Domain.new(d, true)}
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
end
|
data/lib/gandi/domain.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
module Gandi
|
2
|
+
class Domain
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
#
|
7
|
+
# Return all available TLDs
|
8
|
+
#
|
9
|
+
def tlds
|
10
|
+
Gandi.call('domain.tld.list')
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Check the availability of a set of domains
|
15
|
+
#
|
16
|
+
def check_availability(*domains)
|
17
|
+
Gandi.call('domain.available', domains)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Check the availability of a set of domains syncronously
|
22
|
+
#
|
23
|
+
def check_availability!(*domains)
|
24
|
+
max_checks = 10
|
25
|
+
loop do
|
26
|
+
result = check_availability(*domains)
|
27
|
+
return result if result.all? { |k,v| v != 'pending' }
|
28
|
+
max_checks -= 1
|
29
|
+
return result if max_checks <= 0
|
30
|
+
sleep 0.7
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Check the availability of a given domain
|
36
|
+
#
|
37
|
+
def available?(domain, max_checks = 10)
|
38
|
+
loop do
|
39
|
+
result = check_availability(domain)
|
40
|
+
return result[domain] == 'available' unless result[domain] == 'pending'
|
41
|
+
max_checks -= 1
|
42
|
+
return false if max_checks <= 0
|
43
|
+
sleep 0.7
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Return the total number of domains on the account
|
49
|
+
#
|
50
|
+
def count
|
51
|
+
Gandi.call('domain.count')
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Register a domain for the given contact. This is a billable action
|
56
|
+
# so be cautious. This method will return an operation object as this
|
57
|
+
# is not run straight away.
|
58
|
+
#
|
59
|
+
def create(domain, options = {})
|
60
|
+
raise ValidationError, "You must specify 'owner' as an option" unless options[:owner]
|
61
|
+
spec = {}
|
62
|
+
spec['duration'] = options[:duration] || 1
|
63
|
+
spec['owner'] = options[:owner]
|
64
|
+
spec['admin'] = options[:admin] || options[:owner]
|
65
|
+
spec['bill'] = options[:bill] || options[:owner]
|
66
|
+
spec['tech'] = options[:tech] || options[:owner]
|
67
|
+
spec['nameservers'] = options[:nameservers] if options[:nameservers]
|
68
|
+
spec['zone_id'] = options[:zone_id] if options[:zone_id]
|
69
|
+
result = Gandi.call('domain.create', domain, spec)
|
70
|
+
Operation.new(result)
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Return information about a doman
|
75
|
+
#
|
76
|
+
def find(name)
|
77
|
+
self.new(Gandi.call('domain.info', name))
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Return an array of all domains
|
82
|
+
#
|
83
|
+
def list
|
84
|
+
Gandi.call('domain.list').map { |d| self.new(d, true) }
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(attributes, partial = false)
|
90
|
+
@attributes = attributes
|
91
|
+
@partial = partial
|
92
|
+
end
|
93
|
+
|
94
|
+
def partial?
|
95
|
+
@partial
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
"#<Gandi::Domain #{name}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Return the domain name
|
104
|
+
#
|
105
|
+
def name
|
106
|
+
@attributes['fqdn']
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# Return attributes
|
111
|
+
#
|
112
|
+
def method_missing(name, value = nil)
|
113
|
+
if @attributes.keys.include?(name.to_s)
|
114
|
+
@attributes[name.to_s]
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Get all the latest information about the domain
|
122
|
+
#
|
123
|
+
def reload
|
124
|
+
@attributes = Gandi.call('domain.info', name)
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Return the domain's owner object
|
130
|
+
#
|
131
|
+
def owner
|
132
|
+
@owner ||= Gandi::Contact.find(contacts['owner']['handle'])
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# Change the domains. Accepts a hash
|
137
|
+
#
|
138
|
+
def change_contacts(hash)
|
139
|
+
Operation.new(Gandi.call('domain.contacts.set', name, hash))
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Renew the domain
|
144
|
+
#
|
145
|
+
def renew(current_year, duration = 1)
|
146
|
+
Operation.new(Gandi.call('domain.renew', name, 'current_year' => current_year, 'duration' => duration))
|
147
|
+
end
|
148
|
+
|
149
|
+
#
|
150
|
+
# Restore the domain
|
151
|
+
#
|
152
|
+
def restore(duration = 1)
|
153
|
+
Operation.new(Gandi.call('domain.restore', name, 'duration' => duration))
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Gandi
|
2
|
+
class Operation
|
3
|
+
|
4
|
+
def self.find(id)
|
5
|
+
self.new(Gandi.call('operation.info', id))
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(attributes)
|
9
|
+
@attributes = attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def method_missing(name, value = nil)
|
14
|
+
if @attributes.keys.include?(name.to_s)
|
15
|
+
@attributes[name.to_s]
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def reload
|
22
|
+
@attributes = Gandi.call('operation.info', @attributes['id'])
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/lib/gandi/price.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Gandi
|
2
|
+
class Price
|
3
|
+
|
4
|
+
attr_accessor :action
|
5
|
+
attr_accessor :description
|
6
|
+
attr_accessor :price
|
7
|
+
attr_accessor :min_duration
|
8
|
+
attr_accessor :max_duration
|
9
|
+
attr_accessor :currency
|
10
|
+
attr_accessor :grid
|
11
|
+
attr_accessor :duration_unit
|
12
|
+
|
13
|
+
def self.find(domain, options = {})
|
14
|
+
options[:action] ||= 'create'
|
15
|
+
options[:duration] ||= 1
|
16
|
+
options[:currency] ||= 'GBP'
|
17
|
+
options[:grid] ||= 'E'
|
18
|
+
product_spec = {
|
19
|
+
'product' => {
|
20
|
+
'description' => domain,
|
21
|
+
'type' => 'domain'
|
22
|
+
},
|
23
|
+
'action' => {
|
24
|
+
'name' => options[:action],
|
25
|
+
:duration => options[:duration],
|
26
|
+
'param' => {'tld_phase' => 'golive'}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
list = Gandi.client.call("catalog.list", Gandi.apikey, product_spec, options[:currency], options[:grid])
|
30
|
+
if list.size == 1
|
31
|
+
price = self.new
|
32
|
+
price.action = list.first['action']['name']
|
33
|
+
price.description = list.first['product']['description']
|
34
|
+
return nil unless list.first['unit_price'].first
|
35
|
+
price.price = list.first['unit_price'].first['price']
|
36
|
+
price.min_duration = list.first['unit_price'].first['min_duration']
|
37
|
+
price.max_duration = list.first['unit_price'].first['max_duration']
|
38
|
+
price.currency = list.first['unit_price'].first['currency']
|
39
|
+
price.grid = list.first['unit_price'].first['grid']
|
40
|
+
price.duration_unit = list.first['unit_price'].first['duration_unit']
|
41
|
+
price
|
42
|
+
else
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Gandi
|
2
|
+
class ZlibParserDecorator
|
3
|
+
def initialize(parser)
|
4
|
+
@parser = parser
|
5
|
+
end
|
6
|
+
|
7
|
+
def parseMethodResponse(responseText)
|
8
|
+
@parser.parseMethodResponse(Zlib::GzipReader.new(StringIO.new(responseText)).read)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parseMethodCall(*args)
|
12
|
+
@parser.parseMethodCall(*args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: viaduct-gandi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Adam Cooke
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-01-05 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: A Gandi module for working with domain registrations.
|
15
|
+
email:
|
16
|
+
- adam@viaduct.io
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/gandi/contact.rb
|
22
|
+
- lib/gandi/domain.rb
|
23
|
+
- lib/gandi/operation.rb
|
24
|
+
- lib/gandi/price.rb
|
25
|
+
- lib/gandi/version.rb
|
26
|
+
- lib/gandi/zlib_parser_decorator.rb
|
27
|
+
- lib/gandi.rb
|
28
|
+
- MIT-LICENSE
|
29
|
+
- README.md
|
30
|
+
homepage: http://viaduct.io
|
31
|
+
licenses: []
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.8.23
|
51
|
+
signing_key:
|
52
|
+
specification_version: 3
|
53
|
+
summary: A Gandi module.
|
54
|
+
test_files: []
|
55
|
+
has_rdoc:
|