provider-dsl 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []