vagrant-eryph 0.1.0
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/LICENSE +21 -0
- data/README.md +79 -0
- data/lib/vagrant-eryph/actions/connect_eryph.rb +40 -0
- data/lib/vagrant-eryph/actions/create_catlet.rb +88 -0
- data/lib/vagrant-eryph/actions/destroy_catlet.rb +33 -0
- data/lib/vagrant-eryph/actions/is_created.rb +34 -0
- data/lib/vagrant-eryph/actions/is_stopped.rb +20 -0
- data/lib/vagrant-eryph/actions/message_already_created.rb +18 -0
- data/lib/vagrant-eryph/actions/message_not_created.rb +18 -0
- data/lib/vagrant-eryph/actions/message_will_not_destroy.rb +18 -0
- data/lib/vagrant-eryph/actions/prepare_cloud_init.rb +52 -0
- data/lib/vagrant-eryph/actions/read_ssh_info.rb +20 -0
- data/lib/vagrant-eryph/actions/read_state.rb +52 -0
- data/lib/vagrant-eryph/actions/start_catlet.rb +40 -0
- data/lib/vagrant-eryph/actions/stop_catlet.rb +40 -0
- data/lib/vagrant-eryph/actions.rb +210 -0
- data/lib/vagrant-eryph/command.rb +573 -0
- data/lib/vagrant-eryph/config.rb +553 -0
- data/lib/vagrant-eryph/errors.rb +61 -0
- data/lib/vagrant-eryph/helpers/cloud_init.rb +113 -0
- data/lib/vagrant-eryph/helpers/eryph_client.rb +413 -0
- data/lib/vagrant-eryph/plugin.rb +28 -0
- data/lib/vagrant-eryph/provider.rb +142 -0
- data/lib/vagrant-eryph/version.rb +7 -0
- data/lib/vagrant-eryph.rb +8 -0
- data/vagrant-eryph.gemspec +27 -0
- metadata +165 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'digest'
|
5
|
+
|
6
|
+
module VagrantPlugins
|
7
|
+
module Eryph
|
8
|
+
module Helpers
|
9
|
+
class CloudInit
|
10
|
+
VAGRANT_PUBLIC_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant'
|
11
|
+
|
12
|
+
def initialize(machine)
|
13
|
+
@machine = machine
|
14
|
+
@config = machine.provider_config
|
15
|
+
@ui = machine.ui
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generate cloud-init fodder for Vagrant user setup
|
19
|
+
def generate_vagrant_user_fodder
|
20
|
+
return [] unless @config.auto_config
|
21
|
+
|
22
|
+
if detect_os_type == :windows
|
23
|
+
generate_windows_user_fodder
|
24
|
+
else
|
25
|
+
generate_linux_user_fodder
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Merge auto-generated fodder with user-provided fodder
|
30
|
+
def merge_fodder_with_user_config(auto_fodder)
|
31
|
+
@config.merged_fodder(auto_fodder)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generate complete fodder configuration including user config
|
35
|
+
def generate_complete_fodder
|
36
|
+
auto_fodder = generate_vagrant_user_fodder
|
37
|
+
|
38
|
+
# Add Vagrant cloud-init configuration if present
|
39
|
+
cloud_init_fodder = @config.extract_vagrant_cloud_init_config(@machine)
|
40
|
+
auto_fodder.concat(cloud_init_fodder) if cloud_init_fodder.any?
|
41
|
+
|
42
|
+
merge_fodder_with_user_config(auto_fodder)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Detect OS type using Vagrant's guest detection
|
46
|
+
def detect_os_type
|
47
|
+
guest = @machine.config.vm.guest
|
48
|
+
guest if %i[windows linux].include?(guest)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get effective password - resolves :auto based on OS type
|
52
|
+
def effective_password
|
53
|
+
return @config.vagrant_password unless @config.vagrant_password == :auto
|
54
|
+
|
55
|
+
guest = @machine.config.vm.guest
|
56
|
+
if guest == :windows
|
57
|
+
'InitialPassw0rd' # Eryph Windows default
|
58
|
+
else
|
59
|
+
'vagrant' # Standard Vagrant for Linux
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def generate_linux_user_fodder
|
66
|
+
cloud_config = {
|
67
|
+
'users' => [
|
68
|
+
{
|
69
|
+
'name' => 'vagrant',
|
70
|
+
'sudo' => ['ALL=(ALL) NOPASSWD:ALL'],
|
71
|
+
'shell' => '/bin/bash',
|
72
|
+
'groups' => ['adm'],
|
73
|
+
'lock_passwd' => false,
|
74
|
+
'plain_text_passwd' => effective_password,
|
75
|
+
'ssh_authorized_keys' => [VAGRANT_PUBLIC_KEY]
|
76
|
+
}
|
77
|
+
]
|
78
|
+
}
|
79
|
+
|
80
|
+
[
|
81
|
+
{
|
82
|
+
name: 'vagrant-user-setup',
|
83
|
+
type: 'cloud-config',
|
84
|
+
content: cloud_config
|
85
|
+
}
|
86
|
+
]
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_windows_user_fodder
|
90
|
+
# Always create vagrant user with config password
|
91
|
+
cloud_config = {
|
92
|
+
'users' => [
|
93
|
+
{
|
94
|
+
'name' => 'vagrant',
|
95
|
+
'groups' => ['Administrators'],
|
96
|
+
'passwd' => effective_password,
|
97
|
+
'ssh_authorized_keys' => [VAGRANT_PUBLIC_KEY]
|
98
|
+
}
|
99
|
+
]
|
100
|
+
}
|
101
|
+
|
102
|
+
[
|
103
|
+
{
|
104
|
+
name: 'vagrant-user-setup-windows',
|
105
|
+
type: 'cloud-config',
|
106
|
+
content: cloud_config
|
107
|
+
}
|
108
|
+
]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,413 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'eryph'
|
4
|
+
require 'set'
|
5
|
+
require 'log4r'
|
6
|
+
require 'yaml'
|
7
|
+
require_relative '../errors'
|
8
|
+
|
9
|
+
module VagrantPlugins
|
10
|
+
module Eryph
|
11
|
+
module Helpers
|
12
|
+
class EryphClient
|
13
|
+
# Permission scopes for minimal access
|
14
|
+
SCOPES = {
|
15
|
+
# Catlet-specific scopes
|
16
|
+
CATLETS_READ: 'compute:catlets:read',
|
17
|
+
CATLETS_WRITE: 'compute:catlets:write',
|
18
|
+
CATLETS_CONTROL: 'compute:catlets:control',
|
19
|
+
|
20
|
+
# Project-specific scopes
|
21
|
+
PROJECTS_READ: 'compute:projects:read',
|
22
|
+
PROJECTS_WRITE: 'compute:projects:write'
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
def initialize(machine)
|
26
|
+
@machine = machine
|
27
|
+
@config = machine.provider_config
|
28
|
+
@ui = machine.ui
|
29
|
+
@client = nil
|
30
|
+
@logger = Log4r::Logger.new('vagrant::eryph::client')
|
31
|
+
end
|
32
|
+
|
33
|
+
def client(scopes = nil)
|
34
|
+
# Create a new client if scopes changed or client doesn't exist
|
35
|
+
requested_scopes = scopes || [SCOPES[:CATLETS_WRITE], SCOPES[:PROJECTS_WRITE]]
|
36
|
+
|
37
|
+
if @client.nil? || @last_scopes != requested_scopes
|
38
|
+
@client = create_client(requested_scopes)
|
39
|
+
@last_scopes = requested_scopes
|
40
|
+
end
|
41
|
+
|
42
|
+
@client
|
43
|
+
end
|
44
|
+
|
45
|
+
def list_catlets
|
46
|
+
handle_api_errors do
|
47
|
+
response = client([SCOPES[:CATLETS_READ]]).catlets.catlets_list
|
48
|
+
# CatletList object contains 'value' array with actual catlets
|
49
|
+
response.respond_to?(:value) ? response.value : []
|
50
|
+
end
|
51
|
+
rescue Errors::EryphError
|
52
|
+
[] # Return empty array on API errors to allow graceful degradation
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_catlet(catlet_id)
|
56
|
+
handle_api_errors do
|
57
|
+
client([SCOPES[:CATLETS_READ]]).catlets.catlets_get(catlet_id)
|
58
|
+
end
|
59
|
+
rescue Errors::EryphError
|
60
|
+
nil # Return nil on errors to allow graceful handling
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_catlet(catlet_config_hash)
|
64
|
+
SecureRandom.uuid
|
65
|
+
@ui.info("Creating catlet: #{catlet_config_hash[:name]}")
|
66
|
+
|
67
|
+
# Validate configuration first
|
68
|
+
raise 'Catlet configuration validation failed' unless validate_catlet_config(catlet_config_hash)
|
69
|
+
|
70
|
+
# Create proper NewCatletRequest object with hash configuration
|
71
|
+
request_obj = ::Eryph::ComputeClient::NewCatletRequest.new(
|
72
|
+
configuration: catlet_config_hash
|
73
|
+
)
|
74
|
+
|
75
|
+
operation = client([SCOPES[:CATLETS_WRITE]]).catlets.catlets_create({ new_catlet_request: request_obj })
|
76
|
+
|
77
|
+
raise 'Failed to create catlet: No operation returned' unless operation&.id
|
78
|
+
|
79
|
+
@logger.info("Operation ID: #{operation.id} - Creating catlet...")
|
80
|
+
result = wait_for_operation(operation.id)
|
81
|
+
|
82
|
+
if result.completed?
|
83
|
+
# Use OperationResult's catlet accessor
|
84
|
+
catlet = result.catlet
|
85
|
+
raise "Operation ID: #{operation.id} - Catlet creation completed but catlet not found" unless catlet
|
86
|
+
|
87
|
+
@logger.info("Operation ID: #{operation.id} - created catlet with ID: #{catlet.id}")
|
88
|
+
result
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
else
|
93
|
+
error_msg = result.status_message || 'Operation failed'
|
94
|
+
raise "Operation ID: #{operation.id} - Catlet creation failed: #{error_msg}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def start_catlet(catlet_id)
|
99
|
+
@logger.info("Starting catlet: #{catlet_id}")
|
100
|
+
|
101
|
+
operation = client([SCOPES[:CATLETS_CONTROL]]).catlets.catlets_start(catlet_id)
|
102
|
+
|
103
|
+
raise 'Failed to start catlet: No operation returned' unless operation&.id
|
104
|
+
|
105
|
+
wait_for_operation(operation.id)
|
106
|
+
end
|
107
|
+
|
108
|
+
def stop_catlet(catlet_id, stop_mode = 'graceful')
|
109
|
+
@logger.info("Stopping catlet: #{catlet_id}")
|
110
|
+
|
111
|
+
# Map string modes to proper enum values
|
112
|
+
api_mode = case stop_mode.to_s.downcase
|
113
|
+
when 'graceful', 'shutdown'
|
114
|
+
::Eryph::ComputeClient::CatletStopMode::SHUTDOWN
|
115
|
+
when 'hard'
|
116
|
+
::Eryph::ComputeClient::CatletStopMode::HARD
|
117
|
+
when 'kill'
|
118
|
+
::Eryph::ComputeClient::CatletStopMode::KILL
|
119
|
+
else
|
120
|
+
::Eryph::ComputeClient::CatletStopMode::SHUTDOWN
|
121
|
+
end
|
122
|
+
|
123
|
+
# Create proper StopCatletRequestBody object
|
124
|
+
stop_request = ::Eryph::ComputeClient::StopCatletRequestBody.new(
|
125
|
+
mode: api_mode
|
126
|
+
)
|
127
|
+
operation = client([SCOPES[:CATLETS_CONTROL]]).catlets.catlets_stop(catlet_id, stop_request)
|
128
|
+
|
129
|
+
raise 'Failed to stop catlet: No operation returned' unless operation&.id
|
130
|
+
|
131
|
+
wait_for_operation(operation.id)
|
132
|
+
end
|
133
|
+
|
134
|
+
def destroy_catlet(catlet_id)
|
135
|
+
@logger.info("Destroying catlet: #{catlet_id}")
|
136
|
+
|
137
|
+
operation = client([SCOPES[:CATLETS_WRITE]]).catlets.catlets_delete(catlet_id)
|
138
|
+
|
139
|
+
raise 'Failed to destroy catlet: No operation returned' unless operation&.id
|
140
|
+
|
141
|
+
wait_for_operation(operation.id)
|
142
|
+
end
|
143
|
+
|
144
|
+
def list_projects
|
145
|
+
response = client([SCOPES[:PROJECTS_READ]]).projects.projects_list
|
146
|
+
# Handle the response structure - ProjectList has 'value' property with array
|
147
|
+
response.respond_to?(:value) ? response.value : response
|
148
|
+
rescue StandardError => e
|
149
|
+
@ui.error("Failed to list projects: #{e.message}")
|
150
|
+
[]
|
151
|
+
end
|
152
|
+
|
153
|
+
def get_project(project_name)
|
154
|
+
projects = list_projects
|
155
|
+
projects.find { |p| p.name == project_name }
|
156
|
+
rescue StandardError => e
|
157
|
+
@ui.error("Failed to get project #{project_name}: #{e.message}")
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_project(project_name)
|
162
|
+
@logger.info("Creating project: #{project_name}")
|
163
|
+
|
164
|
+
# Create proper NewProjectRequest object
|
165
|
+
project_request = ::Eryph::ComputeClient::NewProjectRequest.new(
|
166
|
+
name: project_name
|
167
|
+
)
|
168
|
+
|
169
|
+
operation = client([SCOPES[:PROJECTS_WRITE]]).projects.projects_create(new_project_request: project_request)
|
170
|
+
|
171
|
+
raise 'Failed to create project: No operation returned' unless operation&.id
|
172
|
+
|
173
|
+
@logger.info("Operation ID: #{operation.id} - Creating project...")
|
174
|
+
result = wait_for_operation(operation.id)
|
175
|
+
|
176
|
+
if result.completed?
|
177
|
+
# Use OperationResult's project accessor
|
178
|
+
project = result.project
|
179
|
+
raise "Operation ID: #{operation.id} - Project creation completed but project not found" unless project
|
180
|
+
|
181
|
+
@logger.info("Operation ID: #{operation.id} - created project with ID: #{project.id}")
|
182
|
+
project # Return the project, not the result
|
183
|
+
else
|
184
|
+
error_msg = result.status_message || 'Operation failed'
|
185
|
+
raise "Operation ID: #{operation.id} - Project creation failed: #{error_msg}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def ensure_project_exists(project_name)
|
190
|
+
return unless project_name
|
191
|
+
|
192
|
+
project = get_project(project_name)
|
193
|
+
return project if project
|
194
|
+
|
195
|
+
unless @config.auto_create_project
|
196
|
+
raise "Project '#{project_name}' not found and auto_create_project is disabled"
|
197
|
+
end
|
198
|
+
|
199
|
+
@ui.info("Project '#{project_name}' not found, creating automatically...")
|
200
|
+
create_project(project_name) # Now returns the project directly, no race condition!
|
201
|
+
end
|
202
|
+
|
203
|
+
def remove_project(project_name)
|
204
|
+
@logger.info("Removing project: #{project_name}")
|
205
|
+
|
206
|
+
project = get_project(project_name)
|
207
|
+
raise "Project '#{project_name}' not found" unless project
|
208
|
+
|
209
|
+
operation = client([SCOPES[:PROJECTS_WRITE]]).projects.projects_delete(project.id)
|
210
|
+
raise 'Failed to remove project: No operation returned' unless operation&.id
|
211
|
+
|
212
|
+
@logger.info("Operation ID: #{operation.id} - Removing project...")
|
213
|
+
result = wait_for_operation(operation.id)
|
214
|
+
|
215
|
+
if result.completed?
|
216
|
+
@logger.info("Operation ID: #{operation.id} - project removed successfully")
|
217
|
+
result
|
218
|
+
else
|
219
|
+
error_msg = result.status_message || 'Operation failed'
|
220
|
+
raise "Operation ID: #{operation.id} - Project removal failed: #{error_msg}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def get_network_config(project_name)
|
225
|
+
project = get_project(project_name)
|
226
|
+
raise "Project '#{project_name}' not found" unless project
|
227
|
+
|
228
|
+
response = client([SCOPES[:PROJECTS_READ]]).virtual_networks.virtual_networks_get_config(project.id)
|
229
|
+
response.respond_to?(:configuration) ? response.configuration : response
|
230
|
+
rescue StandardError => e
|
231
|
+
@ui.error("Failed to get network configuration for project #{project_name}: #{e.message}")
|
232
|
+
raise e
|
233
|
+
end
|
234
|
+
|
235
|
+
def set_network_config(project_name, config_yaml)
|
236
|
+
project = get_project(project_name)
|
237
|
+
raise "Project '#{project_name}' not found" unless project
|
238
|
+
|
239
|
+
# Parse YAML to hash (encoding should be handled by caller)
|
240
|
+
config_hash = YAML.safe_load(config_yaml)
|
241
|
+
|
242
|
+
# Create proper VirtualNetworkConfiguration object
|
243
|
+
network_config = ::Eryph::ComputeClient::VirtualNetworkConfiguration.new(
|
244
|
+
configuration: config_hash
|
245
|
+
)
|
246
|
+
|
247
|
+
operation = client([SCOPES[:PROJECTS_WRITE]]).virtual_networks.virtual_networks_set_config(
|
248
|
+
project.id,
|
249
|
+
virtual_network_configuration: network_config
|
250
|
+
)
|
251
|
+
|
252
|
+
raise 'Failed to set network configuration: No operation returned' unless operation&.id
|
253
|
+
|
254
|
+
@logger.info("Operation ID: #{operation.id} - Setting network configuration...")
|
255
|
+
result = wait_for_operation(operation.id)
|
256
|
+
|
257
|
+
if result.completed?
|
258
|
+
@logger.info("Operation ID: #{operation.id} - network configuration set successfully")
|
259
|
+
result
|
260
|
+
else
|
261
|
+
error_msg = result.status_message || 'Operation failed'
|
262
|
+
raise "Operation ID: #{operation.id} - Network configuration failed: #{error_msg}"
|
263
|
+
end
|
264
|
+
rescue StandardError => e
|
265
|
+
@ui.error("Failed to set network configuration for project #{project_name}: #{e.message}")
|
266
|
+
raise e
|
267
|
+
end
|
268
|
+
|
269
|
+
def validate_catlet_config(catlet_config)
|
270
|
+
@ui.info('Validating catlet configuration...')
|
271
|
+
|
272
|
+
begin
|
273
|
+
validation_result = handle_api_errors do
|
274
|
+
client([SCOPES[:CATLETS_READ]]).validate_catlet_config(catlet_config)
|
275
|
+
end
|
276
|
+
|
277
|
+
if validation_result.respond_to?(:is_valid) && validation_result.is_valid
|
278
|
+
@ui.success('Configuration validated successfully')
|
279
|
+
true
|
280
|
+
elsif validation_result.respond_to?(:errors) && validation_result.errors
|
281
|
+
@ui.error('Configuration validation failed:')
|
282
|
+
validation_result.errors.each do |error|
|
283
|
+
@ui.error(" - #{error}")
|
284
|
+
end
|
285
|
+
false
|
286
|
+
else
|
287
|
+
@ui.detail("Validation result: #{validation_result}")
|
288
|
+
true # Assume valid if we can't determine otherwise
|
289
|
+
end
|
290
|
+
rescue Errors::EryphError => e
|
291
|
+
@ui.error("Config validation failed: #{e.friendly_message}")
|
292
|
+
@ui.detail('Proceeding with catlet creation...')
|
293
|
+
true # Don't block creation if validation service unavailable
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def wait_for_operation(operation_id, timeout = 600)
|
298
|
+
start_time = Time.now
|
299
|
+
current_tasks = {}
|
300
|
+
|
301
|
+
@logger.info("Waiting for operation #{operation_id}...")
|
302
|
+
|
303
|
+
result = client([SCOPES[:CATLETS_READ]]).wait_for_operation(operation_id, timeout: timeout) do |event_type, data|
|
304
|
+
case event_type
|
305
|
+
|
306
|
+
when :resource_new
|
307
|
+
resource_type = data.resource_type || 'Resource'
|
308
|
+
resource_id = data.resource_id || data.id || 'unknown'
|
309
|
+
@logger.debug("Attached #{resource_type} '#{resource_id}' to operation")
|
310
|
+
|
311
|
+
when :task_new, :task_update
|
312
|
+
# Track current tasks by ID
|
313
|
+
if data.respond_to?(:id) && data.id
|
314
|
+
current_tasks[data.id] = {
|
315
|
+
name: data.display_name || data.name,
|
316
|
+
progress: data.respond_to?(:progress) ? data.progress : nil
|
317
|
+
}
|
318
|
+
|
319
|
+
@logger.debug("Task update #{current_tasks[data.id].inspect}")
|
320
|
+
end
|
321
|
+
|
322
|
+
when :status
|
323
|
+
# Report current task with progress if available
|
324
|
+
elapsed = Time.now - start_time
|
325
|
+
|
326
|
+
# Find active tasks with progress
|
327
|
+
active_task = current_tasks.values.find do |task|
|
328
|
+
task[:progress]&.positive? && task[:progress] < 100
|
329
|
+
end
|
330
|
+
|
331
|
+
if active_task
|
332
|
+
@ui.info("Working... - #{active_task[:name]} #{active_task[:progress]}% - #{elapsed.round}s total elapsed")
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Show final result
|
338
|
+
if result.completed?
|
339
|
+
@logger.info("Operation #{operation_id} completed successfully")
|
340
|
+
elsif result.failed?
|
341
|
+
error_msg = result.status_message || 'Operation failed'
|
342
|
+
@ui.error("Operation failed: #{error_msg}")
|
343
|
+
raise "Operation #{operation_id} failed: #{error_msg}"
|
344
|
+
else
|
345
|
+
@ui.warn("Operation finished with status: #{result.status}")
|
346
|
+
end
|
347
|
+
|
348
|
+
result
|
349
|
+
rescue StandardError => e
|
350
|
+
@ui.error("Error waiting for operation: #{e.message}")
|
351
|
+
raise e
|
352
|
+
end
|
353
|
+
|
354
|
+
private
|
355
|
+
|
356
|
+
def create_client(scopes = nil)
|
357
|
+
config_name = @config.configuration_name
|
358
|
+
|
359
|
+
# Build options for client creation
|
360
|
+
client_options = {}
|
361
|
+
|
362
|
+
# Add SSL configuration options
|
363
|
+
ssl_config = {}
|
364
|
+
ssl_config[:verify_ssl] = @config.ssl_verify unless @config.ssl_verify.nil?
|
365
|
+
ssl_config[:ca_file] = @config.ssl_ca_file if @config.ssl_ca_file
|
366
|
+
client_options[:ssl_config] = ssl_config if ssl_config.any?
|
367
|
+
|
368
|
+
# Add minimal scopes - use provided scopes or default to catlets+projects write
|
369
|
+
client_options[:scopes] = scopes || [SCOPES[:CATLETS_WRITE], SCOPES[:PROJECTS_WRITE]]
|
370
|
+
|
371
|
+
# Add client_id if specified
|
372
|
+
client_options[:client_id] = @config.client_id if @config.client_id
|
373
|
+
|
374
|
+
info_msg = if config_name
|
375
|
+
"Connecting to eryph using configuration: #{config_name}"
|
376
|
+
else
|
377
|
+
'Connecting to eryph using automatic credential discovery'
|
378
|
+
end
|
379
|
+
@logger.debug(info_msg)
|
380
|
+
|
381
|
+
begin
|
382
|
+
client = ::Eryph.compute_client(config_name, **client_options)
|
383
|
+
@logger.debug('Successfully connected to eryph.')
|
384
|
+
client
|
385
|
+
rescue StandardError => e
|
386
|
+
@ui.error("Failed to connect to eryph: #{e.message}")
|
387
|
+
@ui.info('Make sure eryph is running and your credentials are configured')
|
388
|
+
raise "Failed to connect to eryph: #{e.message}"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# Enhanced error handling that converts API errors to user-friendly messages
|
393
|
+
def handle_api_errors
|
394
|
+
yield
|
395
|
+
rescue StandardError => e
|
396
|
+
if e.is_a?(::Eryph::Compute::ProblemDetailsError)
|
397
|
+
@ui.error("API Error: #{e.friendly_message}")
|
398
|
+
if e.has_problem_details?
|
399
|
+
@ui.detail("Problem Type: #{e.problem_type}") if e.problem_type
|
400
|
+
@ui.detail("Instance: #{e.instance}") if e.instance
|
401
|
+
end
|
402
|
+
raise Errors::EryphError.new(e.friendly_message, e)
|
403
|
+
else
|
404
|
+
# Re-raise other errors as-is but with better context
|
405
|
+
@ui.error("Unexpected error: #{e.message}")
|
406
|
+
@logger.debug("Error class: #{e.class}")
|
407
|
+
raise e
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VagrantPlugins
|
4
|
+
module Eryph
|
5
|
+
class Plugin < Vagrant.plugin('2')
|
6
|
+
name 'Eryph'
|
7
|
+
description <<-DESC
|
8
|
+
This plugin installs a provider that allows Vagrant to manage
|
9
|
+
catlets using Eryph's compute API.
|
10
|
+
DESC
|
11
|
+
|
12
|
+
config(:eryph, :provider) do
|
13
|
+
require_relative 'config'
|
14
|
+
Config
|
15
|
+
end
|
16
|
+
|
17
|
+
provider(:eryph, parallel: true, defaultable: false, box_optional: true) do
|
18
|
+
require_relative 'provider'
|
19
|
+
Provider
|
20
|
+
end
|
21
|
+
|
22
|
+
command('eryph') do
|
23
|
+
require_relative 'command'
|
24
|
+
Command
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'actions'
|
4
|
+
require_relative 'helpers/eryph_client'
|
5
|
+
require 'eryph'
|
6
|
+
|
7
|
+
# Import Vagrant's UNSET_VALUE constant
|
8
|
+
UNSET_VALUE = Vagrant::Plugin::V2::Config::UNSET_VALUE
|
9
|
+
|
10
|
+
module VagrantPlugins
|
11
|
+
module Eryph
|
12
|
+
class Provider < Vagrant.plugin('2', :provider)
|
13
|
+
# This class method caches status for all catlets within
|
14
|
+
# the Eryph project. A specific catlet's status
|
15
|
+
# may be refreshed by passing :refresh => true as an option.
|
16
|
+
def self.eryph_catlet(machine, opts = {})
|
17
|
+
client = Helpers::EryphClient.new(machine)
|
18
|
+
|
19
|
+
# load status of catlets if it has not been done before
|
20
|
+
@eryph_catlets ||= client.list_catlets || []
|
21
|
+
|
22
|
+
if opts[:refresh] && machine.id
|
23
|
+
# refresh the catlet status for the given machine
|
24
|
+
@eryph_catlets.delete_if { |c| c.id == machine.id }
|
25
|
+
catlet = client.get_catlet(machine.id)
|
26
|
+
@eryph_catlets << catlet if catlet
|
27
|
+
elsif machine.id
|
28
|
+
# lookup catlet status for the given machine
|
29
|
+
catlet = Array(@eryph_catlets).find { |c| c.id == machine.id }
|
30
|
+
end
|
31
|
+
|
32
|
+
# if lookup by id failed, check for a catlet with a matching name
|
33
|
+
# and set the id to ensure vagrant stores locally
|
34
|
+
unless catlet
|
35
|
+
name = machine.config.vm.hostname || machine.name
|
36
|
+
catlet = @eryph_catlets.find { |c| c.name == name.to_s }
|
37
|
+
machine.id = catlet.id.to_s if catlet
|
38
|
+
end
|
39
|
+
|
40
|
+
catlet || OpenStruct.new(status: 'not_created')
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(machine)
|
44
|
+
@machine = machine
|
45
|
+
end
|
46
|
+
|
47
|
+
def action(name)
|
48
|
+
# Attempt to get the action method from the Actions module if it
|
49
|
+
# exists, otherwise return nil to show that we don't support the
|
50
|
+
# given action.
|
51
|
+
action_method = "action_#{name}"
|
52
|
+
return Actions.send(action_method) if Actions.respond_to?(action_method)
|
53
|
+
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# This method is called if the underlying machine ID changes. Providers
|
58
|
+
# can use this method to load in new data for the actual backing
|
59
|
+
# machine or to realize that the machine is now gone (the ID can
|
60
|
+
# become `nil`).
|
61
|
+
def machine_id_changed
|
62
|
+
# Clear cached catlets when machine ID changes
|
63
|
+
@eryph_catlets = nil if defined?(@eryph_catlets)
|
64
|
+
end
|
65
|
+
|
66
|
+
# This should return a hash of information that explains how to
|
67
|
+
# SSH into the machine. If the machine is not at a point where
|
68
|
+
# SSH is even possible, then `nil` should be returned.
|
69
|
+
def ssh_info
|
70
|
+
require 'log4r'
|
71
|
+
logger = Log4r::Logger.new('vagrant::eryph::provider')
|
72
|
+
|
73
|
+
catlet = Provider.eryph_catlet(@machine)
|
74
|
+
logger.debug("ssh_info catlet status: #{catlet&.status}")
|
75
|
+
|
76
|
+
# Return nil if catlet doesn't exist or isn't running
|
77
|
+
return nil unless catlet
|
78
|
+
return nil unless catlet.status&.downcase == 'running'
|
79
|
+
|
80
|
+
# Get IP address from catlet networks
|
81
|
+
ip_address = extract_ip_address(catlet)
|
82
|
+
logger.debug("ssh_info extracted IP: #{ip_address}")
|
83
|
+
return nil unless ip_address
|
84
|
+
|
85
|
+
{
|
86
|
+
host: ip_address,
|
87
|
+
username: 'vagrant'
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# This should return the state of the machine within this provider.
|
92
|
+
# The state must be an instance of {MachineState}.
|
93
|
+
def state
|
94
|
+
catlet = Provider.eryph_catlet(@machine) if @machine.id
|
95
|
+
|
96
|
+
state_id = if catlet&.status
|
97
|
+
map_catlet_state_to_vagrant(catlet.status)
|
98
|
+
else
|
99
|
+
:not_created
|
100
|
+
end
|
101
|
+
|
102
|
+
long = short = state_id.to_s
|
103
|
+
Vagrant::MachineState.new(state_id, short, long)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def extract_ip_address(catlet)
|
109
|
+
# Extract IP address from catlet networks
|
110
|
+
return nil unless catlet.respond_to?(:networks) && catlet.networks
|
111
|
+
|
112
|
+
catlet.networks.each_with_index do |network, _idx|
|
113
|
+
# Only check floating port IP addresses (internal IPs are not accessible from outside)
|
114
|
+
next unless network.respond_to?(:floating_port) && network.floating_port
|
115
|
+
|
116
|
+
next unless network.floating_port.respond_to?(:ip_v4_addresses) &&
|
117
|
+
network.floating_port.ip_v4_addresses &&
|
118
|
+
!network.floating_port.ip_v4_addresses.empty?
|
119
|
+
|
120
|
+
ip = network.floating_port.ip_v4_addresses.first
|
121
|
+
return ip
|
122
|
+
end
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
|
126
|
+
def map_catlet_state_to_vagrant(eryph_status)
|
127
|
+
case eryph_status.downcase
|
128
|
+
when 'running'
|
129
|
+
:running
|
130
|
+
when 'stopped'
|
131
|
+
:stopped
|
132
|
+
when 'pending'
|
133
|
+
:unknown # Pending could be starting or stopping - we don't know which
|
134
|
+
when 'error'
|
135
|
+
:error
|
136
|
+
else
|
137
|
+
:unknown
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|