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.
Files changed (176) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +60 -0
  4. data/.gitignore +2 -1
  5. data/.gitleaks.toml +9 -0
  6. data/.rubocop.yml +49 -0
  7. data/BACKLOG.md +752 -24
  8. data/Gemfile +2 -0
  9. data/Gemfile.lock +36 -1
  10. data/README.md +340 -44
  11. data/Rakefile +2 -0
  12. data/bin/moose-inventory +2 -1
  13. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  14. data/docs/compatibility/cli-output-compatibility.md +76 -0
  15. data/docs/governance/approval-register.md +37 -0
  16. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  17. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  18. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  19. data/docs/product/product-brief.md +161 -0
  20. data/docs/product/requirements-baseline.md +477 -0
  21. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  22. data/docs/release/package-provenance-hardening.md +126 -0
  23. data/docs/release/publishing.md +54 -50
  24. data/docs/release/release-environment-protection.md +70 -0
  25. data/docs/release/release-readiness.md +37 -4
  26. data/docs/security/accepted-risk-register.md +84 -0
  27. data/docs/security/security-privacy-process.md +287 -0
  28. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  29. data/docs/security-audit-2026-05-26.md +63 -0
  30. data/docs/ux/cli-workflow-notes.md +287 -0
  31. data/examples/ansible/ansible.cfg +3 -0
  32. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  33. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  34. data/examples/ci/README.md +16 -0
  35. data/examples/ci/github-actions/inventory-review.yml +38 -0
  36. data/examples/ci/inventory/example-snapshot.yml +19 -0
  37. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  38. data/lib/moose_inventory/cli/application.rb +133 -5
  39. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  40. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  41. data/lib/moose_inventory/cli/audit.rb +62 -0
  42. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  43. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  44. data/lib/moose_inventory/cli/console.rb +135 -0
  45. data/lib/moose_inventory/cli/db.rb +64 -0
  46. data/lib/moose_inventory/cli/factory.rb +28 -0
  47. data/lib/moose_inventory/cli/formatter.rb +8 -12
  48. data/lib/moose_inventory/cli/group.rb +7 -1
  49. data/lib/moose_inventory/cli/group_add.rb +91 -73
  50. data/lib/moose_inventory/cli/group_addchild.rb +41 -66
  51. data/lib/moose_inventory/cli/group_addhost.rb +33 -71
  52. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  53. data/lib/moose_inventory/cli/group_get.rb +8 -42
  54. data/lib/moose_inventory/cli/group_list.rb +7 -40
  55. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  56. data/lib/moose_inventory/cli/group_rm.rb +105 -73
  57. data/lib/moose_inventory/cli/group_rmchild.rb +47 -57
  58. data/lib/moose_inventory/cli/group_rmhost.rb +34 -61
  59. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  60. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  61. data/lib/moose_inventory/cli/helpers.rb +143 -0
  62. data/lib/moose_inventory/cli/host.rb +8 -2
  63. data/lib/moose_inventory/cli/host_add.rb +91 -66
  64. data/lib/moose_inventory/cli/host_addgroup.rb +39 -66
  65. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  66. data/lib/moose_inventory/cli/host_get.rb +9 -37
  67. data/lib/moose_inventory/cli/host_list.rb +24 -21
  68. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  69. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  70. data/lib/moose_inventory/cli/host_rmgroup.rb +39 -55
  71. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  72. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  73. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  74. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  75. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  76. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  77. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  78. data/lib/moose_inventory/config/config.rb +185 -108
  79. data/lib/moose_inventory/db/db.rb +188 -193
  80. data/lib/moose_inventory/db/exceptions.rb +6 -3
  81. data/lib/moose_inventory/db/models.rb +16 -0
  82. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  83. data/lib/moose_inventory/inventory_context.rb +116 -0
  84. data/lib/moose_inventory/operations/add_associations.rb +131 -0
  85. data/lib/moose_inventory/operations/add_groups.rb +123 -0
  86. data/lib/moose_inventory/operations/add_hosts.rb +123 -0
  87. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  88. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  89. data/lib/moose_inventory/operations/group_child_relations.rb +125 -0
  90. data/lib/moose_inventory/operations/group_cleanup.rb +70 -0
  91. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  92. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  94. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  95. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  96. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  97. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  98. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  99. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  100. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  101. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  102. data/lib/moose_inventory/operations/remove_associations.rb +113 -0
  103. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  104. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  105. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  106. data/lib/moose_inventory/runtime_options.rb +31 -0
  107. data/lib/moose_inventory/version.rb +3 -1
  108. data/lib/moose_inventory.rb +10 -7
  109. data/moose-inventory.gemspec +22 -35
  110. data/scripts/check.sh +3 -0
  111. data/scripts/ci/check_generated_artifacts.sh +41 -0
  112. data/scripts/ci/check_permissions.sh +5 -0
  113. data/scripts/ci/check_rubocop.sh +33 -0
  114. data/scripts/ci/check_secrets.sh +26 -0
  115. data/scripts/ci/check_security.sh +18 -0
  116. data/scripts/ci/install_security_tools.sh +47 -0
  117. data/scripts/files.rb +5 -4
  118. data/scripts/install_dependencies.sh +2 -0
  119. data/spec/examples/ci_examples_spec.rb +37 -0
  120. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  121. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  122. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  123. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  124. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  125. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  126. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  127. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  128. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  129. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  130. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  131. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  132. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  133. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  134. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  135. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  136. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +165 -85
  137. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +100 -30
  138. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  139. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  140. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  141. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  142. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  143. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  144. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  145. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  146. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  147. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  148. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  149. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  150. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  151. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  152. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  153. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  154. data/spec/lib/moose_inventory/db/db_spec.rb +551 -29
  155. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  156. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  157. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  158. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  159. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +111 -0
  160. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +80 -0
  161. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +82 -0
  162. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  163. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +122 -0
  164. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  165. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  166. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  167. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  168. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  169. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +113 -0
  170. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +78 -0
  171. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  172. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  173. data/spec/shared/shared_config_setup.rb +4 -3
  174. data/spec/spec_helper.rb +50 -40
  175. data/spec/support/cli_harness.rb +33 -0
  176. metadata +163 -35
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- moose-inventory (1.0.9)
4
+ moose-inventory (2.1)
5
5
  indentation (~> 0)
6
6
  json (>= 2.7, < 3)
7
7
  mysql2 (>= 0.5.7, < 0.6)
@@ -13,15 +13,29 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
+ ast (2.4.3)
16
17
  bigdecimal (4.1.2)
18
+ bundler-audit (0.9.3)
19
+ bundler (>= 1.2.0)
20
+ thor (~> 1.0)
17
21
  diff-lcs (1.6.2)
18
22
  docile (1.4.1)
19
23
  indentation (0.1.1)
20
24
  json (2.19.5)
25
+ language_server-protocol (3.17.0.5)
26
+ lint_roller (1.1.0)
21
27
  mysql2 (0.5.7)
22
28
  bigdecimal
29
+ parallel (1.28.0)
30
+ parser (3.3.11.1)
31
+ ast (~> 2.4.1)
32
+ racc
23
33
  pg (1.6.3-x86_64-linux)
34
+ prism (1.9.0)
35
+ racc (1.8.1)
36
+ rainbow (3.1.1)
24
37
  rake (13.4.2)
38
+ regexp_parser (2.12.0)
25
39
  rspec (3.13.2)
26
40
  rspec-core (~> 3.13.0)
27
41
  rspec-expectations (~> 3.13.0)
@@ -35,6 +49,21 @@ GEM
35
49
  diff-lcs (>= 1.2.0, < 2.0)
36
50
  rspec-support (~> 3.13.0)
37
51
  rspec-support (3.13.7)
52
+ rubocop (1.86.2)
53
+ json (~> 2.3)
54
+ language_server-protocol (~> 3.17.0.2)
55
+ lint_roller (~> 1.1.0)
56
+ parallel (>= 1.10)
57
+ parser (>= 3.3.0.2)
58
+ rainbow (>= 2.2.2, < 4.0)
59
+ regexp_parser (>= 2.9.3, < 3.0)
60
+ rubocop-ast (>= 1.49.0, < 2.0)
61
+ ruby-progressbar (~> 1.7)
62
+ unicode-display_width (>= 2.4.0, < 4.0)
63
+ rubocop-ast (1.49.1)
64
+ parser (>= 3.3.7.2)
65
+ prism (~> 1.7)
66
+ ruby-progressbar (1.13.0)
38
67
  sequel (5.104.0)
39
68
  bigdecimal
40
69
  simplecov (0.22.0)
@@ -45,15 +74,21 @@ GEM
45
74
  simplecov_json_formatter (0.1.4)
46
75
  sqlite3 (2.9.4-x86_64-linux-gnu)
47
76
  thor (1.5.0)
77
+ unicode-display_width (3.2.0)
78
+ unicode-emoji (~> 4.1)
79
+ unicode-emoji (4.2.0)
48
80
 
49
81
  PLATFORMS
50
82
  x86_64-linux
51
83
 
52
84
  DEPENDENCIES
53
85
  bundler (>= 2.2.33, < 3)
86
+ bundler-audit (>= 0.9, < 1)
54
87
  moose-inventory!
88
+ parallel (>= 1.10, < 2.0)
55
89
  rake (>= 13.0, < 14)
56
90
  rspec (~> 3)
91
+ rubocop (>= 1.72, < 2)
57
92
  simplecov (~> 0)
58
93
 
59
94
  BUNDLED WITH
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # moose-inventory
2
2
 
3
- The [moose-inventory](https://github.com/RusDavies/moose-inventory) software is a tool for managing dynamic inventories, intended for use with [Ansible](http://www.ansible.com/home).
3
+ The [moose-inventory](https://github.com/RusDavies/moose-inventory) software is a tool for managing dynamic inventories, intended for use with [Ansible](http://www.ansible.com/home).
4
4
 
5
5
  Note 1: For many, the really interesting part of this tool will be it's ability to write to the inventory database from within Ansible, as described at the end of this document. If that's what tickles your fancy, then I encourage you to get a sense of the capability by [jumping to that section first](https://github.com/RusDavies/moose-inventory#writing-to-the-dynamic-inventory-from-ansible). ;o)
6
6
 
7
7
 
8
- Note 2: This software is intended for use on UNIX/Linux systems. It will likely not work on Windows, due to some hard-wired search paths - I may fix that in the future but, for now, sorry.
8
+ Note 2: This software is intended for use on UNIX/Linux systems. It will likely not work on Windows, due to some hard-wired search paths - I may fix that in the future but, for now, sorry.
9
9
 
10
10
  ## Installation
11
11
 
@@ -25,11 +25,11 @@ gem 'moose-inventory'
25
25
 
26
26
 
27
27
  ## Configuration
28
- The [moose-inventory](https://github.com/RusDavies/moose-inventory) tool makes use of a simple YAML configuration file.
28
+ The [moose-inventory](https://github.com/RusDavies/moose-inventory) tool makes use of a simple YAML configuration file.
29
29
 
30
30
 
31
- ###File Location
32
-
31
+ ### File Location
32
+
33
33
  The following locations, in descending order of precedence, are searched for a configuration file:
34
34
 
35
35
  1. location passed via the `--config` option
@@ -38,14 +38,14 @@ The following locations, in descending order of precedence, are searched for a
38
38
  5. ~/local/etc/moose-tools/inventory/config
39
39
  6. /etc/moose-tools/inventory/config
40
40
 
41
- ###Format
42
- The file consists of a mandatory *general* section, and at least one *environment* section. For example:
41
+ ### Format
42
+ The file consists of a mandatory *general* section, and at least one *environment* section. For example:
43
43
  ```yaml
44
44
  ---
45
45
  general:
46
46
  defaultenv: moose_dev
47
47
 
48
- moose_dev:
48
+ moose_dev:
49
49
  db:
50
50
  adapter: "sqlite3"
51
51
  file: "~/.moose/db/dev.db"
@@ -56,7 +56,7 @@ moose_ops:
56
56
  host: "localhost"
57
57
  database: "water"
58
58
  user: "duck"
59
- password: "quack"
59
+ password_env: "MOOSE_INVENTORY_MYSQL_PASSWORD"
60
60
 
61
61
  another_example_section:
62
62
  db:
@@ -64,21 +64,30 @@ another_example_section:
64
64
  host: "localhost"
65
65
  database: "grass"
66
66
  user: "cow"
67
- password: "moo"
67
+ password_env: "MOOSE_INVENTORY_POSTGRES_PASSWORD"
68
68
 
69
69
  ```
70
70
 
71
- ###The *general* section
72
- The general section is mandatory, and contains a single parameter **defaultenv**, which points to the name of the default environment section.
71
+ ### The *general* section
72
+ The general section is mandatory, and contains a single parameter **defaultenv**, which points to the name of the default environment section.
73
73
 
74
- ###Environment sections
74
+ ### Environment sections
75
75
  You may add as many environment sections as you desire. The intention is to enable the user to easily manage multiple environments, such as development, staging, production, etc., via a single configuration file. The name of each environment section must be unique, but can otherwise be any valid YAML tag.
76
76
 
77
- At present, each environment section contains only a **db** subsection, describing database connection parameters. Additional subsections may be added in the future, as functionality increases.
77
+ At present, each environment section contains only a **db** subsection, describing database connection parameters. Additional subsections may be added in the future, as functionality increases.
78
78
 
79
79
  Each **db** section must include an **adapter** parameter. Currently supported adapter types are *sqlite3*, *mysql*, and *postgresql*. The test suite exercises SQLite with a local database file and includes adapter dispatch/error-path smoke coverage for MySQL and PostgreSQL without requiring live database servers.
80
80
 
81
- Additional parameters are also required in the **db** subsection, depending on the adapter type. For the *sqlite3* adapter only a **file** parameter is required; parent directories are created automatically. For both *mysql* and *postgresql*, **host**, **database**, **user**, and **password** are required.
81
+ Additional parameters are also required in the **db** subsection, depending on the adapter type. For the *sqlite3* adapter only a **file** parameter is required; parent directories are created automatically. For both *mysql* and *postgresql*, **host**, **database**, **user**, and either **password_env** or **password** are required.
82
+
83
+ Prefer **password_env** for MySQL and PostgreSQL configuration. Its value is the name of an environment variable that contains the database password, which keeps reusable configuration files from carrying plaintext credentials:
84
+
85
+ ```sh
86
+ export MOOSE_INVENTORY_MYSQL_PASSWORD='use-a-real-secret-here'
87
+ moose-inventory --env moose_ops host list
88
+ ```
89
+
90
+ The older **password** key is still supported for compatibility, but avoid committing configuration files that contain database passwords. If you must use **password**, keep that configuration file outside version control and restrict its file permissions.
82
91
 
83
92
 
84
93
  ## Usage
@@ -89,19 +98,19 @@ The tool itself provides a convenient help feature. For example, try each of th
89
98
  $ moose-inventory help group
90
99
  $ moose-inventory group help add
91
100
 
92
- ###Global switches
101
+ ### Global switches
93
102
 
94
103
  #### Option `--config <FILE>`
95
104
  The `--config` flag sets the configuration file to be used. If specified, then the file must exist. This takes precedence over all other config files in other locations. If not provided, then the default is to search the locations previously mentioned.
96
105
 
97
- For example,
106
+ For example,
98
107
 
99
108
  $ moose-inventory --config ./mystuff.conf host list
100
109
 
101
110
  #### Option `--env <SECTION>`
102
- The *--env* flag sets the section in the configuration file to be used as the environment configuration. If set, then the section must exist. If not set, then what ever default is provided by the **defaultenv** parameter will be used.
111
+ The *--env* flag sets the section in the configuration file to be used as the environment configuration. If set, then the section must exist. If not set, then what ever default is provided by the **defaultenv** parameter will be used.
103
112
 
104
- For example,
113
+ For example,
105
114
 
106
115
  $ moose-inventory --env my_section host list
107
116
 
@@ -116,13 +125,216 @@ For example,
116
125
  :groups:
117
126
  - ungrouped
118
127
 
119
- ###Transactional Behaviour
120
- The *moose-inventory* tool performs database operations in a transactional manner. That is to say, either all operations of a command succeed, or they are all rolled back.
128
+ ### Transactional Behaviour
129
+ The *moose-inventory* tool performs database operations in a transactional manner. That is to say, either all operations of a command succeed, or they are all rolled back.
130
+
131
+ ### Dry-run and plan output
132
+ Mutating commands support a `--dry-run` option. This renders the same kind of progress output as the real command, but does not write anything to the database. This is useful when checking inventory surgery before applying it, particularly for operations that affect automatic `ungrouped` associations or child-group cleanup.
133
+
134
+ Examples:
135
+
136
+ $ moose-inventory host add web01 --groups web --dry-run
137
+ Add host 'web01':
138
+ - Creating host 'web01'...
139
+ - OK
140
+ - Adding association {host:web01 <-> group:web}...
141
+ - OK
142
+ - All OK
143
+ Dry run complete. No changes applied.
144
+ Succeeded
145
+
146
+ $ moose-inventory group rm --recursive old_parent_group --dry-run
147
+ $ moose-inventory host addvar web01 owner=russ env=prod --dry-run
148
+ $ moose-inventory group addhost web web01 web02 --dry-run
149
+ $ moose-inventory group rmchild --delete-orphans parent_group child_group --dry-run
150
+
151
+ The following mutating command families support `--dry-run`:
152
+
153
+ 1. `host add` and `host rm`
154
+ 2. `group add` and `group rm`
155
+ 3. `host addvar`, `host rmvar`, `group addvar`, and `group rmvar`
156
+ 4. `host addgroup`, `host rmgroup`, `group addhost`, and `group rmhost`
157
+ 5. `group addchild` and `group rmchild`
158
+
159
+ For automation and review workflows, dry-run events can also be emitted as YAML, JSON, or pretty JSON with `--plan-format`. This option requires `--dry-run`; without it, the command aborts before making changes.
160
+
161
+ Destructive removal commands require an explicit acknowledgement before they write. Use `--dry-run` to preview, or add `--yes` when you intentionally want to apply a removal non-interactively. This applies to host/group deletion, variable removal, association removal, child-group dissociation, and metadata tag removal commands.
162
+
163
+ $ moose-inventory host rm old-web01 --dry-run
164
+ $ moose-inventory host rm old-web01 --yes
165
+ $ moose-inventory group rm --recursive old_parent_group --yes
166
+
167
+ $ moose-inventory host add web01 --groups web --dry-run --plan-format pjson
168
+ {
169
+ "command": "host add",
170
+ "dry_run": true,
171
+ "changes_applied": false,
172
+ "events": [
173
+ {
174
+ "type": "host_started",
175
+ "payload": {
176
+ "name": "web01"
177
+ }
178
+ }
179
+ ]
180
+ }
181
+
182
+ The actual `events` array includes the full ordered plan for the command. Each event has a `type` and a `payload`, so scripts can inspect planned host, group, variable, association, automatic `ungrouped`, and child-group cleanup actions without scraping human-readable output.
183
+
184
+ CLI output compatibility is governed by `CLI-OUTPUT-v1` in `docs/compatibility/cli-output-compatibility.md`. Machine-readable JSON/YAML/pjson structures are the supported automation interface. Documented human-readable output is also compatibility-protected when tests, README examples, or release notes rely on it, but scripts should prefer machine-readable formats.
185
+
186
+ ### Import and export snapshots
187
+ The full inventory can be exported as a portable snapshot. The snapshot contains a version number, hosts, host variables, host-to-group memberships, host/group tags, groups, group variables, and child-group relationships. It is intended for review, backup, migration, and automation workflows.
188
+
189
+ $ moose-inventory --format yaml export inventory.yml
190
+ Exported inventory snapshot to inventory.yml.
191
+
192
+ $ moose-inventory --format pjson export
193
+ {
194
+ "version": 1,
195
+ "hosts": {
196
+ "web01": {
197
+ "groups": [
198
+ "web"
199
+ ],
200
+ "tags": [
201
+ "prod"
202
+ ],
203
+ "vars": {
204
+ "env": "prod"
205
+ }
206
+ }
207
+ },
208
+ "groups": {
209
+ "web": {
210
+ "children": [],
211
+ "tags": [
212
+ "frontend"
213
+ ],
214
+ "vars": {
215
+ "role": "frontend"
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ Snapshots can be imported from YAML or JSON. Import validates the file before writing anything. It rejects malformed snapshots, unknown host/group references, unsupported fields, invalid variable shapes, and circular child-group hierarchies.
222
+
223
+ Use `--preview` to validate a snapshot and review its additive import diff without writing to the database. For automation/review gates, add `--preview-format yaml|json|pjson`; the preview uses `snapshot-import-preview-v1`, reports creates, variable updates, association additions, unchanged items, existing records that are absent from the snapshot and therefore ignored, and confirms that destructive changes are not part of normal import.
224
+
225
+ $ moose-inventory import inventory.yml
226
+ Imported inventory snapshot from inventory.yml.
227
+ Created hosts: 1
228
+ Created groups: 1
229
+ Variables changed: 2
230
+ Associations added: 1
231
+
232
+ $ moose-inventory import inventory.yml --preview --preview-format pjson
233
+
234
+ Import is additive and update-oriented: it creates missing hosts and groups, adds missing associations and tags, and creates or updates variables found in the snapshot. It does not delete existing inventory records that are absent from the file. Use a fresh database when you want the imported snapshot to be the whole world, because databases are notoriously bad at guessing intent.
235
+
236
+ ### Inventory doctor
237
+ The `doctor` command runs read-only inventory health checks and exits with a non-zero status if it finds issues. This makes it suitable for CI checks, release gates, and pre-change reviews.
238
+
239
+ $ moose-inventory doctor
240
+ Inventory doctor found no issues.
241
+
242
+ When findings are present, the human-readable output lists each issue with a severity and check id:
243
+
244
+ $ moose-inventory doctor
245
+ Inventory doctor found 2 issue(s):
246
+ - [warning] host_only_in_ungrouped: Host 'web01' is only in automatic group 'ungrouped'.
247
+ - [warning] orphaned_group: Group 'old_web' has no parents and no hosts.
248
+
249
+ For automation, use `--format yaml`, `--format json`, or `--format pjson` on the doctor command itself:
250
+
251
+ $ moose-inventory doctor --format pjson
252
+ {
253
+ "ok": false,
254
+ "issue_count": 1,
255
+ "issues": [
256
+ {
257
+ "id": "host_only_in_ungrouped",
258
+ "severity": "warning",
259
+ "message": "Host 'web01' is only in automatic group 'ungrouped'.",
260
+ "subject": "web01"
261
+ }
262
+ ]
263
+ }
264
+
265
+ Current doctor checks include missing database configuration, plaintext database passwords, hosts only in `ungrouped`, orphaned groups, empty groups, duplicate-ish names, invalid variable records, and circular child-group relationships.
266
+
267
+ ### Metadata tags
268
+ Hosts and groups can carry metadata tags that are separate from Ansible variables. Use tags for operational labels such as environment, owner, lifecycle, location, role, or criticality when you want metadata without exposing it as inventory variables.
269
+
270
+ $ moose-inventory host addtag web01 prod critical owner-platform
271
+ Added host tag(s) to 'web01': prod, critical, owner-platform.
272
+
273
+ $ moose-inventory host listtags web01
274
+ Host 'web01' tags: critical, owner-platform, prod
275
+
276
+ $ moose-inventory host rmtag web01 critical
277
+ Removed host tag(s) from 'web01': critical.
278
+
279
+ Groups support the same tag commands:
280
+
281
+ $ moose-inventory group addtag web frontend public-edge
282
+ $ moose-inventory group listtags web --format json
283
+
284
+ Tag names are case-insensitive operational metadata: CLI tag commands and snapshot imports normalize them to lowercase, strip surrounding whitespace, deduplicate repeated values, and store them in portable join tables. Tag add/remove operations are audited when they change state.
285
+
286
+ ### Audit log / change history
287
+ Moose Inventory records append-only audit events for successful mutating CLI commands. Dry-run commands are intentionally excluded, because planned changes are already available through `--plan-format` and did not actually mutate inventory state.
121
288
 
122
- ###Walk-through example
123
- This walk-through goes through the process of creating three hosts and three groups, assigning variables to some of each, and then associating hosts with groups. Once done, each association, variable, group, and host are removed.
289
+ Audit events record when the change happened, the local actor from `USER`, the command/action, the entity type/name, and structured operation details. The audit log is deliberately small: it is for debugging and accountability, not yet a full rollback system.
124
290
 
125
- We start by creating three hosts, in this case named *host1*, *host2*, and *host3*. Note, we can add as many hosts as we desire via this single command. Also, although we have used short names here, we could equally have used fully qualified names.
291
+ List recent events in a human-readable form:
292
+
293
+ $ moose-inventory audit list
294
+ 12 2026-05-28T17:01:02Z host add host=app01 action=add
295
+
296
+ Machine-readable output is available for scripts and support bundles:
297
+
298
+ $ moose-inventory audit list --format yaml
299
+ $ moose-inventory audit list --format json
300
+ $ moose-inventory audit list --format pjson
301
+
302
+ The default limit is 20 events; use `--limit` to inspect more history:
303
+
304
+ $ moose-inventory audit list --limit 100
305
+
306
+ ### Database lifecycle commands
307
+ Moose Inventory records a small schema metadata table and exposes database lifecycle commands under `db`. These commands are intentionally conservative: they inspect, create missing schema metadata, and back up SQLite databases, but they do not silently rewrite production databases into a modern art installation.
308
+
309
+ $ moose-inventory db status
310
+ Adapter: sqlite3
311
+ Schema version: 4
312
+ Expected schema version: 4
313
+ SQLite file: /home/russ/.moose/db/dev.db
314
+ Tables:
315
+ - hosts: present
316
+ - hostvars: present
317
+ - groups: present
318
+
319
+ $ moose-inventory db doctor
320
+ Database doctor found no issues.
321
+
322
+ $ moose-inventory db migrate
323
+ Database schema is at version 4.
324
+
325
+ `db migrate` runs explicit ordered schema migrations up to the current schema version. The current migration chain is `1 -> 2 -> 3 -> 4`: version 1 creates the core inventory tables and schema metadata, version 2 adds audit history, version 3 adds tag metadata, and version 4 adds DB-level uniqueness and lookup indexes for variables, host/group relationships, group-child relationships, and tag joins. Moose Inventory refuses to open or migrate a database whose recorded schema version is newer than the tool supports; upgrade the tool instead of letting old code write to a future schema. `db doctor` reports missing known tables in a dirty or partially migrated database.
326
+
327
+ SQLite users can create a direct database-file backup:
328
+
329
+ $ moose-inventory db backup ./backup/moose-inventory.sqlite3
330
+ Backed up database to /absolute/path/backup/moose-inventory.sqlite3.
331
+
332
+ `db backup` is currently supported for SQLite only. For MySQL/MariaDB and PostgreSQL, use native database tools such as `mysqldump`, `mariadb-dump`, `pg_dump`, managed-service snapshots, or equivalent backup systems, because those engines already have adult supervision built in. Moose Inventory does not run server-backed restore commands, manage database users/grants, or implement destructive snapshot sync/restore behavior. See `docs/maintenance/database-backup-restore-guidance.md` for adapter-specific backup and restore boundaries.
333
+
334
+ ### Walk-through example
335
+ This walk-through goes through the process of creating three hosts and three groups, assigning variables to some of each, and then associating hosts with groups. Once done, each association, variable, group, and host are removed.
336
+
337
+ We start by creating three hosts, in this case named *host1*, *host2*, and *host3*. Note, we can add as many hosts as we desire via this single command. Also, although we have used short names here, we could equally have used fully qualified names.
126
338
 
127
339
  $ moose-inventory add host host1 host2 host3
128
340
  Add host 'host1':
@@ -145,7 +357,7 @@ We start by creating three hosts, in this case named *host1*, *host2*, and *hos
145
357
  - all OK
146
358
  Succeeded.
147
359
 
148
- Notice that each host is initially associated with an automatic group, *ungrouped*.
360
+ Notice that each host is initially associated with an automatic group, *ungrouped*.
149
361
 
150
362
  Now we can list our hosts, to see that they are stored as expected. In this example, we will request the output be formatted as YAML. If we didn't specify a format, then it would default to regular JSON.
151
363
 
@@ -168,7 +380,7 @@ Now we can list our hosts, to see that they are stored as expected. In this exa
168
380
  }
169
381
  }
170
382
 
171
- The *host list* command simply lists all hosts, in the order that they were entered into the database. We can also get a specific host, or hosts, by name. In this example, we'll get only *host3* and *host1*, outputting the result in YAML.
383
+ The *host list* command simply lists all hosts, in the order that they were entered into the database. We can also get a specific host, or hosts, by name. In this example, we'll get only *host3* and *host1*, outputting the result in YAML.
172
384
 
173
385
  $ moose-inventory host get host3 host1 --format yaml
174
386
  ---
@@ -191,7 +403,7 @@ Now we'll add some host variables. Again, we can add as many variables to a hos
191
403
  - OK
192
404
  - all OK
193
405
  Succeeded.
194
-
406
+
195
407
  $ moose-inventory host addvar host2 owner=caroline id=54321
196
408
  Add variables 'owner=caroline,id=54321' to host 'host2':
197
409
  - retrieve host 'host2'...
@@ -225,7 +437,13 @@ Let's list our hosts again, to see what that looks like.
225
437
 
226
438
  As you can see, the hosts with variables each have a new section, hostvars, in which those variables are listed. Try also with *--format pjson*.
227
439
 
228
- We can do the same with groups. In the following example, the output has been omitted for compactness. Nevertheless, you will see that the form of the commands is as for hosts. Of note, when listing the groups, you will see that the *ungrouped* group is shown. This is an automatic group which cannot be manipulated manually.
440
+ Host listing can also be filtered by group, metadata tag, and host variable. Multiple comma-separated values are treated as an AND filter: the host must match all requested groups, all requested tags, and all requested variable key/value pairs.
441
+
442
+ $ moose-inventory host list --group web --tag prod --var os=fedora --format yaml
443
+
444
+ Variable filters use `key=value` syntax. Metadata tags appear under a `tags` section when present; hosts without tags keep the older compact output. Group-side listing filters are still part of the remaining query/filter backlog, because one haunted query surface per slice is plenty.
445
+
446
+ We can do the same with groups. In the following example, the output has been omitted for compactness. Nevertheless, you will see that the form of the commands is as for hosts. Of note, when listing the groups, you will see that the *ungrouped* group is shown. This is an automatic group which cannot be manipulated manually.
229
447
 
230
448
  $ moose-inventory group add group1 group2 group3
231
449
  $ moose-inventory group list --format yaml
@@ -235,7 +453,7 @@ We can do the same with groups. In the following example, the output has been o
235
453
 
236
454
  At this point, we have three hosts and three groups, some of each with variables. Let's now associate hosts with groups. We can either associate one or more hosts with a group,
237
455
 
238
- $ moose-inventory group addhost group1 host1 host2
456
+ $ moose-inventory group addhost group1 host1 host2
239
457
  Associate group 'group1' with host(s) 'host1,host2':
240
458
  - retrieve group 'group1'...
241
459
  - OK
@@ -252,7 +470,7 @@ At this point, we have three hosts and three groups, some of each with variables
252
470
 
253
471
  or one or more groups with a host,
254
472
 
255
- $ moose-inventory host addgroup host3 group2 group3
473
+ $ moose-inventory host addgroup host3 group2 group3
256
474
  Associate host 'host3' with groups 'group2,group3':
257
475
  - Retrieve host 'host3'...
258
476
  - OK
@@ -303,20 +521,93 @@ We can also list hosts, to get the host-centric view.
303
521
  - group2
304
522
  - group3
305
523
 
306
- Removing variables, groups, and hosts is just as easy. In the following examples, the output is again omitted for compactness; the reader is encouraged to work along to experience the tool. Note, that although we show how to remove the variables, it is not strictly necessary to do so in this example, since deleting hosts and groups would delete all associated variables anyway.
524
+ ### Read-only console
525
+ For human browsing, Moose Inventory includes a small read-only console. It is intentionally conservative: the first console slice lets operators inspect inventory state, tags, and recent audit events, but does not mutate records.
526
+
527
+ $ moose-inventory console
528
+ Moose Inventory console (read-only). Type help or quit.
529
+
530
+ Useful console commands include:
531
+
532
+ help
533
+ hosts
534
+ groups
535
+ host web01
536
+ group web
537
+ tags host web01
538
+ tags group web
539
+ audit 10
540
+ quit
541
+
542
+ Console parsing uses shell-style quoting for read-only lookups, so names containing spaces can be inspected without turning the prompt into confetti:
543
+
544
+ host "web 01"
545
+ tags group 'production web'
546
+
547
+ The console reports command-specific usage for extra arguments, invalid tag targets, invalid audit limits, and malformed quotes. Use the normal CLI commands for edits. Future interactive mutation can be added with confirmation, dry-run, and audit semantics instead of improvising a tiny foot-gun in a prompt loop.
548
+
549
+ Removing variables, groups, and hosts is just as easy. In the following examples, the output is again omitted for compactness; the reader is encouraged to work along to experience the tool. Note, that although we show how to remove the variables, it is not strictly necessary to do so in this example, since deleting hosts and groups would delete all associated variables anyway.
550
+
551
+ By default, deleting a group preserves its child groups as root groups. Use `group rm --recursive` when child groups that become orphaned should also be deleted. Similarly, `group rmchild --delete-orphans` removes a parent-child association and deletes the child subtree only when it becomes orphaned by that removal. Hosts whose last group is deleted are automatically moved to `ungrouped`.
307
552
 
308
553
  $ moose-inventory group rmvar group1 location
309
554
  $ moose-inventory group rm group1 group2 group3
555
+ $ moose-inventory group rm --recursive old_parent_group
556
+ $ moose-inventory group rmchild --delete-orphans parent_group child_group
310
557
  $ moose-inventory host rmvar
311
558
  $ moose-inventory host rmvar host1 owner id
312
559
  $ moose-inventory host rm host1 host2 host3
313
560
 
561
+ ### CI/CD integration examples
562
+ The `examples/ci/` directory contains a pull-request review pattern for inventory changes that does not require production database credentials. It imports a proposed snapshot into a temporary SQLite database, runs `doctor`, exports a canonical snapshot, lists hosts, and writes an Ansible-compatible inventory artifact.
563
+
564
+ Run the example locally with:
565
+
566
+ $ MOOSE_INVENTORY_CMD="bundle exec ruby -Ilib bin/moose-inventory" \
567
+ examples/ci/scripts/validate-inventory-snapshot.sh \
568
+ examples/ci/inventory/example-snapshot.yml \
569
+ tmp/inventory-ci-artifacts
570
+
571
+ The script writes:
572
+
573
+ tmp/inventory-ci-artifacts/doctor.txt
574
+ tmp/inventory-ci-artifacts/inventory.yml
575
+ tmp/inventory-ci-artifacts/hosts.json
576
+ tmp/inventory-ci-artifacts/ansible-inventory.json
577
+
578
+ `examples/ci/github-actions/inventory-review.yml` shows the same pattern as a GitHub Actions workflow. It is stored under `examples/` rather than `.github/workflows/` so teams can adapt paths, snapshot locations, artifact names, and deployment rules before enabling it. Use this as a review gate before applying inventory changes to a shared or production Moose Inventory database; CI should validate proposals, not casually scribble on prod like a bored intern.
579
+
314
580
  ### Using moose-inventory with Ansible
315
581
 
316
582
 
317
583
  The *moose-inventory* tool is compliant with the Ansible specifications for [dynamic inventory sources](http://docs.ansible.com/developing_inventory.html).
318
584
 
319
- However, to make use of *moose-inventory's* multiple environment and configuration file options, a shim script should be used as the target for the [external inventory script](http://docs.ansible.com/intro_dynamic_inventory.html). A trivial example may look something like the following.
585
+ The preferred modern integration is the example inventory plugin shipped in `examples/ansible/inventory_plugins/moose_inventory.py`. Copy or vendor that plugin into your Ansible project, then point `ansible.cfg` at the plugin directory and inventory source file:
586
+
587
+ ```ini
588
+ [defaults]
589
+ inventory = inventory/moose_inventory.yml
590
+ inventory_plugins = inventory_plugins
591
+ ```
592
+
593
+ The inventory source file is plain YAML:
594
+
595
+ ```yaml
596
+ ---
597
+ plugin: moose_inventory
598
+ executable: moose-inventory
599
+ config: ./example.conf
600
+ env: dev
601
+ ```
602
+
603
+ With those files in place, Ansible can use Moose Inventory directly:
604
+
605
+ $ ansible-inventory -i inventory/moose_inventory.yml --list
606
+ $ ansible -i inventory/moose_inventory.yml -u ubuntu us-east-1d -m ping
607
+
608
+ The plugin calls `moose-inventory` for group and host data, preserving Moose Inventory's own configuration file and environment selection instead of hiding them in a shell wrapper. The shipped `examples/ansible/` directory contains a complete minimal `ansible.cfg`, inventory source, and plugin file.
609
+
610
+ A legacy external-inventory shim still works, and remains useful on older Ansible installs or when you want the simplest possible integration. To make use of *moose-inventory's* multiple environment and configuration file options with the shim approach, use a script as the target for the [external inventory script](http://docs.ansible.com/intro_dynamic_inventory.html). A trivial example may look something like the following.
320
611
 
321
612
  ```shell
322
613
  #!/bin/bash
@@ -333,16 +624,16 @@ exit $?
333
624
 
334
625
  $ ./shim.sh host add example
335
626
  $ ./shim.sh host addvar example "my var"="hello world"
336
-
337
627
 
338
- When Ansible calls the external inventory script, it passes certain parameters, which *moose-inventory* automatically recognises and responds to. The Ansible parameters, and their equivalent *moose-inventory* parameters are shown below.
628
+
629
+ When Ansible calls the external inventory script, it passes certain parameters, which *moose-inventory* automatically recognises and responds to. The Ansible parameters, and their equivalent *moose-inventory* parameters are shown below.
339
630
 
340
631
  Ansible | moose-inventory
341
632
  ---------------- |-------------
342
- `--list` | `--ansible group list`
633
+ `--list` | `--ansible group list`
343
634
  `--host HOSTNAME` | `--ansible host listvars HOSTNAME`
344
635
 
345
- Note, the above conversions are performed automatically within *moose-inventory*.
636
+ Note, the above conversions are performed automatically within *moose-inventory*.
346
637
 
347
638
  With *moose-inventory* installed and configured, and a shim script (e.g. *shim.sh*) in place, then integration with Ansible can be acheived via Ansible's `-i <file>` option.
348
639
 
@@ -351,18 +642,18 @@ With *moose-inventory* installed and configured, and a shim script (e.g. *shim.s
351
642
  Alternatively, if using an [Ansible configuration file](http://docs.ansible.com/intro_configuration.html), then one may set the [inventory](http://docs.ansible.com/intro_configuration.html#inventory) option,
352
643
 
353
644
  inventory = ./shim.sh
354
-
355
- Yet another option is to copy the shim script to */etc/ansible/hosts* and `chmod +x` it. However, since this would essentially fix the config file and environment used, doing so would defeat the flexibility intended for *moose-inventory*.
645
+
646
+ Yet another option is to copy the shim script to */etc/ansible/hosts* and `chmod +x` it. However, since this would essentially fix the config file and environment used, doing so would defeat the flexibility intended for *moose-inventory*.
356
647
 
357
648
  #### Writing to the dynamic inventory from Ansible
358
649
  A useful aspect of dynamic inventories is the possibility of writing data to the inventory. To persist data from Ansible to the inventory, simply call the shim script via a local_action command, for example:
359
650
 
360
651
  ```shell
361
652
  - set_fact: mydata="Hello world"
362
- - local_action: command shim.sh host addvar {{ inventory_hostname }} mydata="{{ mydata }}"
653
+ - local_action: command shim.sh host addvar {{ inventory_hostname }} mydata="{{ mydata }}"
363
654
  ```
364
655
 
365
-
656
+
366
657
  ## Development checks
367
658
 
368
659
  Run the local verification gate before committing changes:
@@ -371,7 +662,15 @@ Run the local verification gate before committing changes:
371
662
  ./scripts/check.sh
372
663
  ```
373
664
 
374
- The check script runs the RSpec suite and enforces the SimpleCov coverage minimum.
665
+ The check script runs the RSpec suite, enforces the SimpleCov coverage minimum, checks file permissions, verifies generated/local artifact paths remain ignored and untracked, queries OSV for locked RubyGems advisories, runs `bundler-audit`, runs `gitleaks` when available, and builds/smoke-tests the packaged gem.
666
+
667
+ Optional Go-based security tools used by CI can be installed locally with:
668
+
669
+ ```shell
670
+ ./scripts/ci/install_security_tools.sh
671
+ ```
672
+
673
+ That installs `gitleaks` and `osv-scanner` into `tmp/security-tools/bin` unless they are already on `PATH`. Fedora users can also run `./scripts/install_dependencies.sh` to install the native build dependencies and packaged `gitleaks`; `bundler-audit` is installed through Bundler.
375
674
 
376
675
  ## Contributing
377
676
  1. Fork it (https://github.com/RusDavies/moose-inventory/fork )
@@ -381,9 +680,6 @@ The check script runs the RSpec suite and enforces the SimpleCov coverage minimu
381
680
  5. Create a new Pull Request
382
681
 
383
682
 
384
-
385
-
386
-
387
683
 
388
684
 
389
685
 
data/Rakefile CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
1
2
 
3
+ require 'bundler/gem_tasks'