chef-metal-fog 0.1

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: 3ab481529a81ba71c5d9abb6432a399c4d6f1bb7
4
+ data.tar.gz: 727b503c48bd6064d37f9c512347ec6d16aa0111
5
+ SHA512:
6
+ metadata.gz: a1b422d9526c41aee107ffc58e6ebaecb471f2c3b69d21210473e9ca603fc62a5b6ac0476a632feca41d3b2ef5c55fd9138b5ab7364354c6c858077e0e83addf
7
+ data.tar.gz: 30195a484d57766d08ba97b31eff98437b7bc49cd016a50d4515cebdcce549c10fec2b7b199a7473c634fbb22bc1fafe0f006f9120b399df04a8e05486cb0bd8
data/LICENSE 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,3 @@
1
+ # chef-metal-fog
2
+
3
+ This is the Fog provisioner for chef-metal. It provides EC2, DigitalOcean and Openstack functionality.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler'
2
+ require 'bundler/gem_tasks'
3
+
4
+ task :spec do
5
+ require File.expand_path('spec/run')
6
+ end
@@ -0,0 +1,170 @@
1
+ require 'chef/provider/lwrp_base'
2
+ require 'chef_metal/provider_action_handler'
3
+ require 'chef_metal_fog/fog_provisioner'
4
+
5
+ class Chef::Provider::FogKeyPair < Chef::Provider::LWRPBase
6
+
7
+ include ChefMetal::ProviderActionHandler
8
+
9
+ use_inline_resources
10
+
11
+ def whyrun_supported?
12
+ true
13
+ end
14
+
15
+ action :create do
16
+ create_key
17
+ end
18
+
19
+ action :delete do
20
+ if current_resource_exists?
21
+ converge_by "delete #{key_description}" do
22
+ case new_resource.provisioner.compute_options[:provider]
23
+ when 'DigitalOcean'
24
+ compute.destroy_key_pair(@current_id)
25
+ when 'OpenStack'
26
+ compute.key_pairs.destroy(@current_id)
27
+ else
28
+ compute.key_pairs.delete(new_resource.name)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def key_description
35
+ "#{new_resource.name} on #{new_resource.provisioner.provisioner_url}"
36
+ end
37
+
38
+ def create_key
39
+ if current_resource_exists?
40
+ # If the public keys are different, update the server public key
41
+ if !current_resource.private_key_path
42
+ if new_resource.allow_overwrite
43
+ ensure_keys
44
+ else
45
+ raise "#{key_description} already exists on the server, but the private key #{new_resource.private_key_path} does not exist!"
46
+ end
47
+ else
48
+ ensure_keys
49
+ end
50
+
51
+ new_fingerprint = case new_resource.provisioner.compute_options[:provider]
52
+ when 'DigitalOcean'
53
+ Cheffish::KeyFormatter.encode(desired_key, :format => :openssh)
54
+ when 'OpenStack'
55
+ Cheffish::KeyFormatter.encode(desired_key, :format => :openssh)
56
+ else
57
+ Cheffish::KeyFormatter.encode(desired_key, :format => :fingerprint)
58
+ end
59
+
60
+ if new_fingerprint != @current_fingerprint
61
+ if new_resource.allow_overwrite
62
+ converge_by "update #{key_description} to match local key at #{new_resource.private_key_path}" do
63
+ case new_resource.provisioner.compute_options[:provider]
64
+ when 'DigitalOcean'
65
+ compute.create_ssh_key(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
66
+ when 'OpenStack'
67
+ compute.create_key_pair(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
68
+ else
69
+ compute.import_key_pair(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
70
+ end
71
+ end
72
+ else
73
+ raise "#{key_description} does not match local private key, and allow_overwrite is false!"
74
+ end
75
+ end
76
+ else
77
+ # Generate the private and/or public keys if they do not exist
78
+ ensure_keys
79
+
80
+ # Create key
81
+ converge_by "create #{key_description} from local key at #{new_resource.private_key_path}" do
82
+ case new_resource.provisioner.compute_options[:provider]
83
+ when 'DigitalOcean'
84
+ compute.create_ssh_key(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
85
+ when 'OpenStack'
86
+ compute.create_key_pair(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
87
+ else
88
+ compute.import_key_pair(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def ensure_keys
95
+ resource = new_resource
96
+ Cheffish.inline_resource(self) do
97
+ private_key resource.private_key_path do
98
+ public_key_path resource.public_key_path
99
+ if resource.private_key_options
100
+ resource.private_key_options.each_pair do |key,value|
101
+ send(key, value)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def desired_key
109
+ @desired_key ||= begin
110
+ if new_resource.public_key_path
111
+ public_key, format = Cheffish::KeyFormatter.decode(IO.read(new_resource.public_key_path))
112
+ public_key
113
+ else
114
+ private_key, format = Cheffish::KeyFormatter.decode(IO.read(new_resource.private_key_path))
115
+ private_key.public_key
116
+ end
117
+ end
118
+ end
119
+
120
+ def current_resource_exists?
121
+ @current_resource.action != [ :delete ]
122
+ end
123
+
124
+ def compute
125
+ new_resource.provisioner.compute
126
+ end
127
+
128
+ def current_public_key
129
+ current_resource.source_key
130
+ end
131
+
132
+ def load_current_resource
133
+ if !new_resource.provisioner.kind_of?(ChefMetalFog::FogProvisioner)
134
+ raise 'ec2_key_pair only works with fog_provisioner'
135
+ end
136
+ @current_resource = Chef::Resource::FogKeyPair.new(new_resource.name)
137
+ case new_resource.provisioner.compute_options[:provider]
138
+ when 'DigitalOcean'
139
+ current_key_pair = compute.ssh_keys.select { |key| key.name == new_resource.name }.first
140
+ if current_key_pair
141
+ @current_id = current_key_pair.id
142
+ @current_fingerprint = current_key_pair ? compute.ssh_keys.get(@current_id).ssh_pub_key : nil
143
+ else
144
+ current_resource.action :delete
145
+ end
146
+ when 'OpenStack'
147
+ current_key_pair = compute.key_pairs.get(new_resource.name)
148
+ if current_key_pair
149
+ @current_id = current_key_pair.name
150
+ @current_fingerprint = current_key_pair ? compute.key_pairs.get(@current_id).public_key : nil
151
+ else
152
+ current_resource.action :delete
153
+ end
154
+ else
155
+ current_key_pair = compute.key_pairs.get(new_resource.name)
156
+ if current_key_pair
157
+ @current_fingerprint = current_key_pair ? current_key_pair.fingerprint : nil
158
+ else
159
+ current_resource.action :delete
160
+ end
161
+ end
162
+
163
+ if new_resource.private_key_path && ::File.exist?(new_resource.private_key_path)
164
+ current_resource.private_key_path new_resource.private_key_path
165
+ end
166
+ if new_resource.public_key_path && ::File.exist?(new_resource.public_key_path)
167
+ current_resource.public_key_path new_resource.public_key_path
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,34 @@
1
+ require 'chef_metal'
2
+
3
+ class Chef::Resource::FogKeyPair < Chef::Resource::LWRPBase
4
+ self.resource_name = 'fog_key_pair'
5
+
6
+ def initialize(*args)
7
+ super
8
+ @provisioner = ChefMetal.enclosing_provisioner
9
+ end
10
+
11
+ def after_created
12
+ # Make the credentials usable
13
+ provisioner.key_pairs[name] = self
14
+ end
15
+
16
+ actions :create, :delete, :nothing
17
+ default_action :create
18
+
19
+ attribute :provisioner
20
+ # Private key to use as input (will be generated if it does not exist)
21
+ attribute :private_key_path, :kind_of => String
22
+ # Public key to use as input (will be generated if it does not exist)
23
+ attribute :public_key_path, :kind_of => String
24
+ # List of parameters to the private_key resource used for generation of the key
25
+ attribute :private_key_options, :kind_of => Hash
26
+
27
+ # TODO what is the right default for this?
28
+ attribute :allow_overwrite, :kind_of => [TrueClass, FalseClass], :default => false
29
+
30
+ # Proc that runs after the resource completes. Called with (resource, private_key, public_key)
31
+ def after(&block)
32
+ block ? @after = block : @after
33
+ end
34
+ end
@@ -0,0 +1,4 @@
1
+ require 'chef_metal/provisioner/fog_provisioner'
2
+
3
+ ChefMetal.add_registered_provisioner_class("fog",
4
+ ChefMetal::Provisioner::FogProvisioner)
@@ -0,0 +1,20 @@
1
+ require 'chef_metal'
2
+ require 'chef/resource/fog_key_pair'
3
+ require 'chef/provider/fog_key_pair'
4
+ require 'chef_metal_fog/fog_provisioner'
5
+
6
+ class Chef
7
+ class Recipe
8
+ def with_fog_provisioner(options = {}, &block)
9
+ ChefMetal.with_provisioner(ChefMetalFog::FogProvisioner.new(options), &block)
10
+ end
11
+
12
+ def with_fog_ec2_provisioner(options = {}, &block)
13
+ with_fog_provisioner({ :provider => 'AWS' }.merge(options), &block)
14
+ end
15
+
16
+ def with_fog_openstack_provisioner(options = {}, &block)
17
+ with_fog_provisioner({ :provider => 'OpenStack' }.merge(options), &block)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,546 @@
1
+ require 'chef_metal/provisioner'
2
+ require 'chef_metal/aws_credentials'
3
+ require 'chef_metal/openstack_credentials'
4
+ require 'chef_metal/version'
5
+ require 'chef_metal/machine/windows_machine'
6
+ require 'chef_metal/machine/unix_machine'
7
+ require 'chef_metal/convergence_strategy/install_msi'
8
+ require 'chef_metal/convergence_strategy/install_cached'
9
+ require 'chef_metal/transport/ssh'
10
+ require 'fog'
11
+ require 'fog/compute'
12
+
13
+ module ChefMetalFog
14
+ # Provisions machines in vagrant.
15
+ class FogProvisioner < ChefMetal::Provisioner
16
+
17
+ include Chef::Mixin::ShellOut
18
+
19
+ DEFAULT_OPTIONS = {
20
+ :create_timeout => 600,
21
+ :start_timeout => 600,
22
+ :ssh_timeout => 20
23
+ }
24
+
25
+ def self.inflate(node)
26
+ url = node['normal']['provisioner_output']['provisioner_url']
27
+ scheme, provider, id = url.split(':', 3)
28
+ FogProvisioner.new({ :provider => provider }, id)
29
+ end
30
+
31
+ # Create a new fog provisioner.
32
+ #
33
+ # ## Parameters
34
+ # compute_options - hash of options to be passed to Fog::Compute.new
35
+ # Special options:
36
+ # - :base_bootstrap_options is merged with bootstrap_options in acquire_machine
37
+ # to present the full set of bootstrap options. Write down any bootstrap_options
38
+ # you intend to apply universally here.
39
+ # - :aws_credentials is an AWS CSV file (created with Download Credentials)
40
+ # containing your aws key information. If you do not specify aws_access_key_id
41
+ # and aws_secret_access_key explicitly, the first line from this file
42
+ # will be used. You may pass a Cheffish::AWSCredentials object.
43
+ # - :create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
44
+ # - :start_timeout - the time to wait for the instance to start (defaults to 600)
45
+ # - :ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
46
+ # id - the ID in the provisioner_url (fog:PROVIDER:ID)
47
+ def initialize(compute_options, id=nil)
48
+ @compute_options = compute_options
49
+ @base_bootstrap_options = compute_options.delete(:base_bootstrap_options) || {}
50
+
51
+ case compute_options[:provider]
52
+ when 'AWS'
53
+ aws_credentials = compute_options.delete(:aws_credentials)
54
+ if aws_credentials
55
+ @aws_credentials = aws_credentials
56
+ else
57
+ @aws_credentials = ChefMetal::AWSCredentials.new
58
+ @aws_credentials.load_default
59
+ end
60
+ compute_options[:aws_access_key_id] ||= @aws_credentials.default[:access_key_id]
61
+ compute_options[:aws_secret_access_key] ||= @aws_credentials.default[:secret_access_key]
62
+ # TODO actually find a key with the proper id
63
+ # TODO let the user specify credentials and provider profiles that we can use
64
+ if id && aws_login_info[0] != id
65
+ raise "Default AWS credentials point at AWS account #{aws_login_info[0]}, but inflating from URL #{id}"
66
+ end
67
+ when 'OpenStack'
68
+ openstack_credentials = compute_options.delete(:openstack_credentials)
69
+ if openstack_credentials
70
+ @openstack_credentials = openstack_credentials
71
+ else
72
+ @openstack_credentials = ChefMetal::OpenstackCredentials.new
73
+ @openstack_credentials.load_default
74
+ end
75
+
76
+ compute_options[:openstack_username] ||= @openstack_credentials.default[:openstack_username]
77
+ compute_options[:openstack_api_key] ||= @openstack_credentials.default[:openstack_api_key]
78
+ compute_options[:openstack_auth_url] ||= @openstack_credentials.default[:openstack_auth_url]
79
+ compute_options[:openstack_tenant] ||= @openstack_credentials.default[:openstack_tenant]
80
+ end
81
+ @key_pairs = {}
82
+ @base_bootstrap_options_for = {}
83
+ end
84
+
85
+ attr_reader :compute_options
86
+ attr_reader :aws_credentials
87
+ attr_reader :openstack_credentials
88
+ attr_reader :key_pairs
89
+
90
+ def current_base_bootstrap_options
91
+ result = @base_bootstrap_options.dup
92
+ if key_pairs.size > 0
93
+ last_pair_name = key_pairs.keys.last
94
+ last_pair = key_pairs[last_pair_name]
95
+ result[:key_name] ||= last_pair_name
96
+ result[:private_key_path] ||= last_pair.private_key_path
97
+ result[:public_key_path] ||= last_pair.public_key_path
98
+ end
99
+ result
100
+ end
101
+
102
+ # Inflate a provisioner from node information; we don't want to force the
103
+ # driver to figure out what the provisioner really needs, since it varies
104
+ # from provisioner to provisioner.
105
+ #
106
+ # ## Parameters
107
+ # node - node to inflate the provisioner for
108
+ #
109
+ # returns a FogProvisioner
110
+ # TODO: def self.inflate(node)
111
+ # right now, not implemented, will raise error from base class until overridden
112
+
113
+ # Acquire a machine, generally by provisioning it. Returns a Machine
114
+ # object pointing at the machine, allowing useful actions like setup,
115
+ # converge, execute, file and directory. The Machine object will have a
116
+ # "node" property which must be saved to the server (if it is any
117
+ # different from the original node object).
118
+ #
119
+ # ## Parameters
120
+ # action_handler - the action_handler object that is calling this method; this
121
+ # is generally a action_handler, but could be anything that can support the
122
+ # ChefMetal::ActionHandler interface (i.e., in the case of the test
123
+ # kitchen metal driver for acquiring and destroying VMs; see the base
124
+ # class for what needs providing).
125
+ # node - node object (deserialized json) representing this machine. If
126
+ # the node has a provisioner_options hash in it, these will be used
127
+ # instead of options provided by the provisioner. TODO compare and
128
+ # fail if different?
129
+ # node will have node['normal']['provisioner_options'] in it with any options.
130
+ # It is a hash with this format:
131
+ #
132
+ # -- provisioner_url: fog:<relevant_fog_options>
133
+ # -- bootstrap_options: hash of options to pass to compute.servers.create
134
+ # -- is_windows: true if windows. TODO detect this from ami?
135
+ # -- create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
136
+ # -- start_timeout - the time to wait for the instance to start (defaults to 600)
137
+ # -- ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
138
+ #
139
+ # Example bootstrap_options for ec2:
140
+ # :image_id =>'ami-311f2b45',
141
+ # :flavor_id =>'t1.micro',
142
+ # :key_name => 'key-pair-name'
143
+ #
144
+ # node['normal']['provisioner_output'] will be populated with information
145
+ # about the created machine. For vagrant, it is a hash with this
146
+ # format:
147
+ #
148
+ # -- provisioner_url: fog:<relevant_fog_options>
149
+ # -- server_id: the ID of the server so it can be found again
150
+ #
151
+ def acquire_machine(action_handler, node)
152
+ # Set up the modified node data
153
+ provisioner_output = node['normal']['provisioner_output'] || {
154
+ 'provisioner_url' => provisioner_url,
155
+ 'provisioner_version' => ChefMetal::VERSION,
156
+ 'creator' => aws_login_info[1]
157
+ }
158
+
159
+ if provisioner_output['provisioner_url'] != provisioner_url
160
+ if (provisioner_output['provisioner_version'].to_f <= 0.3) && provisioner_output['provisioner_url'].start_with?('fog:AWS:') && compute_options[:provider] == 'AWS'
161
+ Chef::Log.warn "The upgrade from chef-metal 0.3 to 0.4 changed the provisioner URL format! Metal will assume you are in fact using the same AWS account, and modify the provisioner URL to match."
162
+ provisioner_output['provisioner_url'] = provisioner_url
163
+ provisioner_output['provisioner_version'] ||= ChefMetal::VERSION
164
+ provisioner_output['creator'] ||= aws_login_info[1]
165
+ else
166
+ raise "Switching providers for a machine is not currently supported! Use machine :destroy and then re-create the machine on the new action_handler."
167
+ end
168
+ end
169
+
170
+ node['normal']['provisioner_output'] = provisioner_output
171
+
172
+ if provisioner_output['server_id']
173
+
174
+ # If the server already exists, make sure it is up
175
+
176
+ # TODO verify that the server info matches the specification (ami, etc.)\
177
+ server = server_for(node)
178
+ if !server
179
+ Chef::Log.warn "Machine #{node['name']} (#{provisioner_output['server_id']} on #{provisioner_url}) is not associated with the ec2 account. Recreating ..."
180
+ need_to_create = true
181
+ elsif %w(terminated archive).include?(server.state) # Can't come back from that
182
+ Chef::Log.warn "Machine #{node['name']} (#{server.id} on #{provisioner_url}) is terminated. Recreating ..."
183
+ need_to_create = true
184
+ else
185
+ need_to_create = false
186
+ if !server.ready?
187
+ action_handler.perform_action "start machine #{node['name']} (#{server.id} on #{provisioner_url})" do
188
+ server.start
189
+ end
190
+ action_handler.perform_action "wait for machine #{node['name']} (#{server.id} on #{provisioner_url}) to be ready" do
191
+ wait_until_ready(server, option_for(node, :start_timeout))
192
+ end
193
+ else
194
+ wait_until_ready(server, option_for(node, :ssh_timeout))
195
+ end
196
+ end
197
+ else
198
+ need_to_create = true
199
+ end
200
+
201
+ if need_to_create
202
+ # If the server does not exist, create it
203
+ bootstrap_options = bootstrap_options_for(action_handler.new_resource, node)
204
+ bootstrap_options.merge(:name => action_handler.new_resource.name)
205
+
206
+ start_time = Time.now
207
+ timeout = option_for(node, :create_timeout)
208
+
209
+ description = [ "create machine #{node['name']} on #{provisioner_url}" ]
210
+ bootstrap_options.each_pair { |key,value| description << " #{key}: #{value.inspect}" }
211
+ server = nil
212
+ action_handler.perform_action description do
213
+ server = compute.servers.create(bootstrap_options)
214
+ provisioner_output['server_id'] = server.id
215
+ # Save quickly in case something goes wrong
216
+ save_node(action_handler, node, action_handler.new_resource.chef_server)
217
+ end
218
+
219
+ if server
220
+ @@ip_pool_lock = Mutex.new
221
+ # Re-retrieve the server in a more malleable form and wait for it to be ready
222
+ server = compute.servers.get(server.id)
223
+ if bootstrap_options[:floating_ip_pool]
224
+ Chef::Log.info 'Attaching IP from pool'
225
+ server.wait_for { ready? }
226
+ action_handler.perform_action "attach floating IP from #{bootstrap_options[:floating_ip_pool]} pool" do
227
+ attach_ip_from_pool(server, bootstrap_options[:floating_ip_pool])
228
+ end
229
+ elsif bootstrap_options[:floating_ip]
230
+ Chef::Log.info 'Attaching given IP'
231
+ server.wait_for { ready? }
232
+ action_handler.perform_action "attach floating IP #{bootstrap_options[:floating_ip]}" do
233
+ attach_ip(server, bootstrap_options[:floating_ip])
234
+ end
235
+ end
236
+ action_handler.perform_action "machine #{node['name']} created as #{server.id} on #{provisioner_url}" do
237
+ end
238
+ # Wait for the machine to come up and for ssh to start listening
239
+ transport = nil
240
+ _self = self
241
+ action_handler.perform_action "wait for machine #{node['name']} to boot" do
242
+ server.wait_for(timeout - (Time.now - start_time)) do
243
+ if ready?
244
+ transport ||= _self.transport_for(server)
245
+ begin
246
+ transport.execute('pwd')
247
+ true
248
+ rescue Errno::ECONNREFUSED, Net::SSH::Disconnect
249
+ false
250
+ rescue
251
+ true
252
+ end
253
+ else
254
+ false
255
+ end
256
+ end
257
+ end
258
+
259
+ # If there is some other error, we just wait patiently for SSH
260
+ begin
261
+ server.wait_for(option_for(node, :ssh_timeout)) { transport.available? }
262
+ rescue Fog::Errors::TimeoutError
263
+ # Sometimes (on EC2) the machine comes up but gets stuck or has
264
+ # some other problem. If this is the case, we restart the server
265
+ # to unstick it. Reboot covers a multitude of sins.
266
+ Chef::Log.warn "Machine #{node['name']} (#{server.id} on #{provisioner_url}) was started but SSH did not come up. Rebooting machine in an attempt to unstick it ..."
267
+ action_handler.perform_action "reboot machine #{node['name']} to try to unstick it" do
268
+ server.reboot
269
+ end
270
+ action_handler.perform_action "wait for machine #{node['name']} to be ready after reboot" do
271
+ wait_until_ready(server, option_for(node, :start_timeout))
272
+ end
273
+ end
274
+ end
275
+ end
276
+
277
+ # Create machine object for callers to use
278
+ machine_for(node, server)
279
+ end
280
+
281
+ # Attach IP to machine from IP pool
282
+ # Code taken from kitchen-openstack driver
283
+ # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L196-L207
284
+ def attach_ip_from_pool(server, pool)
285
+ @@ip_pool_lock.synchronize do
286
+ Chef::Log.info "Attaching floating IP from <#{pool}> pool"
287
+ free_addrs = compute.addresses.collect do |i|
288
+ i.ip if i.fixed_ip.nil? and i.instance_id.nil? and i.pool == pool
289
+ end.compact
290
+ if free_addrs.empty?
291
+ raise ActionFailed, "No available IPs in pool <#{pool}>"
292
+ end
293
+ attach_ip(server, free_addrs[0])
294
+ end
295
+ end
296
+
297
+ # Attach given IP to machine
298
+ # Code taken from kitchen-openstack driver
299
+ # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L209-L213
300
+ def attach_ip(server, ip)
301
+ Chef::Log.info "Attaching floating IP <#{ip}>"
302
+ server.associate_address ip
303
+ (server.addresses['public'] ||= []) << { 'version' => 4, 'addr' => ip }
304
+ end
305
+
306
+ # Connect to machine without acquiring it
307
+ def connect_to_machine(node)
308
+ machine_for(node)
309
+ end
310
+
311
+ def delete_machine(action_handler, node)
312
+ if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
313
+ server = compute.servers.get(node['normal']['provisioner_output']['server_id'])
314
+ action_handler.perform_action "destroy machine #{node['name']} (#{node['normal']['provisioner_output']['server_id']} at #{provisioner_url})" do
315
+ server.destroy
316
+ end
317
+ convergence_strategy_for(node).cleanup_convergence(action_handler, node)
318
+ end
319
+ end
320
+
321
+ def stop_machine(action_handler, node)
322
+ # If the machine doesn't exist, we silently do nothing
323
+ if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
324
+ server = compute.servers.get(node['normal']['provisioner_output']['server_id'])
325
+ action_handler.perform_action "stop machine #{node['name']} (#{server.id} at #{provisioner_url})" do
326
+ server.stop
327
+ end
328
+ end
329
+ end
330
+
331
+ def resource_created(machine)
332
+ @base_bootstrap_options_for[machine] = current_base_bootstrap_options
333
+ end
334
+
335
+
336
+ def compute
337
+ @compute ||= Fog::Compute.new(compute_options)
338
+ end
339
+
340
+ def provisioner_url
341
+ provider_identifier = case compute_options[:provider]
342
+ when 'AWS'
343
+ aws_login_info[0]
344
+ when 'DigitalOcean'
345
+ compute_options[:digitalocean_client_id]
346
+ when 'OpenStack'
347
+ compute_options[:openstack_auth_url]
348
+ else
349
+ '???'
350
+ end
351
+ "fog:#{compute_options[:provider]}:#{provider_identifier}"
352
+ end
353
+
354
+ # Not meant to be part of public interface
355
+ def transport_for(server)
356
+ # TODO winrm
357
+ create_ssh_transport(server)
358
+ end
359
+
360
+ protected
361
+
362
+ def option_for(node, key)
363
+ if node['normal']['provisioner_options'] && node['normal']['provisioner_options'][key.to_s]
364
+ node['normal']['provisioner_options'][key.to_s]
365
+ elsif compute_options[key]
366
+ compute_options[key]
367
+ else
368
+ DEFAULT_OPTIONS[key]
369
+ end
370
+ end
371
+
372
+ # Returns [ Account ID, User ]
373
+ # Account ID is the 12 digit identifier on your Manage Account page in AWS Console. It is used as part of all ARNs identifying resources.
374
+ # User is an identifier like "root" or "user/username" or "federated-user/username"
375
+ def aws_login_info
376
+ @aws_login_info ||= begin
377
+ iam = Fog::AWS::IAM.new(:aws_access_key_id => compute_options[:aws_access_key_id], :aws_secret_access_key => compute_options[:aws_secret_access_key])
378
+ arn = begin
379
+ # TODO it would be nice if Fog let you do this normally ...
380
+ iam.send(:request, {
381
+ 'Action' => 'GetUser',
382
+ :parser => Fog::Parsers::AWS::IAM::GetUser.new
383
+ }).body['User']['Arn']
384
+ rescue Fog::AWS::IAM::Error
385
+ # TODO Someone tell me there is a better way to find out your current
386
+ # user ID than this! This is what happens when you use an IAM user
387
+ # with default privileges.
388
+ if $!.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
389
+ arn = $1
390
+ else
391
+ raise
392
+ end
393
+ end
394
+ arn.split(':')[4..5]
395
+ end
396
+ end
397
+
398
+ def symbolize_keys(options)
399
+ options.inject({}) { |result,(key,value)| result[key.to_sym] = value; result }
400
+ end
401
+
402
+ def server_for(node)
403
+ if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
404
+ compute.servers.get(node['normal']['provisioner_output']['server_id'])
405
+ else
406
+ nil
407
+ end
408
+ end
409
+
410
+ def bootstrap_options_for(machine, node)
411
+ provisioner_options = node['normal']['provisioner_options'] || {}
412
+ bootstrap_options = @base_bootstrap_options_for[machine] || current_base_bootstrap_options
413
+ bootstrap_options = bootstrap_options.merge(symbolize_keys(provisioner_options['bootstrap_options'] || {}))
414
+ require 'socket'
415
+ require 'etc'
416
+ tags = {
417
+ 'Name' => node['name'],
418
+ 'BootstrapChefServer' => machine.chef_server[:chef_server_url],
419
+ 'BootstrapHost' => Socket.gethostname,
420
+ 'BootstrapUser' => Etc.getlogin,
421
+ 'BootstrapNodeName' => node['name']
422
+ }
423
+ if machine.chef_server[:options] && machine.chef_server[:options][:data_store]
424
+ tags['ChefLocalRepository'] = machine.chef_server[:options][:data_store].chef_fs.fs_description
425
+ end
426
+ # User-defined tags override the ones we set
427
+ tags.merge!(bootstrap_options[:tags]) if bootstrap_options[:tags]
428
+ bootstrap_options.merge!({ :tags => tags })
429
+
430
+ # Provide reasonable defaults for DigitalOcean
431
+ if compute_options[:provider] == 'DigitalOcean'
432
+ if !bootstrap_options[:image_id]
433
+ bootstrap_options[:image_name] ||= 'CentOS 6.4 x32'
434
+ bootstrap_options[:image_id] = compute.images.select { |image| image.name == bootstrap_options[:image_name] }.first.id
435
+ end
436
+ if !bootstrap_options[:flavor_id]
437
+ bootstrap_options[:flavor_name] ||= '512MB'
438
+ bootstrap_options[:flavor_id] = compute.flavors.select { |flavor| flavor.name == bootstrap_options[:flavor_name] }.first.id
439
+ end
440
+ if !bootstrap_options[:region_id]
441
+ bootstrap_options[:region_name] ||= 'San Francisco 1'
442
+ bootstrap_options[:region_id] = compute.regions.select { |region| region.name == bootstrap_options[:region_name] }.first.id
443
+ end
444
+ bootstrap_options[:ssh_key_ids] ||= [ compute.ssh_keys.select { |k| k.name == bootstrap_options[:key_name] }.first.id ]
445
+
446
+ # You don't get to specify name yourself
447
+ bootstrap_options[:name] = node['name']
448
+ end
449
+
450
+ bootstrap_options
451
+ end
452
+
453
+ def machine_for(node, server = nil)
454
+ server ||= server_for(node)
455
+ if !server
456
+ raise "Server for node #{node['name']} has not been created!"
457
+ end
458
+
459
+ if node['normal']['provisioner_options'] && node['normal']['provisioner_options']['is_windows']
460
+ ChefMetal::Machine::WindowsMachine.new(node, transport_for(server), convergence_strategy_for(node))
461
+ else
462
+ ChefMetal::Machine::UnixMachine.new(node, transport_for(server), convergence_strategy_for(node))
463
+ end
464
+ end
465
+
466
+ def convergence_strategy_for(node)
467
+ if node['normal']['provisioner_options'] && node['normal']['provisioner_options']['is_windows']
468
+ @windows_convergence_strategy ||= begin
469
+ ChefMetal::ConvergenceStrategy::InstallMsi.new
470
+ end
471
+ else
472
+ @unix_convergence_strategy ||= begin
473
+ ChefMetal::ConvergenceStrategy::InstallCached.new
474
+ end
475
+ end
476
+ end
477
+
478
+ def ssh_options_for(server)
479
+ result = {
480
+ # TODO create a user known hosts file
481
+ # :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
482
+ # :paranoid => true,
483
+ :auth_methods => [ 'publickey' ],
484
+ :keys_only => true,
485
+ :host_key_alias => "#{server.id}.#{compute_options[:provider]}"
486
+ }
487
+ if server.respond_to?(:private_key) && server.private_key
488
+ result[:keys] = [ server.private_key ]
489
+ elsif server.respond_to?(:key_name) && key_pairs[server.key_name]
490
+ # TODO generalize for others?
491
+ result[:keys] ||= [ key_pairs[server.key_name].private_key_path ]
492
+ else
493
+ # TODO need a way to know which key if there were multiple
494
+ result[:keys] = [ key_pairs.first[1].private_key_path ]
495
+ end
496
+ result
497
+ end
498
+
499
+ def create_ssh_transport(server)
500
+ ssh_options = ssh_options_for(server)
501
+ # If we're on AWS, the default is to use ubuntu, not root
502
+ if compute_options[:provider] == 'AWS'
503
+ username = compute_options[:ssh_username] || 'ubuntu'
504
+ else
505
+ username = compute_options[:ssh_username] || 'root'
506
+ end
507
+ options = {}
508
+ if compute_options[:sudo] || (!compute_options.has_key?(:sudo) && username != 'root')
509
+ options[:prefix] = 'sudo '
510
+ end
511
+
512
+ remote_host = nil
513
+ if compute_options[:use_private_ip_for_ssh]
514
+ remote_host = server.private_ip_address
515
+ elsif !server.public_ip_address
516
+ Chef::Log.warn("Server has no public ip address. Using private ip '#{server.private_ip_address}'. Set provisioner option 'use_private_ip_for_ssh' => true if this will always be the case ...")
517
+ remote_host = server.private_ip_address
518
+ elsif server.public_ip_address
519
+ remote_host = server.public_ip_address
520
+ else
521
+ raise "Server #{server.id} has no private or public IP address!"
522
+ end
523
+
524
+ #Enable pty by default
525
+ options[:ssh_pty_enable] = true
526
+
527
+ ChefMetal::Transport::SSH.new(remote_host, username, ssh_options, options)
528
+ end
529
+
530
+ def wait_until_ready(server, timeout)
531
+ transport = nil
532
+ _self = self
533
+ server.wait_for(timeout) do
534
+ if transport
535
+ transport.available?
536
+ elsif ready?
537
+ # Don't create the transport until the machine is ready (we won't have the host till then)
538
+ transport = _self.transport_for(server)
539
+ transport.available?
540
+ else
541
+ false
542
+ end
543
+ end
544
+ end
545
+ end
546
+ end
@@ -0,0 +1,3 @@
1
+ module ChefMetalFog
2
+ VERSION = '0.1'
3
+ end
data/lib/fog.rb ADDED
File without changes
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chef-metal-fog
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - John Keiser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: chef
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fog
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Provisioner for creating Fog instances in Chef Metal.
70
+ email: jkeiser@getchef.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - README.md
75
+ - LICENSE
76
+ files:
77
+ - Rakefile
78
+ - LICENSE
79
+ - README.md
80
+ - lib/chef/provider/fog_key_pair.rb
81
+ - lib/chef/resource/fog_key_pair.rb
82
+ - lib/chef_metal/provisioner_init/fog_init.rb
83
+ - lib/chef_metal_fog/fog_provisioner.rb
84
+ - lib/chef_metal_fog/version.rb
85
+ - lib/chef_metal_fog.rb
86
+ - lib/fog.rb
87
+ homepage: https://github.com/opscode/chef-metal-fog
88
+ licenses: []
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.0.3
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Provisioner for creating Fog instances in Chef Metal.
110
+ test_files: []
111
+ has_rdoc: