bbrowning-deltacloud-client 0.0.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,176 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ #
2
+ # Copyright (C) 2009 Red Hat, Inc.
3
+ #
4
+ # Licensed to the Apache Software Foundation (ASF) under one or more
5
+ # contributor license agreements. See the NOTICE file distributed with
6
+ # this work for additional information regarding copyright ownership. The
7
+ # ASF licenses this file to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance with the
9
+ # License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+ # License for the specific language governing permissions and limitations
17
+ # under the License.
18
+
19
+ require 'rake/gempackagetask'
20
+
21
+ load 'deltacloud-client.gemspec'
22
+
23
+ desc "Generate documentation"
24
+ task 'documentation' do
25
+ load 'lib/documentation.rb'
26
+ end
27
+
28
+ Rake::GemPackageTask.new(@spec) do |pkg|
29
+ pkg.need_tar = true
30
+ end
31
+
32
+ if Gem.available?('rspec')
33
+ require 'spec/rake/spectask'
34
+ desc "Run all examples"
35
+ Spec::Rake::SpecTask.new('spec') do |t|
36
+ t.spec_files = FileList['specs/**/*_spec.rb']
37
+ end
38
+ end
39
+
40
+ desc "Setup Fixtures"
41
+ task 'fixtures' do
42
+ FileUtils.rm_rf( File.dirname( __FILE__ ) + '/specs/data' )
43
+ FileUtils.cp_r( File.dirname( __FILE__ ) + '/specs/fixtures', File.dirname( __FILE__ ) + '/specs/data' )
44
+ end
45
+
46
+ desc "Clean Fixtures"
47
+ task 'fixtures:clean' do
48
+ FileUtils.rm_rf( File.dirname( __FILE__ ) + '/specs/data' )
49
+ end
data/bin/deltacloudc ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (C) 2009 Red Hat, Inc.
4
+ #
5
+ # Licensed to the Apache Software Foundation (ASF) under one or more
6
+ # contributor license agreements. See the NOTICE file distributed with
7
+ # this work for additional information regarding copyright ownership. The
8
+ # ASF licenses this file to you under the Apache License, Version 2.0 (the
9
+ # "License"); you may not use this file except in compliance with the
10
+ # License. You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17
+ # License for the specific language governing permissions and limitations
18
+ # under the License.
19
+
20
+ require 'rubygems'
21
+ require 'optparse'
22
+ require 'uri'
23
+ require 'lib/deltacloud'
24
+ require 'lib/plain_formatter'
25
+
26
+ include DeltaCloud::PlainFormatter
27
+
28
+ options = {
29
+ :verbose => false
30
+ }
31
+
32
+ @optparse = OptionParser.new do |opts|
33
+
34
+ opts.banner = <<BANNER
35
+ Usage:
36
+ deltacloudc collection operation [options]
37
+
38
+ URL format:
39
+ API_URL=http://[user]:[password]@[api_url][port][/uri]
40
+
41
+ Options:
42
+ BANNER
43
+ opts.on( '-i', '--id ID', 'ID for operation') { |id| options[:id] = id }
44
+ opts.on( '-d', '--image-id ID', 'Image ID') { |id| options[:image_id] = id }
45
+ opts.on( '-a', '--arch ARCH', 'Architecture (x86, x86_64)') { |id| options[:architecture] = id }
46
+ opts.on( '-p', '--hardware-profile HARDWARE_PROFILE', 'Hardware Profile') { |id| options[:hwp_id] = id }
47
+ opts.on( '-n', '--name NAME', 'Name (for instance eg.)') { |name| options[:name] = name }
48
+ opts.on( '-s', '--state STATE', 'Instance state (RUNNING, STOPPED)') { |state| options[:state] = state }
49
+ opts.on( '-u', '--url URL', 'API url ($API_URL variable)') { |url| options[:api_url] = url }
50
+ opts.on( '-l', '--list', 'List collections/operations') { |id| options[:list] = true }
51
+ opts.on( '-h', '--help', 'Display this screen' ) { puts opts ; exit }
52
+ opts.on( '-v', '--version', 'Display API version' ) { options[:version]=true }
53
+ opts.on( '-V', '--verbose', 'Print verbose messages' ) { options[:verbose]=true }
54
+ end
55
+
56
+ def invalid_usage(error_msg='')
57
+ puts "ERROR: #{error_msg}"
58
+ exit(1)
59
+ end
60
+
61
+ @optparse.parse!
62
+
63
+ # First try to get API_URL from environment
64
+ options[:api_url] = ENV['API_URL'] if options[:api_url].nil?
65
+
66
+ url = URI.parse(options[:api_url])
67
+ api_url = "http://#{url.host}#{url.port ? ":#{url.port}" : ''}#{url.path}"
68
+
69
+ options[:collection] = ARGV[0]
70
+ options[:operation] = ARGV[1]
71
+
72
+ # Connect to Deltacloud API and fetch all entry points
73
+ client = DeltaCloud.new(url.user || ENV['API_USER'], url.password || ENV['API_PASSWORD'], api_url)
74
+ collections = client.entry_points.keys
75
+
76
+ # Exclude collection which don't have methods in client library yet
77
+ collections.delete(:instance_states)
78
+
79
+ # If list parameter passed print out available collection
80
+ # with API documentation
81
+ if options[:list] and options[:collection].nil?
82
+ collections.each do |c|
83
+ doc = client.fetch_documentation(c.to_s)
84
+ puts sprintf("%-22s: %s", c.to_s[0, 22], doc[:description])
85
+ end
86
+ exit(0)
87
+ end
88
+
89
+ # If collection parameter is present and user requested list
90
+ # print all operation defined for collection with API documentation
91
+ if options[:list] and options[:collection]
92
+ doc = client.fetch_documentation(options[:collection])
93
+ doc[:operations].each do |c|
94
+ puts sprintf("%-20s: %s", c[:name][0, 20], c[:description])
95
+ end
96
+ exit(0)
97
+ end
98
+
99
+ if options[:version]
100
+ puts "Deltacloud API(#{client.driver_name}) 0.1"
101
+ exit(0)
102
+ end
103
+
104
+ # List items from collection (typically /instances)
105
+ # Do same if 'index' operation is set
106
+ if options[:collection] and ( options[:operation].nil? or options[:operation].eql?('index') )
107
+ invalid_usage("Unknown collection: #{options[:collection]}") unless collections.include?(options[:collection].to_sym)
108
+ params = {}
109
+ params.merge!(:architecture => options[:architecture]) if options[:architecture]
110
+ params.merge!(:id => options[:id]) if options[:id]
111
+ params.merge!(:state => options[:state]) if options[:state]
112
+ client.send(options[:collection].to_s, params).each do |model|
113
+ puts format(model)
114
+ end
115
+ exit(0)
116
+ end
117
+
118
+ if options[:collection] and options[:operation]
119
+
120
+ invalid_usage("Unknown collection: #{options[:collection]}") unless collections.include?(options[:collection].to_sym)
121
+
122
+ params = {}
123
+ params.merge!(:id => options[:id]) if options[:id]
124
+
125
+ # If collection is set and requested operation is 'show' just 'singularize'
126
+ # collection name and print item with specified id (-i parameter)
127
+ if options[:operation].eql?('show')
128
+ puts format(client.send(options[:collection].gsub(/s$/, ''), options[:id]))
129
+ exit(0)
130
+ end
131
+
132
+ # If collection is set and requested operation is create new instance,
133
+ # --image-id, --hardware-profile and --name parameters are used
134
+ # Returns created instance in plain form
135
+ if options[:collection].eql?('instances') and options[:operation].eql?('create')
136
+ invalid_usage("Missing image-id") unless options[:image_id]
137
+ if options[:name] and ! client.feature?(:instances, :user_name)
138
+ invalid_usage("Driver does not support user-supplied name")
139
+ end
140
+ params.merge!(:name => options[:name]) if options[:name]
141
+ params.merge!(:image_id => options[:image_id]) if options[:image_id]
142
+ params.merge!(:hwp_id => options[:hwp_id]) if options[:hwp_id]
143
+ instance = client.create_instance(options[:image_id], params)
144
+ puts format(instance)
145
+ exit(0)
146
+ end
147
+
148
+ # All other operations above collections is done there:
149
+ if options[:collection].eql?('instances')
150
+ instance = client.instance(options[:id])
151
+ instance.send("#{options[:operation]}!".to_s)
152
+ instance = client.instance(options[:id])
153
+ puts format(instance)
154
+ exit(0)
155
+ end
156
+ end
157
+
158
+ # If all above passed (eg. no parameters)
159
+ puts @optparse
data/init.rb ADDED
@@ -0,0 +1,20 @@
1
+ #
2
+ # Copyright (C) 2009 Red Hat, Inc.
3
+ #
4
+ # Licensed to the Apache Software Foundation (ASF) under one or more
5
+ # contributor license agreements. See the NOTICE file distributed with
6
+ # this work for additional information regarding copyright ownership. The
7
+ # ASF licenses this file to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance with the
9
+ # License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+ # License for the specific language governing permissions and limitations
17
+ # under the License.
18
+
19
+
20
+ require 'deltacloud'
data/lib/deltacloud.rb ADDED
@@ -0,0 +1,525 @@
1
+ #
2
+ # Copyright (C) 2009 Red Hat, Inc.
3
+ #
4
+ # Licensed to the Apache Software Foundation (ASF) under one or more
5
+ # contributor license agreements. See the NOTICE file distributed with
6
+ # this work for additional information regarding copyright ownership. The
7
+ # ASF licenses this file to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance with the
9
+ # License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+ # License for the specific language governing permissions and limitations
17
+ # under the License.
18
+
19
+ require 'hpricot'
20
+ require 'rest_client'
21
+ require 'base64'
22
+ require 'logger'
23
+
24
+ module DeltaCloud
25
+
26
+ # Get a new API client instance
27
+ #
28
+ # @param [String, user_name] API user name
29
+ # @param [String, password] API password
30
+ # @param [String, user_name] API URL (eg. http://localhost:3001/api)
31
+ # @return [DeltaCloud::API]
32
+ def self.new(user_name, password, api_url, &block)
33
+ API.new(user_name, password, api_url, &block)
34
+ end
35
+
36
+ # Return a API driver for specified URL
37
+ #
38
+ # @param [String, url] API URL (eg. http://localhost:3001/api)
39
+ def self.driver_name(url)
40
+ API.new(nil, nil, url).driver_name
41
+ end
42
+
43
+ def self.define_class(name)
44
+ @defined_classes ||= []
45
+ if @defined_classes.include?(name)
46
+ self.module_eval("API::#{name}")
47
+ else
48
+ @defined_classes << name unless @defined_classes.include?(name)
49
+ API.const_set(name, Class.new)
50
+ end
51
+ end
52
+
53
+ def self.classes
54
+ @defined_classes || []
55
+ end
56
+
57
+ class API
58
+ attr_accessor :logger
59
+ attr_reader :api_uri, :driver_name, :api_version, :features, :entry_points
60
+
61
+ def initialize(user_name, password, api_url, opts={}, &block)
62
+ opts[:version] = true
63
+ @logger = opts[:verbose] ? Logger.new(STDERR) : []
64
+ @username, @password = user_name, password
65
+ @api_uri = URI.parse(api_url)
66
+ @features, @entry_points = {}, {}
67
+ @verbose = opts[:verbose] || false
68
+ discover_entry_points
69
+ yield self if block_given?
70
+ end
71
+
72
+ def connect(&block)
73
+ yield self
74
+ end
75
+
76
+ # Return API hostname
77
+ def api_host; @api_uri.host ; end
78
+
79
+ # Return API port
80
+ def api_port; @api_uri.port ; end
81
+
82
+ # Return API path
83
+ def api_path; @api_uri.path ; end
84
+
85
+ # Define methods based on 'rel' attribute in entry point
86
+ # Two methods are declared: 'images' and 'image'
87
+ def declare_entry_points_methods(entry_points)
88
+ logger = @logger
89
+ API.instance_eval do
90
+ entry_points.keys.select {|k| [:instance_states].include?(k)==false }.each do |model|
91
+ define_method model do |*args|
92
+ request(:get, "/#{model}", args.first) do |response|
93
+ # Define a new class based on model name
94
+ c = DeltaCloud.define_class("#{model.to_s.classify}")
95
+ # Create collection from index operation
96
+ base_object_collection(c, model, response)
97
+ end
98
+ end
99
+ logger << "[API] Added method #{model}\n"
100
+ define_method :"#{model.to_s.singularize}" do |*args|
101
+ request(:get, "/#{model}/#{args[0]}") do |response|
102
+ # Define a new class based on model name
103
+ c = DeltaCloud.define_class("#{model.to_s.classify}")
104
+ # Build class for returned object
105
+ base_object(c, model, response)
106
+ end
107
+ end
108
+ logger << "[API] Added method #{model.to_s.singularize}\n"
109
+ define_method :"fetch_#{model.to_s.singularize}" do |url|
110
+ id = url.grep(/\/#{model}\/(.*)$/)
111
+ self.send(model.to_s.singularize.to_sym, $1)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def base_object_collection(c, model, response)
118
+ collection = []
119
+ Hpricot::XML(response).search("#{model}/#{model.to_s.singularize}").each do |item|
120
+ c.instance_eval do
121
+ attr_accessor :id
122
+ attr_accessor :uri
123
+ end
124
+ collection << xml_to_class(c, item)
125
+ end
126
+ return collection
127
+ end
128
+
129
+ # Add default attributes [id and href] to class
130
+ def base_object(c, model, response)
131
+ obj = nil
132
+ Hpricot::XML(response).search("#{model.to_s.singularize}").each do |item|
133
+ c.instance_eval do
134
+ attr_accessor :id
135
+ attr_accessor :uri
136
+ end
137
+ obj = xml_to_class(c, item)
138
+ end
139
+ return obj
140
+ end
141
+
142
+ # Convert XML response to defined Ruby Class
143
+ def xml_to_class(c, item)
144
+ obj = c.new
145
+ # Set default attributes
146
+ obj.id = item['id']
147
+ api = self
148
+ c.instance_eval do
149
+ define_method :client do
150
+ api
151
+ end
152
+ end
153
+ obj.uri = item['href']
154
+ logger = @logger
155
+ logger << "[DC] Creating class #{obj.class.name}\n"
156
+ obj.instance_eval do
157
+ # Declare methods for all attributes in object
158
+ item.search('./*').each do |attribute|
159
+ # If attribute is a link to another object then
160
+ # create a method which request this object from API
161
+ if api.entry_points.keys.include?(:"#{attribute.name}s")
162
+ c.instance_eval do
163
+ define_method :"#{attribute.name.sanitize}" do
164
+ client.send(:"#{attribute.name}", attribute['id'] )
165
+ end
166
+ logger << "[DC] Added #{attribute.name} to class #{obj.class.name}\n"
167
+ end
168
+ else
169
+ # Define methods for other attributes
170
+ c.instance_eval do
171
+ case attribute.name
172
+ # When response cointains 'link' block, declare
173
+ # methods to call links inside. This is used for instance
174
+ # to dynamicaly create .stop!, .start! methods
175
+ when "actions":
176
+ actions = []
177
+ attribute.search('link').each do |link|
178
+ actions << [link['rel'], link[:href]]
179
+ define_method :"#{link['rel'].sanitize}!" do
180
+ client.request(:"#{link['method']}", link['href'], {}, {})
181
+ client.send(:"#{item.name}", item['id'])
182
+ end
183
+ end
184
+ define_method :actions do
185
+ actions.collect { |a| a.first }
186
+ end
187
+ define_method :actions_urls do
188
+ urls = {}
189
+ actions.each { |a| urls[a.first] = a.last }
190
+ urls
191
+ end
192
+ # Property attribute is handled differently
193
+ when "property":
194
+ define_method :"#{attribute['name'].sanitize}" do
195
+ if attribute['value'] =~ /^(\d+)$/
196
+ DeltaCloud::HWP::FloatProperty.new(attribute, attribute['name'])
197
+ else
198
+ DeltaCloud::HWP::Property.new(attribute, attribute['name'])
199
+ end
200
+ end
201
+ # Public and private addresses are returned as Array
202
+ when "public_addresses", "private_addresses":
203
+ attr_accessor :"#{attribute.name.sanitize}"
204
+ obj.send(:"#{attribute.name.sanitize}=",
205
+ attribute.search('address').collect { |address| address.text })
206
+ # Value for other attributes are just returned using
207
+ # method with same name as attribute (eg. .owner_id, .state)
208
+ else
209
+ attr_accessor :"#{attribute.name.sanitize}"
210
+ obj.send(:"#{attribute.name.sanitize}=", attribute.text.convert)
211
+ logger << "[DC] Added method #{attribute.name}[#{attribute.text}] to #{obj.class.name}\n"
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ return obj
218
+ end
219
+
220
+ # Get /api and parse entry points
221
+ def discover_entry_points
222
+ return if discovered?
223
+ request(:get, @api_uri.to_s) do |response|
224
+ api_xml = Hpricot::XML(response)
225
+ @driver_name = api_xml.search('/api').first['driver']
226
+ @api_version = api_xml.search('/api').first['version']
227
+ logger << "[API] Version #{@api_version}\n"
228
+ logger << "[API] Driver #{@driver_name}\n"
229
+ api_xml.search("api > link").each do |entry_point|
230
+ rel, href = entry_point['rel'].to_sym, entry_point['href']
231
+ @entry_points.store(rel, href)
232
+ logger << "[API] Entry point '#{rel}' added\n"
233
+ entry_point.search("feature").each do |feature|
234
+ @features[rel] ||= []
235
+ @features[rel] << feature['name'].to_sym
236
+ logger << "[API] Feature #{feature['name']} added to #{rel}\n"
237
+ end
238
+ end
239
+ end
240
+ declare_entry_points_methods(@entry_points)
241
+ end
242
+
243
+ # Create a new instance, using image +image_id+. Possible optiosn are
244
+ #
245
+ # name - a user-defined name for the instance
246
+ # realm - a specific realm for placement of the instance
247
+ # hardware_profile - either a string giving the name of the
248
+ # hardware profile or a hash. The hash must have an
249
+ # entry +id+, giving the id of the hardware profile,
250
+ # and may contain additional names of properties,
251
+ # e.g. 'storage', to override entries in the
252
+ # hardware profile
253
+ def create_instance(image_id, opts={}, &block)
254
+ name = opts[:name]
255
+ realm_id = opts[:realm]
256
+ user_data = opts[:user_data]
257
+
258
+ params = opts.dup
259
+ ( params[:realm_id] = realm_id ) if realm_id
260
+ ( params[:name] = name ) if name
261
+ ( params[:user_data] = user_data ) if user_data
262
+
263
+ if opts[:hardware_profile].is_a?(String)
264
+ params[:hwp_id] = opts[:hardware_profile]
265
+ elsif opts[:hardware_profile].is_a?(Hash)
266
+ params.delete(:hardware_profile)
267
+ opts[:hardware_profile].each do |k,v|
268
+ params[:"hwp_#{k}"] = v
269
+ end
270
+ end
271
+
272
+ params[:image_id] = image_id
273
+ instance = nil
274
+
275
+ request(:post, entry_points[:instances], {}, params) do |response|
276
+ c = DeltaCloud.define_class("Instance")
277
+ instance = base_object(c, :instance, response)
278
+ yield instance if block_given?
279
+ end
280
+
281
+ return instance
282
+ end
283
+
284
+ # Basic request method
285
+ #
286
+ def request(*args, &block)
287
+ conf = {
288
+ :method => (args[0] || 'get').to_sym,
289
+ :path => (args[1]=~/^http/) ? args[1] : "#{api_uri.to_s}#{args[1]}",
290
+ :query_args => args[2] || {},
291
+ :form_data => args[3] || {}
292
+ }
293
+ if conf[:query_args] != {}
294
+ conf[:path] += '?' + URI.escape(conf[:query_args].collect{ |key, value| "#{key}=#{value}" }.join('&')).to_s
295
+ end
296
+ logger << "[#{conf[:method].to_s.upcase}] #{conf[:path]}\n"
297
+ if conf[:method].eql?(:post)
298
+ RestClient.send(:post, conf[:path], conf[:form_data], default_headers) do |response, request, block|
299
+ if response.respond_to?('body')
300
+ yield response.body if block_given?
301
+ else
302
+ yield response.to_s if block_given?
303
+ end
304
+ end
305
+ else
306
+ RestClient.send(conf[:method], conf[:path], default_headers) do |response, request, block|
307
+ if response.respond_to?('body')
308
+ yield response.body if block_given?
309
+ else
310
+ yield response.to_s if block_given?
311
+ end
312
+ end
313
+ end
314
+ end
315
+
316
+ # Check if specified collection have wanted feature
317
+ def feature?(collection, name)
318
+ @feature.has_key?(collection) && @feature[collection].include?(name)
319
+ end
320
+
321
+ # List available instance states and transitions between them
322
+ def instance_states
323
+ states = []
324
+ request(:get, entry_points[:instance_states]) do |response|
325
+ Hpricot::XML(response).search('states/state').each do |state_el|
326
+ state = DeltaCloud::InstanceState::State.new(state_el['name'])
327
+ state_el.search('transition').each do |transition_el|
328
+ state.transitions << DeltaCloud::InstanceState::Transition.new(
329
+ transition_el['to'],
330
+ transition_el['action']
331
+ )
332
+ end
333
+ states << state
334
+ end
335
+ end
336
+ states
337
+ end
338
+
339
+ # Select instance state specified by name
340
+ def instance_state(name)
341
+ instance_states.select { |s| s.name.to_s.eql?(name.to_s) }.first
342
+ end
343
+
344
+ # Skip parsing /api when we already got entry points
345
+ def discovered?
346
+ true if @entry_points!={}
347
+ end
348
+
349
+ def documentation(collection, operation=nil)
350
+ data = {}
351
+ request(:get, "/docs/#{collection}") do |body|
352
+ document = Hpricot::XML(body)
353
+ if operation
354
+ data[:description] = document.search('/docs/collection/operations/operation[@name = "'+operation+'"]/description').first
355
+ return false unless data[:description]
356
+ data[:params] = []
357
+ (document/"/docs/collection/operations/operation[@name='#{operation}']/parameter").each do |param|
358
+ data[:params] << {
359
+ :name => param['name'],
360
+ :required => param['type'] == 'optional',
361
+ :type => (param/'class').text
362
+ }
363
+ end
364
+ else
365
+ data[:description] = (document/'/docs/collection/description').text
366
+ end
367
+ end
368
+ return Documentation.new(data)
369
+ end
370
+
371
+ private
372
+
373
+ def default_headers
374
+ # The linebreaks inserted every 60 characters in the Base64
375
+ # encoded header cause problems under JRuby
376
+ auth_header = "Basic "+Base64.encode64("#{@username}:#{@password}")
377
+ auth_header.gsub!("\n", "")
378
+ {
379
+ :authorization => auth_header,
380
+ :accept => "application/xml"
381
+ }
382
+ end
383
+
384
+ end
385
+
386
+ class Documentation
387
+ attr_reader :description
388
+ attr_reader :params
389
+
390
+ def initialize(opts={})
391
+ @description = opts[:description]
392
+ @params = parse_parameters(opts[:params]) if opts[:params]
393
+ self
394
+ end
395
+
396
+ class OperationParameter
397
+ attr_reader :name
398
+ attr_reader :type
399
+ attr_reader :required
400
+ attr_reader :description
401
+
402
+ def initialize(data)
403
+ @name, @type, @required, @description = data[:name], data[:type], data[:required], data[:description]
404
+ end
405
+
406
+ def to_comment
407
+ " # @param [#{@type}, #{@name}] #{@description}"
408
+ end
409
+
410
+ end
411
+
412
+ private
413
+
414
+ def parse_parameters(params)
415
+ params.collect { |p| OperationParameter.new(p) }
416
+ end
417
+
418
+ end
419
+
420
+ module InstanceState
421
+
422
+ class State
423
+ attr_reader :name
424
+ attr_reader :transitions
425
+
426
+ def initialize(name)
427
+ @name, @transitions = name, []
428
+ end
429
+ end
430
+
431
+ class Transition
432
+ attr_reader :to
433
+ attr_reader :action
434
+
435
+ def initialize(to, action)
436
+ @to = to
437
+ @action = action
438
+ end
439
+
440
+ def auto?
441
+ @action.nil?
442
+ end
443
+ end
444
+ end
445
+
446
+ module HWP
447
+
448
+ class Property
449
+ attr_reader :name, :unit, :value, :kind
450
+
451
+ def initialize(xml, name)
452
+ @name, @kind, @value, @unit = xml['name'], xml['kind'].to_sym, xml['value'], xml['unit']
453
+ declare_ranges(xml)
454
+ self
455
+ end
456
+
457
+ def present?
458
+ ! @value.nil?
459
+ end
460
+
461
+ private
462
+
463
+ def declare_ranges(xml)
464
+ case xml['kind']
465
+ when 'range':
466
+ self.class.instance_eval do
467
+ attr_reader :range
468
+ end
469
+ @range = { :from => xml.search('range').first['first'], :to => xml.search('range').first['last'] }
470
+ when 'enum':
471
+ self.class.instance_eval do
472
+ attr_reader :options
473
+ end
474
+ @options = xml.search('enum/entry').collect { |e| e['value'] }
475
+ end
476
+ end
477
+
478
+ end
479
+
480
+ # FloatProperty is like Property but return value is Float instead of String.
481
+ class FloatProperty < Property
482
+ def initialize(xml, name)
483
+ super(xml, name)
484
+ @value = @value.to_f if @value
485
+ end
486
+ end
487
+ end
488
+
489
+ end
490
+
491
+ class String
492
+
493
+ unless method_defined?(:classify)
494
+ # Create a class name from string
495
+ def classify
496
+ self.singularize.camelize
497
+ end
498
+ end
499
+
500
+ unless method_defined?(:camelize)
501
+ # Camelize converts strings to UpperCamelCase
502
+ def camelize
503
+ self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
504
+ end
505
+ end
506
+
507
+ unless method_defined?(:singularize)
508
+ # Strip 's' character from end of string
509
+ def singularize
510
+ self.gsub(/s$/, '')
511
+ end
512
+ end
513
+
514
+ # Convert string to float if string value seems like Float
515
+ def convert
516
+ return self.to_f if self.strip =~ /^([\d\.]+$)/
517
+ self
518
+ end
519
+
520
+ # Simply converts whitespaces and - symbols to '_' which is safe for Ruby
521
+ def sanitize
522
+ self.gsub(/(\W+)/, '_')
523
+ end
524
+
525
+ end