foreman_remote_execution 7.1.0 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7c8e05850731a1f10e902deaa2e6797b869bb3363607af339e6a683225fa78a
4
- data.tar.gz: b5a2001957310aeb3a7353efa65592bae1dbb3fcabb68677516220674cea94b2
3
+ metadata.gz: 893050d86f8ab746da3173f25cc9a062454bf9262316f0a6fbb5f89a6282c15f
4
+ data.tar.gz: 02e623b7202ec926f0d744cbfadbbe5f86a7950cd3665c91a3b7ec74a2b02cd2
5
5
  SHA512:
6
- metadata.gz: 03daea7eb15788a97f841d80fddeb3c40c89e79bba550ca7ef9a2eab5ad6646ec13de8d134490a5fb5171d8aeb937e792a88f62588a468613a71948cb0212ba3
7
- data.tar.gz: e1ed8ff83ae15794ca4bcca7bdeb7cd7fef930f6c41394538e14fafa5fcb5f738535a3e1096bd6ee1856f3b95a4299ebdb6cf50aafa94ea233724599cea67395
6
+ metadata.gz: c488b0d6edc98ce77e66edefc2c0f298c323bfd0aa6f0d46a3dbbc4b4642bbe9ac8f2112c237460095f47c5494d5ad7112482475fc886d596e9698fe0cac128f
7
+ data.tar.gz: eb73a3df7acbfa047699d349fcb03393850b5ddded2ed31b3cd30e38b15d3a516e608a17e33f29237271e012c9de69bb7929801de4f8605d8a4b3b59590b73e8
@@ -9,6 +9,7 @@ module ForemanRemoteExecution
9
9
  update_api(:create) do
10
10
  param :registration_command, Hash do
11
11
  param :remote_execution_interface, String, desc: N_("Identifier of the Host interface for Remote execution")
12
+ param :setup_remote_execution_pull, :bool, desc: N_("Set 'host_registration_remote_execution_pull' parameter for the host. If it is set to true, pull provider client will be deployed on the host")
12
13
  end
13
14
  end
14
15
  end
@@ -6,6 +6,7 @@ module ForemanRemoteExecution
6
6
 
7
7
  update_api(:global, :host) do
8
8
  param :remote_execution_interface, String, desc: N_("Identifier of the Host interface for Remote execution")
9
+ param :setup_remote_execution_pull, :bool, desc: N_("Set 'host_registration_remote_execution_pull' parameter for the host. If it is set to true, pull provider client will be deployed on the host")
9
10
  end
10
11
  end
11
12
 
@@ -13,10 +14,17 @@ module ForemanRemoteExecution
13
14
 
14
15
  def host_setup_extension
15
16
  remote_execution_interface
17
+ remote_execution_pull
16
18
  reset_host_known_keys! unless @host.new_record?
17
19
  super
18
20
  end
19
21
 
22
+ def remote_execution_pull
23
+ HostParameter.where(host: @host, name: 'host_registration_remote_execution_pull').destroy_all
24
+
25
+ setup_host_param('host_registration_remote_execution_pull', ActiveRecord::Type::Boolean.new.deserialize(params['setup_remote_execution_pull']))
26
+ end
27
+
20
28
  def remote_execution_interface
21
29
  return unless params['remote_execution_interface'].present?
22
30
 
@@ -50,7 +50,8 @@ class HostStatus::ExecutionStatus < HostStatus::Status
50
50
  end
51
51
 
52
52
  def status_link
53
- job_invocation = last_stopped_task.parent_task.job_invocations.first
53
+ job_invocation = last_stopped_task&.parent_task&.job_invocations&.first
54
+ return unless job_invocation
54
55
  return nil unless User.current.can?(:view_job_invocations, job_invocation)
55
56
 
56
57
  Rails.application.routes.url_helpers.job_invocation_path(job_invocation)
@@ -25,7 +25,7 @@ class JobInvocation < ApplicationRecord
25
25
  validates :job_category, :presence => true
26
26
  validates_associated :targeting, :all_template_invocations
27
27
 
28
- scoped_search :on => :id, :complete_value => true
28
+ scoped_search :on => :id, :complete_value => true, :validator => ScopedSearch::Validators::INTEGER
29
29
  scoped_search :on => :job_category, :complete_value => true
30
30
  scoped_search :on => :description, :complete_value => true
31
31
 
@@ -62,14 +62,16 @@ class JobInvocation < ApplicationRecord
62
62
 
63
63
  has_many :targeted_hosts, :through => :targeting, :source => :hosts
64
64
  scoped_search :on => 'targeted_host_id', :rename => 'targeted_host_id', :operators => ['= '],
65
- :complete_value => false, :only_explicit => true, :ext_method => :search_by_targeted_host
65
+ :complete_value => false, :only_explicit => true, :ext_method => :search_by_targeted_host,
66
+ :validator => ScopedSearch::Validators::INTEGER
66
67
 
67
68
  scoped_search :on => 'pattern_template_name', :rename => 'pattern_template_name', :operators => ['= '],
68
69
  :complete_value => false, :only_explicit => true, :ext_method => :search_by_pattern_template
69
70
 
70
71
  scoped_search :relation => :recurring_logic, :on => 'purpose', :rename => 'recurring_logic.purpose'
71
72
 
72
- scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring_logic.id'
73
+ scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring_logic.id',
74
+ :validator => ScopedSearch::Validators::INTEGER
73
75
 
74
76
  scoped_search :relation => :recurring_logic, :on => 'id', :rename => 'recurring',
75
77
  :ext_method => :search_by_recurring_logic, :only_explicit => true,
@@ -219,7 +219,7 @@ class JobInvocationComposer
219
219
  def format_datetime(datetime)
220
220
  return datetime if datetime.blank?
221
221
 
222
- Time.parse(datetime).in_time_zone.strftime('%Y-%m-%d %H:%M')
222
+ Time.zone.parse(datetime).strftime('%Y-%m-%d %H:%M')
223
223
  end
224
224
  end
225
225
 
@@ -28,6 +28,10 @@ class RemoteExecutionProvider
28
28
  providers.keys.map(&:to_s)
29
29
  end
30
30
 
31
+ def provider_proxy_features
32
+ providers.values.map(&:proxy_feature).flatten.uniq.compact
33
+ end
34
+
31
35
  def proxy_command_options(template_invocation, host)
32
36
  {:proxy_operation_name => proxy_operation_name}.merge(proxy_command_provider_inputs(template_invocation))
33
37
  end
@@ -7,7 +7,7 @@ class RemoteExecutionProxySelector < ::ForemanTasks::ProxySelector
7
7
  return proxies if capability.nil?
8
8
 
9
9
  proxies.reduce({}) do |acc, (strategy, possible_proxies)|
10
- acc.merge(strategy => possible_proxies.select { |proxy| proxy.has_capability?(capability) })
10
+ acc.merge(strategy => possible_proxies.select { |proxy| proxy.has_capability?(provider, capability) })
11
11
  end
12
12
  end
13
13
  end
@@ -1,5 +1,5 @@
1
1
  <div class="tab-pane" id="rex_proxies">
2
2
  <%= fields_for :subnet do |f| %>
3
- <%= multiple_selects f, :remote_execution_proxies, SmartProxy.authorized.with_features(*RemoteExecutionProvider.provider_names).distinct, @subnet.remote_execution_proxy_ids, {:label => _("Proxies"), :help_inline => _("Select as many remote execution proxies as applicable for this subnet. When multiple proxies with the same provider are added, actions will be load balanced among them.")} %>
3
+ <%= multiple_selects f, :remote_execution_proxies, SmartProxy.authorized.with_features(*RemoteExecutionProvider.provider_proxy_features).distinct, @subnet.remote_execution_proxy_ids, {:label => _("Proxies"), :help_inline => _("Select as many remote execution proxies as applicable for this subnet. When multiple proxies with the same provider are added, actions will be load balanced among them.")} %>
4
4
  <% end %>
5
5
  </div>
@@ -275,6 +275,7 @@ module ForemanRemoteExecution
275
275
 
276
276
  # Extend Registration module
277
277
  extend_allowed_registration_vars :remote_execution_interface
278
+ extend_allowed_registration_vars :setup_remote_execution_pull
278
279
  ForemanTasks.dynflow.eager_load_actions!
279
280
  extend_observable_events(
280
281
  ::Dynflow::Action.descendants.select do |klass|
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '7.1.0'.freeze
2
+ VERSION = '7.2.1'.freeze
3
3
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ class ApiParamsTest < ActiveSupport::TestCase
6
+ describe '#format_datetime' do
7
+ let(:params) { JobInvocationComposer::ApiParams.allocate }
8
+
9
+ it 'leaves empty string as is' do
10
+ assert_equal params.send(:format_datetime, ''), ''
11
+ end
12
+
13
+ it 'honors explicitly supplied time zone' do
14
+ in_time_zone(ActiveSupport::TimeZone['America/New_York']) do
15
+ assert_equal '2022-07-08 08:53', params.send(:format_datetime, '2022-07-08 12:53:20 UTC')
16
+ end
17
+ end
18
+
19
+ it 'implicitly honors current user\'s time zone' do
20
+ in_time_zone(ActiveSupport::TimeZone['America/New_York']) do
21
+ assert_equal '2022-07-08 12:53', params.send(:format_datetime, '2022-07-08 12:53:20')
22
+ end
23
+ end
24
+ end
25
+
26
+ def in_time_zone(zone)
27
+ old_tz = Time.zone
28
+ Time.zone = zone
29
+ yield
30
+ ensure
31
+ Time.zone = old_tz
32
+ end
33
+ end
@@ -33,6 +33,10 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
33
33
  it 'accepts strings' do
34
34
  RemoteExecutionProvider.provider_for('SSH').must_equal SSHExecutionProvider
35
35
  end
36
+
37
+ it 'returns a default one if unknown value is provided' do
38
+ RemoteExecutionProvider.provider_for('WinRM').must_equal ScriptExecutionProvider
39
+ end
36
40
  end
37
41
 
38
42
  describe '.provider_names' do
@@ -52,6 +56,28 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
52
56
  end
53
57
  end
54
58
 
59
+ describe '.provider_proxy_features' do
60
+ it 'returns correct values' do
61
+ RemoteExecutionProvider.stubs(:providers).returns(
62
+ :SSH => SSHExecutionProvider,
63
+ :script => ScriptExecutionProvider
64
+ )
65
+
66
+ features = RemoteExecutionProvider.provider_proxy_features
67
+ _(features).must_include 'SSH'
68
+ _(features).must_include 'Script'
69
+ RemoteExecutionProvider.unstub(:providers)
70
+ end
71
+
72
+ it 'can deal with non-arrays' do
73
+ provider = OpenStruct.new(proxy_feature: 'Testing')
74
+ RemoteExecutionProvider.stubs(:providers).returns(:testing => provider)
75
+ features = RemoteExecutionProvider.provider_proxy_features
76
+ _(features).must_include 'Testing'
77
+ RemoteExecutionProvider.unstub(:providers)
78
+ end
79
+ end
80
+
55
81
  describe '.host_setting' do
56
82
  let(:host) { FactoryBot.create(:host) }
57
83
 
@@ -14,6 +14,7 @@ const HostKebabItems = () => {
14
14
  if (!consoleUrl) return null;
15
15
  return (
16
16
  <DropdownItem
17
+ ouiaId="web-console-dropdown-item"
17
18
  icon={<CodeIcon />}
18
19
  href={consoleUrl}
19
20
  target="_blank"
@@ -27,6 +27,7 @@ const RecentJobsCard = ({ hostDetails: { name, id } }) => {
27
27
  <DropdownItem
28
28
  href={foremanUrl(`${JOB_BASE_URL}${name}`)}
29
29
  key="link-to-all"
30
+ ouiaId="link-to-all-dropdown-item"
30
31
  >
31
32
  {__('View all jobs')}
32
33
  </DropdownItem>,
@@ -35,24 +36,28 @@ const RecentJobsCard = ({ hostDetails: { name, id } }) => {
35
36
  `${JOB_BASE_URL}${name}+and+status+%3D+failed+or+status%3D+succeeded`
36
37
  )}
37
38
  key="link-to-finished"
39
+ ouiaId="link-to-finished-dropdown-item"
38
40
  >
39
41
  {__('View finished jobs')}
40
42
  </DropdownItem>,
41
43
  <DropdownItem
42
44
  href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+running`)}
43
45
  key="link-to-running"
46
+ ouiaId="link-to-running-dropdown-item"
44
47
  >
45
48
  {__('View running jobs')}
46
49
  </DropdownItem>,
47
50
  <DropdownItem
48
51
  href={foremanUrl(`${JOB_BASE_URL}${name}+and+status+%3D+queued`)}
49
52
  key="link-to-scheduled"
53
+ ouiaId="link-to-scheduled-dropdown-item"
50
54
  >
51
55
  {__('View scheduled jobs')}
52
56
  </DropdownItem>,
53
57
  ]}
54
58
  >
55
59
  <Tabs
60
+ ouiaId="tabs"
56
61
  mountOnEnter
57
62
  unmountOnExit
58
63
  activeKey={activeTab}
@@ -1,15 +1,7 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import {
4
- DataList,
5
- DataListItem,
6
- DataListItemRow,
7
- DataListItemCells,
8
- DataListCell,
9
- DataListWrapModifier,
10
- Text,
11
- Bullseye,
12
- } from '@patternfly/react-core';
3
+ import { Text, Bullseye } from '@patternfly/react-core';
4
+ import { TableComposable, Tr, Tbody, Td } from '@patternfly/react-table';
13
5
  import { STATUS } from 'foremanReact/constants';
14
6
 
15
7
  import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
@@ -33,55 +25,55 @@ const RecentJobsTable = ({ status, hostId }) => {
33
25
  } = useAPI('get', jobsUrl, RECENT_JOBS_KEY);
34
26
 
35
27
  return (
36
- <DataList aria-label="recent-jobs-table" isCompact>
37
- <SkeletonLoader
38
- skeletonProps={{ count: 3 }}
39
- status={responseStatus || STATUS.PENDING}
40
- emptyState={
41
- <Bullseye>
42
- <Text style={{ marginTop: '20px' }} component="p">
43
- {__('No results found')}
44
- </Text>
45
- </Bullseye>
46
- }
47
- >
48
- {jobs?.length &&
49
- jobs.map(
50
- ({
51
- status: jobStatus,
52
- status_label: label,
53
- id,
54
- start_at: startAt,
55
- description,
56
- }) => (
57
- <DataListItem key={id}>
58
- <DataListItemRow>
59
- <DataListItemCells
60
- dataListCells={[
61
- <DataListCell
62
- wrapModifier={DataListWrapModifier.truncate}
63
- key={`name-${id}`}
64
- >
65
- <a href={foremanUrl(`/job_invocations/${id}`)}>
66
- {description}
67
- </a>
68
- </DataListCell>,
69
- <DataListCell key={`date-${id}`}>
70
- <RelativeDateTime date={startAt} />
71
- </DataListCell>,
72
- <DataListCell key={`status-${id}`}>
73
- <JobStatusIcon status={jobStatus}>
74
- {label}
75
- </JobStatusIcon>
76
- </DataListCell>,
77
- ]}
78
- />
79
- </DataListItemRow>
80
- </DataListItem>
81
- )
82
- )}
83
- </SkeletonLoader>
84
- </DataList>
28
+ <SkeletonLoader
29
+ skeletonProps={{ count: 3 }}
30
+ status={responseStatus || STATUS.PENDING}
31
+ emptyState={
32
+ <Bullseye>
33
+ <Text
34
+ ouiaId="no-results-text"
35
+ style={{ marginTop: '20px' }}
36
+ component="p"
37
+ >
38
+ {__('No results found')}
39
+ </Text>
40
+ </Bullseye>
41
+ }
42
+ >
43
+ {!!jobs?.length && (
44
+ <TableComposable
45
+ aria-label="recent-jobs-table"
46
+ variant="compact"
47
+ borders="compactBorderless"
48
+ >
49
+ <Tbody>
50
+ {jobs.map(
51
+ ({
52
+ status: jobStatus,
53
+ status_label: label,
54
+ id,
55
+ start_at: startAt,
56
+ description,
57
+ }) => (
58
+ <Tr key={id}>
59
+ <Td modifier="truncate" key={`name-${id}`}>
60
+ <a href={foremanUrl(`/job_invocations/${id}`)}>
61
+ {description}
62
+ </a>
63
+ </Td>
64
+ <Td modifier="truncate" key={`date-${id}`}>
65
+ <RelativeDateTime date={startAt} />
66
+ </Td>
67
+ <Td modifier="truncate" key={`status-${id}`}>
68
+ <JobStatusIcon status={jobStatus}>{label}</JobStatusIcon>
69
+ </Td>
70
+ </Tr>
71
+ )
72
+ )}
73
+ </Tbody>
74
+ </TableComposable>
75
+ )}
76
+ </SkeletonLoader>
85
77
  );
86
78
  };
87
79
 
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import LabelIcon from 'foremanReact/components/common/LabelIcon';
6
+
7
+ import {
8
+ FormGroup,
9
+ FormSelectOption,
10
+ FormSelect,
11
+ } from '@patternfly/react-core';
12
+
13
+ const options = (value = '') => {
14
+ const defaultValue = value ? __('yes') : __('no');
15
+ const defaultLabel = `${__('Inherit from host parameter')} (${defaultValue})`;
16
+
17
+ return (
18
+ <>
19
+ <FormSelectOption key={0} value="" label={defaultLabel} />
20
+ <FormSelectOption key={1} value label={__('Yes (override)')} />
21
+ <FormSelectOption key={2} value={false} label={__('No (override)')} />
22
+ </>
23
+ );
24
+ };
25
+
26
+ const RexPull = ({ isLoading, onChange, pluginValues, configParams }) => (
27
+ <FormGroup
28
+ label={__('REX pull mode')}
29
+ isRequired
30
+ labelIcon={
31
+ <LabelIcon
32
+ text={__(
33
+ 'Setup remote execution pull mode. If set to `Yes`, pull provider client will be deployed on the registered host. The inherited value is based on the `host_registration_remote_execution_pull` parameter. It can be inherited e.g. from host group, operating system, organization. When overridden, the selected value will be stored on host parameter level.'
34
+ )}
35
+ />
36
+ }
37
+ fieldId="registration_setup_remote_execution_pull"
38
+ >
39
+ <FormSelect
40
+ value={pluginValues.setupRemoteExecutionPull}
41
+ onChange={setupRemoteExecutionPull =>
42
+ onChange({ setupRemoteExecutionPull })
43
+ }
44
+ className="without_select2"
45
+ id="registration_setup_remote_execution_pull"
46
+ isDisabled={isLoading}
47
+ isRequired
48
+ >
49
+ {/* eslint-disable-next-line camelcase */
50
+ options(configParams?.host_registration_remote_execution_pull)}
51
+ </FormSelect>
52
+ </FormGroup>
53
+ );
54
+
55
+ RexPull.propTypes = {
56
+ onChange: PropTypes.func,
57
+ isLoading: PropTypes.bool,
58
+ pluginValues: PropTypes.shape({
59
+ setupRemoteExecutionPull: PropTypes.bool,
60
+ }),
61
+ configParams: PropTypes.shape({
62
+ host_registration_remote_execution_pull: PropTypes.bool,
63
+ }),
64
+ };
65
+
66
+ RexPull.defaultProps = {
67
+ onChange: undefined,
68
+ isLoading: false,
69
+ pluginValues: {},
70
+ configParams: {},
71
+ };
72
+
73
+ export default RexPull;
@@ -3,6 +3,7 @@ import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
3
3
 
4
4
  import FeaturesDropdown from '../components/FeaturesDropdown';
5
5
  import RexInterface from '../components/RegistrationExtension/RexInterface';
6
+ import RexPull from '../components/RegistrationExtension/RexPull';
6
7
  import RecentJobsCard from '../components/RecentJobsCard';
7
8
  import KebabItems from '../components/HostKebab/KebabItems';
8
9
 
@@ -25,6 +26,12 @@ const fills = [
25
26
  component: props => <RexInterface {...props} />,
26
27
  weight: 500,
27
28
  },
29
+ {
30
+ slot: 'registrationAdvanced',
31
+ name: 'pull',
32
+ component: props => <RexPull {...props} />,
33
+ weight: 500,
34
+ },
28
35
  {
29
36
  slot: '_rex-host-features',
30
37
  name: '_rex-host-features',
@@ -34,11 +41,11 @@ const fills = [
34
41
  ];
35
42
 
36
43
  const registerFills = () => {
37
- fills.forEach(({ slot, id, component: Component, weight, metadata }) =>
44
+ fills.forEach(({ slot, name, component: Component, weight, metadata }) =>
38
45
  addGlobalFill(
39
46
  slot,
40
- id,
41
- <Component key={`rex-fill-${id}`} />,
47
+ `rex-${name}`,
48
+ <Component key={`rex-${name}`} />,
42
49
  weight,
43
50
  metadata
44
51
  )
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.0
4
+ version: 7.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-13 00:00:00.000000000 Z
11
+ date: 2022-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deface
@@ -389,6 +389,7 @@ files:
389
389
  - test/test_plugin_helper.rb
390
390
  - test/unit/actions/run_host_job_test.rb
391
391
  - test/unit/actions/run_hosts_job_test.rb
392
+ - test/unit/api_params_test.rb
392
393
  - test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb
393
394
  - test/unit/concerns/host_extensions_test.rb
394
395
  - test/unit/concerns/nic_extensions_test.rb
@@ -491,6 +492,7 @@ files:
491
492
  - webpack/react_app/components/RecentJobsCard/index.js
492
493
  - webpack/react_app/components/RecentJobsCard/styles.scss
493
494
  - webpack/react_app/components/RegistrationExtension/RexInterface.js
495
+ - webpack/react_app/components/RegistrationExtension/RexPull.js
494
496
  - webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js
495
497
  - webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap
496
498
  - webpack/react_app/components/TargetingHosts/TargetingHosts.js
@@ -544,7 +546,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
544
546
  - !ruby/object:Gem::Version
545
547
  version: '0'
546
548
  requirements: []
547
- rubygems_version: 3.1.4
549
+ rubygems_version: 3.3.20
548
550
  signing_key:
549
551
  specification_version: 4
550
552
  summary: A plugin bringing remote execution to the Foreman, completing the config
@@ -571,6 +573,7 @@ test_files:
571
573
  - test/test_plugin_helper.rb
572
574
  - test/unit/actions/run_host_job_test.rb
573
575
  - test/unit/actions/run_hosts_job_test.rb
576
+ - test/unit/api_params_test.rb
574
577
  - test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb
575
578
  - test/unit/concerns/host_extensions_test.rb
576
579
  - test/unit/concerns/nic_extensions_test.rb