cft_smartcloud 0.3.0 → 0.3.1

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/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ 2011-11-22 0.3.1:
2
+ * Support for invocation of commands using key=value syntax
3
+ Such as: smartcloud display_images name='Red Hat'
4
+
1
5
  2011-11-21 0.3.0:
2
6
  * Support for smartcloud import/export api
3
7
  * Ability to use -S to save responses and -R to play them back
data/README.md CHANGED
@@ -3,7 +3,7 @@ smartcloud
3
3
 
4
4
  Provides support for interacting with IBM SmartCloud API and CLI tools
5
5
 
6
- installation
6
+ Installation
7
7
  ===
8
8
 
9
9
  from rubygems.org:
@@ -15,7 +15,7 @@ locally:
15
15
  rake build
16
16
  gem install pkg/[name of generated gem]
17
17
 
18
- setup
18
+ Setup
19
19
  ===
20
20
 
21
21
  Please set up SMARTCLOUD_USERNAME and SMARTCLOUD_PASSWORD in your .bash_profile
@@ -26,12 +26,8 @@ Please set up SMARTCLOUD_USERNAME and SMARTCLOUD_PASSWORD in your .bash_profile
26
26
  You can now also supply the username and password on the command line using -u and -p
27
27
  Use `smartcloud help` to get a list of all optoins.
28
28
 
29
- screencast
30
- ===
31
-
32
- http://www.youtube.com/cohesiveft#p/u/0/-WdSHP2iwDM (somewhat outdated)
33
29
 
34
- using the console
30
+ Using the console
35
31
  ==
36
32
 
37
33
  script/console
@@ -44,43 +40,36 @@ using the console
44
40
  from the environment variables SMARTCLOUD_USERNAME and SMARTCLOUD_PASSWORD
45
41
  automatically (it's created at the bottom of smartcloud.rb)
46
42
 
47
- using the commandline helper
43
+ Using the commandline
48
44
  ==
49
45
 
50
- smartcloud [method of smartcloud.rb]
51
-
52
- to see a list of methods:
46
+ To see a list of methods:
53
47
 
54
48
  smartcloud help
55
49
 
56
- examples:
50
+ Examples:
57
51
 
58
52
  smartcloud display_volumes
53
+ smartcloud display_volumes Location=82 State=MOUNTED
59
54
  smartcloud display_instances
60
- smartcloud display_instance 12345
61
55
  smartcloud delete_instances 12345 12346 12347
62
- smartcloud "describe_instance('12345')"
63
- smartcloud "display_instances(:Location => 82, :Name => 'match_this')"
56
+ smartcloud display_images Name="Red Hat"
57
+ smartcloud display_instances Name="Red Hat" Location=82
64
58
 
65
- smartcloud delete_unused_keys # this one will prompt for every key
66
-
67
- The 'display_*' methods are intended to generate pretty human readable displays, while the describe methods
68
- will return pretty-formatted hashes, or singular values.
69
-
70
- smartcloud display_volumes
59
+ The 'display_*' methods are intended to generate pretty human readable
60
+ displays, while the describe methods will return pretty-formatted hashes,
61
+ or singular values.
71
62
 
72
- get a list of all available methods
73
- ===
63
+ To save time when dealing with large responses, such as the describe_images
64
+ call, you can save a response in its native XML format:
74
65
 
75
- console:
66
+ smartcloud display_images -S /tmp/images.xml
76
67
 
77
- >> @smartcloud.help
68
+ You can then replay the response, using filters on it
78
69
 
79
- commandline
70
+ smartcloud display_images Name='Red Hat' -R /tmp/images.xml
80
71
 
81
- > smartcloud help
82
72
 
83
- These won't tell you the arguments, you have to look at smartcloud.rb for the args.
84
73
 
85
74
  RestClient vs CurlHttpClient
86
75
  ===
@@ -100,6 +89,15 @@ This project uses the jeweler gem for packaging. See the tasks:
100
89
  rake version:bump:...
101
90
  rake build
102
91
 
92
+ To publish to RubyForge
93
+
94
+ rake gemcutter:release
95
+
96
+ Screencast
97
+ ===
98
+
99
+ http://www.youtube.com/cohesiveft#p/u/0/-WdSHP2iwDM (somewhat outdated)
100
+
103
101
  Copyright
104
102
  ==
105
103
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.3.1
data/bin/cft_smartcloud CHANGED
@@ -18,7 +18,7 @@ opts = Slop.new do
18
18
  on :U, :api_url=, "URL of api endpoint"
19
19
  on :d, :debug, "Enable debug logging"
20
20
  on :R, :simulated_response_file=, "Pass in a file containing the response (do not hit cloud)"
21
- on :S, :save_response, "Save the response in responses/ dir, for later use with -R response file"
21
+ on :S, :save_response=, "Save the response (supply filename) as xml, for later use with -R response file"
22
22
  end
23
23
 
24
24
  # These are the actual commands appearing after the script name,
@@ -31,11 +31,8 @@ end
31
31
 
32
32
  if commands.size == 1
33
33
  if commands[0] == 'help'
34
- puts "#{opts.help}\n\nFunctions:\n"
34
+ puts "#{opts.help}\n\n"
35
35
  end
36
- @cmd=commands[0]
37
- elsif commands.size == 2
38
- @cmd="#{commands[0]}('#{commands[1]}')"
39
36
  elsif commands.size == 0
40
37
  puts %{
41
38
  #{opts.help}
@@ -50,17 +47,45 @@ elsif commands.size == 0
50
47
  smartcloud "delete_instance(12345)"
51
48
  smartcloud delete_instance 12345
52
49
  smartcloud delete_instances 12345 12346 12347
53
- smartcloud "delete_instances(12345,12346,12347)"
54
50
  smartcloud display_instances
51
+ smartcloud display_images name="Red Hat"
55
52
  }
56
53
  exit(0)
54
+ end
55
+
56
+ # parse out foo=bar values and turn them into a hash
57
+ params=[]
58
+ param_hash={}
59
+ commands[1..-1].each do |item|
60
+ if item =~ /=/
61
+ key,val = item.split('=')
62
+ param_hash[key]=val
63
+ else
64
+ params << item
65
+ end
66
+ end
67
+ params = params.map {|param| "'#{param}'" }.join(',')
68
+
69
+ method_invocation = if params.empty? && param_hash.empty?
70
+ commands[0]
71
+ elsif !params.empty? && param_hash.empty?
72
+ "#{commands[0]}(#{params})"
73
+ elsif params.empty? && !param_hash.empty?
74
+ "#{commands[0]}(#{param_hash.inspect})"
57
75
  else
58
- @cmd="#{commands[0]}(#{commands[1..-1].map {|item| "'#{item}'"}.join(',')})"
76
+ "#{commands[0]}(#{params}, #{param_hash.inspect})"
59
77
  end
60
78
 
79
+ puts "Invoking: #{method_invocation}"
61
80
  # allows us to send arbitrary commands like
62
81
  # smartcloud username password describe_instance("122345")
63
- result = eval("@sc.#{@cmd}")
82
+ begin
83
+ result = eval("@sc.#{method_invocation}")
84
+
85
+ rescue ArgumentError => e
86
+ puts e.message
87
+ puts "\nPlease use the following command for more information:\nsmartcloud help #{commands[0]}\n\n"
88
+ end
64
89
 
65
90
  if result == true || result.nil?
66
91
  # do nothing, the command already logged
data/bin/smartcloud CHANGED
@@ -18,7 +18,7 @@ opts = Slop.new do
18
18
  on :U, :api_url=, "URL of api endpoint"
19
19
  on :d, :debug, "Enable debug logging"
20
20
  on :R, :simulated_response_file=, "Pass in a file containing the response (do not hit cloud)"
21
- on :S, :save_response, "Save the response in responses/ dir, for later use with -R response file"
21
+ on :S, :save_response=, "Save the response (supply filename) as xml, for later use with -R response file"
22
22
  end
23
23
 
24
24
  # These are the actual commands appearing after the script name,
@@ -31,11 +31,8 @@ end
31
31
 
32
32
  if commands.size == 1
33
33
  if commands[0] == 'help'
34
- puts "#{opts.help}\n\nFunctions:\n"
34
+ puts "#{opts.help}\n\n"
35
35
  end
36
- @cmd=commands[0]
37
- elsif commands.size == 2
38
- @cmd="#{commands[0]}('#{commands[1]}')"
39
36
  elsif commands.size == 0
40
37
  puts %{
41
38
  #{opts.help}
@@ -50,17 +47,45 @@ elsif commands.size == 0
50
47
  smartcloud "delete_instance(12345)"
51
48
  smartcloud delete_instance 12345
52
49
  smartcloud delete_instances 12345 12346 12347
53
- smartcloud "delete_instances(12345,12346,12347)"
54
50
  smartcloud display_instances
51
+ smartcloud display_images name="Red Hat"
55
52
  }
56
53
  exit(0)
54
+ end
55
+
56
+ # parse out foo=bar values and turn them into a hash
57
+ params=[]
58
+ param_hash={}
59
+ commands[1..-1].each do |item|
60
+ if item =~ /=/
61
+ key,val = item.split('=')
62
+ param_hash[key]=val
63
+ else
64
+ params << item
65
+ end
66
+ end
67
+ params = params.map {|param| "'#{param}'" }.join(',')
68
+
69
+ method_invocation = if params.empty? && param_hash.empty?
70
+ commands[0]
71
+ elsif !params.empty? && param_hash.empty?
72
+ "#{commands[0]}(#{params})"
73
+ elsif params.empty? && !param_hash.empty?
74
+ "#{commands[0]}(#{param_hash.inspect})"
57
75
  else
58
- @cmd="#{commands[0]}(#{commands[1..-1].map {|item| "'#{item}'"}.join(',')})"
76
+ "#{commands[0]}(#{params}, #{param_hash.inspect})"
59
77
  end
60
78
 
79
+ puts "Invoking: #{method_invocation}"
61
80
  # allows us to send arbitrary commands like
62
81
  # smartcloud username password describe_instance("122345")
63
- result = eval("@sc.#{@cmd}")
82
+ begin
83
+ result = eval("@sc.#{method_invocation}")
84
+
85
+ rescue ArgumentError => e
86
+ puts e.message
87
+ puts "\nPlease use the following command for more information:\nsmartcloud help #{commands[0]}\n\n"
88
+ end
64
89
 
65
90
  if result == true || result.nil?
66
91
  # do nothing, the command already logged
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{cft_smartcloud}
8
- s.version = "0.3.0"
8
+ s.version = "0.3.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["yan", "cohesive"]
12
- s.date = %q{2011-11-21}
12
+ s.date = %q{2012-03-12}
13
13
  s.description = %q{CohesiveFT Ruby Interface for IBM SmartCloud and 'smartcloud' command line helper.}
14
14
  s.email = %q{yan.pritzker@cohesiveft.com}
15
15
  s.executables = ["cft_smartcloud", "smartcloud"]
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
29
29
  "cft_smartcloud.gemspec",
30
30
  "lib/config/config.yml",
31
31
  "lib/curl_client.rb",
32
+ "lib/dynamic_help_generator.rb",
32
33
  "lib/hash_fix.rb",
33
34
  "lib/mime-types-1.16/History.txt",
34
35
  "lib/mime-types-1.16/Install.txt",
@@ -1,6 +1,6 @@
1
1
  api_url: https://www-147.ibm.com/computecloud/enterprise/api/rest/20100331/
2
- http_client: CurlHttpClient
3
- # http_client: RestClient
2
+ # http_client: CurlHttpClient
3
+ http_client: RestClient
4
4
  states:
5
5
  instance:
6
6
  0: NEW
data/lib/curl_client.rb CHANGED
@@ -9,7 +9,9 @@ class CurlHttpClient
9
9
 
10
10
  def self.logger; @logger ||= Logger.new(STDOUT); end
11
11
 
12
- def self.get(url)
12
+ # Even though we don't need the options, the REST client does.
13
+ # So we have this for consistency.
14
+ def self.get(url, options={})
13
15
  handle_output(curl(url))
14
16
  end
15
17
 
@@ -0,0 +1,88 @@
1
+ module DynamicHelpGenerator
2
+
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def help_for(method, args, extra_help="")
9
+ @method_help||={}
10
+ @method_help_supplemental||={}
11
+ @method_help[method.to_s] = args
12
+ @method_help_supplemental[method.to_s] = extra_help
13
+ end
14
+
15
+ attr_reader :method_help
16
+ attr_reader :method_help_supplemental
17
+ end
18
+
19
+ def help(method=nil)
20
+
21
+ if method
22
+ args = (self.class.method_help[method.to_s])
23
+ if !(self.respond_to?(method))
24
+ return "Sorry, I don't know method: #{method}"
25
+ end
26
+
27
+ args = args && args.map do |arg|
28
+ if arg.is_a?(Hash)
29
+ # If an argument is required, just list it
30
+ if arg.values.first==:req
31
+ arg.keys.first.to_s
32
+ # If it's optional, list it in brackets
33
+ elsif arg.values.first==:opt
34
+ "[#{arg.keys.first.to_s}]"
35
+ # If there is an array of options, list them
36
+ else
37
+ "#{arg.keys.first.to_s}=>#{arg.values.first.inspect}"
38
+ end
39
+ else
40
+ arg
41
+ end
42
+ end.join(", ")
43
+
44
+ extra_help = self.class.method_help_supplemental[method.to_s] || ""
45
+
46
+ puts %{ * #{method.to_s}#{'(' + args + ')' if args}#{extra_help}}
47
+ else
48
+ # These verbs help us figure out what 'group' the method belongs to
49
+ verbs = %w(describe display create get allocate clone export attach detach delete generate update remove restart rename)
50
+ verb_noun = /(#{verbs.join('|')})_(.*)(s|es)?/ # we're going to remove the verb and trailing 's'
51
+
52
+ methods = public_methods - Object.public_methods - ['post','get','put','delete','logger','logger=','help']
53
+
54
+ # Group methods by the noun they operate on
55
+ methods_grouped_by_noun = methods.inject({}) do |h, method|
56
+ method_name, verb, noun = *(method.match(verb_noun))
57
+ if method_name.nil?
58
+ # match failed
59
+ method_name = method
60
+ noun = "misc"
61
+ end
62
+ synonyms = {
63
+ :keypair => :key,
64
+ :address_offering => :address,
65
+ :location_by_name => :location,
66
+ :storage_offering => :storage,
67
+ :volume => :storage,
68
+ :volume_offering => :storage,
69
+ }
70
+ noun.gsub!(/s$/,'') unless noun =~ /address/
71
+ if synonyms.keys.include?(noun.to_sym)
72
+ noun = synonyms[noun.to_sym].to_s
73
+ end
74
+ h[noun] ||= []
75
+ h[noun] << method_name
76
+ h
77
+ end
78
+ methods_grouped_by_noun.keys.sort.each do |noun|
79
+ methods = methods_grouped_by_noun[noun]
80
+ next unless methods
81
+ puts "== #{noun.capitalize} ==\n\n"
82
+ methods.sort.each {|m| help(m)}
83
+ puts
84
+ end
85
+ nil
86
+ end
87
+ end
88
+ end
data/lib/smartcloud.rb CHANGED
@@ -24,13 +24,14 @@ require 'xmlsimple'
24
24
  require 'smartcloud_logger'
25
25
  require 'curl_client'
26
26
  require 'terminal-table'
27
+ require "dynamic_help_generator"
27
28
 
28
29
  IBM_TOOLS_HOME=File.join(File.dirname(__FILE__), "cli_tools") unless defined?(IBM_TOOLS_HOME)
29
30
 
30
31
  # Encapsulates communications with IBM SmartCloud via REST
31
32
 
32
33
  class IBMSmartCloud
33
-
34
+ include DynamicHelpGenerator
34
35
  attr_accessor :logger
35
36
 
36
37
  def initialize(opts={})
@@ -59,57 +60,22 @@ class IBMSmartCloud
59
60
 
60
61
  class << self
61
62
  @config = YAML.load_file(File.join(File.dirname(__FILE__), "config/config.yml"))
62
- attr_reader :method_help
63
- attr_reader :method_help_supplemental
64
63
  attr_reader :config
65
64
  end
66
65
 
67
- def self.help_for(method, args, extra_help={})
68
- @method_help||={}
69
- @method_help_supplemental||={}
70
- @method_help[method.to_s] = args
71
- @method_help_supplemental[method.to_s] = extra_help
72
- end
73
-
74
- def help(method=nil)
75
- if method
76
- args = (self.class.method_help[method.to_s])
77
- if !(self.respond_to?(method))
78
- return "Sorry, I don't know method: #{method}"
79
- end
80
-
81
- args = args && args.map do |arg|
82
- if arg.is_a?(Hash)
83
- # If an argument is required, just list it
84
- if arg.values.first==:req
85
- arg.keys.first.to_s
86
- # If it's optional, list it in brackets
87
- elsif arg.values.first==:opt
88
- "[#{arg.keys.first.to_s}]"
89
- # If there is an array of options, list them
90
- else
91
- "#{arg.keys.first.to_s}=>#{arg.values.first.inspect}"
92
- end
93
- else
94
- arg
95
- end
96
- end.join(", ")
97
-
98
- extra_help = self.class.method_help_supplemental[method.to_s] || ""
99
-
100
- puts %{ * #{method.to_s}#{'(' + args + ')' if args}#{extra_help + "\n" if extra_help}}
66
+ # Get a list of data centers
67
+ help_for :describe_locations, [{:name => :opt}], %{
68
+ If name is given, will find the location by name
69
+ }
70
+ def describe_locations(name=nil)
71
+ locations = get("/locations").Location
72
+ if name
73
+ locations.detect {|loc| loc.Name =~ /#{name}/}
101
74
  else
102
- methods = public_methods - Object.public_methods - ['post','get','put','delete','logger','logger=','help']
103
- methods.sort.each {|m| help(m)}
104
- nil
75
+ locations
105
76
  end
106
77
  end
107
78
 
108
- # Get a list of data centers
109
- def describe_locations
110
- get("/locations").Location
111
- end
112
-
113
79
  def describe_location(location_id)
114
80
  get("/locations/#{location_id}").Location
115
81
  end
@@ -214,21 +180,26 @@ class IBMSmartCloud
214
180
  post("/offerings/image/#{image_id}", :name => name, :description => description).ImageID
215
181
  end
216
182
 
217
- # Export an image to a volume
183
+ # Export an image to a volume - create the volume first
218
184
  help_for :export_image, [{:name=>:req}, {:size => ['Small','Medium','Large']}, {:image_id => :req}, {:location => :req}]
219
185
  def export_image(name, size, image_id, location)
220
186
  # Note we figure out the correct size based on the name and location
221
187
  storage_offering=describe_storage_offerings(location, size)
222
188
 
223
- response = post("/storage", :name => name, :size => storage_offering.Capacity, :format => 'EXT3', :offeringID => storage_offering.ID, :location => location, :imageID => image_id)
189
+ response = post("/storage", :name => name, :size => storage_offering.Capacity, :format => 'EXT3', :offeringID => storage_offering.ID, :location => location)
190
+ volumeID = response.Volume.ID
191
+
192
+ poll_for_volume_state(volumeID, :unmounted)
193
+
194
+ response = put("/storage/#{volumeID}", :imageId => image_id)
224
195
  response.Volume.ID
225
196
  end
226
197
 
227
198
  help_for :import_image, [{:name=>:req, :volume_id => :req}]
228
199
  def import_image(name, volume_id)
229
200
  # TODO: this is a complete guess as we have no info from IBM as to the URL for this api, only the parameters
230
- response = post("/offerings/image", :volumeID => volume_id, :name => name)
231
- response.ImageID
201
+ response = post("/offerings/image", :volumeId => volume_id, :name => name)
202
+ response.Image.ID
232
203
  end
233
204
 
234
205
  # Launches a clone request and returns ID of new volume
@@ -289,8 +260,6 @@ class IBMSmartCloud
289
260
  delete("/keys/#{name}")
290
261
  true
291
262
  end
292
- alias remove_key remove_keypair
293
- alias delete_key remove_keypair
294
263
 
295
264
  help_for :describe_key, [:name]
296
265
  def describe_key(name)
@@ -341,10 +310,6 @@ class IBMSmartCloud
341
310
  arrayize(get("/keys").PublicKey)
342
311
  end
343
312
 
344
- def describe_unused_keys
345
- describe_keys.select {|key| key.Instances == {}}
346
- end
347
-
348
313
  def display_keys
349
314
  keys = describe_keys
350
315
 
@@ -638,14 +603,13 @@ class IBMSmartCloud
638
603
  output = if @simulated_response
639
604
  @simulated_response
640
605
  else
641
- @http_client.get File.join(@api_url, path)
606
+ @http_client.get File.join(@api_url, path), :accept => :response, :headers => "User-Agent: cloudapi"
642
607
  end
643
608
 
644
609
  # Save Response for posterity
645
610
  if @save_response && !output.empty?
646
- response_file = "responses/#{path.gsub('/','_').gsub(/^_/,'')}.#{Time.now.to_i}"
647
- logger.info "Saving response to: #{response_file}"
648
- File.open(response_file,'w') {|f| f.write(output)}
611
+ logger.info "Saving response to: #{@save_response}"
612
+ File.open(@save_response,'w') {|f| f.write(output)}
649
613
  end
650
614
 
651
615
  if output && !output.empty?
@@ -723,6 +687,7 @@ class IBMSmartCloud
723
687
  order_by = filters.delete(:order)
724
688
 
725
689
  filters.each do |filter, value|
690
+ filter = filter.to_sym
726
691
  value = value.to_s.upcase if (filter==:status || filter==:state)
727
692
  if filter == :name || filter == :Name
728
693
  instances = instances.select {|inst| inst.send(filter.to_s.capitalize) =~ /#{value}/}
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 3
8
- - 0
9
- version: 0.3.0
8
+ - 1
9
+ version: 0.3.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - yan
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-11-21 00:00:00 -06:00
18
+ date: 2012-03-12 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -41,6 +41,7 @@ files:
41
41
  - cft_smartcloud.gemspec
42
42
  - lib/config/config.yml
43
43
  - lib/curl_client.rb
44
+ - lib/dynamic_help_generator.rb
44
45
  - lib/hash_fix.rb
45
46
  - lib/mime-types-1.16/History.txt
46
47
  - lib/mime-types-1.16/Install.txt