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 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: