moose-inventory 2.0 → 2.1
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 +4 -4
- data/.github/workflows/release.yml +2 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +21 -0
- data/BACKLOG.md +630 -8
- data/Gemfile +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +315 -39
- data/Rakefile +2 -0
- data/bin/moose-inventory +2 -1
- data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
- data/docs/compatibility/cli-output-compatibility.md +76 -0
- data/docs/governance/approval-register.md +37 -0
- data/docs/maintenance/database-backup-restore-guidance.md +162 -0
- data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
- data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
- data/docs/product/product-brief.md +161 -0
- data/docs/product/requirements-baseline.md +477 -0
- data/docs/qa/qa-documentation-and-release-gates.md +283 -0
- data/docs/release/package-provenance-hardening.md +126 -0
- data/docs/release/publishing.md +11 -3
- data/docs/release/release-environment-protection.md +70 -0
- data/docs/release/release-readiness.md +23 -4
- data/docs/security/accepted-risk-register.md +84 -0
- data/docs/security/security-privacy-process.md +287 -0
- data/docs/security-audit-2026-05-26-rerun.md +2 -2
- data/docs/ux/cli-workflow-notes.md +287 -0
- data/examples/ansible/ansible.cfg +3 -0
- data/examples/ansible/inventory/moose_inventory.yml +5 -0
- data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
- data/examples/ci/README.md +16 -0
- data/examples/ci/github-actions/inventory-review.yml +38 -0
- data/examples/ci/inventory/example-snapshot.yml +19 -0
- data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
- data/lib/moose_inventory/cli/application.rb +133 -5
- data/lib/moose_inventory/cli/association_rendering.rb +74 -0
- data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
- data/lib/moose_inventory/cli/audit.rb +62 -0
- data/lib/moose_inventory/cli/audit_recording.rb +40 -0
- data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
- data/lib/moose_inventory/cli/console.rb +135 -0
- data/lib/moose_inventory/cli/db.rb +64 -0
- data/lib/moose_inventory/cli/factory.rb +28 -0
- data/lib/moose_inventory/cli/formatter.rb +8 -12
- data/lib/moose_inventory/cli/group.rb +5 -2
- data/lib/moose_inventory/cli/group_add.rb +11 -9
- data/lib/moose_inventory/cli/group_addchild.rb +23 -65
- data/lib/moose_inventory/cli/group_addhost.rb +16 -67
- data/lib/moose_inventory/cli/group_addvar.rb +27 -47
- data/lib/moose_inventory/cli/group_get.rb +8 -42
- data/lib/moose_inventory/cli/group_list.rb +7 -40
- data/lib/moose_inventory/cli/group_listvars.rb +9 -55
- data/lib/moose_inventory/cli/group_rm.rb +12 -10
- data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
- data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
- data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
- data/lib/moose_inventory/cli/group_tags.rb +33 -0
- data/lib/moose_inventory/cli/helpers.rb +68 -1
- data/lib/moose_inventory/cli/host.rb +6 -3
- data/lib/moose_inventory/cli/host_add.rb +69 -29
- data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
- data/lib/moose_inventory/cli/host_addvar.rb +28 -52
- data/lib/moose_inventory/cli/host_get.rb +9 -37
- data/lib/moose_inventory/cli/host_list.rb +24 -21
- data/lib/moose_inventory/cli/host_listvars.rb +9 -62
- data/lib/moose_inventory/cli/host_rm.rb +60 -42
- data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
- data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
- data/lib/moose_inventory/cli/host_tags.rb +33 -0
- data/lib/moose_inventory/cli/listvars_support.rb +55 -0
- data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
- data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
- data/lib/moose_inventory/cli/tag_support.rb +97 -0
- data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
- data/lib/moose_inventory/config/config.rb +185 -108
- data/lib/moose_inventory/db/db.rb +170 -195
- data/lib/moose_inventory/db/exceptions.rb +6 -3
- data/lib/moose_inventory/db/models.rb +16 -0
- data/lib/moose_inventory/db/schema_migrations.rb +248 -0
- data/lib/moose_inventory/inventory_context.rb +68 -2
- data/lib/moose_inventory/operations/add_associations.rb +20 -16
- data/lib/moose_inventory/operations/add_groups.rb +21 -13
- data/lib/moose_inventory/operations/add_hosts.rb +30 -17
- data/lib/moose_inventory/operations/add_variables.rb +77 -0
- data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
- data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
- data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
- data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
- data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
- data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
- data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
- data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
- data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
- data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
- data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
- data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
- data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
- data/lib/moose_inventory/operations/query_inventory.rb +47 -0
- data/lib/moose_inventory/operations/remove_associations.rb +30 -18
- data/lib/moose_inventory/operations/remove_groups.rb +12 -12
- data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
- data/lib/moose_inventory/operations/remove_variables.rb +67 -0
- data/lib/moose_inventory/runtime_options.rb +31 -0
- data/lib/moose_inventory/version.rb +3 -1
- data/lib/moose_inventory.rb +10 -7
- data/moose-inventory.gemspec +19 -35
- data/scripts/check.sh +1 -0
- data/scripts/ci/check_generated_artifacts.sh +41 -0
- data/scripts/ci/check_permissions.sh +2 -0
- data/scripts/ci/check_rubocop.sh +30 -25
- data/scripts/files.rb +5 -4
- data/spec/examples/ci_examples_spec.rb +37 -0
- data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
- data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
- data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
- data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
- data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
- data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
- data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
- data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
- data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
- data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
- data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
- data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
- data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
- data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
- data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
- data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
- data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
- data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
- data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
- data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
- data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
- data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
- data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
- data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
- data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
- data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
- data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
- data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
- data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
- data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
- data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
- data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
- data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
- data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
- data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
- data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
- data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
- data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
- data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
- data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
- data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
- data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
- data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
- data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
- data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
- data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
- data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
- data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
- data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
- data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
- data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
- data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
- data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
- data/spec/shared/shared_config_setup.rb +4 -3
- data/spec/spec_helper.rb +50 -40
- data/spec/support/cli_harness.rb +33 -0
- metadata +80 -41
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moose
|
|
4
|
+
module Inventory
|
|
5
|
+
module Cli
|
|
6
|
+
# Shared rendering helpers for child-group relation commands.
|
|
7
|
+
module ChildRelationRendering
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def addchild_emitter
|
|
11
|
+
lambda do |event|
|
|
12
|
+
render_addchild_event(event)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rmchild_emitter
|
|
17
|
+
lambda do |event|
|
|
18
|
+
render_rmchild_event(event)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_addchild_event(event)
|
|
23
|
+
payload = event.payload
|
|
24
|
+
|
|
25
|
+
return render_addchild_warning(event.type, payload) if addchild_warning?(event.type)
|
|
26
|
+
return render_addchild_existing(payload) if event.type == :already_exists_skipping
|
|
27
|
+
return render_child_relation_dry_run_summary if event.type == :dry_run_summary
|
|
28
|
+
|
|
29
|
+
case event.type
|
|
30
|
+
when :adding_child_association
|
|
31
|
+
fmt.puts 2, "- add association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
|
|
32
|
+
when :child_group_creating_now
|
|
33
|
+
fmt.puts 4, '- child group does not exist, creating now...'
|
|
34
|
+
when :ok
|
|
35
|
+
fmt.puts payload[:indent], '- OK'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_child_relation_dry_run_summary
|
|
40
|
+
puts 'Dry run complete. No changes applied.'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def addchild_warning?(type)
|
|
44
|
+
%i[child_association_exists child_group_missing].include?(type)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_addchild_warning(type, payload)
|
|
48
|
+
if type == :child_association_exists
|
|
49
|
+
fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}} already exists, skipping.\n"
|
|
50
|
+
else
|
|
51
|
+
fmt.warn "Group '#{payload[:name]}' does not exist and will be created.\n"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_addchild_existing(payload)
|
|
56
|
+
fmt.puts payload[:indent], '- already exists, skipping.'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_rmchild_event(event)
|
|
60
|
+
payload = event.payload
|
|
61
|
+
|
|
62
|
+
return render_rmchild_warning(payload) if event.type == :child_association_missing
|
|
63
|
+
return render_rmchild_missing(payload) if event.type == :missing_skipping
|
|
64
|
+
return render_rmchild_progress(event.type, payload) if rmchild_progress_event?(event.type)
|
|
65
|
+
return render_child_relation_dry_run_summary if event.type == :dry_run_summary
|
|
66
|
+
|
|
67
|
+
render_rmchild_status(event.type, payload)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rmchild_progress_event?(type)
|
|
71
|
+
%i[
|
|
72
|
+
removing_child_association
|
|
73
|
+
recursively_delete_orphaned_group
|
|
74
|
+
removing_recursive_child_association
|
|
75
|
+
].include?(type)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_rmchild_progress(type, payload)
|
|
79
|
+
case type
|
|
80
|
+
when :removing_child_association
|
|
81
|
+
fmt.puts 2, "- remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
|
|
82
|
+
when :recursively_delete_orphaned_group
|
|
83
|
+
fmt.puts 2, "- Recursively delete orphaned group '#{payload[:name]}'..."
|
|
84
|
+
when :removing_recursive_child_association
|
|
85
|
+
fmt.puts 4, "- Remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render_rmchild_status(type, payload)
|
|
90
|
+
case type
|
|
91
|
+
when :adding_automatic_group_to_host
|
|
92
|
+
fmt.puts payload[:indent], "- Adding automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
|
|
93
|
+
when :destroying_group
|
|
94
|
+
fmt.puts payload[:indent], "- Destroy group '#{payload[:name]}'..."
|
|
95
|
+
when :ok
|
|
96
|
+
fmt.puts payload[:indent], '- OK'
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_rmchild_warning(payload)
|
|
101
|
+
fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}} does not exist, skipping.\n"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def render_rmchild_missing(payload)
|
|
105
|
+
fmt.puts payload[:indent], "- doesn't exist, skipping."
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Moose
|
|
6
|
+
module Inventory
|
|
7
|
+
module Cli
|
|
8
|
+
# Small read-only interactive console for browsing inventory state.
|
|
9
|
+
class Console
|
|
10
|
+
COMMANDS = ['help', 'hosts', 'groups', 'host NAME', 'group NAME',
|
|
11
|
+
'tags host NAME', 'tags group NAME', 'audit [LIMIT]', 'quit'].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(context:, input: $stdin, output: $stdout)
|
|
14
|
+
@context = context
|
|
15
|
+
@input = input
|
|
16
|
+
@output = output
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
output.puts 'Moose Inventory console (read-only). Type help or quit.'
|
|
21
|
+
input.each_line do |line|
|
|
22
|
+
parts = parse_command(line)
|
|
23
|
+
next if parts.nil? || parts.empty?
|
|
24
|
+
|
|
25
|
+
break if quit_command?(parts)
|
|
26
|
+
|
|
27
|
+
dispatch(parts)
|
|
28
|
+
end
|
|
29
|
+
output.puts 'Goodbye.'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :context, :input, :output
|
|
35
|
+
|
|
36
|
+
def parse_command(line)
|
|
37
|
+
command = line.strip
|
|
38
|
+
return [] if command.empty?
|
|
39
|
+
|
|
40
|
+
Shellwords.split(command)
|
|
41
|
+
rescue ArgumentError => e
|
|
42
|
+
output.puts "Invalid command syntax: #{e.message}"
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dispatch(parts)
|
|
47
|
+
handlers = {
|
|
48
|
+
'help' => -> { render_exact(parts, 'help') { render_help } },
|
|
49
|
+
'hosts' => -> { render_exact(parts, 'hosts') { render_hosts } },
|
|
50
|
+
'groups' => -> { render_exact(parts, 'groups') { render_groups } },
|
|
51
|
+
'host' => -> { render_entity(:host, parts) },
|
|
52
|
+
'group' => -> { render_entity(:group, parts) },
|
|
53
|
+
'tags' => -> { render_tags(parts) },
|
|
54
|
+
'audit' => -> { render_audit(parts) }
|
|
55
|
+
}
|
|
56
|
+
handlers.fetch(parts.first, -> { output.puts "Unknown command: #{parts.join(' ')}" }).call
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def quit_command?(parts)
|
|
60
|
+
parts.length == 1 && %w[quit exit].include?(parts.first)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_exact(parts, usage)
|
|
64
|
+
return output.puts("Usage: #{usage}") unless parts.length == 1
|
|
65
|
+
|
|
66
|
+
yield
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_help
|
|
70
|
+
output.puts 'Commands:'
|
|
71
|
+
COMMANDS.each { |command| output.puts "- #{command}" }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_hosts
|
|
75
|
+
names = context.all_hosts.map(&:name).sort
|
|
76
|
+
output.puts(names.empty? ? 'No hosts.' : "Hosts: #{names.join(', ')}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_groups
|
|
80
|
+
names = context.all_groups.map(&:name).sort
|
|
81
|
+
output.puts(names.empty? ? 'No groups.' : "Groups: #{names.join(', ')}")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_entity(type, parts)
|
|
85
|
+
return output.puts("Usage: #{type} NAME") unless parts.length == 2
|
|
86
|
+
|
|
87
|
+
name = parts[1]
|
|
88
|
+
|
|
89
|
+
entity = context.public_send("find_#{type}", name)
|
|
90
|
+
return output.puts("#{type.capitalize} '#{name}' not found.") if entity.nil?
|
|
91
|
+
|
|
92
|
+
output.puts "#{type.capitalize}: #{name}"
|
|
93
|
+
output.puts "Groups: #{entity.groups_dataset.map(:name).sort.join(', ')}" if type == :host
|
|
94
|
+
output.puts "Hosts: #{entity.hosts_dataset.map(:name).sort.join(', ')}" if type == :group
|
|
95
|
+
output.puts "Tags: #{entity.tags_dataset.map(:name).sort.join(', ')}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def render_tags(parts)
|
|
99
|
+
type = parts[1]
|
|
100
|
+
name = parts[2]
|
|
101
|
+
return output.puts('Usage: tags host|group NAME') unless parts.length == 3 && %w[host group].include?(type)
|
|
102
|
+
|
|
103
|
+
entity = context.public_send("find_#{type}", name)
|
|
104
|
+
return output.puts("#{type.capitalize} '#{name}' not found.") if entity.nil?
|
|
105
|
+
|
|
106
|
+
tags = entity.tags_dataset.map(:name).sort
|
|
107
|
+
output.puts tags.empty? ? "#{type.capitalize} '#{name}' has no tags." : tags.join(', ')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def render_audit(parts)
|
|
111
|
+
limit = audit_limit(parts[1]) if parts.length <= 2
|
|
112
|
+
return output.puts('Usage: audit [LIMIT]') if parts.length > 2 || limit.nil?
|
|
113
|
+
|
|
114
|
+
events = context.audit_events(limit: limit)
|
|
115
|
+
return output.puts('No audit events recorded.') if events.empty?
|
|
116
|
+
|
|
117
|
+
events.each do |event|
|
|
118
|
+
output.puts "#{event.id} #{event.created_at} #{event.command} #{event.entity_type}=#{event.entity_name}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def audit_limit(value)
|
|
123
|
+
return 10 if value.nil?
|
|
124
|
+
|
|
125
|
+
parsed = Integer(value)
|
|
126
|
+
return parsed if parsed.positive?
|
|
127
|
+
|
|
128
|
+
nil
|
|
129
|
+
rescue ArgumentError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require_relative 'formatter'
|
|
7
|
+
|
|
8
|
+
module Moose
|
|
9
|
+
module Inventory
|
|
10
|
+
module Cli
|
|
11
|
+
# Database lifecycle commands.
|
|
12
|
+
class Db < Thor
|
|
13
|
+
desc 'status', 'Show database lifecycle status'
|
|
14
|
+
def status
|
|
15
|
+
render_status(Moose::Inventory::DB.status)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc 'doctor', 'Check database schema state'
|
|
19
|
+
def doctor
|
|
20
|
+
status = Moose::Inventory::DB.status
|
|
21
|
+
missing = status[:tables].reject { |_name, present| present }.keys
|
|
22
|
+
if missing.empty? && status[:schema_version] == status[:expected_schema_version]
|
|
23
|
+
puts 'Database doctor found no issues.'
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
puts 'Database doctor found issue(s):'
|
|
28
|
+
puts "- Missing tables: #{missing.join(', ')}" unless missing.empty?
|
|
29
|
+
if status[:schema_version] != status[:expected_schema_version]
|
|
30
|
+
puts "- Schema version is #{status[:schema_version].inspect}; expected #{status[:expected_schema_version]}."
|
|
31
|
+
end
|
|
32
|
+
exit(1)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc 'migrate', 'Create missing schema tables and record current schema version'
|
|
36
|
+
def migrate
|
|
37
|
+
status = Moose::Inventory::DB.migrate!
|
|
38
|
+
puts "Database schema is at version #{status[:schema_version]}."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc 'backup FILE', 'Back up the configured sqlite3 database file'
|
|
42
|
+
def backup(file)
|
|
43
|
+
destination = Moose::Inventory::DB.backup(file)
|
|
44
|
+
puts "Backed up database to #{destination}."
|
|
45
|
+
rescue Moose::Inventory::DB.exceptions[:moose] => e
|
|
46
|
+
abort("ERROR: #{e.message}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def render_status(status)
|
|
52
|
+
puts "Adapter: #{status[:adapter]}"
|
|
53
|
+
puts "Schema version: #{status[:schema_version] || 'unknown'}"
|
|
54
|
+
puts "Expected schema version: #{status[:expected_schema_version]}"
|
|
55
|
+
puts "SQLite file: #{status[:sqlite_file]}" unless status[:sqlite_file].nil?
|
|
56
|
+
puts 'Tables:'
|
|
57
|
+
status[:tables].each do |name, present|
|
|
58
|
+
puts "- #{name}: #{present ? 'present' : 'missing'}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../operations/query_inventory'
|
|
4
|
+
|
|
5
|
+
module Moose
|
|
6
|
+
module Inventory
|
|
7
|
+
module Cli
|
|
8
|
+
# Small factory for command-side operations and query wrappers.
|
|
9
|
+
class Factory
|
|
10
|
+
def initialize(context:)
|
|
11
|
+
@context = context
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def operation(operation_class, **)
|
|
15
|
+
operation_class.new(context: context, **)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query_inventory
|
|
19
|
+
@query_inventory ||= Moose::Inventory::Operations::QueryInventory.new(context: context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :context
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'json'
|
|
2
4
|
require 'yaml'
|
|
3
5
|
require 'indentation'
|
|
@@ -5,24 +7,18 @@ require 'indentation'
|
|
|
5
7
|
module Moose
|
|
6
8
|
module Inventory
|
|
7
9
|
module Cli
|
|
8
|
-
##
|
|
9
|
-
# TODO: Documentation
|
|
10
10
|
module Formatter
|
|
11
11
|
# rubocop:disable Style/ModuleFunction
|
|
12
12
|
extend self
|
|
13
13
|
# rubocop:enable Style/ModuleFunction
|
|
14
14
|
|
|
15
|
-
def self.dump(arg, format =
|
|
15
|
+
def self.dump(arg, format = 'json')
|
|
16
16
|
out(arg, format)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def self.out(arg, format =
|
|
19
|
+
def self.out(arg, format = 'json')
|
|
20
20
|
return if arg.nil?
|
|
21
21
|
|
|
22
|
-
if format.nil?
|
|
23
|
-
format = Moose::Inventory::Config._confopts[:format].downcase
|
|
24
|
-
end
|
|
25
|
-
|
|
26
22
|
case format
|
|
27
23
|
when 'yaml', 'y'
|
|
28
24
|
$stdout.puts arg.to_yaml
|
|
@@ -50,7 +46,7 @@ module Moose
|
|
|
50
46
|
when 'STDOUT'
|
|
51
47
|
$stdout.puts msg.indent(indent)
|
|
52
48
|
when 'STDERR'
|
|
53
|
-
$stderr.
|
|
49
|
+
$stderr.print("#{msg.indent(indent)}\n")
|
|
54
50
|
else
|
|
55
51
|
abort("Output stream '#{stream}' is not known.")
|
|
56
52
|
end
|
|
@@ -67,12 +63,12 @@ module Moose
|
|
|
67
63
|
end
|
|
68
64
|
end
|
|
69
65
|
|
|
70
|
-
def info(
|
|
66
|
+
def info(indent, msg, stream = 'STDOUT')
|
|
71
67
|
case stream
|
|
72
68
|
when 'STDOUT'
|
|
73
|
-
$stdout.print
|
|
69
|
+
$stdout.print "INFO: #{msg}".indent(indent)
|
|
74
70
|
when 'STDERR'
|
|
75
|
-
$stderr.print
|
|
71
|
+
$stderr.print "INFO: #{msg}".indent(indent)
|
|
76
72
|
else
|
|
77
73
|
abort("Output stream '#{stream}' is not known.")
|
|
78
74
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'thor'
|
|
2
|
-
require_relative '
|
|
3
|
-
require_relative '
|
|
4
|
+
require_relative 'formatter'
|
|
5
|
+
require_relative 'helpers'
|
|
4
6
|
|
|
5
7
|
module Moose
|
|
6
8
|
module Inventory
|
|
@@ -21,6 +23,7 @@ module Moose
|
|
|
21
23
|
require_relative 'group_addvar'
|
|
22
24
|
require_relative 'group_listvars'
|
|
23
25
|
require_relative 'group_rmvar'
|
|
26
|
+
require_relative 'group_tags'
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|
|
@@ -14,8 +14,11 @@ module Moose
|
|
|
14
14
|
#==========================
|
|
15
15
|
desc 'add NAME', 'Add a group NAME to the inventory'
|
|
16
16
|
option :hosts
|
|
17
|
+
option :dry_run, type: :boolean
|
|
18
|
+
option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
|
|
17
19
|
def add(*argv)
|
|
18
20
|
abort_if_missing_args(argv, 1, '1 or more')
|
|
21
|
+
validate_machine_plan_request!
|
|
19
22
|
|
|
20
23
|
names = normalize_names(argv)
|
|
21
24
|
hosts = csv_option_names(options[:hosts])
|
|
@@ -25,16 +28,14 @@ module Moose
|
|
|
25
28
|
"ERROR: Cannot manually manipulate the automatic group 'ungrouped'\n"
|
|
26
29
|
)
|
|
27
30
|
|
|
28
|
-
result = Moose::Inventory::Operations::AddGroups
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
render_add_groups_events(result.events)
|
|
31
|
+
result = build_operation(Moose::Inventory::Operations::AddGroups)
|
|
32
|
+
.call(names: names, hosts: hosts, dry_run: options[:dry_run])
|
|
33
|
+
return if machine_plan_output_rendered?(result, command: 'group add')
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
35
|
+
record_audit({ command: 'group add', action: 'add', entity_type: 'group',
|
|
36
|
+
entity_names: names }, result: result, dry_run: options[:dry_run])
|
|
37
|
+
render_add_groups_events(result.events)
|
|
38
|
+
print_warning_summary(result, success_message: 'Succeeded')
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
private
|
|
@@ -48,6 +49,7 @@ module Moose
|
|
|
48
49
|
|
|
49
50
|
return render_add_groups_event_puts(event.type, payload) if puts_event?(event.type)
|
|
50
51
|
return render_add_groups_event_warn(event.type, payload) if warn_event?(event.type)
|
|
52
|
+
return puts 'Dry run complete. No changes applied.' if event.type == :dry_run_summary
|
|
51
53
|
|
|
52
54
|
render_add_groups_event_fmt(event.type, payload)
|
|
53
55
|
end
|
|
@@ -14,8 +14,11 @@ module Moose
|
|
|
14
14
|
#==========================
|
|
15
15
|
desc 'addchild PARENTGROUP CHILDGROUP_1 [CHILDGROUP_2 ... ]',
|
|
16
16
|
'Associate one or more child-groups CHILDGROUP_n with PARENTGROUP'
|
|
17
|
+
option :dry_run, type: :boolean
|
|
18
|
+
option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
|
|
17
19
|
def addchild(*argv)
|
|
18
20
|
abort_if_missing_args(argv, 2, '2 or more')
|
|
21
|
+
validate_machine_plan_request!
|
|
19
22
|
|
|
20
23
|
pname = argv[0].downcase
|
|
21
24
|
cnames = normalize_names(argv.slice(1, argv.length - 1))
|
|
@@ -23,81 +26,36 @@ module Moose
|
|
|
23
26
|
abort_if_automatic_group([pname] + cnames)
|
|
24
27
|
|
|
25
28
|
result = add_children_to_group(pname, cnames)
|
|
29
|
+
return if machine_plan_output_rendered?(result, command: 'group addchild')
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
puts 'Succeeded, with warnings.'
|
|
31
|
-
end
|
|
31
|
+
record_audit({ command: 'group addchild', action: 'associate_child', entity_type: 'group',
|
|
32
|
+
entity_names: pname }, result: result, dry_run: options[:dry_run])
|
|
33
|
+
print_warning_summary(result)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
private
|
|
35
37
|
|
|
36
38
|
def add_children_to_group(parent_name, child_names)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return result
|
|
52
|
-
end
|
|
53
|
-
rescue db.exceptions[:moose] => e
|
|
54
|
-
abort("ERROR: #{e}")
|
|
39
|
+
operation = build_operation(Moose::Inventory::Operations::GroupChildRelations)
|
|
40
|
+
run_group_relation_transaction(
|
|
41
|
+
heading: "Associate parent group '#{parent_name}' with child group(s) '#{child_names.join(',')}':",
|
|
42
|
+
on_error: method(:exception_to_s)
|
|
43
|
+
) do
|
|
44
|
+
parent_group = fetch_existing_group_or_abort(parent_name)
|
|
45
|
+
result = operation.add_children(
|
|
46
|
+
parent_group: parent_group,
|
|
47
|
+
parent_name: parent_name,
|
|
48
|
+
child_names: child_names,
|
|
49
|
+
dry_run: options[:dry_run]
|
|
50
|
+
)
|
|
51
|
+
render_addchild_events(result.events) unless machine_plan_output_requested?
|
|
52
|
+
result
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
|
|
58
|
-
def fetch_existing_group_for_child_relation(context, name)
|
|
59
|
-
fmt.puts 2, "- retrieve group '#{name}'..."
|
|
60
|
-
group = context.find_group(name)
|
|
61
|
-
abort("ERROR: The group '#{name}' does not exist.") if group.nil?
|
|
62
|
-
|
|
63
|
-
fmt.puts 4, '- OK'
|
|
64
|
-
group
|
|
65
|
-
end
|
|
66
|
-
|
|
67
56
|
def render_addchild_events(events)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def render_addchild_event(event)
|
|
72
|
-
payload = event.payload
|
|
73
|
-
|
|
74
|
-
return render_addchild_warning(event.type, payload) if addchild_warning?(event.type)
|
|
75
|
-
return render_addchild_existing(payload) if event.type == :already_exists_skipping
|
|
76
|
-
|
|
77
|
-
case event.type
|
|
78
|
-
when :adding_child_association
|
|
79
|
-
fmt.puts 2, "- add association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
|
|
80
|
-
when :child_group_creating_now
|
|
81
|
-
fmt.puts 4, '- child group does not exist, creating now...'
|
|
82
|
-
when :ok
|
|
83
|
-
fmt.puts payload[:indent], '- OK'
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def addchild_warning?(type)
|
|
88
|
-
%i[child_association_exists child_group_missing].include?(type)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def render_addchild_warning(type, payload)
|
|
92
|
-
if type == :child_association_exists
|
|
93
|
-
fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}}} already exists, skipping.\n"
|
|
94
|
-
else
|
|
95
|
-
fmt.warn "Group '#{payload[:name]}' does not exist and will be created.\n"
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def render_addchild_existing(payload)
|
|
100
|
-
fmt.puts payload[:indent], '- already exists, skipping.'
|
|
57
|
+
emitter = addchild_emitter
|
|
58
|
+
events.each { |event| emitter.call(event) }
|
|
101
59
|
end
|
|
102
60
|
end
|
|
103
61
|
end
|
|
@@ -14,8 +14,11 @@ module Moose
|
|
|
14
14
|
#==========================
|
|
15
15
|
desc 'addhost NAME HOSTNAME',
|
|
16
16
|
'Associate a host HOSTNAME with the group NAME'
|
|
17
|
+
option :dry_run, type: :boolean
|
|
18
|
+
option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
|
|
17
19
|
def addhost(*args)
|
|
18
20
|
abort_if_missing_args(args, 2, '2 or more')
|
|
21
|
+
validate_machine_plan_request!
|
|
19
22
|
|
|
20
23
|
name = args[0].downcase
|
|
21
24
|
hosts = normalize_names(args.slice(1, args.length - 1))
|
|
@@ -23,83 +26,29 @@ module Moose
|
|
|
23
26
|
abort_if_automatic_group([name])
|
|
24
27
|
|
|
25
28
|
result = add_hosts_to_group(name, hosts)
|
|
29
|
+
return if machine_plan_output_rendered?(result, command: 'group addhost')
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
puts 'Succeeded, with warnings.'
|
|
31
|
-
end
|
|
31
|
+
record_audit({ command: 'group addhost', action: 'associate', entity_type: 'group',
|
|
32
|
+
entity_names: name }, result: result, dry_run: options[:dry_run])
|
|
33
|
+
print_warning_summary(result)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
private
|
|
35
37
|
|
|
36
38
|
def add_hosts_to_group(name, hosts)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
result = operation.group_to_hosts(group: group, group_name: name, host_names: hosts)
|
|
45
|
-
render_group_addhost_events(result.events)
|
|
46
|
-
fmt.puts 2, '- all OK'
|
|
47
|
-
return result
|
|
48
|
-
end
|
|
49
|
-
rescue db.exceptions[:moose] => e
|
|
50
|
-
abort("ERROR: #{e.message}")
|
|
39
|
+
operation = build_operation(Moose::Inventory::Operations::AddAssociations)
|
|
40
|
+
run_group_relation_transaction(heading: "Associate group '#{name}' with host(s) '#{hosts.join(',')}':") do
|
|
41
|
+
group = fetch_existing_group_or_abort(name)
|
|
42
|
+
result = operation.group_to_hosts(group: group, group_name: name, host_names: hosts,
|
|
43
|
+
dry_run: options[:dry_run])
|
|
44
|
+
render_group_addhost_events(result.events) unless machine_plan_output_requested?
|
|
45
|
+
result
|
|
51
46
|
end
|
|
52
47
|
end
|
|
53
48
|
|
|
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
49
|
def render_group_addhost_events(events)
|
|
64
|
-
|
|
65
|
-
|
|
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"
|
|
83
|
-
else
|
|
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'
|
|
102
|
-
end
|
|
50
|
+
emitter = host_group_association_addition_emitter(perspective: :group)
|
|
51
|
+
events.each { |event| emitter.call(event) }
|
|
103
52
|
end
|
|
104
53
|
end
|
|
105
54
|
end
|