cloud-dyndns 0.0.1

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.
@@ -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