steamcannon-deltacloud-client 0.0.9.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/COPYING +176 -0
  2. data/Rakefile +61 -0
  3. data/bin/deltacloudc +158 -0
  4. data/init.rb +20 -0
  5. data/lib/deltacloud.rb +586 -0
  6. data/lib/documentation.rb +98 -0
  7. data/lib/plain_formatter.rb +86 -0
  8. data/specs/data/images/img1.yml +4 -0
  9. data/specs/data/images/img2.yml +4 -0
  10. data/specs/data/images/img3.yml +4 -0
  11. data/specs/data/instances/inst0.yml +16 -0
  12. data/specs/data/instances/inst1.yml +9 -0
  13. data/specs/data/instances/inst2.yml +9 -0
  14. data/specs/data/storage_snapshots/snap1.yml +4 -0
  15. data/specs/data/storage_snapshots/snap2.yml +4 -0
  16. data/specs/data/storage_snapshots/snap3.yml +4 -0
  17. data/specs/data/storage_volumes/vol1.yml +6 -0
  18. data/specs/data/storage_volumes/vol2.yml +6 -0
  19. data/specs/data/storage_volumes/vol3.yml +6 -0
  20. data/specs/fixtures/images/img1.yml +4 -0
  21. data/specs/fixtures/images/img2.yml +4 -0
  22. data/specs/fixtures/images/img3.yml +4 -0
  23. data/specs/fixtures/instances/inst0.yml +16 -0
  24. data/specs/fixtures/instances/inst1.yml +9 -0
  25. data/specs/fixtures/instances/inst2.yml +9 -0
  26. data/specs/fixtures/storage_snapshots/snap1.yml +4 -0
  27. data/specs/fixtures/storage_snapshots/snap2.yml +4 -0
  28. data/specs/fixtures/storage_snapshots/snap3.yml +4 -0
  29. data/specs/fixtures/storage_volumes/vol1.yml +6 -0
  30. data/specs/fixtures/storage_volumes/vol2.yml +6 -0
  31. data/specs/fixtures/storage_volumes/vol3.yml +6 -0
  32. data/specs/hardware_profiles_spec.rb +78 -0
  33. data/specs/images_spec.rb +105 -0
  34. data/specs/initialization_spec.rb +60 -0
  35. data/specs/instance_states_spec.rb +78 -0
  36. data/specs/instances_spec.rb +191 -0
  37. data/specs/realms_spec.rb +64 -0
  38. data/specs/shared/resources.rb +30 -0
  39. data/specs/spec_helper.rb +52 -0
  40. data/specs/storage_snapshot_spec.rb +77 -0
  41. data/specs/storage_volume_spec.rb +89 -0
  42. metadata +183 -0
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,61 @@
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
+ @specs = ['ruby', 'java'].inject({}) do |hash, spec_platform|
29
+ $platform = spec_platform
30
+ hash.update(spec_platform => Gem::Specification.load('deltacloud-client.gemspec'))
31
+ end
32
+
33
+ @specs.values.each do |spec|
34
+ Rake::GemPackageTask.new(spec) do |pkg|
35
+ pkg.need_tar = true
36
+ end
37
+ end
38
+
39
+ if Gem.available?('rspec')
40
+ require 'spec/rake/spectask'
41
+ desc "Run all examples"
42
+ Spec::Rake::SpecTask.new('spec') do |t|
43
+ t.spec_files = FileList['specs/**/*_spec.rb']
44
+ end
45
+ end
46
+
47
+ desc "Setup Fixtures"
48
+ task 'fixtures' do
49
+ FileUtils.rm_rf( File.dirname( __FILE__ ) + '/specs/data' )
50
+ FileUtils.cp_r( File.dirname( __FILE__ ) + '/specs/fixtures', File.dirname( __FILE__ ) + '/specs/data' )
51
+ end
52
+
53
+ desc "Clean Fixtures"
54
+ task 'fixtures:clean' do
55
+ FileUtils.rm_rf( File.dirname( __FILE__ ) + '/specs/data' )
56
+ end
57
+
58
+ begin
59
+ require 'ci/reporter/rake/rspec'
60
+ rescue LoadError
61
+ end
data/bin/deltacloudc ADDED
@@ -0,0 +1,158 @@
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 'deltacloud'
24
+ require '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
+ puts sprintf("%-22s", c.to_s[0, 22])
84
+ end
85
+ exit(0)
86
+ end
87
+
88
+ # If collection parameter is present and user requested list
89
+ # print all operation defined for collection with API documentation
90
+ if options[:list] and options[:collection]
91
+ doc = client.documentation(options[:collection])
92
+ doc.operations.each do |c|
93
+ puts sprintf("%-20s: %s", c.operation, c.description)
94
+ end
95
+ exit(0)
96
+ end
97
+
98
+ if options[:version]
99
+ puts "Deltacloud API(#{client.driver_name}) 0.1"
100
+ exit(0)
101
+ end
102
+
103
+ # List items from collection (typically /instances)
104
+ # Do same if 'index' operation is set
105
+ if options[:collection] and ( options[:operation].nil? or options[:operation].eql?('index') )
106
+ invalid_usage("Unknown collection: #{options[:collection]}") unless collections.include?(options[:collection].to_sym)
107
+ params = {}
108
+ params.merge!(:architecture => options[:architecture]) if options[:architecture]
109
+ params.merge!(:id => options[:id]) if options[:id]
110
+ params.merge!(:state => options[:state]) if options[:state]
111
+ client.send(options[:collection].to_s, params).each do |model|
112
+ puts format(model)
113
+ end
114
+ exit(0)
115
+ end
116
+
117
+ if options[:collection] and options[:operation]
118
+
119
+ invalid_usage("Unknown collection: #{options[:collection]}") unless collections.include?(options[:collection].to_sym)
120
+
121
+ params = {}
122
+ params.merge!(:id => options[:id]) if options[:id]
123
+
124
+ # If collection is set and requested operation is 'show' just 'singularize'
125
+ # collection name and print item with specified id (-i parameter)
126
+ if options[:operation].eql?('show')
127
+ puts format(client.send(options[:collection].gsub(/s$/, ''), options[:id]))
128
+ exit(0)
129
+ end
130
+
131
+ # If collection is set and requested operation is create new instance,
132
+ # --image-id, --hardware-profile and --name parameters are used
133
+ # Returns created instance in plain form
134
+ if options[:collection].eql?('instances') and options[:operation].eql?('create')
135
+ invalid_usage("Missing image-id") unless options[:image_id]
136
+ if options[:name] and ! client.feature?(:instances, :user_name)
137
+ invalid_usage("Driver does not support user-supplied name")
138
+ end
139
+ params.merge!(:name => options[:name]) if options[:name]
140
+ params.merge!(:image_id => options[:image_id]) if options[:image_id]
141
+ params.merge!(:hwp_id => options[:hwp_id]) if options[:hwp_id]
142
+ instance = client.create_instance(options[:image_id], params)
143
+ puts format(instance)
144
+ exit(0)
145
+ end
146
+
147
+ # All other operations above collections is done there:
148
+ if options[:collection].eql?('instances')
149
+ instance = client.instance(options[:id])
150
+ instance.send("#{options[:operation]}!".to_s)
151
+ instance = client.instance(options[:id])
152
+ puts format(instance)
153
+ exit(0)
154
+ end
155
+ end
156
+
157
+ # If all above passed (eg. no parameters)
158
+ 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,586 @@
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 'nokogiri'
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
+ # Check given credentials if their are valid against
37
+ # backend cloud provider
38
+ #
39
+ # @param [String, user_name] API user name
40
+ # @param [String, password] API password
41
+ # @param [String, user_name] API URL (eg. http://localhost:3001/api)
42
+ # @return [true|false]
43
+ def self.valid_credentials?(user_name, password, api_url)
44
+ api=API.new(user_name, password, api_url)
45
+ result = false
46
+ api.request(:get, '', :force_auth => '1') do |response|
47
+ result = true if response.code.eql?(200)
48
+ end
49
+ return result
50
+ end
51
+
52
+ # Return a API driver for specified URL
53
+ #
54
+ # @param [String, url] API URL (eg. http://localhost:3001/api)
55
+ def self.driver_name(url)
56
+ API.new(nil, nil, url).driver_name
57
+ end
58
+
59
+ def self.define_class(name)
60
+ @defined_classes ||= []
61
+ if @defined_classes.include?(name)
62
+ self.module_eval("API::#{name}")
63
+ else
64
+ @defined_classes << name unless @defined_classes.include?(name)
65
+ API.const_set(name, Class.new)
66
+ end
67
+ end
68
+
69
+ def self.classes
70
+ @defined_classes || []
71
+ end
72
+
73
+ class API
74
+ attr_accessor :logger
75
+ attr_reader :api_uri, :driver_name, :api_version, :features, :entry_points
76
+
77
+ def initialize(user_name, password, api_url, opts={}, &block)
78
+ opts[:version] = true
79
+ @logger = opts[:verbose] ? Logger.new(STDERR) : []
80
+ @username, @password = user_name, password
81
+ @api_uri = URI.parse(api_url)
82
+ @features, @entry_points = {}, {}
83
+ @verbose = opts[:verbose] || false
84
+ discover_entry_points
85
+ yield self if block_given?
86
+ end
87
+
88
+ def connect(&block)
89
+ yield self
90
+ end
91
+
92
+ # Return API hostname
93
+ def api_host; @api_uri.host ; end
94
+
95
+ # Return API port
96
+ def api_port; @api_uri.port ; end
97
+
98
+ # Return API path
99
+ def api_path; @api_uri.path ; end
100
+
101
+ # Define methods based on 'rel' attribute in entry point
102
+ # Two methods are declared: 'images' and 'image'
103
+ def declare_entry_points_methods(entry_points)
104
+ logger = @logger
105
+ API.instance_eval do
106
+ entry_points.keys.select {|k| [:instance_states].include?(k)==false }.each do |model|
107
+ define_method model do |*args|
108
+ request(:get, "/#{model}", args.first) do |response|
109
+ # Define a new class based on model name
110
+ c = DeltaCloud.define_class("#{model.to_s.classify}")
111
+ # Create collection from index operation
112
+ base_object_collection(c, model, response)
113
+ end
114
+ end
115
+ logger << "[API] Added method #{model}\n"
116
+ define_method :"#{model.to_s.singularize}" do |*args|
117
+ request(:get, "/#{model}/#{args[0]}") do |response|
118
+ # Define a new class based on model name
119
+ c = DeltaCloud.define_class("#{model.to_s.classify}")
120
+ # Build class for returned object
121
+ base_object(c, model, response)
122
+ end
123
+ end
124
+ logger << "[API] Added method #{model.to_s.singularize}\n"
125
+ define_method :"fetch_#{model.to_s.singularize}" do |url|
126
+ id = url.grep(/\/#{model}\/(.*)$/)
127
+ self.send(model.to_s.singularize.to_sym, $1)
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def base_object_collection(c, model, response)
134
+ collection = []
135
+ Nokogiri::XML(response).xpath("#{model}/#{model.to_s.singularize}").each do |item|
136
+ c.instance_eval do
137
+ attr_accessor :id
138
+ attr_accessor :uri
139
+ end
140
+ collection << xml_to_class(c, item)
141
+ end
142
+ return collection
143
+ end
144
+
145
+ # Add default attributes [id and href] to class
146
+ def base_object(c, model, response)
147
+ obj = nil
148
+ Nokogiri::XML(response).xpath("#{model.to_s.singularize}").each do |item|
149
+ c.instance_eval do
150
+ attr_accessor :id
151
+ attr_accessor :uri
152
+ end
153
+ obj = xml_to_class(c, item)
154
+ end
155
+ return obj
156
+ end
157
+
158
+ # Convert XML response to defined Ruby Class
159
+ def xml_to_class(c, item)
160
+ obj = c.new
161
+ # Set default attributes
162
+ obj.id = item['id']
163
+ api = self
164
+ c.instance_eval do
165
+ define_method :client do
166
+ api
167
+ end
168
+ end
169
+ obj.uri = item['href']
170
+ logger = @logger
171
+ logger << "[DC] Creating class #{obj.class.name}\n"
172
+ obj.instance_eval do
173
+ # Declare methods for all attributes in object
174
+ item.xpath('./*').each do |attribute|
175
+ # If attribute is a link to another object then
176
+ # create a method which request this object from API
177
+ if api.entry_points.keys.include?(:"#{attribute.name}s")
178
+ c.instance_eval do
179
+ define_method :"#{attribute.name.sanitize}" do
180
+ client.send(:"#{attribute.name}", attribute['id'] )
181
+ end
182
+ logger << "[DC] Added #{attribute.name} to class #{obj.class.name}\n"
183
+ end
184
+ else
185
+ # Define methods for other attributes
186
+ c.instance_eval do
187
+ case attribute.name
188
+ # When response cointains 'link' block, declare
189
+ # methods to call links inside. This is used for instance
190
+ # to dynamicaly create .stop!, .start! methods
191
+ when "actions":
192
+ actions = []
193
+ attribute.xpath('link').each do |link|
194
+ actions << [link['rel'], link[:href]]
195
+ define_method :"#{link['rel'].sanitize}!" do |*params|
196
+ client.request(:"#{link['method']}", link['href'], {}, params.first || {})
197
+ @current_state = client.send(:"#{item.name}", item['id']).state
198
+ obj.instance_eval do |o|
199
+ def state
200
+ @current_state
201
+ end
202
+ end
203
+ end
204
+ end
205
+ define_method :actions do
206
+ actions.collect { |a| a.first }
207
+ end
208
+ define_method :actions_urls do
209
+ urls = {}
210
+ actions.each { |a| urls[a.first] = a.last }
211
+ urls
212
+ end
213
+ # Property attribute is handled differently
214
+ when "property":
215
+ attr_accessor :"#{attribute['name'].sanitize}"
216
+ if attribute['value'] =~ /^(\d+)$/
217
+ obj.send(:"#{attribute['name'].sanitize}=",
218
+ DeltaCloud::HWP::FloatProperty.new(attribute, attribute['name']))
219
+ else
220
+ obj.send(:"#{attribute['name'].sanitize}=",
221
+ DeltaCloud::HWP::Property.new(attribute, attribute['name']))
222
+ end
223
+ # Public and private addresses are returned as Array
224
+ when "public_addresses", "private_addresses":
225
+ attr_accessor :"#{attribute.name.sanitize}"
226
+ obj.send(:"#{attribute.name.sanitize}=",
227
+ attribute.xpath('address').collect { |address| address.text })
228
+ # Value for other attributes are just returned using
229
+ # method with same name as attribute (eg. .owner_id, .state)
230
+ else
231
+ attr_accessor :"#{attribute.name.sanitize}"
232
+ obj.send(:"#{attribute.name.sanitize}=", attribute.text.convert)
233
+ logger << "[DC] Added method #{attribute.name}[#{attribute.text}] to #{obj.class.name}\n"
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ return obj
240
+ end
241
+
242
+ # Get /api and parse entry points
243
+ def discover_entry_points
244
+ return if discovered?
245
+ request(:get, @api_uri.to_s) do |response|
246
+ api_xml = Nokogiri::XML(response)
247
+ @driver_name = api_xml.xpath('/api').first['driver']
248
+ @api_version = api_xml.xpath('/api').first['version']
249
+ logger << "[API] Version #{@api_version}\n"
250
+ logger << "[API] Driver #{@driver_name}\n"
251
+ api_xml.css("api > link").each do |entry_point|
252
+ rel, href = entry_point['rel'].to_sym, entry_point['href']
253
+ @entry_points.store(rel, href)
254
+ logger << "[API] Entry point '#{rel}' added\n"
255
+ entry_point.css("feature").each do |feature|
256
+ @features[rel] ||= []
257
+ @features[rel] << feature['name'].to_sym
258
+ logger << "[API] Feature #{feature['name']} added to #{rel}\n"
259
+ end
260
+ end
261
+ end
262
+ declare_entry_points_methods(@entry_points)
263
+ end
264
+
265
+ def create_key(opts={}, &block)
266
+ params = { :name => opts[:name] }
267
+ key = nil
268
+ request(:post, entry_points[:keys], {}, params) do |response|
269
+ c = DeltaCloud.define_class("Key")
270
+ key = base_object(c, :key, response)
271
+ yield key if block_given?
272
+ end
273
+ return key
274
+ end
275
+
276
+ # Create a new instance, using image +image_id+. Possible optiosn are
277
+ #
278
+ # name - a user-defined name for the instance
279
+ # realm - a specific realm for placement of the instance
280
+ # hardware_profile - either a string giving the name of the
281
+ # hardware profile or a hash. The hash must have an
282
+ # entry +id+, giving the id of the hardware profile,
283
+ # and may contain additional names of properties,
284
+ # e.g. 'storage', to override entries in the
285
+ # hardware profile
286
+ def create_instance(image_id, opts={}, &block)
287
+ name = opts[:name]
288
+ realm_id = opts[:realm]
289
+ user_data = opts[:user_data]
290
+ key_name = opts[:key_name]
291
+
292
+ params = opts.dup
293
+ ( params[:realm_id] = realm_id ) if realm_id
294
+ ( params[:name] = name ) if name
295
+ ( params[:user_data] = user_data ) if user_data
296
+ ( params[:keyname] = key_name ) if key_name
297
+
298
+ if opts[:hardware_profile].is_a?(String)
299
+ params[:hwp_id] = opts[:hardware_profile]
300
+ elsif opts[:hardware_profile].is_a?(Hash)
301
+ params.delete(:hardware_profile)
302
+ opts[:hardware_profile].each do |k,v|
303
+ params[:"hwp_#{k}"] = v
304
+ end
305
+ end
306
+
307
+ params[:image_id] = image_id
308
+ instance = nil
309
+
310
+ request(:post, entry_points[:instances], {}, params) do |response|
311
+ c = DeltaCloud.define_class("Instance")
312
+ instance = base_object(c, :instance, response)
313
+ yield instance if block_given?
314
+ end
315
+
316
+ return instance
317
+ end
318
+
319
+
320
+ def create_storage_volume(opts={}, &block)
321
+ params = opts.dup
322
+ params[:realm_id] ||= params.delete(:realm)
323
+ storage_volume = nil
324
+
325
+ request(:post, entry_points[:storage_volumes], {}, params) do |response|
326
+ c = DeltaCloud.define_class("StorageVolume")
327
+ storage_volume = base_object(c, :storage_volume, response)
328
+ yield storage_volume if block_given?
329
+ end
330
+
331
+ storage_volume
332
+ end
333
+
334
+ # Basic request method
335
+ #
336
+ def request(*args, &block)
337
+ conf = {
338
+ :method => (args[0] || 'get').to_sym,
339
+ :path => (args[1]=~/^http/) ? args[1] : "#{api_uri.to_s}#{args[1]}",
340
+ :query_args => args[2] || {},
341
+ :form_data => args[3] || {}
342
+ }
343
+ if conf[:query_args] != {}
344
+ conf[:path] += '?' + URI.escape(conf[:query_args].collect{ |key, value| "#{key}=#{value}" }.join('&')).to_s
345
+ end
346
+ logger << "[#{conf[:method].to_s.upcase}] #{conf[:path]}\n"
347
+ if conf[:method].eql?(:post)
348
+ RestClient.send(:post, conf[:path], conf[:form_data], default_headers) do |response, request, block|
349
+ if response.respond_to?('body')
350
+ yield response.body if block_given?
351
+ else
352
+ yield response.to_s if block_given?
353
+ end
354
+ end
355
+ else
356
+ RestClient.send(conf[:method], conf[:path], default_headers) do |response, request, block|
357
+ if response.respond_to?('body')
358
+ yield response.body if block_given?
359
+ else
360
+ yield response.to_s if block_given?
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ # Check if specified collection have wanted feature
367
+ def feature?(collection, name)
368
+ @features.has_key?(collection) && @features[collection].include?(name)
369
+ end
370
+
371
+ # List available instance states and transitions between them
372
+ def instance_states
373
+ states = []
374
+ request(:get, entry_points[:instance_states]) do |response|
375
+ Nokogiri::XML(response).xpath('states/state').each do |state_el|
376
+ state = DeltaCloud::InstanceState::State.new(state_el['name'])
377
+ state_el.xpath('transition').each do |transition_el|
378
+ state.transitions << DeltaCloud::InstanceState::Transition.new(
379
+ transition_el['to'],
380
+ transition_el['action']
381
+ )
382
+ end
383
+ states << state
384
+ end
385
+ end
386
+ states
387
+ end
388
+
389
+ # Select instance state specified by name
390
+ def instance_state(name)
391
+ instance_states.select { |s| s.name.to_s.eql?(name.to_s) }.first
392
+ end
393
+
394
+ # Skip parsing /api when we already got entry points
395
+ def discovered?
396
+ true if @entry_points!={}
397
+ end
398
+
399
+ def documentation(collection, operation=nil)
400
+ data = {}
401
+ request(:get, "/docs/#{collection}") do |body|
402
+ document = Nokogiri::XML(body)
403
+ if operation
404
+ data[:operation] = operation
405
+ data[:description] = document.xpath('/docs/collection/operations/operation[@name = "'+operation+'"]/description').first.text.strip
406
+ return false unless data[:description]
407
+ data[:params] = []
408
+ (document/"/docs/collection/operations/operation[@name='#{operation}']/parameter").each do |param|
409
+ data[:params] << {
410
+ :name => param['name'],
411
+ :required => param['type'] == 'optional',
412
+ :type => (param/'class').text
413
+ }
414
+ end
415
+ else
416
+ data[:description] = (document/'/docs/collection/description').text
417
+ data[:collection] = collection
418
+ data[:operations] = (document/"/docs/collection/operations/operation").collect{ |o| o['name'] }
419
+ end
420
+ end
421
+ return Documentation.new(self, data)
422
+ end
423
+
424
+ private
425
+
426
+ def default_headers
427
+ # The linebreaks inserted every 60 characters in the Base64
428
+ # encoded header cause problems under JRuby
429
+ auth_header = "Basic "+Base64.encode64("#{@username}:#{@password}")
430
+ auth_header.gsub!("\n", "")
431
+ {
432
+ :authorization => auth_header,
433
+ :accept => "application/xml"
434
+ }
435
+ end
436
+
437
+ end
438
+
439
+ class Documentation
440
+
441
+ attr_reader :api, :description, :params, :collection_operations
442
+ attr_reader :collection, :operation
443
+
444
+ def initialize(api, opts={})
445
+ @description, @api = opts[:description], api
446
+ @params = parse_parameters(opts[:params]) if opts[:params]
447
+ @collection_operations = opts[:operations] if opts[:operations]
448
+ @collection = opts[:collection]
449
+ @operation = opts[:operation]
450
+ self
451
+ end
452
+
453
+ def operations
454
+ @collection_operations.collect { |o| api.documentation(@collection, o) }
455
+ end
456
+
457
+ class OperationParameter
458
+ attr_reader :name
459
+ attr_reader :type
460
+ attr_reader :required
461
+ attr_reader :description
462
+
463
+ def initialize(data)
464
+ @name, @type, @required, @description = data[:name], data[:type], data[:required], data[:description]
465
+ end
466
+
467
+ def to_comment
468
+ " # @param [#{@type}, #{@name}] #{@description}"
469
+ end
470
+
471
+ end
472
+
473
+ private
474
+
475
+ def parse_parameters(params)
476
+ params.collect { |p| OperationParameter.new(p) }
477
+ end
478
+
479
+ end
480
+
481
+ module InstanceState
482
+
483
+ class State
484
+ attr_reader :name
485
+ attr_reader :transitions
486
+
487
+ def initialize(name)
488
+ @name, @transitions = name, []
489
+ end
490
+ end
491
+
492
+ class Transition
493
+ attr_reader :to
494
+ attr_reader :action
495
+
496
+ def initialize(to, action)
497
+ @to = to
498
+ @action = action
499
+ end
500
+
501
+ def auto?
502
+ @action.nil?
503
+ end
504
+ end
505
+ end
506
+
507
+ module HWP
508
+
509
+ class Property
510
+ attr_reader :name, :unit, :value, :kind
511
+
512
+ def initialize(xml, name)
513
+ @name, @kind, @value, @unit = xml['name'], xml['kind'].to_sym, xml['value'], xml['unit']
514
+ declare_ranges(xml)
515
+ self
516
+ end
517
+
518
+ def present?
519
+ ! @value.nil?
520
+ end
521
+
522
+ private
523
+
524
+ def declare_ranges(xml)
525
+ case xml['kind']
526
+ when 'range':
527
+ self.class.instance_eval do
528
+ attr_reader :range
529
+ end
530
+ @range = { :from => xml.xpath('range').first['first'], :to => xml.xpath('range').first['last'] }
531
+ when 'enum':
532
+ self.class.instance_eval do
533
+ attr_reader :options
534
+ end
535
+ @options = xml.xpath('enum/entry').collect { |e| e['value'] }
536
+ end
537
+ end
538
+
539
+ end
540
+
541
+ # FloatProperty is like Property but return value is Float instead of String.
542
+ class FloatProperty < Property
543
+ def initialize(xml, name)
544
+ super(xml, name)
545
+ @value = @value.to_f if @value
546
+ end
547
+ end
548
+ end
549
+
550
+ end
551
+
552
+ class String
553
+
554
+ unless method_defined?(:classify)
555
+ # Create a class name from string
556
+ def classify
557
+ self.singularize.camelize
558
+ end
559
+ end
560
+
561
+ unless method_defined?(:camelize)
562
+ # Camelize converts strings to UpperCamelCase
563
+ def camelize
564
+ self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
565
+ end
566
+ end
567
+
568
+ unless method_defined?(:singularize)
569
+ # Strip 's' character from end of string
570
+ def singularize
571
+ self.gsub(/s$/, '')
572
+ end
573
+ end
574
+
575
+ # Convert string to float if string value seems like Float
576
+ def convert
577
+ return self.to_f if self.strip =~ /^([\d\.]+$)/
578
+ self
579
+ end
580
+
581
+ # Simply converts whitespaces and - symbols to '_' which is safe for Ruby
582
+ def sanitize
583
+ self.gsub(/(\W+)/, '_')
584
+ end
585
+
586
+ end