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.
@@ -0,0 +1,553 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module VagrantPlugins
6
+ module Eryph
7
+ class Config < Vagrant.plugin('2', :config)
8
+ # Eryph-specific configuration
9
+ attr_accessor :project
10
+
11
+ # Client configuration
12
+ attr_accessor :client_id
13
+
14
+ # SSL configuration
15
+ attr_accessor :ssl_verify
16
+
17
+ # Cloud-init and user setup configuration
18
+ attr_accessor :auto_config
19
+
20
+ # Hyper-V/VirtualBox compatible properties
21
+ attr_reader :cpus, :memory, :maxmemory, :vmname, :hostname
22
+ attr_reader :enable_virtualization_extensions, :enable_secure_boot
23
+
24
+ # Eryph top-level catlet properties
25
+ attr_reader :parent, :location, :environment, :store
26
+
27
+ # Complex structures (arrays/hashes)
28
+ attr_accessor :cpu_config, :memory_config, :drives, :networks
29
+ attr_accessor :catlet_name, :config_name, :auto_create_project, :configuration_name, :ssl_ca_file, :enable_winrm,
30
+ :vagrant_password, :ssh_key_injection, :fodder, :network_adapters, :capabilities, :variables
31
+
32
+ # Catlet configuration - direct hash structure
33
+ attr_accessor :catlet
34
+
35
+ def initialize
36
+ @project = UNSET_VALUE
37
+ @catlet_name = UNSET_VALUE
38
+ @config_name = UNSET_VALUE
39
+ @auto_create_project = UNSET_VALUE
40
+
41
+ @client_id = UNSET_VALUE
42
+ @configuration_name = UNSET_VALUE
43
+
44
+ @ssl_verify = UNSET_VALUE
45
+ @ssl_ca_file = UNSET_VALUE
46
+
47
+ @auto_config = UNSET_VALUE
48
+ @enable_winrm = UNSET_VALUE
49
+ @vagrant_password = UNSET_VALUE
50
+ @ssh_key_injection = UNSET_VALUE
51
+ @fodder = UNSET_VALUE
52
+
53
+ # Hyper-V/VirtualBox compatible properties
54
+ @cpus = UNSET_VALUE
55
+ @memory = UNSET_VALUE
56
+ @maxmemory = UNSET_VALUE
57
+ @vmname = UNSET_VALUE
58
+ @hostname = UNSET_VALUE
59
+ @enable_virtualization_extensions = UNSET_VALUE
60
+ @enable_secure_boot = UNSET_VALUE
61
+
62
+ # Eryph top-level properties
63
+ @parent = UNSET_VALUE
64
+ @location = UNSET_VALUE
65
+ @environment = UNSET_VALUE
66
+ @store = UNSET_VALUE
67
+
68
+ # Complex structures
69
+ @cpu_config = UNSET_VALUE
70
+ @memory_config = UNSET_VALUE
71
+ @drives = UNSET_VALUE
72
+ @networks = UNSET_VALUE
73
+ @network_adapters = UNSET_VALUE
74
+ @capabilities = UNSET_VALUE
75
+ @variables = UNSET_VALUE
76
+
77
+ # New catlet configuration
78
+ @catlet = UNSET_VALUE
79
+
80
+ # Gene references (separate from fodder)
81
+ @genes = UNSET_VALUE
82
+ end
83
+
84
+ def finalize!
85
+ @project = 'default' if @project == UNSET_VALUE
86
+ @catlet_name = nil if @catlet_name == UNSET_VALUE
87
+ @config_name = nil if @config_name == UNSET_VALUE
88
+ @auto_create_project = true if @auto_create_project == UNSET_VALUE
89
+
90
+ @client_id = nil if @client_id == UNSET_VALUE
91
+ @configuration_name = nil if @configuration_name == UNSET_VALUE
92
+
93
+ # SSL defaults - disable verification for localhost
94
+ @ssl_verify = determine_ssl_verify_default if @ssl_verify == UNSET_VALUE
95
+ @ssl_ca_file = nil if @ssl_ca_file == UNSET_VALUE
96
+
97
+ @auto_config = true if @auto_config == UNSET_VALUE
98
+ @enable_winrm = true if @enable_winrm == UNSET_VALUE
99
+ # Set auto password - will be resolved based on OS when machine context is available
100
+ @vagrant_password = :auto if @vagrant_password == UNSET_VALUE
101
+ @ssh_key_injection = :direct if @ssh_key_injection == UNSET_VALUE
102
+ @fodder = [] if @fodder == UNSET_VALUE
103
+ @genes = [] if @genes == UNSET_VALUE
104
+
105
+ # Hyper-V/VirtualBox compatible property defaults
106
+ @cpus = nil if @cpus == UNSET_VALUE
107
+ @memory = nil if @memory == UNSET_VALUE
108
+ @maxmemory = nil if @maxmemory == UNSET_VALUE
109
+ @vmname = nil if @vmname == UNSET_VALUE
110
+ @hostname = nil if @hostname == UNSET_VALUE
111
+ @enable_virtualization_extensions = nil if @enable_virtualization_extensions == UNSET_VALUE
112
+ @enable_secure_boot = nil if @enable_secure_boot == UNSET_VALUE
113
+
114
+ # Eryph property defaults
115
+ @parent = nil if @parent == UNSET_VALUE
116
+ @location = nil if @location == UNSET_VALUE
117
+ @environment = nil if @environment == UNSET_VALUE
118
+ @store = nil if @store == UNSET_VALUE
119
+
120
+ # Complex structure defaults
121
+ @cpu_config = nil if @cpu_config == UNSET_VALUE
122
+ @memory_config = nil if @memory_config == UNSET_VALUE
123
+ @drives = [] if @drives == UNSET_VALUE
124
+ @networks = [] if @networks == UNSET_VALUE
125
+ @network_adapters = [] if @network_adapters == UNSET_VALUE
126
+ @capabilities = [] if @capabilities == UNSET_VALUE
127
+ @variables = [] if @variables == UNSET_VALUE
128
+
129
+ # Initialize catlet configuration as empty hash if not set
130
+ @catlet = {} if @catlet == UNSET_VALUE
131
+ end
132
+
133
+ def validate(_machine)
134
+ errors = _detected_errors
135
+
136
+ # Validate required fields - check both catlet hash
137
+ catlet_hash = @catlet.is_a?(Hash) ? @catlet : {}
138
+ parent = catlet_hash[:parent] || catlet_hash['parent']
139
+ errors << 'parent is required (set in catlet hash)' unless parent
140
+
141
+ # Project is optional - defaults to 'default' if not specified
142
+ # No validation needed since we have a fallback
143
+
144
+ # Validate ssh_key_injection option
145
+ if @ssh_key_injection && !%i[direct variable].include?(@ssh_key_injection)
146
+ errors << 'ssh_key_injection must be :direct or :variable'
147
+ end
148
+
149
+
150
+ { 'Eryph Provider' => errors }
151
+ end
152
+
153
+ # Helper method to determine SSL verification default
154
+ def determine_ssl_verify_default
155
+ # Default to false for localhost/development, true for remote endpoints
156
+ # This will be refined when we implement the client lookup logic
157
+ false
158
+ end
159
+
160
+ # Helper method to get the effective catlet name
161
+ def effective_catlet_name(machine)
162
+ @catlet_name || machine.config.vm.hostname || machine.name.to_s
163
+ end
164
+
165
+ # Helper method to build the effective catlet configuration
166
+ # Combines the new catlet hash with legacy individual properties for backward compatibility
167
+ def effective_catlet_configuration(machine)
168
+ # Start with the catlet hash configuration
169
+ config = @catlet.dup
170
+
171
+ # Add name and project (always required)
172
+ config[:name] = effective_catlet_name(machine)
173
+ config[:project] = @project
174
+
175
+ config
176
+ end
177
+
178
+ # Helper method to merge user fodder with auto-generated fodder
179
+ def merged_fodder(auto_generated_fodder = [])
180
+ return @fodder unless @auto_config
181
+
182
+ merged = auto_generated_fodder.dup
183
+
184
+ # Add gene fodder (convert from gene references), deduplicating by source
185
+ if @genes && @genes.any?
186
+ @genes.each do |gene_config|
187
+ # Skip duplicates based on source
188
+ unless merged.any? { |item| item[:source] == gene_config[:source] }
189
+ merged << gene_config
190
+ end
191
+ end
192
+ end
193
+
194
+ # Add user-provided fodder, avoiding duplicates using composite keys
195
+ @fodder.each do |user_item|
196
+ # Build unique key based on source/name combination
197
+ user_key = if user_item[:source] && user_item[:name]
198
+ "#{user_item[:source]}:#{user_item[:name]}" # source + name
199
+ elsif user_item[:source]
200
+ user_item[:source] # source only
201
+ else
202
+ user_item[:name] # name only (local fodder)
203
+ end
204
+
205
+ # Find existing item with same key
206
+ existing_index = merged.find_index do |item|
207
+ existing_key = if item[:source] && item[:name]
208
+ "#{item[:source]}:#{item[:name]}"
209
+ elsif item[:source]
210
+ item[:source]
211
+ else
212
+ item[:name]
213
+ end
214
+ existing_key == user_key
215
+ end
216
+
217
+ if existing_index
218
+ # Replace existing item with user-provided one
219
+ merged[existing_index] = user_item
220
+ else
221
+ # Add new user item
222
+ merged << user_item
223
+ end
224
+ end
225
+
226
+ merged
227
+ end
228
+
229
+ # ============================================================
230
+ # HYPER-V/VIRTUALBOX COMPATIBLE PROPERTY SETTERS
231
+ # ============================================================
232
+
233
+ private
234
+
235
+ def ensure_catlet_hash!
236
+ @catlet = {} if @catlet == UNSET_VALUE || !@catlet.is_a?(Hash)
237
+ end
238
+
239
+ public
240
+
241
+ # CPU configuration
242
+ def cpus=(value)
243
+ @cpus = value
244
+ @catlet = {} if @catlet == UNSET_VALUE || !@catlet.is_a?(Hash)
245
+ @catlet[:cpu] ||= {}
246
+ @catlet[:cpu][:count] = value
247
+ end
248
+
249
+ # Memory configuration (startup memory)
250
+ def memory=(value)
251
+ @memory = value
252
+ @catlet = {} if @catlet == UNSET_VALUE || !@catlet.is_a?(Hash)
253
+ @catlet[:memory] ||= {}
254
+ @catlet[:memory][:startup] = value
255
+ end
256
+
257
+ # Maximum memory (enables dynamic memory)
258
+ def maxmemory=(value)
259
+ @maxmemory = value
260
+ ensure_catlet_hash!
261
+ @catlet[:memory] ||= {}
262
+ @catlet[:memory][:maximum] = value
263
+
264
+ # Auto-enable dynamic memory when maxmemory is set
265
+ @catlet[:capabilities] ||= []
266
+ @catlet[:capabilities].reject! { |c| c[:name] == 'dynamic_memory' }
267
+ @catlet[:capabilities] << { name: 'dynamic_memory' }
268
+ end
269
+
270
+ # VM name (maps to catlet name)
271
+ def vmname=(value)
272
+ @vmname = value
273
+ @catlet_name = value # For backward compatibility
274
+ ensure_catlet_hash!
275
+ @catlet[:name] = value
276
+ end
277
+
278
+ # Network hostname
279
+ def hostname=(value)
280
+ @hostname = value
281
+ ensure_catlet_hash!
282
+ @catlet[:hostname] = value
283
+ end
284
+
285
+ # Nested virtualization support
286
+ def enable_virtualization_extensions=(value)
287
+ @enable_virtualization_extensions = value
288
+ ensure_catlet_hash!
289
+ @catlet[:capabilities] ||= []
290
+ @catlet[:capabilities].reject! { |c| c[:name] == 'nested_virtualization' }
291
+
292
+ return unless value
293
+
294
+ @catlet[:capabilities] << { name: 'nested_virtualization' }
295
+ end
296
+
297
+ # Secure boot support
298
+ def enable_secure_boot=(value)
299
+ @enable_secure_boot = value
300
+ ensure_catlet_hash!
301
+ @catlet[:capabilities] ||= []
302
+ @catlet[:capabilities].reject! { |c| c[:name] == 'secure_boot' }
303
+
304
+ return unless value
305
+
306
+ @catlet[:capabilities] << { name: 'secure_boot' }
307
+ end
308
+
309
+ # ============================================================
310
+ # ERYPH TOP-LEVEL PROPERTY SETTERS
311
+ # ============================================================
312
+
313
+ def parent=(value)
314
+ @parent = value
315
+ ensure_catlet_hash!
316
+ @catlet[:parent] = value
317
+ end
318
+
319
+ def location=(value)
320
+ @location = value
321
+ ensure_catlet_hash!
322
+ @catlet[:location] = value
323
+ end
324
+
325
+ def environment=(value)
326
+ @environment = value
327
+ ensure_catlet_hash!
328
+ @catlet[:environment] = value
329
+ end
330
+
331
+ def store=(value)
332
+ @store = value
333
+ ensure_catlet_hash!
334
+ @catlet[:store] = value
335
+ end
336
+
337
+ # ============================================================
338
+ # DRIVE MANAGEMENT WITH UNIX NAMING
339
+ # ============================================================
340
+
341
+ DRIVE_TYPE_MAP = {
342
+ vhd: 'VHD',
343
+ shared_vhd: 'SharedVHD',
344
+ dvd: 'DVD',
345
+ vhd_set: 'VHDSet'
346
+ }.freeze
347
+
348
+ def add_drive(name, size: nil, type: :vhd, source: nil, **options)
349
+ drive_config = { name: name }
350
+ drive_config[:size] = size if size
351
+
352
+ # Convert symbol to proper API string
353
+ drive_config[:type] = DRIVE_TYPE_MAP[type] || type.to_s
354
+ drive_config[:source] = source if source
355
+ drive_config.merge!(options)
356
+
357
+ @drives = [] if @drives == UNSET_VALUE
358
+ @drives << drive_config
359
+
360
+ ensure_catlet_hash!
361
+ @catlet[:drives] ||= []
362
+ @catlet[:drives] << drive_config
363
+ end
364
+
365
+ # ============================================================
366
+ # CAPABILITIES MANAGEMENT
367
+ # ============================================================
368
+
369
+ def enable_capability(name, details: nil)
370
+ ensure_catlet_hash!
371
+ @catlet[:capabilities] ||= []
372
+ @catlet[:capabilities].reject! { |c| c[:name] == name.to_s }
373
+ cap_config = { name: name.to_s }
374
+ cap_config[:details] = details if details
375
+ @catlet[:capabilities] << cap_config
376
+ end
377
+
378
+ def disable_capability(name)
379
+ ensure_catlet_hash!
380
+ @catlet[:capabilities] ||= []
381
+ @catlet[:capabilities].reject! { |c| c[:name] == name.to_s }
382
+ end
383
+
384
+ # ============================================================
385
+ # GENE MANAGEMENT
386
+ # ============================================================
387
+
388
+ def add_fodder_gene(geneset, gene, fodder_name: nil, variables: nil, **options)
389
+ # Build proper gene fodder reference syntax: gene:geneset:gene
390
+ gene_source = "gene:#{geneset}:#{gene}"
391
+
392
+ # Create fodder item with gene source
393
+ fodder_config = {
394
+ source: gene_source
395
+ }
396
+
397
+ # Add name only if provided
398
+ fodder_config[:name] = fodder_name if fodder_name
399
+
400
+ # Add variables if specified - must be an array of variable objects per spec
401
+ fodder_config[:variables] = variables if variables&.any?
402
+
403
+ # Add any other options
404
+ fodder_config.merge!(options) if options.any?
405
+
406
+ # Add to genes array for later processing
407
+ @genes = [] if @genes == UNSET_VALUE
408
+ @genes << fodder_config
409
+
410
+ fodder_config
411
+
412
+ end
413
+
414
+ # ============================================================
415
+ # VARIABLE MANAGEMENT
416
+ # ============================================================
417
+
418
+ def set_variable(name, value)
419
+ @variables = [] if @variables == UNSET_VALUE
420
+
421
+ # Find existing variable or create new one
422
+ var = @variables.find { |v| v[:name] == name }
423
+ if var
424
+ # Update existing variable's value
425
+ var[:value] = value
426
+ else
427
+ # Create new variable with value (no type inference - must be declared separately)
428
+ @variables << {
429
+ name: name,
430
+ value: value
431
+ }
432
+ end
433
+ end
434
+
435
+ # ============================================================
436
+ # FODDER HELPERS
437
+ # ============================================================
438
+
439
+ def cloud_config(name, content = nil)
440
+ @fodder ||= []
441
+
442
+ if block_given?
443
+ # DSL-style configuration
444
+ config_data = {}
445
+ yield config_data
446
+ content = config_data
447
+ end
448
+
449
+ @fodder << {
450
+ name: name,
451
+ type: 'cloud-config',
452
+ content: content
453
+ }
454
+ end
455
+
456
+ def shell_script(name, content)
457
+ @fodder ||= []
458
+ @fodder << {
459
+ name: name,
460
+ type: 'shellscript',
461
+ content: content
462
+ }
463
+ end
464
+
465
+ # Helper method to extract Vagrant cloud-init configuration
466
+ # NOTE: We don't use Vagrant's cloud-init system - we generate our own fodder
467
+ def extract_vagrant_cloud_init_config(_machine)
468
+ # Always return empty - we handle cloud-init through our own fodder system
469
+ []
470
+ end
471
+
472
+ private
473
+
474
+ # Convert Vagrant cloud-init configuration to Eryph fodder format
475
+ def convert_cloud_init_to_fodder(cloud_init_config, index = 0)
476
+ return nil unless cloud_init_config
477
+ return nil unless cloud_init_config.content_type
478
+ return nil if cloud_init_config.content_type == UNSET_VALUE
479
+
480
+ # Map Vagrant content types to Eryph fodder types
481
+ fodder_type = map_content_type_to_fodder_type(cloud_init_config.content_type)
482
+ return nil unless fodder_type
483
+
484
+ # Generate name based on type and index
485
+ name = "vagrant-cloud-init-#{fodder_type}"
486
+ name += "-#{index}" if index.positive?
487
+
488
+ # Extract content
489
+ content = extract_cloud_init_content(cloud_init_config)
490
+ return nil unless content
491
+
492
+ {
493
+ name: name,
494
+ type: fodder_type,
495
+ content: content
496
+ }
497
+ end
498
+
499
+ # Map Vagrant content types to Eryph fodder types
500
+ def map_content_type_to_fodder_type(content_type)
501
+ case content_type
502
+ when 'text/cloud-config'
503
+ 'cloud-config'
504
+ when 'text/x-shellscript'
505
+ 'shellscript'
506
+ when 'text/cloud-boothook'
507
+ 'cloud-boothook'
508
+ when 'text/cloud-config-archive'
509
+ 'cloud-config-archive'
510
+ when 'text/part-handler'
511
+ 'part-handler'
512
+ when 'text/upstart-job'
513
+ 'upstart-job'
514
+ else
515
+ nil # Unsupported content type
516
+ end
517
+ end
518
+
519
+ # Extract content from cloud-init configuration
520
+ def extract_cloud_init_content(cloud_init_config)
521
+ inline_content = cloud_init_config.inline
522
+ path_content = cloud_init_config.path
523
+
524
+ # Skip UNSET_VALUE properties
525
+ return nil if inline_content == UNSET_VALUE && path_content == UNSET_VALUE
526
+
527
+ if inline_content && inline_content != UNSET_VALUE
528
+ process_cloud_init_content(inline_content, cloud_init_config.content_type)
529
+ elsif path_content && path_content != UNSET_VALUE && File.exist?(path_content)
530
+ content = File.read(path_content)
531
+ process_cloud_init_content(content, cloud_init_config.content_type)
532
+ end
533
+ end
534
+
535
+ # Process cloud-init content based on content type
536
+ def process_cloud_init_content(content, content_type)
537
+ case content_type
538
+ when 'text/cloud-config'
539
+ # Parse YAML content into hash for cloud-config
540
+ begin
541
+ YAML.safe_load(content)
542
+ rescue StandardError
543
+ # If YAML parsing fails, return as string
544
+ content
545
+ end
546
+ else
547
+ # Return other content types as strings
548
+ content
549
+ end
550
+ end
551
+ end
552
+ end
553
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module Eryph
5
+ module Errors
6
+ class EryphError < Vagrant::Errors::VagrantError
7
+ attr_reader :problem_details
8
+
9
+ def initialize(message = nil, problem_details = nil)
10
+ @problem_details = problem_details
11
+ super(message)
12
+ end
13
+
14
+ error_namespace('vagrant_eryph.errors')
15
+
16
+ def has_problem_details?
17
+ !@problem_details.nil?
18
+ end
19
+
20
+ def friendly_message
21
+ if has_problem_details? && @problem_details.respond_to?(:friendly_message)
22
+ @problem_details.friendly_message
23
+ else
24
+ super
25
+ end
26
+ end
27
+ end
28
+
29
+ class APIConnectionError < EryphError
30
+ error_key(:api_connection_failed)
31
+ end
32
+
33
+ class CredentialsError < EryphError
34
+ error_key(:credentials_not_found)
35
+ end
36
+
37
+ class CatletNotFoundError < EryphError
38
+ error_key(:catlet_not_found)
39
+ end
40
+
41
+ class OperationFailedError < EryphError
42
+ error_key(:operation_failed)
43
+ end
44
+
45
+ class ConfigurationError < EryphError
46
+ error_key(:configuration_error)
47
+ end
48
+
49
+ # Helper method to convert API errors to our enhanced errors
50
+ def self.from_api_error(api_error, error_class = EryphError)
51
+ if api_error.is_a?(::Eryph::Compute::ProblemDetailsError)
52
+ error_class.new(api_error.friendly_message, api_error)
53
+ elsif api_error.respond_to?(:message)
54
+ error_class.new(api_error.message, api_error)
55
+ else
56
+ error_class.new(api_error.to_s, api_error)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end