skytap-yf 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.DS_Store +0 -0
- data/Gemfile +4 -0
- data/README.md +0 -0
- data/README.rdoc +6 -0
- data/api_schema.yaml +1016 -0
- data/bin/skytap +4 -0
- data/ca-bundle.crt +3721 -0
- data/data/.DS_Store +0 -0
- data/lib/skytap/api_schema.rb +11 -0
- data/lib/skytap/command_line.rb +145 -0
- data/lib/skytap/commands/base.rb +294 -0
- data/lib/skytap/commands/help.rb +82 -0
- data/lib/skytap/commands/http.rb +196 -0
- data/lib/skytap/commands/root.rb +79 -0
- data/lib/skytap/commands.rb +9 -0
- data/lib/skytap/core_ext.rb +8 -0
- data/lib/skytap/error.rb +4 -0
- data/lib/skytap/help_templates/help.erb +52 -0
- data/lib/skytap/ip_address.rb +63 -0
- data/lib/skytap/logger.rb +52 -0
- data/lib/skytap/plugins/vm_copy_to_region.rb +200 -0
- data/lib/skytap/plugins/vm_download.rb +431 -0
- data/lib/skytap/plugins/vm_upload.rb +401 -0
- data/lib/skytap/requester.rb +134 -0
- data/lib/skytap/response.rb +61 -0
- data/lib/skytap/skytaprc.rb +28 -0
- data/lib/skytap/subnet.rb +92 -0
- data/lib/skytap/templates.rb +216 -0
- data/lib/skytap/version.rb +5 -0
- data/lib/skytap.rb +149 -0
- data/skytap.gemspec +25 -0
- data/skytap.rdoc +5 -0
- metadata +143 -0
@@ -0,0 +1,401 @@
|
|
1
|
+
module Skytap
|
2
|
+
module Commands
|
3
|
+
class Upload < Skytap::Commands::Base
|
4
|
+
MAX_CONCURRENCY = 5
|
5
|
+
CHECK_PERIOD = 15
|
6
|
+
AT_CAPACITY_RETRY_PERIOD = 15.minutes.to_i
|
7
|
+
|
8
|
+
# What was passed in, either a reference to a VM directory or a VM archive file.
|
9
|
+
attr_reader :vms
|
10
|
+
attr_reader :uploaders
|
11
|
+
|
12
|
+
self.parent = Vm
|
13
|
+
self.plugin = true
|
14
|
+
self.ask_interactively = true
|
15
|
+
|
16
|
+
def self.description
|
17
|
+
<<-"EOF"
|
18
|
+
Upload one or more VM directories or VM archive files to Skytap
|
19
|
+
|
20
|
+
The file must be one of the following archive types:
|
21
|
+
.tgz .tar.gz .tbz .tbz2 .tar.bz2 .tar .ova .ovf .7z .7z.001 .zip
|
22
|
+
|
23
|
+
The contents of the archive file must contain a VMware image in either
|
24
|
+
VMX/VMDK or OVF/VMDK format.
|
25
|
+
|
26
|
+
See the following page for more information:
|
27
|
+
https://cloud.skytap.com/docs/index.php/Importing_and_Exporting_Virtual_Machines#How_to_Import_VMs_in_to_Skytap
|
28
|
+
EOF
|
29
|
+
end
|
30
|
+
|
31
|
+
def expected_args
|
32
|
+
ActiveSupport::OrderedHash[
|
33
|
+
'file*', 'One or more directories or VM archive files to upload'
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
def parameters
|
38
|
+
# Only display import parameters if a single VM will be uploaded.
|
39
|
+
if args.length == 1
|
40
|
+
Import.subcommands.detect{|s| s.command_name == 'create'}.spec['params']
|
41
|
+
else
|
42
|
+
{}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def run!
|
47
|
+
@vms = args
|
48
|
+
@uploaders = []
|
49
|
+
params = composed_params
|
50
|
+
|
51
|
+
until finished?
|
52
|
+
if vms.present? && slots_available?
|
53
|
+
kick_off_import(vms.shift, params.dup)
|
54
|
+
else
|
55
|
+
sleep CHECK_PERIOD
|
56
|
+
print_status
|
57
|
+
invoker.try(:call, self)
|
58
|
+
|
59
|
+
signal_available if signal_stale? || (concurrency_at_overage && concurrency < concurrency_at_overage)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
signal_available
|
64
|
+
print_summary
|
65
|
+
return response
|
66
|
+
end
|
67
|
+
|
68
|
+
def concurrency_at_overage
|
69
|
+
@concurrency_at_overage
|
70
|
+
end
|
71
|
+
|
72
|
+
def kick_off_import(vm, params)
|
73
|
+
begin
|
74
|
+
# Create import job on server in main thread.
|
75
|
+
job = VmImportJob.new(logger, username, api_token, vm, params)
|
76
|
+
|
77
|
+
# If successful, start FTP upload and subsequent steps in new thread.
|
78
|
+
up = Uploader.new(job)
|
79
|
+
uploaders << up
|
80
|
+
log_line(up.status_line)
|
81
|
+
rescue NoSlotsAvailable => ex
|
82
|
+
vms << vm
|
83
|
+
signal_full
|
84
|
+
log_line(("#{vm}: " << no_capacity_message).color(:yellow))
|
85
|
+
rescue Exception => ex
|
86
|
+
up = DeadUploader.new(vm, ex)
|
87
|
+
uploaders << up
|
88
|
+
log_line(up.status_line)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def no_capacity_message
|
93
|
+
m = AT_CAPACITY_RETRY_PERIOD / 60
|
94
|
+
"No import capacity is currently available on Skytap. Will retry in #{m} minutes".tap do |msg|
|
95
|
+
if active_uploaders.present?
|
96
|
+
msg << ' or when another import completes.'
|
97
|
+
else
|
98
|
+
msg << '.'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def log_line(msg, include_separator=true)
|
104
|
+
line = msg
|
105
|
+
line += "\n---" if include_separator
|
106
|
+
logger.info line
|
107
|
+
end
|
108
|
+
|
109
|
+
def print_status
|
110
|
+
if (stat = status_lines).present?
|
111
|
+
log_line(stat)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def print_summary
|
116
|
+
unless response.error?
|
117
|
+
logger.info "#{'Summary'.bright}\n#{response.payload}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def finished?
|
122
|
+
vms.empty? && concurrency == 0
|
123
|
+
end
|
124
|
+
|
125
|
+
def slots_available?
|
126
|
+
estimated_available_slots > 0
|
127
|
+
end
|
128
|
+
|
129
|
+
def signal_stale?
|
130
|
+
full? && Time.now - @full_at > AT_CAPACITY_RETRY_PERIOD
|
131
|
+
end
|
132
|
+
|
133
|
+
def seconds_until_retry
|
134
|
+
return unless full?
|
135
|
+
[0, AT_CAPACITY_RETRY_PERIOD - (Time.now - @full_at)].max
|
136
|
+
end
|
137
|
+
|
138
|
+
def estimated_available_slots
|
139
|
+
if full?
|
140
|
+
0
|
141
|
+
else
|
142
|
+
MAX_CONCURRENCY - concurrency
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def signal_full
|
147
|
+
@concurrency_at_overage = concurrency
|
148
|
+
@full_at = Time.now
|
149
|
+
end
|
150
|
+
|
151
|
+
def signal_available
|
152
|
+
@concurrency_at_overage = @full_at = nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def full?
|
156
|
+
@full_at
|
157
|
+
end
|
158
|
+
|
159
|
+
def status_lines
|
160
|
+
active_uploaders.collect(&:status_line).join("\n")
|
161
|
+
end
|
162
|
+
|
163
|
+
def active_uploaders
|
164
|
+
uploaders.reject(&:finished?)
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def concurrency
|
171
|
+
uploaders.select(&:alive?).length
|
172
|
+
end
|
173
|
+
|
174
|
+
def response
|
175
|
+
@_response ||= begin
|
176
|
+
error = !uploaders.any?(&:success?)
|
177
|
+
Response.build(uploaders.collect(&:status_line).join("\n"), error)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class NoSlotsAvailable < RuntimeError
|
183
|
+
end
|
184
|
+
|
185
|
+
class VmImportJob
|
186
|
+
attr_reader :logger, :import_path, :params, :vm, :username, :api_token, :other_credentials
|
187
|
+
|
188
|
+
def initialize(logger, username, api_token, vm, params = {})
|
189
|
+
@logger = logger
|
190
|
+
@username = username
|
191
|
+
@api_token = api_token
|
192
|
+
@vm = File.expand_path(vm)
|
193
|
+
@params = params
|
194
|
+
@vm_filename = File.basename(@vm)
|
195
|
+
|
196
|
+
create_on_server
|
197
|
+
end
|
198
|
+
|
199
|
+
def create_on_server
|
200
|
+
setup
|
201
|
+
begin
|
202
|
+
import
|
203
|
+
rescue Exception => ex
|
204
|
+
if at_capacity?(ex)
|
205
|
+
raise NoSlotsAvailable.new
|
206
|
+
else
|
207
|
+
raise
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def at_capacity?(exception)
|
213
|
+
exception.message.include?('You cannot import a VM because you may not have more than')
|
214
|
+
end
|
215
|
+
|
216
|
+
def setup
|
217
|
+
if File.directory?(vm)
|
218
|
+
@import_path = File.join(vm, 'vm.7z')
|
219
|
+
unless File.exist?(@import_path)
|
220
|
+
raise Skytap::Error.new("Directory provided (#{vm}) but no vm.7z file found inside")
|
221
|
+
end
|
222
|
+
|
223
|
+
metadata_file = File.join(vm, 'vm.yaml')
|
224
|
+
if File.exist?(metadata_file)
|
225
|
+
#TODO:NLA If hostname is present, truncate it to 15 chars. This is the max import hostname length.
|
226
|
+
extra_params = YAML.load_file(metadata_file)
|
227
|
+
end
|
228
|
+
|
229
|
+
elsif File.exist?(vm)
|
230
|
+
@import_path = vm
|
231
|
+
else
|
232
|
+
raise Skytap::Error.new("File does not exist: #{vm}")
|
233
|
+
end
|
234
|
+
|
235
|
+
@params = (extra_params || {}).merge(params)
|
236
|
+
if @params['credentials'].is_a?(Array)
|
237
|
+
cred = @params['credentials'].shift
|
238
|
+
@other_credentials = @params['credentials']
|
239
|
+
@params['credentials'] = cred
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def import(force_reload=false)
|
244
|
+
return @import unless @import.nil? || force_reload
|
245
|
+
|
246
|
+
if @import
|
247
|
+
id = @import['id']
|
248
|
+
@import = Skytap.invoke!(username, api_token, "import show #{id}")
|
249
|
+
else
|
250
|
+
@import = Skytap.invoke!(username, api_token, 'import create', {}, :param => params)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
class DeadUploader
|
256
|
+
def initialize(vm, exception)
|
257
|
+
@vm = vm
|
258
|
+
@exception = exception
|
259
|
+
end
|
260
|
+
|
261
|
+
def finished?
|
262
|
+
true
|
263
|
+
end
|
264
|
+
|
265
|
+
def alive?
|
266
|
+
false
|
267
|
+
end
|
268
|
+
|
269
|
+
def success?
|
270
|
+
false
|
271
|
+
end
|
272
|
+
|
273
|
+
def status_line
|
274
|
+
"VM #{@vm}: Error: #{@exception}"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class Uploader < Thread
|
279
|
+
MAX_WAIT = 2.days
|
280
|
+
IMPORT_CHECK_PERIOD = 5
|
281
|
+
|
282
|
+
attr_reader :job, :bytes_transferred, :bytes_total, :result
|
283
|
+
delegate :logger, :import, :import_path, :params, :vm, :username, :api_token, :other_credentials, :to => :job
|
284
|
+
|
285
|
+
def initialize(job)
|
286
|
+
@job = job
|
287
|
+
@bytes_transferred = @bytes_total = 0
|
288
|
+
path, basename = File.split(File.expand_path(import_path))
|
289
|
+
@vm_filename = File.join(File.basename(path), basename) # E.g., "myfolder/vm.7z"
|
290
|
+
@vm_filename << " (#{params['template_name']})" if params['template_name']
|
291
|
+
|
292
|
+
super do
|
293
|
+
begin
|
294
|
+
run
|
295
|
+
rescue Exception => ex
|
296
|
+
@result = Response.build(ex)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def run
|
302
|
+
ftp_upload
|
303
|
+
Skytap.invoke!(username, api_token, "import update #{id}", {}, :param => {'status' => 'processing'})
|
304
|
+
wait_until_ready
|
305
|
+
add_other_credentials
|
306
|
+
Skytap.invoke!(username, api_token, "import destroy #{id}")
|
307
|
+
@result = Response.build(import['template_url'])
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
def finished?
|
312
|
+
!!@finished
|
313
|
+
end
|
314
|
+
|
315
|
+
def success?
|
316
|
+
result && !result.error?
|
317
|
+
end
|
318
|
+
|
319
|
+
def status_line
|
320
|
+
"#{@vm_filename}: #{status}"
|
321
|
+
end
|
322
|
+
|
323
|
+
def status
|
324
|
+
if result.try(:error?)
|
325
|
+
@finished = true
|
326
|
+
"Error: #{result.error_message}".color(:red).bright
|
327
|
+
elsif result
|
328
|
+
@finished = true
|
329
|
+
"Uploaded to: #{result.payload}".color(:green).bright
|
330
|
+
elsif bytes_transferred == 0
|
331
|
+
'Starting'.color(:yellow)
|
332
|
+
elsif bytes_transferred >= bytes_total
|
333
|
+
'Importing'.color(:yellow)
|
334
|
+
else
|
335
|
+
gb_transferred = bytes_transferred / 1.gigabyte.to_f
|
336
|
+
gb_total = bytes_total / 1.gigabyte.to_f
|
337
|
+
percent_done = 100.0 * bytes_transferred / bytes_total
|
338
|
+
"Uploading #{'%0.1f' % percent_done}% (#{'%0.1f' % gb_transferred} / #{'%0.1f' % gb_total} GB)".color(:yellow)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
private
|
344
|
+
|
345
|
+
|
346
|
+
def ftp_upload
|
347
|
+
local_path = import_path
|
348
|
+
# FTP URL looks like: ftp://67x8meqCr:a3hnalxZLg@ftp.cloud.skytap.com/upload/
|
349
|
+
remote_dir = import['ftp_url'] =~ %r{@.*?(/.+)$} && $1
|
350
|
+
|
351
|
+
ftp = Net::FTP.new(import['ftp_host'])
|
352
|
+
ftp.login(import['ftp_user_name'], import['ftp_password'])
|
353
|
+
ftp.chdir(remote_dir)
|
354
|
+
@bytes_total = File.size(local_path)
|
355
|
+
ftp.putbinaryfile(local_path) do |data|
|
356
|
+
@bytes_transferred += data.size
|
357
|
+
end
|
358
|
+
ftp.close
|
359
|
+
end
|
360
|
+
|
361
|
+
def add_other_credentials
|
362
|
+
template_id = import['template_url'] =~ /\/templates\/(\d+)/ && $1
|
363
|
+
template = Skytap.invoke!(username, api_token, "template show #{template_id}")
|
364
|
+
vm_id = template['vms'].first['id']
|
365
|
+
(other_credentials || []).each do |cred|
|
366
|
+
Skytap.invoke!(username, api_token, "credential create /vms/#{vm_id}", {}, :param => {'vm_id' => vm_id, 'text' => cred})
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def id
|
371
|
+
import['id']
|
372
|
+
end
|
373
|
+
|
374
|
+
def wait_until_ready
|
375
|
+
cutoff = MAX_WAIT.from_now
|
376
|
+
finished = nil
|
377
|
+
|
378
|
+
while Time.now < cutoff
|
379
|
+
case import(true)['status']
|
380
|
+
when 'processing'
|
381
|
+
when 'complete'
|
382
|
+
finished = true
|
383
|
+
break
|
384
|
+
else
|
385
|
+
#TODO:NLA Check that this actually is displayed to the user, for both normal `vm upload` and for `vm copytoregion` too.
|
386
|
+
raise Skytap::Error.new "Import job had unexpected state of #{import['status'].inspect}"
|
387
|
+
end
|
388
|
+
|
389
|
+
sleep IMPORT_CHECK_PERIOD
|
390
|
+
end
|
391
|
+
|
392
|
+
unless finished
|
393
|
+
raise Skytap::Error.new 'Timed out waiting for import job to complete'
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
#TODO:NLA Probably should pull this into a method that also sets e.g., Upload.parent = Vm.
|
399
|
+
Vm.subcommands << Upload
|
400
|
+
end
|
401
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Skytap
|
2
|
+
class Requester
|
3
|
+
#TODO:NLA Move more of the implementation in request() below into this class.
|
4
|
+
class Response
|
5
|
+
attr_reader :logger, :http_response
|
6
|
+
def initialize(logger, http_response, options = {})
|
7
|
+
@logger = logger
|
8
|
+
@http_response = http_response
|
9
|
+
|
10
|
+
logger.debug 'Response:'.color(:cyan).bright
|
11
|
+
each_header do |k, v|
|
12
|
+
logger.debug "#{k}: #{v}".color(:cyan)
|
13
|
+
end
|
14
|
+
logger.debug "\n#{body}".color(:cyan)
|
15
|
+
logger.debug '---'.color(:cyan).bright
|
16
|
+
|
17
|
+
# Raise unless response is 2XX.
|
18
|
+
@http_response.value if options[:raise]
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(*args, &block)
|
22
|
+
http_response.send(*args, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def pretty_body
|
26
|
+
return body if body.blank?
|
27
|
+
if content_type.include?('json')
|
28
|
+
begin
|
29
|
+
JSON.pretty_generate(JSON.load(body))
|
30
|
+
rescue
|
31
|
+
body
|
32
|
+
end
|
33
|
+
else
|
34
|
+
body
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :logger, :format
|
40
|
+
|
41
|
+
def initialize(logger, username, api_token, base_url, http_format, verify_certs=true)
|
42
|
+
@logger = logger
|
43
|
+
@username = username or raise Skytap::Error.new 'No username provided'
|
44
|
+
@api_token = api_token or raise Skytap::Error.new 'No API token provided'
|
45
|
+
@base_uri = URI.parse(base_url) or raise Skytap::Error.new 'No base URL provided'
|
46
|
+
@format = "application/#{http_format}" or raise Skytap::Error.new 'No HTTP format provided'
|
47
|
+
@verify_certs = verify_certs
|
48
|
+
end
|
49
|
+
|
50
|
+
# Raises on error code unless :raise => false is passed.
|
51
|
+
def request(method, url, options = {})
|
52
|
+
options = options.symbolize_keys
|
53
|
+
options[:raise] = true unless options.has_key?(:raise)
|
54
|
+
|
55
|
+
with_session do |http|
|
56
|
+
begin
|
57
|
+
#TODO:NLA Move this into method
|
58
|
+
if p = options[:params]
|
59
|
+
if p.is_a?(Hash)
|
60
|
+
p = p.collect {|k,v| CGI.escape(k) + '=' + CGI.escape(v)}.join('&')
|
61
|
+
end
|
62
|
+
|
63
|
+
path, params = url.split('?', 2)
|
64
|
+
if params
|
65
|
+
params << '&'
|
66
|
+
else
|
67
|
+
params = ''
|
68
|
+
end
|
69
|
+
params << p
|
70
|
+
url = [path, params].join('?')
|
71
|
+
end
|
72
|
+
|
73
|
+
body = options[:body] || ''
|
74
|
+
headers = base_headers.merge(options[:headers] || {})
|
75
|
+
logger.debug 'Request:'.color(:cyan).bright
|
76
|
+
logger.debug "#{method} #{url}\n#{headers.collect {|k,v| "#{k}:#{v}"}.join("\n")}\n\n#{body}".color(:cyan)
|
77
|
+
logger.debug '---'.color(:cyan).bright
|
78
|
+
|
79
|
+
response = Response.new(logger, http.send_request(method, url, body, headers),
|
80
|
+
:raise => options[:raise])
|
81
|
+
rescue OpenSSL::SSL::SSLError => ex
|
82
|
+
$stderr.puts <<-"EOF"
|
83
|
+
An SSL error occurred (probably certificate verification failed).
|
84
|
+
|
85
|
+
If you are pointed against an internal test environment, set
|
86
|
+
"verify-certs: false" in your ~/.skytaprc file or use --no-verify-certs.
|
87
|
+
|
88
|
+
If not, then you may be subject to a man-in-the-middle attack, or the web
|
89
|
+
site's SSL certificate may have expired.
|
90
|
+
|
91
|
+
The error was: #{ex}
|
92
|
+
EOF
|
93
|
+
exit(-1)
|
94
|
+
rescue Net::HTTPServerException => ex
|
95
|
+
logger.debug 'Response:'.color(:cyan).bright
|
96
|
+
logger.info "Code: #{ex.response.code} #{ex.response.message}".color(:red).bright
|
97
|
+
logger.info ex.response.body
|
98
|
+
raise
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def base_headers
|
104
|
+
headers = {
|
105
|
+
'Authorization' => auth_header,
|
106
|
+
'Content-Type' => format,
|
107
|
+
'Accept' => format,
|
108
|
+
'User-Agent' => "SkytapCLI/#{Skytap::VERSION}",
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def auth_header
|
113
|
+
"Basic #{Base64.encode64(@username + ":" + @api_token)}".gsub("\n", '')
|
114
|
+
end
|
115
|
+
|
116
|
+
def with_session
|
117
|
+
http = Net::HTTP.new(@base_uri.host, @base_uri.port)
|
118
|
+
http.use_ssl = @base_uri.port == 443 || @base_uri.scheme == 'https'
|
119
|
+
|
120
|
+
if http.use_ssl?
|
121
|
+
# Allow cert-checking to be disabled, since internal test environments
|
122
|
+
# have bad certs.
|
123
|
+
if @verify_certs
|
124
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
125
|
+
http.ca_file = File.join(File.dirname(__FILE__), '..', '..', 'ca-bundle.crt')
|
126
|
+
else
|
127
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
yield http
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Skytap
|
2
|
+
class Response
|
3
|
+
def self.build(result, is_error=nil)
|
4
|
+
case result
|
5
|
+
when self
|
6
|
+
result
|
7
|
+
when Requester::Response
|
8
|
+
is_error ||= result.code !~ /^[123]/
|
9
|
+
if is_error && result.payload.is_a?(Hash)
|
10
|
+
err_msg = "Server error (code #{result.code}): " +
|
11
|
+
(result.payload['error'] ||
|
12
|
+
result.payload['errors'].try(:join, ' ') ||
|
13
|
+
result.body)
|
14
|
+
end
|
15
|
+
new(result.payload, is_error, err_msg)
|
16
|
+
when Skytap::Error
|
17
|
+
new(result, true, result.to_s)
|
18
|
+
when Exception
|
19
|
+
log_exception(result)
|
20
|
+
new(result, true, "Internal error: #{result}")
|
21
|
+
else
|
22
|
+
new(result, is_error)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :payload
|
27
|
+
|
28
|
+
def initialize(payload, error = nil, error_message = nil)
|
29
|
+
@payload = payload
|
30
|
+
@error = error
|
31
|
+
if @error
|
32
|
+
@error_message = error_message || @payload.to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def error?
|
37
|
+
@error
|
38
|
+
end
|
39
|
+
|
40
|
+
def error_message
|
41
|
+
@error_message.color(:red).bright
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
|
48
|
+
def self.log_exception(ex)
|
49
|
+
begin
|
50
|
+
message = "#{Time.now}: -- #{Skytap::VERSION} -- " +
|
51
|
+
"#{ex} (#{ex.class.name})\n#{ex.backtrace.join("\n")}\n"
|
52
|
+
|
53
|
+
File.open(File.expand_path(File.join(ENV['HOME'], '.skytap.log')), 'a') do |f|
|
54
|
+
f << message
|
55
|
+
end
|
56
|
+
rescue
|
57
|
+
# No-op
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Skytap
|
2
|
+
module SkytapRC
|
3
|
+
extend self
|
4
|
+
|
5
|
+
RC_FILE = File.expand_path(File.join(ENV['HOME'], '.skytaprc'))
|
6
|
+
RECOGNIZED_OPTIONS = [:'verify-certs', :'base-url', :'api-token', :'http-format', :ask, :'log-level', :username, :colorize]
|
7
|
+
|
8
|
+
def exists?
|
9
|
+
File.exist?(RC_FILE)
|
10
|
+
end
|
11
|
+
|
12
|
+
def load
|
13
|
+
@rc_contents ||= (if File.exist?(RC_FILE)
|
14
|
+
YAML.load_file(RC_FILE).symbolize_keys
|
15
|
+
else
|
16
|
+
{}
|
17
|
+
end).subset(RECOGNIZED_OPTIONS)
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(hash)
|
21
|
+
@rc_contents = nil
|
22
|
+
hash = (hash || {}).subset(RECOGNIZED_OPTIONS)
|
23
|
+
File.open(RC_FILE, 'w') do |f|
|
24
|
+
f << YAML.dump(hash)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'skytap/ip_address'
|
2
|
+
|
3
|
+
class Subnet
|
4
|
+
class InvalidSubnet < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :size, :mask, :address
|
8
|
+
|
9
|
+
def initialize(cidr_block)
|
10
|
+
unless cidr_block =~ /^(.*)\/(.*)/
|
11
|
+
raise InvalidSubnet.new 'Not in CIDR block form (XX.XX.XX.XX/YY)'
|
12
|
+
end
|
13
|
+
|
14
|
+
network = $1
|
15
|
+
begin
|
16
|
+
@size = Integer($2)
|
17
|
+
rescue
|
18
|
+
raise InvalidSubnet.new 'Invalid size'
|
19
|
+
end
|
20
|
+
|
21
|
+
@address = IpAddress.new(network)
|
22
|
+
@mask = size_to_mask(@size)
|
23
|
+
end
|
24
|
+
|
25
|
+
def network_portion
|
26
|
+
@network_portion ||= (mask & address)
|
27
|
+
end
|
28
|
+
alias_method :min, :network_portion
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
other.is_a?(Subnet) && \
|
32
|
+
(size == other.size) && \
|
33
|
+
network_portion == other.network_portion
|
34
|
+
end
|
35
|
+
|
36
|
+
def overlaps?(other)
|
37
|
+
min <= other.max && max >= other.min
|
38
|
+
end
|
39
|
+
|
40
|
+
def subsumes?(other)
|
41
|
+
min <= other.min && max >= other.max
|
42
|
+
end
|
43
|
+
|
44
|
+
def strictly_subsumes?(other)
|
45
|
+
subsumes?(other) && self != other
|
46
|
+
end
|
47
|
+
|
48
|
+
def max
|
49
|
+
@max ||= (mask.inverse | network_portion)
|
50
|
+
end
|
51
|
+
|
52
|
+
def each_address
|
53
|
+
(min.to_i..max.to_i).each{|i| yield IpAddress.new(i)}
|
54
|
+
end
|
55
|
+
|
56
|
+
def contains?(ip)
|
57
|
+
ip = IpAddress.new(ip) unless ip.is_a?(IpAddress)
|
58
|
+
(ip & mask) == network_portion
|
59
|
+
end
|
60
|
+
|
61
|
+
def num_addresses
|
62
|
+
2 ** (32-size)
|
63
|
+
end
|
64
|
+
|
65
|
+
def min_machine_ip
|
66
|
+
min + 1
|
67
|
+
end
|
68
|
+
|
69
|
+
def normalized?
|
70
|
+
address == network_portion
|
71
|
+
end
|
72
|
+
|
73
|
+
def normalize
|
74
|
+
Subnet.new("#{network_portion}/#{size}")
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
"#{address}/#{size}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def <=>(other)
|
82
|
+
to_s <=> other.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def size_to_mask(numbits)
|
88
|
+
raise InvalidSubnet.new("Subnet size in bits must be between 0 and 32") unless numbits >= 0 and numbits <= 32
|
89
|
+
IpAddress.new((2**numbits - 1) << (32 - numbits))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|