hula 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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