chef-metal-fog 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +3 -0
- data/Rakefile +6 -0
- data/lib/chef/provider/fog_key_pair.rb +170 -0
- data/lib/chef/resource/fog_key_pair.rb +34 -0
- data/lib/chef_metal/provisioner_init/fog_init.rb +4 -0
- data/lib/chef_metal_fog.rb +20 -0
- data/lib/chef_metal_fog/fog_provisioner.rb +546 -0
- data/lib/chef_metal_fog/version.rb +3 -0
- data/lib/fog.rb +0 -0
- metadata +111 -0
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
data/Rakefile
ADDED
@@ -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,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
|
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:
|