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,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
|
+
|