moose-inventory 1.0.9 → 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/ci.yml +15 -1
- data/.github/workflows/release.yml +60 -0
- data/.gitignore +2 -1
- data/.gitleaks.toml +9 -0
- data/.rubocop.yml +49 -0
- data/BACKLOG.md +752 -24
- data/Gemfile +2 -0
- data/Gemfile.lock +36 -1
- data/README.md +340 -44
- 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 +54 -50
- data/docs/release/release-environment-protection.md +70 -0
- data/docs/release/release-readiness.md +37 -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 +75 -0
- data/docs/security-audit-2026-05-26.md +63 -0
- 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 +7 -1
- data/lib/moose_inventory/cli/group_add.rb +91 -73
- data/lib/moose_inventory/cli/group_addchild.rb +41 -66
- data/lib/moose_inventory/cli/group_addhost.rb +33 -71
- 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 +105 -73
- data/lib/moose_inventory/cli/group_rmchild.rb +47 -57
- data/lib/moose_inventory/cli/group_rmhost.rb +34 -61
- 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 +143 -0
- data/lib/moose_inventory/cli/host.rb +8 -2
- data/lib/moose_inventory/cli/host_add.rb +91 -66
- data/lib/moose_inventory/cli/host_addgroup.rb +39 -66
- 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 +39 -55
- 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 +188 -193
- 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 +116 -0
- data/lib/moose_inventory/operations/add_associations.rb +131 -0
- data/lib/moose_inventory/operations/add_groups.rb +123 -0
- data/lib/moose_inventory/operations/add_hosts.rb +123 -0
- 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 +125 -0
- data/lib/moose_inventory/operations/group_cleanup.rb +70 -0
- 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 +113 -0
- data/lib/moose_inventory/operations/remove_groups.rb +79 -0
- 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 +22 -35
- data/scripts/check.sh +3 -0
- data/scripts/ci/check_generated_artifacts.sh +41 -0
- data/scripts/ci/check_permissions.sh +5 -0
- data/scripts/ci/check_rubocop.sh +33 -0
- data/scripts/ci/check_secrets.sh +26 -0
- data/scripts/ci/check_security.sh +18 -0
- data/scripts/ci/install_security_tools.sh +47 -0
- data/scripts/files.rb +5 -4
- data/scripts/install_dependencies.sh +2 -0
- 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 +165 -85
- data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +100 -30
- 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 +551 -29
- 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 +111 -0
- data/spec/lib/moose_inventory/operations/add_groups_spec.rb +80 -0
- data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +82 -0
- data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
- data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +122 -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 +113 -0
- data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +78 -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 +163 -35
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'association_rendering_support'
|
|
4
|
+
|
|
5
|
+
module Moose
|
|
6
|
+
module Inventory
|
|
7
|
+
module Cli
|
|
8
|
+
# Shared rendering helpers for host/group association commands.
|
|
9
|
+
module AssociationRendering
|
|
10
|
+
include Moose::Inventory::Cli::AssociationRenderingSupport
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def host_group_association_addition_emitter(perspective:)
|
|
15
|
+
lambda do |event|
|
|
16
|
+
render_host_group_association_addition_event(event, perspective:)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def host_group_association_removal_emitter(perspective:)
|
|
21
|
+
lambda do |event|
|
|
22
|
+
render_host_group_association_removal_event(event, perspective:)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_host_group_association_addition_event(event, perspective:)
|
|
27
|
+
payload = event.payload
|
|
28
|
+
|
|
29
|
+
return render_addition_warning(event.type, payload, perspective:) if addition_warning_event?(event.type)
|
|
30
|
+
return render_addition_existing(payload, perspective:) if event.type == :already_exists_skipping
|
|
31
|
+
return render_association_dry_run_summary if event.type == :dry_run_summary
|
|
32
|
+
|
|
33
|
+
render_addition_action_event(event.type, payload, perspective:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render_addition_action_event(type, payload, perspective:)
|
|
37
|
+
case type
|
|
38
|
+
when :adding_host_group_association, :adding_group_host_association
|
|
39
|
+
fmt.puts 2, "- #{verb_for(:add, perspective)} association #{association_label(payload, perspective:)}..."
|
|
40
|
+
when :group_creating_now, :host_creating_now
|
|
41
|
+
fmt.puts 4, "- #{missing_entity_label(perspective)} does not exist, creating now..."
|
|
42
|
+
when :removing_automatic_group
|
|
43
|
+
label = automatic_group_label(payload[:host], perspective:)
|
|
44
|
+
fmt.puts 2, "- #{verb_for(:remove, perspective)} automatic association #{label}..."
|
|
45
|
+
when :ok
|
|
46
|
+
fmt.puts payload[:indent], '- OK'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_association_dry_run_summary
|
|
51
|
+
puts 'Dry run complete. No changes applied.'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_host_group_association_removal_event(event, perspective:)
|
|
55
|
+
payload = event.payload
|
|
56
|
+
|
|
57
|
+
return render_removal_warning(payload, perspective:) if removal_warning_event?(event.type)
|
|
58
|
+
return render_removal_missing(payload, perspective:) if event.type == :missing_skipping
|
|
59
|
+
return render_association_dry_run_summary if event.type == :dry_run_summary
|
|
60
|
+
|
|
61
|
+
case event.type
|
|
62
|
+
when :removing_host_group_association, :removing_group_host_association
|
|
63
|
+
fmt.puts 2, "- #{verb_for(:remove, perspective)} association #{association_label(payload, perspective:)}..."
|
|
64
|
+
when :adding_automatic_group
|
|
65
|
+
label = automatic_group_label(payload[:host], perspective:)
|
|
66
|
+
fmt.puts 2, "- #{verb_for(:add, perspective)} automatic association #{label}..."
|
|
67
|
+
when :ok
|
|
68
|
+
fmt.puts payload[:indent], '- OK'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moose
|
|
4
|
+
module Inventory
|
|
5
|
+
module Cli
|
|
6
|
+
# Shared string helpers for host/group association commands.
|
|
7
|
+
module AssociationRenderingSupport
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def addition_warning_event?(type)
|
|
11
|
+
%i[
|
|
12
|
+
host_group_association_exists
|
|
13
|
+
group_host_association_exists
|
|
14
|
+
group_missing_created
|
|
15
|
+
host_missing_created
|
|
16
|
+
].include?(type)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def removal_warning_event?(type)
|
|
20
|
+
%i[host_group_association_missing group_host_association_missing].include?(type)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render_addition_warning(type, payload, perspective:)
|
|
24
|
+
if %i[host_group_association_exists group_host_association_exists].include?(type)
|
|
25
|
+
fmt.warn warning_text(
|
|
26
|
+
"Association #{association_label(payload, perspective:)} already exists, skipping.",
|
|
27
|
+
perspective:
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
fmt.warn warning_text(
|
|
31
|
+
"#{missing_entity_label(perspective).capitalize} '#{payload[:name]}' does not exist and will be created.",
|
|
32
|
+
perspective:
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def render_addition_existing(payload, perspective:)
|
|
38
|
+
fmt.puts payload[:indent], "- #{existing_status_text(perspective)}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_removal_warning(payload, perspective:)
|
|
42
|
+
fmt.warn "Association #{association_label(payload, perspective:)} doesn't exist, skipping.\n"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_removal_missing(payload, perspective:)
|
|
46
|
+
fmt.puts payload[:indent], "- #{missing_status_text(perspective)}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def association_label(payload, perspective:)
|
|
50
|
+
if perspective == :host
|
|
51
|
+
"{host:#{payload[:host]} <-> group:#{payload[:group]}}"
|
|
52
|
+
else
|
|
53
|
+
"{group:#{payload[:group]} <-> host:#{payload[:host]}}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def automatic_group_label(host, perspective:)
|
|
58
|
+
if perspective == :host
|
|
59
|
+
"{host:#{host} <-> group:ungrouped}"
|
|
60
|
+
else
|
|
61
|
+
"{group:ungrouped <-> host:#{host}}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def verb_for(action, perspective)
|
|
66
|
+
return action.to_s.capitalize if perspective == :host
|
|
67
|
+
|
|
68
|
+
action.to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def missing_entity_label(perspective)
|
|
72
|
+
perspective == :host ? 'Group' : 'host'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def existing_status_text(perspective)
|
|
76
|
+
perspective == :host ? 'Already exists, skipping.' : 'already exists, skipping.'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def missing_status_text(perspective)
|
|
80
|
+
perspective == :host ? "Doesn't exist, skipping." : "doesn't exist, skipping."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def warning_text(text, perspective:)
|
|
84
|
+
perspective == :host ? text : "#{text}\n"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'thor'
|
|
5
|
+
|
|
6
|
+
module Moose
|
|
7
|
+
module Inventory
|
|
8
|
+
module Cli
|
|
9
|
+
# Audit log inspection commands.
|
|
10
|
+
class Audit < Thor
|
|
11
|
+
include Moose::Inventory::Cli::Helpers
|
|
12
|
+
|
|
13
|
+
desc 'list', 'List recent append-only audit events'
|
|
14
|
+
option :limit, type: :numeric, default: 20
|
|
15
|
+
option :format, type: :string, desc: 'Emit audit events as yaml|json|pjson'
|
|
16
|
+
def list
|
|
17
|
+
events = inventory_context.audit_events(limit: options[:limit]).map { |event| serialize_event(event) }
|
|
18
|
+
if options[:format]
|
|
19
|
+
fmt.dump(events, options[:format].downcase)
|
|
20
|
+
else
|
|
21
|
+
render_human_events(events)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def serialize_event(event)
|
|
28
|
+
{
|
|
29
|
+
id: event.id,
|
|
30
|
+
created_at: event.created_at,
|
|
31
|
+
actor: event.actor,
|
|
32
|
+
command: event.command,
|
|
33
|
+
action: event.action,
|
|
34
|
+
entity_type: event.entity_type,
|
|
35
|
+
entity_name: event.entity_name,
|
|
36
|
+
details: parse_details(event.details)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_details(details)
|
|
41
|
+
return nil if details.nil? || details.empty?
|
|
42
|
+
|
|
43
|
+
JSON.parse(details)
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
details
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_human_events(events)
|
|
49
|
+
if events.empty?
|
|
50
|
+
puts 'No audit events recorded.'
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
events.each do |event|
|
|
55
|
+
puts "#{event[:id]} #{event[:created_at]} #{event[:command]} " \
|
|
56
|
+
"#{event[:entity_type]}=#{event[:entity_name]} action=#{event[:action]}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Moose
|
|
6
|
+
module Inventory
|
|
7
|
+
module Cli
|
|
8
|
+
# Shared append-only audit recording helpers for mutating CLI commands.
|
|
9
|
+
module AuditRecording
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def record_audit(metadata, result:, dry_run: false)
|
|
13
|
+
return if dry_run
|
|
14
|
+
|
|
15
|
+
inventory_context.record_audit_event(
|
|
16
|
+
command: metadata.fetch(:command),
|
|
17
|
+
action: metadata.fetch(:action),
|
|
18
|
+
actor: ENV.fetch('USER', nil),
|
|
19
|
+
entity_type: metadata.fetch(:entity_type),
|
|
20
|
+
entity_name: Array(metadata.fetch(:entity_names)).join(','),
|
|
21
|
+
details: audit_details(result)
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def audit_details(result)
|
|
26
|
+
JSON.generate(
|
|
27
|
+
warning_count: result.respond_to?(:warning_count) ? result.warning_count : 0,
|
|
28
|
+
events: audit_events_from_result(result)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def audit_events_from_result(result)
|
|
33
|
+
return [] unless result.respond_to?(:events)
|
|
34
|
+
|
|
35
|
+
result.events.map { |event| { type: event.type, payload: event.payload } }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'thor'
|
|
2
|
-
require_relative '
|
|
4
|
+
require_relative 'formatter'
|
|
5
|
+
require_relative 'helpers'
|
|
3
6
|
|
|
4
7
|
module Moose
|
|
5
8
|
module Inventory
|
|
@@ -7,6 +10,8 @@ module Moose
|
|
|
7
10
|
##
|
|
8
11
|
# Class implementing the "group" methods of the CLI
|
|
9
12
|
class Group < Thor
|
|
13
|
+
include Moose::Inventory::Cli::Helpers
|
|
14
|
+
|
|
10
15
|
require_relative 'group_add'
|
|
11
16
|
require_relative 'group_get'
|
|
12
17
|
require_relative 'group_list'
|
|
@@ -18,6 +23,7 @@ module Moose
|
|
|
18
23
|
require_relative 'group_addvar'
|
|
19
24
|
require_relative 'group_listvars'
|
|
20
25
|
require_relative 'group_rmvar'
|
|
26
|
+
require_relative 'group_tags'
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
end
|