cft_smartcloud 0.3.0 → 0.3.1

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