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
data/data/.DS_Store
ADDED
Binary file
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'skytap/skytaprc'
|
2
|
+
|
3
|
+
module Skytap
|
4
|
+
class CommandLine
|
5
|
+
ALLOWED_GLOBAL_OPTIONS = {
|
6
|
+
# Flags
|
7
|
+
:'base-url' => { :default => 'https://cloud.skytap.com' },
|
8
|
+
:'username' => { :desc => 'Skytap username' },
|
9
|
+
:'api-token' => { :desc => 'Skytap API token, found on "My Account" page', :hide_default_value => true },
|
10
|
+
:'http-format' => { :default => 'json', :in => ['json', 'xml'], :desc => 'HTTP request and response format' },
|
11
|
+
:'log-level' => { :default => 'info', :in => ['quiet', 'info', 'verbose'], :desc => 'Output verbosity' },
|
12
|
+
:'params-file' => { :desc => 'File whose contents to send as request params, read according to http-format' },
|
13
|
+
:'param' => { :desc => 'Request parameters in form --param=key1:value1 --param=key2:value2', :default => [], :multiple => true },
|
14
|
+
|
15
|
+
# Switches
|
16
|
+
:'help' => { :desc => 'Show help message for command', :switch => true },
|
17
|
+
:'version' => { :desc => 'Show CLI version', :switch => true },
|
18
|
+
:'colorize' => { :default => true, :desc => 'Colorize output?', :switch => true, :negatable => true },
|
19
|
+
:'verify-certs' => { :default => true, :desc => 'Verify HTTPS certificates?', :switch => true, :negatable => true },
|
20
|
+
:'ask' => { :default => true, :desc => 'Ask for params interactively?', :switch => true, :negatable => true }
|
21
|
+
}
|
22
|
+
|
23
|
+
|
24
|
+
def self.parse(argv=ARGV)
|
25
|
+
global = read_global_options
|
26
|
+
command_opts = {}
|
27
|
+
args = []
|
28
|
+
argv.each do |arg|
|
29
|
+
if arg =~ /^[^-]/
|
30
|
+
args << arg
|
31
|
+
# Switches
|
32
|
+
elsif arg =~ /^\-\-(no-)?([^=]+)$/
|
33
|
+
negated, name = $1, $2.to_sym
|
34
|
+
val = !negated
|
35
|
+
if global.has_key?(name)
|
36
|
+
global[name] = val
|
37
|
+
else
|
38
|
+
command_opts[name] = val
|
39
|
+
end
|
40
|
+
# Flags
|
41
|
+
elsif arg =~ /^\-\-(.+?)=(.+)$/
|
42
|
+
name, val = $1.to_sym, $2
|
43
|
+
|
44
|
+
if global.has_key?(name)
|
45
|
+
multiple = ALLOWED_GLOBAL_OPTIONS[name].try(:[], :multiple)
|
46
|
+
if multiple
|
47
|
+
if global[name].present?
|
48
|
+
global[name] << val
|
49
|
+
else
|
50
|
+
global[name] = [val]
|
51
|
+
end
|
52
|
+
else
|
53
|
+
global[name] = val
|
54
|
+
end
|
55
|
+
else
|
56
|
+
command_opts[name] = val
|
57
|
+
end
|
58
|
+
elsif arg =~ /^\-([A-Za-z0-9]+)$/
|
59
|
+
raise Skytap::Error.new 'Single-dash flags not supported. Use --flag form.'
|
60
|
+
else
|
61
|
+
raise "Unrecognized command-line arg: #{arg.inspect}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# To simplify client handling, treat a first arg of "help" as --help.
|
66
|
+
if args.first == 'help'
|
67
|
+
args.shift
|
68
|
+
global[:help] = true
|
69
|
+
end
|
70
|
+
|
71
|
+
[args, global, command_opts]
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.read_global_options
|
75
|
+
global_options.inject({}) do |acc, (k, option)|
|
76
|
+
acc[k] = option.val
|
77
|
+
acc
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.global_options
|
82
|
+
return @global_options if @global_options
|
83
|
+
|
84
|
+
rc = SkytapRC.load
|
85
|
+
@global_options = ALLOWED_GLOBAL_OPTIONS.inject({}) do |acc, (k, opts)|
|
86
|
+
opts[:default] = rc[k] if rc.has_key?(k)
|
87
|
+
acc[k] = Option.new(k, opts)
|
88
|
+
acc
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Option
|
94
|
+
attr_reader :name, :desc, :default
|
95
|
+
|
96
|
+
def initialize(name, options = {})
|
97
|
+
@name = name.to_s
|
98
|
+
@options = options.symbolize_keys
|
99
|
+
@desc = @options[:desc]
|
100
|
+
@default = @options[:default]
|
101
|
+
end
|
102
|
+
|
103
|
+
def show_default?
|
104
|
+
!@options[:hide_default_value]
|
105
|
+
end
|
106
|
+
|
107
|
+
def switch?
|
108
|
+
@options[:switch]
|
109
|
+
end
|
110
|
+
|
111
|
+
def negatable?
|
112
|
+
switch? && @options[:negatable]
|
113
|
+
end
|
114
|
+
|
115
|
+
def val
|
116
|
+
if @set
|
117
|
+
@val
|
118
|
+
else
|
119
|
+
default
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def val=(v)
|
124
|
+
@set = true
|
125
|
+
if switch?
|
126
|
+
@val = !!v
|
127
|
+
else
|
128
|
+
@val = v
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def signature
|
133
|
+
if switch?
|
134
|
+
negation = '[no-]' if negatable?
|
135
|
+
"--#{negation}#{name}"
|
136
|
+
else
|
137
|
+
"--#{name}=#{name.upcase}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def choices
|
142
|
+
@options[:in]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,294 @@
|
|
1
|
+
require 'skytap/commands/help'
|
2
|
+
|
3
|
+
module Skytap
|
4
|
+
module Commands
|
5
|
+
class Base
|
6
|
+
include Help
|
7
|
+
|
8
|
+
class_attribute :subcommands, :parent, :ask_interactively
|
9
|
+
self.subcommands = []
|
10
|
+
|
11
|
+
attr_accessor :error
|
12
|
+
attr_reader :args, :global_options, :command_options, :logger, :invoker
|
13
|
+
|
14
|
+
def initialize(logger, args, global_options, command_options, programmatic_context=false, &invoker)
|
15
|
+
@logger = logger
|
16
|
+
@args = args
|
17
|
+
@global_options = global_options
|
18
|
+
@command_options = command_options || {}
|
19
|
+
@programmatic_context = programmatic_context
|
20
|
+
@invoker = invoker
|
21
|
+
end
|
22
|
+
|
23
|
+
def programmatic?
|
24
|
+
!!@programmatic_context
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# A factory that makes a command class from the given resource
|
29
|
+
def make_from(resource, spec={})
|
30
|
+
spec ||= {}
|
31
|
+
Class.new(Base).tap do |klass|
|
32
|
+
klass.instance_eval do
|
33
|
+
self.container = true
|
34
|
+
self.subcommands = [Index, Show, Create, Update, Destroy].reject do |sub|
|
35
|
+
spec['skip_actions'].try(:include?, sub.command_name)
|
36
|
+
end.collect do |sub|
|
37
|
+
sub_spec = spec['actions'].try(:[], sub.command_name)
|
38
|
+
sub.make_for(self, sub_spec)
|
39
|
+
end
|
40
|
+
self.spec = spec
|
41
|
+
alias_method :run!, :help!
|
42
|
+
end
|
43
|
+
|
44
|
+
Skytap::Commands.const_set(resource.classify, klass)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def command_name
|
49
|
+
name.split('::').last.underscore
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def invoke
|
54
|
+
if matching_subcommand
|
55
|
+
matching_subcommand.new(logger, args[1..-1], global_options, command_options, @programmatic_context, &invoker).invoke
|
56
|
+
elsif help?
|
57
|
+
help!
|
58
|
+
elsif version?
|
59
|
+
logger.puts "skytap version #{Skytap::VERSION}"
|
60
|
+
exit
|
61
|
+
elsif container && args.present?
|
62
|
+
self.error = "Subcommand '#{args.first}' not found"
|
63
|
+
help!
|
64
|
+
exit(false)
|
65
|
+
else
|
66
|
+
validate_args
|
67
|
+
validate_command_options
|
68
|
+
run!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def expected_args
|
73
|
+
ActiveSupport::OrderedHash.new
|
74
|
+
end
|
75
|
+
|
76
|
+
# Expected command-specific options (not global options)
|
77
|
+
def expected_options
|
78
|
+
{}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an ID string from an URL, path or ID
|
82
|
+
def find_id(arg)
|
83
|
+
arg.to_s =~ /(.*\/)?(.+)$/ && $2
|
84
|
+
end
|
85
|
+
|
86
|
+
def solicit_user_input?
|
87
|
+
global_options[:ask] && !programmatic? && $stdin.tty?
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def ask_param
|
92
|
+
name = ask('Name: '.bright)
|
93
|
+
value = ask('Value: '.bright)
|
94
|
+
[name, value]
|
95
|
+
end
|
96
|
+
|
97
|
+
def ask(question, choices={})
|
98
|
+
return unless solicit_user_input?
|
99
|
+
default = choices.delete(:default)
|
100
|
+
raise "Default choice must be in the choices hash" if default && !choices.has_key?(default)
|
101
|
+
if choices.present?
|
102
|
+
letters = " [#{choices.collect{|l, _| l == default ? l.upcase : l.downcase}.join}]"
|
103
|
+
end
|
104
|
+
|
105
|
+
loop do
|
106
|
+
line = "#{question}#{letters} ".color(:yellow)
|
107
|
+
$stdout.print line
|
108
|
+
$stdout.flush
|
109
|
+
|
110
|
+
answer = $stdin.gets.try(&:strip)
|
111
|
+
if choices.blank?
|
112
|
+
break answer
|
113
|
+
elsif answer.blank? && default
|
114
|
+
break choices[default]
|
115
|
+
elsif choices.has_key?(answer.downcase)
|
116
|
+
break choices[answer.downcase]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def composed_params
|
122
|
+
noninteractive_params.merge(interactive_params)
|
123
|
+
end
|
124
|
+
|
125
|
+
def file_params
|
126
|
+
file = global_options[:'params-file'] or return {}
|
127
|
+
file = File.expand_path(file)
|
128
|
+
raise 'Params file not found' unless File.exist?(file)
|
129
|
+
case File.basename(file)
|
130
|
+
when /\.json$/i
|
131
|
+
format = 'json'
|
132
|
+
when /\.xml$/i
|
133
|
+
format = 'xml'
|
134
|
+
else
|
135
|
+
format = global_options[:'http-format']
|
136
|
+
end
|
137
|
+
|
138
|
+
body = File.open(file, &:read)
|
139
|
+
deserialized = case format
|
140
|
+
when 'json'
|
141
|
+
JSON.load(body)
|
142
|
+
when 'xml'
|
143
|
+
parsed = Hash.from_xml(body)
|
144
|
+
# Strip out the root name.
|
145
|
+
if parsed.is_a?(Hash)
|
146
|
+
parsed.values.first
|
147
|
+
else
|
148
|
+
parsed
|
149
|
+
end
|
150
|
+
else
|
151
|
+
raise Skytap::Error.new("Unknown format: #{format.inspect}")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def command_line_params
|
156
|
+
param = global_options[:param]
|
157
|
+
return {} if param.blank?
|
158
|
+
case param
|
159
|
+
when Hash
|
160
|
+
param
|
161
|
+
when String
|
162
|
+
#TODO:NLA This will blow up if param has a weird form.
|
163
|
+
Hash[*param.split(':', 2)]
|
164
|
+
when Array
|
165
|
+
split = param.collect {|v| v.split(':', 2)}.flatten
|
166
|
+
Hash[*split]
|
167
|
+
else
|
168
|
+
param
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def noninteractive_params
|
173
|
+
@noninteractive_params ||= file_params.merge(command_line_params)
|
174
|
+
end
|
175
|
+
|
176
|
+
def interactive_params
|
177
|
+
if !solicit_user_input? ||
|
178
|
+
!ask_interactively ||
|
179
|
+
parameters.blank? ||
|
180
|
+
(required_parameters.empty? && noninteractive_params.present?) ||
|
181
|
+
(required_parameters.present? && (required_parameters - noninteractive_params.keys).empty?)
|
182
|
+
|
183
|
+
return {}
|
184
|
+
end
|
185
|
+
|
186
|
+
param_info = Templates::Help.new('command' => self).parameters_table
|
187
|
+
logger.info param_info << "\n"
|
188
|
+
|
189
|
+
params = {}
|
190
|
+
loop do
|
191
|
+
answer = ask('include a param?', ActiveSupport::OrderedHash['y', 'y', 'n', 'n', 'q', 'q', '?', '?', :default, 'n'])
|
192
|
+
case answer.downcase
|
193
|
+
when 'y'
|
194
|
+
k, v = ask_param
|
195
|
+
params[k] = v
|
196
|
+
when 'n'
|
197
|
+
break
|
198
|
+
when 'q'
|
199
|
+
logger.info 'Bye.'
|
200
|
+
exit
|
201
|
+
else
|
202
|
+
logger.puts <<-"EOF"
|
203
|
+
y - include an HTTP request parameter
|
204
|
+
n - stop adding parameters (default answer)
|
205
|
+
q - quit
|
206
|
+
? - show this message
|
207
|
+
|
208
|
+
EOF
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
params
|
213
|
+
end
|
214
|
+
|
215
|
+
def username
|
216
|
+
return @_username if @_username
|
217
|
+
username = global_options[:username]
|
218
|
+
if username.blank?
|
219
|
+
if solicit_user_input?
|
220
|
+
username = ask('Skytap username:') while username.blank?
|
221
|
+
else
|
222
|
+
raise Skytap::Error.new('Must provide --username')
|
223
|
+
end
|
224
|
+
end
|
225
|
+
@_username = username
|
226
|
+
end
|
227
|
+
|
228
|
+
def api_token
|
229
|
+
return @_api_token if @_api_token
|
230
|
+
api_token = global_options[:'api-token']
|
231
|
+
if api_token.blank?
|
232
|
+
if solicit_user_input?
|
233
|
+
api_token = ask('API token:') while api_token.blank?
|
234
|
+
else
|
235
|
+
raise Skytap::Error.new('Must provide --api-token')
|
236
|
+
end
|
237
|
+
end
|
238
|
+
@_api_token = api_token
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
|
244
|
+
def run!
|
245
|
+
raise NotImplementedError.new('Must override #run!')
|
246
|
+
end
|
247
|
+
|
248
|
+
def required_parameters
|
249
|
+
@required_parameters ||= parameters.collect{|p| p.keys.first if p.values.first['required']}.compact
|
250
|
+
end
|
251
|
+
|
252
|
+
#TODO:NLA Need to track somewhere that only the last expected arg may end with *
|
253
|
+
def validate_args
|
254
|
+
unlimited = expected_args.keys.last.try(:end_with?, '*')
|
255
|
+
if unlimited
|
256
|
+
min = expected_args.keys.length
|
257
|
+
if args.length < expected_args.keys.length
|
258
|
+
self.error = "Expected at least #{min} command line #{'argument'.pluralize(min)} but got #{args.length}."
|
259
|
+
help!
|
260
|
+
exit(false)
|
261
|
+
end
|
262
|
+
else
|
263
|
+
expected = expected_args.keys.length
|
264
|
+
unless args.length == expected
|
265
|
+
self.error = "Expected #{expected} command line #{'argument'.pluralize(expected)} but got #{args.length}."
|
266
|
+
help!
|
267
|
+
exit(false)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def validate_command_options
|
273
|
+
bad = command_options.inject([]) do |acc, (name, value)|
|
274
|
+
if !expected_options[name]
|
275
|
+
acc << "Unknown option '#{name}'."
|
276
|
+
elsif !expected_options[name][:flag_arg] && (value != true && value != false)
|
277
|
+
acc << "'#{name}' option is a switch and cannot be set to a value."
|
278
|
+
end
|
279
|
+
acc
|
280
|
+
end
|
281
|
+
if bad.present?
|
282
|
+
self.error = bad.join(' ')
|
283
|
+
help!
|
284
|
+
exit(false)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def matching_subcommand
|
289
|
+
next_command = args.first
|
290
|
+
subcommands.detect {|k| k.command_name == next_command}
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'skytap/templates'
|
2
|
+
|
3
|
+
module Skytap
|
4
|
+
module Commands
|
5
|
+
module Help
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def banner_prefix
|
9
|
+
parent_prefix = parent.try(:banner_prefix)
|
10
|
+
"#{parent_prefix.try(:+, ' ')}#{self.command_name}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def banner
|
14
|
+
b = "#{banner_prefix} - #{short_desc}"
|
15
|
+
if plugin
|
16
|
+
b.color(:cyan)
|
17
|
+
else
|
18
|
+
b
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def description
|
23
|
+
spec['description'] || default_description
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_description
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def short_desc
|
31
|
+
description.split("\n\n").first.split("\n").join(' ') if description
|
32
|
+
end
|
33
|
+
|
34
|
+
def subcommand_banners
|
35
|
+
subcommands.inject([]) do |acc, klass|
|
36
|
+
acc << klass.banner unless klass.container
|
37
|
+
acc.concat(klass.subcommand_banners)
|
38
|
+
acc
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def synopsis
|
44
|
+
if self.container
|
45
|
+
command_name = self.class.command_name
|
46
|
+
"skytap #{command_name + ' ' if command_name}<subcommand> <options>"
|
47
|
+
else
|
48
|
+
"#{self.class.banner_prefix} #{expected_args.keys.collect(&:upcase).join(' ') << ' '}<options> - #{self.class.short_desc}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def help?
|
53
|
+
!!global_options[:help]
|
54
|
+
end
|
55
|
+
|
56
|
+
def help!
|
57
|
+
puts Skytap::Templates::Help.render(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def version?
|
61
|
+
!!global_options[:version]
|
62
|
+
end
|
63
|
+
|
64
|
+
def parameters
|
65
|
+
spec['params']
|
66
|
+
end
|
67
|
+
|
68
|
+
def description
|
69
|
+
self.class.description
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.included(base)
|
73
|
+
base.extend(ClassMethods)
|
74
|
+
# Indicates whether this class is only a container for subcommands.
|
75
|
+
base.class_attribute :container, :spec, :plugin
|
76
|
+
base.container = false
|
77
|
+
base.plugin = false
|
78
|
+
base.spec = {}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|