dd_spacecadet 0.2.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.
@@ -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