foreman_ansible 6.3.0 → 6.4.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 +4 -4
- data/app/helpers/foreman_ansible/ansible_reports_helper.rb +35 -54
- data/app/models/foreman_ansible/ansible_provider.rb +50 -1
- data/app/views/foreman_ansible/config_reports/_ansible.html.erb +14 -5
- data/app/views/foreman_ansible/job_templates/ansible_roles_-_ansible_default.erb +4 -0
- data/lib/foreman_ansible/engine.rb +0 -1
- data/lib/foreman_ansible/register.rb +2 -0
- data/lib/foreman_ansible/version.rb +1 -1
- data/package.json +7 -5
- data/test/unit/helpers/ansible_reports_helper_test.rb +4 -30
- data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.js +35 -0
- data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.scss +6 -0
- data/webpack/components/AnsibleHostDetail/AnsibleHostDetail.test.js +14 -0
- data/webpack/components/AnsibleHostDetail/index.js +6 -0
- data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AnsiblePermissionDenied.test.js.snap +2 -0
- data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AssignedRolesList.test.js.snap +4 -4
- data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AvailableRolesList.test.js.snap +5 -0
- data/webpack/global_index.js +12 -0
- metadata +12 -33
- data/test/unit/lib/foreman_ansible_core/ansible_runner_test.rb +0 -51
- data/test/unit/lib/foreman_ansible_core/command_creator_test.rb +0 -64
- data/test/unit/lib/foreman_ansible_core/playbook_runner_test.rb +0 -110
- data/webpack/__mocks__/foremanReact/common/I18n.js +0 -1
- data/webpack/__mocks__/foremanReact/common/helpers.js +0 -13
- data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +0 -2
- data/webpack/__mocks__/foremanReact/components/common/EmptyState.js +0 -5
- data/webpack/__mocks__/foremanReact/components/common/forms/OrderableSelect/helpers.js +0 -5
- data/webpack/__mocks__/foremanReact/redux/API.js +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05c975cb77fe5add41b7281dfa1c2451bb22a09ab5dc6251ab06fd4d00b834e2
|
|
4
|
+
data.tar.gz: 86613f4be767b5af80a06f83b9e13f8f030cd80bc362d866323111b61a0b23a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc27695cef374e8b709ddc3c3882056587cbf975ac7d497c1fec5aaa1885f85bcb3677c2c7fd3f2602f529cba0a6736017703f6d7ca413eb5f2ed7a6de112a94
|
|
7
|
+
data.tar.gz: ba0c5f9b94a51b7328cc0df73bfcc0f47831b1004b275be935fa73a83ae9c6f12b81086071ebe69292c8aa5ad436c5389a6a6f1ac457720bcefe034b456c0ec8
|
|
@@ -4,21 +4,19 @@ module ForemanAnsible
|
|
|
4
4
|
# This module takes the config reports stored in Foreman for Ansible and
|
|
5
5
|
# modifies them to be properly presented in views
|
|
6
6
|
module AnsibleReportsHelper
|
|
7
|
-
ANSIBLE_META_KEYS = %w[
|
|
8
|
-
_ansible_parsed _ansible_no_log _ansible_item_result
|
|
9
|
-
_ansible_ignore_errors _ansible_verbose_always _ansible_verbose_override
|
|
10
|
-
].freeze
|
|
11
|
-
ANSIBLE_HIDDEN_KEYS = %w[
|
|
12
|
-
invocation module_args results ansible_facts
|
|
13
|
-
stdout stderr
|
|
14
|
-
].freeze
|
|
15
|
-
|
|
16
7
|
def ansible_module_name(log)
|
|
17
8
|
source_value = log.source&.value
|
|
18
9
|
name = source_value.split(':')[0].strip if source_value&.include?(':')
|
|
19
10
|
name
|
|
20
11
|
end
|
|
21
12
|
|
|
13
|
+
def ansible_task_name(log)
|
|
14
|
+
source_value = log.source&.value
|
|
15
|
+
return source_value || no_data_message unless source_value.include? ':'
|
|
16
|
+
name = source_value.split(':')[1].strip if source_value.include?(':')
|
|
17
|
+
name || no_data_message
|
|
18
|
+
end
|
|
19
|
+
|
|
22
20
|
def ansible_run_in_check_mode?(log)
|
|
23
21
|
log.message&.value == 'check_mode_enabled' if check_mode_log?(log)
|
|
24
22
|
end
|
|
@@ -27,12 +25,36 @@ module ForemanAnsible
|
|
|
27
25
|
log.source&.value == 'check_mode'
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
def
|
|
31
|
-
|
|
28
|
+
def ansible_module_message(log)
|
|
29
|
+
msg_json = parsed_message_json(log)
|
|
30
|
+
module_action = msg_json['module']
|
|
31
|
+
case module_action
|
|
32
|
+
when 'package'
|
|
33
|
+
msg_json['results'].empty? ? msg_json['msg'] : msg_json['results']
|
|
34
|
+
when 'template'
|
|
35
|
+
module_args = msg_json['invocation']['module_args']
|
|
36
|
+
_("Rendered template #{module_args['_original_basename']} to #{msg_json['dest']}")
|
|
37
|
+
when 'service'
|
|
38
|
+
_("Service #{msg_json['name']} #{msg_json['state']} (enabled: #{msg_json['enabled']})")
|
|
39
|
+
when 'group'
|
|
40
|
+
_("User group #{msg_json['name']} #{msg_json['state']}, gid: #{msg_json['gid']}")
|
|
41
|
+
when 'user'
|
|
42
|
+
_("User #{msg_json['name']} #{msg_json['state']}, uid: #{msg_json['uid']}")
|
|
43
|
+
when 'cron'
|
|
44
|
+
module_args = msg_json['invocation']['module_args']
|
|
45
|
+
_("Cron job: #{module_args['minute']} #{module_args['hour']} #{module_args['day']} #{module_args['month']} #{module_args['weekday']} #{module_args['job']} (disabled: #{module_args['disabled']})")
|
|
46
|
+
when 'copy'
|
|
47
|
+
module_args = msg_json['invocation']['module_args']
|
|
48
|
+
_("Copy #{module_args['_original_basename']} to #{msg_json['dest']}")
|
|
49
|
+
when 'command', 'shell'
|
|
50
|
+
msg_json['stdout_lines']
|
|
51
|
+
else
|
|
52
|
+
no_data_message
|
|
53
|
+
end
|
|
32
54
|
end
|
|
33
55
|
|
|
34
|
-
def
|
|
35
|
-
|
|
56
|
+
def no_data_message
|
|
57
|
+
_('No additional data')
|
|
36
58
|
end
|
|
37
59
|
|
|
38
60
|
def ansible_report_origin_icon
|
|
@@ -49,49 +71,8 @@ module ForemanAnsible
|
|
|
49
71
|
false
|
|
50
72
|
end
|
|
51
73
|
|
|
52
|
-
def report_json_viewer(json)
|
|
53
|
-
react_component('ReportJsonViewer', data: json)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
74
|
private
|
|
57
75
|
|
|
58
|
-
def module_invocations(hash)
|
|
59
|
-
invocations = []
|
|
60
|
-
invocations << hash.delete('invocation')
|
|
61
|
-
results = hash.delete('results')
|
|
62
|
-
invocations << results
|
|
63
|
-
invocations = invocations.compact.flatten.map do |ih|
|
|
64
|
-
ih.is_a?(Hash) ? remove_keys(ih) : ih
|
|
65
|
-
end
|
|
66
|
-
invocations
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def pretty_print_hash(hash)
|
|
70
|
-
prettyp = JSON.pretty_generate(remove_keys(hash))
|
|
71
|
-
prettyp.gsub!(/{\n*/, "\n")
|
|
72
|
-
prettyp.gsub!(/},*\n*/, "\n")
|
|
73
|
-
prettyp.gsub!(/^(\[|\])/, '')
|
|
74
|
-
prettyp.gsub!(/^[\s]*$\n/, '')
|
|
75
|
-
paragraph_style = 'white-space:pre;padding: 2em 0'
|
|
76
|
-
tag(:p, prettyp, :style => paragraph_style)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def hash_with_keys_removed(hash)
|
|
80
|
-
new_hash = remove_keys(hash)
|
|
81
|
-
remove_keys(new_hash, ANSIBLE_HIDDEN_KEYS)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def remove_keys(hash, keys = ANSIBLE_META_KEYS)
|
|
85
|
-
hash.each do |key, value|
|
|
86
|
-
if value.is_a? Array
|
|
87
|
-
value.each { |h| remove_keys(h) if h.is_a? Hash }
|
|
88
|
-
elsif value.is_a? Hash
|
|
89
|
-
remove_keys(value)
|
|
90
|
-
end
|
|
91
|
-
hash.delete(key) if keys.include? key
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
76
|
def parsed_message_json(log)
|
|
96
77
|
JSON.parse(log.message.value)
|
|
97
78
|
rescue StandardError => e
|
|
@@ -18,6 +18,10 @@ if defined? ForemanRemoteExecution
|
|
|
18
18
|
'Ansible'
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def provider_input_namespace
|
|
22
|
+
:ansible
|
|
23
|
+
end
|
|
24
|
+
|
|
21
25
|
def proxy_command_options(template_invocation, host)
|
|
22
26
|
super(template_invocation, host).merge(
|
|
23
27
|
'ansible_inventory' => ::ForemanAnsible::InventoryCreator.new(
|
|
@@ -59,12 +63,57 @@ if defined? ForemanRemoteExecution
|
|
|
59
63
|
true
|
|
60
64
|
end
|
|
61
65
|
|
|
66
|
+
def provider_inputs
|
|
67
|
+
[
|
|
68
|
+
ForemanRemoteExecution::ProviderInput.new(
|
|
69
|
+
name: 'tags',
|
|
70
|
+
label: _('Tags'),
|
|
71
|
+
value: '',
|
|
72
|
+
value_type: 'plain',
|
|
73
|
+
description: 'Tags used for Ansible execution'
|
|
74
|
+
),
|
|
75
|
+
ForemanRemoteExecution::ProviderInput.new(
|
|
76
|
+
name: 'tags_flag',
|
|
77
|
+
label: _('Include/Exclude Tags'),
|
|
78
|
+
value: 'include',
|
|
79
|
+
description: 'Option whether to include or exclude tags',
|
|
80
|
+
options: "include\nexclude"
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def provider_inputs_doc
|
|
86
|
+
opts = provider_inputs.find { |input| input.name == 'tags_flag' }.options.split("\n")
|
|
87
|
+
{
|
|
88
|
+
:namespace => provider_input_namespace,
|
|
89
|
+
:opts => { :desc => N_('Ansible provider specific inputs') },
|
|
90
|
+
:children => [
|
|
91
|
+
{
|
|
92
|
+
:name => :tags,
|
|
93
|
+
:type => Array,
|
|
94
|
+
:opts => { :required => false, :desc => N_('A comma separated list of tags to use for Ansible run') }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
:name => :tags_flag,
|
|
98
|
+
:type => opts,
|
|
99
|
+
:opts => { :required => false, :desc => N_('Include\Exclude tags for Ansible run') }
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def proxy_command_provider_inputs(template_invocation)
|
|
106
|
+
tags = template_invocation.provider_input_values.find_by(:name => 'tags')&.value || ''
|
|
107
|
+
tags_flag = template_invocation.provider_input_values.find_by(:name => 'tags_flag')&.value || ''
|
|
108
|
+
{ :tags => tags, :tags_flag => tags_flag }
|
|
109
|
+
end
|
|
110
|
+
|
|
62
111
|
def proxy_operation_name
|
|
63
112
|
'ansible-runner'
|
|
64
113
|
end
|
|
65
114
|
|
|
66
115
|
def proxy_action_class
|
|
67
|
-
'
|
|
116
|
+
'Proxy::Ansible::TaskLauncher::Playbook::PlaybookRunnerAction'
|
|
68
117
|
end
|
|
69
118
|
|
|
70
119
|
private
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
<thead>
|
|
11
11
|
<tr>
|
|
12
12
|
<th><%= _("Level") %></th>
|
|
13
|
-
<th><%= _("
|
|
14
|
-
<th><%= _("Arguments") %></th>
|
|
13
|
+
<th><%= _("Task") %></th>
|
|
15
14
|
<th><%= _("Message") %></th>
|
|
16
15
|
</tr>
|
|
17
16
|
</thead>
|
|
@@ -20,9 +19,19 @@
|
|
|
20
19
|
<% unless check_mode_log?(log) %>
|
|
21
20
|
<tr>
|
|
22
21
|
<td><span <%= report_tag log.level %>><%= h log.level %></span></td>
|
|
23
|
-
<td
|
|
24
|
-
<td
|
|
25
|
-
|
|
22
|
+
<td><span title=<%= ansible_module_name(log) %>><%= ansible_task_name(log) %></span></td>
|
|
23
|
+
<td>
|
|
24
|
+
<% log_message = ansible_module_message(log) %>
|
|
25
|
+
<% if log_message.is_a? Array %>
|
|
26
|
+
<ul>
|
|
27
|
+
<% log_message.each do |message_line| %>
|
|
28
|
+
<li><%= message_line %></li>
|
|
29
|
+
<% end %>
|
|
30
|
+
</ul>
|
|
31
|
+
<% else %>
|
|
32
|
+
<%= log_message %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</td>
|
|
26
35
|
</tr>
|
|
27
36
|
<% end %>
|
|
28
37
|
<% end %>
|
|
@@ -15,10 +15,14 @@ model: JobTemplate
|
|
|
15
15
|
- name: Display all parameters known for the Foreman host
|
|
16
16
|
debug:
|
|
17
17
|
var: foreman
|
|
18
|
+
tags:
|
|
19
|
+
- always
|
|
18
20
|
tasks:
|
|
19
21
|
- name: Apply roles
|
|
20
22
|
include_role:
|
|
21
23
|
name: "{{ role }}"
|
|
24
|
+
tags:
|
|
25
|
+
- always
|
|
22
26
|
loop: "{{ foreman_ansible_roles }}"
|
|
23
27
|
loop_control:
|
|
24
28
|
loop_var: role
|
|
@@ -96,6 +96,8 @@ Foreman::Plugin.register :foreman_ansible do
|
|
|
96
96
|
parameter_filter Host::Managed, base_role_assignment_params.merge(:host_ansible_roles_attributes => {})
|
|
97
97
|
parameter_filter Hostgroup, base_role_assignment_params.merge(:hostgroup_ansible_roles_attributes => {})
|
|
98
98
|
|
|
99
|
+
register_global_js_file 'global'
|
|
100
|
+
|
|
99
101
|
divider :top_menu, :caption => N_('Ansible'), :parent => :configure_menu
|
|
100
102
|
menu :top_menu, :ansible_roles,
|
|
101
103
|
:caption => N_('Roles'),
|
data/package.json
CHANGED
|
@@ -14,16 +14,18 @@
|
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@babel/core": "^7.7.0",
|
|
17
|
-
"@theforeman/builder": "^
|
|
18
|
-
"@theforeman/eslint-plugin-foreman": "
|
|
19
|
-
"@theforeman/
|
|
20
|
-
"@theforeman/
|
|
17
|
+
"@theforeman/builder": "^8.4.1",
|
|
18
|
+
"@theforeman/eslint-plugin-foreman": "^8.4.1",
|
|
19
|
+
"@theforeman/find-foreman": "^8.4.1",
|
|
20
|
+
"@theforeman/stories": "^8.4.1",
|
|
21
|
+
"@theforeman/test": "^8.4.1",
|
|
22
|
+
"@theforeman/vendor-dev": "^8.4.1",
|
|
21
23
|
"babel-eslint": "^10.0.3",
|
|
22
24
|
"eslint": "^6.7.2",
|
|
23
25
|
"prettier": "^1.13.5"
|
|
24
26
|
},
|
|
25
27
|
"scripts": {
|
|
26
|
-
"test": "tfm-test --plugin",
|
|
28
|
+
"test": "tfm-test --plugin --config jest.config.js",
|
|
27
29
|
"lint": "tfm-lint --plugin -d webpack"
|
|
28
30
|
},
|
|
29
31
|
"repository": {
|
|
@@ -6,43 +6,17 @@ class AnsibleReportsHelperTest < ActiveSupport::TestCase
|
|
|
6
6
|
include ForemanAnsible::AnsibleReportsHelper
|
|
7
7
|
include ActionView::Helpers::TagHelper
|
|
8
8
|
|
|
9
|
-
test '
|
|
9
|
+
test 'module message extraction' do
|
|
10
10
|
log_value = <<-ANSIBLELOG.strip_heredoc
|
|
11
|
-
|
|
11
|
+
{"msg": "Nothing to do", "changed": false, "results": [], "rc": 0, "invocation": {"module_args": {"name": ["openssh"], "state": "present", "allow_downgrade": false, "autoremove": false, "bugfix": false, "disable_gpg_check": false, "disable_plugin": [], "disablerepo": [], "download_only": false, "enable_plugin": [], "enablerepo": [], "exclude": [], "installroot": "/", "install_repoquery": true, "install_weak_deps": true, "security": false, "skip_broken": false, "update_cache": false, "update_only": false, "validate_certs": true, "lock_timeout": 30, "conf_file": null, "disable_excludes": null, "download_dir": null, "list": null, "releasever": null}}, "_ansible_no_log": false, "failed": false, "module": "package"}
|
|
12
12
|
ANSIBLELOG
|
|
13
13
|
message = FactoryBot.build(:message)
|
|
14
14
|
message.value = log_value
|
|
15
15
|
log = FactoryBot.build(:log)
|
|
16
16
|
log.message = message
|
|
17
17
|
assert_match(
|
|
18
|
-
/
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
test 'pretty print is able to print a hash' do
|
|
24
|
-
hash = {
|
|
25
|
-
'allow_downgrade' => false,
|
|
26
|
-
'name' => ['ntp'],
|
|
27
|
-
'list' => nil,
|
|
28
|
-
'disable_gpg_check' => false,
|
|
29
|
-
'conf_file' => nil,
|
|
30
|
-
'install_repoquery' => true,
|
|
31
|
-
'state' => 'installed',
|
|
32
|
-
'disablerepo' => nil,
|
|
33
|
-
'update_cache' => false,
|
|
34
|
-
'enablerepo' => nil,
|
|
35
|
-
'exclude' => nil,
|
|
36
|
-
'security' => false,
|
|
37
|
-
'validate_certs' => true,
|
|
38
|
-
'installroot' => '/',
|
|
39
|
-
'skip_broken' => false
|
|
40
|
-
}
|
|
41
|
-
assert_equal(
|
|
42
|
-
hash,
|
|
43
|
-
remove_keys(
|
|
44
|
-
hash
|
|
45
|
-
)
|
|
18
|
+
/Nothing to do/,
|
|
19
|
+
ansible_module_message(log).to_s
|
|
46
20
|
)
|
|
47
21
|
end
|
|
48
22
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Tabs, Tab, TabTitleText, Label } from '@patternfly/react-core';
|
|
3
|
+
import { InfoCircleIcon } from '@patternfly/react-icons';
|
|
4
|
+
|
|
5
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
|
+
|
|
7
|
+
import './AnsibleHostDetail.scss';
|
|
8
|
+
|
|
9
|
+
const AnsibleHostDetail = props => {
|
|
10
|
+
// https://projects.theforeman.org/issues/32398
|
|
11
|
+
const [activeTab] = useState('variables');
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Tabs activeKey={activeTab} isSecondary>
|
|
15
|
+
<Tab
|
|
16
|
+
eventKey="variables"
|
|
17
|
+
title={<TabTitleText>{__('Variables')}</TabTitleText>}
|
|
18
|
+
>
|
|
19
|
+
<div className="host-details-tab-item">
|
|
20
|
+
<div className="ansible-host-detail">
|
|
21
|
+
<Label
|
|
22
|
+
color="blue"
|
|
23
|
+
icon={<InfoCircleIcon />}
|
|
24
|
+
style={{ marginTop: '1.5rem' }}
|
|
25
|
+
>
|
|
26
|
+
Ansible Variables coming soon!
|
|
27
|
+
</Label>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</Tab>
|
|
31
|
+
</Tabs>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default AnsibleHostDetail;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
|
|
5
|
+
import AnsibleHostDetail from './';
|
|
6
|
+
|
|
7
|
+
describe('AnsibleHostDetail', () => {
|
|
8
|
+
it('should show content', () => {
|
|
9
|
+
render(<AnsibleHostDetail />);
|
|
10
|
+
expect(
|
|
11
|
+
screen.getByText('Ansible Variables coming soon!')
|
|
12
|
+
).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`AnsiblePermissionDenied should render 1`] = `
|
|
4
4
|
<EmptyStatePattern
|
|
5
|
+
action={null}
|
|
5
6
|
description={
|
|
6
7
|
<span>
|
|
7
8
|
You are not authorized to perform this action.
|
|
@@ -22,5 +23,6 @@ exports[`AnsiblePermissionDenied should render 1`] = `
|
|
|
22
23
|
header="Permission Denied"
|
|
23
24
|
icon="lock"
|
|
24
25
|
iconType="fa"
|
|
26
|
+
secondaryActions={Array []}
|
|
25
27
|
/>
|
|
26
28
|
`;
|
data/webpack/components/AnsibleRolesSwitcher/components/__snapshots__/AssignedRolesList.test.js.snap
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`AssignedRolesList should render 1`] = `
|
|
4
|
-
<DndProvider
|
|
4
|
+
<Memo(DndProvider)
|
|
5
5
|
backend={[Function]}
|
|
6
6
|
>
|
|
7
7
|
<ListView
|
|
8
8
|
className=""
|
|
9
9
|
>
|
|
10
|
-
<
|
|
10
|
+
<DropTarget(DragSource(Orderable(AnsibleRole)))
|
|
11
11
|
icon="fa fa-minus-circle"
|
|
12
12
|
index={0}
|
|
13
13
|
key="1"
|
|
@@ -21,7 +21,7 @@ exports[`AssignedRolesList should render 1`] = `
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
/>
|
|
24
|
-
<
|
|
24
|
+
<DropTarget(DragSource(Orderable(AnsibleRole)))
|
|
25
25
|
icon="fa fa-minus-circle"
|
|
26
26
|
index={1}
|
|
27
27
|
key="2"
|
|
@@ -60,5 +60,5 @@ exports[`AssignedRolesList should render 1`] = `
|
|
|
60
60
|
}
|
|
61
61
|
/>
|
|
62
62
|
</div>
|
|
63
|
-
</DndProvider>
|
|
63
|
+
</Memo(DndProvider)>
|
|
64
64
|
`;
|
|
@@ -8,9 +8,14 @@ exports[`AvailableRolesList should render 1`] = `
|
|
|
8
8
|
className="sticky-pagination"
|
|
9
9
|
>
|
|
10
10
|
<PaginationWrapper
|
|
11
|
+
className=""
|
|
12
|
+
disableNext={false}
|
|
13
|
+
disablePrev={false}
|
|
11
14
|
dropdownButtonId="available-ansible-roles-pagination-row-dropdown"
|
|
12
15
|
itemCount={2}
|
|
13
16
|
onChange={[Function]}
|
|
17
|
+
onPageSet={[Function]}
|
|
18
|
+
onPerPageSelect={[Function]}
|
|
14
19
|
pagination={
|
|
15
20
|
Object {
|
|
16
21
|
"page": 1,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
|
|
4
|
+
|
|
5
|
+
import AnsibleHostDetail from './components/AnsibleHostDetail';
|
|
6
|
+
|
|
7
|
+
addGlobalFill(
|
|
8
|
+
'host-details-page-tabs',
|
|
9
|
+
'Ansible',
|
|
10
|
+
<AnsibleHostDetail key="ansible-host-detail" />,
|
|
11
|
+
500
|
|
12
|
+
);
|
metadata
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_ansible
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Lobato Garcia
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-
|
|
11
|
+
date: 2021-06-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: foreman_ansible_core
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '3.0'
|
|
20
|
-
type: :development
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '3.0'
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: acts_as_list
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -302,9 +288,6 @@ files:
|
|
|
302
288
|
- test/unit/hostgroup_ansible_role_test.rb
|
|
303
289
|
- test/unit/ignore_roles_test.rb
|
|
304
290
|
- test/unit/import_roles_and_variables.rb
|
|
305
|
-
- test/unit/lib/foreman_ansible_core/ansible_runner_test.rb
|
|
306
|
-
- test/unit/lib/foreman_ansible_core/command_creator_test.rb
|
|
307
|
-
- test/unit/lib/foreman_ansible_core/playbook_runner_test.rb
|
|
308
291
|
- test/unit/lib/proxy_api/ansible_test.rb
|
|
309
292
|
- test/unit/services/ansible_report_importer_test.rb
|
|
310
293
|
- test/unit/services/ansible_variables_importer_test.rb
|
|
@@ -317,12 +300,10 @@ files:
|
|
|
317
300
|
- test/unit/services/roles_importer_test.rb
|
|
318
301
|
- test/unit/services/structured_fact_importer_test.rb
|
|
319
302
|
- test/unit/services/ui_roles_importer_test.rb
|
|
320
|
-
- webpack/
|
|
321
|
-
- webpack/
|
|
322
|
-
- webpack/
|
|
323
|
-
- webpack/
|
|
324
|
-
- webpack/__mocks__/foremanReact/components/common/forms/OrderableSelect/helpers.js
|
|
325
|
-
- webpack/__mocks__/foremanReact/redux/API.js
|
|
303
|
+
- webpack/components/AnsibleHostDetail/AnsibleHostDetail.js
|
|
304
|
+
- webpack/components/AnsibleHostDetail/AnsibleHostDetail.scss
|
|
305
|
+
- webpack/components/AnsibleHostDetail/AnsibleHostDetail.test.js
|
|
306
|
+
- webpack/components/AnsibleHostDetail/index.js
|
|
326
307
|
- webpack/components/AnsibleRolesAndVariables/AnsibleRolesAndVariables.js
|
|
327
308
|
- webpack/components/AnsibleRolesAndVariables/AnsibleRolesAndVariables.scss
|
|
328
309
|
- webpack/components/AnsibleRolesAndVariables/AnsibleRolesAndVariablesActions.js
|
|
@@ -368,13 +349,14 @@ files:
|
|
|
368
349
|
- webpack/components/AnsibleRolesSwitcher/components/withProtectedView.js
|
|
369
350
|
- webpack/components/AnsibleRolesSwitcher/index.js
|
|
370
351
|
- webpack/components/ReportJsonViewer.js
|
|
352
|
+
- webpack/global_index.js
|
|
371
353
|
- webpack/index.js
|
|
372
354
|
- webpack/reducer.js
|
|
373
355
|
homepage: https://github.com/theforeman/foreman_ansible
|
|
374
356
|
licenses:
|
|
375
357
|
- GPL-3.0
|
|
376
358
|
metadata: {}
|
|
377
|
-
post_install_message:
|
|
359
|
+
post_install_message:
|
|
378
360
|
rdoc_options: []
|
|
379
361
|
require_paths:
|
|
380
362
|
- lib
|
|
@@ -390,7 +372,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
390
372
|
version: '0'
|
|
391
373
|
requirements: []
|
|
392
374
|
rubygems_version: 3.1.2
|
|
393
|
-
signing_key:
|
|
375
|
+
signing_key:
|
|
394
376
|
specification_version: 4
|
|
395
377
|
summary: Ansible integration with Foreman (theforeman.org)
|
|
396
378
|
test_files:
|
|
@@ -399,8 +381,8 @@ test_files:
|
|
|
399
381
|
- test/factories/ansible_roles.rb
|
|
400
382
|
- test/factories/host_ansible_enhancements.rb
|
|
401
383
|
- test/fixtures/insights_playbook.yaml
|
|
402
|
-
- test/fixtures/report.json
|
|
403
384
|
- test/fixtures/sample_facts.json
|
|
385
|
+
- test/fixtures/report.json
|
|
404
386
|
- test/functional/ansible_roles_controller_test.rb
|
|
405
387
|
- test/functional/api/v2/ansible_inventories_controller_test.rb
|
|
406
388
|
- test/functional/api/v2/ansible_variables_controller_test.rb
|
|
@@ -419,16 +401,13 @@ test_files:
|
|
|
419
401
|
- test/unit/concerns/host_managed_extensions_test.rb
|
|
420
402
|
- test/unit/concerns/hostgroup_extensions_test.rb
|
|
421
403
|
- test/unit/helpers/ansible_reports_helper_test.rb
|
|
422
|
-
- test/unit/lib/foreman_ansible_core/command_creator_test.rb
|
|
423
|
-
- test/unit/lib/foreman_ansible_core/ansible_runner_test.rb
|
|
424
|
-
- test/unit/lib/foreman_ansible_core/playbook_runner_test.rb
|
|
425
404
|
- test/unit/lib/proxy_api/ansible_test.rb
|
|
426
405
|
- test/unit/services/ansible_report_importer_test.rb
|
|
427
|
-
- test/unit/services/fact_importer_test.rb
|
|
428
406
|
- test/unit/services/fact_sparser_test.rb
|
|
429
407
|
- test/unit/services/insights_plan_runner_test.rb
|
|
430
408
|
- test/unit/services/roles_importer_test.rb
|
|
431
409
|
- test/unit/services/structured_fact_importer_test.rb
|
|
410
|
+
- test/unit/services/fact_importer_test.rb
|
|
432
411
|
- test/unit/services/ansible_variables_importer_test.rb
|
|
433
412
|
- test/unit/services/ui_roles_importer_test.rb
|
|
434
413
|
- test/unit/services/api_roles_importer_test.rb
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
module ForemanAnsibleCore
|
|
6
|
-
module Runner
|
|
7
|
-
class AnsibleRunnerTest < ActiveSupport::TestCase
|
|
8
|
-
describe AnsibleRunner do
|
|
9
|
-
it 'parses files without event data' do
|
|
10
|
-
content = <<~JSON
|
|
11
|
-
{"uuid": "a29d8592-f805-4d0e-b73d-7a53cc35a92e", "stdout": " [WARNING]: Consider using the yum module rather than running 'yum'. If you", "counter": 8, "end_line": 8, "runner_ident": "e2d9ae11-026a-4f9f-9679-401e4b852ab0", "start_line": 7, "event": "verbose"}
|
|
12
|
-
JSON
|
|
13
|
-
|
|
14
|
-
File.expects(:read).with('fake.json').returns(content)
|
|
15
|
-
runner = AnsibleRunner.allocate
|
|
16
|
-
runner.expects(:handle_broadcast_data)
|
|
17
|
-
assert runner.send(:handle_event_file, 'fake.json')
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
describe '#rebuild_secrets' do
|
|
22
|
-
let(:inventory) do
|
|
23
|
-
{ 'all' => { 'hosts' => ['foreman.example.com'] },
|
|
24
|
-
'_meta' => { 'hostvars' => { 'foreman.example.com' => {} } } }
|
|
25
|
-
end
|
|
26
|
-
let(:input) do
|
|
27
|
-
host_secrets = { 'ansible_password' => 'letmein', 'ansible_become_password' => 'iamroot' }
|
|
28
|
-
secrets = { 'per-host' => { 'foreman.example.com' => host_secrets } }
|
|
29
|
-
host_input = { 'input' => { 'action_input' => { 'secrets' => secrets } } }
|
|
30
|
-
{ 'foreman.example.com' => host_input }
|
|
31
|
-
end
|
|
32
|
-
let(:runner) { ForemanAnsibleCore::Runner::AnsibleRunner.allocate }
|
|
33
|
-
|
|
34
|
-
test 'uses secrets from inventory' do
|
|
35
|
-
test_inventory = inventory.merge('ssh_password' => 'sshpass', 'effective_user_password' => 'mypass')
|
|
36
|
-
rebuilt = runner.send(:rebuild_secrets, test_inventory, input)
|
|
37
|
-
host_vars = rebuilt.dig('_meta', 'hostvars', 'foreman.example.com')
|
|
38
|
-
assert_equal 'sshpass', host_vars['ansible_password']
|
|
39
|
-
assert_equal 'mypass', host_vars['ansible_become_password']
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
test 'host secrets are used when not overriden by inventory secrest' do
|
|
43
|
-
rebuilt = runner.send(:rebuild_secrets, inventory, input)
|
|
44
|
-
host_vars = rebuilt.dig('_meta', 'hostvars', 'foreman.example.com')
|
|
45
|
-
assert_equal 'letmein', host_vars['ansible_password']
|
|
46
|
-
assert_equal 'iamroot', host_vars['ansible_become_password']
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
class CommandCreatorTest < ActiveSupport::TestCase
|
|
6
|
-
let(:inventory_file) { 'test_inventory' }
|
|
7
|
-
let(:playbook_file) { 'test_palybook.yml' }
|
|
8
|
-
subject do
|
|
9
|
-
ForemanAnsibleCore::CommandCreator.new(inventory_file, playbook_file, {})
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
test 'returns a command array including the ansible-playbook command' do
|
|
13
|
-
assert command_parts.include?('ansible-playbook')
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
test 'the last argument is the playbook_file' do
|
|
17
|
-
assert command_parts.last == playbook_file
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
describe 'environment variables' do
|
|
21
|
-
let(:environment_variables) { subject.command.first }
|
|
22
|
-
|
|
23
|
-
test 'has a JSON_INVENTORY_FILE set' do
|
|
24
|
-
assert environment_variables['JSON_INVENTORY_FILE']
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
test 'has no ANSIBLE_CALLBACK_WHITELIST set by default' do
|
|
28
|
-
assert_not environment_variables['ANSIBLE_CALLBACK_WHITELIST']
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
test 'with a REX command it sets ANSIBLE_CALLBACK_WHITELIST to empty' do
|
|
32
|
-
set_command_options(:remote_execution_command, true)
|
|
33
|
-
assert environment_variables['ANSIBLE_CALLBACK_WHITELIST']
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
describe 'command options' do
|
|
38
|
-
it 'can have verbosity set' do
|
|
39
|
-
level = '3'
|
|
40
|
-
level_string = Array.new(level.to_i).map { 'v' }.join
|
|
41
|
-
set_command_options(:verbosity_level, level)
|
|
42
|
-
assert command_parts.any? do |part|
|
|
43
|
-
part == "-#{level_string}"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
it 'can have a timeout set' do
|
|
48
|
-
timeout = '5555'
|
|
49
|
-
set_command_options(:timeout, timeout)
|
|
50
|
-
assert command_parts.include?(timeout)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def command_parts
|
|
57
|
-
subject.command.flatten.map(&:to_s)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def set_command_options(option, value)
|
|
61
|
-
subject.instance_eval("@options[:#{option}] = \"#{value}\"",
|
|
62
|
-
__FILE__, __LINE__ - 1)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
# Playbook Runner - this class uses foreman_tasks_core
|
|
6
|
-
# to run playbooks
|
|
7
|
-
class PlaybookRunnerTest < ActiveSupport::TestCase
|
|
8
|
-
context 'roles dir' do
|
|
9
|
-
test 'reads default when none provided' do
|
|
10
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.stubs(:unknown_hosts).
|
|
11
|
-
returns([])
|
|
12
|
-
File.expects(:exist?).with(Dir.home).returns(true)
|
|
13
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.expects(:rebuild_secrets).returns(nil)
|
|
14
|
-
runner = ForemanAnsibleCore::Runner::Playbook.new(nil, nil, :suspended_action => nil)
|
|
15
|
-
assert '/etc/ansible', runner.instance_variable_get('@ansible_dir')
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
context 'working_dir' do
|
|
20
|
-
setup do
|
|
21
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.stubs(:unknown_hosts).
|
|
22
|
-
returns([])
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
test 'creates temp one if not provided' do
|
|
26
|
-
Dir.expects(:mktmpdir)
|
|
27
|
-
File.expects(:exist?).with(Dir.home).returns(true)
|
|
28
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.expects(:rebuild_secrets).returns(nil)
|
|
29
|
-
ForemanAnsibleCore::Runner::Playbook.new(nil, nil, :suspended_action => nil)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
test 'reads it when provided' do
|
|
33
|
-
settings = { :working_dir => '/foo', :ansible_dir => '/etc/foo' }
|
|
34
|
-
ForemanAnsibleCore.expects(:settings).returns(settings)
|
|
35
|
-
File.expects(:exist?).with(settings[:ansible_dir]).returns(true)
|
|
36
|
-
Dir.expects(:mktmpdir).never
|
|
37
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.expects(:rebuild_secrets).returns(nil)
|
|
38
|
-
runner = ForemanAnsibleCore::Runner::Playbook.new(nil, nil, :suspended_action => nil)
|
|
39
|
-
assert '/foo', runner.instance_variable_get('@working_dir')
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
context 'TOFU policy' do # Trust On First Use
|
|
44
|
-
setup do
|
|
45
|
-
@inventory = { 'all' => { 'hosts' => ['foreman.example.com'] } }
|
|
46
|
-
@output = StringIO.new
|
|
47
|
-
logger = Logger.new(@output)
|
|
48
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.stubs(:logger).
|
|
49
|
-
returns(logger)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
test 'ignores known hosts' do
|
|
53
|
-
Net::SSH::KnownHosts.expects(:search_for).
|
|
54
|
-
with('foreman.example.com').returns(['somekey'])
|
|
55
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.
|
|
56
|
-
expects(:add_to_known_hosts).never
|
|
57
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.expects(:rebuild_secrets).returns(@inventory)
|
|
58
|
-
ForemanAnsibleCore::Runner::Playbook.new(@inventory, nil, :suspended_action => nil)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
test 'adds unknown hosts to known_hosts' do
|
|
62
|
-
Net::SSH::KnownHosts.expects(:search_for).
|
|
63
|
-
with('foreman.example.com').returns([])
|
|
64
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.
|
|
65
|
-
expects(:add_to_known_hosts).with('foreman.example.com')
|
|
66
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.expects(:rebuild_secrets).returns(@inventory)
|
|
67
|
-
ForemanAnsibleCore::Runner::Playbook.new(@inventory, nil, :suspended_action => nil)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
test 'logs error when it cannot add to known_hosts' do
|
|
71
|
-
Net::SSH::KnownHosts.expects(:search_for).
|
|
72
|
-
with('foreman.example.com').returns([])
|
|
73
|
-
Net::SSH::Transport::Session.expects(:new).with('foreman.example.com').
|
|
74
|
-
raises(Net::Error)
|
|
75
|
-
ForemanAnsibleCore::Runner::Playbook.any_instance.expects(:rebuild_secrets).returns(@inventory)
|
|
76
|
-
ForemanAnsibleCore::Runner::Playbook.new(@inventory, nil, :suspended_action => nil)
|
|
77
|
-
assert_match(
|
|
78
|
-
/ERROR.*Failed to save host key for foreman.example.com: Net::Error/,
|
|
79
|
-
@output.string
|
|
80
|
-
)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
context 'rebuild secrets' do
|
|
85
|
-
let(:inventory) do
|
|
86
|
-
{ 'all' => { 'hosts' => ['foreman.example.com'] },
|
|
87
|
-
'_meta' => { 'hostvars' => { 'foreman.example.com' => {} } } }
|
|
88
|
-
end
|
|
89
|
-
let(:secrets) do
|
|
90
|
-
host_secrets = { 'ansible_password' => 'letmein', 'ansible_become_password' => 'iamroot' }
|
|
91
|
-
{ 'per-host' => { 'foreman.example.com' => host_secrets } }
|
|
92
|
-
end
|
|
93
|
-
let(:runner) { ForemanAnsibleCore::Runner::Playbook.allocate }
|
|
94
|
-
|
|
95
|
-
test 'uses secrets from inventory' do
|
|
96
|
-
test_inventory = inventory.merge('ssh_password' => 'sshpass', 'effective_user_password' => 'mypass')
|
|
97
|
-
rebuilt = runner.send(:rebuild_secrets, test_inventory, secrets)
|
|
98
|
-
host_vars = rebuilt.dig('_meta', 'hostvars', 'foreman.example.com')
|
|
99
|
-
assert_equal 'sshpass', host_vars['ansible_password']
|
|
100
|
-
assert_equal 'mypass', host_vars['ansible_become_password']
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
test 'host secrets are used when not overriden by inventory secrest' do
|
|
104
|
-
rebuilt = runner.send(:rebuild_secrets, inventory, secrets)
|
|
105
|
-
host_vars = rebuilt.dig('_meta', 'hostvars', 'foreman.example.com')
|
|
106
|
-
assert_equal 'letmein', host_vars['ansible_password']
|
|
107
|
-
assert_equal 'iamroot', host_vars['ansible_become_password']
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const translate = s => s;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { camelCase } from 'lodash';
|
|
2
|
-
|
|
3
|
-
export const propsToCamelCase = ob =>
|
|
4
|
-
propsToCase(camelCase, 'propsToCamelCase only takes objects', ob);
|
|
5
|
-
|
|
6
|
-
const propsToCase = (casingFn, errorMsg, ob) => {
|
|
7
|
-
if (typeof ob !== 'object') throw Error(errorMsg);
|
|
8
|
-
|
|
9
|
-
return Object.keys(ob).reduce((memo, key) => {
|
|
10
|
-
memo[casingFn(key)] = ob[key];
|
|
11
|
-
return memo;
|
|
12
|
-
}, {});
|
|
13
|
-
};
|