cult 0.1.1.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +240 -0
- data/Rakefile +6 -0
- data/cult +1 -0
- data/cult.gemspec +38 -0
- data/doc/welcome.txt +1 -0
- data/exe/cult +86 -0
- data/lib/cult/artifact.rb +45 -0
- data/lib/cult/cli/common.rb +265 -0
- data/lib/cult/cli/console_cmd.rb +124 -0
- data/lib/cult/cli/cri_extensions.rb +84 -0
- data/lib/cult/cli/init_cmd.rb +116 -0
- data/lib/cult/cli/load.rb +26 -0
- data/lib/cult/cli/node_cmd.rb +205 -0
- data/lib/cult/cli/provider_cmd.rb +123 -0
- data/lib/cult/cli/role_cmd.rb +149 -0
- data/lib/cult/cli/task_cmd.rb +140 -0
- data/lib/cult/commander.rb +103 -0
- data/lib/cult/config.rb +22 -0
- data/lib/cult/definition.rb +112 -0
- data/lib/cult/driver.rb +88 -0
- data/lib/cult/drivers/common.rb +192 -0
- data/lib/cult/drivers/digital_ocean_driver.rb +179 -0
- data/lib/cult/drivers/linode_driver.rb +282 -0
- data/lib/cult/drivers/load.rb +26 -0
- data/lib/cult/drivers/script_driver.rb +27 -0
- data/lib/cult/drivers/vultr_driver.rb +217 -0
- data/lib/cult/named_array.rb +129 -0
- data/lib/cult/node.rb +62 -0
- data/lib/cult/project.rb +169 -0
- data/lib/cult/provider.rb +134 -0
- data/lib/cult/role.rb +213 -0
- data/lib/cult/skel.rb +85 -0
- data/lib/cult/task.rb +64 -0
- data/lib/cult/template.rb +92 -0
- data/lib/cult/transferable.rb +61 -0
- data/lib/cult/version.rb +3 -0
- data/lib/cult.rb +4 -0
- data/skel/.cultconsolerc +4 -0
- data/skel/.cultrc.erb +29 -0
- data/skel/README.md.erb +22 -0
- data/skel/keys/.keep +0 -0
- data/skel/nodes/.keep +0 -0
- data/skel/providers/.keep +0 -0
- data/skel/roles/all/role.json +4 -0
- data/skel/roles/all/tasks/00000-do-something-cool +27 -0
- data/skel/roles/bootstrap/files/cult-motd +45 -0
- data/skel/roles/bootstrap/role.json +4 -0
- data/skel/roles/bootstrap/tasks/00000-set-hostname +22 -0
- data/skel/roles/bootstrap/tasks/00001-add-cult-user +21 -0
- data/skel/roles/bootstrap/tasks/00002-install-cult-motd +9 -0
- metadata +183 -0
@@ -0,0 +1,282 @@
|
|
1
|
+
require 'cult/driver'
|
2
|
+
require 'cult/cli/common'
|
3
|
+
|
4
|
+
require 'securerandom'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module Cult
|
8
|
+
# This has been submitted as a PR. It lets us set a label and custom
|
9
|
+
# expiration length for an API key.
|
10
|
+
# See: https://github.com/rick/linode/pull/34
|
11
|
+
module LinodeMonkeyPatch
|
12
|
+
def fetch_api_key(options = {})
|
13
|
+
request = {
|
14
|
+
api_action: 'user.getapikey',
|
15
|
+
api_responseFormat: 'json',
|
16
|
+
username: username,
|
17
|
+
password: password
|
18
|
+
}
|
19
|
+
|
20
|
+
if options.key?(:label)
|
21
|
+
request[:label] = options[:label]
|
22
|
+
end
|
23
|
+
|
24
|
+
if options.key?(:expires)
|
25
|
+
expires = options[:expires]
|
26
|
+
request[:expires] = expires.nil? ? 0 : expires
|
27
|
+
end
|
28
|
+
|
29
|
+
response = post(request)
|
30
|
+
if error?(response)
|
31
|
+
fail "Errors completing request [user.getapikey] @ [#{api_url}] for " +
|
32
|
+
"username [#{username}]:\n" +
|
33
|
+
"#{error_message(response, 'user.getapikey')}"
|
34
|
+
end
|
35
|
+
reformat_response(response).api_key
|
36
|
+
end
|
37
|
+
public :fetch_api_key
|
38
|
+
|
39
|
+
module_function
|
40
|
+
def install!
|
41
|
+
::Linode.prepend(self)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module Cult
|
47
|
+
module Drivers
|
48
|
+
class LinodeDriver < ::Cult::Driver
|
49
|
+
self.required_gems = 'linode'
|
50
|
+
|
51
|
+
include Common
|
52
|
+
|
53
|
+
SWAP_SIZE = 256
|
54
|
+
|
55
|
+
attr_reader :client
|
56
|
+
|
57
|
+
def initialize(api_key:)
|
58
|
+
LinodeMonkeyPatch.install!
|
59
|
+
@client = Linode.new(api_key: api_key)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def images_map
|
64
|
+
client.avail.distributions.select(&:is64bit).map do |v|
|
65
|
+
name = v.label
|
66
|
+
[ slugify(distro_name(v.label)), v.distributionid ]
|
67
|
+
end.to_h
|
68
|
+
end
|
69
|
+
memoize :images_map
|
70
|
+
with_id_mapping :images_map
|
71
|
+
|
72
|
+
|
73
|
+
def zones_map
|
74
|
+
client.avail.datacenters.map do |v|
|
75
|
+
[ slugify(v.abbr), v.datacenterid ]
|
76
|
+
end.to_h
|
77
|
+
end
|
78
|
+
memoize :zones_map
|
79
|
+
with_id_mapping :zones_map
|
80
|
+
|
81
|
+
|
82
|
+
def sizes_map
|
83
|
+
client.avail.linodeplans.map do |v|
|
84
|
+
name = v.label.gsub(/^Linode /, '')
|
85
|
+
if name.match(/^\d+$/)
|
86
|
+
mb = name.to_i
|
87
|
+
if mb < 1024
|
88
|
+
"#{mb}mb"
|
89
|
+
else
|
90
|
+
name = "#{mb / 1024}gb"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
[ slugify(name), v.planid ]
|
94
|
+
end.to_h
|
95
|
+
end
|
96
|
+
memoize :sizes_map
|
97
|
+
with_id_mapping :sizes_map
|
98
|
+
|
99
|
+
|
100
|
+
# We try to use the reasonable sizes that the web UI uses, although the
|
101
|
+
# API lets us change it.
|
102
|
+
def disk_size_for_size(size)
|
103
|
+
gb = 1024
|
104
|
+
{
|
105
|
+
'2gb' => 24 * gb,
|
106
|
+
'4gb' => 48 * gb,
|
107
|
+
'8gb' => 96 * gb,
|
108
|
+
'12gb' => 192 * gb,
|
109
|
+
'24gb' => 384 * gb,
|
110
|
+
'48gb' => 768 * gb,
|
111
|
+
'64gb' => 1152 * gb,
|
112
|
+
'80gb' => 1536 * gb,
|
113
|
+
'120gb' => 1920 * gb
|
114
|
+
}.fetch(size.to_s)
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# I've been told by Linode support that this literal will always mean
|
119
|
+
# "Latest x86". But in case that changes...
|
120
|
+
def latest_kernel_id
|
121
|
+
@latest_kernel_id ||= 138 || begin
|
122
|
+
client.avail.kernels.find {|k| k.label.match(/^latest 64 bit/i)}
|
123
|
+
end.kernelid
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
def destroy!(id:)
|
128
|
+
client.linode.delete(linodeid: id, skipchecks: true)
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
def provision!(name:, size:, zone:, image:, ssh_key_files:)
|
133
|
+
sizeid = fetch_mapped(name: :size, from: sizes_map, key: size)
|
134
|
+
imageid = fetch_mapped(name: :image, from: images_map, key: image)
|
135
|
+
zoneid = fetch_mapped(name: :zone, from: zones_map, key: zone)
|
136
|
+
disksize = disk_size_for_size(size)
|
137
|
+
|
138
|
+
linodeid = client.linode.create(datacenterid: zoneid,
|
139
|
+
planid: sizeid).linodeid
|
140
|
+
|
141
|
+
rollback_on_error(id: linodeid) do
|
142
|
+
# We give it a name early so we can find it in the Web UI if anything
|
143
|
+
# goes wrong.
|
144
|
+
client.linode.update(linodeid: linodeid, label: name)
|
145
|
+
client.linode.ip.addprivate(linodeid: linodeid)
|
146
|
+
|
147
|
+
ssh_keys = Array(ssh_key_files).map do |file|
|
148
|
+
ssh_key_info(file: file)
|
149
|
+
end
|
150
|
+
|
151
|
+
# You shouldn't run meaningful swap, but this makes the Web UI not
|
152
|
+
# scare you, and apparently Linux runs better with ANY swap,
|
153
|
+
# regardless of how small. We've matched the small size the Linode
|
154
|
+
# Web UI does by default.
|
155
|
+
swapid = client.linode.disk.create(linodeid: linodeid,
|
156
|
+
label: "Cult: #{name}-swap",
|
157
|
+
type: "swap",
|
158
|
+
size: SWAP_SIZE).diskid
|
159
|
+
|
160
|
+
# Here, we create the OS on-node storage
|
161
|
+
params = {
|
162
|
+
linodeid: linodeid,
|
163
|
+
distributionid: imageid,
|
164
|
+
label: "Cult: #{name}",
|
165
|
+
# Linode's max length is 128, generates longer than that to
|
166
|
+
# no get the fixed == and truncates.
|
167
|
+
rootpass: SecureRandom.base64(100)[0...128],
|
168
|
+
rootsshkey: ssh_keys.map {|k| k[:data] }.join("\n"),
|
169
|
+
size: disksize - SWAP_SIZE
|
170
|
+
}
|
171
|
+
|
172
|
+
diskid = client.linode.disk.createfromdistribution(params).diskid
|
173
|
+
|
174
|
+
|
175
|
+
# We don't have to reference the config specifically: It'll be the only
|
176
|
+
# configuration that exists, so it'll be used.
|
177
|
+
client.linode.config.create(linodeid: linodeid,
|
178
|
+
kernelid: latest_kernel_id,
|
179
|
+
disklist: "#{diskid},#{swapid}",
|
180
|
+
rootdevicenum: 1,
|
181
|
+
label: "Cult: Latest Linux-x64")
|
182
|
+
|
183
|
+
client.linode.reboot(linodeid: linodeid)
|
184
|
+
|
185
|
+
# Information gathering step...
|
186
|
+
all_ips = client.linode.ip.list(linodeid: linodeid)
|
187
|
+
|
188
|
+
ipv4_public = all_ips.find{ |ip| ip.ispublic == 1 }&.ipaddress
|
189
|
+
ipv4_private = all_ips.find{ |ip| ip.ispublic == 0 }&.ipaddress
|
190
|
+
|
191
|
+
# This is a shame: Linode has awesome support for ipv6, but doesn't
|
192
|
+
# expose it in the API.
|
193
|
+
ipv6_public = nil
|
194
|
+
ipv6_private = nil
|
195
|
+
|
196
|
+
await_ssh(ipv4_public)
|
197
|
+
|
198
|
+
return {
|
199
|
+
name: name,
|
200
|
+
size: size,
|
201
|
+
zone: zone,
|
202
|
+
image: image,
|
203
|
+
ssh_key_files: ssh_keys.map{|k| k[:file]},
|
204
|
+
ssh_keys: ssh_keys.map{|k| k[:fingerprint]},
|
205
|
+
|
206
|
+
id: linodeid,
|
207
|
+
created_at: Time.now.iso8601,
|
208
|
+
host: ipv4_public,
|
209
|
+
ipv4_public: ipv4_public,
|
210
|
+
ipv4_private: ipv4_private,
|
211
|
+
ipv6_public: ipv6_public,
|
212
|
+
ipv6_private: ipv6_private,
|
213
|
+
meta: {}
|
214
|
+
}
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
def self.interrupts
|
220
|
+
# I hate IRB.
|
221
|
+
[Interrupt] + (defined?(IRB) ? [IRB::Abort] : [])
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
def self.setup!
|
226
|
+
super
|
227
|
+
LinodeMonkeyPatch.install!
|
228
|
+
|
229
|
+
linode = nil
|
230
|
+
api_key = nil
|
231
|
+
|
232
|
+
begin
|
233
|
+
loop do
|
234
|
+
puts "Cult needs an API key. It can get one for you, but will " +
|
235
|
+
"need your Linode username and password. If you'd rather "
|
236
|
+
"generate it at Linode, hit ctrl-c"
|
237
|
+
username = CLI.ask "Username"
|
238
|
+
password = CLI.password "Password"
|
239
|
+
linode = Linode.new(username: username, password: password)
|
240
|
+
begin
|
241
|
+
linode.fetch_api_key(label: "Cult", expires: nil)
|
242
|
+
api_key = linode.api_key
|
243
|
+
fail RuntimeError if api_key.nil?
|
244
|
+
puts "Got it! In case you're curious: #{api_key}"
|
245
|
+
rescue RuntimeError
|
246
|
+
puts "Linode disagreed with your password."
|
247
|
+
next if CLI.yes_no?("Try again?")
|
248
|
+
end
|
249
|
+
break
|
250
|
+
end
|
251
|
+
rescue *interrupts
|
252
|
+
puts
|
253
|
+
url = "https://manager.linode.com/profile/api"
|
254
|
+
puts "You can obtain an API key for Cult at the following URL:"
|
255
|
+
puts " #{url}"
|
256
|
+
puts
|
257
|
+
CLI.launch_browser(url) if CLI.yes_no?("Open Browser?")
|
258
|
+
api_key = CLI.prompt("API Key")
|
259
|
+
end
|
260
|
+
|
261
|
+
linode ||= Linode.new(api_key: api_key)
|
262
|
+
resp = linode.test.echo(message: "PING")
|
263
|
+
if resp.message != 'PING'
|
264
|
+
raise "Didn't respond to ping. Something went wrong."
|
265
|
+
end
|
266
|
+
|
267
|
+
inst = new(api_key: api_key)
|
268
|
+
|
269
|
+
return {
|
270
|
+
api_key: api_key,
|
271
|
+
driver: driver_name,
|
272
|
+
configurations: {
|
273
|
+
sizes: inst.sizes,
|
274
|
+
zones: inst.zones,
|
275
|
+
images: inst.images,
|
276
|
+
}
|
277
|
+
}
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'cult/driver'
|
2
|
+
require 'cult/named_array'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
module Drivers
|
6
|
+
|
7
|
+
module_function
|
8
|
+
def load!
|
9
|
+
Dir.glob(File.join(__dir__, "*_driver.rb")).each do |file|
|
10
|
+
require file
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def all
|
16
|
+
Cult::Drivers.constants(false).map do |m|
|
17
|
+
Cult::Drivers.const_get(m)
|
18
|
+
end.select do |cls|
|
19
|
+
::Cult::Driver > cls
|
20
|
+
end.to_named_array
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Cult::Drivers.load!
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'cult/driver'
|
2
|
+
require 'cult/drivers/common'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
module Drivers
|
6
|
+
|
7
|
+
class ScriptDriver < ::Cult::Driver
|
8
|
+
include Common
|
9
|
+
|
10
|
+
def initialize(api_key:)
|
11
|
+
fail NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def provision!(name:, size:, zone:, image:, ssh_key_files:, extra: {})
|
16
|
+
fail NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def self.setup!
|
21
|
+
super
|
22
|
+
fail NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'cult/driver'
|
2
|
+
require 'cult/drivers/common'
|
3
|
+
|
4
|
+
require 'net/ssh'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module Cult
|
8
|
+
module Drivers
|
9
|
+
class VultrDriver < ::Cult::Driver
|
10
|
+
self.required_gems = 'vultr'
|
11
|
+
|
12
|
+
include Common
|
13
|
+
|
14
|
+
attr_reader :api_key
|
15
|
+
|
16
|
+
def initialize(api_key:)
|
17
|
+
@api_key = api_key
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# This sets the Vultr API key to this instance's api key for the duration
|
22
|
+
# of a method call and restores it afterwards.
|
23
|
+
def self.with_api_key(method_name)
|
24
|
+
unwrapped_name = "#{method_name}_no_api_key".to_sym
|
25
|
+
alias_method unwrapped_name, method_name
|
26
|
+
define_method(method_name) do |*args, &block|
|
27
|
+
old_api_key = Vultr.api_key
|
28
|
+
begin
|
29
|
+
Vultr.api_key = self.api_key
|
30
|
+
return send(unwrapped_name, *args, &block)
|
31
|
+
ensure
|
32
|
+
Vultr.api_key = old_api_key
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def zones_map
|
39
|
+
Vultr::Region.list[:result].map do |k, v|
|
40
|
+
[slugify(v["regioncode"]), v["DCID"]]
|
41
|
+
end.to_h
|
42
|
+
end
|
43
|
+
memoize :zones_map
|
44
|
+
with_id_mapping :zones_map
|
45
|
+
with_api_key :zones_map
|
46
|
+
|
47
|
+
|
48
|
+
def images_map
|
49
|
+
Vultr::OS.list[:result].select do |k, v|
|
50
|
+
# Doing our part to kill x86/32
|
51
|
+
v['arch'] == 'x64'
|
52
|
+
end.map do |k,v|
|
53
|
+
[slugify(distro_name(v["name"])), v["OSID"]]
|
54
|
+
end.reject do |k,v|
|
55
|
+
%w(custom snapshot backup application).include?(k) ||
|
56
|
+
k.match(/^windows/)
|
57
|
+
end.to_h
|
58
|
+
end
|
59
|
+
memoize :images_map
|
60
|
+
with_id_mapping :images_map
|
61
|
+
with_api_key :images_map
|
62
|
+
|
63
|
+
|
64
|
+
def sizes_map
|
65
|
+
Vultr::Plan.list[:result].values.select do |v|
|
66
|
+
v["plan_type"] == 'SSD'
|
67
|
+
end.map do |v|
|
68
|
+
if (m = v["name"].match(/^(\d+) ([MGTP]B) RAM/i))
|
69
|
+
_, ram, unit = *m
|
70
|
+
ram = ram.to_i
|
71
|
+
|
72
|
+
if unit == "MB" && ram >= 1024
|
73
|
+
ram = ram / 1024
|
74
|
+
unit = "GB"
|
75
|
+
end
|
76
|
+
|
77
|
+
if unit == "GB" && ram >= 1024
|
78
|
+
ram = ram / 1024
|
79
|
+
unit = "TB"
|
80
|
+
end
|
81
|
+
|
82
|
+
["#{ram}#{unit}".downcase, v["VPSPLANID"] ]
|
83
|
+
else
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end.compact.to_h
|
87
|
+
end
|
88
|
+
memoize :sizes_map
|
89
|
+
with_id_mapping :sizes_map
|
90
|
+
with_api_key :sizes_map
|
91
|
+
|
92
|
+
|
93
|
+
def ssh_keys
|
94
|
+
Vultr::SSHKey.list[:result].values
|
95
|
+
end
|
96
|
+
memoize :ssh_keys
|
97
|
+
with_api_key :ssh_keys
|
98
|
+
|
99
|
+
|
100
|
+
def upload_ssh_key(file:)
|
101
|
+
key = ssh_key_info(file: file)
|
102
|
+
|
103
|
+
vkey = if (exist = ssh_keys.find {|e| e["ssh_key"] == key[:data] })
|
104
|
+
exist
|
105
|
+
else
|
106
|
+
ssh_keys_dememo!
|
107
|
+
Vultr::SSHKey.create(name: "Cult: #{key[:name]}",
|
108
|
+
ssh_key: key[:data])[:result]
|
109
|
+
end
|
110
|
+
|
111
|
+
vkey["fingerprint"] = key[:fingerprint]
|
112
|
+
vkey
|
113
|
+
end
|
114
|
+
with_api_key :upload_ssh_key
|
115
|
+
|
116
|
+
|
117
|
+
def fetch_ip(list, type)
|
118
|
+
goal = (type == :public ? "main_ip" : "private")
|
119
|
+
r = list.find{ |v| v["type"] == goal }
|
120
|
+
r.nil? ? nil : r["ip"]
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
def destroy!(id:)
|
125
|
+
Vultr::Server.destroy(SUBID: id)
|
126
|
+
end
|
127
|
+
with_api_key :destroy!
|
128
|
+
|
129
|
+
|
130
|
+
def provision!(name:, size:, zone:, image:, ssh_key_files:)
|
131
|
+
keys = Array(ssh_key_files).map do |filename|
|
132
|
+
upload_ssh_key(file: filename)
|
133
|
+
end
|
134
|
+
|
135
|
+
sizeid = fetch_mapped(name: :size, from: sizes_map, key: size)
|
136
|
+
imageid = fetch_mapped(name: :image, from: images_map, key: image)
|
137
|
+
zoneid = fetch_mapped(name: :zone, from: zones_map, key: zone)
|
138
|
+
|
139
|
+
r = Vultr::Server.create(DCID: zoneid,
|
140
|
+
OSID: imageid,
|
141
|
+
VPSPLANID: sizeid,
|
142
|
+
enable_ipv6: 'yes',
|
143
|
+
enable_private_network: 'yes',
|
144
|
+
label: name,
|
145
|
+
hostname: name,
|
146
|
+
SSHKEYID: keys.map{|v| v["SSHKEYID"] }
|
147
|
+
.join(','))
|
148
|
+
|
149
|
+
subid = r[:result]["SUBID"]
|
150
|
+
|
151
|
+
rollback_on_error(id: subid) do
|
152
|
+
# Wait until it's active, it won't have an IP until then
|
153
|
+
backoff_loop do
|
154
|
+
r = Vultr::Server.list(SUBID: subid)[:result]
|
155
|
+
throw :done if r['status'] == 'active'
|
156
|
+
end
|
157
|
+
|
158
|
+
iplist4 = Vultr::Server.list_ipv4(SUBID: subid)[:result].values[0]
|
159
|
+
iplist6 = Vultr::Server.list_ipv6(SUBID: subid)[:result].values[0]
|
160
|
+
|
161
|
+
host = fetch_ip(iplist4, :public)
|
162
|
+
await_ssh(host)
|
163
|
+
|
164
|
+
return {
|
165
|
+
name: name,
|
166
|
+
size: size,
|
167
|
+
zone: zone,
|
168
|
+
image: image,
|
169
|
+
ssh_key_files: ssh_key_files,
|
170
|
+
ssh_keys: keys.map{|v| v["fingerprint"]},
|
171
|
+
|
172
|
+
id: subid,
|
173
|
+
created_at: Time.now.iso8601,
|
174
|
+
host: host,
|
175
|
+
ipv4_public: host,
|
176
|
+
ipv4_private: fetch_ip(iplist4, :private),
|
177
|
+
ipv6_public: fetch_ip(iplist6, :public),
|
178
|
+
ipv6_private: fetch_ip(iplist6, :private),
|
179
|
+
meta: {}
|
180
|
+
}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
with_api_key :provision!
|
184
|
+
|
185
|
+
|
186
|
+
def self.setup!
|
187
|
+
super
|
188
|
+
url = "https://my.vultr.com/settings/#settingsapi"
|
189
|
+
puts "Vultr does not generate multiple API keys, so you'll need to "
|
190
|
+
"create one (if it does not exist). You can access your API key "
|
191
|
+
"at the following URL:"
|
192
|
+
puts
|
193
|
+
puts " #{url}"
|
194
|
+
puts
|
195
|
+
|
196
|
+
CLI.launch_browser(url) if CLI.yes_no?("Launch browser?")
|
197
|
+
|
198
|
+
api_key = CLI.prompt("API Key")
|
199
|
+
|
200
|
+
unless api_key.match(/^[A-Z2-7]{36}$/)
|
201
|
+
puts "That doesn't look like an API key, but I'll trust you"
|
202
|
+
end
|
203
|
+
|
204
|
+
inst = new(api_key: api_key)
|
205
|
+
return {
|
206
|
+
api_key: api_key,
|
207
|
+
driver: driver_name,
|
208
|
+
configurations: {
|
209
|
+
sizes: inst.sizes,
|
210
|
+
zones: inst.zones,
|
211
|
+
images: inst.images,
|
212
|
+
}
|
213
|
+
}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# A lot of times, we want a sequential array of objects, but it'd still be
|
2
|
+
# really convenient to refer to things by their name. This is particularly
|
3
|
+
# painful in the console, where, e.g., nodes can only be referred to by index,
|
4
|
+
# and you end up calling `find` a lot.
|
5
|
+
#
|
6
|
+
# NamedArray is an array, but overloads [] to also work with a String, Symbol,
|
7
|
+
# Regexp, or a few other things. e.g., nodes[:something]. It works by finding
|
8
|
+
# the first item who responds from `named_array_identifier` with the matching
|
9
|
+
# key.
|
10
|
+
#
|
11
|
+
# By default named_array_identifier returns name, but this can be overridden.
|
12
|
+
|
13
|
+
module Cult
|
14
|
+
class NamedArray < Array
|
15
|
+
# Any Array can convert itself to a NamedArray
|
16
|
+
module ArrayExtensions
|
17
|
+
def to_named_array
|
18
|
+
NamedArray.new(self)
|
19
|
+
end
|
20
|
+
::Array.include(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# This maps #named_array_identifier to #name by default
|
25
|
+
module ObjectExtensions
|
26
|
+
def named_array_identifier
|
27
|
+
name
|
28
|
+
end
|
29
|
+
::Object.include(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def to_named_array
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Wrap any non-mutating methods that can return an Array,
|
39
|
+
# and wrap the result with a NamedArray. This is why NamedArray.select
|
40
|
+
# results in a NamedArray instead of an Array
|
41
|
+
PROXY_METHODS = %i(& * + - << | collect compact flatten reject reverse
|
42
|
+
rotate select shuffle slice sort uniq)
|
43
|
+
PROXY_METHODS.each do |method_name|
|
44
|
+
define_method(method_name) do |*args, &b|
|
45
|
+
r = super(*args, &b)
|
46
|
+
r.respond_to?(:to_named_array) ? r.to_named_array : r
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# It's unforunate that there's not a Regexp constructor that'll
|
52
|
+
# accept this string format with options.
|
53
|
+
def build_regexp_from_string(s)
|
54
|
+
fail RegexpError, "Isn't a Regexp: #{s}" if s[0] != '/'
|
55
|
+
options = extract_regexp_options(s)
|
56
|
+
Regexp.new(s[1 ... s.rindex('/')], options)
|
57
|
+
end
|
58
|
+
private :build_regexp_from_string
|
59
|
+
|
60
|
+
|
61
|
+
def extract_regexp_options(s)
|
62
|
+
offset = s.rindex('/')
|
63
|
+
fail RegexpError, "Unterminated Regexp: #{s}" if offset == 0
|
64
|
+
|
65
|
+
trailing = s[offset + 1 ... s.size]
|
66
|
+
re_string = "%r!!#{trailing}"
|
67
|
+
begin
|
68
|
+
(eval re_string).options
|
69
|
+
rescue SyntaxError => e
|
70
|
+
fail RegexpError, "invalid Regexp options: #{trailing}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
private :extract_regexp_options
|
74
|
+
|
75
|
+
|
76
|
+
# Returns all keys that match if method == :select, the first if
|
77
|
+
# method == :find
|
78
|
+
def all(key, method = :select)
|
79
|
+
key = case key
|
80
|
+
when Integer
|
81
|
+
# Fallback to default behavior
|
82
|
+
return super
|
83
|
+
when String
|
84
|
+
key[0] == '/' ? build_regexp_from_string(key) : key
|
85
|
+
when Regexp, Proc, Range
|
86
|
+
key
|
87
|
+
when Symbol
|
88
|
+
key.to_s
|
89
|
+
when NilClass
|
90
|
+
return nil
|
91
|
+
else
|
92
|
+
fail KeyError, "#{key} did not resolve to an object"
|
93
|
+
end
|
94
|
+
|
95
|
+
send(method) do |v|
|
96
|
+
key === v.named_array_identifier
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
# first matching item
|
102
|
+
def [](key)
|
103
|
+
return super if key.is_a?(Integer)
|
104
|
+
all(key, :find)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
# first matching item, or raises KeyError
|
109
|
+
def fetch(key)
|
110
|
+
all(key, :find) or raise KeyError
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def key?(key)
|
115
|
+
!! all(key, :find)
|
116
|
+
end
|
117
|
+
alias_method :exist?, :key?
|
118
|
+
|
119
|
+
|
120
|
+
def keys
|
121
|
+
map(&:named_array_identifier)
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def values
|
126
|
+
self
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|