cloud-dyndns 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ tags
2
+ test/fixtures/test.log
3
+ .bundle
4
+ pkg/*
5
+
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source 'https://rubygems.org/'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'minitest-reporters'
7
+ gem 'minitest-matchers'
8
+ gem 'minitest'
9
+ gem 'mocha'
10
+ gem 'minitest-spec-expect'
11
+ end
12
+
13
+ group :development do
14
+ gem 'guard'
15
+ gem 'guard-minitest'
16
+ end
@@ -0,0 +1,94 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cloud-dyndns (0.0.1)
5
+ fog (= 1.18.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ansi (1.4.3)
11
+ builder (3.2.2)
12
+ celluloid (0.15.2)
13
+ timers (~> 1.1.0)
14
+ coderay (1.1.0)
15
+ excon (0.28.0)
16
+ ffi (1.9.3)
17
+ fog (1.18.0)
18
+ builder
19
+ excon (~> 0.28.0)
20
+ formatador (~> 0.2.0)
21
+ mime-types
22
+ multi_json (~> 1.0)
23
+ net-scp (~> 1.1)
24
+ net-ssh (>= 2.1.3)
25
+ nokogiri (~> 1.5)
26
+ ruby-hmac
27
+ formatador (0.2.4)
28
+ guard (2.2.4)
29
+ formatador (>= 0.2.4)
30
+ listen (~> 2.1)
31
+ lumberjack (~> 1.0)
32
+ pry (>= 0.9.12)
33
+ thor (>= 0.18.1)
34
+ guard-minitest (2.1.1)
35
+ guard (~> 2.0)
36
+ minitest (>= 3.0)
37
+ hashie (2.0.5)
38
+ listen (2.2.0)
39
+ celluloid (>= 0.15.2)
40
+ rb-fsevent (>= 0.9.3)
41
+ rb-inotify (>= 0.9)
42
+ lumberjack (1.0.4)
43
+ metaclass (0.0.1)
44
+ method_source (0.8.2)
45
+ mime-types (2.0)
46
+ mini_portile (0.5.2)
47
+ minitest (4.7.5)
48
+ minitest-matchers (1.3.0)
49
+ minitest (~> 4.7)
50
+ minitest-reporters (0.14.23)
51
+ ansi
52
+ builder
53
+ minitest (>= 2.12, < 5.0)
54
+ powerbar
55
+ minitest-spec-expect (0.1.1)
56
+ minitest (>= 2.4)
57
+ mocha (0.14.0)
58
+ metaclass (~> 0.0.1)
59
+ multi_json (1.8.2)
60
+ net-scp (1.1.2)
61
+ net-ssh (>= 2.6.5)
62
+ net-ssh (2.7.0)
63
+ nokogiri (1.6.0)
64
+ mini_portile (~> 0.5.0)
65
+ powerbar (1.0.11)
66
+ ansi (~> 1.4.0)
67
+ hashie (>= 1.1.0)
68
+ pry (0.9.12.3)
69
+ coderay (~> 1.0)
70
+ method_source (~> 0.8)
71
+ slop (~> 3.4)
72
+ rake (10.1.0)
73
+ rb-fsevent (0.9.3)
74
+ rb-inotify (0.9.2)
75
+ ffi (>= 0.5.0)
76
+ ruby-hmac (0.4.0)
77
+ slop (3.4.7)
78
+ thor (0.18.1)
79
+ timers (1.1.0)
80
+
81
+ PLATFORMS
82
+ ruby
83
+
84
+ DEPENDENCIES
85
+ bundler (~> 1.3)
86
+ cloud-dyndns!
87
+ guard
88
+ guard-minitest
89
+ minitest
90
+ minitest-matchers
91
+ minitest-reporters
92
+ minitest-spec-expect
93
+ mocha
94
+ rake
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :minitest do
5
+
6
+ # with Minitest::Spec
7
+ watch(%r{^spec/(.*)_spec\.rb})
8
+ watch(%r{^lib/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" }
9
+ watch(%r{^spec/spec_helper\.rb}) { 'spec' }
10
+
11
+ # Rails 4
12
+ # watch(%r{^app/(.+)\.rb}) { |m| "test/#{m[1]}_test.rb" }
13
+ # watch(%r{^app/controllers/application_controller\.rb}) { 'test/controllers' }
14
+ # watch(%r{^app/controllers/(.+)_controller\.rb}) { |m| "test/integration/#{m[1]}_test.rb" }
15
+ # watch(%r{^app/views/(.+)_mailer/.+}) { |m| "test/mailers/#{m[1]}_mailer_test.rb" }
16
+ # watch(%r{^lib/(.+)\.rb}) { |m| "test/lib/#{m[1]}_test.rb" }
17
+ # watch(%r{^test/.+_test\.rb})
18
+ # watch(%r{^test/test_helper\.rb}) { 'test' }
19
+
20
+ # Rails < 4
21
+ # watch(%r{^app/controllers/(.*)\.rb}) { |m| "test/functional/#{m[1]}_test.rb" }
22
+ # watch(%r{^app/helpers/(.*)\.rb}) { |m| "test/helpers/#{m[1]}_test.rb" }
23
+ # watch(%r{^app/models/(.*)\.rb}) { |m| "test/unit/#{m[1]}_test.rb" }
24
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 nat
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,101 @@
1
+ # cloud-dyndns
2
+
3
+ Use a cloud DNS provider like a dynamic dns service. A Ruby executable to
4
+ stick in a crontab. If the excellent [fog](http://fog.io/) supports the
5
+ service, cloud-dyndns will (probably) work for you.
6
+
7
+ # install
8
+
9
+ ```
10
+ gem install cloud-dyndns
11
+ ```
12
+
13
+ # usage
14
+
15
+ ```
16
+ cloud-dyndns --config ~/$your_yaml_file
17
+ ```
18
+
19
+ where `$your_yaml_file` is the path to a file that looks like:
20
+
21
+ # config example
22
+
23
+ ```yaml
24
+ :credentials:
25
+ :provider: 'AWS'
26
+ :aws_secret_access_key: 'your-secretkeykeykey'
27
+ :aws_access_key_id: 'your-access-id'
28
+ :zones:
29
+ - :domain: "looting.biz"
30
+ :targets:
31
+ - "phl.looting.biz"
32
+ - "*.phl.looting.biz"
33
+ - :domain: "narf.io"
34
+ :targets:
35
+ - "phl.narf.io"
36
+ - "*.phl.narf.io"
37
+ ```
38
+
39
+ # command line options
40
+
41
+ there's -c/--config and -l/--log:
42
+
43
+ * --config is mandatory: a path to your config file
44
+ * --log is optional: if not given it will log to stdout. if given a path it
45
+ will log to that file.
46
+
47
+ # crontab example
48
+
49
+ ```
50
+ */15 * * * * cloud-dyndns --config /Users/nat/.cloud-dyndns.yml > ~/.cdyndns.log
51
+ ```
52
+
53
+ will make sure your domain is up to date every 15 minutes, and will log to
54
+ `~/.cdyndns.log`
55
+
56
+ If you use rbenv, rvm, or rb-john-wanamaker...I'm not sure what you can do. I
57
+ vaguely remember with rvm you could create "wrappers". I know it's possible to
58
+ use software the way you might want. Sorry I'm not more helpful here.
59
+
60
+ # other
61
+
62
+ I refactored this and added tests for release. I've been using it for about a
63
+ year. [Fog](http://fog.io/) is good, contributions welcome.
64
+
65
+ # testing
66
+
67
+ ```
68
+ bundle install --without development
69
+ ```
70
+
71
+ ```
72
+ rake test
73
+ ```
74
+
75
+ This will allow running tests on ruby >= 1.9.2
76
+
77
+ # development
78
+
79
+ ```
80
+ bundle install --without nothing
81
+ ```
82
+
83
+ (See [this SO
84
+ post](http://stackoverflow.com/questions/4118055/rails-bundler-doesnt-install-gems-inside-a-group)
85
+ about why I suggest just throwing that `--without nothing` in there.
86
+
87
+ ```
88
+ bundle exec guard
89
+ ```
90
+
91
+ Development requires Ruby >= 1.9.3 due to the listen gem's requirement of Ruby
92
+ 1.9.3.
93
+
94
+ # todo
95
+
96
+ * allow setting of hostmaster email?
97
+ * allow usage of a different wtfismyip-type service?
98
+ * use a version of fog that requires only 1.8.7?
99
+ * this would include adding a fix for [this
100
+ issue](https://github.com/fog/fog/issues/1093) in this library.
101
+
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = "spec/**/*_spec.rb"
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require File.expand_path(File.dirname(__FILE__) + '/../cloud-dyndns')
5
+
6
+ config = CloudDyndns::Config.new(ARGV)
7
+ dns = CloudDyndns::Updater.new(config.config, config.log)
8
+
9
+ dns.update!
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'version'
5
+
6
+ description = %q{
7
+ update DNS entries on cloud providers (Route 53 et al) based on your current external IP address. think like a dyndns updater. no-ip, et al.
8
+ }
9
+
10
+ Gem::Specification.new do |spec|
11
+ spec.name = "cloud-dyndns"
12
+ spec.version = CloudDyndns::VERSION
13
+ spec.authors = ["Nat Lownes"]
14
+ spec.email = ["nat.lownes@gmail.com"]
15
+ spec.description = description
16
+ spec.summary = description
17
+ spec.homepage = "https://github.com/natlownes/cloud-dyndns"
18
+ spec.license = "MIT"
19
+
20
+ spec.files = `git ls-files`.split($/)
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "fog", '=1.18.0'
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.3"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "minitest-reporters"
30
+ spec.add_development_dependency "minitest-matchers"
31
+ spec.add_development_dependency "minitest"
32
+ spec.add_development_dependency "mocha"
33
+ spec.add_development_dependency "minitest-spec-expect"
34
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+
3
+
4
+ module CloudDyndns
5
+ ROOTDIR=File.expand_path(File.join(File.dirname(__FILE__)))
6
+ LIBDIR=File.join(ROOTDIR, 'lib')
7
+ end
8
+
9
+
10
+ require File.join CloudDyndns::LIBDIR, 'version'
11
+ require File.join CloudDyndns::LIBDIR, 'config'
12
+ require File.join CloudDyndns::LIBDIR, 'updater'
@@ -0,0 +1,2 @@
1
+ */15 * * * * cd ~/.ddns && /Users/nat/.rvm/bin/ruby-1.9.2-p290@ddns phl_looting_biz.rb > ~/.ddns/out.log
2
+
@@ -0,0 +1,68 @@
1
+ require 'optparse'
2
+ require 'logger'
3
+ require 'yaml'
4
+
5
+
6
+ module CloudDyndns
7
+
8
+ class Config
9
+
10
+ def usage_banner
11
+ <<-EOF
12
+ USAGE: cloud-dyndns --config [/path/to/config.yaml] --log [/path/to/log]
13
+ EOF
14
+ end
15
+
16
+ def config_instructions
17
+ 'Path to config file (required)'
18
+ end
19
+
20
+ def log_instructions
21
+ 'Path to log file (if not given, will write to stdout)'
22
+ end
23
+
24
+ def initialize(args)
25
+ check_args(args)
26
+
27
+ parser = OptionParser.new do |opts|
28
+ opts.banner = usage_banner
29
+
30
+ opts.on '-c', '--config PATH', config_instructions do |v|
31
+ set_config!(v)
32
+ end
33
+
34
+ opts.on '-l', '--log [PATH]', log_instructions do |v|
35
+ set_log!(v)
36
+ end
37
+ end
38
+
39
+ parser.parse!(args)
40
+ end
41
+
42
+ def log
43
+ @log ||= ::Logger.new(@log_path || STDOUT)
44
+ end
45
+
46
+ def config
47
+ @config ||= ::YAML.load_file(@config_path)
48
+ end
49
+
50
+ private
51
+
52
+ def check_args(args)
53
+ if !args or args.length == 0
54
+ puts usage_banner
55
+ exit(1)
56
+ end
57
+ end
58
+
59
+ def set_config!(path)
60
+ @config_path = File.expand_path(path) if path
61
+ end
62
+
63
+ def set_log!(path)
64
+ @log_path = path
65
+ ::Kernel.at_exit { @log.close() if @log }
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,169 @@
1
+ require 'open-uri'
2
+ require 'fog'
3
+
4
+
5
+ class CloudDyndns::Updater
6
+ class NoIPAddressError < StandardError
7
+ def message
8
+ "could not get your current ip address: \n
9
+ cowardly refusing to update your DNS record to nothing"
10
+ end
11
+ end
12
+
13
+ class NoTargetsSpecified < StandardError
14
+ def message
15
+ "no domain names to update were specified"
16
+ end
17
+ end
18
+
19
+ def self.domain_name_to_provider_name(name)
20
+ "#{name}."
21
+ end
22
+
23
+ def initialize(config_object, logger)
24
+ @credentials = config_object[:credentials]
25
+ @log = logger
26
+ @zone_configs = config_object[:zones]
27
+ end
28
+
29
+ def update!
30
+ updates = []
31
+ zone_configs.each do |zone_config|
32
+ targets = zone_config[:targets]
33
+ top_level_zone = zone_config[:domain]
34
+
35
+ check_config_targets(top_level_zone, targets)
36
+
37
+ zone = find_zone_by_name(top_level_zone)
38
+
39
+ targets.each do |target|
40
+ updates << update_record_for_zone(zone, target, zone_config)
41
+ end
42
+ end
43
+
44
+ updates = []
45
+ end
46
+
47
+ def zones
48
+ dns.zones
49
+ end
50
+
51
+ def dns
52
+ @dns ||= create_dns(@credentials)
53
+ end
54
+
55
+ def log
56
+ @log
57
+ end
58
+
59
+ def zone_configs
60
+ @zone_configs || []
61
+ end
62
+
63
+ def find_zone_by_name(zone_domain)
64
+ find_zone_by_domain(zone_domain) or create_zone_by_domain(zone_domain)
65
+ end
66
+
67
+ def find_zone_by_domain(domain_name)
68
+ zones.find do |z|
69
+ z.domain.match(%r{#{domain_name}\.$})
70
+ end
71
+ end
72
+
73
+ def create_zone_by_domain(domain_name)
74
+ zones.create({
75
+ :domain => domain_name,
76
+ :email => "hostmaster@#{domain_name}"
77
+ })
78
+ end
79
+
80
+ def records_for_zone(zone)
81
+ zone.records.reload
82
+ end
83
+
84
+ def find_record_for_zone_by_name(zone, name)
85
+ records_for_zone(zone).find do |r|
86
+ r.name == self.class.domain_name_to_provider_name(name)
87
+ end
88
+ end
89
+
90
+ def update_record_for_zone(zone, target, zone_config={})
91
+ # zone is the zone object from Fog
92
+ # target is a string, like "*.example.com"
93
+ # or just name.example.com" or just
94
+ # "example.com"
95
+ # zone_config is the object from the zones array
96
+ # in the yaml config
97
+ current_ip = get_ip_address()
98
+
99
+ if current_ip.empty?
100
+ raise NoIPAddressError.new
101
+ end
102
+
103
+ record = find_record_for_zone_by_name(zone, target)
104
+
105
+ target_attributes = {
106
+ :name => target,
107
+ :value => current_ip,
108
+ :ttl => zone_config[:ttl]
109
+ }
110
+
111
+ if is_record_create?(record)
112
+ return create_record_for_zone(zone, target_attributes)
113
+ end
114
+
115
+ if is_record_update?(record)
116
+ record.destroy
117
+ return create_record_for_zone(zone, target_attributes)
118
+ end
119
+
120
+ record
121
+ end
122
+
123
+ private
124
+
125
+ def check_config_targets(top_level_zone, targets)
126
+ if !targets or targets.empty?
127
+ @log << %{
128
+ #{top_level_zone} has zero targets in config, add some domains to update in your config file
129
+ }
130
+ raise NoTargetsSpecified.new
131
+ end
132
+ end
133
+
134
+ def create_dns(credentials)
135
+ Fog::DNS.new(credentials)
136
+ end
137
+
138
+ def default_ttl
139
+ 300
140
+ end
141
+
142
+ def get_ip_address
143
+ ip_url = "http://wtfismyip.com/text"
144
+
145
+ @external_ip ||= OpenURI.open_uri(ip_url).read.strip
146
+ end
147
+
148
+ def is_record_create?(record)
149
+ !record
150
+ end
151
+
152
+ def is_record_update?(record)
153
+ # record exists and is not our external ip
154
+ external_ip = get_ip_address()
155
+ record && !record.value.include?(external_ip)
156
+ end
157
+
158
+ def create_record_for_zone(zone, attrs={})
159
+ attributes = {
160
+ :ttl => (attrs[:ttl] || default_ttl()),
161
+ :value => attrs[:value],
162
+ :name => attrs[:name],
163
+ :type => 'A'
164
+ }
165
+ zone.records.create(attributes)
166
+ @log << "created record: #{attributes.to_yaml}"
167
+ end
168
+
169
+ end
@@ -0,0 +1,3 @@
1
+ module CloudDyndns
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,83 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+ root = CloudDyndns::ROOTDIR
3
+
4
+ config_file_path = File.join(root, 'spec', 'fixtures', 'config.yml')
5
+ test_log_path = File.join(root, 'spec', 'fixtures', 'test.log')
6
+ cloud_dyndyns_path = File.join(root, 'bin', 'cloud-dyndns')
7
+
8
+
9
+ describe 'option parsing' do
10
+
11
+ describe 'with no options' do
12
+ it 'should exit with exit code 1' do
13
+ exited_status_zero = system(cloud_dyndyns_path)
14
+
15
+ expect(exited_status_zero).to_equal false
16
+ end
17
+
18
+ it 'should print the usage banner to stdout' do
19
+ out = `#{cloud_dyndyns_path}`
20
+
21
+ expect(out).to_match(/USAGE: cloud-dyndns/)
22
+ end
23
+
24
+ end
25
+
26
+ describe 'with invalid --config' do
27
+
28
+ it 'should raise MissingArgument' do
29
+ args = ['--config']
30
+
31
+ expect { CloudDyndns::Config.new(args) }.
32
+ to_raise OptionParser::MissingArgument
33
+ end
34
+
35
+ end
36
+
37
+ describe 'with valid options' do
38
+ after do
39
+ FileUtils.rm_rf(test_log_path)
40
+ end
41
+
42
+ it 'should read the config file' do
43
+ args = ['--config', config_file_path]
44
+ config = CloudDyndns::Config.new(args)
45
+
46
+ expect(config.config[:credentials][:provider]).to_equal 'AWS'
47
+ expect(config.config[:zones][0][:domain]).to_equal 'looting.biz'
48
+ expect(config.config[:zones][1][:domain]).to_equal 'narf.io'
49
+
50
+ expect(
51
+ config.config[:credentials][:aws_secret_access_key]
52
+ ).to_equal 'honk-secret'
53
+
54
+ expect(
55
+ config.config[:credentials][:aws_access_key_id]
56
+ ).to_equal 'honk-key'
57
+ end
58
+
59
+ describe 'with no log option' do
60
+ it 'should set a log to stdout' do
61
+ args = ['--config', config_file_path]
62
+ config = CloudDyndns::Config.new(args)
63
+
64
+ logdev = config.log.instance_variable_get :@logdev
65
+
66
+ expect(logdev.dev).to_equal $stdout
67
+ end
68
+ end
69
+
70
+ describe 'with a log option' do
71
+ it 'should create a file at that path' do
72
+ args = ['--config', config_file_path, '--log', test_log_path]
73
+ config = CloudDyndns::Config.new(args)
74
+
75
+ logdev = config.log.instance_variable_get :@logdev
76
+
77
+ expect(logdev.filename).to_equal test_log_path
78
+ end
79
+ end
80
+
81
+ end #valid options
82
+ end
83
+
@@ -0,0 +1,17 @@
1
+ :credentials:
2
+ :provider: 'AWS'
3
+ :aws_secret_access_key: 'honk-secret'
4
+ :aws_access_key_id: 'honk-key'
5
+ :zones:
6
+ - :domain: "looting.biz"
7
+ :targets:
8
+ - "phl.looting.biz"
9
+ - "*.phl.looting.biz"
10
+ :ttl: 600
11
+ - :domain: "narf.io"
12
+ :targets:
13
+ - "phl.narf.io"
14
+ - "*.phl.narf.io"
15
+
16
+
17
+
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'fileutils'
3
+ require 'bundler/setup'
4
+ require 'minitest/autorun'
5
+ require 'minitest/mock'
6
+ require 'minitest/reporters'
7
+ require 'minitest/matchers'
8
+ require 'minitest/spec/expect'
9
+ require 'mocha/setup'
10
+
11
+ require File.join(File.dirname(__FILE__), '..', 'cloud-dyndns')
12
+
13
+ Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
@@ -0,0 +1,313 @@
1
+ require 'ostruct'
2
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
3
+ root = CloudDyndns::ROOTDIR
4
+
5
+ config_file_path = File.join(root, 'spec', 'fixtures', 'config.yml')
6
+ test_log_path = File.join(root, 'spec', 'fixtures', 'test.log')
7
+
8
+ Fog.mock!
9
+ test_logger = Logger.new(test_log_path)
10
+ config = YAML.load_file(config_file_path)
11
+
12
+
13
+ describe 'CloudDyndns::Updater' do
14
+
15
+ after do
16
+ FileUtils.rm_rf test_log_path
17
+ end
18
+ describe '.domain_name_to_provider_name' do
19
+ it 'should return the given name appended with a "."' do
20
+ result = CloudDyndns::Updater.domain_name_to_provider_name('ox')
21
+
22
+ expect(result).to_equal 'ox.'
23
+ end
24
+ end
25
+
26
+ describe '#initialize' do
27
+ it 'should set a dns interface' do
28
+ dns = CloudDyndns::Updater.new(config, test_logger)
29
+
30
+ expect(dns.dns).to_respond_to :zones
31
+ end
32
+
33
+ it 'should set a log interface' do
34
+ dns = CloudDyndns::Updater.new(config, test_logger)
35
+
36
+ expect(dns.log.class).to_equal ::Logger
37
+ end
38
+
39
+ it 'should set zone_configs' do
40
+ dns = CloudDyndns::Updater.new(config, test_logger)
41
+
42
+ expect(dns.zone_configs).to_not_be_empty
43
+ expect(dns.zone_configs[0][:domain]).to_equal 'looting.biz'
44
+ expect(dns.zone_configs[0][:targets]).to_include 'phl.looting.biz'
45
+ end
46
+ end
47
+
48
+ describe '#find_zone_by_name' do
49
+ before do
50
+ @dns = CloudDyndns::Updater.new(config, test_logger)
51
+ end
52
+
53
+ describe 'when a zone is found' do
54
+ before do
55
+ @mock_zone = {}
56
+ @dns.expects(:find_zone_by_domain).
57
+ with('looting.biz').
58
+ returns(@mock_zone)
59
+ end
60
+
61
+ it 'should return that zone' do
62
+ expect(@dns.find_zone_by_name('looting.biz')).to_equal(@mock_zone)
63
+ end
64
+ end
65
+
66
+ describe 'when a zone is not found' do
67
+ before do
68
+ @dns.expects(:find_zone_by_domain).
69
+ with('looting.biz').
70
+ returns(nil)
71
+ end
72
+
73
+ it 'should create a new zone for the given domain' do
74
+ zone = @dns.find_zone_by_name('looting.biz')
75
+
76
+ expect(zone.domain).to_equal 'looting.biz'
77
+ end
78
+ end
79
+
80
+ it 'should return a zone' do
81
+ dns = CloudDyndns::Updater.new(config, test_logger)
82
+
83
+ result = dns.find_zone_by_name('honk')
84
+
85
+ expect(result).to_respond_to :records
86
+ end
87
+ end
88
+
89
+ describe '#find_zone_by_domain' do
90
+ before do
91
+ @dns = CloudDyndns::Updater.new(config, test_logger)
92
+ end
93
+
94
+ it 'should return a domain if it matches the domain with a trailing "."' do
95
+ zone = @dns.zones.create(
96
+ :domain => 'looting.biz.'
97
+ )
98
+ found_zone = @dns.find_zone_by_domain('looting.biz')
99
+
100
+ expect(found_zone.domain).to_equal 'looting.biz.'
101
+ end
102
+
103
+ it 'should return nil if not found' do
104
+ @dns.zones.clear()
105
+ found_zone = @dns.find_zone_by_domain('narf.io')
106
+
107
+ expect(found_zone).to_be_nil
108
+ end
109
+ end
110
+
111
+ describe '#create_zone_by_domain' do
112
+ before do
113
+ @dns = CloudDyndns::Updater.new(config, test_logger)
114
+ end
115
+
116
+ it 'should create a zone for the passed domain' do
117
+ zone = @dns.create_zone_by_domain('looting.biz')
118
+
119
+ expect(zone.domain).to_equal 'looting.biz'
120
+ end
121
+
122
+ it 'should set the :email as hostmaster@{{passed domain}}' do
123
+ @dns.stubs(:zones).returns(mock_zones = mock())
124
+
125
+ mock_zones.expects(:create).with(({
126
+ :domain => 'example.com',
127
+ :email => 'hostmaster@example.com'
128
+ })
129
+ )
130
+
131
+ @dns.create_zone_by_domain('example.com')
132
+ end
133
+ end
134
+
135
+ describe '#records_for_zone' do
136
+ before do
137
+ @dns = CloudDyndns::Updater.new(config, test_logger)
138
+ end
139
+
140
+ it 'should get the most up to date records for the zone' do
141
+ zone = mock()
142
+ records = mock()
143
+ zone.expects(:records).returns(records)
144
+ records.expects(:reload)
145
+
146
+ @dns.records_for_zone(zone)
147
+ end
148
+ end
149
+
150
+ describe '#find_record_for_zone_by_name' do
151
+ before do
152
+ @dns = CloudDyndns::Updater.new(config, test_logger)
153
+ end
154
+
155
+ it 'should return the zone if the name matches the given name + "."' do
156
+ zone = @dns.zones.create(:domain => 'horseblood.biz')
157
+ # openstruct because it behaves like the returned object
158
+ record = OpenStruct.new(
159
+ :name => 'phl.horseblood.biz.',
160
+ :value => "8.8.8.8",
161
+ :type => 'A'
162
+ )
163
+
164
+ @dns.stubs(:records_for_zone).returns([record])
165
+
166
+ expect(@dns.find_record_for_zone_by_name(zone, 'phl.horseblood.biz')).
167
+ to_equal record
168
+
169
+ end
170
+ end
171
+
172
+ describe '#update_record_for_zone' do
173
+
174
+ before do
175
+ @dns = CloudDyndns::Updater.new(config, test_logger)
176
+ end
177
+
178
+ describe 'when current_ip is empty' do
179
+ it 'should raise error' do
180
+ @dns.stubs(:get_ip_address).returns('')
181
+ zone = mock()
182
+ target = 'phl.looting.biz'
183
+
184
+ expect(lambda {@dns.update_record_for_zone(zone, target) }).
185
+ to_raise CloudDyndns::Updater::NoIPAddressError
186
+ end
187
+ end
188
+
189
+ describe 'when current ip is not empty' do
190
+ before do
191
+ @dns.stubs(:get_ip_address).returns('8.8.4.4')
192
+ end
193
+
194
+ describe 'when record does not exist' do
195
+ before do
196
+ @dns.stubs(:find_record_for_zone_by_name).returns(nil)
197
+ end
198
+
199
+ it 'should call create_record_for_zone' do
200
+ zone = mock()
201
+ expected_attrs = {
202
+ :name => 'ff.phl.looting.biz',
203
+ :value => '8.8.4.4',
204
+ :ttl => nil
205
+ }
206
+
207
+ @dns.expects(:create_record_for_zone).with(zone, expected_attrs)
208
+
209
+ @dns.update_record_for_zone(zone, 'ff.phl.looting.biz')
210
+ end
211
+ end
212
+
213
+ describe 'when record exists' do
214
+ before do
215
+ @mock_record = mock()
216
+ @dns.stubs(:find_record_for_zone_by_name).returns(@mock_record)
217
+ end
218
+
219
+ describe 'and matches current ip address' do
220
+ before do
221
+ @mock_zone = mock()
222
+ @mock_record = OpenStruct.new(
223
+ :name => 'f.phl.narf.io',
224
+ :value => ['8.8.4.4']
225
+ )
226
+ @dns.stubs(:get_ip_address).returns('8.8.4.4')
227
+ @dns.stubs(:find_record_for_zone_by_name).returns(@mock_record)
228
+ end
229
+
230
+ it 'should not call :create_record_for_zone' do
231
+ @dns.expects(:create_record_for_zone).never
232
+
233
+ @dns.update_record_for_zone(@mock_zone, 'f.phl.narf.io')
234
+ end
235
+
236
+ it 'should return record' do
237
+ result = @dns.update_record_for_zone(@mock_zone, 'f.phl.narf.io')
238
+
239
+ expect(result).to_equal @mock_record
240
+ end
241
+ end
242
+
243
+ describe 'and does not match current ip address' do
244
+ before do
245
+ @mock_zone = mock()
246
+ @mock_record = OpenStruct.new(
247
+ :name => 'f.phl.narf.io',
248
+ :value => ['192.168.1.12']
249
+ )
250
+ @dns.stubs(:get_ip_address).returns('10.30.1.2')
251
+ @dns.stubs(:find_record_for_zone_by_name).returns(@mock_record)
252
+ end
253
+
254
+ it 'should destroy the record and create a new record' do
255
+ @mock_record.expects(:destroy)
256
+
257
+ @dns.expects(:create_record_for_zone)
258
+ @dns.update_record_for_zone(@mock_zone, 'f.phl.narf.io')
259
+ end
260
+
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ describe '#update!' do
267
+ describe 'when domains to update are not set' do
268
+ before do
269
+ empty_config = config.clone
270
+ empty_config[:zones].first[:targets] = nil
271
+
272
+ @dns = CloudDyndns::Updater.new(empty_config, test_logger)
273
+ end
274
+
275
+ it 'should raise error' do
276
+ operation = lambda {
277
+ @dns.update!
278
+ }
279
+ expect(operation).to_raise CloudDyndns::Updater::NoTargetsSpecified
280
+ end
281
+ end
282
+
283
+ describe 'when domains are set' do
284
+ before do
285
+ @dns = CloudDyndns::Updater.new(config, test_logger)
286
+ end
287
+
288
+ it 'should update the domains for each zone' do
289
+ mock_zone1 = mock()
290
+ mock_zone2 = mock()
291
+
292
+ @dns.expects(:find_zone_by_name).
293
+ with('looting.biz').returns(mock_zone1)
294
+ @dns.expects(:find_zone_by_name).
295
+ with('narf.io').returns(mock_zone2)
296
+
297
+ @dns.expects(:update_record_for_zone).
298
+ with(mock_zone1, 'phl.looting.biz', config[:zones][0])
299
+ @dns.expects(:update_record_for_zone).
300
+ with(mock_zone1, '*.phl.looting.biz', config[:zones][0])
301
+
302
+ @dns.expects(:update_record_for_zone).
303
+ with(mock_zone2, 'phl.narf.io', config[:zones][1])
304
+ @dns.expects(:update_record_for_zone).
305
+ with(mock_zone2, '*.phl.narf.io', config[:zones][1])
306
+
307
+ @dns.update!
308
+ end
309
+
310
+ end
311
+ end
312
+
313
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloud-dyndns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nat Lownes
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-19 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fog
16
+ requirement: &2164884840 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - =
20
+ - !ruby/object:Gem::Version
21
+ version: 1.18.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2164884840
25
+ - !ruby/object:Gem::Dependency
26
+ name: bundler
27
+ requirement: &2164884340 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2164884340
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &2164883960 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2164883960
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest-reporters
49
+ requirement: &2164883480 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2164883480
58
+ - !ruby/object:Gem::Dependency
59
+ name: minitest-matchers
60
+ requirement: &2164880620 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2164880620
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: &2164880200 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2164880200
80
+ - !ruby/object:Gem::Dependency
81
+ name: mocha
82
+ requirement: &2164879700 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *2164879700
91
+ - !ruby/object:Gem::Dependency
92
+ name: minitest-spec-expect
93
+ requirement: &2164879000 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *2164879000
102
+ description: ! '
103
+
104
+ update DNS entries on cloud providers (Route 53 et al) based on your current external
105
+ IP address. think like a dyndns updater. no-ip, et al.
106
+
107
+ '
108
+ email:
109
+ - nat.lownes@gmail.com
110
+ executables:
111
+ - cloud-dyndns
112
+ extensions: []
113
+ extra_rdoc_files: []
114
+ files:
115
+ - .gitignore
116
+ - Gemfile
117
+ - Gemfile.lock
118
+ - Guardfile
119
+ - LICENSE.txt
120
+ - README.md
121
+ - Rakefile
122
+ - bin/cloud-dyndns
123
+ - cloud-dyndns.gemspec
124
+ - cloud-dyndns.rb
125
+ - doc/crontab_example
126
+ - lib/config.rb
127
+ - lib/updater.rb
128
+ - lib/version.rb
129
+ - spec/config_spec.rb
130
+ - spec/fixtures/config.yml
131
+ - spec/test_helper.rb
132
+ - spec/updater_spec.rb
133
+ homepage: https://github.com/natlownes/cloud-dyndns
134
+ licenses:
135
+ - MIT
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 1.8.10
155
+ signing_key:
156
+ specification_version: 3
157
+ summary: update DNS entries on cloud providers (Route 53 et al) based on your current
158
+ external IP address. think like a dyndns updater. no-ip, et al.
159
+ test_files:
160
+ - spec/config_spec.rb
161
+ - spec/fixtures/config.yml
162
+ - spec/test_helper.rb
163
+ - spec/updater_spec.rb