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