foreman_openbolt 1.0.0 → 1.1.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +190 -19
  3. data/Rakefile +17 -93
  4. data/app/controllers/foreman_openbolt/task_controller.rb +61 -49
  5. data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +11 -10
  6. data/app/lib/actions/foreman_openbolt/poll_task_status.rb +70 -60
  7. data/app/models/foreman_openbolt/task_job.rb +16 -17
  8. data/config/routes.rb +0 -1
  9. data/lib/foreman_openbolt/engine.rb +11 -11
  10. data/lib/foreman_openbolt/version.rb +1 -1
  11. data/lib/proxy_api/openbolt.rb +25 -9
  12. data/lib/tasks/foreman_openbolt_tasks.rake +1 -22
  13. data/locale/gemspec.rb +1 -1
  14. data/package.json +11 -15
  15. data/test/acceptance/acceptance_helper.rb +146 -0
  16. data/test/acceptance/docker/docker-compose.yml +69 -0
  17. data/test/acceptance/docker/foreman/Dockerfile +45 -0
  18. data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
  19. data/test/acceptance/docker/target/Dockerfile +29 -0
  20. data/test/acceptance/docker/target/entrypoint.sh +11 -0
  21. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
  22. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
  23. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
  24. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
  25. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
  26. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
  27. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
  28. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
  29. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
  30. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
  31. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
  32. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
  33. data/test/acceptance/fixtures/openbolt.yml +7 -0
  34. data/test/acceptance/tests/error_handling_test.rb +40 -0
  35. data/test/acceptance/tests/host_selector_test.rb +31 -0
  36. data/test/acceptance/tests/launch_task_test.rb +96 -0
  37. data/test/acceptance/tests/parameter_table_test.rb +61 -0
  38. data/test/acceptance/tests/settings_test.rb +95 -0
  39. data/test/acceptance/tests/ssh_options_test.rb +77 -0
  40. data/test/acceptance/tests/task_execution_test.rb +40 -0
  41. data/test/acceptance/tests/task_history_test.rb +84 -0
  42. data/test/acceptance/tests/transport_options_test.rb +121 -0
  43. data/test/test_plugin_helper.rb +12 -3
  44. data/test/unit/controllers/task_controller_test.rb +351 -0
  45. data/test/unit/docker/Dockerfile +47 -0
  46. data/test/unit/docker/docker-compose.yml +33 -0
  47. data/test/unit/docker/entrypoint.sh +4 -0
  48. data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
  49. data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
  50. data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
  51. data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
  52. data/test/unit/models/task_job_test.rb +278 -0
  53. data/webpack/__mocks__/foremanReact/common/I18n.js +15 -0
  54. data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +6 -0
  55. data/webpack/__mocks__/foremanReact/redux/API/index.js +11 -0
  56. data/webpack/src/Components/LaunchTask/FieldTable.js +8 -5
  57. data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +74 -62
  58. data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +11 -13
  59. data/webpack/src/Components/LaunchTask/HostSelector/index.js +28 -33
  60. data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +3 -2
  61. data/webpack/src/Components/LaunchTask/ParameterField.js +2 -0
  62. data/webpack/src/Components/LaunchTask/SmartProxySelect.js +2 -1
  63. data/webpack/src/Components/LaunchTask/TaskSelect.js +3 -3
  64. data/webpack/src/Components/LaunchTask/__tests__/EmptyContent.test.js +10 -0
  65. data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +83 -0
  66. data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +86 -0
  67. data/webpack/src/Components/LaunchTask/__tests__/ParametersSection.test.js +50 -0
  68. data/webpack/src/Components/LaunchTask/__tests__/SmartProxySelect.test.js +63 -0
  69. data/webpack/src/Components/LaunchTask/__tests__/TaskSelect.test.js +39 -0
  70. data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +90 -0
  71. data/webpack/src/Components/LaunchTask/hooks/__tests__/useSmartProxies.test.js +69 -0
  72. data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +103 -0
  73. data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +9 -11
  74. data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +12 -13
  75. data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +6 -13
  76. data/webpack/src/Components/LaunchTask/index.js +9 -27
  77. data/webpack/src/Components/TaskExecution/ExecutionDetails.js +29 -29
  78. data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +9 -10
  79. data/webpack/src/Components/TaskExecution/LoadingIndicator.js +7 -2
  80. data/webpack/src/Components/TaskExecution/ResultDisplay.js +13 -17
  81. data/webpack/src/Components/TaskExecution/TaskDetails.js +58 -67
  82. data/webpack/src/Components/TaskExecution/__tests__/ExecutionDetails.test.js +47 -0
  83. data/webpack/src/Components/TaskExecution/__tests__/ExecutionDisplay.test.js +29 -0
  84. data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +25 -0
  85. data/webpack/src/Components/TaskExecution/__tests__/ResultDisplay.test.js +28 -0
  86. data/webpack/src/Components/TaskExecution/__tests__/TaskDetails.test.js +38 -0
  87. data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +80 -0
  88. data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +177 -0
  89. data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +34 -33
  90. data/webpack/src/Components/TaskExecution/index.js +10 -12
  91. data/webpack/src/Components/TaskHistory/TaskPopover.js +9 -12
  92. data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +109 -0
  93. data/webpack/src/Components/TaskHistory/__tests__/TaskPopover.test.js +26 -0
  94. data/webpack/src/Components/TaskHistory/index.js +21 -29
  95. data/webpack/src/Components/common/HostsPopover.js +12 -3
  96. data/webpack/src/Components/common/__tests__/HostsPopover.test.js +20 -0
  97. data/webpack/src/Components/common/__tests__/helpers.test.js +135 -0
  98. data/webpack/src/Components/common/helpers.js +34 -5
  99. data/webpack/test_setup.js +34 -11
  100. metadata +65 -87
  101. data/test/factories/foreman_openbolt_factories.rb +0 -7
  102. data/test/unit/foreman_openbolt_test.rb +0 -13
  103. data/webpack/global_test_setup.js +0 -11
  104. data/webpack/webpack.config.js +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f42a0b837a7513fd603eaeb6e9b5a75a31be236f6f64d7c8d142404f3d557f1
4
- data.tar.gz: 605ce929b8b03b615cebf42eb343b18bdc6ae3f0169af349f8476be86a8737eb
3
+ metadata.gz: 3747d57bc5198327d01255ec665724e38a46dbfd91fd1e5bd764d6cf3ddd3bb3
4
+ data.tar.gz: ad67d388a524680a567ffdf020e35bbb364471bd190cbdfc597c25d88928a4d2
5
5
  SHA512:
6
- metadata.gz: 490aa0856a8806f9237c1a1284d626290571835881776e92892921d8414f6fa8f6d18edf73a6e7328ff3d666d5d515158474db6097f0e40915a0b2855dc2eb99
7
- data.tar.gz: 92be9c08b82ff27aace85e78005bdf73c905c1d0b2ff2583eda208f535c03c69fa0a3736d48c8e940cedd3eb929a8325a93218efe6873618c777820a0ba84025
6
+ metadata.gz: 1570a5623355cd514276d9e25aece89f2b3f5970bde5efa80db4f9cbe1c4e59eb030b4b5194906d9e9b93f6ec62ac3059e9ed1671cfd612b025d805e3cff7f8e
7
+ data.tar.gz: b335d7b62ff7c6a6d93e39d2ae12c8357ca32b36299313ecc28a23ecc9190d74016a60f03e61f8bacbbf2ba7583cfb15b96b31acf5c21590d59dafc70934fb84
data/README.md CHANGED
@@ -10,8 +10,8 @@ Bringing OpenBolt Task & Plans into Foreman!
10
10
 
11
11
  ## Introduction
12
12
 
13
- [OpenBolt](https://github.com/OpenVoxProject/openbolt) is the open source successor of [bolt](https://github.com/puppetlabs/bolt) by [Perforce](https://www.perforce.com/).
14
- OpenBolt supports running Tasks or Plans against various targets / via different transport protocols.
13
+ [OpenBolt](https://github.com/OpenVoxProject/openbolt) is the open source successor of [Bolt](https://github.com/puppetlabs/bolt) by [Perforce](https://www.perforce.com/).
14
+ OpenBolt supports running Tasks or Plans against various targets via different transport protocols.
15
15
  OpenBolt and Bolt are CLI-only tools.
16
16
  They connect to the targets from a central location (usually a jumpnode or workstation).
17
17
 
@@ -45,21 +45,17 @@ See [How_to_Install_a_Plugin](https://theforeman.org/plugins/#2.Installation) fo
45
45
  The [theforeman/foreman](https://github.com/theforeman/puppet-foreman/blob/master/manifests/plugin/openbolt.pp) puppet module also supports the **Foreman plugin** installation.
46
46
  The [theforeman/foreman_proxy](https://github.com/theforeman/puppet-foreman_proxy/blob/master/manifests/plugin/openbolt.pp) puppet module also supports the **Foreman Smartproxy plugin** installation.
47
47
 
48
- **as of 2025-12-15 integration into the foreman installer is still pending**
49
48
 
50
49
  The Foreman plugin provides UI elements to start Tasks on various nodes.
51
50
  Foreman then talks to a Smartproxy to run OpenBolt.
52
51
  The Smartproxy also establishes the connections to the various targets.
53
- This is usually a ssh or WinRM connection (and soon choria, see [the TODO section](#todo).
52
+ This is usually a ssh or WinRM connection (and soon choria, see [the TODO section](#todo)).
54
53
 
55
54
  You need to have `bolt` in your `$PATH` on the Smartproxy.
56
- You can use the legacy bolt packages from Perforce from the `puppet-tools` repo on [apt.puppet.com](https://apt.puppet.com/) or [yum.puppet.com](https://yum.puppet.com/).
57
- If you have an active Perforce license, you can also download [their commercial bolt version](https://help.puppet.com/bolt/current/topics/bolt_installing.htm).
58
55
  OpenBolt packages are available at [yum.voxpupuli.org](https://yum.voxpupuli.org/) & [apt.voxpupuli.org](https://apt.voxpupuli.org/) in the openvox8 repo.
56
+ You can also use the legacy Bolt packages from Perforce from the `puppet-tools` repo on [apt.puppet.com](https://apt.puppet.com/) or [yum.puppet.com](https://yum.puppet.com/).
59
57
 
60
- **The Foreman integration is tested with OpenBolt and the last Bolt opensource release 4.0.0**
61
-
62
- The integration is supported on Foreman 3.15 and all following versions, including development/nightly builds.
58
+ The integration is supported on Foreman 3.17 and all following versions, including development/nightly builds.
63
59
 
64
60
  OpenBolt relies on Tasks & Plans. They are distributed as puppet modules.
65
61
  The plugin assumes that you deployed your code.
@@ -115,11 +111,118 @@ For failed tasks but also for passed tasks.
115
111
 
116
112
  ![service task passed on two nodes](./ext/task-successful-result.png)
117
113
 
114
+ ## Development
115
+
116
+ ### Linting
117
+
118
+ ```bash
119
+ bundle exec rake lint # Run all linters (rubocop, erb_lint, eslint)
120
+ bundle exec rake lint:fix # Auto-fix where possible
121
+ ```
122
+
123
+ Ruby and ERB linters run directly. The JavaScript linter requires npm dependencies, so either install them locally (`npm install --legacy-peer-deps`) or run lint:js inside a container:
124
+
125
+ ```bash
126
+ CONTAINER=1 bundle exec rake lint:js
127
+ ```
128
+
129
+ ### Unit Tests
130
+
131
+ Unit tests run inside Docker containers with a full Foreman installation. Requires Docker with compose support.
132
+
133
+ ```bash
134
+ bundle exec rake test:unit:up # Build image, start containers, install deps
135
+ bundle exec rake test:unit:ruby # Run Ruby tests
136
+ bundle exec rake test:unit:js # Run JavaScript tests
137
+ bundle exec rake test:unit:all # Run all unit tests
138
+ bundle exec rake test:unit:down # Stop and remove containers
139
+ bundle exec rake test # Shortcut: up, test, down in one step
140
+ ```
141
+
142
+ Set `FOREMAN_VERSION` to test against a specific Foreman version (default: `3.18`):
143
+
144
+ ```bash
145
+ FOREMAN_VERSION=3.17 bundle exec rake test:unit:up
146
+ ```
147
+
148
+ ### Acceptance Tests
149
+
150
+ Acceptance tests exercise the plugin through the browser using Capybara and Selenium. They build RPMs, start a multi-container environment (Foreman + OpenVox + SSH targets + Chromium), and run tests against the real UI.
151
+
152
+ **Prerequisites:**
153
+
154
+ ```bash
155
+ bundle install --with acceptance
156
+ ```
157
+
158
+ The [smart_proxy_openbolt](https://github.com/overlookinfra/smart_proxy_openbolt) and [foreman-packaging](https://github.com/theforeman/foreman-packaging) repos are cloned automatically when needed.
159
+
160
+ **Running:**
161
+
162
+ ```bash
163
+ bundle exec rake acceptance # Full cycle: up, run tests, down
164
+ bundle exec rake acceptance:up # Build RPMs, start Foreman, configure everything
165
+ bundle exec rake acceptance:run # Run tests (requires up first)
166
+ bundle exec rake acceptance:down # Stop containers
167
+ bundle exec rake acceptance:clean # Full reset: stop containers, remove images and artifacts
168
+ ```
169
+
170
+ The `acceptance:up` task is idempotent and can be re-run to pick up new RPM changes. It caches the Foreman Docker image per version so subsequent runs are faster.
171
+
172
+ **Watching tests in the browser:**
173
+
174
+ Set `HEADFUL=1` to disable headless mode, then open `http://localhost:7900` (password: `secret`) to watch the tests via noVNC:
175
+
176
+ ```bash
177
+ HEADFUL=1 bundle exec rake acceptance:run
178
+ ```
179
+
180
+ **Running a subset of tests:**
181
+
182
+ `acceptance:run` accepts `TEST=<path>` to limit which test files are loaded, and `TESTOPTS=<opts>` to forward options (e.g. `--name=/pattern/`) to the Test::Unit autorunner. Both can be combined.
183
+
184
+ ```bash
185
+ # Run every test in one file
186
+ bundle exec rake acceptance:run TEST=test/acceptance/tests/settings_test.rb
187
+
188
+ # Run a single test by exact method name (any file)
189
+ bundle exec rake acceptance:run TESTOPTS='--name=test_echo_task_succeeds_on_all_targets'
190
+
191
+ # Run tests whose name matches a regex within one file
192
+ bundle exec rake acceptance:run \
193
+ TEST=test/acceptance/tests/settings_test.rb \
194
+ TESTOPTS='--name=/host_key/'
195
+ ```
196
+
197
+ **Environment variables:**
198
+
199
+ | Variable | Default | Description |
200
+ |----------|---------|-------------|
201
+ | `CHROMEDRIVER_URL` | `http://localhost:4444` | Selenium WebDriver endpoint |
202
+ | `FOREMAN_BRANCH` | `<version>-stable` | Foreman git branch for unit test image (derived from `FOREMAN_VERSION`) |
203
+ | `FOREMAN_PACKAGING_REPO` | `https://github.com/theforeman/foreman-packaging.git` | Git URL for foreman-packaging (cloned automatically for RPM builds) |
204
+ | `FOREMAN_PASS` | `changeme` | Foreman login password |
205
+ | `FOREMAN_URL` | `https://foreman` | Foreman URL as seen by Chrome. Override to run tests against a live instance |
206
+ | `FOREMAN_USER` | `admin` | Foreman login username |
207
+ | `FOREMAN_VERSION` | `3.18` | Foreman version to test against |
208
+ | `HEADFUL` | unset | Set to `1` to show the browser in noVNC |
209
+ | `SELENIUM_IMAGE` | auto-detected (ARM/x86) | Selenium container image (auto-selects `seleniarm/standalone-chromium` or `selenium/standalone-chrome`) |
210
+ | `SMART_PROXY_OPENBOLT_REF` | `main` | Branch or tag to clone |
211
+ | `SMART_PROXY_OPENBOLT_REPO` | `https://github.com/overlookinfra/smart_proxy_openbolt.git` | Git URL for smart_proxy_openbolt (cloned automatically for RPM builds) |
212
+
213
+ ### Building Packages
214
+
215
+ Build RPM or DEB packages locally using containers. The [foreman-packaging](https://github.com/theforeman/foreman-packaging) repo is cloned automatically:
216
+
217
+ ```bash
218
+ bundle exec rake build:rpm # Build RPM
219
+ bundle exec rake build:deb # Build DEB
220
+ ```
118
221
 
119
222
  ## TODO
120
223
 
121
224
  * Integrate plans into the web UI
122
- * provide a choria transport plugin
225
+ * Provide a choria transport plugin
123
226
 
124
227
  ## Contributing & support
125
228
 
@@ -145,14 +248,82 @@ GNU General Public License for more details.
145
248
  You should have received a copy of the GNU General Public License
146
249
  along with this program. If not, see <http://www.gnu.org/licenses/>.
147
250
 
148
- ## how to release
251
+ ## How to Release
252
+
253
+ ### Version locations
254
+
255
+ The version is maintained in two files:
256
+
257
+ 1. `lib/foreman_openbolt/version.rb` -- the gem version (authoritative source)
258
+ 2. `package.json` -- the npm package version (must match)
259
+
260
+ If the minimum Foreman version changes, also update:
261
+
262
+ 3. `lib/foreman_openbolt/engine.rb` -- `requires_foreman '>= X.Y.Z'`
263
+ 4. `.github/workflows/build.yml` -- default `foreman_version` and `foreman_packaging_ref` inputs
264
+
265
+ ### Release steps
266
+
267
+ 1. Go to [Actions > Prepare Release](../../actions/workflows/prepare_release.yml) and run the workflow with the version to release (e.g. `1.2.0`)
268
+ 2. The workflow bumps the version in `version.rb` and `package.json`, generates the changelog, and opens a PR with the `skip-changelog` label
269
+ 3. Review and merge the PR
270
+ 4. Go to [Actions > Release](../../actions/workflows/release.yml) and run the workflow with the same version
271
+ 5. The release workflow:
272
+ - Verifies the version in `version.rb` matches the input
273
+ - Creates and pushes a git tag
274
+ - Builds the gem
275
+ - Creates a GitHub Release with auto-generated notes and the gem attached
276
+ - Publishes the gem to GitHub Packages
277
+ - Publishes the gem to RubyGems.org (requires the `release` environment)
278
+ - Verifies the gem is available on RubyGems.org
279
+
280
+ ### RPM/DEB packaging
281
+
282
+ After the gem is published to RubyGems, both RPM and DEB packages need to be updated in [theforeman/foreman-packaging](https://github.com/theforeman/foreman-packaging).
283
+
284
+ A bot automatically creates PRs against the `rpm/develop` and `deb/develop` branches to pick up the new gem version. These PRs build packages for Foreman nightly.
285
+
286
+ For stable Foreman releases (currently 3.17 and 3.18), cherry-pick the packaging commits from the develop branches into the corresponding stable branches. For each stable version you want to support:
287
+
288
+ ```bash
289
+ cd foreman-packaging
290
+
291
+ # RPM: cherry-pick from rpm/develop into a branch off the stable target
292
+ git checkout rpm/3.18
293
+ git checkout -b cherry-pick/rubygem-foreman_openbolt-rpm-3.18
294
+ git cherry-pick <commit-from-rpm/develop>
295
+ # Push to your fork and open a PR targeting rpm/3.18
296
+
297
+ # DEB: same approach for the deb side
298
+ git checkout deb/3.18
299
+ git checkout -b cherry-pick/rubygem-foreman-openbolt-deb-3.18
300
+ git cherry-pick <commit-from-deb/develop>
301
+ # Push to your fork and open a PR targeting deb/3.18
302
+ ```
303
+
304
+ PRs against stable branches should be labeled "Stable branch".
305
+
306
+ **Alternative: manual version bump**
307
+
308
+ If the cherry-pick doesn't apply cleanly, you can bump the version manually on the stable branch instead.
309
+
310
+ *RPM:* Checkout the target branch and run `bump_rpm.sh`:
311
+ ```bash
312
+ cd foreman-packaging
313
+ git checkout rpm/3.18
314
+ git checkout -b bump_rpm/rubygem-foreman_openbolt
315
+ ./bump_rpm.sh packages/plugins/rubygem-foreman_openbolt
316
+ # Review changes, push to your fork, and open a PR targeting rpm/3.18
317
+ ```
149
318
 
150
- * bump version in `lib/foreman_openbolt/version.rb` and `package.json`
151
- * run `CHANGELOG_GITHUB_TOKEN=github_pat... bundle exec rake changelog`
152
- * create a PR
153
- * get a review & merge
154
- * create and push a tag
155
- * github actions will publish the tag
319
+ *DEB:* Checkout the target branch and update these files:
320
+ - `debian/gem.list` -- new gem filename
321
+ - `foreman_openbolt.rb` -- new version
322
+ - `debian/control` -- dependency versions (if changed)
323
+ - `debian/changelog` -- add a new entry
156
324
 
157
- The Foreman team packages this gem as Debian package (deb) and as RedHat package (rpm).
158
- They have a bot that will automatically propose an rpm/deb update at [github.com/theforeman/foreman-packaging](https://github.com/theforeman/foreman-packaging/pulls).
325
+ ```bash
326
+ git checkout deb/3.18
327
+ git checkout -b bump_deb/ruby-foreman-openbolt
328
+ # Make the changes above, push to your fork, and open a PR targeting deb/3.18
329
+ ```
data/Rakefile CHANGED
@@ -1,99 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # !/usr/bin/env rake
4
-
5
- begin
6
- require 'rdoc/task'
7
- rescue LoadError
8
- require 'rdoc/rdoc'
9
- require 'rake/rdoctask'
10
- RDoc::Task = Rake::RDocTask
11
- end
12
-
13
- RDoc::Task.new(:rdoc) do |rdoc|
14
- rdoc.rdoc_dir = 'rdoc'
15
- rdoc.title = 'ForemanOpenbolt'
16
- rdoc.options << '--line-numbers'
17
- rdoc.rdoc_files.include('README.rdoc')
18
- rdoc.rdoc_files.include('lib/**/*.rb')
19
- end
20
-
21
- require 'rake/testtask'
22
-
23
- Rake::TestTask.new(:test) do |t|
24
- t.libs << 'lib'
25
- t.libs << 'test'
26
- t.pattern = 'test/**/*_test.rb'
27
- t.verbose = false
28
- end
29
-
30
- begin
31
- require 'rubocop/rake_task'
32
- RuboCop::RakeTask.new
33
- rescue LoadError
34
- puts 'Rubocop not loaded.'
35
- end
36
-
37
- # This is all kinda screwy. Fix it up later.
38
- LINTERS = {
39
- ruby: { cmd: 'rubocop', fix: '--auto-correct' },
40
- erb: { cmd: 'erb_lint', fix: '--autocorrect', glob: '**/*.erb' },
41
- js: { image: 'registry.access.redhat.com/ubi9/nodejs-20:latest', cmd: 'npm run lint --', fix: '--fix' },
42
- }.freeze
43
-
44
- namespace :lint do
45
- def fix?
46
- !ENV['FIX'].nil?
47
- end
48
-
49
- def local?
50
- !ENV['LOCAL'].nil?
51
- end
52
-
53
- def bin
54
- ENV['CONTAINER_BIN'] || 'docker'
55
- end
56
-
57
- LINTERS.each do |name, cfg|
58
- desc "Run #{name} linter#{' (fix)' if fix?}"
59
- task name do
60
- cmd = [cfg[:cmd]]
61
- cmd << cfg[:fix] if fix?
62
- cmd << cfg[:glob] unless cfg[:glob].nil? || cfg[:glob].empty? # rubocop:disable Rails/Blank
63
- cmd = cmd.join(' ')
64
- if cfg[:image] && !local?
65
- cmd = "#{bin} run --rm -v #{Dir.pwd}:/code #{cfg[:image]} /bin/bash -c " +
66
- "'cd /code && npm install --loglevel=error && #{cmd}'"
67
- end
68
- sh cmd
69
- end
70
- end
71
-
72
- desc 'Run all linters'
73
- task all: LINTERS.keys
74
-
75
- desc 'Run all linters and apply fixes'
76
- task :fix do
77
- ENV['FIX'] = 'true'
78
- Rake::Task['lint:all'].invoke
3
+ require_relative 'rakelib/utils/shell'
4
+
5
+ def latest_foreman_version
6
+ result = Shell.capture(
7
+ ['git', 'ls-remote', '--tags', 'https://github.com/theforeman/foreman.git'],
8
+ print_command: false, allowed_exit_codes: [0, 1]
9
+ )
10
+ tags = result.output.scan(%r{refs/tags/([^\s]+)$}).flatten
11
+ versions = tags.filter_map do |tag|
12
+ Gem::Version.new(tag)
13
+ rescue ArgumentError
14
+ nil
79
15
  end
16
+ latest = versions.reject(&:prerelease?).max
17
+ latest ? "#{latest.segments[0]}.#{latest.segments[1]}" : '3.18'
80
18
  end
81
19
 
82
- task default: ['lint:all', 'test']
83
-
84
- begin
85
- require 'rubygems'
86
- require 'github_changelog_generator/task'
20
+ DEFAULT_FOREMAN_VERSION = latest_foreman_version
21
+ FOREMAN_VERSION = ENV.fetch('FOREMAN_VERSION', DEFAULT_FOREMAN_VERSION)
87
22
 
88
- GitHubChangelogGenerator::RakeTask.new :changelog do |config|
89
- config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog github_actions]
90
- config.user = 'overlookinfra'
91
- config.project = 'foreman_openbolt'
92
- gem_version = Gem::Specification.load("#{config.project}.gemspec").version
93
- config.future_release = gem_version
94
- end
95
- rescue LoadError
96
- task :changelog do
97
- abort("Run `bundle install --with release` to install the `github_changelog_generator` gem.")
98
- end
99
- end
23
+ task default: :lint
@@ -13,10 +13,10 @@ module ForemanOpenbolt
13
13
  REDACTED_PLACEHOLDER = '*****'
14
14
 
15
15
  before_action :load_smart_proxy, only: [
16
- :fetch_tasks, :reload_tasks, :fetch_openbolt_options, :launch_task, :job_status, :job_result
16
+ :fetch_tasks, :reload_tasks, :fetch_openbolt_options, :launch_task
17
17
  ]
18
18
  before_action :load_openbolt_api, only: [
19
- :fetch_tasks, :reload_tasks, :fetch_openbolt_options, :launch_task, :job_status, :job_result
19
+ :fetch_tasks, :reload_tasks, :fetch_openbolt_options, :launch_task
20
20
  ]
21
21
  before_action :load_task_job, only: [:job_status, :job_result]
22
22
 
@@ -44,11 +44,18 @@ module ForemanOpenbolt
44
44
  def fetch_openbolt_options
45
45
  options = @openbolt_api.openbolt_options
46
46
 
47
- # Get defaults from Foreman settings
47
+ # Get defaults from Foreman settings.
48
+ # For encrypted settings, show the placeholder only if a non-empty value
49
+ # has been saved, so the UI shows an empty field for unconfigured passwords
50
+ # instead of a misleading placeholder.
48
51
  defaults = {}
49
52
  openbolt_settings.each do |setting|
50
53
  key = setting.name.sub(/^openbolt_/, '')
51
- defaults[key] = setting.encrypted? ? ENCRYPTED_PLACEHOLDER : setting.value if setting.value.present?
54
+ if setting.encrypted?
55
+ defaults[key] = ENCRYPTED_PLACEHOLDER unless setting.value.to_s.empty?
56
+ else
57
+ defaults[key] = setting.value
58
+ end
52
59
  end
53
60
 
54
61
  # Merge the defaults into the options metadata
@@ -60,10 +67,10 @@ module ForemanOpenbolt
60
67
 
61
68
  render json: result
62
69
  rescue ProxyAPI::ProxyException => e
63
- logger.error("OpenBolt API error for fetch_openbolt_options: #{e.message}")
70
+ log_exception('fetch_openbolt_options', e)
64
71
  render_error("Smart Proxy error: #{e.message}", :bad_gateway)
65
72
  rescue StandardError => e
66
- logger.error("Unexpected error in fetch_openbolt_options: #{e.class}: #{e.message}")
73
+ log_exception('fetch_openbolt_options', e)
67
74
  render_error("Internal server error: #{e.message}", :internal_server_error)
68
75
  end
69
76
 
@@ -94,9 +101,12 @@ module ForemanOpenbolt
94
101
  options: options
95
102
  )
96
103
 
97
- logger.info("Task execution response: #{response.inspect}")
104
+ logger.debug("Task execution response: #{response.inspect}")
98
105
 
99
- return render_error("Task execution failed: #{response['error']}", :bad_request) if response['error']
106
+ if response['error']
107
+ error_detail = response['error'].is_a?(Hash) ? response['error']['message'] : response['error']
108
+ return render_error("Task execution failed: #{error_detail}", :bad_request)
109
+ end
100
110
  return render_error('Task execution failed: No job ID returned', :bad_request) unless response['id']
101
111
 
102
112
  metadata = @openbolt_api.tasks[task_name] || {}
@@ -117,15 +127,17 @@ module ForemanOpenbolt
117
127
 
118
128
  render json: {
119
129
  job_id: response['id'],
120
- proxy_id: @smart_proxy.id,
121
- proxy_name: @smart_proxy.name,
122
130
  }
131
+ rescue ArgumentError => e
132
+ # From merge_encrypted_defaults when a user submits the encrypted
133
+ # placeholder for an option that has no saved Foreman setting.
134
+ log_exception('launch_task', e)
135
+ render_error(e.message, :bad_request)
123
136
  rescue ActiveRecord::RecordInvalid => e
124
- logger.error("Failed to create TaskJob: #{e.message}")
137
+ log_exception('launch_task', e)
125
138
  render_error("Database error: #{e.message}", :internal_server_error)
126
139
  rescue StandardError => e
127
- logger.error("Task launch error: #{e.class}: #{e.message}")
128
- logger.error("Backtrace: #{e.backtrace.first(5).join("\n")}")
140
+ log_exception('launch_task', e)
129
141
  render_error("Error launching task: #{e.message}", :internal_server_error)
130
142
  end
131
143
  end
@@ -142,6 +154,10 @@ module ForemanOpenbolt
142
154
  task_description: @task_job.task_description,
143
155
  task_parameters: @task_job.task_parameters,
144
156
  targets: @task_job.targets,
157
+ smart_proxy: {
158
+ id: @task_job.smart_proxy_id,
159
+ name: @task_job.smart_proxy&.name || '(unknown)',
160
+ },
145
161
  }
146
162
  end
147
163
 
@@ -158,10 +174,10 @@ module ForemanOpenbolt
158
174
 
159
175
  # List of all task history
160
176
  def fetch_task_history
177
+ per_page = [(params[:per_page] || 20).to_i, 100].min
161
178
  @task_history = TaskJob.includes(:smart_proxy)
162
179
  .recent
163
- .paginate(page: params[:page],
164
- per_page: (params[:per_page] || 20).to_i)
180
+ .paginate(page: params[:page], per_page: per_page)
165
181
 
166
182
  render json: {
167
183
  results: @task_history.map { |job| serialize_task_job(job) },
@@ -169,18 +185,11 @@ module ForemanOpenbolt
169
185
  page: @task_history.current_page,
170
186
  per_page: @task_history.per_page,
171
187
  }
188
+ rescue StandardError => e
189
+ log_exception('fetch_task_history', e)
190
+ render_error("Error loading task history: #{e.message}", :internal_server_error)
172
191
  end
173
192
 
174
- # Show a specific task job
175
- def show
176
- task_job = TaskJob.find(params[:id])
177
- render json: serialize_task_job(task_job, detailed: true)
178
- rescue ActiveRecord::RecordNotFound
179
- render_error('Task job not found', :not_found)
180
- end
181
-
182
- private
183
-
184
193
  def load_smart_proxy
185
194
  proxy_id = params[:proxy_id]
186
195
  if proxy_id.blank?
@@ -207,7 +216,7 @@ module ForemanOpenbolt
207
216
  begin
208
217
  @openbolt_api = ProxyAPI::Openbolt.new(url: @smart_proxy.url)
209
218
  rescue StandardError => e
210
- logger.error("Failed to initialize OpenBolt API for proxy #{@smart_proxy.name}: #{e.message}")
219
+ log_exception("load_openbolt_api for proxy #{@smart_proxy.name}", e)
211
220
  render_error("Failed to connect to Smart Proxy", :bad_gateway)
212
221
  return false
213
222
  end
@@ -218,14 +227,17 @@ module ForemanOpenbolt
218
227
  def load_task_job
219
228
  job_id = params[:job_id]
220
229
  logger.debug("load_task_job - Job ID: #{job_id}")
221
- return if job_id.blank?
230
+ if job_id.blank?
231
+ render_error('Job ID is required', :bad_request)
232
+ return false
233
+ end
222
234
 
223
235
  @task_job = TaskJob.find_by(job_id: job_id)
224
236
  logger.debug("load_task_job - Task Job: #{@task_job.inspect}")
225
237
  end
226
238
 
227
239
  def openbolt_settings
228
- @openbolt_settings ||= Setting.where("name LIKE 'openbolt_%'")
240
+ @openbolt_settings ||= Foreman.settings.select { |s| s.name.start_with?('openbolt_') }
229
241
  end
230
242
 
231
243
  def encrypted_settings
@@ -233,13 +245,18 @@ module ForemanOpenbolt
233
245
  end
234
246
 
235
247
  def merge_encrypted_defaults(options)
236
- encrypted = options.select { |_, v| v == ENCRYPTED_PLACEHOLDER }
237
- encrypted.each do |key, _|
238
- setting = Setting.find_by(name: "openbolt_#{key}")
239
- raise "Could not find setting called openbolt_#{key}" if setting.nil?
240
- options[key] = setting.value
248
+ merged = options.dup
249
+ merged.each do |key, value|
250
+ next unless value == ENCRYPTED_PLACEHOLDER
251
+
252
+ saved = Setting["openbolt_#{key}"]
253
+ if saved.nil? || saved.to_s.empty?
254
+ raise ArgumentError,
255
+ "No saved value for encrypted option '#{key}'. Configure it in Administer > Settings or provide a value."
256
+ end
257
+ merged[key] = saved
241
258
  end
242
- options
259
+ merged
243
260
  end
244
261
 
245
262
  def scrub_options_for_storage(options)
@@ -256,19 +273,24 @@ module ForemanOpenbolt
256
273
  logger.debug("OpenBolt API call #{method_name} successful for proxy #{@smart_proxy.name}")
257
274
  render json: result
258
275
  rescue ProxyAPI::ProxyException => e
259
- logger.error("OpenBolt API error for #{method_name}: #{e.message}")
276
+ log_exception(method_name, e)
260
277
  render_error("Smart Proxy error: #{e.message}", :bad_gateway)
261
278
  rescue StandardError => e
262
- logger.error("Unexpected error in #{method_name}: #{e.class}: #{e.message}")
279
+ log_exception(method_name, e)
263
280
  render_error("Internal server error: #{e.message}", :internal_server_error)
264
281
  end
265
282
 
283
+ def log_exception(message, exception)
284
+ logger.error("#{message}: #{exception.class}: #{exception.message}")
285
+ logger.error(exception.backtrace.join("\n")) if exception.backtrace
286
+ end
287
+
266
288
  def render_error(message, status)
267
289
  render json: { error: message }, status: status
268
290
  end
269
291
 
270
- def serialize_task_job(task_job, detailed: false)
271
- data = {
292
+ def serialize_task_job(task_job)
293
+ {
272
294
  job_id: task_job.job_id,
273
295
  task_name: task_job.task_name,
274
296
  task_description: task_job.task_description,
@@ -277,22 +299,12 @@ module ForemanOpenbolt
277
299
  status: task_job.status,
278
300
  smart_proxy: {
279
301
  id: task_job.smart_proxy_id,
280
- name: task_job.smart_proxy.name,
302
+ name: task_job.smart_proxy&.name || '(unknown)',
281
303
  },
282
304
  submitted_at: task_job.submitted_at,
283
305
  completed_at: task_job.completed_at,
284
306
  duration: task_job.duration,
285
307
  }
286
-
287
- if detailed
288
- data.merge!(
289
- openbolt_options: task_job.scrubbed_openbolt_options,
290
- result: task_job.result,
291
- log: task_job.log
292
- )
293
- end
294
-
295
- data
296
308
  end
297
309
  end
298
310
  end
@@ -8,16 +8,19 @@ module Actions
8
8
  end
9
9
 
10
10
  def run
11
- proxy = ::SmartProxy.find(input[:proxy_id])
12
- api = ::ProxyAPI::Openbolt.new(url: proxy.url)
11
+ proxy = ::SmartProxy.find_by(id: input[:proxy_id])
12
+ unless proxy
13
+ Rails.logger.warn("Proxy #{input[:proxy_id]} not found during cleanup for job #{input[:job_id]}, skipping")
14
+ return
15
+ end
13
16
 
17
+ api = ::ProxyAPI::Openbolt.new(url: proxy.url)
14
18
  response = api.delete_job_artifacts(job_id: input[:job_id])
15
- Rails.logger.info("Cleaned up artifacts for job #{input[:job_id]}: #{response}")
16
-
17
- Rails.logger.info("Would delete artifacts for job #{input[:job_id]} on proxy #{proxy.name}")
19
+ Rails.logger.debug { "Cleaned up artifacts for job #{input[:job_id]} on proxy #{proxy.name}: #{response}" }
18
20
  rescue StandardError => e
19
21
  # Don't fail the action if cleanup fails - it's not critical
20
- Rails.logger.error("Failed to cleanup artifacts for job #{input[:job_id]}: #{e.message}")
22
+ Rails.logger.error("Failed to cleanup artifacts for job #{input[:job_id]}: #{e.class}: #{e.message}")
23
+ Rails.logger.error(e.backtrace.join("\n")) if e.backtrace
21
24
  end
22
25
 
23
26
  def rescue_strategy
@@ -30,10 +33,8 @@ module Actions
30
33
  end
31
34
 
32
35
  def humanized_input
33
- {
34
- job_id: input[:job_id],
35
- proxy: ::SmartProxy.find_by(id: input[:proxy_id])&.name,
36
- }
36
+ proxy_name = ::SmartProxy.find_by(id: input[:proxy_id])&.name || '(unknown)'
37
+ "job #{input[:job_id]} on #{proxy_name}"
37
38
  end
38
39
  end
39
40
  end