jetpants 0.7.8 → 0.7.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -87,6 +87,15 @@ module Jetpants
87
87
  raise "Plugin must override Topology#count_spares"
88
88
  end
89
89
 
90
+ # Returns a list of valid role symbols in use in Jetpants.
91
+ def valid_roles
92
+ [:master, :active_slave, :standby_slave, :backup_slave]
93
+ end
94
+
95
+ # Returns a list of valid role symbols which indicate a slave status
96
+ def slave_roles
97
+ valid_roles.reject {|r| r == :master}
98
+ end
90
99
 
91
100
  ###### Instance Methods ####################################################
92
101
 
@@ -144,6 +153,19 @@ module Jetpants
144
153
  claim_spares(1, options)[0]
145
154
  end
146
155
 
156
+ # Returns if the supplied role is valid
157
+ def valid_role? role
158
+ valid_roles.include? role.to_s.downcase.to_sym
159
+ end
160
+
161
+ # Converts the supplied roles (strings or symbols) into lowercase symbol versions
162
+ # Will expand out special role of :slave to be all slave roles.
163
+ def normalize_roles(*roles)
164
+ roles = roles.flatten.map {|r| r.to_s.downcase == 'slave' ? slave_roles.map(&:to_s) : r.to_s.downcase}.flatten
165
+ roles.each {|r| raise "#{r} is not a valid role" unless valid_role? r}
166
+ roles.uniq.map &:to_sym
167
+ end
168
+
147
169
  synchronized
148
170
  # Clears the pool list and nukes cached DB and Host object lookup tables
149
171
  def clear
@@ -0,0 +1,77 @@
1
+ # Adds conversion methods to the Collins::Asset class for obtaining Jetpants equivalents
2
+
3
+ module Collins
4
+ class Asset
5
+
6
+ # Convert a Collins::Asset to a Jetpants::DB. Requires asset TYPE to be SERVER_NODE.
7
+ def to_db
8
+ raise "Can only call to_db on SERVER_NODE assets, but #{self} has type #{type}" unless type.upcase == 'SERVER_NODE'
9
+ backend_ip_address.to_db
10
+ end
11
+
12
+
13
+ # Convert a Collins::Asset to a Jetpants::Host. Requires asset TYPE to be SERVER_NODE.
14
+ def to_host
15
+ raise "Can only call to_host on SERVER_NODE assets, but #{self} has type #{type}" unless type.upcase == 'SERVER_NODE'
16
+ backend_ip_address.to_host
17
+ end
18
+
19
+
20
+ # Convert a Collins::Asset to either a Jetpants::Pool or a Jetpants::Shard, depending
21
+ # on the value of PRIMARY_ROLE. Requires asset TYPE to be CONFIGURATION.
22
+ def to_pool
23
+ raise "Can only call to_pool on CONFIGURATION assets, but #{self} has type #{type}" unless type.upcase == 'CONFIGURATION'
24
+ raise "Unknown primary role #{primary_role} for configuration asset #{self}" unless ['MYSQL_POOL', 'MYSQL_SHARD'].include?(primary_role.upcase)
25
+ raise "No pool attribute set on asset #{self}" unless pool && pool.length > 0
26
+
27
+ # Find the master(s) for this pool. If we got back multiple masters, first
28
+ # try ignoring the remote datacenter ones
29
+ master_assets = Jetpants.topology.server_node_assets(pool.downcase, :master)
30
+ if master_assets.count > 1
31
+ results = master_assets.select {|a| a.location.nil? || a.location.upcase == Plugin::JetCollins.datacenter}
32
+ master_assets = results if results.count > 0
33
+ end
34
+ puts "WARNING: multiple masters found for pool #{pool}; using first match" if master_assets.count > 1
35
+
36
+ if master_assets.count == 0
37
+ puts "WARNING: no masters found for pool #{pool}; ignoring pool entirely"
38
+ result = nil
39
+
40
+ elsif primary_role.upcase == 'MYSQL_POOL'
41
+ result = Jetpants::Pool.new(pool.downcase, master_assets.first.to_db)
42
+ if aliases
43
+ aliases.split(',').each {|a| result.add_alias(a.downcase)}
44
+ end
45
+ result.slave_name = slave_pool_name if slave_pool_name
46
+ result.master_read_weight = master_read_weight if master_read_weight
47
+
48
+ # We intentionally only look for active slaves in the current datacenter, since we
49
+ # treat other datacenters' slaves as backup slaves to prevent promotion or cross-DC usage
50
+ active_slave_assets = Jetpants.topology.server_node_assets(pool.downcase, :active_slave)
51
+ active_slave_assets.reject! {|a| a.location && a.location.upcase != Plugin::JetCollins.datacenter}
52
+ active_slave_assets.each do |asset|
53
+ weight = asset.slave_weight && asset.slave_weight.to_i > 0 ? asset.slave_weight.to_i : 100
54
+ result.has_active_slave(asset.to_db, weight)
55
+ end
56
+
57
+ elsif primary_role.upcase == 'MYSQL_SHARD'
58
+ result = Jetpants::Shard.new(shard_min_id.to_i,
59
+ shard_max_id == 'INFINITY' ? 'INFINITY' : shard_max_id.to_i,
60
+ master_assets.first.to_db,
61
+ shard_state.downcase.to_sym)
62
+
63
+ # We'll need to set up the parent/child relationship if a shard split is in progress,
64
+ # BUT we need to wait to do that later since the shards may have been returned by
65
+ # Collins out-of-order, so the parent shard object might not exist yet.
66
+ # For now we just remember the NAME of the parent shard.
67
+ result.has_parent = shard_parent if shard_parent
68
+
69
+ else
70
+ raise "Unknown configuration asset primary role #{primary_role} for asset #{self}"
71
+ end
72
+
73
+ result
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,77 @@
1
+ # JetCollins monkeypatches to add Collins integration
2
+
3
+ module Jetpants
4
+ class DB
5
+
6
+ ##### JETCOLLINS MIX-IN ####################################################
7
+
8
+ include Plugin::JetCollins
9
+
10
+ collins_attr_accessor :slave_weight
11
+
12
+ # Because we only support 1 mysql instance per machine for now, we can just
13
+ # delegate this over to the host
14
+ def collins_asset
15
+ @host.collins_asset
16
+ end
17
+
18
+
19
+ ##### METHOD OVERRIDES #####################################################
20
+
21
+ # Add an actual collins check to confirm a machine is a standby
22
+ def is_standby?
23
+ !(running?) || (is_slave? && !taking_connections? && collins_secondary_role == 'standby_slave')
24
+ end
25
+
26
+ # Treat any node outside of current data center as being for backups.
27
+ # This prevents inadvertent cross-data-center master promotion.
28
+ def for_backups?
29
+ hostname.start_with?('backup') || in_remote_datacenter?
30
+ end
31
+
32
+
33
+ ##### CALLBACKS ############################################################
34
+
35
+ # Determine master from Collins if machine is unreachable or MySQL isn't running.
36
+ def after_probe_master
37
+ unless @running
38
+ if collins_secondary_role == 'master'
39
+ @master = false
40
+ else
41
+ pool = Jetpants.topology.pool(collins_pool)
42
+ @master = pool.master if pool
43
+ end
44
+ end
45
+
46
+ # We completely ignore cross-data-center master unless inter_dc_mode is enabled.
47
+ # This may change in a future Jetpants release, once we support tiered replication more cleanly.
48
+ if @master && @master.in_remote_datacenter? && !Jetpants::Plugin::JetCollins.inter_dc_mode?
49
+ @remote_master = @master # keep track of it, in case we need to know later
50
+ @master = false
51
+ elsif !@master
52
+ in_remote_datacenter? # just calling to cache for current node, before we probe its slaves, so that its slaves don't need to query Collins
53
+ end
54
+ end
55
+
56
+ # Determine slaves from Collins if machine is unreachable or MySQL isn't running
57
+ def after_probe_slaves
58
+ # If this machine has a master AND has slaves of its own AND is in another data center,
59
+ # ignore its slaves entirely unless inter_dc_mode is enabled.
60
+ # This may change in a future Jetpants release, once we support tiered replication more cleanly.
61
+ @slaves = [] if @running && @master && @slaves.count > 0 && in_remote_datacenter? && !Jetpants::Plugin::JetCollins.inter_dc_mode?
62
+
63
+ unless @running
64
+ p = Jetpants.topology.pool(self)
65
+ @slaves = (p ? p.slaves_according_to_collins : [])
66
+ end
67
+ end
68
+
69
+
70
+ ##### NEW METHODS ##########################################################
71
+
72
+ def in_remote_datacenter?
73
+ @host.collins_location != Plugin::JetCollins.datacenter
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,41 @@
1
+ # JetCollins monkeypatches to add Collins integration
2
+
3
+ module Jetpants
4
+ class Host
5
+
6
+ ##### JETCOLLINS MIX-IN ####################################################
7
+
8
+ include Plugin::JetCollins
9
+
10
+ def collins_asset
11
+ # try IP first; failing that, try hostname
12
+ selector = {ip_address: ip, details: true}
13
+ selector[:remoteLookup] = true if Jetpants.plugins['jetpants_collins']['remote_lookup']
14
+ assets = Plugin::JetCollins.find selector
15
+
16
+ if (!assets || assets.count == 0) && available?
17
+ selector = {hostname: "^#{hostname}$", details: true}
18
+ selector[:remoteLookup] = true if Jetpants.plugins['jetpants_collins']['remote_lookup']
19
+ assets = Plugin::JetCollins.find selector
20
+ end
21
+
22
+ raise "Multiple assets found for #{self}" if assets.count > 1
23
+ if ! assets || assets.count == 0
24
+ output "WARNING: no Collins assets found for this host"
25
+ nil
26
+ else
27
+ assets.first
28
+ end
29
+ end
30
+
31
+ # Returns which datacenter this host is in. Only a getter, intentionally no setter.
32
+ def collins_location
33
+ return @collins_location if @collins_location
34
+ ca = collins_asset
35
+ @collins_location ||= (ca ? ca.location || Plugin::JetCollins.datacenter : 'unknown')
36
+ @collins_location.upcase!
37
+ @collins_location
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,214 @@
1
+ require 'collins_client'
2
+
3
+ # Entrypoint for jetpants_collins plugin (namespace Jetpants::Plugin::JetCollins),
4
+ # which offers integration with the Collins hardware asset tracking system.
5
+ # This particular file accomplishes the following:
6
+ #
7
+ # * Provides a JetCollins mixin module. Any class including this should also
8
+ # implement a collins_asset method to convert objects to Collins assets;
9
+ # the class can then use the provided collins_get and collins_set wrappers,
10
+ # along with the collins_attr_accessor class method.
11
+ #
12
+ # * Jetpants::Plugin::JetCollins can also be used as a global Collins API
13
+ # client -- the module itself will delegate all missing methods to a
14
+ # Collins::Client object.
15
+ #
16
+ # * Loads monkeypatches for Jetpants classes DB, Host, Pool, Shard, Topology,
17
+ # and Collins class Asset.
18
+ #
19
+ # Configuration options in Jetpants config file include:
20
+ # user => collins account username (required)
21
+ # password => collins account password (required)
22
+ # url => collins URL (required)
23
+ # timeout => collins client timeout, in seconds (default: 30)
24
+ # datacenter => collins data center name that we're running Jetpants in the context of (required if multi-datacenter)
25
+ # remote_lookup => if true, supply remoteLookup parameter to search multiple datacenters (default: false)
26
+
27
+
28
+ module Jetpants
29
+ module Plugin
30
+ module JetCollins
31
+ @collins_service = nil
32
+
33
+ ##### CLASS METHODS ######################################################
34
+
35
+ class << self
36
+
37
+ # We delegate missing class (module) methods to the collins API client,
38
+ # if it responds to them.
39
+ def method_missing(name, *args, &block)
40
+ if service.respond_to? name
41
+ service.send name, *args, &block
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ # Eigenclass mix-in for collins_attr_accessor
48
+ # Calling "collins_attr_accessor :foo" in your class body will create
49
+ # methods collins_foo and collins_foo= which automatically get/set
50
+ # Collins attribute foo
51
+ def included(base)
52
+ base.class_eval do
53
+ def self.collins_attr_accessor(*fields)
54
+ fields.each do |field|
55
+ define_method("collins_#{field}") do
56
+ (collins_get(field) || '').downcase
57
+ end
58
+ define_method("collins_#{field}=") do |value|
59
+ collins_set(field, value)
60
+ end
61
+ end
62
+ end
63
+
64
+ # We make these 4 accessors available to ANY class including this mixin
65
+ collins_attr_accessor :primary_role, :secondary_role, :pool, :status
66
+ end
67
+ end
68
+
69
+ # Returns the 'datacenter' config option for this plugin, or 'UNKNOWN-DC' if
70
+ # none has been configured. This only matters in multi-datacenter Collins
71
+ # topologies.
72
+ def datacenter
73
+ (Jetpants.plugins['jetpants_collins']['datacenter'] || 'UNKNOWN-DC').upcase
74
+ end
75
+
76
+ # Ordinarily, in a multi-dacenter environment, jetpants_collins places a number
77
+ # of restrictions on interacting with assets that aren't in the local datacenter,
78
+ # for safety's sake and to simplify how hierarchical replication trees are represented:
79
+ #
80
+ # * Won't change Collins attributes on remote server node assets.
81
+ # * If a local node has a master in a remote datacenter, it is ignored/hidden.
82
+ # * If a local node has a slave in a remote datacenter, it's treated as a backup_slave,
83
+ # in order to prevent cross-datacenter master promotions. If any of these
84
+ # remote-datacenter slaves have slaves of their own, they're ignored/hidden.
85
+ #
86
+ # You may DISABLE these restrictions by calling enable_inter_dc_mode. Normally you
87
+ # do NOT want to do this, except in special sitautions like a migration between
88
+ # datacenters.
89
+ def enable_inter_dc_mode
90
+ Jetpants.plugins['jetpants_collins']['inter_dc_mode'] = true
91
+ Jetpants.plugins['jetpants_collins']['remote_lookup'] = true
92
+ end
93
+
94
+ # Returns true if enable_inter_dc_mode has been called, false otherwise.
95
+ def inter_dc_mode?
96
+ Jetpants.plugins['jetpants_collins']['inter_dc_mode'] || false
97
+ end
98
+
99
+
100
+ private
101
+
102
+ # Returns a Collins::Client object
103
+ def service
104
+ return @collins_service if @collins_service
105
+
106
+ %w(url user password).each do |setting|
107
+ raise "No Collins #{setting} set in plugins -> jetpants_collins -> #{setting}" unless Jetpants.plugins['jetpants_collins'][setting]
108
+ end
109
+
110
+ logger = Logger.new(STDOUT)
111
+ logger.level = Logger::INFO
112
+ config = {
113
+ :host => Jetpants.plugins['jetpants_collins']['url'],
114
+ :timeout => Jetpants.plugins['jetpants_collins']['timeout'] || 30,
115
+ :username => Jetpants.plugins['jetpants_collins']['user'],
116
+ :password => Jetpants.plugins['jetpants_collins']['password'],
117
+ :logger => logger,
118
+ }
119
+ @collins_service = Collins::Client.new(config)
120
+ end
121
+ end
122
+
123
+
124
+ ##### INSTANCE (MIX-IN) METHODS ##########################################
125
+
126
+ # The base class needs to implement this!
127
+ def collins_asset
128
+ raise "Any class including Plugin::JetCollins must also implement collins_asset instance method!"
129
+ end
130
+
131
+ # Pass in a symbol, or array of symbols, to obtain from Collins for this
132
+ # asset. For example, :status, :pool, :primary_role, :secondary_role.
133
+ # If you pass in a single symbol, returns a single value.
134
+ # If you pass in an array, returns a hash mapping each of these fields to their values.
135
+ # Hash will also contain an extra field called :asset, storing the Collins::Asset object.
136
+ def collins_get(*field_names)
137
+ asset = collins_asset
138
+ if field_names.count > 1 || field_names[0].is_a?(Array)
139
+ field_names.flatten!
140
+ results = Hash[field_names.map {|field| [field, (asset ? asset.send(field) : '')]}]
141
+ results[:asset] = asset
142
+ results
143
+ elsif field_names.count == 1
144
+ return '' unless asset
145
+ asset.send field_names[0]
146
+ else
147
+ nil
148
+ end
149
+ end
150
+
151
+ # Pass in a hash mapping field name symbols to values to set
152
+ # Symbol => String -- optionally set any Collins attribute
153
+ # :status => String -- optionally set the status value for the asset
154
+ # :asset => Collins::Asset -- optionally pass this in to avoid an extra Collins API lookup, if asset already obtained
155
+ #
156
+ # Alternatively, pass in 2 strings (field_name, value) to set just a single Collins attribute (or status)
157
+ def collins_set(*args)
158
+ attrs = (args.count == 1 ? args[0] : {args[0] => args[1]})
159
+ asset = attrs[:asset] || collins_asset
160
+
161
+ # refuse to set Collins values on machines in remote data center unless
162
+ # inter_dc_mode is enabled
163
+ if asset && asset.type.downcase == 'server_node' && asset.location && asset.location.upcase != Plugin::JetCollins.datacenter
164
+ asset = nil unless Jetpants::Plugin::JetCollins.inter_dc_mode?
165
+ end
166
+
167
+ attrs.each do |key, val|
168
+ val ||= ''
169
+ case key
170
+ when :asset
171
+ next
172
+ when :status
173
+ unless asset
174
+ output "WARNING: unable to set Collins status to #{val}"
175
+ next
176
+ end
177
+ previous_value = asset.status
178
+ if previous_value != val.to_s
179
+ success = Jetpants::Plugin::JetCollins.set_status!(asset, val)
180
+ raise "#{self}: Unable to set Collins status to #{val}" unless success
181
+ output "Collins status changed from #{previous_value} to #{val}"
182
+ end
183
+ else
184
+ unless asset
185
+ output "WARNING: unable to set Collins attribute #{key} to #{val}"
186
+ next
187
+ end
188
+ previous_value = asset.send(key)
189
+ if previous_value != val.to_s.upcase
190
+ success = Jetpants::Plugin::JetCollins.set_attribute!(asset, key.to_s.upcase, val.to_s.upcase)
191
+ raise "#{self}: Unable to set Collins attribute #{key} to #{val}" unless success
192
+ if (val.to_s == '' || !val) && (previous_value == '' || !previous_value)
193
+ false
194
+ elsif val.to_s == ''
195
+ output "Collins attribute #{key.to_s.upcase} removed (was: #{previous_value})"
196
+ elsif !previous_value || previous_value == ''
197
+ output "Collins attribute #{key.to_s.upcase} set to #{val.to_s.upcase}"
198
+ else
199
+ output "Collins attribute #{key.to_s.upcase} changed from #{previous_value} to #{val.to_s.upcase}"
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ end
206
+
207
+ end # module JetCollins
208
+ end # module Plugin
209
+ end
210
+
211
+
212
+ # load all the monkeypatches for other Jetpants classes
213
+ %w(asset host db pool shard topology).each {|mod| require "jetpants_collins/#{mod}"}
214
+
@@ -0,0 +1,145 @@
1
+ # JetCollins monkeypatches to add Collins integration
2
+
3
+ module Jetpants
4
+ class Pool
5
+
6
+ ##### JETCOLLINS MIX-IN ####################################################
7
+
8
+ include Plugin::JetCollins
9
+
10
+ # Used at startup time, to keep track of parent/child shard relationships
11
+ attr_accessor :has_parent
12
+
13
+ # Collins accessors for configuration asset metadata
14
+ collins_attr_accessor :slave_pool_name, :aliases, :master_read_weight, :config_sort_order
15
+
16
+ # Returns a Collins::Asset for this pool. Can optionally create one if not found.
17
+ def collins_asset(create_if_missing=false)
18
+ selector = {
19
+ operation: 'and',
20
+ details: true,
21
+ type: 'CONFIGURATION',
22
+ primary_role: 'MYSQL_POOL',
23
+ pool: "^#{@name.upcase}$",
24
+ status: 'Allocated',
25
+ }
26
+ selector[:remoteLookup] = true if Jetpants.plugins['jetpants_collins']['remote_lookup']
27
+
28
+ results = Plugin::JetCollins.find selector
29
+
30
+ # If we got back multiple results, try ignoring the remote datacenter ones
31
+ if results.count > 1
32
+ filtered_results = results.select {|a| a.location.nil? || a.location.upcase == Plugin::JetCollins.datacenter}
33
+ results = filtered_results if filtered_results.count > 0
34
+ end
35
+
36
+ if results.count > 1
37
+ raise "Multiple configuration assets found for pool #{name}"
38
+ elsif results.count == 0 && create_if_missing
39
+ output "Could not find configuration asset for pool; creating now"
40
+ new_tag = 'mysql-' + @name
41
+ asset = Collins::Asset.new type: 'CONFIGURATION', tag: new_tag, status: 'Allocated'
42
+ begin
43
+ Plugin::JetCollins.create!(asset)
44
+ rescue
45
+ collins_set asset: asset,
46
+ status: 'Allocated'
47
+ end
48
+ collins_set asset: asset,
49
+ primary_role: 'MYSQL_POOL',
50
+ pool: @name.upcase
51
+ Plugin::JetCollins.get new_tag
52
+ elsif results.count == 0 && !create_if_missing
53
+ raise "Could not find configuration asset for pool #{name}"
54
+ else
55
+ results.first
56
+ end
57
+ end
58
+
59
+
60
+ ##### METHOD OVERRIDES #####################################################
61
+
62
+ # Examines the current state of the pool (as known to Jetpants) and updates
63
+ # Collins to reflect this, in terms of the pool's configuration asset as
64
+ # well as the individual hosts.
65
+ def sync_configuration
66
+ asset = collins_asset(true)
67
+ collins_set asset: asset,
68
+ slave_pool_name: slave_name || '',
69
+ aliases: aliases.join(',') || '',
70
+ master_read_weight: master_read_weight
71
+ [@master, slaves].flatten.each do |db|
72
+ current_status = (db.collins_status || '').downcase
73
+ db.collins_status = 'Allocated' unless current_status == 'maintenance'
74
+ db.collins_pool = @name
75
+ end
76
+ @master.collins_secondary_role = 'MASTER'
77
+ slaves(:active).each do |db|
78
+ db.collins_secondary_role = 'ACTIVE_SLAVE'
79
+ weight = @active_slave_weights[db]
80
+ db.collins_slave_weight = (weight == 100 ? '' : weight)
81
+ end
82
+
83
+ slaves(:standby).each {|db| db.collins_secondary_role = 'STANDBY_SLAVE'}
84
+ slaves(:backup).each {|db| db.collins_secondary_role = 'BACKUP_SLAVE'}
85
+ true
86
+ end
87
+
88
+ # If the pool's master hasn't been probed yet, return active_slaves list
89
+ # based strictly on what we found in Collins. This is a major speed-up at
90
+ # start-up time, especially for tasks that need to iterate over all pools.
91
+ alias :active_slaves_from_probe :active_slaves
92
+ def active_slaves
93
+ if @master.probed?
94
+ active_slaves_from_probe
95
+ else
96
+ @active_slave_weights.keys
97
+ end
98
+ end
99
+
100
+
101
+ ##### CALLBACKS ############################################################
102
+
103
+ # Pushes slave removal to Collins. (Normally this type of logic is handled by
104
+ # Pool#sync_configuration, but that won't handle this case, since
105
+ # sync_configuration only updates hosts still in the pool.)
106
+ def after_remove_slave!(slave_db)
107
+ slave_db.collins_pool = slave_db.collins_secondary_role = slave_db.collins_slave_weight = ''
108
+ current_status = (slave_db.collins_status || '').downcase
109
+ slave_db.collins_status = 'Unallocated' unless current_status == 'maintenance'
110
+ end
111
+
112
+ # If the demoted master was offline, record some info in Collins, otherwise
113
+ # there will be 2 masters listed
114
+ def after_master_promotion!(promoted)
115
+ Jetpants.topology.clear_asset_cache
116
+
117
+ # Find the master asset(s) for this pool, filtering down to only current datacenter
118
+ assets = Jetpants.topology.server_node_assets(@name, :master)
119
+ assets.reject! {|a| a.location && a.location.upcase != Plugin::JetCollins.datacenter}
120
+ assets.map(&:to_db).each do |db|
121
+ if db != @master || !db.running?
122
+ db.collins_status = 'Maintenance'
123
+ db.collins_pool = ''
124
+ db.collins_secondary_role = ''
125
+ end
126
+ end
127
+ end
128
+
129
+
130
+ ##### NEW METHODS ##########################################################
131
+
132
+ # Called from DB#after_probe_master and DB#after_probe_slave for machines
133
+ # that are unreachable via SSH, or reachable but MySQL isn't running.
134
+ def slaves_according_to_collins
135
+ results = []
136
+ Jetpants.topology.server_node_assets(@name, :slave).each do |asset|
137
+ slave = asset.to_db
138
+ output "Collins found slave #{slave.ip} (#{slave.hostname})"
139
+ results << slave
140
+ end
141
+ results
142
+ end
143
+
144
+ end
145
+ end