skytap-yf 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+