provider-dsl 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1e03114f692a9bb504c5c542872a125844180cfb
4
+ data.tar.gz: 6d5d68f7aa7cc8d21dce62ac696e8abf84259a7e
5
+ SHA512:
6
+ metadata.gz: b055051b2bcb8e59bd06c7088dd05ef42def0657069182cc051fe0b791a6c5148c857ee05bd6d2710cd0cf4866a3fa34c74fea6eb6f9a035d16c24657fdf2924
7
+ data.tar.gz: 3e0db13f2286a8091d44ceb4ce9c1f72819ee31fe620e1e5d7f2be449cd089141a0ba3c83ee46f3d2a709459bf962d62a7fe43b4f5a4b6e0f68bec57470810f9
@@ -0,0 +1,25 @@
1
+ require 'provider_dsl/gandi'
2
+ require 'provider_dsl/log'
3
+
4
+ module ProviderDSL
5
+ # The DSL processor
6
+ class DSL
7
+ def initialize
8
+ @logger = Log.instance
9
+ end
10
+
11
+ def execute(glob = nil, &block)
12
+ Dir[glob].each do |filename|
13
+ @logger.log("DSL processing #{filename}")
14
+ instance_eval(File.read(filename))
15
+ @logger.log("DSL completed processing #{filename}")
16
+ end if glob.is_a?(String)
17
+ instance_eval(&block) if block_given?
18
+ end
19
+
20
+ def gandi(parameters, &block)
21
+ parameters[:session_factory] = GandiSessionFactory.new unless parameters.key?(:session_factory)
22
+ Gandi.new(parameters, &block)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,98 @@
1
+ require 'gandi'
2
+ require 'provider_dsl/gandi_proxy'
3
+ require 'provider_dsl/zone'
4
+ require 'provider_dsl/log'
5
+
6
+ module ProviderDSL
7
+ #
8
+ class GandiSessionFactory
9
+ def instance(api_key, environment)
10
+ ::Gandi::Session.new(api_key, env: environment)
11
+ end
12
+ end
13
+
14
+ # Manage a domain on Gandi
15
+ class Gandi
16
+ attr_reader :name_servers, :zone_name
17
+
18
+ def initialize(parameters = {}, &block)
19
+ session_factory = parameters[:session_factory]
20
+ api_key = parameters[:api_key]
21
+ raise 'Gandi API key is not a valid string' unless api_key.is_a?(String)
22
+ environment = parameters[:environment] || :production
23
+ @domain_name = parameters[:domain_name]
24
+ @logger = Log.instance
25
+ @logger.confidential(/#{Regexp.quote(api_key)}/)
26
+ @logger.log("Processing Gandi account in #{environment}")
27
+ @session = session_factory.instance(api_key, environment)
28
+ if @domain_name
29
+ @logger.log("Domain name: #{@domain_name}")
30
+ @name_servers = @session.domain.info(@domain_name).nameservers.uniq.sort
31
+ @name_server_addresses =
32
+ Hash[@session.domain.host.list(@domain_name).map { |data| [data['name'], data['ips'].uniq.sort] }]
33
+ end
34
+ @zone_name = nil
35
+ @required_zone = []
36
+ instance_eval(&block) if block_given?
37
+ end
38
+
39
+ def execute(&block)
40
+ instance_eval(&block)
41
+ end
42
+
43
+ def zone(zone_name, parameters = {}, &block)
44
+ @logger.log("Zone: #{zone_name}")
45
+ original_zone = @session.domain.zone.list.select { |data| data['name'] == zone_name }
46
+ if original_zone.count.zero?
47
+ zone_id = nil
48
+ original_zone = nil
49
+ else
50
+ zone_id = original_zone.first['id']
51
+ original_zone = @session.domain.zone.record.list(zone_id, 0).map { |record| Hash[record] }
52
+ end
53
+ if block_given?
54
+ zone = Zone.new(original_zone.nil? ? [] : original_zone, parameters)
55
+ zone.create(&block)
56
+ zone_id = original_zone.nil? ? @session.domain.zone.create(name: zone_name).id : zone_id
57
+ if original_zone.nil? || zone.changed?
58
+ @logger.log("Zone records:\n#{zone.to_s(' ')}")
59
+ version = @session.domain.zone.version.new(zone_id)
60
+ @session.domain.zone.record.set(zone_id, version, zone.hash)
61
+ @session.domain.zone.version.set(zone_id, version)
62
+ @logger.log("Created version #{version} of zone #{zone_name}")
63
+ else
64
+ @logger.log("Zone #{zone_name} is unchanged")
65
+ end
66
+ elsif original_zone.nil?
67
+ raise "Zone #{zone_name} is undefined"
68
+ end
69
+ return unless @domain_name
70
+ @logger.log("Attaching Gandi zone #{zone_name} to domain #{@domain_name}")
71
+ @session.domain.zone.set(@domain_name, zone_id)
72
+ name_servers!
73
+ end
74
+
75
+ def name_server_addresses(name_server)
76
+ name_server = "#{name_server}.#{@domain_name}"
77
+ @name_server_addresses.key?(name_server) ? @name_server_addresses[name_server] : []
78
+ end
79
+
80
+ def name_server_addresses!(name_server, required_name_server_addresses)
81
+ current_name_server_addresses = name_server_addresses(name_server)
82
+ required_name_server_addresses = Array(required_name_server_addresses).uniq.sort
83
+ name_server = "#{name_server}.#{@domain_name}"
84
+ return if current_name_server_addresses == required_name_server_addresses
85
+ if @name_server_addresses.key?(name_server)
86
+ @session.domain.host.update(name_server, required_name_server_addresses)
87
+ else
88
+ @session.domain.host.create(name_server, required_name_server_addresses)
89
+ end
90
+ end
91
+
92
+ def name_servers!(required_name_servers = %w(a.dns.gandi.net b.dns.gandi.net c.dns.gandi.net))
93
+ @name_servers = Array(required_name_servers).uniq.sort
94
+ @logger.log((["Setting name servers for Gandi domain #{@domain_name} to:"] + @name_servers).join("\n "))
95
+ @session.domain.nameservers.set(@domain_name, @name_servers)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,27 @@
1
+ require 'gandi'
2
+ require 'provider_dsl/rate_limiter'
3
+ require 'provider_dsl/log'
4
+
5
+ module ProviderDSL
6
+ module GandiProxy
7
+ # Gandi allow 30 calls to their API every 2 seconds - http://doc.rpc.gandi.net/overview.html#rate-limit
8
+ # To be safe we'll limit calls to no more than 20 every 2 seconds
9
+ LIMITER = RateLimiter.new(20, 2)
10
+
11
+ def method_missing(method, *arguments)
12
+ result = super(method, *arguments)
13
+ return result unless ::Gandi::VALID_METHODS.include?(chained.join('.'))
14
+ Log.instance.debug((["-> Gandi #{method}"] + [*arguments]).join("\n "))
15
+ Log.instance.debug("Result:\n#{result.to_yaml}")
16
+ LIMITER.wait
17
+ result
18
+ end
19
+ end
20
+ end
21
+
22
+ module Gandi
23
+ # Mix in this custom call handler to the standard Gandi client
24
+ class ProxyCall
25
+ prepend ProviderDSL::GandiProxy
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ module ProviderDSL
2
+ module GemDescription
3
+ NAME = 'provider-dsl'.freeze
4
+ VERSION = '1.0.0'.freeze
5
+ SUMMARY = 'A DSL for interacting with various service provider APIs'.freeze
6
+ PAGE = 'https://github.com/sappho/gem-provider_dsl'.freeze
7
+ AUTHORS = ['Andrew Heald'].freeze
8
+ EMAIL = ['andrew@heald.uk'].freeze
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ require 'singleton'
2
+
3
+ module ProviderDSL
4
+ # A simple logger
5
+ class Log
6
+ include Singleton
7
+
8
+ attr_accessor :callback, :debug
9
+
10
+ def initialize
11
+ @log = []
12
+ @confidential = []
13
+ @callback = nil
14
+ @debug = ENV['DSL_DEBUG'] == 'true'
15
+ end
16
+
17
+ def log(message)
18
+ @confidential.each { |regex| message.gsub!(regex, '******') }
19
+ @log << message
20
+ @callback.call(message) unless @callback.nil?
21
+ end
22
+
23
+ def debug(message)
24
+ log(message) if @debug
25
+ end
26
+
27
+ def confidential(regex)
28
+ @confidential << regex
29
+ @confidential.uniq!
30
+ end
31
+
32
+ def to_s
33
+ @log.join("\n")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ module ProviderDSL
2
+ # Simple rate limiter
3
+ class RateLimiter
4
+ def initialize(maximum_calls, time_period)
5
+ @maximum_calls = maximum_calls
6
+ @time_period = time_period
7
+ reset
8
+ end
9
+
10
+ def wait
11
+ if @call_countdown.zero?
12
+ delta = Time.now - @timestamp
13
+ sleep(@time_period - delta) if delta < @time_period
14
+ reset
15
+ end
16
+ @call_countdown -= 1
17
+ end
18
+
19
+ private
20
+
21
+ def reset
22
+ @call_countdown = @maximum_calls
23
+ @timestamp = Time.now
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module ProviderDSL
2
+ # Manage a DNS record
3
+ class Record
4
+ attr_reader :name, :type, :value, :ttl, :hash
5
+
6
+ def initialize(name, type, value, ttl)
7
+ @name = name
8
+ @type = type
9
+ @value = value
10
+ @ttl = ttl
11
+ @hash = { name: name, type: type, value: value, ttl: ttl }
12
+ end
13
+
14
+ def to_s
15
+ "#{ttl} #{name} #{type} #{value}"
16
+ end
17
+
18
+ def ==(other)
19
+ self === other && ttl == other.ttl
20
+ end
21
+
22
+ def ===(other)
23
+ name == other.name && type == other.type && value == other.value
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,127 @@
1
+ require 'ipaddress'
2
+ require 'provider_dsl/record'
3
+ require 'provider_dsl/log'
4
+
5
+ module ProviderDSL
6
+ # Manage a DNS zone
7
+ class Zone
8
+ attr_reader :records
9
+
10
+ def initialize(original_records, parameters = {})
11
+ @logger = Log.instance
12
+ @original_records = original_records.map do |record|
13
+ if record.is_a?(Hash)
14
+ Record.new(record['name'], record['type'], record['value'], record['ttl'])
15
+ else
16
+ record
17
+ end
18
+ end
19
+ @records = parameters[:inherit_records] ? @original_records.clone : []
20
+ @names = []
21
+ name
22
+ ttl
23
+ end
24
+
25
+ def create(&block)
26
+ instance_eval(&block)
27
+ end
28
+
29
+ def name(name = nil, &block)
30
+ new_names = name.nil? ? @names : [name] + @names
31
+ effective_name(new_names)
32
+ if block_given?
33
+ saved_names = @names
34
+ @names = new_names
35
+ instance_eval(&block)
36
+ @names = saved_names
37
+ effective_name(@names)
38
+ end
39
+ end
40
+
41
+ def ttl(ttl = 3600)
42
+ @ttl = ttl
43
+ end
44
+
45
+ def aaaa(ip_addresses)
46
+ record('AAAA', ip_addresses) do |ip_address|
47
+ raise "#{ip_address} is not a valid IPv6 address" unless IPAddress.valid_ipv6?(ip_address)
48
+ IPAddress(ip_address).compressed
49
+ end
50
+ end
51
+
52
+ def a(ip_addresses)
53
+ record('A', ip_addresses) do |ip_address|
54
+ raise "#{ip_address} is not a valid IPv4 address" unless IPAddress.valid_ipv4?(ip_address)
55
+ IPAddress(ip_address).octets.join('.')
56
+ end
57
+ end
58
+
59
+ def cname(value)
60
+ value = String(value)
61
+ raise "CNAME #{value} cannot be defined for a naked domain" if @name == '@'
62
+ record('CNAME', value) do
63
+ @records = records.select { |other| !(other.type == 'CNAME' && other.name == @name) }
64
+ value
65
+ end
66
+ end
67
+
68
+ def mx(values)
69
+ record('MX', values)
70
+ end
71
+
72
+ def txt(values)
73
+ record('TXT', values)
74
+ end
75
+
76
+ def new_records
77
+ records.select { |record| @original_records.select { |original| original == record }.count.zero? }
78
+ end
79
+
80
+ def removed_records
81
+ @original_records.select { |original| records.select { |record| original == record }.count.zero? }
82
+ end
83
+
84
+ def changed?
85
+ !(new_records + removed_records).count.zero?
86
+ end
87
+
88
+ def to_s(prefix = '', suffix = '')
89
+ "#{prefix}#{sorted_records.join("#{suffix}\n#{prefix}")}#{suffix}"
90
+ end
91
+
92
+ def hash
93
+ sorted_records.map(&:hash)
94
+ end
95
+
96
+ private
97
+
98
+ def effective_name(names)
99
+ @name = names.count.zero? ? '@' : names.join('.')
100
+ end
101
+
102
+ def record(type, values)
103
+ values = Array(values)
104
+ keepers = values.select { |value| value == '?' }.count
105
+ if keepers > 0
106
+ raise "Keeper ? flag is inconsistently used for #{@name} #{type} #{values}" if keepers != 1 || values.count != 1
107
+ @original_records.select { |original| original.name == @name && original.type == type }.each do |original|
108
+ add(original)
109
+ end
110
+ else
111
+ values.each do |value|
112
+ value = yield(value) if block_given?
113
+ add(Record.new(@name, type, value, @ttl))
114
+ end
115
+ end
116
+ end
117
+
118
+ def add(record)
119
+ @logger.log("Adding #{record}")
120
+ @records = records.select { |other| !(record === other) } + [record]
121
+ end
122
+
123
+ def sorted_records
124
+ records.sort_by { |record| [record.name, record.type, record.value] }
125
+ end
126
+ end
127
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: provider-dsl
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Heald
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '11.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '11.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.44.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.44.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-mocks
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ipaddress
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.3
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.3
83
+ - !ruby/object:Gem::Dependency
84
+ name: gandi
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.3'
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 3.3.27
93
+ type: :runtime
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '3.3'
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.3.27
103
+ - !ruby/object:Gem::Dependency
104
+ name: map
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '6.6'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '6.6'
117
+ description: See the project home page for more information
118
+ email:
119
+ - andrew@heald.uk
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - lib/provider_dsl/dsl.rb
125
+ - lib/provider_dsl/gandi.rb
126
+ - lib/provider_dsl/gandi_proxy.rb
127
+ - lib/provider_dsl/gem_description.rb
128
+ - lib/provider_dsl/log.rb
129
+ - lib/provider_dsl/rate_limiter.rb
130
+ - lib/provider_dsl/record.rb
131
+ - lib/provider_dsl/zone.rb
132
+ homepage: https://github.com/sappho/gem-provider_dsl
133
+ licenses: []
134
+ metadata: {}
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubyforge_project:
151
+ rubygems_version: 2.5.1
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: A DSL for interacting with various service provider APIs
155
+ test_files: []