fog-prune 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # FogPrune
2
+
3
+ Prune node related data with the help of fog.
4
+
5
+ Prune data using fog for help.
6
+
7
+ ## Info
8
+
9
+ * Repository: https://github.com/heavywater/fog-prune
data/bin/fog-prune ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fog-prune'
4
+
5
+ FogPrune::Options.new.configure(ARGV)
6
+ FogPrune.new.prune!
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
@@ -0,0 +1,6 @@
1
+ class FogPrune
2
+ class Version < Gem::Version
3
+ end
4
+
5
+ VERSION = Version.new('0.1.0')
6
+ end
data/lib/fog-prune.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'fog-prune/version'
2
+ require 'fog-prune/prune'
3
+
4
+ class FogPrune
5
+ autoload :Config, 'fog-prune/config'
6
+ autoload :Options, 'fog-prune/options'
7
+ end
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: