r53z 0.2.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.
data/LICENSE.txt ADDED
@@ -0,0 +1,16 @@
1
+ Name: r53z
2
+ Copyright (c) 2016 Joe Cooper
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # R53z
2
+
3
+ A simple CLI, REPL, and library for managing Route 53. It's primary purpose is to back up and restore Route 53 zones. It can write zones to files in JSON format. It also provides a simple API (not a whole lot easier than the Amazon official API, but for backups and restores, it's much easier to script with and removes tons of boilerplate).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'r53z'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install r53z
20
+
21
+ ## Usage
22
+
23
+ Configure a credentials file in `~/.aws/credentials` (this should be an INI file; same as several other AWS utilities expect). It'll look something like this:
24
+
25
+ ```ini
26
+ [default]
27
+ aws_access_key_id = ACCESS_KEY_ID
28
+ aws_secret_access_key = SECRET_ACCESS_KEY
29
+ region=us-east-1
30
+ ```
31
+
32
+ You can use the `--section` option to choose which section to use from the credentials file, and the credentials file can be specified with the `--credentials` option. Region is irrelevant with Route 53, but the aws-sdk weirdly still requires it be present.
33
+
34
+ ```
35
+ Usage: r53z [options] [args...]
36
+
37
+ Simple CLI to manage, backup, and restore, Route 53 zones
38
+
39
+ v0.2.0
40
+
41
+ Options:
42
+ -h, --help Show command line help
43
+ -x, --export Export zones to files in specified directory, optionally specify one or more zones.
44
+ -r, --restore Restore zone from directory, optionally specify one or more zones.
45
+ -l, --list List name and ID of one or all zones.
46
+ -s, --record-sets List record sets for the given zone.
47
+ -c, --create NAME Create a zone with the given name and optional --comment and --delegation-set.
48
+ -n, --comment COMMENT Optional comment when creating a zone.
49
+ -d, --delete Delete one or more zone(s) by name (WARNING: No confirmation!)
50
+ -C, --credentials File containing credentials information.
51
+ -u, --section Section (user) in the credentials file to use.
52
+ -g, --delegation-set ID Delegation set ID to use for various operations.
53
+ -t, --list-delegation-sets List delegation set for named zone, or all sets if no zone specified.
54
+ -D, --delete-delegation-sets Delete one or more delegation sets by ID (WARNING: No confirmation!
55
+ -N, --name-servers ID List name servers for delegation set.
56
+ --version Show help/version info
57
+ --log-level LEVEL Set the logging level
58
+ (debug|info|warn|error|fatal)
59
+ (Default: info)
60
+
61
+ ```
62
+
63
+ ### Command Line Options
64
+
65
+ #### --export|-x {path} [zones]
66
+
67
+ Export one or more zones to the named directory.
68
+
69
+ Requires a directory path (e.g. /tmp/zonedumps), and optionally one or more zone names. All zones will be exported if none are specified. If a delegation set ID is given, only zones that share the given delegation set will be exported.
70
+
71
+ Two files will be generated in the directory specified, one for the zone metadata (with a zoneinfo.json extension) and the zone records information (with a .json extension). The zone metadata file will contain all of the information needed to recreate the zone, incluing the delegation set ID. And, the record set file will contain all of the record sets needed to repopulate the zone; SOA and NS records are not restored as they are defined by the delegation set, rather than by records within the zone.
72
+
73
+ ##### Example
74
+
75
+ ```
76
+ $ r53z --export /home/joe/zones swelljoe.com
77
+ ```
78
+
79
+ #### --restore|-r {path} [zones]
80
+
81
+ Restore one or more zones from files in the named directory.
82
+
83
+ Requires a directory path, and optionally one or more zone names. If no names are given, all files in the directory will be restored. If a delegation set is specified, all zones will be added to the delegation set specified. (The zone info and the record sets don't contain delegation set information, making delegation set selection on restore a little difficult to automate.)
84
+
85
+ If `--delegation-set` is specified on the command line, it will override the delegation-set information provided in the zoneinfo file. This can be used if the delegation set specified in the file is no longer available, for some reason (deletion, migrating to a new account, etc.).
86
+
87
+ ##### Example
88
+
89
+ ```
90
+ $ r53z --restore /home/joe/zones swelljoe.com
91
+ ```
92
+
93
+ #### --list|-l [--delegation-set ID] [zones]
94
+
95
+ List hosted zones. List can be restricted to just the listed zones, or to a given delegation set. Output is JSON encoded, and will contain the name and ID of each zone. If no zones are specified, all zones will be listed.
96
+
97
+ #### --create|-c NAME [--comment COMMENT] [--delegation-set ID]
98
+
99
+ Create zone of the NAME provided. An optional command an delegation set ID may be provided.
100
+
101
+ ##### Example
102
+
103
+ ```
104
+ $ r53z --create swelljoe.com --comment "My domain"
105
+ ```
106
+
107
+ #### --delete|-d {zone}
108
+
109
+ Delete one or more zones. Argument is the name of the zone, or zones, to delete. This command deletes the record sets for the zone first, and then deletes the zone itself (a zone with records cannot be deleted). There is no confirmation step for this option.
110
+
111
+ ##### Example
112
+
113
+ ```
114
+ $ r53z --delete swelljoe.com virtualmin.com
115
+ ```
116
+
117
+ #### --credentials|-c {path/filename}
118
+
119
+ Specify the credentials configuration file on the command line. The file must be an INI file. By default, it will look for a file in ~/.aws/credentials (which is common across several AWS management tools). You can use the `--section` option to choose what section of the file to use.
120
+
121
+ ## Development
122
+
123
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
124
+
125
+ To install this gem onto your local machine, run `bundle exec rake install`.
126
+
127
+ #### Running Tests
128
+
129
+ To run the full test suite, you need a read/write capable account. There must be a configuration file in `test/data/secret-credentials` containing a `[default]` section with your keys. The format of this file is, as above, a .ini file.
130
+
131
+ To run all tests:
132
+
133
+ ```
134
+ $ rake test
135
+ ```
136
+
137
+ This will create a few test zones in your account, but unless something goes wrong during the test, they will be removed immediately after, never triggering billing from Amazon. The zones will have somewhat randomly generated names, so they should never clash with existing names (but you may wish to create a non-production account just for testing).
138
+
139
+ ## Contributing
140
+
141
+ Bug reports and pull requests are welcome on GitHub at https://github.com/swelljoe/r53z.
data/README.rdoc ADDED
@@ -0,0 +1 @@
1
+ See README.md
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ def dump_load_path
2
+ puts $LOAD_PATH.join("\n")
3
+ found = nil
4
+ $LOAD_PATH.each do |path|
5
+ if File.exists?(File.join(path,"rspec"))
6
+ puts "Found rspec in #{path}"
7
+ if File.exists?(File.join(path,"rspec","core"))
8
+ puts "Found core"
9
+ if File.exists?(File.join(path,"rspec","core","rake_task"))
10
+ puts "Found rake_task"
11
+ found = path
12
+ else
13
+ puts "!! no rake_task"
14
+ end
15
+ else
16
+ puts "!!! no core"
17
+ end
18
+ end
19
+ end
20
+ if found.nil?
21
+ puts "Didn't find rspec/core/rake_task anywhere"
22
+ else
23
+ puts "Found in #{path}"
24
+ end
25
+ end
26
+ require 'bundler'
27
+ require 'rake/clean'
28
+
29
+ require 'rake/testtask'
30
+
31
+ require 'cucumber'
32
+ require 'cucumber/rake/task'
33
+ gem 'rdoc' # we need the installed RDoc gem, not the system one
34
+ require 'rdoc/task'
35
+
36
+ include Rake::DSL
37
+
38
+ Bundler::GemHelper.install_tasks
39
+
40
+
41
+ Rake::TestTask.new do |t|
42
+ t.pattern = 'test/tc_*.rb'
43
+ t.warning = false # Get rid of purious warnings from the AWS and Methadone libs?
44
+ end
45
+
46
+
47
+ CUKE_RESULTS = 'results.html'
48
+ CLEAN << CUKE_RESULTS
49
+ Cucumber::Rake::Task.new(:features) do |t|
50
+ t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty --no-source -x"
51
+ t.fork = false
52
+ end
53
+
54
+ Rake::RDocTask.new do |rd|
55
+
56
+ rd.main = "README.rdoc"
57
+
58
+ rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
59
+ end
60
+
61
+ task :default => [:test,:features]
62
+
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "r53z"
5
+ require "pry"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+ creds = R53z::Config.new()
10
+ client = R53z::Client.new('default', creds)
11
+
12
+ # (If you use this, don't forget to add pry to your Gemfile!)
13
+ binding.pry
14
+
15
+ #require "irb"
16
+ #IRB.start
data/bin/r53z ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'methadone'
5
+ require 'r53z.rb'
6
+
7
+ class App
8
+ include Methadone::Main
9
+ include Methadone::CLILogging
10
+
11
+ main do |*args|
12
+ help_now! "No options provided." if options.empty?
13
+ R53z::Cli.new(:options => options, :args => args)
14
+ end
15
+
16
+ # Command line interface specification
17
+ description "Simple CLI to manage, backup, and restore, Route 53 zones"
18
+ #
19
+ # Accept flags via:
20
+ on("-x", "--export", "Export zones to files in specified directory, optionally specify one or more zones")
21
+ on("-r", "--restore", "Restore zone from directory, optionally specify one or more zones")
22
+ on("-l", "--list", "List name and ID of one or all zones")
23
+ on("-s", "--record-sets", "List record sets for the given zone")
24
+ on("-d", "--delete", "Delete one or more zone(s) by name (WARNING: No confirmation!)")
25
+ on("-c", "--credentials", "File containing credentials information")
26
+ on("-u", "--section", "Section (user) in the credentials file to use")
27
+ on("-g ID", "--delegation-set", "Delegation set ID to use for various operations")
28
+ on("-t", "--list-delegation-sets", "List all delegation sets")
29
+ on("-n ID", "--name-servers", "List name servers for delegation set")
30
+ # args greedily grabs any files or zones listed after the options
31
+ arg(:args, :any)
32
+
33
+ version R53z::VERSION
34
+
35
+ use_log_level_option :toggle_debug_on_signal => 'USR1'
36
+
37
+ go!
38
+ end
39
+
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/r53z.rb ADDED
@@ -0,0 +1,10 @@
1
+ require_relative "r53z/version"
2
+ require "methadone"
3
+ require "aws-sdk"
4
+ require_relative "r53z/config"
5
+ require_relative "r53z/client"
6
+ require_relative "r53z/file"
7
+ require_relative "r53z/cli"
8
+
9
+ module R53z
10
+ end
data/lib/r53z/cli.rb ADDED
@@ -0,0 +1,117 @@
1
+ require 'json'
2
+
3
+ module R53z
4
+ class Cli
5
+ include Methadone::Main
6
+ include Methadone::CLILogging
7
+
8
+ def initialize(options:, args:)
9
+ section = options[:section] || 'default'
10
+ config_file = options[:credentials]
11
+ creds = R53z::Config.new(config_file)
12
+ @client = R53z::Client.new(section, creds)
13
+
14
+ # XXX Dispath table seems smarter...can't figure out how to calls methods based
15
+ # directly on hash keys at the moment.
16
+ if options[:export]
17
+ help_now! "Export requires a directory path for zone files" if args.length < 1
18
+ export(:options => options, :args => args)
19
+ end
20
+
21
+ if options[:restore]
22
+ if args.empty?
23
+ help_now! "Restore requires a directory containing zone files and optionally one or more zones to restore"
24
+ end
25
+ restore(:options => options, :args => args)
26
+ end
27
+
28
+ if options[:list]
29
+ list(options: options, args: args)
30
+ end
31
+
32
+ if options[:delete]
33
+ if args.empty?
34
+ help_now! "Delete requires one or more zone names"
35
+ end
36
+ args.each do |name|
37
+ @client.delete(name)
38
+ end
39
+ end
40
+
41
+ if options['list-delegation-sets']
42
+ sets = @client.list_delegation_sets
43
+ sets.each do |set|
44
+ puts JSON.pretty_generate(set.to_h)
45
+ end
46
+ end
47
+
48
+ if options['record-sets']
49
+ if args.empty?
50
+ help_now! "List record sets requires one or more zone names"
51
+ end
52
+ args.each do |name|
53
+ sets = @client.record_list(@client.get_zone_id(name))
54
+ puts JSON.pretty_generate(sets)
55
+ end
56
+ end
57
+
58
+ if options['name-servers']
59
+ dset = @client.get_delegation_set(id: options['name-servers'])
60
+ puts JSON.pretty_generate(dset.delegation_set[:name_servers])
61
+ end
62
+ end
63
+
64
+ def export(options:, args:)
65
+ path = args.shift
66
+ # If no zones, dump all zones
67
+ zones = []
68
+ # One zone, multiple, or all?
69
+ if args.empty?
70
+ @client.list.each do |zone|
71
+ zones.push(zone[:name])
72
+ end
73
+ else
74
+ zones = args
75
+ end
76
+
77
+ zones.each do |name|
78
+ @client.dump(path, name)
79
+ end
80
+ end
81
+
82
+ def restore(options:, args:)
83
+ path = args.shift
84
+ # If no zones, restore all zones in directory
85
+ zones = []
86
+ if args.empty?
87
+ # derive list of zones from files in path
88
+ zones = Dir[File.join(path, "*.json")].reject {|n| n.match("zoneinfo")}
89
+ else
90
+ # restore the ones specified
91
+ args.each do |zone|
92
+ zones.push(zone)
93
+ end
94
+ end
95
+
96
+ zones.each do |zone|
97
+ @client.restore(path, zone)
98
+ end
99
+ end
100
+
101
+ def list(options:, args:)
102
+ if args.any?
103
+ args.each do |name|
104
+ puts JSON.pretty_generate(
105
+ @client.list(
106
+ :name => name,
107
+ :delegation_set_id => options['delegation-set']))
108
+ end
109
+ else
110
+ puts JSON.pretty_generate(
111
+ @client.list(:delegation_set_id => options['delegation-set'])
112
+ )
113
+ end
114
+ end
115
+ end
116
+ end
117
+
@@ -0,0 +1,220 @@
1
+ module R53z
2
+ class Client
3
+ include Methadone::Main
4
+ include Methadone::CLILogging
5
+ attr_accessor :client
6
+
7
+ def initialize(section, creds)
8
+ @client = Aws::Route53::Client.new(
9
+ access_key_id: creds[section]['aws_access_key_id'],
10
+ secret_access_key: creds[section]['aws_secret_access_key'],
11
+ region: creds[section]['region']
12
+ )
13
+ end
14
+
15
+ # list one or all zones by name and ID
16
+ def list(name: nil, delegation_set_id: nil)
17
+ begin
18
+ zones = self.client.list_hosted_zones(
19
+ delegation_set_id: delegation_set_id
20
+ )['hosted_zones']
21
+ rescue Aws::Route53::Errors::ServiceError
22
+ error "Failed to list zones" # XXX How do we get AWS error message out of it?
23
+ end
24
+
25
+ rv = []
26
+ zones.each do |zone|
27
+ if name
28
+ unless name[-1] == '.'
29
+ name = name + '.'
30
+ end
31
+ unless name == zone[:name]
32
+ next
33
+ end
34
+ end
35
+ rv.push({:name => zone[:name], :id => zone[:id]})
36
+ end
37
+ rv
38
+ end
39
+
40
+ # Create zone with record(s) from an info and records hash
41
+ def create(info:, records:)
42
+ rv = {} # pile up the responses in a single hash
43
+ #if self.list(info[:name]).any?
44
+ # error(info[:name] + "exists")
45
+ #end
46
+ # XXX: AWS sends out a data structure with config:, but expects
47
+ # hosted_zone_config on create/restore. argh.
48
+ # XXX: also, private_zone is not accepted here for some reason
49
+ zone = info[:hosted_zone]
50
+ zone_data = {
51
+ :name => zone[:name],
52
+ :caller_reference => 'r53z-create-' + self.random_string,
53
+ :hosted_zone_config => {
54
+ :comment => zone[:config][:comment]
55
+ }
56
+ }
57
+ # command line overrides everything else
58
+ if options['delegation-set']
59
+ zone_data[:delegation_set_id] = options['delegation-set']
60
+ elsif info[:delegation_set] and info[:delegation_set][:id]
61
+ zone_data[:delegation_set_id] = info[:delegation_set][:id]
62
+ end
63
+ zone_resp = self.client.create_hosted_zone(zone_data)
64
+ rv[:hosted_zone_resp] = zone_resp
65
+ rv[:record_set_resp] = []
66
+ records.each do |record|
67
+ # skip these, as they are handled separately (delegation set?)
68
+ unless (record[:type] == "NS" || record[:type] == "SOA")
69
+ record_resp = self.client.change_resource_record_sets({
70
+ :hosted_zone_id => zone_resp[:hosted_zone][:id],
71
+ :change_batch => {
72
+ :changes => [
73
+ {
74
+ :action => "CREATE",
75
+ :resource_record_set => record
76
+ }
77
+ ]
78
+ }
79
+ })
80
+ rv[:record_set_resp].push(record_resp)
81
+ end
82
+ end
83
+ return rv
84
+ end
85
+
86
+ # delete a zone by name
87
+ def delete(name)
88
+ # get the ID
89
+ zone_id = self.list(:name => name).first[:id]
90
+ self.delete_all_rr_sets(zone_id)
91
+ client.delete_hosted_zone(:id => zone_id)
92
+ end
93
+
94
+ # delete all of the resource record sets in a zone (this is required to delete
95
+ # a zone
96
+ def delete_all_rr_sets(zone_id)
97
+ self.record_list(zone_id).reject do |rs|
98
+ (rs[:type] == "NS" || rs[:type] == "SOA")
99
+ end.each do |record_set|
100
+ self.client.change_resource_record_sets({
101
+ :hosted_zone_id => zone_id,
102
+ :change_batch => {
103
+ :changes => [{
104
+ :action=> "DELETE",
105
+ :resource_record_set => record_set
106
+ }]
107
+ }
108
+ })
109
+ end
110
+ end
111
+
112
+ # dump a zone to a direcory. Will generate two files; a zoneinfo file and a
113
+ # records file.
114
+ def dump(dirpath, name)
115
+ # Get the ID
116
+ zone_id = self.list(:name => name).first[:id]
117
+
118
+ # normalize name
119
+ unless name[-1] == '.'
120
+ name = name + '.'
121
+ end
122
+
123
+ # dump the record sets
124
+ R53z::JsonFile.write_json(
125
+ path: File.join(dirpath, name),
126
+ data: self.record_list(zone_id))
127
+
128
+ # Dump the zone metadata, plus the delegated set info
129
+ out = { :hosted_zone =>
130
+ self.client.get_hosted_zone({ :id => zone_id}).hosted_zone.to_h,
131
+ :delegation_set =>
132
+ self.client.get_hosted_zone({:id => zone_id}).delegation_set.to_h
133
+ }
134
+
135
+ R53z::JsonFile.write_json(
136
+ path: File.join(dirpath, name + "zoneinfo"),
137
+ data: out)
138
+ #data: self.client.get_hosted_zone({
139
+ # :id => zone_id}).hosted_zone.to_h)
140
+ end
141
+
142
+ # Restore a zone from the given path. It expects files named
143
+ # zone.zoneinfo.json and zone.json
144
+ def restore(path, domain)
145
+ # normalize domain
146
+ unless domain[-1] == '.'
147
+ domain = domain + '.'
148
+ end
149
+ # Load up the zone info file
150
+ file = File.join(path, domain)
151
+ info = R53z::JsonFile.read_json(path: file + "zoneinfo")
152
+ records = R53z::JsonFile.read_json(path: file)
153
+ # create the zone and the record sets
154
+ self.create(:info => info, :records => records)
155
+ end
156
+
157
+ def record_list(zone_id)
158
+ records = self.client.list_resource_record_sets(hosted_zone_id: zone_id)
159
+ rv = []
160
+ records[:resource_record_sets].each do |record|
161
+ rv.push(record.to_h)
162
+ end
163
+ rv
164
+ end
165
+
166
+ # create a new delegation set, optionally associated with an existing zone
167
+ def create_delegation_set(zone_id = nil)
168
+ self.client.create_reusable_delegation_set({
169
+ caller_reference: 'r53z-create-del-set-' + self.random_string,
170
+ hosted_zone_id: zone_id
171
+ })
172
+ end
173
+
174
+ # list all delegation sets
175
+ def list_delegation_sets
176
+ resp = self.client.list_reusable_delegation_sets({})
177
+ return resp.delegation_sets
178
+ end
179
+
180
+ # get details of a delegation set specified by ID, incuding name servers
181
+ def get_delegation_set(id)
182
+ self.client.get_reusable_delegation_set({
183
+ id: id
184
+ })
185
+ end
186
+
187
+ # delete a delegation set by ID or name
188
+ def delete_delegation_set(id: nil, name: nil)
189
+ if name and not id
190
+ id = get_delegation_set_id(name)
191
+ end
192
+ self.client.delete_reusable_delegation_set({
193
+ id: id
194
+ })
195
+ end
196
+
197
+ # Get delegation set ID for the given zone
198
+ def get_delegation_set_id(name)
199
+ begin
200
+ zone_id = self.list(:name => name).first[:id]
201
+ rescue
202
+ return nil
203
+ end
204
+ return self.client.get_hosted_zone({
205
+ id: zone_id
206
+ }).delegation_set[:id]
207
+ end
208
+
209
+ # Get the zone id from name
210
+ def get_zone_id(name)
211
+ return self.list(:name => name).first[:id]
212
+ end
213
+
214
+ # random string generator helper function
215
+ def random_string(len=16)
216
+ rand(36**len).to_s(36)
217
+ end
218
+ end
219
+ end
220
+