skytap-yf 0.2.3
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.
- 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,196 @@
|
|
1
|
+
require 'skytap/requester'
|
2
|
+
|
3
|
+
module Skytap
|
4
|
+
module Commands
|
5
|
+
class HttpBase < Base
|
6
|
+
self.ask_interactively = false
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# This is a factory that is called on subclasses.
|
10
|
+
def make_for(command_class, spec={})
|
11
|
+
command_name = self.command_name
|
12
|
+
Class.new(self).tap do |klass|
|
13
|
+
klass.parent = command_class
|
14
|
+
klass.singleton_class.send(:define_method, :command_name) { command_name }
|
15
|
+
klass.spec = spec || {}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def requester
|
21
|
+
base_url = global_options[:'base-url'] or raise Skytap::Error.new 'Must provide base-url option'
|
22
|
+
http_format = global_options[:'http-format'] or raise Skytap::Error.new 'Must provide http-format option'
|
23
|
+
verify_certs = global_options.has_key?(:'verify-certs') ? global_options[:'verify-certs'] : true
|
24
|
+
|
25
|
+
@requester ||= Requester.new(logger, username, api_token, base_url, http_format, verify_certs)
|
26
|
+
end
|
27
|
+
|
28
|
+
def root_path
|
29
|
+
"/#{resource.pluralize}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource_path(id)
|
33
|
+
"#{root_path}/#{id}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def resource
|
37
|
+
parent.command_name
|
38
|
+
end
|
39
|
+
|
40
|
+
def encode_body(params)
|
41
|
+
return unless params.present?
|
42
|
+
|
43
|
+
case format = global_options[:'http-format']
|
44
|
+
when 'json'
|
45
|
+
JSON.dump(params)
|
46
|
+
when 'xml'
|
47
|
+
params.to_xml(:root => resource)
|
48
|
+
else
|
49
|
+
raise "Unknown http-format: #{format.inspect}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def request(method, path, options={})
|
54
|
+
response = requester.request(method, path, options_for_request)
|
55
|
+
success = response.code.start_with?('2')
|
56
|
+
|
57
|
+
logger.info "Code: #{response.code} #{response.message}".color(success ? :green : :red).bright
|
58
|
+
logger.puts response.pretty_body
|
59
|
+
response.tap do |resp|
|
60
|
+
resp.singleton_class.instance_eval do
|
61
|
+
define_method(:payload) do
|
62
|
+
return unless body.present?
|
63
|
+
|
64
|
+
case self['Content-Type']
|
65
|
+
when /json/i
|
66
|
+
JSON.load(body)
|
67
|
+
when /xml/i
|
68
|
+
parsed = Hash.from_xml(body)
|
69
|
+
# Strip out the root name.
|
70
|
+
if parsed.is_a?(Hash)
|
71
|
+
parsed.values.first
|
72
|
+
else
|
73
|
+
parsed
|
74
|
+
end
|
75
|
+
else
|
76
|
+
body
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def options_for_request
|
84
|
+
{:raise => false}.tap do |options|
|
85
|
+
if ask_interactively
|
86
|
+
options[:body] = encode_body(composed_params)
|
87
|
+
else
|
88
|
+
options[:params] = composed_params
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def expected_args
|
94
|
+
ActiveSupport::OrderedHash.new.tap do |expected|
|
95
|
+
if parent_resources = parent.spec['parent_resources']
|
96
|
+
example_path = join_paths(*parent_resources.collect {|r| [r.pluralize, "#{r.singularize.upcase}-ID"]}.flatten)
|
97
|
+
expected['parent_path'] = "path of parent #{'resource'.pluralize(parent_resources.length)}, in the form #{example_path}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def parent_path
|
103
|
+
if expected_args['parent_path']
|
104
|
+
index = expected_args.keys.index('parent_path')
|
105
|
+
path = args[index].dup
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def path
|
110
|
+
path = parent_path || ''
|
111
|
+
if expected_args['id']
|
112
|
+
index = expected_args.keys.index('id')
|
113
|
+
id = args[index]
|
114
|
+
join_paths(path, resource_path(id))
|
115
|
+
else
|
116
|
+
join_paths(path, root_path)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def join_paths(*parts)
|
121
|
+
'/' + parts.collect{|p| p.split('/')}.flatten.reject(&:blank?).join('/')
|
122
|
+
end
|
123
|
+
|
124
|
+
def get(*args) ; request('GET', *args) ; end
|
125
|
+
def post(*args) ; request('POST', *args) ; end
|
126
|
+
def put(*args) ; request('PUT', *args) ; end
|
127
|
+
def delete(*args) ; request('DELETE', *args) ; end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
class Show < HttpBase
|
132
|
+
def expected_args
|
133
|
+
super.merge('id' => "ID or URL of #{resource} to show")
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.default_description
|
137
|
+
"Show specified #{parent.command_name.gsub('_', ' ')}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def run!
|
141
|
+
get(path)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Index < HttpBase
|
146
|
+
def self.default_description
|
147
|
+
"Show all #{parent.command_name.pluralize.gsub('_', ' ')} to which you have access"
|
148
|
+
end
|
149
|
+
|
150
|
+
def run!
|
151
|
+
get(path)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class Create < HttpBase
|
156
|
+
self.ask_interactively = true
|
157
|
+
|
158
|
+
def self.default_description
|
159
|
+
"Create #{parent.command_name.gsub('_', ' ')}"
|
160
|
+
end
|
161
|
+
|
162
|
+
def run!
|
163
|
+
post(path)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class Destroy < HttpBase
|
168
|
+
def expected_args
|
169
|
+
super.merge('id' => "ID or URL of #{resource} to delete")
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.default_description
|
173
|
+
"Delete specified #{parent.command_name.gsub('_', ' ')}"
|
174
|
+
end
|
175
|
+
|
176
|
+
def run!
|
177
|
+
delete(path)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class Update < HttpBase
|
182
|
+
def expected_args
|
183
|
+
super.merge('id' => "ID or URL of #{resource} to update")
|
184
|
+
end
|
185
|
+
self.ask_interactively = true
|
186
|
+
|
187
|
+
def self.default_description
|
188
|
+
"Update attributes of specified #{parent.command_name.gsub('_', ' ')}"
|
189
|
+
end
|
190
|
+
|
191
|
+
def run!
|
192
|
+
put(path)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Skytap
|
2
|
+
module Commands
|
3
|
+
class Root < Base
|
4
|
+
self.container = true
|
5
|
+
|
6
|
+
def self.go!(logger, args, global_options, command_options, programmatic_context=false, &invoker)
|
7
|
+
new(logger, args, global_options, command_options, programmatic_context, &invoker).invoke
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.banner_prefix
|
11
|
+
'skytap'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.command_name
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.populate_with(specification={})
|
19
|
+
self.subcommands = specification.sort.collect do |resource, spec|
|
20
|
+
Base.make_from(resource, spec).tap do |klass|
|
21
|
+
klass.parent = self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def run!
|
27
|
+
help!
|
28
|
+
end
|
29
|
+
|
30
|
+
def help_with_initial_setup!
|
31
|
+
if SkytapRC.exists? || !solicit_user_input?
|
32
|
+
help_without_initial_setup!
|
33
|
+
else
|
34
|
+
logger.puts <<-"EOF"
|
35
|
+
Do you want to store your API credentials in a text file for convenience?
|
36
|
+
|
37
|
+
If you answer yes, your username and API token will be stored in a .skytaprc
|
38
|
+
file in plain text. If you answer no, you must provide your username and API
|
39
|
+
token each time you invoke this tool.
|
40
|
+
|
41
|
+
EOF
|
42
|
+
if ask('Store username?', ActiveSupport::OrderedHash['y', true, 'n', false, :default, 'y'])
|
43
|
+
username = ask('Skytap username:') while username.blank?
|
44
|
+
end
|
45
|
+
if ask('Store API token?', ActiveSupport::OrderedHash['y', true, 'n', false, :default, 'y'])
|
46
|
+
logger.puts "\nYour API security token is on the My Account page.\nIf missing, ask your admin to enable API tokens on the Security Policies page."
|
47
|
+
# Allow API token to be blank, in case the user realizes they don't have one.
|
48
|
+
api_token = ask('API token:')
|
49
|
+
end
|
50
|
+
|
51
|
+
logger.puts <<-"EOF"
|
52
|
+
|
53
|
+
Do you want to turn on interactive mode?
|
54
|
+
|
55
|
+
If so, you will be shown the request parameters available for a command and
|
56
|
+
asked if you want to enter any. If you plan to use this tool primarily in
|
57
|
+
scripts, you may want to answer no.
|
58
|
+
EOF
|
59
|
+
|
60
|
+
ask_mode = ask('Enable interactive mode?', ActiveSupport::OrderedHash['y', true, 'n', false, :default, 'y'])
|
61
|
+
|
62
|
+
config = global_options.symbolize_keys.merge(:username => username, :'api-token' => api_token, :ask => ask_mode)
|
63
|
+
|
64
|
+
SkytapRC.write(config)
|
65
|
+
|
66
|
+
logger.puts <<-"EOF"
|
67
|
+
Config file written to #{SkytapRC::RC_FILE}.
|
68
|
+
|
69
|
+
Example commands:
|
70
|
+
#{'skytap'.bright} - show all commands
|
71
|
+
#{'skytap help configuration'.bright} - show configuration commands
|
72
|
+
#{'skytap help vm upload'.bright} - help for VM upload command
|
73
|
+
EOF
|
74
|
+
end
|
75
|
+
end
|
76
|
+
alias_method_chain :help!, :initial_setup
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/skytap/error.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
<% if error_msg -%>
|
2
|
+
<%= error_msg %>
|
3
|
+
|
4
|
+
|
5
|
+
<% end -%>
|
6
|
+
<% if command.container -%>
|
7
|
+
<%= header('SUBCOMMANDS') %>
|
8
|
+
<% subcommands.each do |subcommand| -%>
|
9
|
+
<%= subcommand %>
|
10
|
+
<% end -%>
|
11
|
+
|
12
|
+
<%#TODO:NLA Only display this message if there are any scenarios/plugins. -%>
|
13
|
+
Scenarios and plugins are highlighted in blue.
|
14
|
+
<% else -%>
|
15
|
+
<%= header('SYNOPSIS') %>
|
16
|
+
<%= command.synopsis %>
|
17
|
+
<% end -%>
|
18
|
+
<% if command.description -%>
|
19
|
+
|
20
|
+
<%= header('DESCRIPTION') %>
|
21
|
+
<%= indent(command.class.description) %>
|
22
|
+
<% end -%>
|
23
|
+
<% if command.expected_args.present? -%>
|
24
|
+
|
25
|
+
<%= header('ARGUMENTS') %>
|
26
|
+
<% command.expected_args.each do |name, desc| -%>
|
27
|
+
<%= name.upcase %> - <%= desc %>
|
28
|
+
<% end -%>
|
29
|
+
<% end -%>
|
30
|
+
<% if command.expected_options.present? -%>
|
31
|
+
|
32
|
+
<%= header('COMMAND OPTIONS') %>
|
33
|
+
<% command.expected_options.each do |name, info| -%>
|
34
|
+
<%= info[:flag_arg] ? "--#{name}=#{info[:flag_arg].upcase.gsub('-', '_')}" : "--#{name}" %> - <%= info[:desc] %>
|
35
|
+
<% end -%>
|
36
|
+
<% end -%>
|
37
|
+
|
38
|
+
<%= header('GLOBAL OPTIONS') %>
|
39
|
+
<%= global_options_table %>
|
40
|
+
<% if command.parameters.present? -%>
|
41
|
+
|
42
|
+
<%= header('PARAMETERS') %>
|
43
|
+
You may provide the following request parameters, either interactively
|
44
|
+
with --ask or using --param or --params-file:
|
45
|
+
|
46
|
+
<%= parameters_table %>
|
47
|
+
<% end -%>
|
48
|
+
<% if error_msg -%>
|
49
|
+
|
50
|
+
|
51
|
+
<%= error_msg %>
|
52
|
+
<% end -%>
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class IpAddress
|
2
|
+
class InvalidIp < RuntimeError; end
|
3
|
+
IP_REGEX = /^(0[0-7]*|0[x][0-9a-f]+|[1-9][0-9]*)\.(0[0-7]*|0[x][0-9a-f]+|[1-9][0-9]*)\.(0[0-7]*|0[x][0-9a-f]+|[1-9][0-9]*)\.(0[0-7]*|0[x][0-9a-f]+|[1-9][0-9]*)$/i
|
4
|
+
MAX_IP4_INT = 2**32 - 1
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def str_to_int(str)
|
8
|
+
raise InvalidIp.new("'#{str}' does not look like an IP") unless str =~ IP_REGEX
|
9
|
+
bytes = [Integer($1), Integer($2), Integer($3), Integer($4)]
|
10
|
+
raise InvalidIp.new("'#{str}' octet exceeds 255") if bytes.any?{|b| b > 255}
|
11
|
+
bytes.zip([24, 16, 8, 0]).collect{|n,shift| n << shift}.inject(&:+)
|
12
|
+
end
|
13
|
+
def int_to_str(i)
|
14
|
+
raise InvalidIp.new("#{i} exceeds maximum IPv4 address") if i > MAX_IP4_INT
|
15
|
+
[24, 16, 8, 0].collect{|shift| (i & (255 << shift)) >> shift}.join('.')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(str_or_int)
|
20
|
+
if str_or_int.is_a?(String)
|
21
|
+
@i = self.class.str_to_int(str_or_int)
|
22
|
+
elsif str_or_int.is_a?(Integer)
|
23
|
+
raise InvalidIp.new("IP #{str_or_int} is greater than the maximum IPv4 value") if str_or_int > MAX_IP4_INT
|
24
|
+
raise InvalidIp.new("IP value must be non-negative") if str_or_int < 0
|
25
|
+
@i = str_or_int
|
26
|
+
else
|
27
|
+
raise InvalidIp.new("Don't know how to make an IP out of #{str_or_int}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_i
|
32
|
+
@i
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
self.class.int_to_str(@i)
|
37
|
+
end
|
38
|
+
|
39
|
+
MAX_IP4 = self.new(MAX_IP4_INT)
|
40
|
+
|
41
|
+
# IpAddress arithmetic ops result in new IP addresses
|
42
|
+
[:+, :-, :&, :|, :<<, :>>].each do |name|
|
43
|
+
define_method(name) do |other|
|
44
|
+
IpAddress.new(@i.send(name, other.to_i))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# IpAddress comparisons just proxy to the .to_i value
|
49
|
+
[:==, :<, :>, :>=, :<=, :<=>].each do |name|
|
50
|
+
define_method(name) do |other|
|
51
|
+
@i.send(name, other.to_i)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def inverse
|
56
|
+
MAX_IP4 - self
|
57
|
+
end
|
58
|
+
alias_method :~, :inverse
|
59
|
+
|
60
|
+
def succ
|
61
|
+
self + 1
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Skytap
|
2
|
+
class Logger
|
3
|
+
attr_accessor :log_level
|
4
|
+
|
5
|
+
def initialize(log_level)
|
6
|
+
@log_level = log_level
|
7
|
+
end
|
8
|
+
|
9
|
+
# The message is logged unconditionally.
|
10
|
+
def puts(msg = '', options = {})
|
11
|
+
do_log(msg, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
# The message is logged unless the --quiet option is present.
|
15
|
+
def info(msg = '', options = {})
|
16
|
+
if log_level == 'info' || log_level == 'verbose'
|
17
|
+
do_log(msg, options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug(msg = '', options = {})
|
22
|
+
if log_level == 'verbose'
|
23
|
+
do_log(msg, options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mute!
|
28
|
+
@muted = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def unmute!
|
32
|
+
@muted = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def muted?
|
36
|
+
!!@muted
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def do_log(msg, options = {})
|
43
|
+
return if muted?
|
44
|
+
options = options.symbolize_keys
|
45
|
+
newline = options.has_key?(:newline) ? options[:newline] : true
|
46
|
+
$stdout.flush
|
47
|
+
$stdout.print msg
|
48
|
+
$stdout.puts if newline
|
49
|
+
$stdout.flush
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module Skytap
|
2
|
+
module Commands
|
3
|
+
class CopyToRegion < Skytap::Commands::Base
|
4
|
+
CHECK_PERIOD = 20
|
5
|
+
|
6
|
+
self.parent = Vm
|
7
|
+
self.plugin = true
|
8
|
+
|
9
|
+
def self.command_name
|
10
|
+
'copytoregion'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.description
|
14
|
+
<<-"EOF"
|
15
|
+
Copy one or more VMs to a new region
|
16
|
+
|
17
|
+
This process involves exporting the VMs to the local filesystem,
|
18
|
+
importing them into the new region, then deleting them from the local
|
19
|
+
filesystem. Ensure you have sufficient disk space.
|
20
|
+
EOF
|
21
|
+
end
|
22
|
+
|
23
|
+
def expected_args
|
24
|
+
ActiveSupport::OrderedHash[
|
25
|
+
'region', 'Name of target region',
|
26
|
+
'vm_id*', 'One or more IDs of template VMs to copy to the region'
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
def expected_options
|
31
|
+
ActiveSupport::OrderedHash[
|
32
|
+
:tmpdir, {:flag_arg => 'TMPDIR', :desc => 'Temporary directory into which to download VMs'},
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :copiers
|
37
|
+
|
38
|
+
def run!
|
39
|
+
@copiers = []
|
40
|
+
region = args.shift
|
41
|
+
vm_ids = args.collect {|a| find_id(a)}
|
42
|
+
|
43
|
+
until vm_ids.empty? && concurrency == 0
|
44
|
+
if vm_ids.present?
|
45
|
+
cop = Copier.new(logger, username, api_token, vm_ids.shift, region, command_options[:tmpdir])
|
46
|
+
copiers << cop
|
47
|
+
if (line = cop.status_line).present?
|
48
|
+
logger.info "#{line}\n---"
|
49
|
+
end
|
50
|
+
else
|
51
|
+
sleep CHECK_PERIOD
|
52
|
+
if (lines = status_lines).present?
|
53
|
+
logger.info "#{lines}\n---"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
response.tap do |res|
|
59
|
+
logger.info "#{'Summary:'.bright}\n#{res.payload}" unless res.error?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def status_lines
|
64
|
+
copiers.reject(&:finished?).collect(&:status_line).reject(&:blank?).join("\n")
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def concurrency
|
71
|
+
copiers.select(&:alive?).length
|
72
|
+
end
|
73
|
+
|
74
|
+
def response
|
75
|
+
error = !copiers.any?(&:success?)
|
76
|
+
Response.build(copiers.collect(&:summary).join("\n"), error)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Copier < Thread
|
81
|
+
attr_reader :logger, :vm_id, :region, :root_dir, :result, :username, :api_token
|
82
|
+
|
83
|
+
def initialize(logger, username, api_token, vm_id, region, root_dir = nil)
|
84
|
+
@logger = logger
|
85
|
+
@username = username
|
86
|
+
@api_token = api_token
|
87
|
+
@vm_id = vm_id
|
88
|
+
@region = region
|
89
|
+
@root_dir = File.expand_path(root_dir || '.')
|
90
|
+
|
91
|
+
warn_if_manual_network
|
92
|
+
|
93
|
+
super do
|
94
|
+
begin
|
95
|
+
run
|
96
|
+
rescue Exception => ex
|
97
|
+
@result = Response.build(ex)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def run
|
103
|
+
FileUtils.mkdir_p(root_dir)
|
104
|
+
|
105
|
+
#TODO:NLA Set vmdir = File.join(tmpdir, "tmp_vm_#{vm_id}"). Then on success, remove vmdir.
|
106
|
+
|
107
|
+
downloads = Skytap.invoke!(username, api_token, "vm download #{vm_id}", :dir => root_dir) do |downloader|
|
108
|
+
@no_slots_msg = if seconds = downloader.seconds_until_retry
|
109
|
+
m = Integer(seconds / 60)
|
110
|
+
"VM #{vm_id}: No export capacity is currently available on Skytap. Will retry ".tap do |msg|
|
111
|
+
if m < 1
|
112
|
+
msg << 'soon.'
|
113
|
+
else
|
114
|
+
msg << "in #{m} minutes or when more capacity is detected."
|
115
|
+
end
|
116
|
+
end.color(:yellow)
|
117
|
+
end
|
118
|
+
@status_line = downloader.status_lines
|
119
|
+
end
|
120
|
+
|
121
|
+
@no_slots_msg = nil
|
122
|
+
|
123
|
+
vm_dir = File.join(root_dir, "vm_#{vm_id}")
|
124
|
+
unless downloads.include?(vm_dir)
|
125
|
+
raise Skytap::Error.new("Response dir unexpected (was: #{downloads}; expected to contain #{vm_dir})")
|
126
|
+
end
|
127
|
+
|
128
|
+
# Invoke with an array to treat vm_dir as one token, even if it contains spaces.
|
129
|
+
uploads = Skytap.invoke!(username, api_token, ['vm', 'upload', vm_dir], {}, :param => {'region' => region}) do |uploader|
|
130
|
+
@no_slots_msg = if seconds = uploader.seconds_until_retry
|
131
|
+
m = Integer(seconds / 60)
|
132
|
+
"VM #{vm_id}: No import capacity is currently available on Skytap. Will retry ".tap do |msg|
|
133
|
+
if m < 1
|
134
|
+
msg << 'soon.'
|
135
|
+
else
|
136
|
+
msg << "in #{m} minutes or when more capacity is detected."
|
137
|
+
end
|
138
|
+
msg
|
139
|
+
end.color(:yellow)
|
140
|
+
end
|
141
|
+
@status_line = uploader.status_lines
|
142
|
+
end
|
143
|
+
|
144
|
+
FileUtils.rm_r(vm_dir)
|
145
|
+
|
146
|
+
@result = Response.build(uploads)
|
147
|
+
end
|
148
|
+
|
149
|
+
def finished?
|
150
|
+
@finished.tap do
|
151
|
+
@finished = true if @result
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def success?
|
156
|
+
result && !result.error?
|
157
|
+
end
|
158
|
+
|
159
|
+
def status_line
|
160
|
+
payload = result.try(:payload) and return payload
|
161
|
+
|
162
|
+
if @no_slots_msg
|
163
|
+
unless @skip_print
|
164
|
+
@skip_print = true
|
165
|
+
return @no_slots_msg
|
166
|
+
end
|
167
|
+
else
|
168
|
+
@skip_print = false
|
169
|
+
end
|
170
|
+
|
171
|
+
@status_line
|
172
|
+
end
|
173
|
+
|
174
|
+
def summary
|
175
|
+
status_line.tap do |msg|
|
176
|
+
if manual_network?
|
177
|
+
msg << " This template has an automatic network, but the template from which it was copied has a manual network. You may want to change the network settings of the new template by creating a configuration from it, editing the network, and finally creating another template from that configuration."
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def manual_network?
|
183
|
+
@_manual_network ||= begin
|
184
|
+
vm = Skytap.invoke!(username, api_token, "vm show #{vm_id}")
|
185
|
+
(iface = vm['interfaces'].try(:first)) && iface['network_type'] == 'manual'
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def warn_if_manual_network
|
190
|
+
if manual_network?
|
191
|
+
msg = 'This VM is attached to a manual network, but the new template will instead contain an automatic network. You may want to change the network settings of the new template by creating a configuration from it, editing the network, and finally creating another template from that configuration.'
|
192
|
+
logger.info "VM #{vm_id}: #{msg.color(:yellow)}\n---"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
Vm.subcommands << CopyToRegion
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|