moose-inventory 1.0.8 → 2.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.
Files changed (104) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +49 -0
  3. data/.github/workflows/release.yml +58 -0
  4. data/.gitignore +1 -1
  5. data/.gitleaks.toml +9 -0
  6. data/.rubocop.yml +19 -784
  7. data/BACKLOG.md +290 -0
  8. data/Gemfile.lock +95 -0
  9. data/README.md +38 -9
  10. data/Rakefile +1 -1
  11. data/bin/moose-inventory +1 -1
  12. data/docs/release/publishing.md +109 -0
  13. data/docs/release/release-readiness.md +55 -0
  14. data/docs/security-audit-2026-05-21.md +71 -0
  15. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  16. data/docs/security-audit-2026-05-26.md +63 -0
  17. data/lib/moose_inventory/cli/formatter.rb +16 -17
  18. data/lib/moose_inventory/cli/group.rb +4 -1
  19. data/lib/moose_inventory/cli/group_add.rb +89 -75
  20. data/lib/moose_inventory/cli/group_addchild.rb +84 -71
  21. data/lib/moose_inventory/cli/group_addhost.rb +78 -69
  22. data/lib/moose_inventory/cli/group_addvar.rb +37 -37
  23. data/lib/moose_inventory/cli/group_get.rb +23 -26
  24. data/lib/moose_inventory/cli/group_list.rb +12 -15
  25. data/lib/moose_inventory/cli/group_listvars.rb +12 -14
  26. data/lib/moose_inventory/cli/group_rm.rb +104 -76
  27. data/lib/moose_inventory/cli/group_rmchild.rb +99 -54
  28. data/lib/moose_inventory/cli/group_rmhost.rb +64 -60
  29. data/lib/moose_inventory/cli/group_rmvar.rb +5 -5
  30. data/lib/moose_inventory/cli/helpers.rb +76 -0
  31. data/lib/moose_inventory/cli/host.rb +4 -1
  32. data/lib/moose_inventory/cli/host_add.rb +51 -66
  33. data/lib/moose_inventory/cli/host_addgroup.rb +77 -68
  34. data/lib/moose_inventory/cli/host_addvar.rb +6 -6
  35. data/lib/moose_inventory/cli/host_get.rb +15 -18
  36. data/lib/moose_inventory/cli/host_list.rb +3 -3
  37. data/lib/moose_inventory/cli/host_listvars.rb +21 -23
  38. data/lib/moose_inventory/cli/host_rm.rb +9 -9
  39. data/lib/moose_inventory/cli/host_rmgroup.rb +63 -60
  40. data/lib/moose_inventory/cli/host_rmvar.rb +3 -3
  41. data/lib/moose_inventory/config/config.rb +43 -40
  42. data/lib/moose_inventory/db/db.rb +92 -52
  43. data/lib/moose_inventory/db/models.rb +11 -12
  44. data/lib/moose_inventory/inventory_context.rb +50 -0
  45. data/lib/moose_inventory/operations/add_associations.rb +127 -0
  46. data/lib/moose_inventory/operations/add_groups.rb +115 -0
  47. data/lib/moose_inventory/operations/add_hosts.rb +110 -0
  48. data/lib/moose_inventory/operations/group_child_relations.rb +118 -0
  49. data/lib/moose_inventory/operations/group_cleanup.rb +55 -0
  50. data/lib/moose_inventory/operations/remove_associations.rb +101 -0
  51. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  52. data/lib/moose_inventory/version.rb +1 -1
  53. data/moose-inventory.gemspec +38 -20
  54. data/scripts/check.sh +10 -0
  55. data/scripts/ci/check_permissions.sh +35 -0
  56. data/scripts/ci/check_rubocop.sh +28 -0
  57. data/scripts/ci/check_secrets.sh +26 -0
  58. data/scripts/ci/check_security.sh +68 -0
  59. data/scripts/ci/install_security_tools.sh +47 -0
  60. data/scripts/ci/package_sanity.sh +46 -0
  61. data/scripts/files.rb +1 -4
  62. data/scripts/install_dependencies.sh +19 -0
  63. data/scripts/reports.sh +2 -2
  64. data/spec/lib/moose_inventory/cli/cli_spec.rb +13 -14
  65. data/spec/lib/moose_inventory/cli/group_add_spec.rb +118 -119
  66. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +49 -51
  67. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +80 -83
  68. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +91 -91
  69. data/spec/lib/moose_inventory/cli/group_get_spec.rb +22 -23
  70. data/spec/lib/moose_inventory/cli/group_list_spec.rb +19 -20
  71. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +35 -36
  72. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +115 -78
  73. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +86 -45
  74. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +43 -46
  75. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +131 -131
  76. data/spec/lib/moose_inventory/cli/group_spec.rb +9 -9
  77. data/spec/lib/moose_inventory/cli/host_add_spec.rb +103 -43
  78. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +78 -80
  79. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +122 -122
  80. data/spec/lib/moose_inventory/cli/host_get_spec.rb +16 -16
  81. data/spec/lib/moose_inventory/cli/host_list_spec.rb +8 -8
  82. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +50 -52
  83. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +12 -12
  84. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +48 -51
  85. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +136 -136
  86. data/spec/lib/moose_inventory/config/config_spec.rb +16 -3
  87. data/spec/lib/moose_inventory/db/db_spec.rb +386 -2
  88. data/spec/lib/moose_inventory/db/models_spec.rb +10 -11
  89. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +77 -0
  90. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +65 -0
  91. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +69 -0
  92. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +76 -0
  93. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +78 -0
  94. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +57 -0
  95. data/spec/shared/shared_config_setup.rb +2 -2
  96. data/spec/spec_helper.rb +7 -8
  97. metadata +157 -105
  98. data/.coveralls.yml +0 -0
  99. data/Guardfile +0 -38
  100. data/config/dotfiles/coveralls.yml +0 -0
  101. data/config/dotfiles/gitignore +0 -20
  102. data/config/dotfiles/rubocop.yml +0 -793
  103. data/scripts/guard_quality.sh +0 -3
  104. data/scripts/guard_test.sh +0 -2
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
 
4
- require_relative './formatter.rb'
5
- require_relative '../db/exceptions.rb'
6
+ require_relative 'formatter'
7
+ require_relative '../db/exceptions'
8
+ require_relative '../inventory_context'
9
+ require_relative '../operations/remove_associations'
6
10
 
7
11
  module Moose
8
12
  module Inventory
@@ -13,67 +17,66 @@ module Moose
13
17
  #==========================
14
18
  desc 'rmgroup HOSTNAME GROUPNAME [GROUPNAME ...]',
15
19
  'dissociation the host from a group'
16
- # rubocop:disable Metrics/LineLength
17
- def rmgroup(*args) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
18
- # rubocop:enable Metrics/LineLength
19
- if args.length < 2
20
- abort('ERROR: Wrong number of arguments, '\
21
- "#{args.length} for 2 or more.")
22
- end
23
-
24
- # Convenience
25
- db = Moose::Inventory::DB
26
- fmt = Moose::Inventory::Cli::Formatter
27
-
28
- # arguments
29
- name = args[0].downcase
30
- groups = args.slice(1, args.length - 1).uniq.map(&:downcase)
31
-
32
- # Sanity
33
- if groups.include?('ungrouped')
34
- abort 'ERROR: Cannot manually manipulate the automatic '\
35
- 'group \'ungrouped\'.'
36
- end
37
-
38
- # Transaction
39
- db.transaction do # Transaction start
40
- puts "Dissociate host '#{name}' from groups '#{groups.join(',')}':"
41
- fmt.puts 2, "- Retrieve host '#{name}'..."
42
- host = db.models[:host].find(name: name)
43
- if host.nil?
44
- fail db.exceptions[:moose],
45
- "The host '#{name}' was not found in the database."
46
- end
47
- fmt.puts 4, '- OK'
48
-
49
- # dissociate host from the groups
50
- groups_ds = host.groups_dataset
51
- groups.each do |g|
52
- fmt.puts 2, "- Remove association {host:#{name} <-> group:#{g}}..."
53
-
54
- # Check against existing associations
55
- if groups_ds[name: g].nil?
56
- fmt.warn "Association {host:#{name} <-> group:#{g}} doesn't exist, skipping.\n"
57
- fmt.puts 4, "- Doesn't exist, skipping."
58
- else
59
- group = db.models[:group].find(name: g)
60
- host.remove_group(group) unless group.nil?
61
- end
62
- fmt.puts 4, '- OK'
63
- end
64
-
65
- # Handle 'ungrouped' group automation
66
- if host.groups_dataset.count == 0
67
- fmt.puts 2, '- Add automatic association '\
68
- "{host:#{name} <-> group:ungrouped}..."
69
- ungrouped = db.models[:group].find_or_create(name: 'ungrouped')
70
- host.add_group(ungrouped) unless ungrouped.nil?
71
- fmt.puts 4, '- OK'
72
- end
20
+ def rmgroup(*args)
21
+ abort_if_missing_args(args, 2, '2 or more')
22
+
23
+ name = args[0].downcase
24
+ groups = normalize_names(args.slice(1, args.length - 1))
25
+
26
+ abort_if_automatic_group(groups)
27
+
28
+ context = Moose::Inventory::InventoryContext.new(db: db)
29
+ operation = Moose::Inventory::Operations::RemoveAssociations.new(context: context)
30
+
31
+ db.transaction do
32
+ puts "Dissociate host '#{name}' from groups '#{groups.join(',')}':"
33
+ host = fetch_existing_host_for_rmgroup(context, name)
34
+ render_host_rmgroup_events(
35
+ operation.host_from_groups(host: host, host_name: name, group_names: groups).events
36
+ )
73
37
  fmt.puts 2, '- All OK'
74
- end # End transaction
38
+ end
75
39
  puts 'Succeeded'
76
40
  end
41
+
42
+ private
43
+
44
+ def fetch_existing_host_for_rmgroup(context, name)
45
+ fmt.puts 2, "- Retrieve host '#{name}'..."
46
+ host = context.find_host(name)
47
+ raise db.exceptions[:moose], "The host '#{name}' was not found in the database." if host.nil?
48
+
49
+ fmt.puts 4, '- OK'
50
+ host
51
+ end
52
+
53
+ def render_host_rmgroup_events(events)
54
+ events.each { |event| render_host_rmgroup_event(event) }
55
+ end
56
+
57
+ def render_host_rmgroup_event(event)
58
+ payload = event.payload
59
+
60
+ return render_host_rmgroup_warning(payload) if event.type == :host_group_association_missing
61
+ return render_host_rmgroup_missing(payload) if event.type == :missing_skipping
62
+
63
+ case event.type
64
+ when :removing_host_group_association
65
+ fmt.puts 2, "- Remove association {host:#{payload[:host]} <-> group:#{payload[:group]}}..."
66
+ when :adding_automatic_group
67
+ fmt.puts 2, "- Add automatic association {host:#{payload[:host]} <-> group:ungrouped}..."
68
+ when :ok
69
+ fmt.puts payload[:indent], '- OK'
70
+ end
71
+ end
72
+
73
+ def render_host_rmgroup_warning(payload)
74
+ fmt.warn "Association {host:#{payload[:host]} <-> group:#{payload[:group]}} doesn't exist, skipping.\n"
75
+ end
76
+
77
+ def render_host_rmgroup_missing(payload)
78
+ fmt.puts payload[:indent], "- Doesn't exist, skipping."
79
+ end
77
80
  end
78
81
  end
79
82
  end
@@ -13,7 +13,7 @@ module Moose
13
13
  #==========================
14
14
  desc 'rmvar', 'Remove a variable from the host'
15
15
  # rubocop:disable Metrics/LineLength
16
- def rmvar(*args) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
16
+ def rmvar(*args) # rubocop:disable Metrics/AbcSize
17
17
  # rubocop:enableMetrics/LineLength
18
18
  if args.length < 2
19
19
  abort('ERROR: Wrong number of arguments, ' \
@@ -30,8 +30,8 @@ module Moose
30
30
 
31
31
  # Transaction
32
32
  db.transaction do # Transaction start
33
- puts "Remove variable(s) '#{vars.join(",")}' from host '#{name}':"
34
-
33
+ puts "Remove variable(s) '#{vars.join(',')}' from host '#{name}':"
34
+
35
35
  fmt.puts 2, "- retrieve host '#{name}'..."
36
36
  host = db.models[:host].find(name: name)
37
37
  if host.nil?
@@ -41,19 +41,19 @@ module Moose
41
41
  # Default is to search standard locations.
42
42
  #
43
43
  # --env ENV => sets the section to be used as the configuration.
44
- # Defaults to "", which forces the use of the
45
- # defaultenv parameter from the general section of
46
- # the config file.
44
+ # Defaults to "", which forces the use of the
45
+ # defaultenv parameter from the general section of
46
+ # the config file.
47
47
  #
48
48
  # --format FORMAT=> See formatter for supported types.
49
49
  # Defaults to json.
50
50
  #
51
51
  # -- trace => Enable more complete exceptions for db transactions
52
- # Default is not to trace.
52
+ # Default is not to trace.
53
53
 
54
54
  @_confopts = { env: '', format: 'json', ansible: false, trace: false }
55
55
 
56
- # Check for two-part flags
56
+ # Check for two-part flags
57
57
  %w(config env format).each do |var|
58
58
  @_argv.each_with_index do |val, index|
59
59
  next if val != "--#{var}"
@@ -62,7 +62,7 @@ module Moose
62
62
  break
63
63
  end
64
64
  end
65
-
65
+
66
66
  # Check for one-part flags
67
67
  %w(ansible trace).each do |var|
68
68
  @_argv.each_with_index do |val, index|
@@ -72,59 +72,58 @@ module Moose
72
72
  break
73
73
  end
74
74
  end
75
-
75
+
76
76
  # Sanity
77
- # - Ansible output format must be json - pjson is permitted, but yaml is not.
77
+ # - Ansible output format must be json - pjson is permitted, but yaml is not.
78
78
  if @_confopts[:ansible] == true
79
- unless @_confopts[:format] =~ /p|pjson|j|json/
79
+ unless @_confopts[:format] =~ /p|pjson|j|json/
80
80
  @_confopts[:format] = 'json'
81
- end
81
+ end
82
82
  end
83
-
84
83
  end
85
84
 
86
85
  #----------------------
87
86
  def self.top_level_help
88
87
  if @_argv[0] == 'help'
89
- puts "Global flags:"
90
- printf " %-31s %-10s", "--ansible", "# Force Ansible mode (automatically set when using ansible flags)\n"
91
- printf " %-31s %-10s", "--config FILE", "# Specifies a configuration file to use\n"
92
- printf " %-31s %-10s", "--env ENV", "# Specifies the environment section of the config to use\n"
93
- printf " %-31s %-10s", "--format yaml|json|pjson", "# Format for the output of 'get', 'list', and 'listvars' subcommands\n"
94
- printf " %-31s %-10s", "--trace", "# Enable more complete exception dumps for database transactions\n"
88
+ puts 'Global flags:'
89
+ printf ' %-31s %-10s', '--ansible', "# Force Ansible mode (automatically set when using ansible flags)\n"
90
+ printf ' %-31s %-10s', '--config FILE', "# Specifies a configuration file to use\n"
91
+ printf ' %-31s %-10s', '--env ENV', "# Specifies the environment section of the config to use\n"
92
+ printf ' %-31s %-10s', '--format yaml|json|pjson', "# Format for the output of 'get', 'list', and 'listvars' subcommands\n"
93
+ printf ' %-31s %-10s', '--trace', "# Enable more complete exception dumps for database transactions\n"
95
94
  puts "\nAnsible flags:"
96
- printf " %-31s %-10s", "--host HOSTNAME", "# Retrieves host variables for the specified host (alias for 'host listvars HOSTNAME')\n"
97
- printf " %-31s %-10s", "--list", "# Retrieves the list of groups (alias for 'group list')\n\n"
95
+ printf ' %-31s %-10s', '--host HOSTNAME', "# Retrieves host variables for the specified host (alias for 'host listvars HOSTNAME')\n"
96
+ printf ' %-31s %-10s', '--list', "# Retrieves the list of groups (alias for 'group list')\n\n"
98
97
  end
99
98
  end
100
-
99
+
101
100
  #----------------------
102
- def self.ansible_args # rubocop:disable Metrics/AbcSize
101
+ def self.ansible_args
103
102
  #
104
103
  # See http://docs.ansible.com/developing_inventory.html for Ansible specs
105
104
  # for dynamic inventory sources
106
-
105
+
107
106
  # --list => group list
108
107
  # --host HOSTNAME => host getvars HOSTNAME
109
108
 
110
109
  case @_argv[0]
111
- when '--list'
112
- @_confopts[:ansible] = true
113
- @_confopts[:format] = 'json' unless @_confopts[:format] =~ /p|pjson|j|json/
114
- @_argv.clear
115
- @_argv.concat(['group', 'list']).flatten
116
- when '--host'
117
- @_confopts[:ansible] = true
118
- @_confopts[:format] = 'json' unless @_confopts[:format] =~ /p|pjson|j|json/
119
- host = @_argv[1]
120
- @_argv.clear
121
- @_argv.concat(['host', 'listvars', "#{host}"]).flatten
122
- end
110
+ when '--list'
111
+ @_confopts[:ansible] = true
112
+ @_confopts[:format] = 'json' unless @_confopts[:format] =~ /p|pjson|j|json/
113
+ @_argv.clear
114
+ @_argv.concat(%w(group list)).flatten
115
+ when '--host'
116
+ @_confopts[:ansible] = true
117
+ @_confopts[:format] = 'json' unless @_confopts[:format] =~ /p|pjson|j|json/
118
+ host = @_argv[1]
119
+ @_argv.clear
120
+ @_argv.concat(['host', 'listvars', host.to_s]).flatten
121
+ end
123
122
  end
124
123
 
125
124
  #----------------------
126
- def self.resolve_config_file # rubocop:disable Metrics/AbcSize
127
- if ! @_confopts[:config].nil?
125
+ def self.resolve_config_file
126
+ if !@_confopts[:config].nil?
128
127
  path = File.expand_path(@_confopts[:config])
129
128
  if File.exist?(path)
130
129
  @_confopts[:config] = path
@@ -135,8 +134,7 @@ module Moose
135
134
  possibles = ['./.moose-tools/inventory/config',
136
135
  '~/.moose-tools/inventory/config',
137
136
  '~/local/etc/moose-tools/inventory/config',
138
- '/etc/moose-tools/inventory/config'
139
- ]
137
+ '/etc/moose-tools/inventory/config']
140
138
  possibles.each do |f|
141
139
  file = File.expand_path(f)
142
140
  @_confopts[:config] = file if File.exist?(file)
@@ -170,7 +168,12 @@ module Moose
170
168
  # rubocop:disable PerceivedComplexity
171
169
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
172
170
  def self.load
173
- newsets = symbolize_keys(YAML.load_file(@_confopts[:config]))
171
+ newsets = symbolize_keys(YAML.safe_load_file(
172
+ @_confopts[:config],
173
+ aliases: false,
174
+ permitted_classes: [],
175
+ permitted_symbols: []
176
+ ))
174
177
 
175
178
  path = @_confopts[:config]
176
179
 
@@ -184,7 +187,7 @@ module Moose
184
187
  env = @_confopts[:env]
185
188
  @_settings[:config] = newsets[@_confopts[:env].to_sym]
186
189
  else
187
- env = @_settings[:general][:defaultenv]
190
+ env = @_settings[:general][:defaultenv]
188
191
  (env.nil? || env.empty?) && fail("No defaultenv set in #{path}")
189
192
  @_settings[:config] = newsets[env.to_sym]
190
193
  end
@@ -22,6 +22,8 @@ module Moose
22
22
 
23
23
  #----------------------
24
24
  def self.init
25
+ init_exceptions
26
+
25
27
  # If we allow init more than once, then the db connection is remade,
26
28
  # which changes Sequel:DATABASES[0], thereby invalidating the sequel
27
29
  # models. This causes unexpected behavour. That is to say, because
@@ -31,8 +33,8 @@ module Moose
31
33
  # So, we allow init only once, gated by whether @db is nil. In effect,
32
34
  # this means we pool the DB connection for the life of the application.
33
35
  # Again, not a problem for our one-shot app, but it may be an issue in
34
- # long-running code. Personally, I don't like this pooling regime -
35
- # perhaps I'm not understanding how it's supposed to be used?
36
+ # long-running code. Personally, I don't like this pooling regime -
37
+ # perhaps I'm not understanding how it's supposed to be used?
36
38
  #
37
39
  # QUESTION: can the models be refreshed, to make then again valid? What if
38
40
  # we "load" instead of "require" the models?
@@ -57,55 +59,58 @@ module Moose
57
59
  @models[:group] = Moose::Inventory::DB::Group
58
60
  @models[:groupvar] = Moose::Inventory::DB::Groupvar
59
61
 
60
- @exceptions = {}
61
- @exceptions[:moose] = Moose::Inventory::DB::MooseDBException
62
+ end
62
63
 
64
+ #--------------------
65
+ def self.init_exceptions
66
+ @exceptions ||= {}
67
+ @exceptions[:moose] ||= Moose::Inventory::DB::MooseDBException
63
68
  end
64
69
 
65
70
  #--------------------
66
71
  def self.transaction
67
72
  fail('Database connection has not been established') if @db.nil?
68
-
73
+
69
74
  tries = 0
70
-
75
+
71
76
  begin
72
77
  @db.transaction(savepoint: true) do
73
78
  yield
74
79
  end
75
80
 
76
81
  rescue Sequel::DatabaseError => e
77
- # We want to rescue Sqlite3::BusyException. But, sequel catches that
78
- # and re-raises it as Sequel::DatabaseError, with a message referencing
79
- # the original exception class
80
-
81
- # We look into e, to see whether it is a BusyException. If not,
82
- # we re-raise immediately.
83
- raise unless e.message.include?("BusyException")
84
-
85
- # Looks like a BusyException, so we retry, after a random delay.
86
- tries += 1
87
- case tries
88
- when 1..10
89
- if Moose::Inventory::Config._confopts[:trace] == true
90
- STDERR.puts e.message
82
+ # We want to rescue Sqlite3::BusyException. But, sequel catches that
83
+ # and re-raises it as Sequel::DatabaseError, with a message referencing
84
+ # the original exception class
85
+
86
+ # We look into e, to see whether it is a BusyException. If not,
87
+ # we re-raise immediately.
88
+ raise unless e.message.include?('BusyException')
89
+
90
+ # Looks like a BusyException, so we retry, after a random delay.
91
+ tries += 1
92
+ case tries
93
+ when 1..10
94
+ if Moose::Inventory::Config._confopts[:trace] == true
95
+ $stderr.puts e.message
96
+ end
97
+ sleep rand
98
+ retry
99
+ else
100
+ warn('The database appears to be locked by another process, and '\
101
+ " did not become free after #{tries} tries. Giving up. ")
102
+
103
+ # TODO: Some useful advice to the user, as to what to do about this error.
104
+ raise
91
105
  end
92
- sleep rand()
93
- retry
94
- else
95
- warn('The database appears to be locked by another process, and '\
96
- " did not become free after #{tries} tries. Giving up. ")
97
106
 
98
- # TODO: Some useful advice to the user, as to what to do about this error.
99
- raise
100
- end
101
-
102
107
  rescue @exceptions[:moose] => e
103
108
  warn 'An error occurred during a transaction, any changes have been rolled back.'
104
109
 
105
110
  if Moose::Inventory::Config._confopts[:trace] == true
106
- STDERR.puts $!.backtrace
107
- abort("ERROR: #{e}")
108
- else
111
+ $stderr.puts e.full_message(highlight: false, order: :top)
112
+ abort("ERROR: #{e.message}")
113
+ else
109
114
  abort("ERROR: #{e.message}")
110
115
  end
111
116
 
@@ -158,7 +163,7 @@ module Moose
158
163
 
159
164
  else
160
165
  @db.drop_table(:hosts, :hostvars,
161
- :groups, :groupvars, :group_hosts,
166
+ :groups, :groupvars, :group_hosts,
162
167
  if_exists: true, cascade: true)
163
168
  end
164
169
  end
@@ -195,7 +200,7 @@ module Moose
195
200
  foreign_key :child_id, :groups
196
201
  end
197
202
  end
198
-
203
+
199
204
  unless @db.table_exists? :groupvars
200
205
  @db.create_table(:groupvars) do
201
206
  primary_key :id
@@ -225,28 +230,29 @@ module Moose
225
230
  when 'sqlite3'
226
231
  init_sqlite3
227
232
 
228
- when 'msqsql'
233
+ when 'mysql'
229
234
  init_mysql
230
235
 
231
236
  when 'postgresql'
232
237
  init_postgresql
233
238
 
234
239
  else
235
- fail @exceptions[:moose ],
236
- "database adapter #{adapter} is not yet supported."
240
+ fail @exceptions[:moose],
241
+ "database adapter #{adapter} is not yet supported."
237
242
  end
238
243
  end
239
244
 
240
245
  #--------------------
241
246
  def self.init_sqlite3 # rubocop:disable Metrics/AbcSize
242
247
  require 'sqlite3'
248
+ require 'fileutils'
243
249
 
244
250
  # Quick check that expected keys are at least present & sensible
245
251
  config = Moose::Inventory::Config._settings[:config][:db]
246
252
  [:file].each do |key|
247
253
  if config[key].nil?
248
- fail @exceptions[:moose ],
249
- "Expected key #{key} missing in sqlite3 configuration"
254
+ fail @exceptions[:moose],
255
+ "Expected key #{key} missing in sqlite3 configuration"
250
256
  end
251
257
  end
252
258
  config[:file].empty? && fail("SQLite3 DB 'file' cannot be empty")
@@ -254,7 +260,7 @@ module Moose
254
260
  # Make sure the directory exists
255
261
  dbfile = File.expand_path(config[:file])
256
262
  dbdir = File.dirname(dbfile)
257
- Dir.mkdir(dbdir) unless Dir.exist?(dbdir)
263
+ FileUtils.mkdir_p(dbdir) unless Dir.exist?(dbdir)
258
264
 
259
265
  # Create and/or open the database file
260
266
  @db = Sequel.sqlite(dbfile)
@@ -262,26 +268,60 @@ module Moose
262
268
 
263
269
  #--------------------
264
270
  def self.init_mysql
265
- require 'mysql'
271
+ require 'mysql2'
266
272
 
267
- # TODO: native MySQL driver vs the pure ruby one?
268
- # Sequel requires the native on.
269
- # gem('mysql')
273
+ # Quick check that expected keys are at least present
274
+ config = Moose::Inventory::Config._settings[:config][:db]
275
+ [:host, :database, :user].each do |key|
276
+ if config[key].nil?
277
+ fail @exceptions[:moose],
278
+ "Expected key #{key} missing in mysql configuration"
279
+ end
280
+ end
281
+ password = db_password(config, 'mysql')
282
+
283
+ @db = Sequel.mysql2(user: config[:user],
284
+ password: password,
285
+ host: config[:host],
286
+ database: config[:database])
287
+ end
288
+
289
+ #--------------------
290
+ def self.init_postgresql
291
+ require 'pg'
270
292
 
271
293
  # Quick check that expected keys are at least present
272
294
  config = Moose::Inventory::Config._settings[:config][:db]
273
- [:host, :database, :user, :password].each do |key|
295
+ [:host, :database, :user].each do |key|
274
296
  if config[key].nil?
275
- fail @exceptions[:moose ],
276
- "Expected key #{key} missing in mysql configuration"
297
+ fail @exceptions[:moose],
298
+ "Expected key #{key} missing in postgresql configuration"
277
299
  end
278
300
  end
301
+ password = db_password(config, 'postgresql')
302
+
303
+ @db = Sequel.postgres(user: config[:user],
304
+ password: password,
305
+ host: config[:host],
306
+ database: config[:database])
307
+ end
308
+
309
+ #--------------------
310
+ def self.db_password(config, adapter)
311
+ return config[:password] unless config[:password].nil?
312
+
313
+ if config[:password_env].nil?
314
+ fail @exceptions[:moose],
315
+ "Expected key password or password_env missing in #{adapter} configuration"
316
+ end
317
+
318
+ password = ENV.fetch(config[:password_env].to_s, nil)
319
+ if password.nil? || password.empty?
320
+ fail @exceptions[:moose],
321
+ "Environment variable #{config[:password_env]} is not set for #{adapter} password"
322
+ end
279
323
 
280
- @db = Sequel.mysql(user: config[:user],
281
- password: config[:password],
282
- host: config[:host],
283
- database: config[:database]
284
- )
324
+ password
285
325
  end
286
326
  end
287
327
  end
@@ -11,20 +11,19 @@ module Moose
11
11
  ##
12
12
  # Model for the groups table
13
13
  class Group < Sequel::Model
14
-
15
- many_to_many :parents,
16
- :left_key=>:parent_id,
17
- :right_key=>:child_id,
18
- :class=>self
19
-
20
- many_to_many :children,
21
- :left_key=>:child_id,
22
- :right_key=>:parent_id,
23
- :class=>self
14
+ many_to_many :parents,
15
+ left_key: :parent_id,
16
+ right_key: :child_id,
17
+ class: self
24
18
 
25
- many_to_many :hosts
19
+ many_to_many :children,
20
+ left_key: :child_id,
21
+ right_key: :parent_id,
22
+ class: self
23
+
24
+ many_to_many :hosts
26
25
  one_to_many :groupvars
27
- end
26
+ end
28
27
 
29
28
  ##
30
29
  # Model for the hostvars table
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ ##
6
+ # Thin facade over the current DB singleton.
7
+ #
8
+ # This gives new operation/service objects a small inventory-facing seam
9
+ # without forcing the legacy CLI to stop using the DB singleton all at once.
10
+ class InventoryContext
11
+ AUTOMATIC_GROUP = 'ungrouped'
12
+
13
+ def initialize(db: Moose::Inventory::DB)
14
+ @db = db
15
+ end
16
+
17
+ def transaction(&)
18
+ db.transaction(&)
19
+ end
20
+
21
+ def find_host(name)
22
+ db.models[:host].find(name: name)
23
+ end
24
+
25
+ def create_host(name)
26
+ db.models[:host].create(name: name)
27
+ end
28
+
29
+ def find_group(name)
30
+ db.models[:group].find(name: name)
31
+ end
32
+
33
+ def create_group(name)
34
+ db.models[:group].create(name: name)
35
+ end
36
+
37
+ def find_or_create_group(name)
38
+ db.models[:group].find_or_create(name: name)
39
+ end
40
+
41
+ def automatic_group
42
+ find_or_create_group(AUTOMATIC_GROUP)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :db
48
+ end
49
+ end
50
+ end