foreman_openbolt 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3747d57bc5198327d01255ec665724e38a46dbfd91fd1e5bd764d6cf3ddd3bb3
4
- data.tar.gz: ad67d388a524680a567ffdf020e35bbb364471bd190cbdfc597c25d88928a4d2
3
+ metadata.gz: b212e4874311ac07caa39290021ef940849d6a64c4e903f393497b2cf97654a5
4
+ data.tar.gz: e8658c692b134da9b542234b30064f6274bd0b1aed2ad4f25ef5397b015afeb8
5
5
  SHA512:
6
- metadata.gz: 1570a5623355cd514276d9e25aece89f2b3f5970bde5efa80db4f9cbe1c4e59eb030b4b5194906d9e9b93f6ec62ac3059e9ed1671cfd612b025d805e3cff7f8e
7
- data.tar.gz: b335d7b62ff7c6a6d93e39d2ae12c8357ca32b36299313ecc28a23ecc9190d74016a60f03e61f8bacbbf2ba7583cfb15b96b31acf5c21590d59dafc70934fb84
6
+ metadata.gz: 8fcfe54eef80b1951fb8cc483659b6219ede35010b120670b2dab5de998787bcc965bc3b73756d2ba7c9a684cc75a4fb007c7bc85a619f42168d63982029f275
7
+ data.tar.gz: dac7ac671cfa773d0ec2a28fcb0562634b59a38d7eed754f3fde79172f3c3da381f70a9ed3ef8a9f19fc34fc0d033931a2fdd2fabc302f88e6208c40029b72cc
data/README.md CHANGED
@@ -23,14 +23,14 @@ A task is copied to N targets and executed there.
23
23
 
24
24
  ## Plans
25
25
 
26
- Plans provide complex logic options, written in Puppet DSL.
27
- Besides the usual Puppet DSL functions, it's also possible to execute tasks and evaluate their responses.
26
+ Plans provide complex logic options, written in the Puppet language.
27
+ Besides the usual Puppet language functions, it's also possible to execute tasks and evaluate their responses.
28
28
 
29
29
  ## OpenBolt in Foreman!
30
30
 
31
- OpenBolt is the Ansible counterpart and OpenBolt is Puppet "native".
32
- OpenBolt and Puppet integrate very well together and OpenBolt can reuse your existing Puppet code.
33
- Since OpenBolt is a CLI only application, and most Puppet users run Foreman anyways, it made sense to integrate OpenBolt into Foreman, instead of writing another web UI.
31
+ OpenBolt is the Ansible counterpart and OpenBolt is OpenVox/Puppet native.
32
+ OpenBolt and OpenVox/Puppet integrate very well together and OpenBolt can reuse your existing code.
33
+ Since OpenBolt is a CLI only application, and most OpenVox and Puppet users run Foreman anyways, it made sense to integrate OpenBolt into Foreman, instead of writing another web UI.
34
34
 
35
35
  ## Installation
36
36
 
@@ -49,7 +49,9 @@ The [theforeman/foreman_proxy](https://github.com/theforeman/puppet-foreman_prox
49
49
  The Foreman plugin provides UI elements to start Tasks on various nodes.
50
50
  Foreman then talks to a Smartproxy to run OpenBolt.
51
51
  The Smartproxy also establishes the connections to the various targets.
52
- This is usually a ssh or WinRM connection (and soon choria, see [the TODO section](#todo)).
52
+ This is usually a SSH, WinRM, or Choria connection. The Choria transport
53
+ requires OpenBolt 5.5 or later and is not available in Puppet Bolt.
54
+ SSH and WinRM transports work with any version.
53
55
 
54
56
  You need to have `bolt` in your `$PATH` on the Smartproxy.
55
57
  OpenBolt packages are available at [yum.voxpupuli.org](https://yum.voxpupuli.org/) & [apt.voxpupuli.org](https://apt.voxpupuli.org/) in the openvox8 repo.
@@ -57,172 +59,18 @@ You can also use the legacy Bolt packages from Perforce from the `puppet-tools`
57
59
 
58
60
  The integration is supported on Foreman 3.17 and all following versions, including development/nightly builds.
59
61
 
60
- OpenBolt relies on Tasks & Plans. They are distributed as puppet modules.
62
+ OpenBolt relies on Tasks & Plans. They are distributed as modules.
61
63
  The plugin assumes that you deployed your code.
62
64
  We recommend to use [r10k](https://github.com/puppetlabs/r10k?tab=readme-ov-file#r10k) or [g10k](https://github.com/xorpaul/g10k?tab=readme-ov-file#g10k) to deploy code, as you do it on your compilers.
63
65
 
64
66
  A handful of core/default Tasks & Plans are also included in the [OpenBolt rpm/deb packages](https://github.com/OpenVoxProject/openbolt/blob/main/Puppetfile).
65
67
 
66
- ## Usage
68
+ ## Documentation
67
69
 
68
- (all screenshots were taken on Foreman 3.17)
69
-
70
- After installation, you will see a new UI element
71
-
72
- ![foreman UI menu screenshot](./ext/foreman-ui-menu.png)
73
-
74
- The "Launch Task" option allows you to select any smartproxy with the `openbolt` feature (which is available when the OpenBolt Smartproxy plugin is installed).
75
- Afterwards you can select N targets to run the task and select an available task from the selected Smartproxy.
76
- On the right side you can configure OpenBolt connection settings.
77
-
78
- ![launch task detail view](./ext/foreman-launch-task.png)
79
-
80
- After selecting a task, the task metadata is fetched and shown.
81
- Additional input elements will appear, if the task support it.
82
-
83
- ![service task metadata](./ext/task-metadata-minimal.png)
84
-
85
- The metadata can contains a description and datatypes for tasks.
86
- Those information can be shown as well.
87
-
88
- ![service task detailed metadata](./ext/task-metadata.png)
89
-
90
- While the task is running, the UI polls the status from the smart proxy.
91
-
92
- ![task loading screen](./ext/task-running.png)
93
-
94
- After the task finished, it will display a success for failure page.
95
-
96
- ![failed task view](./ext/task-execution-details.png)
97
-
98
- You can also see the used parameters for a task.
99
-
100
- ![task used parameters](./ext/task-task-details.png)
101
-
102
- We also display the used OpenBolt command line, in case you want to manually run it or debug it.
103
-
104
- ![display used OpenBolt command](./ext/task-log-output.png)
105
-
106
- OpenBolt returns JSON for executed tasks.
107
- That's visible in the UI.
108
- For failed tasks but also for passed tasks.
109
-
110
- ![failed task output](./ext/task-result.png)
111
-
112
- ![service task passed on two nodes](./ext/task-successful-result.png)
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
- ```
221
-
222
- ## TODO
223
-
224
- * Integrate plans into the web UI
225
- * Provide a choria transport plugin
70
+ - [Usage](docs/usage.md): screenshots and walkthrough of the UI
71
+ - [Development](docs/development.md): linting, unit tests, acceptance tests, building packages
72
+ - [Releasing](docs/releasing.md): version locations, release steps, RPM/DEB packaging
73
+ - [Choria Testing](docs/choria-testing.md): setting up Choria on a Foreman install for transport testing
226
74
 
227
75
  ## Contributing & support
228
76
 
@@ -247,83 +95,3 @@ GNU General Public License for more details.
247
95
 
248
96
  You should have received a copy of the GNU General Public License
249
97
  along with this program. If not, see <http://www.gnu.org/licenses/>.
250
-
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
- ```
318
-
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
324
-
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
- ```
@@ -53,7 +53,7 @@ module ForemanOpenbolt
53
53
  key = setting.name.sub(/^openbolt_/, '')
54
54
  if setting.encrypted?
55
55
  defaults[key] = ENCRYPTED_PLACEHOLDER unless setting.value.to_s.empty?
56
- else
56
+ elsif !setting.value.to_s.empty?
57
57
  defaults[key] = setting.value
58
58
  end
59
59
  end
@@ -136,6 +136,9 @@ module ForemanOpenbolt
136
136
  rescue ActiveRecord::RecordInvalid => e
137
137
  log_exception('launch_task', e)
138
138
  render_error("Database error: #{e.message}", :internal_server_error)
139
+ rescue ProxyAPI::ProxyException => e
140
+ log_exception('launch_task', e)
141
+ render_error("Smart Proxy error: #{e.message}", :bad_gateway)
139
142
  rescue StandardError => e
140
143
  log_exception('launch_task', e)
141
144
  render_error("Error launching task: #{e.message}", :internal_server_error)
@@ -35,6 +35,7 @@ module ForemanOpenbolt
35
35
  TRANSPORTS = {
36
36
  ssh: N_('SSH'),
37
37
  winrm: N_('WinRM'),
38
+ choria: N_('Choria'),
38
39
  }.freeze
39
40
  LOG_LEVELS = {
40
41
  error: N_('Error'),
@@ -43,28 +44,19 @@ module ForemanOpenbolt
43
44
  debug: N_('Debug'),
44
45
  trace: N_('Trace'),
45
46
  }.freeze
47
+ CHORIA_TASK_AGENTS = {
48
+ bolt_tasks: N_('Bolt Tasks'),
49
+ shell: N_('Shell'),
50
+ }.freeze
46
51
  # rubocop:enable Lint/ConstantDefinitionInBlock
47
52
 
48
- setting 'openbolt_transport',
49
- type: :string,
50
- default: 'ssh',
51
- full_name: N_('Transport'),
52
- description: N_('The transport method to use for connecting to target hosts'),
53
- collection: proc { TRANSPORTS }
53
+ # General (all transports)
54
54
  setting 'openbolt_log-level',
55
55
  type: :string,
56
56
  default: 'debug',
57
57
  full_name: N_('Log Level'),
58
58
  description: N_('Set the log level during OpenBolt execution'),
59
59
  collection: proc { LOG_LEVELS }
60
- setting 'openbolt_verbose',
61
- type: :boolean,
62
- default: false,
63
- full_name: N_('Verbose'),
64
- description: N_(
65
- 'Run the OpenBolt command with the --verbose flag. This prints additional information ' \
66
- 'during OpenBolt execution and will print any out::verbose plan statements.'
67
- )
68
60
  setting 'openbolt_noop',
69
61
  type: :boolean,
70
62
  default: false,
@@ -72,22 +64,132 @@ module ForemanOpenbolt
72
64
  description: N_(
73
65
  'Run the OpenBolt command with the --noop flag, which will make no changes to the target host'
74
66
  )
67
+ setting 'openbolt_password',
68
+ type: :string,
69
+ default: nil,
70
+ full_name: N_('Password'),
71
+ description: N_('Password used for SSH or WinRM authentication'),
72
+ encrypted: true
75
73
  setting 'openbolt_tmpdir',
76
74
  type: :string,
77
- default: '',
75
+ default: nil,
78
76
  full_name: N_('Temporary Directory'),
79
77
  description: N_('Directory to use for temporary files on target hosts during OpenBolt execution')
78
+ setting 'openbolt_transport',
79
+ type: :string,
80
+ default: 'ssh',
81
+ full_name: N_('Transport'),
82
+ description: N_('The transport method to use for connecting to target hosts'),
83
+ collection: proc { TRANSPORTS }
80
84
  setting 'openbolt_user',
81
85
  type: :string,
82
- default: '',
86
+ default: nil,
83
87
  full_name: N_('User'),
84
88
  description: N_('Username used for SSH or WinRM authentication')
85
- setting 'openbolt_password',
89
+ setting 'openbolt_verbose',
90
+ type: :boolean,
91
+ default: false,
92
+ full_name: N_('Verbose'),
93
+ description: N_(
94
+ 'Run the OpenBolt command with the --verbose flag. This prints additional information ' \
95
+ 'during OpenBolt execution and will print any out::verbose plan statements.'
96
+ )
97
+
98
+ # Choria
99
+ setting 'openbolt_choria-broker-timeout',
100
+ type: :integer,
101
+ default: nil,
102
+ full_name: N_('Choria Broker Timeout'),
103
+ description: N_('Time in seconds to wait when establishing a connection to a Choria broker.')
104
+ setting 'openbolt_choria-brokers',
86
105
  type: :string,
87
- default: '',
88
- full_name: N_('Password'),
89
- description: N_('Password used for SSH or WinRM authentication'),
90
- encrypted: true
106
+ default: nil,
107
+ full_name: N_('Choria Brokers'),
108
+ description: N_(
109
+ 'Comma-separated list of Choria broker addresses in host or host:port format ' \
110
+ '(e.g. broker1.example.com:4222,broker2.example.com:4222). Port defaults to 4222 if omitted. ' \
111
+ 'When not set, the Choria client checks the config file, then SRV records, then falls back to puppet:4222.'
112
+ )
113
+ setting 'openbolt_choria-collective',
114
+ type: :string,
115
+ default: nil,
116
+ full_name: N_('Choria Collective'),
117
+ description: N_('Choria collective to route messages through.')
118
+ setting 'openbolt_choria-command-timeout',
119
+ type: :integer,
120
+ default: nil,
121
+ full_name: N_('Choria Command Timeout'),
122
+ description: N_('Time in seconds to wait for command completion on target nodes.')
123
+ setting 'openbolt_choria-config-file',
124
+ type: :string,
125
+ default: nil,
126
+ full_name: N_('Choria Config File'),
127
+ description: N_(
128
+ 'Path on the smart proxy host to the Choria client configuration file. This file ' \
129
+ 'must be readable by the foreman-proxy user. When not set, the proxy uses a built-in default.'
130
+ )
131
+ setting 'openbolt_choria-mcollective-certname',
132
+ type: :string,
133
+ default: nil,
134
+ full_name: N_('Choria MCollective Certname'),
135
+ description: N_(
136
+ 'Override the MCollective certname for Choria client identity. When not set, the ' \
137
+ 'proxy derives this automatically from its SSL certificate.'
138
+ )
139
+ setting 'openbolt_choria-puppet-environment',
140
+ type: :string,
141
+ default: nil,
142
+ full_name: N_('Choria Puppet Environment'),
143
+ description: N_(
144
+ 'Puppet environment used by the Choria bolt_tasks agent to locate task files. ' \
145
+ "Only applies when the Choria Task Agent is 'bolt_tasks'. Defaults to " \
146
+ "'production' when not specified."
147
+ )
148
+ setting 'openbolt_choria-rpc-timeout',
149
+ type: :integer,
150
+ default: nil,
151
+ full_name: N_('Choria RPC Timeout'),
152
+ description: N_('Time in seconds to wait for RPC responses from target nodes.')
153
+ setting 'openbolt_choria-ssl-ca',
154
+ type: :string,
155
+ default: nil,
156
+ full_name: N_('Choria SSL CA'),
157
+ description: N_(
158
+ 'Path on the smart proxy host to the Choria CA certificate. This file must be ' \
159
+ 'readable by the foreman-proxy user.'
160
+ )
161
+ setting 'openbolt_choria-ssl-cert',
162
+ type: :string,
163
+ default: nil,
164
+ full_name: N_('Choria SSL Certificate'),
165
+ description: N_(
166
+ 'Path on the smart proxy host to the Choria client SSL certificate. This file ' \
167
+ 'must be readable by the foreman-proxy user.'
168
+ )
169
+ setting 'openbolt_choria-ssl-key',
170
+ type: :string,
171
+ default: nil,
172
+ full_name: N_('Choria SSL Key'),
173
+ description: N_(
174
+ 'Path on the smart proxy host to the Choria client SSL private key. This file ' \
175
+ 'must be readable by the foreman-proxy user.'
176
+ )
177
+ setting 'openbolt_choria-task-agent',
178
+ type: :string,
179
+ default: 'bolt_tasks',
180
+ full_name: N_('Choria Task Agent'),
181
+ description: N_(
182
+ 'Choria agent used to execute tasks on target nodes. Use the bolt_tasks agent for ' \
183
+ 'standard OpenBolt tasks, or the shell agent to run shell commands.'
184
+ ),
185
+ collection: proc { CHORIA_TASK_AGENTS }
186
+ setting 'openbolt_choria-task-timeout',
187
+ type: :integer,
188
+ default: nil,
189
+ full_name: N_('Choria Task Timeout'),
190
+ description: N_('Time in seconds to wait for task completion on target nodes.')
191
+
192
+ # SSH
91
193
  setting 'openbolt_host-key-check',
92
194
  type: :boolean,
93
195
  default: true,
@@ -95,7 +197,7 @@ module ForemanOpenbolt
95
197
  description: N_('Whether to perform host key verification when connecting to targets over SSH')
96
198
  setting 'openbolt_private-key',
97
199
  type: :string,
98
- default: '',
200
+ default: nil,
99
201
  full_name: N_('SSH Private Key'),
100
202
  description: N_(
101
203
  'Path on the smart proxy host to the private key used for SSH authentication. This key must be ' \
@@ -103,7 +205,7 @@ module ForemanOpenbolt
103
205
  )
104
206
  setting 'openbolt_run-as',
105
207
  type: :string,
106
- default: '',
208
+ default: nil,
107
209
  full_name: N_('SSH Run As User'),
108
210
  description: N_(
109
211
  'The user to run commands as on the target host. This requires that the user specified ' \
@@ -111,10 +213,12 @@ module ForemanOpenbolt
111
213
  )
112
214
  setting 'openbolt_sudo-password',
113
215
  type: :string,
114
- default: '',
216
+ default: nil,
115
217
  full_name: N_('SSH Sudo Password'),
116
218
  description: N_('Password used for privilege escalation when using SSH'),
117
219
  encrypted: true
220
+
221
+ # WinRM
118
222
  setting 'openbolt_ssl',
119
223
  type: :boolean,
120
224
  default: true,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanOpenbolt
4
- VERSION = '1.1.1'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -52,15 +52,25 @@ module ProxyAPI
52
52
  end
53
53
 
54
54
  def parse_response(response, operation)
55
- raise ProxyException.new(@url, nil, "No response from Smart Proxy during #{operation}") unless response
55
+ unless response
56
+ raise ProxyException.new(
57
+ @url, RuntimeError.new("No response from Smart Proxy during #{operation}"),
58
+ "No response from Smart Proxy during #{operation}"
59
+ )
60
+ end
56
61
 
57
62
  body = response.body
58
- raise ProxyException.new(@url, nil, "Empty response body from Smart Proxy during #{operation}") if body.nil?
63
+ if body.nil?
64
+ raise ProxyException.new(
65
+ @url, RuntimeError.new("Empty response body from Smart Proxy during #{operation}"),
66
+ "Empty response body from Smart Proxy during #{operation}"
67
+ )
68
+ end
59
69
 
60
70
  JSON.parse(body)
61
71
  rescue JSON::ParserError => e
62
72
  raise ProxyException.new(
63
- @url, nil,
73
+ @url, e,
64
74
  "Invalid JSON from Smart Proxy during #{operation}: #{e.message}. " \
65
75
  "Response body (first 500 chars): #{body.to_s[0..500]}"
66
76
  )
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_openbolt",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "OpenBolt integration into Foreman",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -91,13 +91,13 @@ class TransportOptionsTest < AcceptanceTestCase
91
91
  text: 'Select a task to see parameters', wait: 10
92
92
  end
93
93
 
94
- def test_transport_option_renders_as_select_with_ssh_and_winrm
95
- # The transport OpenBolt option has type ["ssh", "winrm"] and
96
- # ParameterField renders array-typed values as a <select>.
94
+ def test_transport_option_renders_as_select_with_ssh_winrm_and_choria
95
+ # The transport OpenBolt option has type ["ssh", "winrm", "choria"]
96
+ # and ParameterField renders array-typed values as a <select>.
97
97
  field = find_by_id('param_transport', wait: 10)
98
98
  assert_equal 'select', field.tag_name
99
99
  option_values = all('#param_transport option').map(&:value)
100
- assert_equal %w[ssh winrm], option_values
100
+ assert_equal %w[ssh winrm choria], option_values
101
101
  assert_equal 'ssh', field.value
102
102
  end
103
103
 
@@ -118,4 +118,44 @@ class TransportOptionsTest < AcceptanceTestCase
118
118
  assert_selector '#param_host-key-check', wait: 10
119
119
  assert_selector '#param_private-key', wait: 10
120
120
  end
121
+
122
+ def test_switching_transport_to_choria_hides_ssh_only_options_and_shows_choria_options
123
+ # SSH-only options are present on load because the default transport is ssh.
124
+ assert_selector '#param_host-key-check', wait: 10
125
+ assert_selector '#param_private-key', wait: 10
126
+ assert_selector '#param_run-as', wait: 10
127
+ assert_selector '#param_sudo-password', wait: 10
128
+
129
+ # Switching transport to choria should hide ssh-only options and reveal
130
+ # choria-tagged options (OpenBoltOptionsSection filters by transport).
131
+ select 'choria', from: 'param_transport'
132
+ assert_no_selector '#param_host-key-check', wait: 10
133
+ assert_no_selector '#param_private-key', wait: 10
134
+ assert_no_selector '#param_run-as', wait: 10
135
+ assert_no_selector '#param_sudo-password', wait: 10
136
+
137
+ assert_selector '#param_choria-config-file', wait: 10
138
+ assert_selector '#param_choria-ssl-ca', wait: 10
139
+ assert_selector '#param_choria-brokers', wait: 10
140
+
141
+ # choria-task-agent must render as a <select> whose options match the
142
+ # proxy's enum exactly. The launch form gets the enum from the proxy's
143
+ # option metadata type array, so a drift in the proxy-side enum (or a
144
+ # mismatch with engine.rb's CHORIA_TASK_AGENTS collection shown on the
145
+ # Settings page) would only surface at task launch time otherwise.
146
+ task_agent = find_by_id('param_choria-task-agent', wait: 10)
147
+ assert_equal 'select', task_agent.tag_name
148
+ assert_equal %w[bolt_tasks shell], all('#param_choria-task-agent option').map(&:value)
149
+ assert_equal 'bolt_tasks', task_agent.value
150
+
151
+ # Switching back to ssh restores ssh-only options and hides choria-only ones.
152
+ # Includes the select-type field so a future regression that special-cases
153
+ # selects in the transport filter would be caught here.
154
+ select 'ssh', from: 'param_transport'
155
+ assert_selector '#param_host-key-check', wait: 10
156
+ assert_selector '#param_private-key', wait: 10
157
+ assert_no_selector '#param_choria-config-file', wait: 10
158
+ assert_no_selector '#param_choria-task-agent', wait: 10
159
+ assert_no_selector '#param_choria-brokers', wait: 10
160
+ end
121
161
  end
@@ -141,6 +141,20 @@ class TaskControllerTest < ActionController::TestCase
141
141
  assert_match(/Task execution failed/, JSON.parse(response.body)['error'])
142
142
  end
143
143
 
144
+ test 'returns bad_gateway when proxy returns invalid JSON' do
145
+ stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
146
+ .to_return(status: 200, body: 'not valid json',
147
+ headers: { 'Content-Type' => 'application/json' })
148
+
149
+ post :launch_task, params: {
150
+ proxy_id: @proxy.id,
151
+ task_name: 'mymod::install',
152
+ targets: 'host1',
153
+ }, session: @session
154
+ assert_response :bad_gateway
155
+ assert_match(/Smart Proxy error/, JSON.parse(response.body)['error'])
156
+ end
157
+
144
158
  test 'returns error when proxy returns no job ID' do
145
159
  stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
146
160
  .to_return(status: 200, body: { 'status' => 'ok' }.to_json,
@@ -214,7 +228,7 @@ class TaskControllerTest < ActionController::TestCase
214
228
  ForemanTasks.stubs(:async_task)
215
229
  end
216
230
 
217
- test 'replaces encrypted placeholder with real setting value before sending to proxy' do
231
+ test 'sends real encrypted value to proxy and scrubs it in database' do
218
232
  Setting['openbolt_password'] = 'real-secret-password'
219
233
 
220
234
  post :launch_task, params: {
@@ -226,27 +240,11 @@ class TaskControllerTest < ActionController::TestCase
226
240
 
227
241
  assert_response :success
228
242
 
229
- # Verify the real value was sent to the proxy (not the placeholder)
230
- proxy_request = WebMock::RequestRegistry.instance
231
- .requested_signatures
232
- .hash
233
- .keys
234
- .find { |sig| sig.uri.path == '/openbolt/launch/task' }
235
- sent_body = JSON.parse(proxy_request.body)
236
- assert_equal 'real-secret-password', sent_body['options']['password']
237
- end
238
-
239
- test 'scrubs encrypted options before storing in database' do
240
- Setting['openbolt_password'] = 'real-secret-password'
241
-
242
- post :launch_task, params: {
243
- proxy_id: @proxy.id,
244
- task_name: 'mymod::install',
245
- targets: 'host1.example.com',
246
- options: { 'password' => '[Use saved encrypted default]', 'transport' => 'ssh' },
247
- }, session: @session
243
+ assert_requested(:post, "#{@proxy.url}/openbolt/launch/task") do |req|
244
+ sent_body = JSON.parse(req.body)
245
+ sent_body['options']['password'] == 'real-secret-password'
246
+ end
248
247
 
249
- assert_response :success
250
248
  job = ForemanOpenbolt::TaskJob.find_by(job_id: 'encrypted-job-1')
251
249
  assert_equal '*****', job.openbolt_options['password']
252
250
  assert_equal 'ssh', job.openbolt_options['transport']
@@ -318,6 +316,83 @@ class TaskControllerTest < ActionController::TestCase
318
316
  end
319
317
  end
320
318
 
319
+ context 'fetch_openbolt_options with Choria settings defaults' do
320
+ # Mirrors the real GET /openbolt/tasks/options response for Choria
321
+ # (see smart_proxy_openbolt/lib/smart_proxy_openbolt/main.rb OPENBOLT_OPTIONS).
322
+ def self.choria_proxy_options
323
+ {
324
+ 'choria-task-agent' => { 'type' => %w[bolt_tasks shell], 'transport' => ['choria'], 'sensitive' => false },
325
+ 'choria-config-file' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
326
+ 'choria-mcollective-certname' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
327
+ 'choria-ssl-ca' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
328
+ 'choria-ssl-cert' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
329
+ 'choria-ssl-key' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
330
+ 'choria-collective' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
331
+ 'choria-puppet-environment' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
332
+ 'choria-rpc-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
333
+ 'choria-task-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
334
+ 'choria-command-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
335
+ 'choria-brokers' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
336
+ 'choria-broker-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
337
+ }
338
+ end
339
+
340
+ test 'merges all Choria setting values into their option defaults' do
341
+ Setting['openbolt_choria-task-agent'] = 'shell'
342
+ Setting['openbolt_choria-config-file'] = '/etc/choria/client.conf'
343
+ Setting['openbolt_choria-mcollective-certname'] = 'primary.example.com'
344
+ Setting['openbolt_choria-ssl-ca'] = '/etc/choria/ca.pem'
345
+ Setting['openbolt_choria-ssl-cert'] = '/etc/choria/client.pem'
346
+ Setting['openbolt_choria-ssl-key'] = '/etc/choria/client.key'
347
+ Setting['openbolt_choria-collective'] = 'mcollective'
348
+ Setting['openbolt_choria-puppet-environment'] = 'production'
349
+ Setting['openbolt_choria-rpc-timeout'] = 60
350
+ Setting['openbolt_choria-task-timeout'] = 300
351
+ Setting['openbolt_choria-command-timeout'] = 120
352
+ Setting['openbolt_choria-brokers'] = 'broker.example.com:4222'
353
+ Setting['openbolt_choria-broker-timeout'] = 10
354
+
355
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
356
+ .to_return(status: 200, body: self.class.choria_proxy_options.to_json,
357
+ headers: { 'Content-Type' => 'application/json' })
358
+
359
+ get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
360
+ assert_response :success
361
+
362
+ body = JSON.parse(response.body)
363
+ assert_equal 'shell', body['choria-task-agent']['default']
364
+ assert_equal '/etc/choria/client.conf', body['choria-config-file']['default']
365
+ assert_equal 'primary.example.com', body['choria-mcollective-certname']['default']
366
+ assert_equal '/etc/choria/ca.pem', body['choria-ssl-ca']['default']
367
+ assert_equal '/etc/choria/client.pem', body['choria-ssl-cert']['default']
368
+ assert_equal '/etc/choria/client.key', body['choria-ssl-key']['default']
369
+ assert_equal 'mcollective', body['choria-collective']['default']
370
+ assert_equal 'production', body['choria-puppet-environment']['default']
371
+ assert_equal 60, body['choria-rpc-timeout']['default']
372
+ assert_equal 300, body['choria-task-timeout']['default']
373
+ assert_equal 120, body['choria-command-timeout']['default']
374
+ assert_equal 'broker.example.com:4222', body['choria-brokers']['default']
375
+ assert_equal 10, body['choria-broker-timeout']['default']
376
+ end
377
+
378
+ test 'omits nil-default settings and keeps real defaults when Choria settings are not configured' do
379
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
380
+ .to_return(status: 200, body: self.class.choria_proxy_options.to_json,
381
+ headers: { 'Content-Type' => 'application/json' })
382
+
383
+ get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
384
+ assert_response :success
385
+
386
+ body = JSON.parse(response.body)
387
+ assert_equal 'bolt_tasks', body['choria-task-agent']['default']
388
+ assert_not body['choria-config-file'].key?('default')
389
+ assert_not body['choria-mcollective-certname'].key?('default')
390
+ assert_not body['choria-ssl-key'].key?('default')
391
+ assert_not body['choria-brokers'].key?('default')
392
+ assert_not body['choria-broker-timeout'].key?('default')
393
+ end
394
+ end
395
+
321
396
  context 'fetch_task_history' do
322
397
  test 'returns paginated task history' do
323
398
  3.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
@@ -83,4 +83,49 @@ describe('ParameterField', () => {
83
83
  fireEvent.change(input, { target: { value: 'new-value' } });
84
84
  expect(handleChange).toHaveBeenCalledWith('username', 'new-value');
85
85
  });
86
+
87
+ test('shows empty field for encrypted default instead of the placeholder value', () => {
88
+ const { container } = render(
89
+ <ParameterField
90
+ name="password"
91
+ metadata={{
92
+ type: 'String',
93
+ sensitive: true,
94
+ default: '[Use saved encrypted default]',
95
+ }}
96
+ onChange={jest.fn()}
97
+ />
98
+ );
99
+ const input = container.querySelector('input[type="password"]');
100
+ expect(input.value).toBe('');
101
+ });
102
+
103
+ test('shows encrypted placeholder when value matches placeholder', () => {
104
+ const { container } = render(
105
+ <ParameterField
106
+ name="password"
107
+ metadata={{
108
+ type: 'String',
109
+ sensitive: true,
110
+ default: '[Use saved encrypted default]',
111
+ }}
112
+ value="[Use saved encrypted default]"
113
+ onChange={jest.fn()}
114
+ />
115
+ );
116
+ const input = container.querySelector('input[type="password"]');
117
+ expect(input.value).toBe('[Use saved encrypted default]');
118
+ });
119
+
120
+ test('renders empty text input when value is undefined and no default', () => {
121
+ const { container } = render(
122
+ <ParameterField
123
+ name="tmpdir"
124
+ metadata={{ type: 'String' }}
125
+ onChange={jest.fn()}
126
+ />
127
+ );
128
+ const input = container.querySelector('input[type="text"]');
129
+ expect(input.value).toBe('');
130
+ });
86
131
  });
@@ -47,6 +47,7 @@ describe('useOpenBoltOptions', () => {
47
47
  transport: 'ssh',
48
48
  verbose: false,
49
49
  });
50
+ expect(result.current.openBoltOptions.verbose).toBe(false);
50
51
  expect(result.current.openBoltOptions.user).toBeUndefined();
51
52
  });
52
53
 
@@ -8,7 +8,7 @@ describe('LoadingIndicator', () => {
8
8
  expect(screen.getByText(/running/i)).toBeInTheDocument();
9
9
  });
10
10
 
11
- test('shows running status message for pending jobs', () => {
11
+ test('shows current status message for pending jobs', () => {
12
12
  render(<LoadingIndicator jobStatus="pending" />);
13
13
  expect(screen.getByText(/pending/i)).toBeInTheDocument();
14
14
  });
@@ -10,7 +10,7 @@ afterEach(() => {
10
10
  });
11
11
 
12
12
  describe('useJobPolling', () => {
13
- test('returns undefined state when jobId is null', () => {
13
+ test('returns initial pending state and does not poll when jobId is null', () => {
14
14
  const { result } = renderHook(() => useJobPolling(null));
15
15
  expect(result.current.status).toBe('pending');
16
16
  expect(result.current.isPolling).toBe(false);
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_openbolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Overlook InfraTech