dd_spacecadet 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,49 @@
1
+ # SpaceCadet
2
+ SpaceCadet is a library written in Ruby for interacting with the Rackspace Cloud Load Balancers.
3
+ The library itself uses the `fog` gem, which is a very popular Ruby library for interacting with
4
+ all of the different cloud providers (including Rackspace).
5
+
6
+ ## License
7
+ SpaceCadet is released under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. See the [LICENSE](https://github.com/doubledutch/spacecadet/blob/master/LICENSE) file for full the full details of the license.
8
+
9
+ ## Why SpaceCadet?
10
+ Because this library is meant to speed up deploys, and it's a little "special"...
11
+
12
+ It's not meant to be pretty, clean, or re-usable. This library was written with one purpose in mind:
13
+ being able to change a single backend node from `ENABLED` to `DRAINING` within multiple LBs.
14
+
15
+ Also: `(Rack)space`.
16
+
17
+ ## Including in Gemfile
18
+
19
+ ```Ruby
20
+ gem 'dd_spacecadet', git: 'git@ddgit.me:EngOps/spacecadet.git'
21
+ ```
22
+
23
+ ## Usage
24
+ Here's an example of using the library while interacting with the `DFW` region:
25
+
26
+ ```Ruby
27
+ require 'dd_spacecadet'
28
+
29
+ env = 'dfw-prod'
30
+ region = 'DFW'
31
+
32
+ DoubleDutch::SpaceCadet::Config.register(
33
+ env, ENV['RS_CLOUD_USERNAME'], ENV['RS_CLOUD_KEY'], region
34
+ )
35
+
36
+ dfw_prod = DoubleDutch::SpaceCadet::LB.new(env)
37
+
38
+ # search for an LB by its label, in this example "prod-lb"
39
+ # if multiple LBs match it will use *ALL* of them
40
+ dfw_prod.find_lb_and_use('prod-lb')
41
+
42
+ # gets the status of each LB and its nodes
43
+ # you can use dfw.print_status to print the info to stdout with formatting
44
+ dfw.status
45
+
46
+ dfw.update_node('node01', :draining)
47
+
48
+ dfw.update_node('node01', :enabled)
49
+ ```
@@ -0,0 +1,31 @@
1
+ # Copyright 2016 DoubleDutch, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'rubocop/rake_task'
16
+ require 'rspec/core/rake_task'
17
+ require 'bundler/gem_tasks'
18
+
19
+ RSpec::Core::RakeTask.new(:spec)
20
+
21
+ desc 'Run RuboCop'
22
+ RuboCop::RakeTask.new(:rubocop) do |t|
23
+ t.options = %w(-D)
24
+ t.fail_on_error = true
25
+ t.patterns = %w(
26
+ Rakefile Gemfile *.gemspec
27
+ lib/**/*.rb spec/**/*.rb
28
+ )
29
+ end
30
+
31
+ task default: [:rubocop, :spec]
@@ -0,0 +1,40 @@
1
+ # Copyright 2016 DoubleDutch, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
16
+
17
+ require 'dd_spacecadet/version'
18
+
19
+ Gem::Specification.new do |s|
20
+ s.name = 'dd_spacecadet'
21
+ s.summary = 'Library for manipulating Rackspace Cloud Load Balancers'
22
+ s.author = 'DoubleDutch Engineering Operations'
23
+ s.email = 'engops@doubledutch.me'
24
+ s.license = 'Apache 2.0'
25
+ s.version = DoubleDutch::SpaceCadet::VERSION
26
+ s.required_ruby_version = '~> 2.3'
27
+ s.date = Time.now.strftime('%Y-%m-%d')
28
+ s.homepage = 'https://github.com/DoubleDutch/spacecadet'
29
+ s.description = 'Rubygem for safely managing Rackspace Cloud Load Balancer backend servers'
30
+
31
+ s.test_files = `git ls-files spec/*`.split
32
+ s.files = `git ls-files`.split
33
+
34
+ s.add_development_dependency 'rake', '~> 11.2.2'
35
+ s.add_development_dependency 'rspec', '~> 3.5.0'
36
+ s.add_development_dependency 'rubocop', '~> 0.42.0'
37
+ s.add_development_dependency 'irbtools', '~> 2.0.1'
38
+
39
+ s.add_runtime_dependency 'fog', '~> 1.38.0'
40
+ end
@@ -0,0 +1,27 @@
1
+ # Copyright 2016 DoubleDutch, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'dd_spacecadet/version'
16
+ require 'dd_spacecadet/util'
17
+ require 'dd_spacecadet/error'
18
+ require 'dd_spacecadet/config'
19
+ require 'dd_spacecadet/node_ip'
20
+ require 'dd_spacecadet/lb'
21
+
22
+ # DoubleDutch is the top-level module for
23
+ # internal DoubleDutch modules and classes
24
+ module DoubleDutch
25
+ # SpaceCadet is a module for configuring the Rackspace Cloud Load Balancers
26
+ module SpaceCadet; end
27
+ end
@@ -0,0 +1,57 @@
1
+ # Copyright 2016 DoubleDutch, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'fog'
16
+
17
+ module DoubleDutch
18
+ module SpaceCadet
19
+ # Class Config is used to configure a client for a specific Rackspace account
20
+ # You provide the `env`, usually for format is <geo>-<env> (e.g., dfw-prod)
21
+ class Config
22
+ @@servers_client = {}
23
+ @@lbs_client = {}
24
+
25
+ class << self
26
+ def register(env, username, key, region)
27
+ # init servers_client if it is nil
28
+ @@servers_client[env] ||= Fog::Compute.new(
29
+ provider: 'rackspace',
30
+ rackspace_username: username,
31
+ rackspace_api_key: key,
32
+ rackspace_region: region
33
+ )
34
+
35
+ # init lbs_client if it is nil
36
+ @@lbs_client[env] ||= Fog::Rackspace::LoadBalancers.new(
37
+ rackspace_username: username,
38
+ rackspace_api_key: key,
39
+ rackspace_region: region
40
+ )
41
+ end
42
+
43
+ # DoubleDutch::SpaceCadet::Config.servers.client
44
+ # returns @@servers_client
45
+ def servers_client
46
+ @@servers_client
47
+ end
48
+
49
+ # DoubleDutch::SpaceCadet::Config.lbs.client
50
+ # returns @@lbs_client
51
+ def lbs_client
52
+ @@lbs_client
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ # Copyright 2016 DoubleDutch, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module DoubleDutch
16
+ module SpaceCadet
17
+ class Error < StandardError; end
18
+ class ServerNotFound < DoubleDutch::SpaceCadet::Error; end
19
+ class LBNotFound < DoubleDutch::SpaceCadet::Error; end
20
+ class LBInconsistentState < DoubleDutch::SpaceCadet::Error; end
21
+ class LBUnsafe < DoubleDutch::SpaceCadet::Error; end
22
+ class MalformedNodeObject < DoubleDutch::SpaceCadet::Error; end
23
+ end
24
+ end
@@ -0,0 +1,194 @@
1
+ # Copyright 2016 DoubleDutch, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'dd_spacecadet/error'
16
+ require 'dd_spacecadet/util'
17
+
18
+ module DoubleDutch
19
+ module SpaceCadet
20
+ # LB is the class used for manging the Load Balancer configs
21
+ class LB
22
+ attr_reader :env, :lbs
23
+
24
+ def initialize(env)
25
+ @env = env
26
+ @lbs = []
27
+ end
28
+
29
+ # reset the class (clear the added LBs)
30
+ def reset
31
+ @lbs.clear
32
+ end
33
+
34
+ # add an LB, by ID, to be managed by this class
35
+ def add_lb(lb_id)
36
+ @lbs = (@lbs << lb_id).uniq
37
+ end
38
+
39
+ # find an LB using a search string
40
+ def find_lb(search)
41
+ DoubleDutch::SpaceCadet::Util.find_lb(@env, search)
42
+ end
43
+
44
+ # the same as find_lb, but it adds each to the classs
45
+ def find_lb_and_use(search)
46
+ find_lb(search).each { |lb| add_lb(lb[:id]) }
47
+ end
48
+
49
+ # gets the status of managed LBs
50
+ def status
51
+ details = @lbs.map { |lb_id| get_lb_details(lb_id) }
52
+
53
+ parse_lb_details(details)
54
+ end
55
+
56
+ # updates the condition of a node with the Load Balancer
57
+ # this is used to move from :enabled => :draining
58
+ def update_node(name, condition)
59
+ # check whether the condition is valid
60
+ unless [:enabled, :draining].include?(condition)
61
+ raise ArgumentError, 'Invalid condition (can be :enabled or :draining)'
62
+ end
63
+
64
+ lb_details = status
65
+
66
+ raise LoadBalancerNotFound, 'No LB details found!' if lb_details.empty?
67
+
68
+ to_update = calculate_update(name.downcase, lb_details, condition)
69
+
70
+ if to_update.size != lb_details.size
71
+ raise LBInconsistentState, "We only found #{to_update.size} nodes across #{lb_details.size} LBs"
72
+ end
73
+
74
+ flush_updates(to_update)
75
+ end
76
+
77
+ # this does the same thing as status
78
+ # put it prints it the information to stdout
79
+ def render_status
80
+ status.each do |st|
81
+ puts "#{st[:name]} (#{st[:id]})"
82
+ st[:nodes].each { |n| puts " #{n[:name]} #{n[:condition]} #{n[:id]} #{n[:ip]}" }
83
+ puts '---'
84
+ end
85
+
86
+ nil
87
+ end
88
+
89
+ private
90
+
91
+ def lbs_client
92
+ DoubleDutch::SpaceCadet::Config.lbs_client[@env]
93
+ end
94
+
95
+ def get_lb_details(lb_id)
96
+ lbs_client.get_load_balancer(lb_id).data[:body]
97
+ end
98
+
99
+ def flush_updates(to_update)
100
+ to_update.each do |update|
101
+ flush_update(update)
102
+ end
103
+ end
104
+
105
+ # safety check before taking actions
106
+ # this makes sure we cannot set more than one node to draining
107
+ def safe?(lbd, condition)
108
+ # if it's a draining operation
109
+ # check for safety
110
+ if condition == :draining
111
+ # this inverts the result of the boolean statement
112
+ # if these conditions are met, we are *NOT* safe
113
+ # if there is onle one mode enabled: NOT SAFE
114
+ # if any node is disabled/draining: NOT SAFE
115
+ return !(lbd[:nodes_enabled] == 1 || lbd[:nodes_enabled] != lbd[:nodes].size)
116
+ end
117
+
118
+ # otherwise, it's safe
119
+ true
120
+ end
121
+
122
+ # calculate which node IDs need to be updated
123
+ def calculate_update(name, details, condition)
124
+ to_update = []
125
+
126
+ # loop over the individual load balancers
127
+ details.each do |lbd|
128
+ # make sure this LB is in a safe state to mutate
129
+ unless safe?(lbd, condition)
130
+ raise LBUnsafe, "#{lbd[:name]} LB unsafe for draining"
131
+ end
132
+
133
+ # loop over the registered nodes to find the ID
134
+ # of the one we want to update the condition of
135
+ lbd[:nodes].each do |lbn|
136
+ to_update << { lb_id: lbd[:id], node_id: lbn[:id], condition: condition } if lbn[:name] == name
137
+ end
138
+ end
139
+
140
+ to_update
141
+ end
142
+
143
+ def flush_update(update)
144
+ call_update(update)
145
+ # immediately after updating an LB config, Rackspace marks the LB
146
+ # as being immutable (meaning no further config changes can happen)
147
+ # this exception below is what is thrown by Fog if we hit that situation
148
+ rescue Fog::Rackspace::LoadBalancers::ServiceError
149
+ # the LB is currently marked as being immutable
150
+ # wait N arbitrary seconds before trying again
151
+ sleep(5)
152
+ call_update(update)
153
+ end
154
+
155
+ def call_update(update)
156
+ lbs_client.update_node(
157
+ update[:lb_id],
158
+ update[:node_id],
159
+ condition: update[:condition].to_s.upcase
160
+ )
161
+ end
162
+
163
+ def parse_lb_details(details)
164
+ details.map do |lb|
165
+ detail = {
166
+ name: lb['loadBalancer']['name'].downcase,
167
+ id: lb['loadBalancer']['id']
168
+ }
169
+
170
+ detail[:nodes], detail[:nodes_enabled] = parse_nodes(lb['loadBalancer']['nodes'])
171
+
172
+ detail
173
+ end
174
+ end
175
+
176
+ def parse_nodes(nodes)
177
+ enabled_count = 0
178
+
179
+ n = nodes.map do |node|
180
+ enabled_count += 1 if node['condition'].casecmp('enabled').zero?
181
+
182
+ {
183
+ name: DoubleDutch::SpaceCadet::NodeIP.get_name_for(@env, node['address']),
184
+ ip: node['address'],
185
+ id: node['id'],
186
+ condition: node['condition']
187
+ }
188
+ end
189
+
190
+ [n.sort { |x, y| x[:name] <=> y[:name] }, enabled_count]
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,104 @@
1
+ # Copyright 2016 DoubleDutch, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'dd_spacecadet/error'
16
+ require 'dd_spacecadet/config'
17
+
18
+ module DoubleDutch
19
+ module SpaceCadet
20
+ # NodeIP is a class of helper methods to find a node
21
+ # based on its name or IP address
22
+ class NodeIP
23
+ # internal data structures
24
+ @@nodes_by_name = {}
25
+ @@nodes_by_ip = {}
26
+
27
+ class << self
28
+ # get the IP address for a node
29
+ # based on its label (name)
30
+ def get_ip_for(env, name)
31
+ refresh_nodes(env)
32
+
33
+ ip = @@nodes_by_name.dig(env, name)
34
+
35
+ raise ServerNotFound, "unable to locate #{name} in #{env} data" if ip.nil?
36
+
37
+ ip
38
+ end
39
+
40
+ # get the label (name) for a node
41
+ # based on its IP address
42
+ def get_name_for(env, ip)
43
+ refresh_nodes(env)
44
+
45
+ name = @@nodes_by_ip.dig(env, ip)
46
+
47
+ raise ServerNotFound, "unable to locate #{ip} in #{env} data" if name.nil?
48
+
49
+ name
50
+ end
51
+
52
+ # clear flushes all cached data
53
+ def clear(env)
54
+ @@nodes_by_name.delete(env)
55
+ @@nodes_by_ip.delete(env)
56
+ end
57
+
58
+ private
59
+
60
+ # if any of the internal structures are nil or empty
61
+ # we should probably try to do an update
62
+ def needs_refresh?(env)
63
+ (@@nodes_by_name[env].nil? || @@nodes_by_name[env].empty?) ||
64
+ (@@nodes_by_ip[env].nil? || @@nodes_by_ip[env].empty?)
65
+ end
66
+
67
+ # this gets the details of the nodes we care about:
68
+ # name and IP
69
+ def get_details(server)
70
+ priv_addresses = server.dig('addresses', 'private')
71
+
72
+ raise MailformedNodeObject, 'Node missing private addresses' if priv_addresses.nil? || priv_addresses.empty?
73
+
74
+ [server['name'].downcase, priv_addresses[0]['addr']]
75
+ end
76
+
77
+ # refresh the information we have by pulling down a listing of all
78
+ # nodes from Rackspace
79
+ def refresh_nodes(env)
80
+ # only refresh if a refresh is needed
81
+ if needs_refresh?(env)
82
+ # get an Array of all of the servers
83
+ servers = DoubleDutch::SpaceCadet::Config.servers_client[env].list_servers.data[:body]['servers']
84
+
85
+ # hbn: HashByName
86
+ # hbi: HashByIp
87
+ hbn = {}
88
+ hbi = {}
89
+
90
+ servers.each do |server|
91
+ name, ip = get_details(server)
92
+
93
+ hbn[name] = ip
94
+ hbi[ip] = name
95
+ end
96
+
97
+ @@nodes_by_name[env] = hbn
98
+ @@nodes_by_ip[env] = hbi
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end