kitchen-oraclecloud 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 33b69dd01851094025ad850c33a4aabe6d99d423
4
+ data.tar.gz: 98ccb73064289c4ac8a899fc3a21a56761ed5892
5
+ SHA512:
6
+ metadata.gz: d3def71eb20c1a700bb650601e9d5b13eb96ef99c50829bd93f7ccef2f83ad32520ec6923ee66a4e784d8e39057eb58baf20b6b9e0f7e35f6b79257ffe2c67ca
7
+ data.tar.gz: 3091a8663a5754f2ead5829e5aac8c79382599f6c5a380670aa20e7524cf7c3e5bafa86ea5c20c2644f9ad06f7d13ee4b9fa55ae8c7b95aa9d8467fda58c249a
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.kitchen
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ Metrics/AbcSize:
2
+ Max: 50
3
+ Metrics/ClassLength:
4
+ Max: 125
5
+ Metrics/LineLength:
6
+ Max: 130
7
+ Metrics/MethodLength:
8
+ Max: 25
9
+ Style/Documentation:
10
+ Enabled: false
11
+ Style/SignalException:
12
+ Enabled: false
13
+ Style/SpaceInsideBrackets:
14
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # kitchen-oraclecloud
2
+
3
+ ## v1.0.0 (2015-10-15)
4
+ * Initial release
5
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,201 @@
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
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # kitchen-oraclecloud
2
+
3
+ A driver to allow Test Kitchen to consume Oracle Cloud resources to perform testing.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'kitchen-oraclecloud'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install kitchen-oraclecloud
20
+
21
+ Or even better, install it via ChefDK:
22
+
23
+ $ chef gem install kitchen-oraclecloud
24
+
25
+ ## Usage
26
+
27
+ After installing the gem as described above, edit your .kitchen.yml file to set the driver to 'oraclecloud' and supply your login credentials:
28
+
29
+ ```yaml
30
+ driver:
31
+ name: oraclecloud
32
+ username: user@domain.io
33
+ password: mypassword
34
+ identity_domain: oracle12345
35
+ api_url: https://api.cloud.oracle.com
36
+ verify_ssl: true
37
+ ```
38
+
39
+ Then configure your platforms. A shape and image is required for each platform:
40
+
41
+ ```yaml
42
+ platforms:
43
+ - name: oel64
44
+ driver:
45
+ shape: oc3
46
+ image: /oracle/public/oel_6.4_20GB_x11_RD
47
+ - name: oel66
48
+ driver:
49
+ shape: oc3
50
+ image: /oracle/public/oel_6.6_20GB_x11_RD
51
+ ```
52
+
53
+ Other options that you can set include:
54
+
55
+ * **sshkeys**: array of Oracle Cloud SSH keys to associate with the instance.
56
+ * **project_name**: optional; descriptive string to be used in the orchestration name, in addition to the Test Kitchen instance name. If one is not provided, a UUID will be generated. This helps keep the orchestration names unique but allows you to set something more descriptive for use when displaying all orchestrations in the UI.
57
+ * **description**: optional; override the default description supplied by Test Kitchen
58
+ * **public_ip**: optional; set to `pool` if you want Oracle Cloud to assign an IP from the default pool, or specify an existing IP Reservation name
59
+ * **wait_time**: optional; number of seconds to wait for a server to start. Defaults to 600.
60
+ * **refresh_time**: optional; number of seconds sleep between checks on whether a server has started. Defaults to 2.
61
+
62
+ All of these settings can be set per-platform, as shown above, or can be set globally in the `driver` section of your .kitchen.yml:
63
+
64
+ ```yaml
65
+ driver:
66
+ name: oraclecloud
67
+ sshkeys:
68
+ - user1@domain.io/key1
69
+ - user1@domain.io/key2
70
+ - user2@domain.io/user2key
71
+ ```
72
+
73
+ ### Username
74
+
75
+ Most Oracle Cloud images use a default username of "opc". However, Test Kitchen assumes the default user is "root" so you will need to override this in your .kitchen.yml:
76
+
77
+ ```yaml
78
+ transport:
79
+ username: opc
80
+ ```
81
+
82
+ ## License and Authors
83
+
84
+ Author:: Chef Partner Engineering (<partnereng@chef.io>)
85
+
86
+ Copyright:: Copyright (c) 2015 Chef Software, Inc.
87
+
88
+ License:: Apache License, Version 2.0
89
+
90
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
91
+ this file except in compliance with the License. You may obtain a copy of the License at
92
+
93
+ ```
94
+ http://www.apache.org/licenses/LICENSE-2.0
95
+ ```
96
+
97
+ Unless required by applicable law or agreed to in writing, software distributed under the
98
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
99
+ either express or implied. See the License for the specific language governing permissions
100
+ and limitations under the License.
101
+
102
+ ## Contributing
103
+
104
+ We'd love to hear from you if this doesn't work for you. Please log a GitHub issue, or even better, submit a Pull Request with a fix!
105
+
106
+ 1. Fork it ( https://github.com/chef-partners/kitchen-oraclecloud/fork )
107
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
108
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
109
+ 4. Push to the branch (`git push origin my-new-feature`)
110
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kitchen/driver/oraclecloud_version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'kitchen-oraclecloud'
8
+ spec.version = Kitchen::Driver::ORACLECLOUD_VERSION
9
+ spec.authors = ['Chef Partner Engineering']
10
+ spec.email = ['partnereng@chef.io']
11
+ spec.summary = 'A Test Kitchen driver for Oracle Cloud'
12
+ spec.description = spec.summary
13
+ spec.homepage = 'https://github.com/chef-partners/kitchen-oraclecloud'
14
+ spec.license = 'Apache 2.0'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = []
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'test-kitchen', '~> 1.4', '>= 1.4.1'
22
+ spec.add_dependency 'oraclecloud', '~> 1.0'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.7'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec', '~> 3.2'
27
+ end
@@ -0,0 +1,213 @@
1
+ #
2
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
3
+ # Copyright:: Copyright (c) 2015 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'kitchen'
20
+ require 'oraclecloud'
21
+ require 'securerandom'
22
+ require_relative 'oraclecloud_version'
23
+
24
+ module Kitchen
25
+ module Driver
26
+ class Oraclecloud < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
27
+ kitchen_driver_api_version 2
28
+ plugin_version Kitchen::Driver::ORACLECLOUD_VERSION
29
+
30
+ required_config :username
31
+ required_config :password
32
+ required_config :api_url
33
+ required_config :identity_domain
34
+ required_config :shape
35
+ required_config :image
36
+
37
+ default_config :verify_ssl, true
38
+ default_config :wait_time, 600
39
+ default_config :refresh_time, 2
40
+ default_config :sshkeys, []
41
+ default_config :description, nil
42
+ default_config :project_name, nil
43
+ default_config :public_ip, nil
44
+
45
+ def name
46
+ 'OracleCloud'
47
+ end
48
+
49
+ def create(state)
50
+ return if state[:orchestration_id]
51
+
52
+ info('Creating Oracle Cloud orchestration...')
53
+ orchestration
54
+
55
+ info("Orchestration #{orchestration.name_with_container} created. Starting...")
56
+ orchestration.start
57
+ wait_for_status(orchestration, 'ready')
58
+
59
+ state[:orchestration_id] = orchestration.name_with_container
60
+
61
+ ip_address = server_ip_address
62
+ raise 'No IP address returned for Oracle Cloud instance' if ip_address.nil?
63
+
64
+ state[:hostname] = ip_address
65
+
66
+ wait_for_server(state)
67
+ info("Server #{orchestration_name} ready.")
68
+ end
69
+
70
+ def destroy(state)
71
+ return if state[:orchestration_id].nil?
72
+
73
+ info("Looking up orchestration #{state[:orchestration_id]}...")
74
+
75
+ begin
76
+ orchestration(state[:orchestration_id])
77
+ rescue OracleCloud::Exception::HTTPNotFound
78
+ warn("No orchestration found with ID #{state[:orchestration_id]}, assuming it has been destroyed already.")
79
+ return
80
+ end
81
+
82
+ info("Stopping orchestration #{orchestration.name_with_container} and associated instance...")
83
+ orchestration.stop
84
+ wait_for_status(orchestration, 'stopped')
85
+
86
+ info("Deleting orchestration #{orchestration.name_with_container} and associated instance...")
87
+ orchestration.delete
88
+ info('Orchestration deleted.')
89
+ end
90
+
91
+ def oraclecloud_client
92
+ @client ||= OracleCloud::Client.new(
93
+ username: config[:username],
94
+ password: config[:password],
95
+ api_url: config[:api_url],
96
+ identity_domain: config[:identity_domain],
97
+ verify_ssl: config[:verify_ssl]
98
+ )
99
+ end
100
+
101
+ def orchestration(name = nil)
102
+ return @orchestration if @orchestration
103
+
104
+ if name
105
+ @orchestration = oraclecloud_client.orchestrations.by_name(name)
106
+ else
107
+ @orchestration = oraclecloud_client.orchestrations.create(
108
+ name: orchestration_name,
109
+ description: description,
110
+ instances: [ instance_request ]
111
+ )
112
+ end
113
+
114
+ @orchestration
115
+ end
116
+
117
+ def instance_request
118
+ oraclecloud_client.instance_request(
119
+ name: orchestration_name,
120
+ shape: config[:shape],
121
+ imagelist: config[:image],
122
+ sshkeys: sshkeys,
123
+ public_ip: public_ip
124
+ )
125
+ end
126
+
127
+ def server
128
+ @server ||= orchestration.instances.first
129
+ end
130
+
131
+ def server_ip_address
132
+ public_ips = server.public_ip_addresses
133
+ public_ips.empty? ? server.ip_address : public_ips.first
134
+ end
135
+
136
+ def wait_for_server(state)
137
+ info("Server #{orchestration_name} created. Waiting until ready...")
138
+ begin
139
+ instance.transport.connection(state).wait_until_ready
140
+ rescue
141
+ error("Server #{orchestration_name} not reachable. Destroying server...")
142
+ destroy(state)
143
+ raise
144
+ end
145
+ end
146
+
147
+ def wait_for_status(item, requested_status)
148
+ last_status = ''
149
+
150
+ begin
151
+ Timeout.timeout(wait_time) do
152
+ loop do
153
+ item.refresh
154
+ current_status = item.status
155
+
156
+ if item.error?
157
+ error_str = "Request encountered an error: #{item.errors}"
158
+ error(error_str)
159
+ raise error_str
160
+ end
161
+
162
+ unless last_status == current_status
163
+ last_status = current_status
164
+ info("Current status: #{current_status}.")
165
+ end
166
+
167
+ break if current_status == requested_status
168
+
169
+ sleep refresh_time
170
+ end
171
+ end
172
+ rescue Timeout::Error
173
+ error("Request did not complete in #{wait_time} seconds. Check the Oracle Cloud Web UI for more information.")
174
+ raise
175
+ end
176
+ end
177
+
178
+ def wait_time
179
+ config[:wait_time].to_i
180
+ end
181
+
182
+ def refresh_time
183
+ config[:refresh_time].to_i
184
+ end
185
+
186
+ def description
187
+ config[:description].nil? ? "#{instance.name} for #{username} via Test Kitchen" : config[:description]
188
+ end
189
+
190
+ def username
191
+ config[:username]
192
+ end
193
+
194
+ def orchestration_name
195
+ "TK-#{project_name}-#{instance.name.gsub(/\s+/, '')}"
196
+ end
197
+
198
+ def project_name
199
+ @project_name ||= config[:project_name].nil? ? SecureRandom.uuid : config[:project_name].gsub(/\s+/, '')
200
+ end
201
+
202
+ def sshkeys
203
+ config[:sshkeys].map { |key| "#{oraclecloud_client.compute_identity_domain}/#{key}" }
204
+ end
205
+
206
+ def public_ip
207
+ return nil unless config[:public_ip]
208
+
209
+ (config[:public_ip] == 'pool') ? :pool : "ipreservation:#{config[:public_ip]}"
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,23 @@
1
+ #
2
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
3
+ # Copyright:: Copyright (c) 2015 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Kitchen
20
+ module Driver
21
+ ORACLECLOUD_VERSION = '1.0.0'
22
+ end
23
+ end
@@ -0,0 +1,447 @@
1
+ #
2
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
3
+ # Copyright:: Copyright (c) 2015 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'spec_helper'
20
+ require 'oraclecloud'
21
+ require 'kitchen/driver/oraclecloud'
22
+ require 'kitchen/provisioner/dummy'
23
+ require 'kitchen/transport/dummy'
24
+ require 'kitchen/verifier/dummy'
25
+
26
+ describe Kitchen::Driver::Oraclecloud do
27
+ let(:logged_output) { StringIO.new }
28
+ let(:logger) { Logger.new(logged_output) }
29
+ let(:platform) { Kitchen::Platform.new(name: 'fake_platform') }
30
+ let(:transport) { Kitchen::Transport::Dummy.new }
31
+ let(:driver) { Kitchen::Driver::Oraclecloud.new(config) }
32
+
33
+ let(:config) do
34
+ {
35
+ api_url: 'https://testcloud.oracle.com',
36
+ username: 'test_user',
37
+ password: 'test_password',
38
+ identity_domain: 'test_domain',
39
+ shape: 'test_shape',
40
+ image: 'test_image',
41
+ verify_ssl: true
42
+ }
43
+ end
44
+
45
+ let(:instance) do
46
+ instance_double(Kitchen::Instance,
47
+ logger: logger,
48
+ transport: transport,
49
+ platform: platform,
50
+ to_str: 'instance_str'
51
+ )
52
+ end
53
+
54
+ before do
55
+ allow(driver).to receive(:instance).and_return(instance)
56
+ end
57
+
58
+ it 'driver API version is 2' do
59
+ expect(driver.diagnose_plugin[:api_version]).to eq(2)
60
+ end
61
+
62
+ describe '#name' do
63
+ it 'has an overridden name' do
64
+ expect(driver.name).to eq('OracleCloud')
65
+ end
66
+ end
67
+
68
+ describe '#create' do
69
+ let(:state) { {} }
70
+ let(:orchestration) { double('orchestration') }
71
+
72
+ before do
73
+ allow(driver).to receive(:orchestration).and_return(orchestration)
74
+ allow(driver).to receive(:wait_for_status)
75
+ allow(driver).to receive(:wait_for_server)
76
+ allow(driver).to receive(:server_ip_address).and_return('1.2.3.4')
77
+ allow(instance).to receive(:name).and_return('instance_name')
78
+ allow(orchestration).to receive(:start)
79
+ allow(orchestration).to receive(:name_with_container)
80
+ end
81
+
82
+ context 'when the server is already created' do
83
+ let(:state) { { orchestration_id: 'orch1' } }
84
+
85
+ it 'does not create an orchestration' do
86
+ expect(driver).not_to receive(:orchestration)
87
+ driver.create(state)
88
+ end
89
+ end
90
+
91
+ it 'requests the server' do
92
+ expect(driver).to receive(:orchestration).and_return(orchestration)
93
+ driver.create(state)
94
+ end
95
+
96
+ it 'starts the orchestration' do
97
+ expect(orchestration).to receive(:start)
98
+ driver.create(state)
99
+ end
100
+
101
+ it 'waits for the orchestration to become ready' do
102
+ expect(driver).to receive(:wait_for_status).with(orchestration, 'ready')
103
+ driver.create(state)
104
+ end
105
+
106
+ it 'sets the orchesration ID in the state object' do
107
+ allow(orchestration).to receive(:name_with_container).and_return('orch1')
108
+ driver.create(state)
109
+
110
+ expect(state[:orchestration_id]).to eq('orch1')
111
+ end
112
+
113
+ context 'when no IP address is available for the server' do
114
+ it 'raises an exception' do
115
+ expect(driver).to receive(:server_ip_address).and_return(nil)
116
+ expect { driver.create(state) }.to raise_error(RuntimeError)
117
+ end
118
+ end
119
+
120
+ context 'when an IP address is available' do
121
+ it 'sets it in the state object' do
122
+ expect(driver).to receive(:server_ip_address).and_return('1.2.3.4')
123
+ driver.create(state)
124
+ expect(state[:hostname]).to eq('1.2.3.4')
125
+ end
126
+ end
127
+
128
+ it 'waits for the server to be ready' do
129
+ expect(driver).to receive(:wait_for_server).with(state)
130
+ driver.create(state)
131
+ end
132
+ end
133
+
134
+ describe '#destroy' do
135
+ let(:state) { { orchestration_id: 'orch1' } }
136
+ let(:orchestration) { double('orchestration') }
137
+
138
+ before do
139
+ allow(driver).to receive(:wait_for_status)
140
+ allow(driver).to receive(:orchestration).and_return(orchestration)
141
+ allow(orchestration).to receive(:name_with_container)
142
+ allow(orchestration).to receive(:stop)
143
+ allow(orchestration).to receive(:delete)
144
+ end
145
+
146
+ context 'when the orchestration is not in the state object' do
147
+ let(:state) { {} }
148
+ it 'does not attempt to delete the orchestration' do
149
+ expect(driver).not_to receive(:orchestration)
150
+ driver.destroy(state)
151
+ end
152
+ end
153
+
154
+ it 'looks up the orchestration by ID' do
155
+ expect(driver).to receive(:orchestration).with('orch1').and_return(orchestration)
156
+ driver.destroy(state)
157
+ end
158
+
159
+ it 'does not attempt to stop the orchestration if it cannot be found' do
160
+ allow(driver).to receive(:orchestration).with('orch1').and_raise(OracleCloud::Exception::HTTPNotFound)
161
+
162
+ expect(driver).to receive(:warn)
163
+ expect(orchestration).not_to receive(:stop)
164
+ driver.destroy(state)
165
+ end
166
+
167
+ it 'stops the orchestration' do
168
+ expect(orchestration).to receive(:stop)
169
+ driver.destroy(state)
170
+ end
171
+
172
+ it 'waits for the orchestration to stop' do
173
+ expect(driver).to receive(:wait_for_status).with(orchestration, 'stopped')
174
+ driver.destroy(state)
175
+ end
176
+
177
+ it 'deletes the orchestration' do
178
+ expect(orchestration).to receive(:delete)
179
+ driver.destroy(state)
180
+ end
181
+ end
182
+
183
+ describe '#oraclecloud_client' do
184
+ it 'returns an OracleCloud::Client instance' do
185
+ expect(driver.oraclecloud_client).to be_an_instance_of(OracleCloud::Client)
186
+ end
187
+ end
188
+
189
+ describe '#orchestration' do
190
+ let(:client) { double('oraclecloud_client') }
191
+ let(:orchestration) { double('orchestration') }
192
+ let(:orchestrations) { double('orchestrations') }
193
+
194
+ before do
195
+ allow(driver).to receive(:oraclecloud_client).and_return(client)
196
+ allow(client).to receive(:orchestrations).and_return(orchestrations)
197
+ end
198
+
199
+ context 'when an orchestration has already been created' do
200
+ it 'returns the existing orchestration' do
201
+ driver.instance_variable_set(:@orchestration, '123')
202
+ expect(driver.orchestration).to eq('123')
203
+ end
204
+ end
205
+
206
+ context 'when a name is not provided' do
207
+ it 'creates a new orchestration and returns it' do
208
+ allow(driver).to receive(:orchestration_name).and_return('test_orchestration')
209
+ allow(driver).to receive(:description).and_return('test_description')
210
+ allow(driver).to receive(:instance_request).and_return('test_instance')
211
+
212
+ expect(orchestrations).to receive(:create).with(name: 'test_orchestration',
213
+ description: 'test_description',
214
+ instances: [ 'test_instance' ])
215
+ .and_return(orchestration)
216
+ expect(driver.orchestration).to eq(orchestration)
217
+ end
218
+ end
219
+
220
+ context 'when a name is provided' do
221
+ it 'locates the orchestration and returns it' do
222
+ expect(orchestrations).to receive(:by_name).with('orch1').and_return(orchestration)
223
+ expect(driver.orchestration('orch1')).to eq(orchestration)
224
+ end
225
+ end
226
+ end
227
+
228
+ describe '#instance_request' do
229
+ let(:client) { double('oraclecloud_client') }
230
+ let(:config) { { shape: 'test_shape', image: 'test_image' } }
231
+ let(:instance_request) { double('instance_request') }
232
+ it 'creates an instance request and returns it' do
233
+ allow(driver).to receive(:oraclecloud_client).and_return(client)
234
+ allow(driver).to receive(:config).and_return(config)
235
+ allow(driver).to receive(:orchestration_name).and_return('test_name')
236
+ allow(driver).to receive(:sshkeys).and_return('test_keys')
237
+ allow(driver).to receive(:public_ip).and_return('test_ip')
238
+
239
+ expect(client).to receive(:instance_request).with(name: 'test_name',
240
+ shape: 'test_shape',
241
+ imagelist: 'test_image',
242
+ sshkeys: 'test_keys',
243
+ public_ip: 'test_ip')
244
+ .and_return(instance_request)
245
+ expect(driver.instance_request).to eq(instance_request)
246
+ end
247
+ end
248
+
249
+ describe '#server' do
250
+ let(:orchestration) { double('orchestration') }
251
+ let(:instances) { [ 'server1'] }
252
+ it 'returns the server' do
253
+ allow(driver).to receive(:orchestration).and_return(orchestration)
254
+
255
+ expect(orchestration).to receive(:instances).and_return(instances)
256
+ expect(driver.server).to eq('server1')
257
+ end
258
+ end
259
+
260
+ describe '#server_ip_address' do
261
+ let(:server) { double('server') }
262
+ it 'returns the private IP address when no public IPs are available' do
263
+ allow(driver).to receive(:server).and_return(server)
264
+ allow(server).to receive(:public_ip_addresses).and_return([])
265
+ allow(server).to receive(:ip_address).and_return('192.168.100.100')
266
+
267
+ expect(driver.server_ip_address).to eq('192.168.100.100')
268
+ end
269
+
270
+ it 'returns the public IP address if a public IP is available' do
271
+ allow(driver).to receive(:server).and_return(server)
272
+ allow(server).to receive(:public_ip_addresses).and_return([ '1.2.3.4' ])
273
+
274
+ expect(driver.server_ip_address).to eq('1.2.3.4')
275
+ end
276
+ end
277
+
278
+ describe '#wait_for_server' do
279
+ let(:connection) { instance.transport.connection(state) }
280
+ let(:state) { {} }
281
+
282
+ before do
283
+ allow(transport).to receive(:connection).and_return(connection)
284
+ allow(driver).to receive(:orchestration_name)
285
+ end
286
+
287
+ it 'waits for the server to be ready' do
288
+ expect(connection).to receive(:wait_until_ready)
289
+ driver.wait_for_server(state)
290
+ end
291
+
292
+ it 'destroys the server and raises an exception if it fails to become ready' do
293
+ allow(connection).to receive(:wait_until_ready).and_raise(Timeout::Error)
294
+ expect(driver).to receive(:destroy).with(state)
295
+ expect { driver.wait_for_server(state) }.to raise_error(Timeout::Error)
296
+ end
297
+ end
298
+
299
+ describe '#wait_for_status' do
300
+ let(:item) { double('item') }
301
+
302
+ before do
303
+ allow(driver).to receive(:wait_time).and_return(600)
304
+ allow(driver).to receive(:refresh_time).and_return(2)
305
+ allow(item).to receive(:error?)
306
+
307
+ # don't actually sleep
308
+ allow(driver).to receive(:sleep)
309
+ end
310
+
311
+ context 'when the items completes normally, 3 loops' do
312
+ it 'only refreshes the item 3 times' do
313
+ allow(item).to receive(:status).exactly(3).times.and_return('working', 'working', 'complete')
314
+ expect(item).to receive(:refresh).exactly(3).times
315
+
316
+ driver.wait_for_status(item, 'complete')
317
+ end
318
+ end
319
+
320
+ context 'when the item is completed on the first loop' do
321
+ it 'only refreshes the item 1 time' do
322
+ allow(item).to receive(:status).once.and_return('complete')
323
+ expect(item).to receive(:refresh).once
324
+
325
+ driver.wait_for_status(item, 'complete')
326
+ end
327
+ end
328
+
329
+ context 'when the timeout is exceeded' do
330
+ it 'prints a warning and exits' do
331
+ allow(Timeout).to receive(:timeout).and_raise(Timeout::Error)
332
+ expect(driver).to receive(:error)
333
+ .with('Request did not complete in 600 seconds. Check the Oracle Cloud Web UI for more information.')
334
+ expect { driver.wait_for_status(item, 'complete') }.to raise_error(Timeout::Error)
335
+ end
336
+ end
337
+
338
+ context 'when a non-timeout exception is raised' do
339
+ it 'raises the original exception' do
340
+ allow(item).to receive(:refresh).and_raise(RuntimeError)
341
+ expect { driver.wait_for_status(item, 'complete') }.to raise_error(RuntimeError)
342
+ end
343
+ end
344
+
345
+ context 'when the item errors out' do
346
+ it 'raises an exception' do
347
+ allow(item).to receive(:refresh)
348
+ allow(item).to receive(:status).and_return('error')
349
+ allow(item).to receive(:error?).and_return(true)
350
+ allow(item).to receive(:errors).and_return('test_error')
351
+
352
+ expect(driver).to receive(:error).with('Request encountered an error: test_error')
353
+ expect { driver.wait_for_status(item, 'complete') }.to raise_error(RuntimeError)
354
+ end
355
+ end
356
+ end
357
+
358
+ describe '#wait_time' do
359
+ it 'returns the correct wait time' do
360
+ allow(driver).to receive(:config).and_return(wait_time: 123)
361
+ expect(driver.wait_time).to eq(123)
362
+ end
363
+ end
364
+
365
+ describe '#refresh_time' do
366
+ it 'returns the correct refresh time' do
367
+ allow(driver).to receive(:config).and_return(refresh_time: 123)
368
+ expect(driver.refresh_time).to eq(123)
369
+ end
370
+ end
371
+
372
+ describe '#description' do
373
+ let(:instance) { double('instance', name: 'test_instance_name') }
374
+ it 'returns a default description if none is specified in the config' do
375
+ allow(driver).to receive(:config).and_return({})
376
+ allow(driver).to receive(:instance).and_return(instance)
377
+ allow(driver).to receive(:username).and_return('test_username')
378
+
379
+ expect(driver.description).to eq('test_instance_name for test_username via Test Kitchen')
380
+ end
381
+
382
+ it 'returns the configured description if one is provided' do
383
+ allow(driver).to receive(:config).and_return(description: 'test_description')
384
+ expect(driver.description).to eq('test_description')
385
+ end
386
+ end
387
+
388
+ describe '#username' do
389
+ it 'returns the correct username ' do
390
+ allow(driver).to receive(:config).and_return(username: 'test_username')
391
+ expect(driver.username).to eq('test_username')
392
+ end
393
+ end
394
+
395
+ describe '#orchestration_name' do
396
+ let(:instance) { double('instance', name: 'test instance') }
397
+ it 'returns a properly formatted name' do
398
+ allow(driver).to receive(:project_name).and_return('testproject')
399
+ allow(driver).to receive(:instance).and_return(instance)
400
+
401
+ expect(driver.orchestration_name).to eq('TK-testproject-testinstance')
402
+ end
403
+ end
404
+
405
+ describe '#project_name' do
406
+ it 'returns a UUID if a project name has not been configured' do
407
+ allow(driver).to receive(:config).and_return({})
408
+
409
+ expect(SecureRandom).to receive(:uuid).and_return('test_uuid')
410
+ expect(driver.project_name).to eq('test_uuid')
411
+ end
412
+
413
+ it 'returns the configured project name with spaces stripped' do
414
+ allow(driver).to receive(:config).and_return(project_name: 'my test project')
415
+
416
+ expect(driver.project_name).to eq('mytestproject')
417
+ end
418
+ end
419
+
420
+ describe '#sshkeys' do
421
+ let(:client) { double('oraclecloud_client') }
422
+ it 'returns an array of formatted keys' do
423
+ allow(driver).to receive(:config).and_return(sshkeys: %w(key1 key2))
424
+ allow(driver).to receive(:oraclecloud_client).and_return(client)
425
+ allow(client).to receive(:compute_identity_domain).and_return('test_domain')
426
+
427
+ expect(driver.sshkeys).to eq([ 'test_domain/key1', 'test_domain/key2' ])
428
+ end
429
+ end
430
+
431
+ describe '#public_ip' do
432
+ it 'returns nil if no public_ip is configured' do
433
+ allow(driver).to receive(:config).and_return({})
434
+ expect(driver.public_ip).to eq(nil)
435
+ end
436
+
437
+ it 'returns :pool if pool is configured' do
438
+ allow(driver).to receive(:config).and_return(public_ip: 'pool')
439
+ expect(driver.public_ip).to eq(:pool)
440
+ end
441
+
442
+ it 'returns a reservation name if a non-pool is configured' do
443
+ allow(driver).to receive(:config).and_return(public_ip: 'test_reservation')
444
+ expect(driver.public_ip).to eq('ipreservation:test_reservation')
445
+ end
446
+ end
447
+ end
@@ -0,0 +1,17 @@
1
+ #
2
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
3
+ # Copyright:: Copyright (c) 2015 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kitchen-oraclecloud
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Chef Partner Engineering
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: test-kitchen
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.4.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.4'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.4.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: oraclecloud
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.7'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.7'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '10.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.2'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.2'
89
+ description: A Test Kitchen driver for Oracle Cloud
90
+ email:
91
+ - partnereng@chef.io
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - ".gitignore"
97
+ - ".rubocop.yml"
98
+ - CHANGELOG.md
99
+ - Gemfile
100
+ - LICENSE.txt
101
+ - README.md
102
+ - Rakefile
103
+ - kitchen-oraclecloud.gemspec
104
+ - lib/kitchen/driver/oraclecloud.rb
105
+ - lib/kitchen/driver/oraclecloud_version.rb
106
+ - spec/oraclecloud_spec.rb
107
+ - spec/spec_helper.rb
108
+ homepage: https://github.com/chef-partners/kitchen-oraclecloud
109
+ licenses:
110
+ - Apache 2.0
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.4.8
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: A Test Kitchen driver for Oracle Cloud
132
+ test_files:
133
+ - spec/oraclecloud_spec.rb
134
+ - spec/spec_helper.rb
135
+ has_rdoc: