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 +7 -0
- data/LEGAL +13 -0
- data/lib/hula/bosh_director.rb +246 -0
- data/lib/hula/bosh_manifest/job.rb +35 -0
- data/lib/hula/bosh_manifest.rb +102 -0
- data/lib/hula/cloud_foundry/service_broker.rb +36 -0
- data/lib/hula/cloud_foundry.rb +308 -0
- data/lib/hula/command_runner.rb +38 -0
- data/lib/hula/helpers/socket_tools.rb +44 -0
- data/lib/hula/helpers/timeout_tools.rb +27 -0
- data/lib/hula/http_proxy_upstream_socks.rb +72 -0
- data/lib/hula/service_broker/api.rb +123 -0
- data/lib/hula/service_broker/catalog.rb +43 -0
- data/lib/hula/service_broker/client.rb +69 -0
- data/lib/hula/service_broker/errors.rb +24 -0
- data/lib/hula/service_broker/http_json_client.rb +90 -0
- data/lib/hula/service_broker/instance_binding.rb +30 -0
- data/lib/hula/service_broker/plan.rb +32 -0
- data/lib/hula/service_broker/service.rb +47 -0
- data/lib/hula/service_broker/service_instance.rb +24 -0
- data/lib/hula/socks4_proxy_ssh.rb +118 -0
- data/lib/hula/version.rb +13 -0
- data/lib/hula.rb +16 -0
- metadata +235 -0
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
|