moose-inventory 1.0.9 → 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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +58 -0
  4. data/.gitleaks.toml +9 -0
  5. data/.rubocop.yml +28 -0
  6. data/BACKLOG.md +130 -24
  7. data/Gemfile.lock +36 -1
  8. data/README.md +26 -6
  9. data/Rakefile +1 -1
  10. data/docs/release/publishing.md +44 -48
  11. data/docs/release/release-readiness.md +14 -0
  12. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  13. data/docs/security-audit-2026-05-26.md +63 -0
  14. data/lib/moose_inventory/cli/group.rb +3 -0
  15. data/lib/moose_inventory/cli/group_add.rb +89 -73
  16. data/lib/moose_inventory/cli/group_addchild.rb +77 -60
  17. data/lib/moose_inventory/cli/group_addhost.rb +78 -65
  18. data/lib/moose_inventory/cli/group_rm.rb +101 -71
  19. data/lib/moose_inventory/cli/group_rmchild.rb +99 -53
  20. data/lib/moose_inventory/cli/group_rmhost.rb +64 -56
  21. data/lib/moose_inventory/cli/helpers.rb +76 -0
  22. data/lib/moose_inventory/cli/host.rb +3 -0
  23. data/lib/moose_inventory/cli/host_add.rb +47 -62
  24. data/lib/moose_inventory/cli/host_addgroup.rb +73 -64
  25. data/lib/moose_inventory/cli/host_rmgroup.rb +58 -55
  26. data/lib/moose_inventory/db/db.rb +27 -7
  27. data/lib/moose_inventory/inventory_context.rb +50 -0
  28. data/lib/moose_inventory/operations/add_associations.rb +127 -0
  29. data/lib/moose_inventory/operations/add_groups.rb +115 -0
  30. data/lib/moose_inventory/operations/add_hosts.rb +110 -0
  31. data/lib/moose_inventory/operations/group_child_relations.rb +118 -0
  32. data/lib/moose_inventory/operations/group_cleanup.rb +55 -0
  33. data/lib/moose_inventory/operations/remove_associations.rb +101 -0
  34. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  35. data/lib/moose_inventory/version.rb +1 -1
  36. data/moose-inventory.gemspec +3 -0
  37. data/scripts/check.sh +2 -0
  38. data/scripts/ci/check_permissions.sh +3 -0
  39. data/scripts/ci/check_rubocop.sh +28 -0
  40. data/scripts/ci/check_secrets.sh +26 -0
  41. data/scripts/ci/check_security.sh +18 -0
  42. data/scripts/ci/install_security_tools.sh +47 -0
  43. data/scripts/install_dependencies.sh +2 -0
  44. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +40 -0
  45. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +45 -0
  46. data/spec/lib/moose_inventory/db/db_spec.rb +162 -0
  47. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +77 -0
  48. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +65 -0
  49. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +69 -0
  50. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +76 -0
  51. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +78 -0
  52. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +57 -0
  53. metadata +90 -1
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
- require_relative './formatter.rb'
4
+ require_relative 'formatter'
5
+ require_relative '../inventory_context'
6
+ require_relative '../operations/add_associations'
3
7
 
4
8
  module Moose
5
9
  module Inventory
@@ -10,82 +14,91 @@ module Moose
10
14
  #==========================
11
15
  desc 'addhost NAME HOSTNAME',
12
16
  'Associate a host HOSTNAME with the group NAME'
13
- def addhost(*args) # rubocop:disable Metrics/AbcSize
14
- # Sanity
15
- if args.length < 2
16
- abort("ERROR: Wrong number of arguments, #{args.length} "\
17
- 'for 2 or more.')
18
- end
17
+ def addhost(*args)
18
+ abort_if_missing_args(args, 2, '2 or more')
19
+
20
+ name = args[0].downcase
21
+ hosts = normalize_names(args.slice(1, args.length - 1))
22
+
23
+ abort_if_automatic_group([name])
19
24
 
20
- # Arguments
21
- name = args[0].downcase
22
- hosts = args.slice(1, args.length - 1).uniq.map(&:downcase)
25
+ result = add_hosts_to_group(name, hosts)
23
26
 
24
- # Sanity
25
- if name == 'ungrouped'
26
- abort("ERROR: Cannot manually manipulate the automatic group 'ungrouped'.")
27
+ if result.warning_count.zero?
28
+ puts 'Succeeded.'
29
+ else
30
+ puts 'Succeeded, with warnings.'
27
31
  end
32
+ end
28
33
 
29
- # Convenience
30
- db = Moose::Inventory::DB
31
- fmt = Moose::Inventory::Cli::Formatter
34
+ private
35
+
36
+ def add_hosts_to_group(name, hosts)
37
+ context = Moose::Inventory::InventoryContext.new(db: db)
38
+ operation = Moose::Inventory::Operations::AddAssociations.new(context: context)
32
39
 
33
- # Transaction
34
- warn_count = 0
35
40
  begin
36
- db.transaction do # Transaction start
41
+ db.transaction do
37
42
  puts "Associate group '#{name}' with host(s) '#{hosts.join(',')}':"
38
- # Get the target group
39
- fmt.puts 2, "- retrieve group '#{name}'..."
40
- group = db.models[:group].find(name: name)
41
- abort("ERROR: The group '#{name}' does not exist.") if group.nil?
42
- fmt.puts 4, '- OK'
43
-
44
- # Associate group with the hosts
45
- ungrouped = db.models[:group].find_or_create(name: 'ungrouped')
46
- hosts_ds = group.hosts_dataset
47
- hosts.each do |h|
48
- fmt.puts 2, "- add association {group:#{name} <-> host:#{h}}..."
49
-
50
- # Check against existing associations
51
- unless hosts_ds[name: h].nil?
52
- warn_count += 1
53
- fmt.warn "Association {group:#{name} <-> host:#{h}} already"\
54
- " exists, skipping.\n"
55
- fmt.puts 4, '- already exists, skipping.'
56
- fmt.puts 4, '- OK'
57
- next
58
- end
59
-
60
- # Add new association
61
- host = db.models[:host].find(name: h)
62
- if host.nil?
63
- warn_count += 1
64
- fmt.warn "Host '#{h}' does not exist and will be created.\n"
65
- fmt.puts 4, '- host does not exist, creating now...'
66
- host = db.models[:host].create(name: h)
67
- fmt.puts 6, '- OK'
68
- end
69
-
70
- group.add_host(host)
71
- fmt.puts 4, '- OK'
72
-
73
- # Remove the host from the ungrouped group, if necessary
74
- next if host.groups_dataset[name: 'ungrouped'].nil?
75
- fmt.puts 2, '- remove automatic association '\
76
- "{group:ungrouped <-> host:#{h}}..."
77
- host.remove_group(ungrouped)
78
- fmt.puts 4, '- OK'
79
- end
43
+ group = fetch_existing_group_for_addhost(context, name)
44
+ result = operation.group_to_hosts(group: group, group_name: name, host_names: hosts)
45
+ render_group_addhost_events(result.events)
80
46
  fmt.puts 2, '- all OK'
81
- end # Transaction end
47
+ return result
48
+ end
82
49
  rescue db.exceptions[:moose] => e
83
50
  abort("ERROR: #{e.message}")
84
51
  end
85
- if warn_count == 0
86
- puts 'Succeeded.'
52
+ end
53
+
54
+ def fetch_existing_group_for_addhost(context, name)
55
+ fmt.puts 2, "- retrieve group '#{name}'..."
56
+ group = context.find_group(name)
57
+ abort("ERROR: The group '#{name}' does not exist.") if group.nil?
58
+
59
+ fmt.puts 4, '- OK'
60
+ group
61
+ end
62
+
63
+ def render_group_addhost_events(events)
64
+ events.each { |event| render_group_addhost_event(event) }
65
+ end
66
+
67
+ def render_group_addhost_event(event)
68
+ payload = event.payload
69
+
70
+ return render_group_addhost_warning(event.type, payload) if group_addhost_warning?(event.type)
71
+ return render_group_addhost_status(payload) if event.type == :already_exists_skipping
72
+
73
+ render_group_addhost_output(event.type, payload)
74
+ end
75
+
76
+ def group_addhost_warning?(type)
77
+ %i[group_host_association_exists host_missing_created].include?(type)
78
+ end
79
+
80
+ def render_group_addhost_warning(type, payload)
81
+ if type == :group_host_association_exists
82
+ fmt.warn "Association {group:#{payload[:group]} <-> host:#{payload[:host]}} already exists, skipping.\n"
87
83
  else
88
- puts 'Succeeded, with warnings.'
84
+ fmt.warn "Host '#{payload[:name]}' does not exist and will be created.\n"
85
+ end
86
+ end
87
+
88
+ def render_group_addhost_status(payload)
89
+ fmt.puts payload[:indent], '- already exists, skipping.'
90
+ end
91
+
92
+ def render_group_addhost_output(type, payload)
93
+ case type
94
+ when :adding_group_host_association
95
+ fmt.puts 2, "- add association {group:#{payload[:group]} <-> host:#{payload[:host]}}..."
96
+ when :host_creating_now
97
+ fmt.puts 4, '- host does not exist, creating now...'
98
+ when :removing_automatic_group
99
+ fmt.puts 2, "- remove automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
100
+ when :ok
101
+ fmt.puts payload[:indent], '- OK'
89
102
  end
90
103
  end
91
104
  end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
- require_relative './formatter.rb'
4
+ require_relative 'formatter'
5
+ require_relative '../inventory_context'
6
+ require_relative '../operations/remove_groups'
3
7
 
4
8
  module Moose
5
9
  module Inventory
@@ -8,84 +12,110 @@ module Moose
8
12
  # Implementation of "group rm" methods of the CLI
9
13
  class Group
10
14
  #==========================
15
+ option :recursive,
16
+ type: :boolean,
17
+ default: false,
18
+ desc: 'Also delete child groups that become orphaned'
11
19
  desc 'rm NAME',
12
20
  'Remove a group NAME from the inventory'
13
- def rm(*argv) # rubocop:disable Metrics/AbcSize
14
- #
15
- # Sanity
16
- if argv.empty?
17
- abort('ERROR: Wrong number of arguments, '\
18
- "#{argv.length} for 1 or more.")
19
- end
20
-
21
- # Convenience
22
- db = Moose::Inventory::DB
23
- fmt = Moose::Inventory::Cli::Formatter
24
-
25
- # Arguments
26
- names = argv.uniq.map(&:downcase)
27
-
28
- # sanity
29
- if names.include?('ungrouped')
30
- abort("Cannot manually manipulate the automatic group 'ungrouped'\n")
31
- end
21
+ def rm(*argv)
22
+ abort_if_missing_args(argv, 1, '1 or more')
23
+
24
+ names = normalize_names(argv)
25
+
26
+ abort_if_automatic_group(
27
+ names,
28
+ "Cannot manually manipulate the automatic group 'ungrouped'\n"
29
+ )
32
30
 
33
- # Transaction
34
- warn_count = 0
35
- db.transaction do # Transaction start
36
- names.each do |name|
37
- puts "Remove group '#{name}':"
38
- fmt.puts 2, "- Retrieve group '#{name}'..."
39
- group = db.models[:group].find(name: name)
40
- if group.nil?
41
- warn_count += 1
42
- fmt.warn "Group '#{name}' does not exist, skipping.\n"
43
- fmt.puts 4, '- No such group, skipping.'
44
- end
45
- fmt.puts 4, '- OK'
46
- unless group.nil?
47
- # Dissociate from any parent groups
48
- pgroups_ds = group.parents_dataset
49
- pgroups_ds.each do |parent|
50
- fmt.puts 2, "- Remove association {group:#{name} <-> group:#{parent.name}}..."
51
- parent.remove_child(group)
52
- fmt.puts 4, '- OK'
53
- end
54
-
55
- # Dissociate from any child groups
56
- groups_ds = group.children_dataset
57
- groups_ds.each do |child|
58
- fmt.puts 2, "- Remove association {group:#{name} <-> group:#{child.name}}..."
59
- group.remove_child(child)
60
- # TODO: Should we propagate the delete to orphaned children?
61
- fmt.puts 4, '- OK'
62
- end
63
-
64
- # Handle automatic group for any associated hosts
65
- hosts_ds = group.hosts_dataset
66
- hosts_ds.each do |host|
67
- host_groups_ds = host.groups_dataset
68
- next unless host_groups_ds.count == 1 # We're the only group
69
- fmt.puts 2, "- Adding automatic association {group:ungrouped <-> host:#{host[:name]}}..."
70
- ungrouped = db.models[:group].find_or_create(name: 'ungrouped')
71
- host.add_group(ungrouped)
72
- fmt.puts 4, '- OK'
73
- end
74
- # Remove the group
75
- fmt.puts 2, "- Destroy group '#{name}'..."
76
- group.remove_all_hosts
77
- group.destroy
78
- fmt.puts 4, '- OK'
79
- end
80
- fmt.puts 2, '- All OK'
81
- end
82
- end # Transaction end
83
- if warn_count == 0
31
+ result = remove_groups(names)
32
+
33
+ if result.warning_count.zero?
84
34
  puts 'Succeeded.'
85
35
  else
86
36
  puts 'Succeeded, with warnings.'
87
37
  end
88
38
  end
39
+
40
+ private
41
+
42
+ def remove_groups(names)
43
+ context = Moose::Inventory::InventoryContext.new(db: db)
44
+ operation = Moose::Inventory::Operations::RemoveGroups.new(context: context)
45
+
46
+ db.transaction do
47
+ result = operation.call(names: names, recursive: options[:recursive])
48
+ render_group_rm_events(result.events)
49
+ return result
50
+ end
51
+ end
52
+
53
+ def render_group_rm_events(events)
54
+ events.each { |event| render_group_rm_event(event) }
55
+ end
56
+
57
+ def render_group_rm_event(event)
58
+ payload = event.payload
59
+
60
+ render_group_rm_warning(payload) if event.type == :group_missing
61
+ return render_group_rm_progress(event.type, payload) if group_rm_progress?(event.type)
62
+
63
+ render_group_rm_status(event.type, payload)
64
+ end
65
+
66
+ def group_rm_progress?(type)
67
+ %i[group_started retrieving_group removing_parent_association removing_child_association].include?(type)
68
+ end
69
+
70
+ def render_group_rm_warning(payload)
71
+ fmt.warn "Group '#{payload[:name]}' does not exist, skipping.\n"
72
+ end
73
+
74
+ def render_group_rm_progress(type, payload)
75
+ case type
76
+ when :group_started
77
+ puts "Remove group '#{payload[:name]}':"
78
+ when :retrieving_group
79
+ fmt.puts 2, "- Retrieve group '#{payload[:name]}'..."
80
+ when :removing_parent_association, :removing_child_association
81
+ fmt.puts 2, "- Remove association {group:#{payload[:group]} <-> group:#{payload[:related_group]}}..."
82
+ end
83
+ end
84
+
85
+ def render_group_rm_status(type, payload)
86
+ return render_group_rm_secondary_status(type, payload) if group_rm_secondary_status?(type)
87
+
88
+ case type
89
+ when :group_missing
90
+ fmt.puts 4, '- No such group, skipping.'
91
+ when :destroying_group
92
+ fmt.puts payload[:indent], "- Destroy group '#{payload[:name]}'..."
93
+ when :group_complete
94
+ fmt.puts 2, '- All OK'
95
+ end
96
+ end
97
+
98
+ def group_rm_secondary_status?(type)
99
+ %i[
100
+ recursively_delete_orphaned_group
101
+ removing_recursive_child_association
102
+ adding_automatic_group_to_host
103
+ ok
104
+ ].include?(type)
105
+ end
106
+
107
+ def render_group_rm_secondary_status(type, payload)
108
+ case type
109
+ when :recursively_delete_orphaned_group
110
+ fmt.puts 2, "- Recursively delete orphaned group '#{payload[:name]}'..."
111
+ when :removing_recursive_child_association
112
+ fmt.puts 4, "- Remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
113
+ when :adding_automatic_group_to_host
114
+ fmt.puts payload[:indent], "- Adding automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
115
+ when :ok
116
+ fmt.puts payload[:indent], '- OK'
117
+ end
118
+ end
89
119
  end
90
120
  end
91
121
  end
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
 
3
- require_relative './formatter.rb'
5
+ require_relative 'formatter'
6
+ require_relative '../inventory_context'
7
+ require_relative '../operations/group_child_relations'
4
8
 
5
9
  module Moose
6
10
  module Inventory
@@ -9,72 +13,114 @@ module Moose
9
13
  # Implemention of the "group rmchild" methods of the CLI
10
14
  class Group
11
15
  #==========================
16
+ option :delete_orphans,
17
+ type: :boolean,
18
+ default: false,
19
+ desc: 'Delete child groups that become orphaned'
12
20
  desc 'rmchild PARENTGROUP CHILDGROUP_1 [CHILDGROUP_2 ... ]',
13
21
  'Dissociate one or more child-groups CHILDGROUP_n from PARENTGROUP'
14
- def rmchild(*_argv)
15
- # Sanity check
16
- if args.length < 2
17
- abort("ERROR: Wrong number of arguments, #{args.length} "\
18
- 'for 2 or more.')
19
- end
22
+ def rmchild(*argv)
23
+ abort_if_missing_args(argv, 2, '2 or more')
24
+
25
+ pname = argv[0].downcase
26
+ cnames = normalize_names(argv.slice(1, argv.length - 1))
27
+
28
+ abort_if_automatic_group([pname] + cnames)
20
29
 
21
- # Arguments
22
- pname = args[0].downcase
23
- cnames = args.slice(1, args.length - 1).uniq.map(&:downcase)
30
+ result = remove_children_from_group(pname, cnames)
24
31
 
25
- # Sanity
26
- if pname == 'ungrouped' || cnames.include?('ungrouped')
27
- abort("ERROR: Cannot manually manipulate the automatic group 'ungrouped'.")
32
+ if result.warning_count.zero?
33
+ puts 'Succeeded.'
34
+ else
35
+ puts 'Succeeded, with warnings.'
28
36
  end
37
+ end
29
38
 
30
- # Convenience
31
- db = Moose::Inventory::DB
32
- fmt = Moose::Inventory::Cli::Formatter
39
+ private
40
+
41
+ def remove_children_from_group(parent_name, child_names)
42
+ context = Moose::Inventory::InventoryContext.new(db: db)
43
+ operation = Moose::Inventory::Operations::GroupChildRelations.new(context: context)
33
44
 
34
- # Transaction
35
- warn_count = 0
36
45
  begin
37
- db.transaction do # Transaction start
38
- puts "Dissociate parent group '#{pname}' from child group(s) '#{cnames.join(',')}':"
39
- # Get the target group
40
- fmt.puts 2, "- retrieve group '#{pname}'..."
41
- pgroup = db.models[:group].find(name: pname)
42
- if pgroup.nil?
43
- abort("ERROR: The group '#{pname}' does not exist.")
44
- end
45
- fmt.puts 4, '- OK'
46
-
47
- # Dissociate parent group from the child groups
48
- groups_ds = pgroup.children_dataset
49
- cnames.each do |cname|
50
- fmt.puts 2, "- remove association {group:#{pname} <-> group:#{cname}}..."
51
-
52
- # Check against existing associations
53
- if groups_ds[name: cname].nil?
54
- warn_count += 1
55
- fmt.warn "Association {group:#{pname} <-> group:#{cname}}"\
56
- " does not exist, skipping.\n"
57
- fmt.puts 4, "- doesn't exist, skipping."
58
- fmt.puts 4, '- OK'
59
- next
60
- end
61
-
62
- # remove association
63
- cgroup = db.models[:group].find(name: cname)
64
- pgroup.remove_child(cgroup)
65
- fmt.puts 4, '- OK'
66
- end
46
+ db.transaction do
47
+ puts "Dissociate parent group '#{parent_name}' from child group(s) '#{child_names.join(',')}':"
48
+ parent_group = fetch_existing_group_for_rmchild(context, parent_name)
49
+ result = operation.remove_children(
50
+ parent_group: parent_group,
51
+ parent_name: parent_name,
52
+ child_names: child_names,
53
+ delete_orphans: options[:delete_orphans]
54
+ )
55
+ render_rmchild_events(result.events)
67
56
  fmt.puts 2, '- all OK'
68
- end # Transaction end
57
+ return result
58
+ end
69
59
  rescue db.exceptions[:moose] => e
70
60
  abort("ERROR: #{e}")
71
61
  end
72
- if warn_count == 0
73
- puts 'Succeeded.'
74
- else
75
- puts 'Succeeded, with warnings.'
62
+ end
63
+
64
+ def fetch_existing_group_for_rmchild(context, name)
65
+ fmt.puts 2, "- retrieve group '#{name}'..."
66
+ group = context.find_group(name)
67
+ abort("ERROR: The group '#{name}' does not exist.") if group.nil?
68
+
69
+ fmt.puts 4, '- OK'
70
+ group
71
+ end
72
+
73
+ def render_rmchild_events(events)
74
+ events.each { |event| render_rmchild_event(event) }
75
+ end
76
+
77
+ def render_rmchild_event(event)
78
+ payload = event.payload
79
+
80
+ return render_rmchild_warning(payload) if event.type == :child_association_missing
81
+ return render_rmchild_missing(payload) if event.type == :missing_skipping
82
+ return render_rmchild_progress(event.type, payload) if rmchild_progress_event?(event.type)
83
+
84
+ render_rmchild_status(event.type, payload)
85
+ end
86
+
87
+ def rmchild_progress_event?(type)
88
+ %i[
89
+ removing_child_association
90
+ recursively_delete_orphaned_group
91
+ removing_recursive_child_association
92
+ ].include?(type)
93
+ end
94
+
95
+ def render_rmchild_progress(type, payload)
96
+ case type
97
+ when :removing_child_association
98
+ fmt.puts 2, "- remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
99
+ when :recursively_delete_orphaned_group
100
+ fmt.puts 2, "- Recursively delete orphaned group '#{payload[:name]}'..."
101
+ when :removing_recursive_child_association
102
+ fmt.puts 4, "- Remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
103
+ end
104
+ end
105
+
106
+ def render_rmchild_status(type, payload)
107
+ case type
108
+ when :adding_automatic_group_to_host
109
+ fmt.puts payload[:indent], "- Adding automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
110
+ when :destroying_group
111
+ fmt.puts payload[:indent], "- Destroy group '#{payload[:name]}'..."
112
+ when :ok
113
+ fmt.puts payload[:indent], '- OK'
76
114
  end
77
115
  end
116
+
117
+ def render_rmchild_warning(payload)
118
+ fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}} does not exist, skipping.\n"
119
+ end
120
+
121
+ def render_rmchild_missing(payload)
122
+ fmt.puts payload[:indent], "- doesn't exist, skipping."
123
+ end
78
124
  end
79
125
  end
80
126
  end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
- require_relative './formatter.rb'
4
+ require_relative 'formatter'
5
+ require_relative '../inventory_context'
6
+ require_relative '../operations/remove_associations'
3
7
 
4
8
  module Moose
5
9
  module Inventory
@@ -10,75 +14,79 @@ module Moose
10
14
  #==========================
11
15
  desc 'rmhost GROUPNAME HOSTNAME_1 [HOSTNAME_2 ...]',
12
16
  'Dissociate the hosts HOSTNAME_n from the group NAME'
13
- # rubocop:disable Metrics/LineLength
14
- def rmhost(*args) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity
15
- # rubocop:enable Metrics/LineLength
16
- # Sanity
17
- if args.length < 2
18
- abort("ERROR: Wrong number of arguments, #{args.length} for 2 or more.")
19
- end
17
+ def rmhost(*args)
18
+ abort_if_missing_args(args, 2, '2 or more')
20
19
 
21
- # Arguments
22
20
  name = args[0].downcase
23
- hosts = args.slice(1, args.length - 1).uniq.map(&:downcase)
21
+ hosts = normalize_names(args.slice(1, args.length - 1))
22
+
23
+ abort_if_automatic_group([name])
24
+
25
+ result = remove_hosts_from_group(name, hosts)
24
26
 
25
- # Sanity
26
- if name == 'ungrouped'
27
- abort("ERROR: Cannot manually manipulate the automatic group 'ungrouped'.")
27
+ if result.warning_count.zero?
28
+ puts 'Succeeded.'
29
+ else
30
+ puts 'Succeeded, with warnings.'
28
31
  end
32
+ end
33
+
34
+ private
29
35
 
30
- # Convenience
31
- db = Moose::Inventory::DB
32
- fmt = Moose::Inventory::Cli::Formatter
36
+ def remove_hosts_from_group(name, hosts)
37
+ context = Moose::Inventory::InventoryContext.new(db: db)
38
+ operation = Moose::Inventory::Operations::RemoveAssociations.new(context: context)
33
39
 
34
- # Transaction
35
- warn_count = 0
36
40
  begin
37
- db.transaction do # Transaction start
38
- # Get the target group
41
+ db.transaction do
39
42
  puts "Dissociate group '#{name}' from host(s) '#{hosts.join(',')}':"
40
- fmt.puts 2, "- retrieve group '#{name}'..."
41
- group = db.models[:group].find(name: name)
42
- abort("ERROR: The group '#{name}' does not exist.") if group.nil?
43
- fmt.puts 4, '- OK'
44
-
45
- # dissociate group from the hosts
46
- ungrouped = db.models[:group].find_or_create(name: 'ungrouped')
47
- hosts_ds = group.hosts_dataset
48
- hosts.each do |h|
49
- fmt.puts 2, "- remove association {group:#{name} <-> host:#{h}}..."
50
-
51
- # Check against existing associations
52
- if hosts_ds[name: h].nil?
53
- warn_count += 1
54
- fmt.warn "Association {group:#{name} <-> host:#{h}} doesn't"\
55
- " exist, skipping.\n"
56
- fmt.puts 4, '- doesn\'t exist, skipping.'
57
- fmt.puts 4, '- OK'
58
- next
59
- end
60
-
61
- host = db.models[:host].find(name: h)
62
- group.remove_host(host) unless host.nil?
63
- fmt.puts 4, '- OK'
64
-
65
- # Add the host to the ungrouped group if not in any other group
66
- next unless host.groups_dataset.count == 0
67
- fmt.puts 2, "- add automatic association {group:ungrouped <-> host:#{h}}..."
68
- host.add_group(ungrouped)
69
- fmt.puts 4, '- OK'
70
- end
43
+ group = fetch_existing_group_for_rmhost(context, name)
44
+ result = operation.group_from_hosts(group: group, group_name: name, host_names: hosts)
45
+ render_group_rmhost_events(result.events)
71
46
  fmt.puts 2, '- all OK'
72
- end # Transaction end
47
+ return result
48
+ end
73
49
  rescue db.exceptions[:moose] => e
74
50
  abort("ERROR: #{e.message}")
75
51
  end
76
- if warn_count == 0
77
- puts 'Succeeded.'
78
- else
79
- puts 'Succeeded, with warnings.'
52
+ end
53
+
54
+ def fetch_existing_group_for_rmhost(context, name)
55
+ fmt.puts 2, "- retrieve group '#{name}'..."
56
+ group = context.find_group(name)
57
+ abort("ERROR: The group '#{name}' does not exist.") if group.nil?
58
+
59
+ fmt.puts 4, '- OK'
60
+ group
61
+ end
62
+
63
+ def render_group_rmhost_events(events)
64
+ events.each { |event| render_group_rmhost_event(event) }
65
+ end
66
+
67
+ def render_group_rmhost_event(event)
68
+ payload = event.payload
69
+
70
+ return render_group_rmhost_warning(payload) if event.type == :group_host_association_missing
71
+ return render_group_rmhost_missing(payload) if event.type == :missing_skipping
72
+
73
+ case event.type
74
+ when :removing_group_host_association
75
+ fmt.puts 2, "- remove association {group:#{payload[:group]} <-> host:#{payload[:host]}}..."
76
+ when :adding_automatic_group
77
+ fmt.puts 2, "- add automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
78
+ when :ok
79
+ fmt.puts payload[:indent], '- OK'
80
80
  end
81
81
  end
82
+
83
+ def render_group_rmhost_warning(payload)
84
+ fmt.warn "Association {group:#{payload[:group]} <-> host:#{payload[:host]}} doesn't exist, skipping.\n"
85
+ end
86
+
87
+ def render_group_rmhost_missing(payload)
88
+ fmt.puts payload[:indent], "- doesn't exist, skipping."
89
+ end
82
90
  end
83
91
  end
84
92
  end