fog-prune 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.
- data/CHANGELOG.md +2 -0
- data/Gemfile +3 -0
- data/README.md +9 -0
- data/bin/fog-prune +6 -0
- data/fog-prune.gemspec +17 -0
- data/lib/fog-prune/config.rb +27 -0
- data/lib/fog-prune/options.rb +122 -0
- data/lib/fog-prune/prune.rb +189 -0
- data/lib/fog-prune/version.rb +6 -0
- data/lib/fog-prune.rb +7 -0
- metadata +88 -0
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/bin/fog-prune
ADDED
data/fog-prune.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
|
2
|
+
require 'fog-prune/version'
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'fog-prune'
|
5
|
+
s.version = FogPrune::VERSION.version
|
6
|
+
s.summary = 'Chef pruning with fog integration'
|
7
|
+
s.author = 'Chris Roberts'
|
8
|
+
s.email = 'chrisroberts.code@gmail.com'
|
9
|
+
s.homepage = 'http://github.com/heavywater/fog-prune'
|
10
|
+
s.description = 'Chef pruner'
|
11
|
+
s.require_path = 'lib'
|
12
|
+
s.bindir = 'bin'
|
13
|
+
s.executables << 'fog-prune'
|
14
|
+
s.add_dependency 'chef'
|
15
|
+
s.add_dependency 'fog', '~> 1.20'
|
16
|
+
s.files = Dir['**/*']
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'chef/mash'
|
2
|
+
require 'mixlib/config'
|
3
|
+
|
4
|
+
class FogPrune
|
5
|
+
|
6
|
+
class Config
|
7
|
+
extend Mixlib::Config
|
8
|
+
|
9
|
+
fog Mash.new
|
10
|
+
chef_converge_every 3600
|
11
|
+
prune []
|
12
|
+
nodes nil
|
13
|
+
debug false
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def load_config_file!
|
17
|
+
raise 'No config file path defined!' unless self[:config]
|
18
|
+
raise 'No configuration file found at defined path!' unless File.exists?(self[:config])
|
19
|
+
Mash.new(Chef::JSONCompat.from_json(File.read(self[:config]))).each do |k,v|
|
20
|
+
self.send(k, v)
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'mixlib/cli'
|
2
|
+
require 'fog-prune/config'
|
3
|
+
|
4
|
+
class FogPrune
|
5
|
+
|
6
|
+
class Options
|
7
|
+
include Mixlib::CLI
|
8
|
+
|
9
|
+
option(:config,
|
10
|
+
:short => '-c PATH',
|
11
|
+
:long => '--config-file PATH',
|
12
|
+
:description => 'JSON configuration file'
|
13
|
+
)
|
14
|
+
option(:fog_credentials,
|
15
|
+
:short => '-f KEY=VALUE[,KEY=VALUE,...]',
|
16
|
+
:long => '--fog-credentials KEY=VALUE[,KEY=VALUE,...]',
|
17
|
+
:description => 'Fog credentials',
|
18
|
+
:proc => lambda {|val|
|
19
|
+
pairs = val.split(',').map{|v| v.split('=').map(&:strip) }
|
20
|
+
provider = pairs.detect do |ary|
|
21
|
+
ary.first == 'provider'
|
22
|
+
end.last
|
23
|
+
raise 'Credentials must include `provider`' unless provider
|
24
|
+
FogPrune::Config[:fog][provider] ||= []
|
25
|
+
FogPrune::Config[:fog][provider].push(Mash[*pairs.flatten]).uniq!
|
26
|
+
}
|
27
|
+
)
|
28
|
+
option(:prune,
|
29
|
+
:short => '-a sensu,chef',
|
30
|
+
:long => '--apply-to sensu,chef',
|
31
|
+
:description => 'Apply pruning to these items',
|
32
|
+
:proc => lambda {|v| v.split(',')}
|
33
|
+
)
|
34
|
+
option(:chef_converge_every,
|
35
|
+
:short => '-r SECS',
|
36
|
+
:long => '--converge-recur SECS',
|
37
|
+
:description => 'Number of seconds between convergences',
|
38
|
+
:default => 3600
|
39
|
+
)
|
40
|
+
option(:sensu_host,
|
41
|
+
:short => '-h HOST',
|
42
|
+
:long => '--sensu-host HOST',
|
43
|
+
:description => 'Hostname of sensu API'
|
44
|
+
)
|
45
|
+
option(:sensu_port,
|
46
|
+
:short => '-p PORT',
|
47
|
+
:long => '--sensu-port PORT',
|
48
|
+
:description => 'Port of sensu API',
|
49
|
+
:default => 4567,
|
50
|
+
:proc => lambda {|v| v.to_i }
|
51
|
+
)
|
52
|
+
option(:sensu_username,
|
53
|
+
:short => '-u USERNAME',
|
54
|
+
:long => '--sensu-username USERNAME',
|
55
|
+
:description => 'Sensu API username'
|
56
|
+
)
|
57
|
+
option(:sensu_password,
|
58
|
+
:short => '-P PASSWORD',
|
59
|
+
:long => '--sensu-password PASSWORD',
|
60
|
+
:description => 'Sensu API password'
|
61
|
+
)
|
62
|
+
option(:nodes,
|
63
|
+
:short => '-n NODE[,NODE...]',
|
64
|
+
:long => '--nodes NODE[,NODE...]',
|
65
|
+
:description => 'List of nodes to prune',
|
66
|
+
:proc => lambda {|val|
|
67
|
+
val.split(',').map(&:strip)
|
68
|
+
}
|
69
|
+
)
|
70
|
+
option(:filter,
|
71
|
+
:short => '-f "Solr filter"',
|
72
|
+
:long => '--filter "Solr filter"',
|
73
|
+
:description => 'Add filter to node search',
|
74
|
+
:proc => lambda {|val|
|
75
|
+
FogPrune::Config[:filter] = [FogPrune::Config[:filter], val].compact.join(' AND ')
|
76
|
+
}
|
77
|
+
)
|
78
|
+
option(:debug,
|
79
|
+
:short => '-d',
|
80
|
+
:long => '--debug',
|
81
|
+
:description => 'Turn on debug output'
|
82
|
+
)
|
83
|
+
option(:print_only,
|
84
|
+
:short => '-o',
|
85
|
+
:long => '--[no-]print-only',
|
86
|
+
:boolean => true,
|
87
|
+
:default => false,
|
88
|
+
:description => 'Print action and exit'
|
89
|
+
)
|
90
|
+
option(:stale_nodes,
|
91
|
+
:short => '-S',
|
92
|
+
:long => '--[no-]stale-nodes',
|
93
|
+
:boolean => true,
|
94
|
+
:default => false,
|
95
|
+
:description => 'Remove stale nodes'
|
96
|
+
)
|
97
|
+
option(:stale_node_timeout,
|
98
|
+
:long => '--stale-node-timeout TIMEOUT',
|
99
|
+
:default => 3600,
|
100
|
+
:description => 'Delete stale nodes that exceed given timeout'
|
101
|
+
)
|
102
|
+
option(:tag_stale_nodes,
|
103
|
+
:short => '-T',
|
104
|
+
:long => '--[no-]tag-stale-nodes',
|
105
|
+
:default => true,
|
106
|
+
:description => 'Tag nodes with no ohai time set (used for stale node removal)'
|
107
|
+
)
|
108
|
+
|
109
|
+
def configure(args)
|
110
|
+
begin
|
111
|
+
require 'chef/knife'
|
112
|
+
Chef::Knife.new.configure_chef
|
113
|
+
rescue => e
|
114
|
+
$stderr.puts 'WARN: Failed to load defaults via knife configuration.'
|
115
|
+
end
|
116
|
+
parse_options(args)
|
117
|
+
Config.merge!(config)
|
118
|
+
Config[:debug] = true if Config[:print_only]
|
119
|
+
Config.load_config_file! if Config[:config]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'fog'
|
2
|
+
require 'chef'
|
3
|
+
require 'chef/knife'
|
4
|
+
require 'chef/knife/core/ui'
|
5
|
+
|
6
|
+
require 'fog-prune/config'
|
7
|
+
require 'fog-prune/options'
|
8
|
+
|
9
|
+
class FogPrune
|
10
|
+
|
11
|
+
QUERY_ROWS = 1000
|
12
|
+
PROVIDER_ALIASES = Mash.new(
|
13
|
+
:ec2 => 'aws'
|
14
|
+
)
|
15
|
+
|
16
|
+
attr_reader :ui
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
Chef::Knife.new.configure_chef
|
20
|
+
@compute = Mash.new
|
21
|
+
@ui = Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
|
22
|
+
end
|
23
|
+
|
24
|
+
def compute(provider)
|
25
|
+
p_key = PROVIDER_ALIASES[provider] || provider
|
26
|
+
unless(@compute[p_key])
|
27
|
+
raise "No Fog credentials provided for #{provider}!" unless Config[:fog][p_key]
|
28
|
+
@compute[p_key] = []
|
29
|
+
@compute[p_key] = Config[:fog][p_key].map do |args|
|
30
|
+
Fog::Compute.new(
|
31
|
+
format_fog_hash(args.merge(:provider => p_key))
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@compute[p_key]
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_fog_hash(hash)
|
39
|
+
new_hash = {}
|
40
|
+
hash.each do |k,v|
|
41
|
+
new_hash[k.to_sym] = v
|
42
|
+
end
|
43
|
+
new_hash
|
44
|
+
end
|
45
|
+
|
46
|
+
def sensu?
|
47
|
+
Config[:prune].include?('sensu')
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def chef?
|
52
|
+
Config[:prune].include?('chef')
|
53
|
+
end
|
54
|
+
|
55
|
+
def prune!
|
56
|
+
tag_stale_nodes if Config[:tag_stale_nodes]
|
57
|
+
ui.info "Starting node pruning..."
|
58
|
+
ui.warn "Pruning from: #{Config[:prune].join(', ')}"
|
59
|
+
nodes_to_prune = discover_prunable_nodes
|
60
|
+
debug "Initial nodes discovered: #{nodes_to_prune.map(&:name).sort.join(', ')}"
|
61
|
+
debug "Initial node count: #{nodes_to_prune.size}"
|
62
|
+
nodes_to_prune = filter_prunables_via_fog(nodes_to_prune)
|
63
|
+
ui.warn "Nodes to prune: #{nodes_to_prune.size}"
|
64
|
+
debug "#{nodes_to_prune.map(&:name).sort.join(', ')}"
|
65
|
+
unless(Config[:print_only])
|
66
|
+
ui.confirm('Destroy these nodes')
|
67
|
+
nodes_to_prune.each do |node|
|
68
|
+
prune_node(node)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def discover_prunable_nodes
|
74
|
+
if(Config[:nodes])
|
75
|
+
query = Array(Config[:nodes]).flatten.map do |name|
|
76
|
+
"name:#{name}"
|
77
|
+
end.join(' OR ')
|
78
|
+
else
|
79
|
+
max_ohai_time = Time.now.to_f - Config[:chef_converge_every].to_f
|
80
|
+
query = ["ohai_time:[0.0 TO #{max_ohai_time}]"]
|
81
|
+
if(Config[:stale_nodes])
|
82
|
+
max_prune_time = Time.now.to_f - Config[:stale_node_timeout].to_f
|
83
|
+
query << ["(prune_tag_time:[0.0 TO #{max_prune_time}] NOT ohai_time:[* TO *])"]
|
84
|
+
query = ["(#{query.join(' OR ')})"]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
collect_search('AND ' + query.join(' AND '))
|
88
|
+
end
|
89
|
+
|
90
|
+
def collect_search(query)
|
91
|
+
new_query = ['name:*', Config[:filter]].compact.join(' AND ')
|
92
|
+
new_query << ' ' << query
|
93
|
+
debug "Running search with query: #{new_query.inspect}"
|
94
|
+
result = []
|
95
|
+
idx = 0
|
96
|
+
while((new_items = Chef::Search::Query.new.search(:node, new_query, 'X_CHEF_id_CHEF_X asc', QUERY_ROWS * idx, QUERY_ROWS).first).size == QUERY_ROWS)
|
97
|
+
debug "Fetched #{new_items.size} new items from server"
|
98
|
+
result += new_items
|
99
|
+
idx += 1
|
100
|
+
end
|
101
|
+
debug "Adding last fetched items (#{new_items.size})"
|
102
|
+
result += new_items
|
103
|
+
result
|
104
|
+
end
|
105
|
+
|
106
|
+
def tag_stale_nodes
|
107
|
+
query = '-ohai_time:[* TO *] -prune_tag_time:[* TO *]'
|
108
|
+
nodes_to_tag = collect_search(query)
|
109
|
+
ui.info "Tagging nodes with no ohai_time set. (#{nodes_to_tag.size} nodes)"
|
110
|
+
debug "Nodes to be tagged: #{nodes_to_tag.map(&:name).sort.join(', ')}"
|
111
|
+
unless(Config[:print_only])
|
112
|
+
nodes_to_tag.each do |node|
|
113
|
+
node.set[:prune_tag_time] = Time.now.to_f
|
114
|
+
node.save
|
115
|
+
end
|
116
|
+
else
|
117
|
+
ui.warn 'Skipping node tagging due to print only restriction'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def prune_node(node)
|
122
|
+
prune_sensu(node) if sensu?
|
123
|
+
prune_chef(node) if chef?
|
124
|
+
end
|
125
|
+
|
126
|
+
def prune_sensu(node)
|
127
|
+
debug "Pruning node from sensu server: #{node.name}"
|
128
|
+
url = "http://#{args[:sensu][:host]}:#{args[:sensu][:port]}" <<
|
129
|
+
"/clients/#{node.name}"
|
130
|
+
begin
|
131
|
+
timeout(30) do
|
132
|
+
RestClient::Resource.new(
|
133
|
+
api_url,
|
134
|
+
:user => args[:sensu][:username],
|
135
|
+
:password => args[:sensu][:password]
|
136
|
+
).delete
|
137
|
+
end
|
138
|
+
rescue => e
|
139
|
+
ui.error "Failed to remove #{node.name} - Unexpected error: #{e}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def prune_chef(node)
|
144
|
+
debug "Pruning node from chef server: #{node.name}"
|
145
|
+
node.destroy
|
146
|
+
Chef::ApiClient.load(node.name).destroy
|
147
|
+
end
|
148
|
+
|
149
|
+
def filter_prunables_via_fog(nodes_to_prune)
|
150
|
+
nodes_to_prune.map do |node|
|
151
|
+
if(node[:cloud] && node.cloud.provider)
|
152
|
+
if(respond_to?(check_method = "#{node.cloud.provider}_check"))
|
153
|
+
send(check_method, node) ? node : nil
|
154
|
+
end
|
155
|
+
elsif(node[:prune_tag_time] && node[:ohai_time].nil?)
|
156
|
+
node
|
157
|
+
end
|
158
|
+
end.compact
|
159
|
+
end
|
160
|
+
|
161
|
+
## Checks
|
162
|
+
|
163
|
+
def ec2_check(node)
|
164
|
+
aws_node = ec2_nodes.detect{|n| n.id == node.ec2.instance_id}
|
165
|
+
unless(aws_node)
|
166
|
+
debug "#{node.name} returned nil from aws"
|
167
|
+
else
|
168
|
+
debug "#{node.name} state on aws: #{aws_node.state}"
|
169
|
+
end
|
170
|
+
aws_node.nil? || aws_node.state == 'terminated'
|
171
|
+
end
|
172
|
+
|
173
|
+
def ec2_nodes
|
174
|
+
unless(@ec2_nodes)
|
175
|
+
@ec2_nodes = compute(:aws).map do |compute_con|
|
176
|
+
compute_con.servers.all
|
177
|
+
end.flatten
|
178
|
+
end
|
179
|
+
@ec2_nodes
|
180
|
+
end
|
181
|
+
|
182
|
+
def rackspace_check(node)
|
183
|
+
raise 'Not implemented'
|
184
|
+
end
|
185
|
+
|
186
|
+
def debug(msg)
|
187
|
+
ui.info "#{ui.color('[DEBUG]', :magenta)} #{msg}" if Config[:debug]
|
188
|
+
end
|
189
|
+
end
|
data/lib/fog-prune.rb
ADDED
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fog-prune
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Roberts
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-05-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chef
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: fog
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.20'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.20'
|
46
|
+
description: Chef pruner
|
47
|
+
email: chrisroberts.code@gmail.com
|
48
|
+
executables:
|
49
|
+
- fog-prune
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- lib/fog-prune.rb
|
54
|
+
- lib/fog-prune/version.rb
|
55
|
+
- lib/fog-prune/options.rb
|
56
|
+
- lib/fog-prune/config.rb
|
57
|
+
- lib/fog-prune/prune.rb
|
58
|
+
- Gemfile
|
59
|
+
- README.md
|
60
|
+
- bin/fog-prune
|
61
|
+
- fog-prune.gemspec
|
62
|
+
- CHANGELOG.md
|
63
|
+
homepage: http://github.com/heavywater/fog-prune
|
64
|
+
licenses: []
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.8.24
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Chef pruning with fog integration
|
87
|
+
test_files: []
|
88
|
+
has_rdoc:
|