cult 0.1.1.pre → 0.1.2.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -28
  3. data/cult +1 -1
  4. data/cult.gemspec +4 -4
  5. data/doc/images/masthead@0.5x.png +0 -0
  6. data/exe/cult +2 -5
  7. data/lib/cult/artifact.rb +2 -1
  8. data/lib/cult/bundle.rb +26 -0
  9. data/lib/cult/cli/common.rb +2 -0
  10. data/lib/cult/cli/console_cmd.rb +11 -11
  11. data/lib/cult/cli/cri_extensions.rb +1 -2
  12. data/lib/cult/cli/fleet_cmd.rb +37 -0
  13. data/lib/cult/cli/init_cmd.rb +0 -2
  14. data/lib/cult/cli/node_cmd.rb +54 -22
  15. data/lib/cult/cli/task_cmd.rb +25 -9
  16. data/lib/cult/commander.rb +78 -52
  17. data/lib/cult/definition.rb +4 -7
  18. data/lib/cult/driver.rb +1 -1
  19. data/lib/cult/drivers/common.rb +8 -21
  20. data/lib/cult/drivers/digital_ocean_driver.rb +41 -48
  21. data/lib/cult/drivers/linode_driver.rb +12 -19
  22. data/lib/cult/drivers/load.rb +0 -3
  23. data/lib/cult/drivers/vultr_driver.rb +33 -44
  24. data/lib/cult/named_array.rb +62 -14
  25. data/lib/cult/node.rb +43 -8
  26. data/lib/cult/project.rb +0 -8
  27. data/lib/cult/project_context.rb +23 -0
  28. data/lib/cult/provider.rb +0 -3
  29. data/lib/cult/role.rb +30 -50
  30. data/lib/cult/singleton_instances.rb +43 -0
  31. data/lib/cult/skel.rb +2 -2
  32. data/lib/cult/task.rb +30 -8
  33. data/lib/cult/template.rb +18 -70
  34. data/lib/cult/transaction.rb +44 -0
  35. data/lib/cult/transferable.rb +2 -5
  36. data/lib/cult/user_refinements.rb +65 -0
  37. data/lib/cult/version.rb +1 -1
  38. data/lib/cult.rb +26 -0
  39. data/skel/roles/all/tasks/sync +24 -0
  40. data/skel/roles/bootstrap/files/cult-motd +1 -1
  41. metadata +19 -14
  42. data/lib/cult/config.rb +0 -22
  43. data/lib/cult/drivers/script_driver.rb +0 -27
  44. data/skel/keys/.keep +0 -0
@@ -2,33 +2,9 @@ require 'net/ssh'
2
2
  require 'net/scp'
3
3
  require 'shellwords'
4
4
  require 'rainbow'
5
- require 'rubygems/package'
6
- require 'rubygems/package/tar_writer'
5
+ require 'securerandom'
7
6
 
8
7
  module Cult
9
-
10
- class Bundle
11
- attr_reader :tar
12
- def initialize(io, &block)
13
- @tar = Gem::Package::TarWriter.new(io)
14
- if block_given?
15
- begin
16
- yield self
17
- ensure
18
- @tar.close
19
- @tar = nil
20
- end
21
- end
22
- end
23
-
24
- def add_file(project, role, node, transferable)
25
- data = transferable.contents(project, role, node, pwd: role.path)
26
- tar.add_file(transferable.remote_path, transferable.file_mode) do |io|
27
- io.write(data)
28
- end
29
- end
30
- end
31
-
32
8
  class Commander
33
9
  attr_reader :project
34
10
  attr_reader :node
@@ -42,59 +18,109 @@ module Cult
42
18
  Shellwords.escape(s)
43
19
  end
44
20
 
45
- def send_bundle(ssh, role)
21
+ def send_tar(io, ssh)
22
+ filename = SecureRandom.hex + ".tar"
23
+ puts "Uploading bundle: #{filename}"
24
+ scp = Net::SCP.new(ssh)
25
+ scp.upload!(io, filename)
26
+ ssh.exec! "tar -xf #{esc(filename)} && rm #{esc(filename)}"
27
+ end
28
+
29
+ def create_build_tar(role)
46
30
  io = StringIO.new
47
31
  Bundle.new(io) do |bundle|
48
32
  puts "Building bundle..."
49
33
  role.build_order.each do |r|
50
- (r.artifacts + r.tasks).each do |transferable|
34
+ (r.artifacts + r.build_tasks).each do |transferable|
51
35
  bundle.add_file(project, r, node, transferable)
52
36
  end
53
37
  end
54
38
  end
55
- filename = "cult-#{role.name}.tar"
56
- puts "Uploading bundle #{filename}..."
57
39
 
58
- scp = Net::SCP.new(ssh)
59
40
  io.rewind
60
- scp.upload!(io, filename)
61
- ssh.exec! "tar -xf #{esc(filename)} && rm #{esc(filename)}"
41
+ io
42
+ end
43
+
44
+ def exec_remote!(ssh:, role:, task:)
45
+ token = SecureRandom.hex
46
+ task_bin = role.relative_path(task.path)
47
+
48
+ puts "Executing: #{task.remote_path}"
49
+ res = ssh.exec! <<~BASH
50
+ cd #{esc(role.remote_path)}; \
51
+ ./#{esc(task_bin)} && \
52
+ echo #{esc(token)}
53
+ BASH
54
+
55
+ if res.chomp.end_with?(token)
56
+ res = res.gsub(token, '')
57
+ puts Rainbow(res.gsub(/^/, ' ')).darkgray.italic
58
+ true
59
+ else
60
+ puts Rainbow(res).red
61
+ puts "Failed"
62
+ false
63
+ end
62
64
  end
63
65
 
64
66
  def install!(role)
65
- connect(user: role.definition['user']) do |ssh|
66
- send_bundle(ssh, role)
67
+ connect(user: role.user) do |ssh|
68
+ io = create_build_tar(role)
69
+ send_tar(io, ssh)
67
70
 
68
71
  role.build_order.each do |r|
69
72
  puts "Installing role: #{Rainbow(r.name).blue}"
70
- working_dir = r.remote_path
71
- r.tasks.each do |t|
72
- puts "Executing: #{t.remote_path}"
73
- task_bin = r.relative_path(t.path)
74
- res = ssh.exec! <<~BASH
75
- cd #{esc(working_dir)}; \
76
- if [ ! -f ./#{esc(task_bin)}.success ]; then \
77
- touch ./#{esc(task_bin)}.attempt && \
78
- ./#{esc(task_bin)} && \
79
- mv ./#{esc(task_bin)}.attempt ./#{esc(task_bin)}.success; \
80
- fi
81
- BASH
82
- unless res.empty?
83
- puts Rainbow(res.gsub(/^/, ' ')).darkgray.italic
84
- end
73
+ r.build_tasks.each do |task|
74
+ exec_remote!(ssh: ssh, role: r, task: task)
85
75
  end
86
76
  end
87
77
  end
88
78
  end
89
79
 
80
+ def find_sync_tasks
81
+ r = []
82
+ node.build_order.each do |role|
83
+ role.event_tasks.each do |task|
84
+ r << task if task.name == 'sync'
85
+ end
86
+ end
87
+ r
88
+ end
89
+
90
+ def create_sync_tar
91
+ io = StringIO.new
92
+ Bundle.new(io) do |bundle|
93
+ find_sync_tasks.each do |task|
94
+ bundle.add_file(project, task.role, node, task)
95
+ end
96
+ end
97
+
98
+ io.rewind
99
+ io
100
+ end
101
+
102
+ def sync!
103
+ connect do |ssh|
104
+ io = create_sync_tar
105
+ send_tar(io, ssh)
106
+ find_sync_tasks.each do |task|
107
+ exec_remote!(ssh: ssh, role: task.role, task: task)
108
+ end
109
+ end
110
+ end
111
+
90
112
  def bootstrap!
91
113
  bootstrap_role = CLI.fetch_item('bootstrap', from: Role)
92
114
  install!(bootstrap_role)
93
115
  end
94
116
 
95
- def connect(user:, &block)
96
- puts "Connecting with user=#{user}"
97
- Net::SSH.start(node.host, user) do |ssh|
117
+ def connect(user: nil, &block)
118
+ user ||= node.user
119
+ puts "Connecting with user=#{user}, key=#{node.ssh_private_key_file}"
120
+ Net::SSH.start(node.host,
121
+ user,
122
+ keys_only: true,
123
+ keys: [node.ssh_private_key_file]) do |ssh|
98
124
  yield ssh
99
125
  end
100
126
  end
@@ -2,23 +2,20 @@ require 'yaml'
2
2
  require 'json'
3
3
  require 'forwardable'
4
4
 
5
- require 'cult/template'
6
-
7
5
  module Cult
8
6
  class Definition
9
7
  attr_reader :object
10
8
  attr_reader :bag
11
9
 
12
10
  extend Forwardable
13
- def_delegators :object, :definition_parameters, :definition_path,
11
+ def_delegators :object, :definition_parameters,
12
+ :definition_path,
14
13
  :definition_parents
15
14
 
16
-
17
15
  def initialize(object)
18
16
  @object = object
19
17
  end
20
18
 
21
-
22
19
  def inspect
23
20
  "\#<#{self.class.name} " +
24
21
  "object: #{object.inspect}, " +
@@ -63,8 +60,8 @@ module Cult
63
60
  @bag ||= begin
64
61
  result = {}
65
62
  filenames.each do |filename|
66
- erb = Template.new(definition_parameters)
67
- contents = erb.process(File.read(filename))
63
+ erb = ::Cult::Template.new(project: nil, **definition_parameters)
64
+ contents = erb.process(File.read(filename), filename: filename)
68
65
  result.merge! decoder_for(filename).call(contents)
69
66
  end
70
67
  result
data/lib/cult/driver.rb CHANGED
@@ -1,8 +1,8 @@
1
- require 'cult/definition'
2
1
  require 'cult/drivers/common'
3
2
 
4
3
  module Cult
5
4
  class Driver
5
+ include ::Cult::Drivers::Common
6
6
 
7
7
  # This is raised when a Driver is instantiated, but the required
8
8
  # gems are not installed.
@@ -1,10 +1,12 @@
1
1
  require 'socket'
2
2
  require 'net/ssh'
3
3
 
4
+ require 'cult/transaction'
5
+
4
6
  module Cult
5
7
  module Drivers
6
-
7
8
  module Common
9
+
8
10
  module ClassMethods
9
11
  # Lets us write a method "something_map" that returns {'ident' => ...},
10
12
  # and also get a function "something" that returns the keys.
@@ -37,11 +39,6 @@ module Cult
37
39
  end
38
40
 
39
41
 
40
- def self.included(cls)
41
- cls.extend(ClassMethods)
42
- end
43
-
44
-
45
42
  # works with with_id_mapping to convert a human-readible/normalized key
46
43
  # to the id the backend service expects. Allows '=value' to force a
47
44
  # literal value, and gives better error messages.
@@ -81,21 +78,6 @@ module Cult
81
78
  end
82
79
 
83
80
 
84
- # Enter this block once a node has been created. It makes sure it's
85
- # destroyed if there's an error later in the procedure.
86
- def rollback_on_error(id:, &block)
87
- begin
88
- yield
89
- rescue Exception => e
90
- begin
91
- destroy!(id: id)
92
- ensure
93
- raise e
94
- end
95
- end
96
- end
97
-
98
-
99
81
  def slugify(s)
100
82
  s.gsub(/[^a-z0-9]+/i, '-').gsub(/(^\-)|(-\z)/, '').downcase
101
83
  end
@@ -187,6 +169,11 @@ module Cult
187
169
  end
188
170
  end
189
171
 
172
+ def self.included(cls)
173
+ cls.extend(ClassMethods)
174
+ cls.include(::Cult::Transaction)
175
+ end
176
+
190
177
  end
191
178
  end
192
179
  end
@@ -1,6 +1,3 @@
1
- require 'cult/driver'
2
- require 'cult/drivers/common'
3
- require 'net/ssh'
4
1
  require 'json'
5
2
 
6
3
  module Cult
@@ -9,8 +6,6 @@ module Cult
9
6
  class DigitalOceanDriver < ::Cult::Driver
10
7
  self.required_gems = 'droplet_kit'
11
8
 
12
- include Common
13
-
14
9
  attr_reader :client
15
10
 
16
11
  def initialize(api_key:)
@@ -49,24 +44,13 @@ module Cult
49
44
  with_id_mapping :zones_map
50
45
 
51
46
 
52
- def ssh_keys
53
- client.ssh_keys.all.to_a.map(&:to_h)
54
- end
55
- memoize :ssh_keys
56
-
57
47
 
58
48
  def upload_ssh_key(file:)
59
49
  key = ssh_key_info(file: file)
60
50
  # If we already have one with this fingerprint, use it.
61
- if (exist = ssh_keys.find {|dk| dk[:fingerprint] == key[:fingerprint]})
62
- exist
63
- else
64
- ssh_keys_dememo!
65
- client.ssh_keys.create \
66
- DropletKit::SSHKey.new(fingerprint: key[:fingerprint],
67
- public_key: key[:data],
68
- name: key[:name])
69
- end
51
+ dk_key = DropletKit::SSHKey.new(public_key: key[:data],
52
+ name: "Cult: #{key[:name]}")
53
+ client.ssh_keys.create(dk_key).id
70
54
  end
71
55
 
72
56
 
@@ -80,39 +64,48 @@ module Cult
80
64
  end
81
65
 
82
66
 
83
- def destroy!(id:)
67
+ def destroy!(id:, ssh_key_id: nil)
84
68
  client.droplets.delete(id: id)
69
+ destroy_ssh_key!(ssh_key_id: ssh_key_id) if ssh_key_id
85
70
  end
86
71
 
72
+ def destroy_ssh_key!(ssh_key_id:)
73
+ client.ssh_keys.delete(id: ssh_key_id)
74
+ end
87
75
 
88
- def provision!(name:, size:, zone:, image:, ssh_key_files:)
89
- fingerprints = Array(ssh_key_files).map do |file|
90
- upload_ssh_key(file: file)[:fingerprint]
91
- end
92
-
93
- begin
94
- params = {
95
- name: name,
96
- size: fetch_mapped(name: :size, from: sizes_map, key: size),
97
- image: fetch_mapped(name: :image, from: images_map, key: image),
98
- region: fetch_mapped(name: :zone, from: zones_map, key: zone),
99
- ssh_keys: fingerprints,
100
-
101
- private_networking: true,
102
- ipv6: true
103
- }
104
- rescue KeyError => e
105
- fail ArgumentError, "Invalid argument: #{e.message}"
106
- end
107
-
108
- droplet = DropletKit::Droplet.new(params)
76
+ def provision!(name:, size:, zone:, image:, ssh_public_key:)
77
+ transaction do |xac|
78
+ ssh_key_id = upload_ssh_key(file: ssh_public_key)
79
+ xac.rollback do
80
+ destroy_ssh_key!(id: ssh_key_id)
81
+ end
82
+
83
+ begin
84
+ params = {
85
+ name: name,
86
+ size: fetch_mapped(name: :size, from: sizes_map, key: size),
87
+ image: fetch_mapped(name: :image, from: images_map, key: image),
88
+ region: fetch_mapped(name: :zone, from: zones_map, key: zone),
89
+ ssh_keys: [ssh_key_id],
90
+
91
+ private_networking: true,
92
+ ipv6: true
93
+ }
94
+ rescue KeyError => e
95
+ fail ArgumentError, "Invalid argument: #{e.message}"
96
+ end
97
+
98
+ droplet = DropletKit::Droplet.new(params)
99
+
100
+ if droplet.nil?
101
+ fail "Droplet was nil: #{params.inspect}"
102
+ end
109
103
 
110
- if droplet.nil?
111
- fail "Droplet was nil: #{params.inspect}"
112
- end
113
-
114
- rollback_on_error(id: droplet.id) do
115
104
  droplet = client.droplets.create(droplet)
105
+ xac.rollback do
106
+ destroy!(id: droplet.id)
107
+ end
108
+
116
109
  droplet = await_creation(droplet)
117
110
 
118
111
  ipv4_public = droplet.networks.v4.find {|n| n.type == 'public' }
@@ -126,8 +119,8 @@ module Cult
126
119
  size: size,
127
120
  zone: zone,
128
121
  image: image,
129
- ssh_key_files: ssh_key_files,
130
- ssh_keys: fingerprints,
122
+
123
+ ssh_key_id: ssh_key_id,
131
124
 
132
125
  id: droplet.id,
133
126
  created_at: droplet.created_at,
@@ -1,6 +1,3 @@
1
- require 'cult/driver'
2
- require 'cult/cli/common'
3
-
4
1
  require 'securerandom'
5
2
  require 'time'
6
3
 
@@ -48,8 +45,6 @@ module Cult
48
45
  class LinodeDriver < ::Cult::Driver
49
46
  self.required_gems = 'linode'
50
47
 
51
- include Common
52
-
53
48
  SWAP_SIZE = 256
54
49
 
55
50
  attr_reader :client
@@ -124,30 +119,29 @@ module Cult
124
119
  end
125
120
 
126
121
 
127
- def destroy!(id:)
122
+ def destroy!(id:, ssh_key_id: [])
128
123
  client.linode.delete(linodeid: id, skipchecks: true)
129
124
  end
130
125
 
131
126
 
132
- def provision!(name:, size:, zone:, image:, ssh_key_files:)
127
+ def provision!(name:, size:, zone:, image:, ssh_public_key:)
133
128
  sizeid = fetch_mapped(name: :size, from: sizes_map, key: size)
134
129
  imageid = fetch_mapped(name: :image, from: images_map, key: image)
135
130
  zoneid = fetch_mapped(name: :zone, from: zones_map, key: zone)
136
131
  disksize = disk_size_for_size(size)
137
132
 
138
- linodeid = client.linode.create(datacenterid: zoneid,
139
- planid: sizeid).linodeid
133
+ transaction do |xac|
134
+ linodeid = client.linode.create(datacenterid: zoneid,
135
+ planid: sizeid).linodeid
136
+ xac.rollback do
137
+ destroy!(id: linodeid)
138
+ end
140
139
 
141
- rollback_on_error(id: linodeid) do
142
140
  # We give it a name early so we can find it in the Web UI if anything
143
141
  # goes wrong.
144
142
  client.linode.update(linodeid: linodeid, label: name)
145
143
  client.linode.ip.addprivate(linodeid: linodeid)
146
144
 
147
- ssh_keys = Array(ssh_key_files).map do |file|
148
- ssh_key_info(file: file)
149
- end
150
-
151
145
  # You shouldn't run meaningful swap, but this makes the Web UI not
152
146
  # scare you, and apparently Linux runs better with ANY swap,
153
147
  # regardless of how small. We've matched the small size the Linode
@@ -165,15 +159,15 @@ module Cult
165
159
  # Linode's max length is 128, generates longer than that to
166
160
  # no get the fixed == and truncates.
167
161
  rootpass: SecureRandom.base64(100)[0...128],
168
- rootsshkey: ssh_keys.map {|k| k[:data] }.join("\n"),
162
+ rootsshkey: ssh_key_info(file: ssh_public_key)[:data],
169
163
  size: disksize - SWAP_SIZE
170
164
  }
171
165
 
172
166
  diskid = client.linode.disk.createfromdistribution(params).diskid
173
167
 
174
168
 
175
- # We don't have to reference the config specifically: It'll be the only
176
- # configuration that exists, so it'll be used.
169
+ # We don't have to reference the config specifically: It'll be the
170
+ # only configuration that exists, so it'll be used.
177
171
  client.linode.config.create(linodeid: linodeid,
178
172
  kernelid: latest_kernel_id,
179
173
  disklist: "#{diskid},#{swapid}",
@@ -200,8 +194,6 @@ module Cult
200
194
  size: size,
201
195
  zone: zone,
202
196
  image: image,
203
- ssh_key_files: ssh_keys.map{|k| k[:file]},
204
- ssh_keys: ssh_keys.map{|k| k[:fingerprint]},
205
197
 
206
198
  id: linodeid,
207
199
  created_at: Time.now.iso8601,
@@ -213,6 +205,7 @@ module Cult
213
205
  meta: {}
214
206
  }
215
207
  end
208
+
216
209
  end
217
210
 
218
211
 
@@ -1,6 +1,3 @@
1
- require 'cult/driver'
2
- require 'cult/named_array'
3
-
4
1
  module Cult
5
2
  module Drivers
6
3
 
@@ -1,6 +1,3 @@
1
- require 'cult/driver'
2
- require 'cult/drivers/common'
3
-
4
1
  require 'net/ssh'
5
2
  require 'time'
6
3
 
@@ -9,8 +6,6 @@ module Cult
9
6
  class VultrDriver < ::Cult::Driver
10
7
  self.required_gems = 'vultr'
11
8
 
12
- include Common
13
-
14
9
  attr_reader :api_key
15
10
 
16
11
  def initialize(api_key:)
@@ -90,26 +85,11 @@ module Cult
90
85
  with_api_key :sizes_map
91
86
 
92
87
 
93
- def ssh_keys
94
- Vultr::SSHKey.list[:result].values
95
- end
96
- memoize :ssh_keys
97
- with_api_key :ssh_keys
98
-
99
88
 
100
89
  def upload_ssh_key(file:)
101
90
  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
91
+ Vultr::SSHKey.create(name: "Cult: #{key[:name]}",
92
+ ssh_key: key[:data])[:result]["SSHKEYID"]
113
93
  end
114
94
  with_api_key :upload_ssh_key
115
95
 
@@ -121,34 +101,43 @@ module Cult
121
101
  end
122
102
 
123
103
 
124
- def destroy!(id:)
104
+ def destroy!(id:, ssh_key_id: nil)
125
105
  Vultr::Server.destroy(SUBID: id)
106
+ destroy_ssh_key!(ssh_key_id: ssh_key_id) if ssh_key_id
126
107
  end
127
108
  with_api_key :destroy!
128
109
 
110
+ def destroy_ssh_key!(ssh_key_id:)
111
+ Vultr::SSHKey.destroy(SSHKEYID: ssh_key_id)
112
+ end
113
+ with_api_key :destroy_ssh_key!
129
114
 
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
115
 
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(','))
116
+ def provision!(name:, size:, zone:, image:, ssh_public_key:)
117
+ transaction do |xac|
118
+ ssh_key_id = upload_ssh_key(file: ssh_public_key)
119
+ xac.rollback do
120
+ destroy_ssh_key!(ssh_key_id: ssh_key_id)
121
+ end
148
122
 
149
- subid = r[:result]["SUBID"]
123
+ sizeid = fetch_mapped(name: :size, from: sizes_map, key: size)
124
+ imageid = fetch_mapped(name: :image, from: images_map, key: image)
125
+ zoneid = fetch_mapped(name: :zone, from: zones_map, key: zone)
126
+
127
+ r = Vultr::Server.create(DCID: zoneid,
128
+ OSID: imageid,
129
+ VPSPLANID: sizeid,
130
+ enable_ipv6: 'yes',
131
+ enable_private_network: 'yes',
132
+ label: name,
133
+ hostname: name,
134
+ SSHKEYID: ssh_key_id)
135
+
136
+ subid = r[:result]["SUBID"]
137
+ xac.rollback do
138
+ destroy!(id: subid)
139
+ end
150
140
 
151
- rollback_on_error(id: subid) do
152
141
  # Wait until it's active, it won't have an IP until then
153
142
  backoff_loop do
154
143
  r = Vultr::Server.list(SUBID: subid)[:result]
@@ -166,8 +155,8 @@ module Cult
166
155
  size: size,
167
156
  zone: zone,
168
157
  image: image,
169
- ssh_key_files: ssh_key_files,
170
- ssh_keys: keys.map{|v| v["fingerprint"]},
158
+
159
+ ssh_key_id: ssh_key_id,
171
160
 
172
161
  id: subid,
173
162
  created_at: Time.now.iso8601,