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.
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'skytap/commands/base'
2
+ require 'skytap/commands/root'
3
+ require 'skytap/commands/http'
4
+
5
+ module Skytap
6
+ module Commands
7
+ class NotImplementedError < RuntimeError ; end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ def subset(keys)
3
+ keys = keys.to_set
4
+ reject do |k, v|
5
+ !keys.include?(k)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module Skytap
2
+ class Error < RuntimeError
3
+ end
4
+ end
@@ -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
+