bosh_cli 0.18 → 0.19
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.
- data/bin/bosh +7 -4
- data/lib/cli.rb +2 -0
- data/lib/cli/commands/base.rb +1 -18
- data/lib/cli/commands/biff.rb +3 -7
- data/lib/cli/commands/cloudcheck.rb +1 -0
- data/lib/cli/commands/deployment.rb +10 -6
- data/lib/cli/commands/job_rename.rb +116 -0
- data/lib/cli/commands/stemcell.rb +3 -3
- data/lib/cli/commands/task.rb +52 -143
- data/lib/cli/commands/vms.rb +2 -2
- data/lib/cli/config.rb +1 -0
- data/lib/cli/deployment_helper.rb +62 -22
- data/lib/cli/director.rb +85 -196
- data/lib/cli/director_task.rb +4 -4
- data/lib/cli/event_log_renderer.rb +5 -1
- data/lib/cli/null_renderer.rb +19 -0
- data/lib/cli/package_builder.rb +91 -62
- data/lib/cli/packaging_helper.rb +1 -1
- data/lib/cli/release_builder.rb +47 -13
- data/lib/cli/runner.rb +21 -39
- data/lib/cli/task_log_renderer.rb +9 -0
- data/lib/cli/task_tracker.rb +168 -0
- data/lib/cli/templates/help_message.erb +1 -0
- data/lib/cli/version.rb +1 -1
- data/lib/cli/versions_index.rb +3 -3
- data/spec/unit/biff_spec.rb +5 -0
- data/spec/unit/director_spec.rb +96 -192
- data/spec/unit/job_rename_spec.rb +195 -0
- data/spec/unit/package_builder_spec.rb +188 -186
- data/spec/unit/release_builder_spec.rb +27 -9
- data/spec/unit/runner_spec.rb +0 -25
- data/spec/unit/task_tracker_spec.rb +154 -0
- metadata +11 -4
data/lib/cli/director_task.rb
CHANGED
@@ -8,10 +8,10 @@ module Bosh
|
|
8
8
|
|
9
9
|
def initialize(director, task_id, log_type = nil)
|
10
10
|
@director = director
|
11
|
-
@task_id
|
12
|
-
@offset
|
11
|
+
@task_id = task_id
|
12
|
+
@offset = 0
|
13
13
|
@log_type = log_type
|
14
|
-
@buf
|
14
|
+
@buf = ""
|
15
15
|
end
|
16
16
|
|
17
17
|
def state
|
@@ -36,7 +36,7 @@ module Bosh
|
|
36
36
|
|
37
37
|
last_nl = @buf.rindex("\n")
|
38
38
|
|
39
|
-
if
|
39
|
+
if last_nl.nil?
|
40
40
|
result = nil
|
41
41
|
elsif last_nl != @buf.size - 1
|
42
42
|
result = @buf[0..last_nl]
|
@@ -124,8 +124,12 @@ module Bosh::Cli
|
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
127
|
+
def duration_known?
|
128
|
+
@started_at && @finished_at
|
129
|
+
end
|
130
|
+
|
127
131
|
def duration
|
128
|
-
return
|
132
|
+
return unless duration_known?
|
129
133
|
@finished_at - @started_at
|
130
134
|
end
|
131
135
|
|
data/lib/cli/package_builder.rb
CHANGED
@@ -4,6 +4,37 @@ module Bosh::Cli
|
|
4
4
|
class PackageBuilder
|
5
5
|
include PackagingHelper
|
6
6
|
|
7
|
+
class GlobMatch
|
8
|
+
# Helper class encapsulating the data we know about the glob. We need
|
9
|
+
# both directory and file path, as we match the same path in several
|
10
|
+
# directories (src, src_alt, blobs)
|
11
|
+
attr_reader :dir
|
12
|
+
attr_reader :path
|
13
|
+
|
14
|
+
def initialize(dir, path)
|
15
|
+
@dir = dir
|
16
|
+
@path = path
|
17
|
+
end
|
18
|
+
|
19
|
+
def full_path
|
20
|
+
File.join(dir, path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def <=>(other)
|
24
|
+
@path <=> other.path
|
25
|
+
end
|
26
|
+
|
27
|
+
# GlobMatch will be used as Hash key (as implied by using Set),
|
28
|
+
# hence we need to define both eql? and hash
|
29
|
+
def eql?(other)
|
30
|
+
@path == other.path
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash
|
34
|
+
@path.hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
7
38
|
attr_reader :name, :globs, :version, :dependencies, :tarball_path
|
8
39
|
# We have two ways of getting/storing a package:
|
9
40
|
# development versions of packages, kept in release directory
|
@@ -13,21 +44,28 @@ module Bosh::Cli
|
|
13
44
|
# final builds metadata should be checked in
|
14
45
|
|
15
46
|
def initialize(spec, release_dir, final, blobstore,
|
16
|
-
sources_dir = nil, blobs_dir = nil)
|
47
|
+
sources_dir = nil, blobs_dir = nil, alt_src_dir = nil)
|
17
48
|
spec = load_yaml_file(spec) if spec.is_a?(String) && File.file?(spec)
|
18
49
|
|
19
50
|
@name = spec["name"]
|
20
51
|
@globs = spec["files"]
|
21
52
|
@dependencies = Array(spec["dependencies"])
|
53
|
+
|
22
54
|
@release_dir = release_dir
|
23
55
|
@sources_dir = sources_dir || File.join(@release_dir, "src")
|
56
|
+
@alt_sources_dir = alt_src_dir || File.join(@release_dir, "src_alt")
|
24
57
|
@blobs_dir = blobs_dir || File.join(@release_dir, "blobs")
|
58
|
+
|
25
59
|
@final = final
|
26
60
|
@blobstore = blobstore
|
27
61
|
@artefact_type = "package"
|
28
62
|
|
29
63
|
@metadata_files = %w(packaging pre_packaging)
|
30
64
|
|
65
|
+
if @final && File.exists?(@alt_sources_dir)
|
66
|
+
err("Please remove `#{File.basename(@alt_sources_dir)}' first")
|
67
|
+
end
|
68
|
+
|
31
69
|
if @name.blank?
|
32
70
|
raise InvalidPackage, "Package name is missing"
|
33
71
|
end
|
@@ -65,22 +103,10 @@ module Bosh::Cli
|
|
65
103
|
@fingerprint ||= make_fingerprint
|
66
104
|
end
|
67
105
|
|
68
|
-
def
|
106
|
+
def glob_matches
|
69
107
|
@resolved_globs ||= resolve_globs
|
70
108
|
end
|
71
109
|
|
72
|
-
def source_files
|
73
|
-
resolved_globs[:source]
|
74
|
-
end
|
75
|
-
|
76
|
-
def blob_files
|
77
|
-
resolved_globs[:blob]
|
78
|
-
end
|
79
|
-
|
80
|
-
def files
|
81
|
-
(resolved_globs[:blob] + resolved_globs[:source]).sort
|
82
|
-
end
|
83
|
-
|
84
110
|
def build_dir
|
85
111
|
@build_dir ||= Dir.mktmpdir
|
86
112
|
end
|
@@ -92,15 +118,14 @@ module Bosh::Cli
|
|
92
118
|
def copy_files
|
93
119
|
copied = 0
|
94
120
|
|
95
|
-
|
96
|
-
|
97
|
-
destination = File.join(build_dir, filename)
|
121
|
+
glob_matches.each do |match|
|
122
|
+
destination = File.join(build_dir, match.path)
|
98
123
|
|
99
|
-
if File.directory?(
|
124
|
+
if File.directory?(match.full_path)
|
100
125
|
FileUtils.mkdir_p(destination)
|
101
126
|
else
|
102
127
|
FileUtils.mkdir_p(File.dirname(destination))
|
103
|
-
FileUtils.cp(
|
128
|
+
FileUtils.cp(match.full_path, destination, :preserve => true)
|
104
129
|
copied += 1
|
105
130
|
end
|
106
131
|
end
|
@@ -160,27 +185,18 @@ module Bosh::Cli
|
|
160
185
|
|
161
186
|
private
|
162
187
|
|
163
|
-
def get_file_path(file)
|
164
|
-
file_path = File.join(@sources_dir, file)
|
165
|
-
if !File.exists?(file_path)
|
166
|
-
file_path = File.join(@blobs_dir, file)
|
167
|
-
unless File.exists?(file_path)
|
168
|
-
raise InvalidPackage, "#{file} cannot be found"
|
169
|
-
end
|
170
|
-
end
|
171
|
-
file_path
|
172
|
-
end
|
173
|
-
|
174
188
|
def make_fingerprint
|
175
189
|
contents = ""
|
176
190
|
|
177
|
-
signatures =
|
178
|
-
|
179
|
-
|
180
|
-
|
191
|
+
signatures = glob_matches.map do |match|
|
192
|
+
file_digest = nil
|
193
|
+
|
194
|
+
unless File.directory?(match.full_path)
|
195
|
+
file_digest = Digest::SHA1.file(match.full_path).hexdigest
|
181
196
|
end
|
182
197
|
|
183
|
-
"%s%s%s" % [
|
198
|
+
"%s%s%s" % [match.path, file_digest,
|
199
|
+
tracked_permissions(match.full_path)]
|
184
200
|
end
|
185
201
|
contents << signatures.join("")
|
186
202
|
|
@@ -198,49 +214,62 @@ module Bosh::Cli
|
|
198
214
|
Digest::SHA1.hexdigest(contents)
|
199
215
|
end
|
200
216
|
|
217
|
+
# @return Array<GlobMatch>
|
201
218
|
def resolve_globs
|
202
|
-
|
203
|
-
blob_list = []
|
204
|
-
source_list = []
|
219
|
+
matches = Set.new
|
205
220
|
|
206
221
|
@globs.each do |glob|
|
207
|
-
|
208
|
-
|
222
|
+
# Alternative source dir completely shadows the source dir, there can be
|
223
|
+
# no partial match of a particular glob in both.
|
224
|
+
found = false
|
225
|
+
|
226
|
+
[@alt_sources_dir, @sources_dir].each do |dir|
|
227
|
+
next unless File.directory?(dir)
|
228
|
+
|
229
|
+
Dir.chdir(dir) do
|
230
|
+
dir_matches = resolve_glob_in_cwd(glob)
|
209
231
|
|
210
|
-
|
211
|
-
|
212
|
-
|
232
|
+
unless dir_matches.empty?
|
233
|
+
matches += dir_matches.map do |path|
|
234
|
+
GlobMatch.new(dir, path)
|
235
|
+
end
|
236
|
+
found = true
|
237
|
+
end
|
213
238
|
end
|
239
|
+
|
240
|
+
break if found
|
214
241
|
end
|
215
242
|
|
216
|
-
|
217
|
-
|
218
|
-
|
243
|
+
# Blobs directory is a little bit different: whatever matches a blob
|
244
|
+
# will complement already found matches, unless this particular path
|
245
|
+
# has already been matched.
|
246
|
+
if File.directory?(File.join(@blobs_dir))
|
247
|
+
Dir.chdir(@blobs_dir) do
|
248
|
+
blob_matches = resolve_glob_in_cwd(glob)
|
249
|
+
|
250
|
+
unless blob_matches.empty?
|
251
|
+
blob_matches.each do |path|
|
252
|
+
matches << GlobMatch.new(@blobs_dir, path)
|
253
|
+
end
|
254
|
+
|
255
|
+
found = true
|
256
|
+
end
|
219
257
|
end
|
220
258
|
end
|
221
259
|
|
222
|
-
|
260
|
+
unless found
|
223
261
|
raise InvalidPackage, "`#{name}' has a glob that " +
|
224
|
-
|
262
|
+
"resolves to an empty file list: #{glob}"
|
225
263
|
end
|
226
|
-
|
227
|
-
blob_list << matching_blob
|
228
|
-
source_list << matching_source
|
229
264
|
end
|
230
|
-
glob_map[:blob] = blob_list.flatten.sort
|
231
|
-
glob_map[:source] = source_list.flatten.sort
|
232
|
-
glob_map
|
233
|
-
end
|
234
265
|
|
235
|
-
|
236
|
-
# old release does not have 'blob'
|
237
|
-
if File.directory?(@blobs_dir)
|
238
|
-
Dir.chdir(@blobs_dir) { yield }
|
239
|
-
end
|
266
|
+
matches.sort
|
240
267
|
end
|
241
268
|
|
242
|
-
def
|
243
|
-
Dir.
|
269
|
+
def resolve_glob_in_cwd(glob)
|
270
|
+
Dir.glob(glob, File::FNM_DOTMATCH).reject do |fn|
|
271
|
+
%w(. ..).include?(File.basename(fn))
|
272
|
+
end
|
244
273
|
end
|
245
274
|
|
246
275
|
def in_build_dir(&block)
|
data/lib/cli/packaging_helper.rb
CHANGED
@@ -92,7 +92,7 @@ module Bosh::Cli
|
|
92
92
|
end
|
93
93
|
|
94
94
|
if need_fetch
|
95
|
-
say("Downloading `#{name} (#{version})'
|
95
|
+
say("Downloading `#{name} (#{version})'...".green)
|
96
96
|
payload = @blobstore.get(blobstore_id)
|
97
97
|
if Digest::SHA1.hexdigest(payload) == item["sha1"]
|
98
98
|
@tarball_path = @final_index.add_version(fingerprint, item, payload)
|
data/lib/cli/release_builder.rb
CHANGED
@@ -3,34 +3,47 @@
|
|
3
3
|
module Bosh::Cli
|
4
4
|
class ReleaseBuilder
|
5
5
|
include Bosh::Cli::DependencyHelper
|
6
|
+
include Bosh::Cli::VersionCalc
|
6
7
|
|
7
8
|
DEFAULT_RELEASE_NAME = "bosh_release"
|
8
9
|
|
9
|
-
attr_reader :release, :packages, :jobs, :
|
10
|
+
attr_reader :release, :packages, :jobs, :version
|
10
11
|
|
12
|
+
# @param [Bosh::Cli::Release] release Current release
|
13
|
+
# @param [Array<Bosh::Cli::PackageBuilder>] packages Built packages
|
14
|
+
# @param [Array<Bosh::Cli::JobBuilder>] jobs Built jobs
|
15
|
+
# @param [Hash] options Release build options
|
11
16
|
def initialize(release, packages, jobs, options = { })
|
12
|
-
@release
|
13
|
-
@final
|
17
|
+
@release = release
|
18
|
+
@final = options.has_key?(:final) ? !!options[:final] : false
|
14
19
|
@packages = packages
|
15
|
-
@jobs
|
20
|
+
@jobs = jobs
|
21
|
+
|
22
|
+
@final_index = VersionsIndex.new(final_releases_dir, release_name)
|
23
|
+
@dev_index = VersionsIndex.new(dev_releases_dir, release_name)
|
24
|
+
@index = @final ? @final_index : @dev_index
|
16
25
|
|
17
|
-
@index = VersionsIndex.new(releases_dir, release_name)
|
18
26
|
create_release_build_dir
|
19
27
|
end
|
20
28
|
|
29
|
+
# @return [String] Release name
|
21
30
|
def release_name
|
22
31
|
name = @final ? @release.final_name : @release.dev_name
|
23
32
|
name.blank? ? DEFAULT_RELEASE_NAME : name
|
24
33
|
end
|
25
34
|
|
35
|
+
# @return [String] Release version
|
26
36
|
def version
|
27
37
|
@version ||= assign_version
|
28
38
|
end
|
29
39
|
|
40
|
+
# @return [Boolean] Is release final?
|
30
41
|
def final?
|
31
42
|
@final
|
32
43
|
end
|
33
44
|
|
45
|
+
# @return [Array] List of jobs affected by this release compared
|
46
|
+
# to the previous one.
|
34
47
|
def affected_jobs
|
35
48
|
result = Set.new(@jobs.select { |job| job.new_version? })
|
36
49
|
return result if @packages.empty?
|
@@ -47,6 +60,8 @@ module Bosh::Cli
|
|
47
60
|
result.to_a
|
48
61
|
end
|
49
62
|
|
63
|
+
# Builds release
|
64
|
+
# @param [Hash] options Release build options
|
50
65
|
def build(options = {})
|
51
66
|
options = { :generate_tarball => true }.merge(options)
|
52
67
|
|
@@ -59,6 +74,7 @@ module Bosh::Cli
|
|
59
74
|
@build_complete = true
|
60
75
|
end
|
61
76
|
|
77
|
+
# Copies packages into release
|
62
78
|
def copy_packages
|
63
79
|
packages.each do |package|
|
64
80
|
say("%-40s %s" % [package.name.green,
|
@@ -70,6 +86,7 @@ module Bosh::Cli
|
|
70
86
|
@packages_copied = true
|
71
87
|
end
|
72
88
|
|
89
|
+
# Copies jobs into release
|
73
90
|
def copy_jobs
|
74
91
|
jobs.each do |job|
|
75
92
|
say("%-40s %s" % [job.name.green, pretty_size(job.tarball_path)])
|
@@ -80,24 +97,25 @@ module Bosh::Cli
|
|
80
97
|
@jobs_copied = true
|
81
98
|
end
|
82
99
|
|
100
|
+
# Generates release manifest
|
83
101
|
def generate_manifest
|
84
102
|
manifest = {}
|
85
103
|
manifest["packages"] = []
|
86
104
|
|
87
105
|
manifest["packages"] = packages.map do |package|
|
88
106
|
{
|
89
|
-
"name"
|
90
|
-
"version"
|
91
|
-
"sha1"
|
107
|
+
"name" => package.name,
|
108
|
+
"version" => package.version,
|
109
|
+
"sha1" => package.checksum,
|
92
110
|
"dependencies" => package.dependencies
|
93
111
|
}
|
94
112
|
end
|
95
113
|
|
96
114
|
manifest["jobs"] = jobs.map do |job|
|
97
115
|
{
|
98
|
-
"name"
|
116
|
+
"name" => job.name,
|
99
117
|
"version" => job.version,
|
100
|
-
"sha1"
|
118
|
+
"sha1" => job.checksum,
|
101
119
|
}
|
102
120
|
end
|
103
121
|
|
@@ -161,7 +179,15 @@ module Bosh::Cli
|
|
161
179
|
end
|
162
180
|
|
163
181
|
def releases_dir
|
164
|
-
|
182
|
+
@final ? final_releases_dir : dev_releases_dir
|
183
|
+
end
|
184
|
+
|
185
|
+
def final_releases_dir
|
186
|
+
File.join(@release.dir, "releases")
|
187
|
+
end
|
188
|
+
|
189
|
+
def dev_releases_dir
|
190
|
+
File.join(@release.dir, "dev_releases")
|
165
191
|
end
|
166
192
|
|
167
193
|
def tarball_path
|
@@ -191,8 +217,16 @@ module Bosh::Cli
|
|
191
217
|
end
|
192
218
|
|
193
219
|
def assign_version
|
194
|
-
|
195
|
-
|
220
|
+
latest_final_version = @final_index.latest_version.to_i
|
221
|
+
latest_dev_version = @dev_index.latest_version(latest_final_version)
|
222
|
+
|
223
|
+
if @final
|
224
|
+
latest_final_version + 1
|
225
|
+
else
|
226
|
+
major = latest_final_version
|
227
|
+
minor = minor_version(latest_dev_version).to_i + 1
|
228
|
+
"#{major}.#{minor}-dev"
|
229
|
+
end
|
196
230
|
end
|
197
231
|
|
198
232
|
def build_dir
|
data/lib/cli/runner.rb
CHANGED
@@ -25,43 +25,14 @@ module Bosh::Cli
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def initialize(args)
|
28
|
-
trap("SIGINT") {
|
29
|
-
handle_ctrl_c
|
30
|
-
}
|
31
28
|
define_commands
|
32
29
|
@args = args
|
33
30
|
@options = {
|
34
|
-
|
35
|
-
|
31
|
+
:director_checks => true,
|
32
|
+
:colorize => true,
|
36
33
|
}
|
37
34
|
end
|
38
35
|
|
39
|
-
##
|
40
|
-
# When user issues ctrl-c it asks if they really want to quit. If so
|
41
|
-
# then it will cancel the current running task if it exists.
|
42
|
-
def handle_ctrl_c
|
43
|
-
if !@runner.task_running?
|
44
|
-
exit(1)
|
45
|
-
elsif kill_current_task?
|
46
|
-
@runner.cancel_current_task
|
47
|
-
exit(1)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
##
|
52
|
-
# Asks user if they really want to quit and returns the boolean answer.
|
53
|
-
#
|
54
|
-
# @return [Boolean] Whether the user wants to quit or not.
|
55
|
-
def kill_current_task?
|
56
|
-
# Use say and stdin.gets instead of ask because of 2 bugs in Highline.
|
57
|
-
# The bug makes it so that if something else has called ask and was in
|
58
|
-
# the middle of waiting for a response then ctrl-c is issued and it
|
59
|
-
# calls ask again then highline will re-issue the first question again.
|
60
|
-
# If the input is a newline character then highline will choke.
|
61
|
-
say("\nAre you sure you'd like to cancel running tasks? [yN]")
|
62
|
-
$stdin.gets.chomp.downcase == "y"
|
63
|
-
end
|
64
|
-
|
65
36
|
def prepare
|
66
37
|
define_commands
|
67
38
|
define_plugin_commands
|
@@ -70,8 +41,10 @@ module Bosh::Cli
|
|
70
41
|
parse_options!
|
71
42
|
|
72
43
|
Config.interactive = !@options[:non_interactive]
|
73
|
-
Config.colorize
|
74
|
-
Config.output
|
44
|
+
Config.colorize = @options.delete(:colorize)
|
45
|
+
Config.output ||= STDOUT unless @options[:quiet]
|
46
|
+
Config.cache = Bosh::Cli::Cache.new(@options[:cache_dir] ||
|
47
|
+
Bosh::Cli::DEFAULT_CACHE_DIR)
|
75
48
|
end
|
76
49
|
|
77
50
|
def run
|
@@ -81,10 +54,10 @@ module Bosh::Cli
|
|
81
54
|
if @namespace && @action
|
82
55
|
ns_class_name = @namespace.to_s.gsub(/(?:_|^)(.)/) { $1.upcase }
|
83
56
|
klass = eval("Bosh::Cli::Command::#{ns_class_name}")
|
84
|
-
|
85
|
-
|
57
|
+
runner = klass.new(@options)
|
58
|
+
runner.usage = @usage
|
86
59
|
|
87
|
-
action_arity =
|
60
|
+
action_arity = runner.method(@action.to_sym).arity
|
88
61
|
n_required_args = action_arity >= 0 ? action_arity : -action_arity - 1
|
89
62
|
|
90
63
|
if n_required_args > @args.size
|
@@ -94,8 +67,8 @@ module Bosh::Cli
|
|
94
67
|
err("Too many arguments, correct usage is: bosh #{@usage}")
|
95
68
|
end
|
96
69
|
|
97
|
-
|
98
|
-
elsif @args.empty? || @args ==
|
70
|
+
runner.send(@action.to_sym, *@args)
|
71
|
+
elsif @args.empty? || @args == %w(help)
|
99
72
|
say(help_message)
|
100
73
|
say(plugin_help_message) if @plugins
|
101
74
|
elsif @args[0] == "help"
|
@@ -246,7 +219,7 @@ module Bosh::Cli
|
|
246
219
|
usage "scp <job> <--upload | --download> [options] " +
|
247
220
|
"/path/to/source /path/to/destination"
|
248
221
|
desc "upload/download the source file to the given job. " +
|
249
|
-
"Note: for
|
222
|
+
"Note: for download /path/to/destination is a directory"
|
250
223
|
option "--index <job_index>"
|
251
224
|
option "--public_key <file>"
|
252
225
|
option "--gateway_host <host>"
|
@@ -340,6 +313,15 @@ module Bosh::Cli
|
|
340
313
|
power_option "--force"
|
341
314
|
end
|
342
315
|
|
316
|
+
command :rename_job do
|
317
|
+
usage "rename <old_job_name> <new_job_name>"
|
318
|
+
desc "renames a job. NOTE, your deployment manifest must also be " +
|
319
|
+
"updated to reflect the new job name."
|
320
|
+
power_option "--force"
|
321
|
+
|
322
|
+
route :job_rename, :rename
|
323
|
+
end
|
324
|
+
|
343
325
|
command :fetch_logs do
|
344
326
|
usage "logs <job> <index>"
|
345
327
|
desc "Fetch job (default) or agent (if option provided) logs"
|