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,573 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'yaml'
5
+ require 'json'
6
+
7
+ module VagrantPlugins
8
+ module Eryph
9
+ class Command < Vagrant.plugin('2', :command)
10
+ def self.synopsis
11
+ 'manage Eryph projects and network configurations'
12
+ end
13
+
14
+ def initialize(argv, env)
15
+ super
16
+ @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
17
+ @subcommands = Vagrant::Registry.new
18
+ @subcommands.register(:project) { ProjectCommand }
19
+ @subcommands.register(:network) { NetworkCommand }
20
+ end
21
+
22
+ def execute
23
+ if @main_args.include?('-h') || @main_args.include?('--help')
24
+ return help
25
+ end
26
+
27
+ command_class = @subcommands.get(@sub_command.to_sym) if @sub_command
28
+ return help if !command_class || !@sub_command
29
+
30
+ @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}")
31
+ command_class.new(@sub_args, @env).execute
32
+ end
33
+
34
+ def help
35
+ opts = OptionParser.new do |o|
36
+ o.banner = 'Usage: vagrant eryph <subcommand> [<args>]'
37
+ o.separator ''
38
+ o.separator 'Available subcommands:'
39
+ o.separator ' project Manage Eryph projects'
40
+ o.separator ' network Manage project network configurations'
41
+ o.separator ''
42
+ o.separator 'For help on any individual subcommand run `vagrant eryph <subcommand> -h`'
43
+ end
44
+
45
+ @env.ui.info(opts.help, prefix: false)
46
+ end
47
+ end
48
+
49
+ class ProjectCommand < Vagrant.plugin('2', :command)
50
+ def self.synopsis
51
+ 'manage Eryph projects'
52
+ end
53
+
54
+ def initialize(argv, env)
55
+ super
56
+ @options = {}
57
+ @parser = OptionParser.new do |o|
58
+ o.banner = 'Usage: vagrant eryph project <subcommand> [options]'
59
+ o.separator ''
60
+ o.separator 'Subcommands:'
61
+ o.separator ' list List available projects'
62
+ o.separator ' create Create a new project'
63
+ o.separator ' remove Remove a project'
64
+ o.separator ''
65
+ o.separator 'Global Options:'
66
+
67
+ o.on('--configuration-name NAME', String, 'Eryph configuration name (default: auto-detect)') do |name|
68
+ @options[:configuration_name] = name
69
+ end
70
+
71
+ o.on('--client-id ID', String, 'Eryph client ID') do |id|
72
+ @options[:client_id] = id
73
+ end
74
+
75
+ o.separator ''
76
+ end
77
+
78
+ @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
79
+ end
80
+
81
+ def execute
82
+ case @sub_command
83
+ when 'list'
84
+ execute_list
85
+ when 'create'
86
+ execute_create
87
+ when 'remove'
88
+ execute_remove
89
+ else
90
+ @env.ui.info(@parser.help, prefix: false)
91
+ 1
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def execute_list
98
+ client = get_eryph_client
99
+ @env.ui.info('Available Eryph projects:', prefix: false)
100
+
101
+ projects = client.list_projects
102
+ if projects.empty?
103
+ @env.ui.warn('No projects found')
104
+ else
105
+ projects.each do |project|
106
+ @env.ui.info(" #{project.name} (ID: #{project.id})", prefix: false)
107
+ end
108
+ end
109
+ 0
110
+ end
111
+
112
+ def execute_create
113
+ parser = OptionParser.new do |o|
114
+ o.banner = 'Usage: vagrant eryph project create <project-name> [options]'
115
+ o.separator ''
116
+ o.separator 'Options:'
117
+ o.on('--no-wait', 'Do not wait for operation to complete') do
118
+ @options[:no_wait] = true
119
+ end
120
+ end
121
+
122
+ begin
123
+ argv = parser.parse!(@sub_args.dup)
124
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
125
+ @env.ui.error("#{e.message}")
126
+ @env.ui.info(parser.help, prefix: false)
127
+ return nil
128
+ end
129
+
130
+ if argv.empty?
131
+ @env.ui.error('Project name is required')
132
+ @env.ui.info(parser.help, prefix: false)
133
+ return 1
134
+ end
135
+
136
+ project_name = argv[0]
137
+ client = get_eryph_client
138
+
139
+ begin
140
+ @env.ui.info("Creating project: #{project_name}")
141
+ project = client.create_project(project_name)
142
+ @env.ui.info("Project '#{project.name}' created successfully (ID: #{project.id})")
143
+ 0
144
+ rescue StandardError => e
145
+ @env.ui.error("Failed to create project: #{e.message}")
146
+ 1
147
+ end
148
+ end
149
+
150
+ def execute_remove
151
+ parser = OptionParser.new do |o|
152
+ o.banner = 'Usage: vagrant eryph project remove <project-name> [options]'
153
+ o.separator ''
154
+ o.separator 'Options:'
155
+ o.on('--force', 'Do not ask for confirmation') do
156
+ @options[:force] = true
157
+ end
158
+ o.on('--no-wait', 'Do not wait for operation to complete') do
159
+ @options[:no_wait] = true
160
+ end
161
+ end
162
+
163
+ begin
164
+ argv = parser.parse!(@sub_args.dup)
165
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
166
+ @env.ui.error("#{e.message}")
167
+ @env.ui.info(parser.help, prefix: false)
168
+ return nil
169
+ end
170
+
171
+ if argv.empty?
172
+ @env.ui.error('Project name is required')
173
+ @env.ui.info(parser.help, prefix: false)
174
+ return 1
175
+ end
176
+
177
+ project_name = argv[0]
178
+ client = get_eryph_client
179
+
180
+ begin
181
+ project = client.get_project(project_name)
182
+ unless project
183
+ @env.ui.error("Project '#{project_name}' not found")
184
+ return 1
185
+ end
186
+
187
+ unless @options[:force]
188
+ response = @env.ui.ask("Project '#{project.name}' (ID: #{project.id}) and all catlets will be deleted! Continue? (y/N)")
189
+ return 0 unless response.downcase.start_with?('y')
190
+ end
191
+
192
+ @env.ui.info("Removing project: #{project.name}")
193
+ delete_project(client, project.id)
194
+ @env.ui.info("Project '#{project.name}' removed successfully")
195
+ 0
196
+ rescue StandardError => e
197
+ @env.ui.error("Failed to remove project: #{e.message}")
198
+ 1
199
+ end
200
+ end
201
+
202
+ def get_eryph_client
203
+ # Try to get client from existing machines
204
+ @env.machine_names.each do |name|
205
+ machine = @env.machine(name, :eryph)
206
+ if machine.provider_config.is_a?(VagrantPlugins::Eryph::Config)
207
+ client = Helpers::EryphClient.new(machine)
208
+
209
+ # Override client configuration if command line options are provided
210
+ if @options[:configuration_name] || @options[:client_id]
211
+ override_client_config(client)
212
+ end
213
+
214
+ return client
215
+ end
216
+ end
217
+
218
+ # If no machines found, create a temporary config with command line options
219
+ create_standalone_client
220
+ end
221
+
222
+ private
223
+
224
+ def override_client_config(client)
225
+ # Update the client's configuration with command line options
226
+ config = client.instance_variable_get(:@config)
227
+ config.configuration_name = @options[:configuration_name] if @options[:configuration_name]
228
+ config.client_id = @options[:client_id] if @options[:client_id]
229
+
230
+ # Force recreation of the client with new config
231
+ client.instance_variable_set(:@client, nil)
232
+ end
233
+
234
+ def create_standalone_client
235
+ # Create a minimal machine-like object for standalone client
236
+ require 'ostruct'
237
+
238
+ config = VagrantPlugins::Eryph::Config.new
239
+ config.configuration_name = @options[:configuration_name] if @options[:configuration_name]
240
+ config.client_id = @options[:client_id] if @options[:client_id]
241
+ config.finalize!
242
+
243
+ # Create a fake machine with just the provider config we need
244
+ fake_machine = OpenStruct.new(
245
+ provider_config: config,
246
+ ui: @env.ui
247
+ )
248
+
249
+ Helpers::EryphClient.new(fake_machine)
250
+ rescue StandardError => e
251
+ raise "Failed to create Eryph client: #{e.message}. Please configure at least one machine with the Eryph provider or ensure your Eryph configuration is set up."
252
+ end
253
+
254
+ def delete_project(client, project_id)
255
+ operation = client.client.projects.projects_delete(project_id)
256
+ raise 'Failed to delete project: No operation returned' unless operation&.id
257
+
258
+ result = client.wait_for_operation(operation.id)
259
+ unless result.completed?
260
+ error_msg = result.status_message || 'Operation failed'
261
+ raise "Project deletion failed: #{error_msg}"
262
+ end
263
+ end
264
+ end
265
+
266
+ class NetworkCommand < Vagrant.plugin('2', :command)
267
+ def self.synopsis
268
+ 'manage project network configurations'
269
+ end
270
+
271
+ def initialize(argv, env)
272
+ super
273
+ @options = {}
274
+ @parser = OptionParser.new do |o|
275
+ o.banner = 'Usage: vagrant eryph network <subcommand> [options]'
276
+ o.separator ''
277
+ o.separator 'Subcommands:'
278
+ o.separator ' get Get project network configuration (YAML)'
279
+ o.separator ' set Set project network configuration from YAML'
280
+ o.separator ''
281
+ o.separator 'Global Options:'
282
+
283
+ o.on('--configuration-name NAME', String, 'Eryph configuration name (default: auto-detect)') do |name|
284
+ @options[:configuration_name] = name
285
+ end
286
+
287
+ o.on('--client-id ID', String, 'Eryph client ID') do |id|
288
+ @options[:client_id] = id
289
+ end
290
+
291
+ o.separator ''
292
+ end
293
+
294
+ @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
295
+ end
296
+
297
+ def execute
298
+ case @sub_command
299
+ when 'get'
300
+ execute_get
301
+ when 'set'
302
+ execute_set
303
+ else
304
+ @env.ui.info(@parser.help, prefix: false)
305
+ 1
306
+ end
307
+ end
308
+
309
+ private
310
+
311
+ def execute_get
312
+ parser = OptionParser.new do |o|
313
+ o.banner = 'Usage: vagrant eryph network get <project-name> [options]'
314
+ o.separator ''
315
+ o.separator 'Options:'
316
+ o.on('-o', '--output FILE', 'Write configuration to file') do |file|
317
+ @options[:output] = file
318
+ end
319
+ end
320
+
321
+ begin
322
+ argv = parser.parse!(@sub_args.dup)
323
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
324
+ @env.ui.error("#{e.message}")
325
+ @env.ui.info(parser.help, prefix: false)
326
+ return nil
327
+ end
328
+
329
+ if argv.empty?
330
+ @env.ui.error('Project name is required')
331
+ @env.ui.info(parser.help, prefix: false)
332
+ return 1
333
+ end
334
+
335
+ project_name = argv[0]
336
+ client = get_eryph_client
337
+
338
+ begin
339
+ project = client.get_project(project_name)
340
+ unless project
341
+ @env.ui.error("Project '#{project_name}' not found")
342
+ return 1
343
+ end
344
+
345
+ config_response = client.client.virtual_networks.virtual_networks_get_config(project.id)
346
+
347
+ if config_response&.configuration
348
+ # Configuration is already a Hash/Object, convert symbols to strings for clean YAML
349
+ clean_config = deep_stringify_keys(config_response.configuration)
350
+ yaml_config = clean_config.to_yaml
351
+
352
+ if @options[:output]
353
+ File.write(@options[:output], yaml_config)
354
+ @env.ui.info("Network configuration written to: #{@options[:output]}")
355
+ else
356
+ @env.ui.info("Network configuration for project '#{project.name}':", prefix: false)
357
+ @env.ui.info(yaml_config, prefix: false)
358
+ end
359
+ else
360
+ @env.ui.info("No network configuration found for project '#{project.name}'")
361
+ end
362
+ 0
363
+ rescue StandardError => e
364
+ @env.ui.error("Failed to get network configuration: #{e.message}")
365
+ 1
366
+ end
367
+ end
368
+
369
+ def execute_set
370
+ parser = OptionParser.new do |o|
371
+ o.banner = 'Usage: vagrant eryph network set <project-name> [options]'
372
+ o.separator ''
373
+ o.separator 'Options:'
374
+ o.on('-f', '--file FILE', 'Read configuration from file') do |file|
375
+ @options[:file] = file
376
+ end
377
+ o.on('-c', '--config CONFIG', 'Configuration as string') do |config|
378
+ @options[:config] = config
379
+ end
380
+ o.on('--force', 'Force import even if project names differ') do
381
+ @options[:force] = true
382
+ end
383
+ o.on('--no-wait', 'Do not wait for operation to complete') do
384
+ @options[:no_wait] = true
385
+ end
386
+ end
387
+
388
+ begin
389
+ argv = parser.parse!(@sub_args.dup)
390
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
391
+ @env.ui.error("#{e.message}")
392
+ @env.ui.info(parser.help, prefix: false)
393
+ return nil
394
+ end
395
+
396
+ if argv.empty?
397
+ @env.ui.error('Project name is required')
398
+ @env.ui.info(parser.help, prefix: false)
399
+ return 1
400
+ end
401
+
402
+ unless @options[:file] || @options[:config]
403
+ @env.ui.error('Either --file or --config is required')
404
+ @env.ui.info(parser.help, prefix: false)
405
+ return 1
406
+ end
407
+
408
+ project_name = argv[0]
409
+ client = get_eryph_client
410
+
411
+ begin
412
+ project = client.get_project(project_name)
413
+ unless project
414
+ @env.ui.error("Project '#{project_name}' not found")
415
+ return 1
416
+ end
417
+
418
+ # Read configuration
419
+ config_content = if @options[:file]
420
+ unless File.exist?(@options[:file])
421
+ @env.ui.error("File not found: #{@options[:file]}")
422
+ return 1
423
+ end
424
+ File.read(@options[:file])
425
+ else
426
+ @options[:config]
427
+ end
428
+
429
+ # Parse configuration
430
+ config_data = parse_network_config(config_content)
431
+ unless config_data
432
+ @env.ui.error('Invalid configuration format. Expected YAML or JSON.')
433
+ return 1
434
+ end
435
+
436
+ # Validate project name in config
437
+ if config_data['project'] && config_data['project'] != project_name
438
+ unless @options[:force]
439
+ response = @env.ui.ask("Configuration was exported from project '#{config_data['project']}' but will be imported to '#{project_name}'. Continue? (y/N)")
440
+ return 0 unless response.downcase.start_with?('y')
441
+ end
442
+ end
443
+
444
+ # Set project name in config
445
+ config_data['project'] = project_name
446
+
447
+ @env.ui.info("Setting network configuration for project '#{project.name}'...")
448
+
449
+ # Create request body - API expects Hash object directly
450
+ request_body = ::Eryph::ComputeClient::UpdateProjectNetworksRequestBody.new(
451
+ configuration: config_data
452
+ )
453
+
454
+ operation = client.client.virtual_networks.virtual_networks_update_config(
455
+ project.id,
456
+ request_body
457
+ )
458
+
459
+ raise 'Failed to update network configuration: No operation returned' unless operation&.id
460
+
461
+ unless @options[:no_wait]
462
+ result = client.wait_for_operation(operation.id)
463
+ unless result.completed?
464
+ error_msg = result.status_message || 'Operation failed'
465
+ raise "Network configuration update failed: #{error_msg}"
466
+ end
467
+ end
468
+
469
+ @env.ui.info("Network configuration updated successfully for project '#{project.name}'")
470
+ 0
471
+ rescue StandardError => e
472
+ @env.ui.error("Failed to set network configuration: #{e.message}")
473
+ 1
474
+ end
475
+ end
476
+
477
+ def get_eryph_client
478
+ # Try to get client from existing machines
479
+ @env.machine_names.each do |name|
480
+ machine = @env.machine(name, :eryph)
481
+ if machine.provider_config.is_a?(VagrantPlugins::Eryph::Config)
482
+ client = Helpers::EryphClient.new(machine)
483
+
484
+ # Override client configuration if command line options are provided
485
+ if @options[:configuration_name] || @options[:client_id]
486
+ override_client_config(client)
487
+ end
488
+
489
+ return client
490
+ end
491
+ end
492
+
493
+ # If no machines found, create a temporary config with command line options
494
+ create_standalone_client
495
+ end
496
+
497
+ private
498
+
499
+ def override_client_config(client)
500
+ # Update the client's configuration with command line options
501
+ config = client.instance_variable_get(:@config)
502
+ config.configuration_name = @options[:configuration_name] if @options[:configuration_name]
503
+ config.client_id = @options[:client_id] if @options[:client_id]
504
+
505
+ # Force recreation of the client with new config
506
+ client.instance_variable_set(:@client, nil)
507
+ end
508
+
509
+ def create_standalone_client
510
+ # Create a minimal machine-like object for standalone client
511
+ require 'ostruct'
512
+
513
+ config = VagrantPlugins::Eryph::Config.new
514
+ config.configuration_name = @options[:configuration_name] if @options[:configuration_name]
515
+ config.client_id = @options[:client_id] if @options[:client_id]
516
+ config.finalize!
517
+
518
+ # Create a fake machine with just the provider config we need
519
+ fake_machine = OpenStruct.new(
520
+ provider_config: config,
521
+ ui: @env.ui
522
+ )
523
+
524
+ Helpers::EryphClient.new(fake_machine)
525
+ rescue StandardError => e
526
+ raise "Failed to create Eryph client: #{e.message}. Please configure at least one machine with the Eryph provider or ensure your Eryph configuration is set up."
527
+ end
528
+
529
+ def parse_network_config(config_string)
530
+ # Handle encoding issues first
531
+ # Detect UTF-16LE content (null bytes between characters)
532
+ if config_string.include?("\u0000")
533
+ # This is UTF-16LE content read as UTF-8, convert it properly
534
+ clean_config = config_string.force_encoding('UTF-16LE').encode('UTF-8')
535
+ else
536
+ clean_config = config_string
537
+ end
538
+
539
+ clean_config = clean_config.strip
540
+ clean_config = clean_config.gsub(/\r\n/, "\n")
541
+
542
+ # Try JSON first
543
+ if clean_config.start_with?('{') && clean_config.end_with?('}')
544
+ begin
545
+ return JSON.parse(clean_config)
546
+ rescue JSON::ParserError
547
+ return nil
548
+ end
549
+ end
550
+
551
+ # Try YAML
552
+ begin
553
+ return YAML.safe_load(clean_config)
554
+ rescue Psych::SyntaxError
555
+ return nil
556
+ end
557
+ end
558
+
559
+ def deep_stringify_keys(obj)
560
+ case obj
561
+ when Hash
562
+ obj.each_with_object({}) do |(key, value), result|
563
+ result[key.to_s] = deep_stringify_keys(value)
564
+ end
565
+ when Array
566
+ obj.map { |item| deep_stringify_keys(item) }
567
+ else
568
+ obj
569
+ end
570
+ end
571
+ end
572
+ end
573
+ end