knife-windows 1.3.0 → 1.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/.gitignore +5 -5
- data/.travis.yml +26 -26
- data/CHANGELOG.md +112 -108
- data/DOC_CHANGES.md +14 -14
- data/Gemfile +12 -12
- data/LICENSE +201 -201
- data/README.md +391 -385
- data/RELEASE_NOTES.md +34 -34
- data/Rakefile +21 -21
- data/appveyor.yml +42 -42
- data/ci.gemfile +15 -15
- data/features/knife_help.feature +20 -20
- data/features/support/env.rb +5 -5
- data/knife-windows.gemspec +25 -25
- data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +233 -247
- data/lib/chef/knife/bootstrap_windows_base.rb +449 -415
- data/lib/chef/knife/bootstrap_windows_ssh.rb +115 -115
- data/lib/chef/knife/bootstrap_windows_winrm.rb +95 -95
- data/lib/chef/knife/core/windows_bootstrap_context.rb +372 -366
- data/lib/chef/knife/knife_windows_base.rb +33 -33
- data/lib/chef/knife/windows_cert_generate.rb +155 -155
- data/lib/chef/knife/windows_cert_install.rb +68 -68
- data/lib/chef/knife/windows_helper.rb +36 -36
- data/lib/chef/knife/windows_listener_create.rb +107 -107
- data/lib/chef/knife/winrm.rb +122 -122
- data/lib/chef/knife/winrm_base.rb +117 -117
- data/lib/chef/knife/winrm_knife_base.rb +305 -303
- data/lib/chef/knife/winrm_session.rb +88 -87
- data/lib/chef/knife/winrm_shared_options.rb +47 -47
- data/lib/chef/knife/wsman_endpoint.rb +44 -44
- data/lib/chef/knife/wsman_test.rb +117 -117
- data/lib/knife-windows/path_helper.rb +234 -234
- data/lib/knife-windows/version.rb +6 -6
- data/spec/assets/win_template_rendered_with_bootstrap_install_command.txt +217 -217
- data/spec/assets/win_template_rendered_with_bootstrap_install_command_on_12_5_client.txt +217 -217
- data/spec/assets/win_template_rendered_without_bootstrap_install_command.txt +329 -329
- data/spec/assets/win_template_rendered_without_bootstrap_install_command_on_12_5_client.txt +329 -329
- data/spec/assets/win_template_unrendered.txt +246 -246
- data/spec/functional/bootstrap_download_spec.rb +241 -234
- data/spec/spec_helper.rb +94 -93
- data/spec/unit/knife/bootstrap_options_spec.rb +155 -155
- data/spec/unit/knife/bootstrap_template_spec.rb +98 -92
- data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +341 -295
- data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +177 -177
- data/spec/unit/knife/windows_cert_generate_spec.rb +90 -90
- data/spec/unit/knife/windows_cert_install_spec.rb +51 -51
- data/spec/unit/knife/windows_listener_create_spec.rb +76 -76
- data/spec/unit/knife/winrm_session_spec.rb +65 -65
- data/spec/unit/knife/winrm_spec.rb +516 -516
- data/spec/unit/knife/wsman_test_spec.rb +202 -202
- metadata +23 -4
@@ -1,115 +1,115 @@
|
|
1
|
-
#
|
2
|
-
# Author:: Seth Chisamore (<schisamo@
|
3
|
-
# Copyright:: Copyright (c) 2011
|
4
|
-
# License:: Apache License, Version 2.0
|
5
|
-
#
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
-
# you may not use this file except in compliance with the License.
|
8
|
-
# You may obtain a copy of the License at
|
9
|
-
#
|
10
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
-
#
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
-
# See the License for the specific language governing permissions and
|
16
|
-
# limitations under the License.
|
17
|
-
#
|
18
|
-
|
19
|
-
require 'chef/knife/bootstrap_windows_base'
|
20
|
-
|
21
|
-
class Chef
|
22
|
-
class Knife
|
23
|
-
class BootstrapWindowsSsh < Bootstrap
|
24
|
-
|
25
|
-
include Chef::Knife::BootstrapWindowsBase
|
26
|
-
|
27
|
-
deps do
|
28
|
-
require 'chef/knife/core/windows_bootstrap_context'
|
29
|
-
require 'chef/json_compat'
|
30
|
-
require 'tempfile'
|
31
|
-
require 'highline'
|
32
|
-
require 'net/ssh'
|
33
|
-
require 'net/ssh/multi'
|
34
|
-
Chef::Knife::Ssh.load_deps
|
35
|
-
end
|
36
|
-
|
37
|
-
banner "knife bootstrap windows ssh FQDN (options)"
|
38
|
-
|
39
|
-
option :ssh_user,
|
40
|
-
:short => "-x USERNAME",
|
41
|
-
:long => "--ssh-user USERNAME",
|
42
|
-
:description => "The ssh username",
|
43
|
-
:default => "root"
|
44
|
-
|
45
|
-
option :ssh_password,
|
46
|
-
:short => "-P PASSWORD",
|
47
|
-
:long => "--ssh-password PASSWORD",
|
48
|
-
:description => "The ssh password"
|
49
|
-
|
50
|
-
option :ssh_port,
|
51
|
-
:short => "-p PORT",
|
52
|
-
:long => "--ssh-port PORT",
|
53
|
-
:description => "The ssh port",
|
54
|
-
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key.strip }
|
55
|
-
|
56
|
-
option :ssh_gateway,
|
57
|
-
:short => "-G GATEWAY",
|
58
|
-
:long => "--ssh-gateway GATEWAY",
|
59
|
-
:description => "The ssh gateway",
|
60
|
-
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
|
61
|
-
|
62
|
-
option :forward_agent,
|
63
|
-
:short => "-A",
|
64
|
-
:long => "--forward-agent",
|
65
|
-
:description => "Enable SSH agent forwarding",
|
66
|
-
:boolean => true
|
67
|
-
|
68
|
-
option :identity_file,
|
69
|
-
:long => "--identity-file IDENTITY_FILE",
|
70
|
-
:description => "The SSH identity file used for authentication. [DEPRECATED] Use --ssh-identity-file instead."
|
71
|
-
|
72
|
-
option :ssh_identity_file,
|
73
|
-
:short => "-i IDENTITY_FILE",
|
74
|
-
:long => "--ssh-identity-file IDENTITY_FILE",
|
75
|
-
:description => "The SSH identity file used for authentication"
|
76
|
-
|
77
|
-
# DEPR: Remove this option for the next release.
|
78
|
-
option :host_key_verification,
|
79
|
-
:long => "--[no-]host-key-verify",
|
80
|
-
:description => "Verify host key, enabled by default. [DEPRECATED] Use --host-key-verify option instead.",
|
81
|
-
:boolean => true,
|
82
|
-
:default => true,
|
83
|
-
:proc => Proc.new { |key|
|
84
|
-
Chef::Log.warn("[DEPRECATED] --host-key-verification option is deprecated. Use --host-key-verify option instead.")
|
85
|
-
config[:host_key_verify] = key
|
86
|
-
}
|
87
|
-
|
88
|
-
option :host_key_verify,
|
89
|
-
:long => "--[no-]host-key-verify",
|
90
|
-
:description => "Verify host key, enabled by default.",
|
91
|
-
:boolean => true,
|
92
|
-
:default => true
|
93
|
-
|
94
|
-
def run
|
95
|
-
bootstrap
|
96
|
-
end
|
97
|
-
|
98
|
-
def run_command(command = '')
|
99
|
-
ssh = Chef::Knife::Ssh.new
|
100
|
-
ssh.name_args = [ server_name, command ]
|
101
|
-
ssh.config[:ssh_user] = locate_config_value(:ssh_user)
|
102
|
-
ssh.config[:ssh_password] = locate_config_value(:ssh_password)
|
103
|
-
ssh.config[:ssh_port] = locate_config_value(:ssh_port)
|
104
|
-
ssh.config[:ssh_gateway] = locate_config_value(:ssh_gateway)
|
105
|
-
ssh.config[:identity_file] = config[:identity_file]
|
106
|
-
ssh.config[:ssh_identity_file] = config[:ssh_identity_file] || config[:identity_file]
|
107
|
-
ssh.config[:forward_agent] = config[:forward_agent]
|
108
|
-
ssh.config[:manual] = true
|
109
|
-
ssh.config[:host_key_verify] = config[:host_key_verify]
|
110
|
-
ssh.run
|
111
|
-
end
|
112
|
-
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
1
|
+
#
|
2
|
+
# Author:: Seth Chisamore (<schisamo@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2011-2016 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'chef/knife/bootstrap_windows_base'
|
20
|
+
|
21
|
+
class Chef
|
22
|
+
class Knife
|
23
|
+
class BootstrapWindowsSsh < Bootstrap
|
24
|
+
|
25
|
+
include Chef::Knife::BootstrapWindowsBase
|
26
|
+
|
27
|
+
deps do
|
28
|
+
require 'chef/knife/core/windows_bootstrap_context'
|
29
|
+
require 'chef/json_compat'
|
30
|
+
require 'tempfile'
|
31
|
+
require 'highline'
|
32
|
+
require 'net/ssh'
|
33
|
+
require 'net/ssh/multi'
|
34
|
+
Chef::Knife::Ssh.load_deps
|
35
|
+
end
|
36
|
+
|
37
|
+
banner "knife bootstrap windows ssh FQDN (options)"
|
38
|
+
|
39
|
+
option :ssh_user,
|
40
|
+
:short => "-x USERNAME",
|
41
|
+
:long => "--ssh-user USERNAME",
|
42
|
+
:description => "The ssh username",
|
43
|
+
:default => "root"
|
44
|
+
|
45
|
+
option :ssh_password,
|
46
|
+
:short => "-P PASSWORD",
|
47
|
+
:long => "--ssh-password PASSWORD",
|
48
|
+
:description => "The ssh password"
|
49
|
+
|
50
|
+
option :ssh_port,
|
51
|
+
:short => "-p PORT",
|
52
|
+
:long => "--ssh-port PORT",
|
53
|
+
:description => "The ssh port",
|
54
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key.strip }
|
55
|
+
|
56
|
+
option :ssh_gateway,
|
57
|
+
:short => "-G GATEWAY",
|
58
|
+
:long => "--ssh-gateway GATEWAY",
|
59
|
+
:description => "The ssh gateway",
|
60
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
|
61
|
+
|
62
|
+
option :forward_agent,
|
63
|
+
:short => "-A",
|
64
|
+
:long => "--forward-agent",
|
65
|
+
:description => "Enable SSH agent forwarding",
|
66
|
+
:boolean => true
|
67
|
+
|
68
|
+
option :identity_file,
|
69
|
+
:long => "--identity-file IDENTITY_FILE",
|
70
|
+
:description => "The SSH identity file used for authentication. [DEPRECATED] Use --ssh-identity-file instead."
|
71
|
+
|
72
|
+
option :ssh_identity_file,
|
73
|
+
:short => "-i IDENTITY_FILE",
|
74
|
+
:long => "--ssh-identity-file IDENTITY_FILE",
|
75
|
+
:description => "The SSH identity file used for authentication"
|
76
|
+
|
77
|
+
# DEPR: Remove this option for the next release.
|
78
|
+
option :host_key_verification,
|
79
|
+
:long => "--[no-]host-key-verify",
|
80
|
+
:description => "Verify host key, enabled by default. [DEPRECATED] Use --host-key-verify option instead.",
|
81
|
+
:boolean => true,
|
82
|
+
:default => true,
|
83
|
+
:proc => Proc.new { |key|
|
84
|
+
Chef::Log.warn("[DEPRECATED] --host-key-verification option is deprecated. Use --host-key-verify option instead.")
|
85
|
+
config[:host_key_verify] = key
|
86
|
+
}
|
87
|
+
|
88
|
+
option :host_key_verify,
|
89
|
+
:long => "--[no-]host-key-verify",
|
90
|
+
:description => "Verify host key, enabled by default.",
|
91
|
+
:boolean => true,
|
92
|
+
:default => true
|
93
|
+
|
94
|
+
def run
|
95
|
+
bootstrap
|
96
|
+
end
|
97
|
+
|
98
|
+
def run_command(command = '')
|
99
|
+
ssh = Chef::Knife::Ssh.new
|
100
|
+
ssh.name_args = [ server_name, command ]
|
101
|
+
ssh.config[:ssh_user] = locate_config_value(:ssh_user)
|
102
|
+
ssh.config[:ssh_password] = locate_config_value(:ssh_password)
|
103
|
+
ssh.config[:ssh_port] = locate_config_value(:ssh_port)
|
104
|
+
ssh.config[:ssh_gateway] = locate_config_value(:ssh_gateway)
|
105
|
+
ssh.config[:identity_file] = config[:identity_file]
|
106
|
+
ssh.config[:ssh_identity_file] = config[:ssh_identity_file] || config[:identity_file]
|
107
|
+
ssh.config[:forward_agent] = config[:forward_agent]
|
108
|
+
ssh.config[:manual] = true
|
109
|
+
ssh.config[:host_key_verify] = config[:host_key_verify]
|
110
|
+
ssh.run
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -1,95 +1,95 @@
|
|
1
|
-
#
|
2
|
-
# Author:: Seth Chisamore (<schisamo@
|
3
|
-
# Copyright:: Copyright (c) 2011
|
4
|
-
# License:: Apache License, Version 2.0
|
5
|
-
#
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
-
# you may not use this file except in compliance with the License.
|
8
|
-
# You may obtain a copy of the License at
|
9
|
-
#
|
10
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
-
#
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
-
# See the License for the specific language governing permissions and
|
16
|
-
# limitations under the License.
|
17
|
-
#
|
18
|
-
|
19
|
-
require 'chef/knife/bootstrap_windows_base'
|
20
|
-
require 'chef/knife/winrm'
|
21
|
-
require 'chef/knife/winrm_base'
|
22
|
-
require 'chef/knife/winrm_knife_base'
|
23
|
-
|
24
|
-
|
25
|
-
class Chef
|
26
|
-
class Knife
|
27
|
-
class BootstrapWindowsWinrm < Bootstrap
|
28
|
-
|
29
|
-
include Chef::Knife::BootstrapWindowsBase
|
30
|
-
include Chef::Knife::WinrmBase
|
31
|
-
include Chef::Knife::WinrmCommandSharedFunctions
|
32
|
-
|
33
|
-
deps do
|
34
|
-
require 'chef/knife/core/windows_bootstrap_context'
|
35
|
-
require 'chef/json_compat'
|
36
|
-
require 'tempfile'
|
37
|
-
Chef::Knife::Winrm.load_deps
|
38
|
-
end
|
39
|
-
|
40
|
-
banner 'knife bootstrap windows winrm FQDN (options)'
|
41
|
-
|
42
|
-
def run
|
43
|
-
if (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
|
44
|
-
if !negotiate_auth? && !(locate_config_value(:winrm_transport) == 'ssl')
|
45
|
-
ui.error('Validatorless bootstrap over unsecure winrm channels could expose your key to network sniffing')
|
46
|
-
exit 1
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
config[:manual] = true
|
51
|
-
configure_session
|
52
|
-
|
53
|
-
bootstrap
|
54
|
-
end
|
55
|
-
|
56
|
-
protected
|
57
|
-
|
58
|
-
def wait_for_remote_response(wait_max_minutes)
|
59
|
-
wait_max_seconds = wait_max_minutes * 60
|
60
|
-
retry_interval_seconds = 10
|
61
|
-
retries_left = wait_max_seconds / retry_interval_seconds
|
62
|
-
print(ui.color("\nWaiting for remote response before bootstrap", :magenta))
|
63
|
-
wait_start_time = Time.now
|
64
|
-
begin
|
65
|
-
print(".")
|
66
|
-
# Return status of the command is non-zero, typically nil,
|
67
|
-
# for our simple echo command in cases where run_command
|
68
|
-
# swallows the exception, such as 401's. Treat such cases
|
69
|
-
# the same as the case where we encounter an exception.
|
70
|
-
status = run_command("echo . & echo Response received.")
|
71
|
-
raise RuntimeError, 'Command execution failed.' if status != 0
|
72
|
-
ui.info(ui.color("Remote node responded after #{elapsed_time_in_minutes(wait_start_time)} minutes.", :magenta))
|
73
|
-
return
|
74
|
-
rescue Errno::ECONNREFUSED => e
|
75
|
-
ui.error("Connection refused connecting to #{locate_config_value(:server_name)}:#{locate_config_value(:winrm_port)}.")
|
76
|
-
raise
|
77
|
-
rescue Exception => e
|
78
|
-
retries_left -= 1
|
79
|
-
if retries_left <= 0 || (elapsed_time_in_minutes(wait_start_time) > wait_max_minutes)
|
80
|
-
ui.error("No response received from remote node after #{elapsed_time_in_minutes(wait_start_time)} minutes, giving up.")
|
81
|
-
ui.error("Exception: #{e.message}")
|
82
|
-
raise
|
83
|
-
end
|
84
|
-
print '.'
|
85
|
-
sleep retry_interval_seconds
|
86
|
-
retry
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def elapsed_time_in_minutes(start_time)
|
91
|
-
((Time.now - start_time) / 60).round(2)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
1
|
+
#
|
2
|
+
# Author:: Seth Chisamore (<schisamo@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2011-2016 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'chef/knife/bootstrap_windows_base'
|
20
|
+
require 'chef/knife/winrm'
|
21
|
+
require 'chef/knife/winrm_base'
|
22
|
+
require 'chef/knife/winrm_knife_base'
|
23
|
+
|
24
|
+
|
25
|
+
class Chef
|
26
|
+
class Knife
|
27
|
+
class BootstrapWindowsWinrm < Bootstrap
|
28
|
+
|
29
|
+
include Chef::Knife::BootstrapWindowsBase
|
30
|
+
include Chef::Knife::WinrmBase
|
31
|
+
include Chef::Knife::WinrmCommandSharedFunctions
|
32
|
+
|
33
|
+
deps do
|
34
|
+
require 'chef/knife/core/windows_bootstrap_context'
|
35
|
+
require 'chef/json_compat'
|
36
|
+
require 'tempfile'
|
37
|
+
Chef::Knife::Winrm.load_deps
|
38
|
+
end
|
39
|
+
|
40
|
+
banner 'knife bootstrap windows winrm FQDN (options)'
|
41
|
+
|
42
|
+
def run
|
43
|
+
if (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
|
44
|
+
if !negotiate_auth? && !(locate_config_value(:winrm_transport) == 'ssl')
|
45
|
+
ui.error('Validatorless bootstrap over unsecure winrm channels could expose your key to network sniffing')
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
config[:manual] = true
|
51
|
+
configure_session
|
52
|
+
|
53
|
+
bootstrap
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def wait_for_remote_response(wait_max_minutes)
|
59
|
+
wait_max_seconds = wait_max_minutes * 60
|
60
|
+
retry_interval_seconds = 10
|
61
|
+
retries_left = wait_max_seconds / retry_interval_seconds
|
62
|
+
print(ui.color("\nWaiting for remote response before bootstrap", :magenta))
|
63
|
+
wait_start_time = Time.now
|
64
|
+
begin
|
65
|
+
print(".")
|
66
|
+
# Return status of the command is non-zero, typically nil,
|
67
|
+
# for our simple echo command in cases where run_command
|
68
|
+
# swallows the exception, such as 401's. Treat such cases
|
69
|
+
# the same as the case where we encounter an exception.
|
70
|
+
status = run_command("echo . & echo Response received.")
|
71
|
+
raise RuntimeError, 'Command execution failed.' if status != 0
|
72
|
+
ui.info(ui.color("Remote node responded after #{elapsed_time_in_minutes(wait_start_time)} minutes.", :magenta))
|
73
|
+
return
|
74
|
+
rescue Errno::ECONNREFUSED => e
|
75
|
+
ui.error("Connection refused connecting to #{locate_config_value(:server_name)}:#{locate_config_value(:winrm_port)}.")
|
76
|
+
raise
|
77
|
+
rescue Exception => e
|
78
|
+
retries_left -= 1
|
79
|
+
if retries_left <= 0 || (elapsed_time_in_minutes(wait_start_time) > wait_max_minutes)
|
80
|
+
ui.error("No response received from remote node after #{elapsed_time_in_minutes(wait_start_time)} minutes, giving up.")
|
81
|
+
ui.error("Exception: #{e.message}")
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
print '.'
|
85
|
+
sleep retry_interval_seconds
|
86
|
+
retry
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def elapsed_time_in_minutes(start_time)
|
91
|
+
((Time.now - start_time) / 60).round(2)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|