bosh_cli 0.19.6 → 1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/bosh +3 -0
- data/lib/cli.rb +15 -5
- data/lib/cli/{commands/base.rb → base_command.rb} +38 -44
- data/lib/cli/command_discovery.rb +40 -0
- data/lib/cli/command_handler.rb +135 -0
- data/lib/cli/commands/biff.rb +16 -12
- data/lib/cli/commands/blob_management.rb +10 -3
- data/lib/cli/commands/cloudcheck.rb +13 -11
- data/lib/cli/commands/complete.rb +29 -0
- data/lib/cli/commands/deployment.rb +137 -28
- data/lib/cli/commands/help.rb +96 -0
- data/lib/cli/commands/job.rb +4 -1
- data/lib/cli/commands/job_management.rb +36 -23
- data/lib/cli/commands/job_rename.rb +11 -12
- data/lib/cli/commands/log_management.rb +28 -32
- data/lib/cli/commands/maintenance.rb +6 -1
- data/lib/cli/commands/misc.rb +129 -87
- data/lib/cli/commands/package.rb +6 -65
- data/lib/cli/commands/property_management.rb +20 -8
- data/lib/cli/commands/release.rb +211 -206
- data/lib/cli/commands/ssh.rb +178 -188
- data/lib/cli/commands/stemcell.rb +114 -51
- data/lib/cli/commands/task.rb +74 -56
- data/lib/cli/commands/user.rb +6 -3
- data/lib/cli/commands/vms.rb +17 -15
- data/lib/cli/config.rb +27 -1
- data/lib/cli/core_ext.rb +27 -1
- data/lib/cli/deployment_helper.rb +47 -0
- data/lib/cli/director.rb +18 -9
- data/lib/cli/errors.rb +6 -0
- data/lib/cli/job_builder.rb +75 -23
- data/lib/cli/job_property_collection.rb +87 -0
- data/lib/cli/job_property_validator.rb +130 -0
- data/lib/cli/package_builder.rb +32 -5
- data/lib/cli/release.rb +2 -0
- data/lib/cli/release_builder.rb +9 -13
- data/lib/cli/release_compiler.rb +5 -34
- data/lib/cli/release_tarball.rb +4 -19
- data/lib/cli/runner.rb +118 -694
- data/lib/cli/version.rb +1 -1
- data/spec/assets/config/swift-hp/config/final.yml +6 -0
- data/spec/assets/config/swift-hp/config/private.yml +7 -0
- data/spec/assets/config/swift-rackspace/config/final.yml +6 -0
- data/spec/assets/config/swift-rackspace/config/private.yml +6 -0
- data/spec/spec_helper.rb +0 -5
- data/spec/unit/base_command_spec.rb +32 -37
- data/spec/unit/biff_spec.rb +11 -10
- data/spec/unit/cli_commands_spec.rb +96 -88
- data/spec/unit/core_ext_spec.rb +1 -1
- data/spec/unit/deployment_manifest_spec.rb +36 -0
- data/spec/unit/director_spec.rb +17 -3
- data/spec/unit/job_builder_spec.rb +2 -2
- data/spec/unit/job_property_collection_spec.rb +111 -0
- data/spec/unit/job_property_validator_spec.rb +7 -0
- data/spec/unit/job_rename_spec.rb +7 -6
- data/spec/unit/package_builder_spec.rb +2 -2
- data/spec/unit/release_builder_spec.rb +33 -0
- data/spec/unit/release_spec.rb +54 -0
- data/spec/unit/release_tarball_spec.rb +2 -7
- data/spec/unit/runner_spec.rb +1 -151
- data/spec/unit/ssh_spec.rb +15 -9
- metadata +41 -12
- data/lib/cli/command_definition.rb +0 -52
- data/lib/cli/templates/help_message.erb +0 -80
data/lib/cli/errors.rb
CHANGED
@@ -16,6 +16,8 @@ module Bosh::Cli
|
|
16
16
|
def self.exit_code(code = nil)
|
17
17
|
define_method(:exit_code) { code }
|
18
18
|
end
|
19
|
+
|
20
|
+
error_code(42)
|
19
21
|
end
|
20
22
|
|
21
23
|
class UnknownCommand < CliError; error_code(100); end
|
@@ -45,4 +47,8 @@ module Bosh::Cli
|
|
45
47
|
class UndefinedProperty < CliError; error_code(509); end
|
46
48
|
class MalformedManifest < CliError; error_code(511); end
|
47
49
|
class MissingTarget < CliError; error_code(512); end
|
50
|
+
class InvalidProperty < CliError; error_code(513); end
|
51
|
+
class InvalidManifest < CliError; error_code(514); end
|
52
|
+
class PropertyMismatch < CliError; error_code(515); end
|
53
|
+
class InvalidPropertyMapping < CliError; error_code(516); end
|
48
54
|
end
|
data/lib/cli/job_builder.rb
CHANGED
@@ -7,6 +7,9 @@ module Bosh::Cli
|
|
7
7
|
attr_reader :name, :version, :packages, :templates,
|
8
8
|
:release_dir, :built_packages, :tarball_path
|
9
9
|
|
10
|
+
# @return [Hash] Properties defined in this job
|
11
|
+
attr_reader :properties
|
12
|
+
|
10
13
|
def self.run_prepare_script(script_path)
|
11
14
|
unless File.exists?(script_path)
|
12
15
|
raise InvalidJob, "Prepare script at `#{script_path}' doesn't exist"
|
@@ -27,42 +30,88 @@ module Bosh::Cli
|
|
27
30
|
# with CLI itself
|
28
31
|
%w{ BUNDLE_GEMFILE RUBYOPT }.each { |key| ENV.delete(key) }
|
29
32
|
|
33
|
+
output = nil
|
30
34
|
Dir.chdir(script_dir) do
|
31
35
|
cmd = "./#{script_name} 2>&1"
|
32
|
-
|
33
|
-
script_output = `#{cmd}`
|
34
|
-
script_output.split("\n").each do |line|
|
35
|
-
say("> #{line}")
|
36
|
-
end
|
36
|
+
output = `#{cmd}`
|
37
37
|
end
|
38
38
|
|
39
39
|
unless $?.exitstatus == 0
|
40
40
|
raise InvalidJob, "`#{script_path}' script failed"
|
41
41
|
end
|
42
|
+
|
43
|
+
output
|
42
44
|
ensure
|
43
45
|
ENV.each_pair { |k, v| ENV[k] = old_env[k] }
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
49
|
+
# @param [String] directory Release directory
|
50
|
+
# @param [Hash] options Build options
|
51
|
+
def self.discover(directory, options = {})
|
52
|
+
builders = []
|
53
|
+
|
54
|
+
Dir[File.join(directory, "jobs", "*")].each do |job_dir|
|
55
|
+
next unless File.directory?(job_dir)
|
56
|
+
job_dirname = File.basename(job_dir)
|
57
|
+
|
58
|
+
prepare_script = File.join(job_dir, "prepare")
|
59
|
+
if File.exists?(prepare_script)
|
60
|
+
run_prepare_script(prepare_script)
|
61
|
+
end
|
62
|
+
|
63
|
+
job_spec = load_yaml_file(File.join(job_dir, "spec"))
|
64
|
+
if job_spec["name"] != job_dirname
|
65
|
+
raise InvalidJob,
|
66
|
+
"Found `#{job_spec["name"]}' job in " +
|
67
|
+
"`#{job_dirname}' directory, please fix it"
|
68
|
+
end
|
69
|
+
|
70
|
+
final = options[:final]
|
71
|
+
dry_run = options[:dry_run]
|
72
|
+
blobstore = options[:blobstore]
|
73
|
+
package_names = options[:package_names]
|
74
|
+
|
75
|
+
builder = new(job_spec, directory, final, blobstore, package_names)
|
76
|
+
builder.dry_run = true if dry_run
|
77
|
+
builders << builder
|
78
|
+
end
|
79
|
+
|
80
|
+
builders
|
81
|
+
end
|
82
|
+
|
47
83
|
def initialize(spec, release_dir, final, blobstore, built_packages = [])
|
48
84
|
spec = load_yaml_file(spec) if spec.is_a?(String) && File.file?(spec)
|
49
85
|
|
50
|
-
@name
|
51
|
-
@
|
86
|
+
@name = spec["name"]
|
87
|
+
@version = nil
|
88
|
+
@tarball_path = nil
|
89
|
+
@packages = spec["packages"].to_a
|
52
90
|
@built_packages = built_packages.to_a
|
53
|
-
@release_dir
|
54
|
-
@templates_dir
|
55
|
-
@tarballs_dir
|
56
|
-
@final
|
57
|
-
@blobstore
|
58
|
-
@artefact_type
|
91
|
+
@release_dir = release_dir
|
92
|
+
@templates_dir = File.join(job_dir, "templates")
|
93
|
+
@tarballs_dir = File.join(release_dir, "tmp", "jobs")
|
94
|
+
@final = final
|
95
|
+
@blobstore = blobstore
|
96
|
+
@artefact_type = "job"
|
59
97
|
|
60
98
|
case spec["templates"]
|
61
99
|
when Hash
|
62
100
|
@templates = spec["templates"].keys
|
63
101
|
else
|
64
102
|
raise InvalidJob, "Incorrect templates section in `#{@name}' " +
|
65
|
-
|
103
|
+
"job spec (Hash expected, #{spec["properties"].class} given)"
|
104
|
+
end
|
105
|
+
|
106
|
+
if spec.has_key?("properties")
|
107
|
+
if spec["properties"].is_a?(Hash)
|
108
|
+
@properties = spec["properties"]
|
109
|
+
else
|
110
|
+
raise InvalidJob, "Incorrect properties section in `#{@name}' " +
|
111
|
+
"job spec (Hash expected, #{spec["properties"].class} given)"
|
112
|
+
end
|
113
|
+
else
|
114
|
+
@properties = {}
|
66
115
|
end
|
67
116
|
|
68
117
|
if @name.blank?
|
@@ -109,8 +158,6 @@ module Bosh::Cli
|
|
109
158
|
FileUtils.mkdir_p(@dev_builds_dir)
|
110
159
|
FileUtils.mkdir_p(@final_builds_dir)
|
111
160
|
|
112
|
-
at_exit { FileUtils.rm_rf(build_dir) }
|
113
|
-
|
114
161
|
init_indices
|
115
162
|
end
|
116
163
|
|
@@ -139,7 +186,7 @@ module Bosh::Cli
|
|
139
186
|
end
|
140
187
|
|
141
188
|
def prepare_files
|
142
|
-
|
189
|
+
File.join(job_dir, "prepare")
|
143
190
|
end
|
144
191
|
|
145
192
|
def build_dir
|
@@ -176,17 +223,22 @@ module Bosh::Cli
|
|
176
223
|
self
|
177
224
|
end
|
178
225
|
|
226
|
+
# @return [Array<String>] Returns full paths of all templates in the job
|
227
|
+
# (regular job templates and monit)
|
228
|
+
def all_templates
|
229
|
+
regular_templates = @templates.map do |template|
|
230
|
+
File.join(@templates_dir, template)
|
231
|
+
end
|
232
|
+
|
233
|
+
regular_templates.sort + monit_files
|
234
|
+
end
|
235
|
+
|
179
236
|
private
|
180
237
|
|
181
238
|
def make_fingerprint
|
182
239
|
contents = ""
|
183
240
|
|
184
|
-
|
185
|
-
files = templates.map do |template|
|
186
|
-
File.join(@templates_dir, template)
|
187
|
-
end.sort
|
188
|
-
|
189
|
-
files += monit_files
|
241
|
+
files = all_templates
|
190
242
|
files << File.join(job_dir, "spec")
|
191
243
|
|
192
244
|
files.each do |filename|
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::Cli
|
4
|
+
class JobPropertyCollection
|
5
|
+
include Enumerable
|
6
|
+
include Bosh::Common::PropertyHelper
|
7
|
+
|
8
|
+
# @param [JobBuilder] job_builder Which job this property collection is for
|
9
|
+
# @param [Hash] global_properties Globally defined properties
|
10
|
+
# @param [Hash] job_properties Properties defined for this job only
|
11
|
+
# @param [Hash] mappings Property mappings for this job
|
12
|
+
def initialize(job_builder, global_properties, job_properties, mappings)
|
13
|
+
@job_builder = job_builder
|
14
|
+
|
15
|
+
@job_properties = deep_copy(job_properties || {})
|
16
|
+
merge(@job_properties, deep_copy(global_properties))
|
17
|
+
|
18
|
+
@mappings = mappings || {}
|
19
|
+
@properties = []
|
20
|
+
|
21
|
+
resolve_mappings
|
22
|
+
filter_properties
|
23
|
+
end
|
24
|
+
|
25
|
+
def each
|
26
|
+
@properties.each { |property| yield property }
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Hash] Property hash (keys are property name components)
|
30
|
+
def to_hash
|
31
|
+
@properties
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def resolve_mappings
|
37
|
+
@mappings.each_pair do |to, from|
|
38
|
+
resolved = lookup_property(@job_properties, from)
|
39
|
+
|
40
|
+
if resolved.nil?
|
41
|
+
raise InvalidPropertyMapping,
|
42
|
+
"Cannot satisfy property mapping `#{to}: #{from}', " +
|
43
|
+
"as `#{from}' is not in deployment properties"
|
44
|
+
end
|
45
|
+
|
46
|
+
@job_properties[to] = resolved
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [void] Modifies @properties
|
51
|
+
def filter_properties
|
52
|
+
if @job_builder.properties.empty?
|
53
|
+
# If at least one template doesn't have properties defined, we
|
54
|
+
# need all properties to be available to job (backward-compatibility)
|
55
|
+
@properties = @job_properties
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
@properties = {}
|
60
|
+
|
61
|
+
@job_builder.properties.each_pair do |name, definition|
|
62
|
+
copy_property(
|
63
|
+
@properties, @job_properties, name, definition["default"])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param [Object] object Serializable object
|
68
|
+
# @return [Object] Deep copy of the object
|
69
|
+
def deep_copy(object)
|
70
|
+
Marshal.load(Marshal.dump(object))
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param [Hash] base
|
74
|
+
# @param [Hash] extras
|
75
|
+
# @return [void] Modifies base
|
76
|
+
def merge(base, extras)
|
77
|
+
base.merge!(extras) do |_, old_value, new_value|
|
78
|
+
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
79
|
+
merge(old_value, new_value)
|
80
|
+
end
|
81
|
+
|
82
|
+
old_value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::Cli
|
4
|
+
class JobPropertyValidator
|
5
|
+
# TODO: tests
|
6
|
+
|
7
|
+
attr_reader :template_errors
|
8
|
+
attr_reader :jobs_without_properties
|
9
|
+
|
10
|
+
# @param [Array<JobBuilder>] built_jobs Built job templates
|
11
|
+
# @param [Hash] manifest Deployment manifest
|
12
|
+
def initialize(built_jobs, manifest)
|
13
|
+
@built_jobs = {}
|
14
|
+
@manifest = manifest
|
15
|
+
|
16
|
+
@jobs_without_properties = []
|
17
|
+
|
18
|
+
built_jobs.each do |job|
|
19
|
+
@built_jobs[job.name] = job
|
20
|
+
if job.properties.empty?
|
21
|
+
@jobs_without_properties << job
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
unless @manifest["properties"].is_a?(Hash)
|
26
|
+
bad_manifest("Invalid properties format in deployment " +
|
27
|
+
"manifest, Hash expected, #{@manifest["properties"].class} given")
|
28
|
+
end
|
29
|
+
|
30
|
+
unless @manifest["jobs"].is_a?(Array)
|
31
|
+
bad_manifest("Invalid jobs format in deployment " +
|
32
|
+
"manifest, Array expected, #{@manifest["jobs"].class} given")
|
33
|
+
end
|
34
|
+
|
35
|
+
@manifest["jobs"].each do |job|
|
36
|
+
unless job.is_a?(Hash)
|
37
|
+
bad_manifest("Invalid job spec in the manifest " +
|
38
|
+
"Hash expected, #{job.class} given")
|
39
|
+
end
|
40
|
+
|
41
|
+
job_name = job["name"]
|
42
|
+
if job_name.nil?
|
43
|
+
bad_manifest("Manifest contains at least one job without name")
|
44
|
+
end
|
45
|
+
|
46
|
+
if job["template"].nil?
|
47
|
+
bad_manifest("Job `#{job_name}' doesn't have a template")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@template_errors = []
|
52
|
+
# TODO: track missing props and show the list to user (super helpful!)
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate
|
56
|
+
@manifest["jobs"].each do |job_spec|
|
57
|
+
validate_templates(job_spec)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Tries to fill in each job template with job properties, collects errors
|
62
|
+
# @param [Hash] job_spec Job spec from the manifest
|
63
|
+
def validate_templates(job_spec)
|
64
|
+
built_job = @built_jobs[job_spec["template"]]
|
65
|
+
|
66
|
+
if built_job.nil?
|
67
|
+
raise CliError, "Job `#{job_spec["template"]}' has not been built"
|
68
|
+
end
|
69
|
+
|
70
|
+
collection = JobPropertyCollection.new(
|
71
|
+
built_job, @manifest["properties"],
|
72
|
+
job_spec["properties"], job_spec["property_mappings"])
|
73
|
+
|
74
|
+
# Spec is usually more than that but jobs rarely use anything but
|
75
|
+
# networks and properties.
|
76
|
+
# TODO: provide all keys in the spec?
|
77
|
+
spec = {
|
78
|
+
"job" => {
|
79
|
+
"name" => job_spec["name"]
|
80
|
+
},
|
81
|
+
"networks" => {
|
82
|
+
"default" => {"ip" => "10.0.0.1"}
|
83
|
+
},
|
84
|
+
"properties" => collection.to_hash,
|
85
|
+
"index" => 0
|
86
|
+
}
|
87
|
+
|
88
|
+
built_job.all_templates.each do |template_path|
|
89
|
+
# TODO: add progress bar?
|
90
|
+
evaluate_template(built_job, template_path, spec)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# @param [JobBuilder] job Job builder
|
97
|
+
# @param [String] template_path Template path
|
98
|
+
# @param [Hash] spec Fake instance spec
|
99
|
+
def evaluate_template(job, template_path, spec)
|
100
|
+
erb = ERB.new(File.read(template_path))
|
101
|
+
context = Bosh::Common::TemplateEvaluationContext.new(spec)
|
102
|
+
begin
|
103
|
+
erb.result(context.get_binding)
|
104
|
+
rescue Exception => e
|
105
|
+
@template_errors << TemplateError.new(job, template_path, e)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def bad_manifest(message)
|
110
|
+
raise InvalidManifest, message
|
111
|
+
end
|
112
|
+
|
113
|
+
class TemplateError
|
114
|
+
attr_reader :job
|
115
|
+
attr_reader :template_path
|
116
|
+
attr_reader :exception
|
117
|
+
attr_reader :line
|
118
|
+
|
119
|
+
# @param [JobBuilder] job
|
120
|
+
# @param [String] template_path
|
121
|
+
# @param [Exception] exception
|
122
|
+
def initialize(job, template_path, exception)
|
123
|
+
@job = job
|
124
|
+
@template_path = template_path
|
125
|
+
@exception = exception
|
126
|
+
@line = exception.backtrace.first.split(":")[1]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/cli/package_builder.rb
CHANGED
@@ -43,6 +43,35 @@ module Bosh::Cli
|
|
43
43
|
# final build tarballs should be ignored as well
|
44
44
|
# final builds metadata should be checked in
|
45
45
|
|
46
|
+
# @param [String] directory Release directory
|
47
|
+
# @param [Hash] options Package build options
|
48
|
+
def self.discover(directory, options = {})
|
49
|
+
builders = []
|
50
|
+
|
51
|
+
Dir[File.join(directory, "packages", "*")].each do |package_dir|
|
52
|
+
next unless File.directory?(package_dir)
|
53
|
+
package_dirname = File.basename(package_dir)
|
54
|
+
package_spec = load_yaml_file(File.join(package_dir, "spec"))
|
55
|
+
|
56
|
+
if package_spec["name"] != package_dirname
|
57
|
+
raise InvalidPackage,
|
58
|
+
"Found `#{package_spec["name"]}' package in " +
|
59
|
+
"`#{package_dirname}' directory, please fix it"
|
60
|
+
end
|
61
|
+
|
62
|
+
is_final = options[:final]
|
63
|
+
blobstore = options[:blobstore]
|
64
|
+
dry_run = options[:dry_run]
|
65
|
+
|
66
|
+
builder = new(package_spec, directory, is_final, blobstore)
|
67
|
+
builder.dry_run = true if dry_run
|
68
|
+
|
69
|
+
builders << builder
|
70
|
+
end
|
71
|
+
|
72
|
+
builders
|
73
|
+
end
|
74
|
+
|
46
75
|
def initialize(spec, release_dir, final, blobstore,
|
47
76
|
sources_dir = nil, blobs_dir = nil, alt_src_dir = nil)
|
48
77
|
spec = load_yaml_file(spec) if spec.is_a?(String) && File.file?(spec)
|
@@ -87,8 +116,6 @@ module Bosh::Cli
|
|
87
116
|
FileUtils.mkdir_p(@dev_builds_dir)
|
88
117
|
FileUtils.mkdir_p(@final_builds_dir)
|
89
118
|
|
90
|
-
at_exit { FileUtils.rm_rf(build_dir) }
|
91
|
-
|
92
119
|
init_indices
|
93
120
|
end
|
94
121
|
|
@@ -166,10 +193,10 @@ module Bosh::Cli
|
|
166
193
|
ENV["RELEASE_DIR"] = @release_dir
|
167
194
|
in_build_dir do
|
168
195
|
pre_packaging_out = `bash -x pre_packaging 2>&1`
|
169
|
-
pre_packaging_out.split("\n").each do |line|
|
170
|
-
say("> #{line}")
|
171
|
-
end
|
172
196
|
unless $?.exitstatus == 0
|
197
|
+
pre_packaging_out.split("\n").each do |line|
|
198
|
+
say("> #{line}")
|
199
|
+
end
|
173
200
|
raise InvalidPackage, "`#{name}' pre-packaging failed"
|
174
201
|
end
|
175
202
|
end
|