hula 0.7.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c4c2d89b28b0e529f5236121502d5fc7da46a3db
4
+ data.tar.gz: 380624ff929ed4d53a4383132ca4a49ac75fd893
5
+ SHA512:
6
+ metadata.gz: 6d9b0269dd701fd4a12b864315ae06af48438e02197359e01663466c2bb7312d47258f642d84ef36475356fe5de214c14d4a14b0d1c4a59cf8a417cabe247963
7
+ data.tar.gz: 2fa96cadf394a54cc57e004a8e1d4ab21ce82f9702566482b70bb69dd15d39503538f713e94e8dc9c21871cf463f83aab7de32f8c355f494adbbd9f9d49447df
data/LEGAL ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2014-2015 Pivotal Software, Inc. All rights reserved.
2
+
3
+ Unauthorized use, copying or distribution of this source code via any
4
+ medium is strictly prohibited without the express written consent of
5
+ Pivotal Software, Inc.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
8
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
9
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
10
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
11
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
12
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
13
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,246 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'tempfile'
12
+ require 'yaml'
13
+ require 'socket'
14
+ require 'uri'
15
+
16
+ require 'hula/command_runner'
17
+
18
+ module Hula
19
+ class BoshDirector
20
+ class NoManifestSpecified < StandardError; end
21
+ class DirectorPortNotOpen < StandardError; end
22
+ class DirectorIsBroken < StandardError; end
23
+
24
+ def initialize(
25
+ target_url:,
26
+ username:,
27
+ password:,
28
+ manifest_path: nil,
29
+ command_runner: CommandRunner.new,
30
+ logger: default_logger
31
+ )
32
+ @target_url = target_url
33
+ @username = username
34
+ @password = password
35
+ @default_manifest_path = manifest_path
36
+ @command_runner = command_runner
37
+ @logger = logger
38
+
39
+ target_and_login
40
+ end
41
+
42
+ # Should rely on `bosh status` and CPI, but currently Bosh Lite is
43
+ # reporting 'vsphere' instead of 'warden'.
44
+ def lite?
45
+ target_url.include? '192.168.50.4'
46
+ end
47
+
48
+ def deploy(manifest_path = default_manifest_path)
49
+ run_bosh("--deployment #{manifest_path} deploy")
50
+ end
51
+
52
+ def delete_deployment(deployment_name, force: false)
53
+ cmd = ["delete deployment #{deployment_name}"]
54
+ cmd << '-f' if force
55
+ run_bosh(cmd.join(' '))
56
+ end
57
+
58
+ def run_errand(name, manifest_path: default_manifest_path)
59
+ run_bosh("--deployment #{manifest_path} run errand #{name}")
60
+ end
61
+
62
+ def recreate_all(jobs)
63
+ jobs.each do |name|
64
+ recreate(name)
65
+ end
66
+ end
67
+
68
+ def recreate_instance(name, index)
69
+ validate_job_instance_index(name, index)
70
+
71
+ run_bosh("recreate #{name} #{index}")
72
+ end
73
+
74
+ def recreate(name)
75
+ properties = job_properties(name)
76
+
77
+ instances = properties.fetch('instances')
78
+
79
+ instances.times do |instance_index|
80
+ run_bosh("recreate #{name} #{instance_index}")
81
+ end
82
+ end
83
+
84
+ def stop(name, index)
85
+ validate_job_instance_index(name, index)
86
+
87
+ run_bosh("stop #{name} #{index} --force")
88
+ end
89
+
90
+ def start(name, index)
91
+ validate_job_instance_index(name, index)
92
+
93
+ run_bosh("start #{name} #{index} --force")
94
+ end
95
+
96
+ def job_logfiles(job_name)
97
+ tmpdir = Dir.tmpdir
98
+ run_bosh("logs #{job_name} 0 --job --dir #{tmpdir}")
99
+ tarball = Dir[File.join(tmpdir, job_name.to_s + '*.tgz')].last
100
+ output = command_runner.run("tar tf #{tarball}")
101
+ lines = output.split(/\n+/)
102
+ filepaths = lines.map { |f| Pathname.new(f) }
103
+ logpaths = filepaths.select { |f| f.extname == '.log' }
104
+ logpaths.map(&:basename).map(&:to_s)
105
+ end
106
+
107
+ def has_logfiles?(job_name, logfile_names)
108
+ logs = job_logfiles(job_name)
109
+ logfile_names.each do |logfile_name|
110
+ return false unless logs.include?(logfile_name)
111
+ end
112
+ true
113
+ end
114
+
115
+ def deployment_names
116
+ deployments = run_bosh('deployments')
117
+ # [\n\r]+ a new line,
118
+ # \s* maybe followed by whitespace,
119
+ # \| followed by a pipe,
120
+ # \s+ followed by whitespace,
121
+ # ([^\s]+) followed some characters (ie, not whitespace, or a pipe) — this is the match
122
+ first_column = deployments.scan(/[\n\r]+\s*\|\s+([^\s\|]+)/).flatten
123
+
124
+ first_column.drop(1) # without header
125
+ end
126
+
127
+ # Parses output of `bosh vms` like below, getting an array of IPs for a job name
128
+ # +------------------------------------+---------+---------------+--------------+
129
+ # | Job/index | State | Resource Pool | IPs |
130
+ # +------------------------------------+---------+---------------+--------------+
131
+ # | api_z1/0 | running | large_z1 | 10.244.0.138 |
132
+ # ...
133
+ def ips_for_job(job, deployment_name = nil)
134
+ output = run_bosh("vms #{deployment_name}")
135
+ deployments = output.split(/^Deployment/)
136
+
137
+ job_ip_map = {}
138
+
139
+ deployments.each do |deployment|
140
+ rows = deployment.split("\n")
141
+ row_cols = rows.map { |row| row.split('|') }
142
+ job_cols = row_cols. select { |cols| cols.length == 5 } # match job boxes
143
+ job_ip_pairs = job_cols.map { |cols| [cols[1].strip, cols.last.strip] }
144
+ jobs_with_real_ips = job_ip_pairs.select { |pairs| pairs.last =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ }
145
+ # converts eg cf-redis-broker/2 to cf-redis-broker
146
+ jobs_without_instance_numbers = jobs_with_real_ips.map { |pair| [pair.first.gsub(/\/.*/, ''), pair.last] }
147
+ jobs_without_instance_numbers.each do |job|
148
+ name, ip = job
149
+ job_ip_map[name] ||= []
150
+ job_ip_map[name] << ip
151
+ end
152
+ end
153
+
154
+ job_ip_map.fetch(job, [])
155
+ end
156
+
157
+ private
158
+
159
+ attr_reader :target_url, :username, :password, :command_runner, :logger
160
+
161
+ def job_properties(job_name)
162
+ manifest.fetch('jobs').find { |job| job.fetch('name') == job_name }.tap do |properties|
163
+ fail ArgumentError.new('Job not found in manifest') unless properties
164
+ end
165
+ end
166
+
167
+ def validate_job_instance_index(job_name, index)
168
+ properties = job_properties(job_name)
169
+ instances = properties.fetch('instances')
170
+ fail ArgumentError.new('Index out of range') unless (0...instances).include? index
171
+ end
172
+
173
+ def default_logger
174
+ @default_logger ||= begin
175
+ STDOUT.sync = true
176
+ require 'logger'
177
+ Logger.new(STDOUT)
178
+ end
179
+ end
180
+
181
+ def default_manifest_path?
182
+ !!@default_manifest_path
183
+ end
184
+
185
+ def default_manifest_path
186
+ fail NoManifestSpecified unless default_manifest_path?
187
+ @default_manifest_path
188
+ end
189
+
190
+ def manifest
191
+ YAML.load_file(default_manifest_path)
192
+ end
193
+
194
+ def target_and_login
195
+ run_bosh("target #{target_url}")
196
+ run_bosh("deployment #{default_manifest_path}") if default_manifest_path?
197
+ run_bosh("login #{username} #{password}")
198
+ end
199
+
200
+ def run_bosh(cmd)
201
+ command = "bosh -v -n --config '#{bosh_config_path}' #{cmd}"
202
+ logger.info(command)
203
+
204
+ command_runner.run(command)
205
+ rescue CommandFailedError => e
206
+ logger.error(e.message)
207
+ health_check!
208
+ raise e
209
+ end
210
+
211
+ def bosh_config_path
212
+ # We should keep a reference to the tempfile, otherwise,
213
+ # when the object gets GC'd, the tempfile is deleted.
214
+ @bosh_config_tempfile ||= Tempfile.new('bosh_config')
215
+ @bosh_config_tempfile.path
216
+ end
217
+
218
+ def health_check!
219
+ check_port!
220
+ check_deployments!
221
+ end
222
+
223
+ def target_uri
224
+ @target_uri ||= URI.parse(target_url)
225
+ end
226
+
227
+ def check_deployments!
228
+ http = Net::HTTP.new(target_uri.host, target_uri.port)
229
+ http.use_ssl = target_uri.scheme == 'https'
230
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
231
+
232
+ response = http.get('/deployments')
233
+
234
+ unless response.is_a? Net::HTTPSuccess
235
+ fail DirectorIsBroken, "Failed to GET /deployments from #{target_uri}. Returned:\n\n#{response.to_hash}\n\n#{response.body}"
236
+ end
237
+ end
238
+
239
+ def check_port!
240
+ socket = TCPSocket.new(target_uri.host, target_uri.port)
241
+ socket.close
242
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
243
+ raise DirectorPortNotOpen, "Cannot connect to #{target_uri.host}:#{target_uri.port}"
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ module Hula
12
+ class BoshManifest
13
+ class Job
14
+ def initialize(job_hash)
15
+ @job_hash = job_hash
16
+ end
17
+
18
+ def static_ips
19
+ first_network.fetch('static_ips')
20
+ end
21
+
22
+ def properties
23
+ @job_hash.fetch('properties')
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :job_hash
29
+
30
+ def first_network
31
+ job_hash.fetch('networks').first
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,102 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'yaml'
12
+ require 'hula/bosh_manifest/job'
13
+
14
+ module Hula
15
+ class BoshManifest
16
+ class NoManifestPathGiven < StandardError; end
17
+
18
+ attr_reader :path
19
+
20
+ def initialize(manifest_yaml, path: nil)
21
+ @manifest_hash = YAML.load(manifest_yaml)
22
+ @path = path
23
+ fail 'Invalid manifest' unless manifest_hash.is_a?(Hash)
24
+ end
25
+
26
+ def self.from_file(path)
27
+ new(File.read(path), path: path)
28
+ rescue Errno::ENOENT
29
+ raise "Could not open the manifest file: '#{path}'"
30
+ end
31
+
32
+ def property(property_name)
33
+ components = property_components(property_name)
34
+ traverse_properties(components)
35
+ rescue KeyError
36
+ raise "Could not find property '#{property_name}' in #{properties.inspect}"
37
+ end
38
+
39
+ def set_property(property_name, value)
40
+ components = property_components(property_name)
41
+ traverse_properties_and_set(properties, components, value)
42
+ save
43
+ end
44
+
45
+ def deployment_name
46
+ manifest_hash.fetch('name')
47
+ rescue KeyError
48
+ raise "Could not find deployment name in #{manifest_hash.inspect}"
49
+ end
50
+
51
+ def job(job_name)
52
+ jobs = manifest_hash.fetch('jobs')
53
+ job = jobs.detect { |j| j.fetch('name') == job_name }
54
+
55
+ fail "Could not find job name '#{job_name}' in job list: #{jobs.inspect}" if job.nil?
56
+
57
+ Job.new(job)
58
+ end
59
+
60
+ def resource_pools
61
+ manifest_hash.fetch('resource_pools')
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :manifest_hash
67
+
68
+ def save
69
+ unless path
70
+ fail NoManifestPathGiven, 'Cannot save manifest without providing a path'
71
+ end
72
+ File.write(path, manifest_hash.to_yaml)
73
+ end
74
+
75
+ def properties
76
+ manifest_hash.fetch('properties')
77
+ end
78
+
79
+ def property_components(property_name)
80
+ property_name.split('.')
81
+ end
82
+
83
+ def traverse_properties(components)
84
+ components.inject(properties) do |current_node, component|
85
+ current_node.fetch(component)
86
+ end
87
+ end
88
+
89
+ def traverse_properties_and_set(properties, components, value)
90
+ component = components.shift
91
+
92
+ if components.any?
93
+ unless properties.key?(component)
94
+ fail "Could not find property '#{component}' in #{properties.inspect}"
95
+ end
96
+ traverse_properties_and_set(properties[component], components, value)
97
+ else
98
+ properties[component] = value
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,36 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'uri'
12
+
13
+ module Hula
14
+ class CloudFoundry
15
+ class ServiceBroker
16
+ attr_reader :name
17
+
18
+ def initialize(name:, url:)
19
+ @name = name
20
+ @url = url
21
+ end
22
+
23
+ def ==(other)
24
+ is_a?(other.class) && @name == other.name && @url == other.url
25
+ end
26
+
27
+ def uri
28
+ URI.parse(url)
29
+ end
30
+
31
+ protected
32
+
33
+ attr_reader :url
34
+ end
35
+ end
36
+ end