cknife 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6a8aeeb2e92a76ac079da5b63fe8e9403b451d6e
4
+ data.tar.gz: a74e4274ccc2b39ab451c06d3069ef649884d80f
5
+ SHA512:
6
+ metadata.gz: edd89d9f659ef18a4c88b4b91b57f13252c6caa48d1443ac0a99ddbf5e8b5e183d5c8bfd66b7a3d7edcd60548d8c268996760da19638c63462faae89ac5febe0
7
+ data.tar.gz: 626d66f0e47119dae79626e6819196b75356d4b49a893edb04a2a0bc9867011ba32f4f2062223acdee678dc9c440c0f5e1ce8e5de8406a879854de5d76c8331a
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rest-client"
4
+ gem "nokogiri"
5
+ gem "i18n"
6
+ gem "activesupport"
7
+ gem "thor"
8
+ gem "builder"
9
+ gem "fog", ">= 1.15.0"
10
+ gem "unf"
11
+
12
+ # Add dependencies to develop your gem here.
13
+ # Include everything needed to run rake, tests, features, etc.
14
+ group :development do
15
+ gem "bundler", "~> 1.0"
16
+ gem "jeweler", "~> 2.0.1"
17
+ end
@@ -0,0 +1,86 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.0.7)
5
+ addressable (2.3.6)
6
+ builder (3.0.0)
7
+ descendants_tracker (0.0.4)
8
+ thread_safe (~> 0.3, >= 0.3.1)
9
+ excon (0.27.6)
10
+ faraday (0.9.0)
11
+ multipart-post (>= 1.2, < 3)
12
+ fog (1.17.0)
13
+ builder
14
+ excon (~> 0.27.0)
15
+ formatador (~> 0.2.0)
16
+ mime-types
17
+ multi_json (~> 1.0)
18
+ net-scp (~> 1.1)
19
+ net-ssh (>= 2.1.3)
20
+ nokogiri (~> 1.5)
21
+ ruby-hmac
22
+ formatador (0.2.4)
23
+ git (1.2.6)
24
+ github_api (0.11.3)
25
+ addressable (~> 2.3)
26
+ descendants_tracker (~> 0.0.1)
27
+ faraday (~> 0.8, < 0.10)
28
+ hashie (>= 1.2)
29
+ multi_json (>= 1.7.5, < 2.0)
30
+ nokogiri (~> 1.6.0)
31
+ oauth2
32
+ hashie (3.0.0)
33
+ highline (1.6.21)
34
+ i18n (0.6.0)
35
+ jeweler (2.0.1)
36
+ builder
37
+ bundler (>= 1.0)
38
+ git (>= 1.2.5)
39
+ github_api
40
+ highline (>= 1.6.15)
41
+ nokogiri (>= 1.5.10)
42
+ rake
43
+ rdoc
44
+ jwt (1.0.0)
45
+ mime-types (1.16)
46
+ mini_portile (0.5.2)
47
+ multi_json (1.8.2)
48
+ multi_xml (0.5.5)
49
+ multipart-post (2.0.0)
50
+ net-scp (1.1.2)
51
+ net-ssh (>= 2.6.5)
52
+ net-ssh (2.7.0)
53
+ nokogiri (1.6.0)
54
+ mini_portile (~> 0.5.0)
55
+ oauth2 (0.9.4)
56
+ faraday (>= 0.8, < 0.10)
57
+ jwt (~> 1.0)
58
+ multi_json (~> 1.3)
59
+ multi_xml (~> 0.5)
60
+ rack (~> 1.2)
61
+ rack (1.5.2)
62
+ rake (0.9.6)
63
+ rdoc (4.0.0)
64
+ rest-client (1.6.3)
65
+ mime-types (>= 1.16)
66
+ ruby-hmac (0.4.0)
67
+ thor (0.14.6)
68
+ thread_safe (0.3.4)
69
+ unf (0.1.3)
70
+ unf_ext
71
+ unf_ext (0.0.6)
72
+
73
+ PLATFORMS
74
+ ruby
75
+
76
+ DEPENDENCIES
77
+ activesupport
78
+ builder
79
+ bundler (~> 1.0)
80
+ fog (>= 1.15.0)
81
+ i18n
82
+ jeweler (~> 2.0.1)
83
+ nokogiri
84
+ rest-client
85
+ thor
86
+ unf
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 The Mike De La Loza Company
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,169 @@
1
+ Cali Army Knife
2
+ ==============
3
+
4
+ An Amazon Web Services S3 command line tool, and a few other command
5
+ line tools. Written in Ruby with Thor. It depends on the Fog gem for
6
+ all of its S3 operations.
7
+
8
+ Uses multipart uploads with a chunksize of 10 megabytes to keep RAM
9
+ usage down.
10
+
11
+ The premier feature of the tool is the "upsync" command, which can be
12
+ used to run a backups schedule with multiple classes of files
13
+ (partitioned by a glob pattern). **It is your responsibility to
14
+ generate one uniquely-named backup file per day**, as this tool does
15
+ not do that part for you.
16
+
17
+ If you *don't* use the `backups-retain` option, then its like a very
18
+ weak **rsync** that can upload from a local filesystem into a bucket.
19
+ Which is also pretty useful.
20
+
21
+ [Github Link](https://github.com/mikedll/cali-army-knife)
22
+
23
+ Examples:
24
+
25
+ # download entire my-photos bucke to CWD
26
+ > aws.rb download my-photos
27
+
28
+ # upload and sync /tmp/*.sql into my-frog-app-backups
29
+ # bucket. Treat the files as backup files, and keep one backup
30
+ # file for each of the last 5 months, 10 weeks, and 30 days.
31
+ > aws.rb upsync my-frog-app-backups ./tmp --glob "*.sql" --noprompt --backups-retain true --months-retain 5 --weeks-retain 10 --days-retain 30
32
+
33
+ # as above, but now do redis backup files (./tmp/*.rdb). these will not produce
34
+ # namespace collisions with the sql files, and thus the same bucket
35
+ # can be used to store backups for both.
36
+ > aws.rb upsync my-frog-app-backups ./tmp --glob "*.rdb" --noprompt --backups-retain true --months-retain 5 --weeks-retain 10 --days-retain 30
37
+
38
+ # DO NOT DO THIS INSTEAD OF THE ABOVE 2 COMMANDS, THINKING IT WILL
39
+ # TREAT .SQL AND .RDB FILES SEPARATELY. INSTEAD, YOU WILL LOSE
40
+ # SOME OF YOUR BACKUP FILES.
41
+ > aws.rb upsync my-frog-app-backups ./tmp --glob "*" --noprompt --backups-retain true --months-retain 5 --weeks-retain 10 --days-retain 30
42
+
43
+ # Dry run mode. Try one of the prior backups retain commands, but
44
+ # let's see what will happen, first.
45
+ > aws.rb upsync my-frog-app-backups ./tmp --glob "*.sql" --noprompt --backups-retain true --months-retain 5 --weeks-retain 10 --days-retain 30 --dry-run
46
+
47
+
48
+ # Getting Started / Installation
49
+
50
+ Clone this repo, `bundle` the Gemfile, and make the aws.rb invokable
51
+ either by adding it to your PATH or making a bash alias to it, using
52
+ an absolute path to it, or something. I dunno, I'm working on that.
53
+
54
+ # aws
55
+
56
+ Tasks:
57
+ aws.rb afew [BUCKET_NAME] # Show first 5 files in bucket
58
+ aws.rb create [BUCKET_NAME] # Create a bucket
59
+ aws.rb create_cloudfront [BUCKET_NAME] # Create a cloudfront distribution (a CDN)
60
+ aws.rb delete [BUCKET_NAME] # Destroy a bucket
61
+ aws.rb download [BUCKET_NAME] # Download all files in a bucket to CWD. Or one file.
62
+ aws.rb help [TASK] # Describe available tasks or one specific task
63
+ aws.rb list # Show all buckets
64
+ aws.rb list_cloudfront # List cloudfront distributions (CDNs)
65
+ aws.rb list_servers # Show all servers
66
+ aws.rb show [BUCKET_NAME] # Show info about bucket
67
+ aws.rb start_server [SERVER_ID] # Start a given EC2 server
68
+ aws.rb stop_server [SERVER_ID] # Stop a given EC2 server (does not terminate it)
69
+ aws.rb upsync [BUCKET_NAME] [DIRECTORY] # Push local files matching glob PATTERN into bucket. Ignore unchanged files.
70
+
71
+ ## Synchronizing a local directory's files with an Amazon S3 Bucket
72
+
73
+ Usage:
74
+ aws.rb upsync [BUCKET_NAME] [DIRECTORY]
75
+
76
+ Options:
77
+ [--public]
78
+ [--region=REGION]
79
+ # Default: us-east-1
80
+ [--noprompt=NOPROMPT]
81
+ [--glob=GLOB]
82
+ # Default: **/*
83
+ [--backups-retain]
84
+ [--days-retain=N]
85
+ # Default: 30
86
+ [--months-retain=N]
87
+ # Default: 3
88
+ [--weeks-retain=N]
89
+ # Default: 5
90
+ [--dry-run]
91
+
92
+ The glob allows you to determine whether you want to recursively
93
+ upload an entire directory, or just a set of *.dat or *.sql files,
94
+ ignoring whatever else may be in the specified directory. This glob
95
+ pattern is appended to the directory you specify.
96
+
97
+ For determining whether to upload a file, first the mod time is used,
98
+ and if that match fails, an md5 checksum comparison is used.
99
+
100
+ The file's local modification time, from the file system from which it
101
+ was uploaded, is used to determined whether it qualifies for retention
102
+ in the backup program you specify.
103
+
104
+ This info is uploaded with the file to Amazon's S3 servers when the
105
+ file is uploaded, in the S3 file metadata. Without this, S3 uses a
106
+ modtime that is equal to when the file was last uploaded, which is not
107
+ comparable to the file's local mod time.
108
+
109
+ ## Dumping an Amazon S3 bucket
110
+
111
+ Sometimes you want to download an entire S3 bucket to your local
112
+ directory - a set of photos, for example.
113
+
114
+ > aws.rb help download
115
+ Usage:
116
+ aws.rb download [BUCKET_NAME]
117
+
118
+ Options:
119
+ [--region=REGION]
120
+ # Default: us-east-1
121
+ [--one=ONE]
122
+
123
+ Download all files in a bucket to CWD. Or one file.
124
+
125
+
126
+ ## Key and Secret Configuration
127
+
128
+ In order of priority, Setup your key and secret with:
129
+
130
+ - $CWD/cknife.yml
131
+ - $CWD/tmp/cknife.yml
132
+ - environment variables: `KEY`, `SECRET`
133
+ - environment variablse: `AMAZON_ACCESS_KEY_ID`, `AMAZON_SECRET_ACCESS_KEY`
134
+
135
+ The format of your cknife.yml must be like so:
136
+
137
+ ---
138
+ key: AKIAblahblahb...
139
+ secret: 8xILhOsecretsecretsecretsecret...
140
+
141
+
142
+ # zerigo
143
+
144
+ These tasks can be used to manage your DNS via Zerigo. They changed
145
+ their rates drastically with little notice in January of 2014, so I
146
+ switched to DNS Simple and don't use this much anymore.
147
+
148
+ > zerigo
149
+ Tasks:
150
+ zerigo.rb create [HOST_NAME] # Create a host
151
+ zerigo.rb delete [ID] # Delete an entry by id
152
+ zerigo.rb help [TASK] # Describe available tasks or one specific task
153
+ zerigo.rb list # List available host names.
154
+
155
+ # dub
156
+
157
+ Like du, but sorts your output by size. This helps you determine
158
+ which directories are taking up the most space:
159
+
160
+ > dub
161
+ 37.0G .
162
+ 23.0G ./Personal
163
+ 14.0G ./Library
164
+ 673.0M ./Work
165
+ 0.0B ./Colloquy Transcripts
166
+
167
+ ## options
168
+
169
+ -c Enable colorized output.
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "cknife"
18
+ gem.homepage = "http://github.com/mikedll/cknife"
19
+ gem.license = "MIT"
20
+ gem.summary = "Cali Army Knife"
21
+ gem.description = "An Amazon Web Services S3 command line tool, and a few other command line tools."
22
+ gem.email = "mikedll@mikedll.com"
23
+ gem.authors = ["Mike De La Loza"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ task :default => :spec
29
+
30
+ require 'rdoc/task'
31
+ Rake::RDocTask.new do |rdoc|
32
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
33
+
34
+ rdoc.rdoc_dir = 'rdoc'
35
+ rdoc.title = "cknife #{version}"
36
+ rdoc.rdoc_files.include('README*')
37
+ rdoc.rdoc_files.include('lib/**/*.rb')
38
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENV['BUNDLE_GEMFILE'] = File.join( File.dirname( File.expand_path( __FILE__ ) ), "../Gemfile" )
4
+
5
+ require 'rubygems'
6
+ require 'bundler'
7
+ Bundler.require
8
+
9
+ require 'active_support/all'
10
+ require 'zlib'
11
+ require 'digest/md5'
12
+
13
+ class Aws < Thor
14
+
15
+ FILE_BUFFER_SIZE = 10.megabytes
16
+ LOCAL_MOD_KEY = "x-amz-meta-mtime"
17
+ EPSILON = 1.second
18
+
19
+ no_tasks do
20
+
21
+ def config
22
+ return @config if @config
23
+
24
+ @config = {
25
+ :key => ENV["KEY"] || ENV['AMAZON_ACCESS_KEY_ID'],
26
+ :secret => ENV["SECRET"] || ENV['AMAZON_SECRET_ACCESS_KEY']
27
+ }
28
+
29
+ config_file = nil
30
+ Pathname.new(Dir.getwd).tap do |here|
31
+ config_file = [["cknife.yml"], ["tmp", "cknife.yml"]].map { |args|
32
+ here.join(*args)
33
+ }.select { |path|
34
+ File.exists?(path)
35
+ }.first
36
+ end
37
+
38
+ if config_file
39
+ begin
40
+ @config.merge!(YAML.load(config_file.read).symbolize_keys!)
41
+ rescue
42
+ say ("Found, but could not parse config: #{config_file}")
43
+ end
44
+ end
45
+
46
+ @config
47
+ end
48
+
49
+ def fog_opts
50
+ opts = {
51
+ :provider => 'AWS',
52
+ :aws_access_key_id => config[:key],
53
+ :aws_secret_access_key => config[:secret]
54
+ }
55
+ opts.merge!({ :region => options[:region] }) if !options[:region].blank?
56
+ opts
57
+ end
58
+
59
+ def fog_storage
60
+ return @storage if @storage
61
+ @storage = Fog::Storage.new(fog_opts)
62
+ begin
63
+ @storage.directories.count # test login
64
+ rescue Excon::Errors::Forbidden => e
65
+ say("Received Forbidden error while accessing account info. Is your key/secret correct?")
66
+ raise SystemExit
67
+ end
68
+ @storage
69
+ end
70
+
71
+ def fog_compute
72
+ @compute ||= Fog::Compute.new(fog_opts)
73
+ end
74
+
75
+ def fog_cdn
76
+ @cdn ||= Fog::CDN.new(fog_opts)
77
+ end
78
+
79
+ def show_buckets
80
+ fog_storage.directories.sort { |a,b| a.key <=> b.key }.each { |b| puts "#{b.key}" }
81
+ end
82
+
83
+ def show_servers
84
+ fog_compute.servers.sort { |a,b| a.key_name <=> b.key_name }.each do |s|
85
+ puts "#{s.tags['Name']} (state: #{s.state}): id=#{s.id} keyname=#{s.key_name} dns=#{s.dns_name} flavor=#{s.flavor_id}"
86
+ end
87
+ end
88
+
89
+ def show_cdns
90
+ puts fog_cdn.get_distribution_list.body['DistributionSummary'].to_yaml
91
+ end
92
+
93
+ def with_bucket(bucket_name)
94
+ d = fog_storage.directories.select { |d| d.key == bucket_name }.first
95
+ if d.nil?
96
+ say ("Could not find bucket with name #{bucket_name}")
97
+ return
98
+ end
99
+
100
+ say ("Found bucket named #{bucket_name}")
101
+ yield d
102
+ end
103
+
104
+ def fog_key_for(target_root, file_path)
105
+ target_root_path_length ||= target_root.to_s.length + "/".length
106
+ relative = file_path[ target_root_path_length, file_path.length]
107
+ relative
108
+ end
109
+
110
+ def s3_download(s3_file)
111
+ dir_path = Pathname.new(s3_file.key).dirname
112
+ dir_path.mkpath
113
+ File.open(s3_file.key, "w") do |f|
114
+ f.write s3_file.body
115
+ end
116
+ end
117
+
118
+
119
+ def content_hash(file)
120
+ md5 = Digest::MD5.new
121
+
122
+ while !file.eof?
123
+ md5.update(file.read(FILE_BUFFER_SIZE))
124
+ end
125
+
126
+ md5.hexdigest
127
+ end
128
+
129
+ end
130
+
131
+ desc "list_servers", "Show all servers"
132
+ def list_servers
133
+ show_servers
134
+ end
135
+
136
+ desc "start_server [SERVER_ID]", "Start a given EC2 server"
137
+ def start_server(server_id)
138
+ s = fog_compute.servers.select { |s| s.id == server_id}.first
139
+ if s
140
+ say("found server. starting/resuming. #{s.id}")
141
+ s.start
142
+ show_servers
143
+ else
144
+ say("no server with that id found. nothing done.")
145
+ end
146
+ end
147
+
148
+ desc "stop_server [SERVER_ID]", "Stop a given EC2 server (does not terminate it)"
149
+ def stop_server(server_id)
150
+ s = fog_compute.servers.select { |s| s.id == server_id}.first
151
+ if s
152
+ say("found server. stopping. #{s.id}")
153
+ s.stop
154
+ else
155
+ say("no server with that id found. nothing done.")
156
+ end
157
+ end
158
+
159
+ desc "list_cloudfront", "List cloudfront distributions (CDNs)"
160
+ def list_cloudfront
161
+ show_cdns
162
+ end
163
+
164
+ desc "create_cloudfront [BUCKET_NAME]", "Create a cloudfront distribution (a CDN)"
165
+ def create_cloudfront(bucket_id)
166
+ fog_cdn.post_distribution({
167
+ 'S3Origin' => {
168
+ 'DNSName' => "#{bucket_id}.s3.amazonaws.com"
169
+ },
170
+ 'Enabled' => true
171
+ })
172
+
173
+ show_cdns
174
+ end
175
+
176
+ desc "list", "Show all buckets"
177
+ method_options :region => "us-east-1"
178
+ def list
179
+ show_buckets
180
+ end
181
+
182
+ desc "afew [BUCKET_NAME]", "Show first 5 files in bucket"
183
+ method_options :count => "5"
184
+ method_options :glob => "**/*"
185
+ def afew(bucket_name)
186
+ d = fog_storage.directories.select { |d| d.key == bucket_name }.first
187
+ if d.nil?
188
+ say ("Found no bucket by name #{bucket_name}")
189
+ return
190
+ end
191
+
192
+ found = []
193
+
194
+ i = 0
195
+ d.files.each do |f|
196
+ if File.fnmatch(options[:glob], f.key)
197
+ found.push(d.files.head(f.key))
198
+ break if i >= options[:count].to_i
199
+ i += 1
200
+ end
201
+ end
202
+
203
+ unit_to_mult = {
204
+ 'B' => 1,
205
+ 'K' => 2**10,
206
+ 'M' => 2**20,
207
+ 'G' => 2**30
208
+ }
209
+
210
+ found.map { |f|
211
+ matching = unit_to_mult.keys.select { |k|
212
+ f.content_length >= unit_to_mult[k]
213
+ }.last
214
+
215
+ [f.key,
216
+ "#{(f.content_length.to_f / unit_to_mult[matching]).round(2)}#{matching}",
217
+ f.content_type,
218
+ f.last_modified
219
+ ]
220
+ }.tap do |tabular|
221
+ print_table(tabular, :ident => 2)
222
+ end
223
+
224
+ end
225
+
226
+ desc "download [BUCKET_NAME]", "Download all files in a bucket to CWD. Or one file."
227
+ method_options :region => "us-east-1"
228
+ method_options :one => nil
229
+ def download(bucket_name)
230
+ with_bucket bucket_name do |d|
231
+ if options[:one].nil?
232
+ if yes?("Are you sure you want to download all files into the CWD?", :red)
233
+ d.files.each do |s3_file|
234
+ say("Creating path for and downloading #{s3_file.key}")
235
+ s3_download(s3_file)
236
+ end
237
+ else
238
+ say("No action taken.")
239
+ end
240
+ else
241
+ s3_file = d.files.get(options[:one])
242
+ if !s3_file.nil?
243
+ s3_download(s3_file)
244
+ else
245
+ say("Could not find #{options[:one]}. No action taken.")
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ desc "upsync [BUCKET_NAME] [DIRECTORY]", "Push local files matching glob PATTERN into bucket. Ignore unchanged files."
252
+ method_options :public => false
253
+ method_options :region => "us-east-1"
254
+ method_options :noprompt => nil
255
+ method_options :glob => "**/*"
256
+ method_options :backups_retain => false
257
+ method_options :days_retain => 30
258
+ method_options :months_retain => 3
259
+ method_options :weeks_retain => 5
260
+ method_options :dry_run => false
261
+ def upsync(bucket_name, directory)
262
+
263
+ say("This is a dry run.") if options[:dry_run]
264
+
265
+ if !File.exists?(directory) || !File.directory?(directory)
266
+ say("'#{directory} does not exist or is not a directory.")
267
+ return
268
+ end
269
+
270
+ target_root = Pathname.new(directory)
271
+
272
+ files = Dir.glob(target_root.join(options[:glob])).select { |f| !File.directory?(f) }.map(&:to_s)
273
+ if !options[:backups_retain] && files.count == 0
274
+ say("No files to upload and no backups retain requested.")
275
+ return
276
+ end
277
+
278
+ say("Found #{files.count} candidate file upload(s).")
279
+
280
+ spn = dn = sn = un = cn = 0
281
+ with_bucket bucket_name do |d|
282
+
283
+ # having a brain fart and cant get this to simplify
284
+ go = false
285
+ if options[:noprompt] != nil
286
+ go = true
287
+ else
288
+ go = yes?("Proceed?", :red)
289
+ end
290
+
291
+ if go
292
+ time_marks = []
293
+ immediate_successors = {}
294
+ if options[:backups_retain]
295
+ # inclusive lower bound, exclusive upper bound
296
+ time_marks = []
297
+ Time.now.beginning_of_day.tap do |start|
298
+ options[:days_retain].times do |i|
299
+ time_marks.push(start - i.days)
300
+ end
301
+ end
302
+
303
+ Time.now.beginning_of_week.tap do |start|
304
+ options[:weeks_retain].times do |i|
305
+ time_marks.push(start - i.weeks)
306
+ end
307
+ end
308
+
309
+ Time.now.beginning_of_month.tap do |start|
310
+ options[:months_retain].times do |i|
311
+ time_marks.push(start - i.months)
312
+ end
313
+ end
314
+
315
+ time_marks.each do |tm|
316
+ files.each do |to_upload|
317
+ File.open(to_upload) do |localfile|
318
+ if localfile.mtime >= tm && (immediate_successors[tm].nil? || localfile.mtime < immediate_successors[tm][:last_modified])
319
+ immediate_successors[tm] = { :local_path => to_upload, :last_modified => localfile.mtime }
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ # don't pointlessly upload large files if we already know we're going to delete them!
327
+ if options[:backups_retain]
328
+ immediate_successors.values.map { |h| h[:local_path] }.tap do |kept_files|
329
+ before_reject = files.count # blah...lame
330
+ files.reject! { |to_upload| !kept_files.include?(to_upload) }
331
+ sn += before_reject - files.count
332
+
333
+ say("Found #{files.count} file(s) that meet backups retention criteria for upload. Comparing against bucket...")
334
+
335
+ end
336
+ end
337
+
338
+ files.each do |to_upload|
339
+ say("#{to_upload} (no output if skipped)...")
340
+ k = fog_key_for(target_root, to_upload)
341
+
342
+ existing_head = d.files.head(k)
343
+
344
+ time_mismatch = false
345
+ content_hash_mistmatched = false
346
+ File.open(to_upload) do |localfile|
347
+ time_mismatch = !existing_head.nil? && (existing_head.metadata[LOCAL_MOD_KEY].nil? || (Time.parse(existing_head.metadata[LOCAL_MOD_KEY]) - localfile.mtime).abs > EPSILON)
348
+ if time_mismatch
349
+ content_hash_mistmatched = existing_head.etag != content_hash(localfile)
350
+ end
351
+ end
352
+
353
+ if existing_head && time_mismatch && content_hash_mistmatched
354
+ if !options[:dry_run]
355
+ File.open(to_upload) do |localfile|
356
+ existing_head.metadata = { LOCAL_MOD_KEY => localfile.mtime.to_s }
357
+ existing_head.body = localfile
358
+ existing_head.multipart_chunk_size = FILE_BUFFER_SIZE # creates multipart_save
359
+ existing_head.save
360
+ end
361
+ end
362
+ say("updated.")
363
+ un += 1
364
+ elsif existing_head && time_mismatch
365
+ if !options[:dry_run]
366
+ File.open(to_upload) do |localfile|
367
+ existing_head.metadata = { LOCAL_MOD_KEY => localfile.mtime.to_s }
368
+ existing_head.save
369
+ end
370
+ end
371
+ say("updated.")
372
+ un += 1
373
+ elsif existing_head.nil?
374
+ if !options[:dry_run]
375
+ File.open(to_upload) do |localfile|
376
+ file = d.files.create(
377
+ :key => k,
378
+ :public => options[:public],
379
+ :body => ""
380
+ )
381
+ file.metadata = { LOCAL_MOD_KEY => localfile.mtime.to_s }
382
+ file.multipart_chunk_size = FILE_BUFFER_SIZE # creates multipart_save
383
+ file.body = localfile
384
+ file.save
385
+ end
386
+ end
387
+ say("created.")
388
+ cn += 1
389
+ else
390
+ sn += 1
391
+ # skipped
392
+ end
393
+ end
394
+
395
+
396
+ if options[:backups_retain]
397
+
398
+ # This array of hashes is computed because we need to do
399
+ # nested for loops of M*N complexity, where M=time_marks
400
+ # and N=files. We also need to do an remote get call to
401
+ # fetch the metadata of all N remote files (d.files.each
402
+ # will not do this). so, for performance sanity, we cache
403
+ # all the meta data for all the N files.
404
+
405
+ file_keys_modtimes = []
406
+ d.files.each { |f|
407
+ if File.fnmatch(options[:glob], f.key)
408
+ existing_head = d.files.head(f.key)
409
+ md = existing_head.metadata
410
+ file_keys_modtimes.push({
411
+ :key => f.key,
412
+ :last_modified => md[LOCAL_MOD_KEY] ? Time.parse(md[LOCAL_MOD_KEY]) : f.last_modified,
413
+ :existing_head => existing_head
414
+ })
415
+ end
416
+ }
417
+
418
+ say("#{file_keys_modtimes.length} file(s) found to consider for remote retention or remote deletion.")
419
+
420
+ # this generates as many 'kept files' as there are time marks...which seems wrong.
421
+ immediate_successors = {}
422
+ time_marks.each do |tm|
423
+ file_keys_modtimes.each do |fkm|
424
+ if fkm[:last_modified] >= tm && (immediate_successors[tm].nil? || fkm[:last_modified] < immediate_successors[tm][:last_modified])
425
+ immediate_successors[tm] = fkm
426
+ end
427
+ end
428
+ end
429
+
430
+ immediate_successors.values.map { |v| v[:key] }.tap do |kept_keys|
431
+ file_keys_modtimes.each do |fkm|
432
+ if kept_keys.include?(fkm[:key])
433
+ say("Remote retained #{fkm[:key]}.")
434
+ spn += 1
435
+ else
436
+ fkm[:existing_head].destroy if !options[:dry_run]
437
+ say("Remote deleted #{fkm[:key]}.")
438
+ dn += 1
439
+ end
440
+ end
441
+ end
442
+ end
443
+ else
444
+ say ("No action taken.")
445
+ end
446
+ end
447
+ say("Done. #{cn} created. #{un} updated. #{sn} local skipped. #{dn} deleted remotely. #{spn} retained remotely.")
448
+ end
449
+
450
+ desc "delete [BUCKET_NAME]", "Destroy a bucket"
451
+ method_options :region => "us-east-1"
452
+ def delete(bucket_name)
453
+ d = fog_storage.directories.select { |d| d.key == bucket_name }.first
454
+
455
+ if d.nil?
456
+ say ("Found no bucket by name #{bucket_name}")
457
+ return
458
+ end
459
+
460
+ if d.files.length > 0
461
+ say "Bucket has #{d.files.length} files. Please empty before destroying."
462
+ return
463
+ end
464
+
465
+ if yes?("Are you sure you want to delete this bucket #{d.key}?", :red)
466
+ d.destroy
467
+ say "Destroyed bucket named #{bucket_name}."
468
+ show_buckets
469
+ else
470
+ say "No action taken."
471
+ end
472
+
473
+ end
474
+
475
+ desc "create [BUCKET_NAME]", "Create a bucket"
476
+ method_options :region => "us-east-1"
477
+ def create(bucket_name = nil)
478
+ if !bucket_name
479
+ puts "No bucket name given."
480
+ return
481
+ end
482
+
483
+ fog_storage.directories.create(
484
+ :key => bucket_name,
485
+ :location => options[:region]
486
+ )
487
+
488
+ puts "Created bucket #{bucket_name}."
489
+ show_buckets
490
+ end
491
+
492
+ desc "show [BUCKET_NAME]", "Show info about bucket"
493
+ method_options :region => "us-east-1"
494
+ def show(bucket_name = nil)
495
+ if !bucket_name
496
+ puts "No bucket name given."
497
+ return
498
+ end
499
+
500
+ with_bucket(bucket_name) do |d|
501
+ say "#{d}: "
502
+ say d.location
503
+ end
504
+ end
505
+
506
+ end
507
+
508
+ Aws.start
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ color_highlighting = false
4
+
5
+ ARGV.each do |arg|
6
+ if arg == '-c'
7
+ color_highlighting = true
8
+ end
9
+ end
10
+
11
+ duout = `du -h -d 1`
12
+
13
+ comps = []
14
+
15
+ duout.split(/\n/).each do |l|
16
+ l =~ /\s*(\d+(\.\d+)?)(\w)\s+(.*)$/
17
+ size = $1
18
+ unit = $3
19
+ path = $4
20
+ comps << [size.to_f, unit, path]
21
+ end
22
+
23
+ unit_to_mult = {
24
+ 'B' => 1,
25
+ 'K' => 2**10,
26
+ 'M' => 2**20,
27
+ 'G' => 2**30
28
+ }
29
+
30
+ comps.sort! do |a, b|
31
+ (b[0] * unit_to_mult[b[1]]) <=> (a[0] * unit_to_mult[a[1]])
32
+ end
33
+
34
+ BLACK = "\033[30m"
35
+ RED = "\033[31m"
36
+ RED_BLINK = "\033[5;31m"
37
+ GREEN = "\033[32m"
38
+ YELLOW = "\033[33m"
39
+ BLUE = "\033[34m"
40
+ MAGENTA = "\033[35m"
41
+ CYAN = "\033[36m"
42
+ WHITE = "\033[37m"
43
+ RESET_COLOR = "\033[0m"
44
+
45
+ if color_highlighting
46
+ comps.each do |(size,unit,path)|
47
+ case unit
48
+ when 'G'
49
+ output_color = RED
50
+ when 'M'
51
+ output_color = YELLOW
52
+ when 'K'
53
+ output_color = CYAN
54
+ when 'B'
55
+ output_color = GREEN
56
+ else
57
+ output_color = BLACK
58
+ end
59
+
60
+ printf( "%s%10.1f%s %s%s\n",output_color, size, unit, path,RESET_COLOR)
61
+ end
62
+ else
63
+ comps.each do |(size,unit,path)|
64
+ printf( "%10.1f%s %s\n",size, unit, path)
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ puts Time.now.strftime("%Y%m%d%H%M%S")
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def run
4
+ wccmd = `find . -ipath #{ARGV[0]} -exec wc -l {} \\;`
5
+
6
+ comps = []
7
+
8
+ total = 0
9
+ wccmd.split(/\n/).each do |l|
10
+ l =~ /\s*(\d+(\.\d+)?)\s+(.*)$/
11
+ size = $1
12
+ path = $3
13
+ comps << [size.to_i, path]
14
+ total += size.to_i
15
+ end
16
+
17
+ comps.sort! do |a, b|
18
+ b[0] <=> a[0]
19
+ end
20
+
21
+ comps.each do |(size,path)|
22
+ printf( "%d %s\n", size, path)
23
+ end
24
+
25
+ puts "Total: #{total}"
26
+ end
27
+
28
+ run
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENV['BUNDLE_GEMFILE'] = File.join( File.dirname( File.expand_path( __FILE__ ) ), "Gemfile" )
4
+
5
+ require 'rubygems'
6
+ require 'bundler'
7
+ Bundler.require
8
+
9
+ require 'active_support/all'
10
+
11
+ class Zerigo < Thor
12
+
13
+ ENDPOINT = "https://ns.zerigo.com/api/1.1"
14
+ USERNAME = ENV['ZERIGO_USERNAME']
15
+ PASSWORD = ENV['ZERIGO_PASSWORD']
16
+ DOMAIN = ENV['ZERIGO_DEFAULT_DOMAIN']
17
+
18
+
19
+ no_tasks do
20
+ def zerigo
21
+ @zerigo ||= RestClient::Resource.new ENDPOINT, USERNAME, PASSWORD
22
+ end
23
+
24
+ def show_hosts(response)
25
+ fmt = "%6.5s %30.29s %40.39s %12.11s"
26
+
27
+ puts sprintf(fmt,
28
+ "Type",
29
+ "Hostname",
30
+ "Data",
31
+ "ID")
32
+
33
+ Nokogiri::XML( response.body ).css('host').each do |host|
34
+ puts sprintf(fmt,
35
+ host.at_css('host-type').text,
36
+ host.at_css('hostname').text,
37
+ host.at_css('data').text,
38
+ host.at_css('id').text)
39
+ end
40
+ end
41
+
42
+ def zone
43
+ if @zone.nil?
44
+ zones = []
45
+ response = zerigo['/zones.xml'].get
46
+ unless response.code.to_s !~ /2\d{2}/
47
+ Nokogiri::XML( response.body ).css('zone').each do |zone|
48
+ zones << {
49
+ :id => zone.css('id').first.text,
50
+ :domain => zone.css('domain').first.text,
51
+ }
52
+ end
53
+ end
54
+ found = zones.select { |z| z[:domain] == DOMAIN }.first
55
+ raise "Unable to find zone matching domain #{DOMAIN}" unless found
56
+
57
+ @zone = found[:id]
58
+ end
59
+ @zone
60
+ end
61
+ end
62
+
63
+ module HostTypes
64
+ CNAME = "CNAME"
65
+ ARECORD = "A"
66
+ MX = "MX"
67
+ end
68
+
69
+ desc "list", "List available host names."
70
+ def list
71
+ response = zerigo["/zones/#{zone}/hosts.xml"].get
72
+ unless response.code.to_s !~ /2\d{2}/
73
+ show_hosts( response )
74
+ end
75
+ end
76
+
77
+ desc "create [HOST_NAME]", "Create a host"
78
+ method_options :data => "proxy.heroku.com"
79
+ method_options :host_type => HostTypes::CNAME
80
+ def create(hostname = "")
81
+ new_host = { :data => options[:data], :host_type => options[:host_type], :hostname => hostname }
82
+
83
+ response = zerigo["/zones/#{zone}/hosts.xml"].post new_host.to_xml(:root => "host"), :content_type => "application/xml"
84
+ if response.code.to_s =~ /201/
85
+ puts "Created a host."
86
+ show_hosts( response )
87
+ end
88
+ end
89
+
90
+
91
+ desc "delete [ID]", "Delete an entry by id"
92
+ def delete(id)
93
+ response = zerigo["/hosts/#{id}.xml"].delete
94
+ if response.code.to_s =~ /200/
95
+ puts "Delete a host with id #{id}"
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ Zerigo.start
@@ -0,0 +1,80 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: cknife 0.1.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "cknife"
9
+ s.version = "0.1.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Mike De La Loza"]
14
+ s.date = "2014-06-06"
15
+ s.description = "An Amazon Web Services S3 command line tool, and a few other command line tools."
16
+ s.email = "mikedll@mikedll.com"
17
+ s.executables = ["cknifeaws", "cknifedub", "cknifenowtimestamp", "cknifewcdir", "cknifezerigo"]
18
+ s.extra_rdoc_files = [
19
+ "LICENSE",
20
+ "README.md"
21
+ ]
22
+ s.files = [
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE",
26
+ "README.md",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "bin/cknifeaws",
30
+ "bin/cknifedub",
31
+ "bin/cknifenowtimestamp",
32
+ "bin/cknifewcdir",
33
+ "bin/cknifezerigo",
34
+ "cknife.gemspec"
35
+ ]
36
+ s.homepage = "http://github.com/mikedll/cknife"
37
+ s.licenses = ["MIT"]
38
+ s.rubygems_version = "2.2.2"
39
+ s.summary = "Cali Army Knife"
40
+
41
+ if s.respond_to? :specification_version then
42
+ s.specification_version = 4
43
+
44
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
45
+ s.add_runtime_dependency(%q<rest-client>, [">= 0"])
46
+ s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
47
+ s.add_runtime_dependency(%q<i18n>, [">= 0"])
48
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
49
+ s.add_runtime_dependency(%q<thor>, [">= 0"])
50
+ s.add_runtime_dependency(%q<builder>, [">= 0"])
51
+ s.add_runtime_dependency(%q<fog>, [">= 1.15.0"])
52
+ s.add_runtime_dependency(%q<unf>, [">= 0"])
53
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
54
+ s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
55
+ else
56
+ s.add_dependency(%q<rest-client>, [">= 0"])
57
+ s.add_dependency(%q<nokogiri>, [">= 0"])
58
+ s.add_dependency(%q<i18n>, [">= 0"])
59
+ s.add_dependency(%q<activesupport>, [">= 0"])
60
+ s.add_dependency(%q<thor>, [">= 0"])
61
+ s.add_dependency(%q<builder>, [">= 0"])
62
+ s.add_dependency(%q<fog>, [">= 1.15.0"])
63
+ s.add_dependency(%q<unf>, [">= 0"])
64
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
65
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<rest-client>, [">= 0"])
69
+ s.add_dependency(%q<nokogiri>, [">= 0"])
70
+ s.add_dependency(%q<i18n>, [">= 0"])
71
+ s.add_dependency(%q<activesupport>, [">= 0"])
72
+ s.add_dependency(%q<thor>, [">= 0"])
73
+ s.add_dependency(%q<builder>, [">= 0"])
74
+ s.add_dependency(%q<fog>, [">= 1.15.0"])
75
+ s.add_dependency(%q<unf>, [">= 0"])
76
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
77
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
78
+ end
79
+ end
80
+
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cknife
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mike De La Loza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: i18n
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: builder
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: fog
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: 1.15.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: 1.15.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: unf
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundler
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: '1.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: '1.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: jeweler
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: 2.0.1
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ~>
151
+ - !ruby/object:Gem::Version
152
+ version: 2.0.1
153
+ description: An Amazon Web Services S3 command line tool, and a few other command
154
+ line tools.
155
+ email: mikedll@mikedll.com
156
+ executables:
157
+ - cknifeaws
158
+ - cknifedub
159
+ - cknifenowtimestamp
160
+ - cknifewcdir
161
+ - cknifezerigo
162
+ extensions: []
163
+ extra_rdoc_files:
164
+ - LICENSE
165
+ - README.md
166
+ files:
167
+ - Gemfile
168
+ - Gemfile.lock
169
+ - LICENSE
170
+ - README.md
171
+ - Rakefile
172
+ - VERSION
173
+ - bin/cknifeaws
174
+ - bin/cknifedub
175
+ - bin/cknifenowtimestamp
176
+ - bin/cknifewcdir
177
+ - bin/cknifezerigo
178
+ - cknife.gemspec
179
+ homepage: http://github.com/mikedll/cknife
180
+ licenses:
181
+ - MIT
182
+ metadata: {}
183
+ post_install_message:
184
+ rdoc_options: []
185
+ require_paths:
186
+ - lib
187
+ required_ruby_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - '>='
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - '>='
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ requirements: []
198
+ rubyforge_project:
199
+ rubygems_version: 2.2.2
200
+ signing_key:
201
+ specification_version: 4
202
+ summary: Cali Army Knife
203
+ test_files: []