cheftacular 2.2.2 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cheftacular/README.md +11 -5
- data/lib/cheftacular/actions/deploy.rb +1 -1
- data/lib/cheftacular/actions/migrate.rb +1 -1
- data/lib/cheftacular/chef/data_bag.rb +2 -0
- data/lib/cheftacular/cheftacular.rb +3 -0
- data/lib/cheftacular/dns.rb +2 -2
- data/lib/cheftacular/initializer.rb +1 -1
- data/lib/cheftacular/stateless_actions/chef_environment.rb +1 -1
- data/lib/cheftacular/stateless_actions/cloud.rb +5 -5
- data/lib/cheftacular/stateless_actions/cloud_bootstrap.rb +1 -1
- data/lib/cheftacular/stateless_actions/get_log_from_bag.rb +5 -5
- data/lib/cheftacular/stateless_actions/get_shorewall_allowed_connections.rb +77 -9
- data/lib/cheftacular/stateless_actions/test_env.rb +2 -5
- data/lib/cheftacular/version.rb +1 -1
- data/lib/sshkit/actions/start_task.rb +1 -1
- data/lib/sshkit/helpers.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cacccbd73021b45400cb3eda03bb8cb22f1ba9fe
|
4
|
+
data.tar.gz: 70f82785708bd038080088e905a0ef5cf4f84598
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 08b9fbaee4acc4edebff56241cdf369a30fc1684dbee05f37021c4e3d85002f267fd9c942303d6003fcbd095010d34a0a1a775d30c8e73c71f2cbca3134d8b2b
|
7
|
+
data.tar.gz: 2dee13991458ac134f24af9b2685e2c9e389934a5214a100d737612a23220d9d4ef5eca643a652c1be978471d685b1bcb93d90e7a9784b742bf4dce2a923583a
|
data/lib/cheftacular/README.md
CHANGED
@@ -220,8 +220,8 @@
|
|
220
220
|
2. NOTE! Most flavors have spaces in them, you must use quotes at the command line to utilize them!
|
221
221
|
|
222
222
|
4. `destroy:SERVER_NAME` destroys the server on the cloud. This must be an exact match of the server's actual name or the script will error.
|
223
|
-
|
224
|
-
|
223
|
+
|
224
|
+
5. `poll:SERVER_NAME` polls the cloud's server for the status of the SERVER_NAME. This command will stop polling if / when the status of the server is ACTIVE and its build progress is 100%.
|
225
225
|
|
226
226
|
6. `attach_volume:SERVER_NAME:VOLUME_NAME[:VOLUME_SIZE[:DEVICE_LOCATION]]` If VOLUME_NAME exists it will attach it if it is unattached otherwise it will create it
|
227
227
|
|
@@ -303,9 +303,13 @@
|
|
303
303
|
|
304
304
|
21. `cft get_pg_pass ['clip']` command will output the current environment's pg_password to your terminal. Optionally you can pass in clip like `cft get_pg_pass clip` to have it also copy the pass to your clipboard.
|
305
305
|
|
306
|
-
22. `cft get_shorewall_allowed_connections` command will query a single server and return all of its ACCEPT connections from shorewall in it's syslog and return the results in a CSV format. Useful for tracking IP activity.
|
306
|
+
22. `cft get_shorewall_allowed_connections [PATH_TO_LOCAL_FILE] -n NODE_NAME` command will query a single server and return all of its ACCEPT connections from shorewall in it's syslog and return the results in a CSV format. Useful for tracking IP activity.
|
307
|
+
|
308
|
+
1. You must pass in a node name to query with `-n NODE_NAME`
|
307
309
|
|
308
|
-
|
310
|
+
2. This command will attempt to `dig` each ip address to give you the most likely culprit.
|
311
|
+
|
312
|
+
3. If `PATH_TO_LOCAL_FILE` is not blank, the command will use that file instead of building a file on the remote server
|
309
313
|
|
310
314
|
23. `cft help COMMAND|MODE` this command returns the documentation for a specific command if COMMAND matches the name of a command. Alternatively, it can be passed `action|arguments|application|current|devops|stateless_action` to fetch the commands for a specific mode.Misspellings of commands will display near hits.
|
311
315
|
|
@@ -369,12 +373,14 @@
|
|
369
373
|
|
370
374
|
35. `cft ubuntu_bootstrap ADDRESS ROOT_PASS` This command will bring a fresh server to a state where chef-client can be run on it via `cft chef-bootstrap`. It should be noted that it is in this step where a server's randomized deploy_user sudo password is generated.
|
371
375
|
|
372
|
-
36. `cft update_cloudflare_dns_from_cloud` command will force a full dns update for cloudflare.
|
376
|
+
36. `cft update_cloudflare_dns_from_cloud [skip_update_tld]` command will force a full dns update for cloudflare.
|
373
377
|
|
374
378
|
1. It will ensure all the subdomain entries are correct (based on the contents of the addresses data bag) and update them if they are not. It will also create the local subdomain for the entry as well if it does exist and point it to the correct private address for an environment.
|
375
379
|
|
376
380
|
2. This command will also ensure any dns records on your cloud are also migrated over to cloudflare as well. This also includes the reverse in the event you would like to turn off cloudflare.
|
377
381
|
|
382
|
+
3. The argument `skip_update_tld` will stop the long process of checking and updating all the server domains _before_ cloudflare is updated. Only skip if you believe your domain info on your cloud is accurate.
|
383
|
+
|
378
384
|
37. `cft update_split_branches` will perform a series of git commands that will merge all the split branches for your split_branch enabled repositories with what is currently on master and push them.
|
379
385
|
|
380
386
|
1. Repository must be set with `-R REPOSITORY_NAME` for this command to work.
|
@@ -33,7 +33,7 @@ class Cheftacular
|
|
33
33
|
logs_bag_hash["#{ n.name }-deploy"] = { text: log_data.scrub_pretty_text, timestamp: timestamp }
|
34
34
|
end
|
35
35
|
|
36
|
-
|
36
|
+
#@config['ChefDataBag'].save_logs_bag unless @options['debug'] #We don't really need to store entire chef runs in the logs bag
|
37
37
|
|
38
38
|
migrate(nodes) if @config['getter'].get_current_repo_config['database'] != 'none' && !@options['run_migration_already']
|
39
39
|
end
|
@@ -22,6 +22,7 @@ require 'timeout'
|
|
22
22
|
require 'slack-notifier'
|
23
23
|
require 'cloudflare'
|
24
24
|
require 'zlib'
|
25
|
+
require 'csv'
|
25
26
|
|
26
27
|
Dir["#{File.dirname(__FILE__)}/../**/*.rb"].each { |f| require f }
|
27
28
|
|
@@ -29,6 +30,8 @@ class Cheftacular
|
|
29
30
|
def initialize options={'env'=>'staging'}, config={}
|
30
31
|
@options, @config = options, config
|
31
32
|
|
33
|
+
SSHKit.config.format = :blackhole
|
34
|
+
|
32
35
|
@config['start_time'] = Time.now
|
33
36
|
|
34
37
|
@config['helper'] = Helper.new(@options, @config)
|
data/lib/cheftacular/dns.rb
CHANGED
@@ -180,12 +180,12 @@ class Cheftacular
|
|
180
180
|
if !target_serv_index.nil? && target_serv_index.is_a?(Fixnum) && !@options['dont_remove_address_or_server'] && args.include?('set_hash_to_nil')
|
181
181
|
puts("Found entry in addresses data bag corresponding to #{ @options['node_name'] } for #{ @options['env'] }, removing...") unless @options['quiet']
|
182
182
|
|
183
|
+
domain_obj = PublicSuffix.parse @config[@options['env']]['addresses_bag_hash']['addresses'][target_serv_index]['dn']
|
184
|
+
|
183
185
|
@config[@options['env']]['addresses_bag_hash']['addresses'][target_serv_index] = nil
|
184
186
|
|
185
187
|
@config[@options['env']]['addresses_bag_hash']['addresses'] = @config[@options['env']]['addresses_bag_hash']['addresses'].compact
|
186
188
|
|
187
|
-
domain_obj = PublicSuffix.parse @config[@options['env']]['addresses_bag_hash']['addresses'][target_serv_index]['dn']
|
188
|
-
|
189
189
|
@config['stateless_action'].cloud "domain", "destroy_record:#{ domain_obj.tld }:#{ domain_obj.trd }" if domain_obj.tld == @config[@options['env']]['config_bag_hash'][@options['sub_env']]['tld']
|
190
190
|
end
|
191
191
|
|
@@ -397,7 +397,7 @@ class Cheftacular
|
|
397
397
|
|
398
398
|
exit
|
399
399
|
else
|
400
|
-
unless File.exists?( current_version_file_path )
|
400
|
+
unless File.exists?( @config['helper'].current_version_file_path )
|
401
401
|
puts "Creating file cache for #{ Time.now.strftime("%Y%m%d") } (#{ detected_version }). No new version detected."
|
402
402
|
|
403
403
|
@config['helper'].write_version_file detected_version
|
@@ -7,7 +7,7 @@ class Cheftacular
|
|
7
7
|
"If no args are passed nothing will happen.",
|
8
8
|
|
9
9
|
[
|
10
|
-
" 1. `domain`
|
10
|
+
" 1. `domain` first level argument for interacting with cloud domains",
|
11
11
|
|
12
12
|
" 1. `list` default behavior",
|
13
13
|
|
@@ -28,7 +28,7 @@ class Cheftacular
|
|
28
28
|
|
29
29
|
" 9. `update_record:TOP_LEVEL_DOMAIN:SUBDOMAIN_NAME:IP_ADDRESS[:RECORD_TYPE[:TTL]]` similar to `create_record`.",
|
30
30
|
|
31
|
-
" 2. `server`
|
31
|
+
" 2. `server` first level argument for interacting with cloud servers, " +
|
32
32
|
"if no additional args are passed the command will return a list of all servers on the preferred cloud.",
|
33
33
|
|
34
34
|
" 1. `list` default behavior",
|
@@ -44,7 +44,7 @@ class Cheftacular
|
|
44
44
|
|
45
45
|
" 4. `destroy:SERVER_NAME` destroys the server on the cloud. This must be an exact match of the server's actual name or the script will error.",
|
46
46
|
|
47
|
-
"
|
47
|
+
" 5. `poll:SERVER_NAME` polls the cloud's server for the status of the SERVER_NAME. This command " +
|
48
48
|
"will stop polling if / when the status of the server is ACTIVE and its build progress is 100%.",
|
49
49
|
|
50
50
|
" 6. `attach_volume:SERVER_NAME:VOLUME_NAME[:VOLUME_SIZE[:DEVICE_LOCATION]]` " +
|
@@ -66,7 +66,7 @@ class Cheftacular
|
|
66
66
|
|
67
67
|
" 9. `read_volume:SERVER_NAME:VOLUME_NAME` returns the data of VOLUME_NAME if it is attached to the server.",
|
68
68
|
|
69
|
-
" 3. `volume`
|
69
|
+
" 3. `volume` first level argument for interacting with cloud storage volumes, if no additional args are passed the command will return a list of all cloud storage containers.",
|
70
70
|
|
71
71
|
" 1. `list` default behavior",
|
72
72
|
|
@@ -76,7 +76,7 @@ class Cheftacular
|
|
76
76
|
|
77
77
|
" 4. `destroy:VOLUME_NAME` destroys the volume. This operation will not work if the volume is attached to a server.",
|
78
78
|
|
79
|
-
" 4. `flavor`
|
79
|
+
" 4. `flavor` first level argument for listing the flavors available on the cloud service",
|
80
80
|
|
81
81
|
" 1. `list` default behavior",
|
82
82
|
|
@@ -75,7 +75,7 @@ class Cheftacular
|
|
75
75
|
|
76
76
|
@options['dont_remove_address_or_server'] = true #flag to make sure our entry isnt removed in addresses bag
|
77
77
|
|
78
|
-
full_bootstrap #bootstrap server with ruby and attach it to the chef server
|
78
|
+
@config['stateless_action'].full_bootstrap #bootstrap server with ruby and attach it to the chef server
|
79
79
|
end
|
80
80
|
end
|
81
81
|
end
|
@@ -23,14 +23,14 @@ class Cheftacular
|
|
23
23
|
nodes = @config['parser'].exclude_nodes( nodes, [{ unless: { env: @options['env'] }}])
|
24
24
|
|
25
25
|
nodes.each do |node|
|
26
|
-
if @config[@options['env']]['logs_bag_hash'].has_key?("#{ node.name }-
|
27
|
-
puts("Found log data in logs bag. Outputting to #{ log_loc }/stashedlog/#{ node.name }-deploystash-#{ @config[@options['env']]['logs_bag_hash']["#{ node.name }-
|
26
|
+
if @config[@options['env']]['logs_bag_hash'].has_key?("#{ node.name }-run")
|
27
|
+
puts("Found log data in logs bag. Outputting to #{ log_loc }/stashedlog/#{ node.name }-deploystash-#{ @config[@options['env']]['logs_bag_hash']["#{ node.name }-run"][:timestamp] }.txt") unless @options['quiet']
|
28
28
|
|
29
|
-
File.open("#{ log_loc }/stashedlog/#{ node.name }-deploystash-#{@config[@options['env']]['logs_bag_hash']["#{ node.name }-
|
30
|
-
f.write(@config[@options['env']]['logs_bag_hash']["#{ node.name }-
|
29
|
+
File.open("#{ log_loc }/stashedlog/#{ node.name }-deploystash-#{@config[@options['env']]['logs_bag_hash']["#{ node.name }-run"][:timestamp] }.txt", "w") do |f|
|
30
|
+
f.write(@config[@options['env']]['logs_bag_hash']["#{ node.name }-run"][:text])
|
31
31
|
end
|
32
32
|
|
33
|
-
puts(@config[@options['env']]['logs_bag_hash']["#{ node.name }-
|
33
|
+
puts(@config[@options['env']]['logs_bag_hash']["#{ node.name }-run"][:text]) if @options['verbose']
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
@@ -2,24 +2,54 @@ class Cheftacular
|
|
2
2
|
class StatelessActionDocumentation
|
3
3
|
def get_shorewall_allowed_connections
|
4
4
|
@config['documentation']['stateless_action'] << [
|
5
|
-
"`[
|
5
|
+
"`cft get_shorewall_allowed_connections [PATH_TO_LOCAL_FILE] -n NODE_NAME` command will query a single server and return all of its ACCEPT connections " +
|
6
6
|
"from shorewall in it's syslog and return the results in a CSV format. Useful for tracking IP activity.",
|
7
7
|
|
8
8
|
[
|
9
|
-
" 1.
|
9
|
+
" 1. You must pass in a node name to query with `-n NODE_NAME`",
|
10
|
+
|
11
|
+
" 2. This command will attempt to `dig` each ip address to give you the most likely culprit.",
|
12
|
+
|
13
|
+
" 3. If `PATH_TO_LOCAL_FILE` is not blank, the command will use that file instead of building a file on the remote server"
|
10
14
|
]
|
11
15
|
]
|
12
16
|
end
|
13
17
|
end
|
14
18
|
|
15
19
|
class StatelessAction
|
16
|
-
def get_shorewall_allowed_connections
|
17
|
-
|
18
|
-
|
20
|
+
def get_shorewall_allowed_connections master_log_data=''
|
21
|
+
|
22
|
+
if ARGV[1].nil?
|
23
|
+
raise "Please pass a NODE_NAME with -n NODE_NAME" if @options['node_name'].nil? || @options['node_name'].empty?
|
24
|
+
|
25
|
+
nodes = @config['getter'].get_true_node_objects true
|
26
|
+
|
27
|
+
nodes = @config['parser'].exclude_nodes(nodes, [{ unless: { env: @options['env'] }}, { unless: { node: @options['node_name'] }}], true)
|
28
|
+
|
29
|
+
#this must always precede on () calls so they have the instance variables they need
|
30
|
+
options, locs, ridley, logs_bag_hash, pass_bag_hash, bundle_command, cheftacular, passwords = @config['helper'].set_local_instance_vars
|
31
|
+
|
32
|
+
#on is namespaced to SSHKit::Backend::Netssh.on
|
33
|
+
on ( nodes.map { |n| "deploy@" + n.public_ipaddress } ) do |host|
|
34
|
+
n = get_node_from_address(nodes, host.hostname)
|
35
|
+
|
36
|
+
puts("Beginning shorewall log capture run for #{ n.name } (#{ n.public_ipaddress })") unless options['quiet']
|
37
|
+
|
38
|
+
master_log_data = start_shorewall_log_capture( n.name, n.public_ipaddress, options, locs, cheftacular, passwords)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
master_log_file = ARGV[1]
|
42
|
+
|
43
|
+
raise "File not found! Did you enter the path correctly?" unless File.exist?(master_log_file)
|
44
|
+
|
45
|
+
master_log_data = File.read(File.expand_path(master_log_file))
|
46
|
+
end
|
47
|
+
|
48
|
+
puts("Parsing addresses from log data...") unless @options['quiet']
|
19
49
|
|
20
50
|
addresses = {}
|
21
51
|
|
22
|
-
|
52
|
+
master_log_data.scan(/^.*Shorewall:net2fw:ACCEPT.*SRC=([\d]+\.[\d]+\.[\d]+\.[\d]+) DST.*DPT=80.*$/).each do |ip_address|
|
23
53
|
addresses[ip_address] ||= 0
|
24
54
|
addresses[ip_address] += 1
|
25
55
|
end
|
@@ -27,6 +57,8 @@ class Cheftacular
|
|
27
57
|
final_addresses = {}
|
28
58
|
check_count = 0
|
29
59
|
addresses.each_pair do |address, count|
|
60
|
+
next if count < 100
|
61
|
+
|
30
62
|
domain = `dig +short -x #{ address[0] }`.chomp.split("\n").join('|')
|
31
63
|
domain = domain[0..(domain.length-2)]
|
32
64
|
|
@@ -40,18 +72,18 @@ class Cheftacular
|
|
40
72
|
|
41
73
|
check_count += 1
|
42
74
|
|
43
|
-
puts
|
75
|
+
puts("Processed #{ check_count } addresses (#{ address[0] }):#{ domain }:#{ count }") unless @options['quiet']
|
44
76
|
end
|
45
77
|
|
46
78
|
final_addresses = final_addresses.sort_by {|key, value_hash| value_hash['count']}.to_h
|
47
79
|
|
48
80
|
final_addresses = Hash[final_addresses.to_a.reverse]
|
49
81
|
|
50
|
-
ap(final_addresses)
|
82
|
+
ap(final_addresses) if @options['verbose']
|
51
83
|
|
52
84
|
log_loc, timestamp = @config['helper'].set_log_loc_and_timestamp
|
53
85
|
|
54
|
-
CSV.open(File.expand_path("#{ @locs['chef-log'] }
|
86
|
+
CSV.open(File.expand_path("#{ @config['locs']['chef-log'] }/shorewall-parse-#{ timestamp }.csv"), "wb") do |csv|
|
55
87
|
final_addresses.each_pair do |dns, info_hash|
|
56
88
|
csv << [dns, info_hash['addresses'].join('|'), info_hash['count']]
|
57
89
|
end
|
@@ -59,3 +91,39 @@ class Cheftacular
|
|
59
91
|
end
|
60
92
|
end
|
61
93
|
end
|
94
|
+
|
95
|
+
module SSHKit
|
96
|
+
module Backend
|
97
|
+
class Netssh
|
98
|
+
def start_shorewall_log_capture name, ip_address, options, locs, cheftacular, passwords, out=[]
|
99
|
+
log_loc, timestamp = set_log_loc_and_timestamp(locs)
|
100
|
+
|
101
|
+
puts("Generating master log file for shorewall for #{ name } (#{ ip_address }) at #{ log_loc }/#{ name }-shorewall-#{ timestamp }.html") unless options['quiet']
|
102
|
+
|
103
|
+
syslog_files = capture(:ls, '/var/log', :|, :grep, :syslog).split("\n")
|
104
|
+
|
105
|
+
puts("Found #{ syslog_files.count } syslog files to parse (#{ syslog_files.join(', ') }).\nPreparing to parse...") unless options['quiet']
|
106
|
+
|
107
|
+
syslog_files.each do |file|
|
108
|
+
puts("Parsing #{ file } into master log file...") unless options['quiet']
|
109
|
+
|
110
|
+
if file.include?('.gz')
|
111
|
+
sudo_execute(passwords[ip_address], :gunzip, '-c', "/var/log/#{ file }", '>>', '/tmp/syslog_master.log' )
|
112
|
+
else
|
113
|
+
sudo_execute(passwords[ip_address], :cat, "/var/log/#{ file }", '>>', '/tmp/syslog_master.log' )
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
puts("Writing master log...") unless options['quiet']
|
118
|
+
|
119
|
+
out << sudo_capture( passwords[ip_address], :cat, "/tmp/syslog_master.log" )
|
120
|
+
|
121
|
+
::File.open("#{ log_loc }/#{ name }-shorewall-#{ timestamp }.html", "w") { |f| f.write(out.join("\n").scrub_pretty_text.gsub('[sudo] password for deploy: ', '')) } unless options['no_logs']
|
122
|
+
|
123
|
+
sudo_execute(passwords[ip_address], :rm, '-f', '/tmp/syslog_master.log')
|
124
|
+
|
125
|
+
out.join("\n")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -30,7 +30,7 @@ class Cheftacular
|
|
30
30
|
|
31
31
|
type = ARGV[env_index] if ARGV[env_index]
|
32
32
|
|
33
|
-
raise "Unknown split_env: #{ split_env }, can only be #{ cheftacular['run_list_environments'].values.join(', ') }" unless (split_env =~ /#{ cheftacular['run_list_environments'].values.join('|') }/) == 0
|
33
|
+
raise "Unknown split_env: #{ split_env }, can only be #{ @config['cheftacular']['run_list_environments'].values.join(', ') }" unless (split_env =~ /#{ @config['cheftacular']['run_list_environments'].values.join('|') }/) == 0
|
34
34
|
|
35
35
|
raise "Unknown type: #{ type }, can only be 'boot' or 'destroy'" unless (type =~ /boot|destroy/) == 0
|
36
36
|
|
@@ -41,11 +41,9 @@ class Cheftacular
|
|
41
41
|
@options['force_yes'] = true
|
42
42
|
@options['in_scaling'] = true
|
43
43
|
|
44
|
-
initial_servers = @config['cheftacular']['split_env_nodes']
|
45
|
-
|
46
44
|
case type
|
47
45
|
when 'boot'
|
48
|
-
|
46
|
+
@config['cheftacular']['split_env_nodes'].each_pair do |name, config_hash|
|
49
47
|
true_name = name.gsub('SPLITENV', split_env)
|
50
48
|
@options['node_name'] = "#{ true_name }#{ 'p' if @options['env'] == 'production' }"
|
51
49
|
@options['flavor_name'] = config_hash.has_key?('flavor') ? config_hash['flavor'] : @config['cheftacular']['default_flavor_name']
|
@@ -66,7 +64,6 @@ class Cheftacular
|
|
66
64
|
@options['delete_server_on_remove'] = true
|
67
65
|
|
68
66
|
nodes.each do |node|
|
69
|
-
next if !targets.empty? && !targets.include?(node.name)
|
70
67
|
|
71
68
|
@options['node_name'] = node.name
|
72
69
|
|
data/lib/cheftacular/version.rb
CHANGED
data/lib/sshkit/helpers.rb
CHANGED
@@ -9,6 +9,10 @@ module SSHKit
|
|
9
9
|
capture :echo, pass, :|, :sudo, '-S', *args
|
10
10
|
end
|
11
11
|
|
12
|
+
def sudo_execute pass, *args
|
13
|
+
execute :echo, pass, :|, :sudo, '-S', *args
|
14
|
+
end
|
15
|
+
|
12
16
|
def sudo_test pass, file_location
|
13
17
|
sudo_capture( pass, :test, '-e', file_location, '&&', :echo, 'true', { raise_on_non_zero_exit: false, verbosity: Logger::DEBUG }) == 'true'
|
14
18
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cheftacular
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Louis Alridge
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashie
|