bosh_cli 0.16

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.
Files changed (113) hide show
  1. data/README +4 -0
  2. data/Rakefile +55 -0
  3. data/bin/bosh +17 -0
  4. data/lib/cli.rb +76 -0
  5. data/lib/cli/cache.rb +44 -0
  6. data/lib/cli/changeset_helper.rb +142 -0
  7. data/lib/cli/command_definition.rb +52 -0
  8. data/lib/cli/commands/base.rb +245 -0
  9. data/lib/cli/commands/biff.rb +300 -0
  10. data/lib/cli/commands/blob.rb +125 -0
  11. data/lib/cli/commands/cloudcheck.rb +169 -0
  12. data/lib/cli/commands/deployment.rb +147 -0
  13. data/lib/cli/commands/job.rb +42 -0
  14. data/lib/cli/commands/job_management.rb +117 -0
  15. data/lib/cli/commands/log_management.rb +81 -0
  16. data/lib/cli/commands/maintenance.rb +131 -0
  17. data/lib/cli/commands/misc.rb +240 -0
  18. data/lib/cli/commands/package.rb +112 -0
  19. data/lib/cli/commands/property_management.rb +125 -0
  20. data/lib/cli/commands/release.rb +469 -0
  21. data/lib/cli/commands/ssh.rb +271 -0
  22. data/lib/cli/commands/stemcell.rb +184 -0
  23. data/lib/cli/commands/task.rb +213 -0
  24. data/lib/cli/commands/user.rb +28 -0
  25. data/lib/cli/commands/vms.rb +53 -0
  26. data/lib/cli/config.rb +154 -0
  27. data/lib/cli/core_ext.rb +145 -0
  28. data/lib/cli/dependency_helper.rb +62 -0
  29. data/lib/cli/deployment_helper.rb +263 -0
  30. data/lib/cli/deployment_manifest_compiler.rb +28 -0
  31. data/lib/cli/director.rb +633 -0
  32. data/lib/cli/director_task.rb +64 -0
  33. data/lib/cli/errors.rb +48 -0
  34. data/lib/cli/event_log_renderer.rb +351 -0
  35. data/lib/cli/job_builder.rb +226 -0
  36. data/lib/cli/package_builder.rb +254 -0
  37. data/lib/cli/packaging_helper.rb +248 -0
  38. data/lib/cli/release.rb +176 -0
  39. data/lib/cli/release_builder.rb +215 -0
  40. data/lib/cli/release_compiler.rb +178 -0
  41. data/lib/cli/release_tarball.rb +272 -0
  42. data/lib/cli/runner.rb +771 -0
  43. data/lib/cli/stemcell.rb +83 -0
  44. data/lib/cli/task_log_renderer.rb +40 -0
  45. data/lib/cli/templates/help_message.erb +75 -0
  46. data/lib/cli/validation.rb +42 -0
  47. data/lib/cli/version.rb +7 -0
  48. data/lib/cli/version_calc.rb +48 -0
  49. data/lib/cli/versions_index.rb +126 -0
  50. data/lib/cli/yaml_helper.rb +62 -0
  51. data/spec/assets/biff/bad_gateway_config.yml +28 -0
  52. data/spec/assets/biff/good_simple_config.yml +63 -0
  53. data/spec/assets/biff/good_simple_golden_config.yml +63 -0
  54. data/spec/assets/biff/good_simple_template.erb +69 -0
  55. data/spec/assets/biff/multiple_subnets_config.yml +40 -0
  56. data/spec/assets/biff/network_only_template.erb +34 -0
  57. data/spec/assets/biff/no_cc_config.yml +27 -0
  58. data/spec/assets/biff/no_range_config.yml +27 -0
  59. data/spec/assets/biff/no_subnet_config.yml +16 -0
  60. data/spec/assets/biff/ok_network_config.yml +30 -0
  61. data/spec/assets/biff/properties_template.erb +6 -0
  62. data/spec/assets/deployment.MF +0 -0
  63. data/spec/assets/plugins/bosh/cli/commands/echo.rb +43 -0
  64. data/spec/assets/plugins/bosh/cli/commands/ruby.rb +24 -0
  65. data/spec/assets/release/jobs/cacher.tgz +0 -0
  66. data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
  67. data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
  68. data/spec/assets/release/jobs/cacher/job.MF +6 -0
  69. data/spec/assets/release/jobs/cacher/monit +1 -0
  70. data/spec/assets/release/jobs/cleaner.tgz +0 -0
  71. data/spec/assets/release/jobs/cleaner/job.MF +4 -0
  72. data/spec/assets/release/jobs/cleaner/monit +1 -0
  73. data/spec/assets/release/jobs/sweeper.tgz +0 -0
  74. data/spec/assets/release/jobs/sweeper/config/test.conf +1 -0
  75. data/spec/assets/release/jobs/sweeper/job.MF +5 -0
  76. data/spec/assets/release/jobs/sweeper/monit +1 -0
  77. data/spec/assets/release/packages/mutator.tar.gz +0 -0
  78. data/spec/assets/release/packages/stuff.tgz +0 -0
  79. data/spec/assets/release/release.MF +17 -0
  80. data/spec/assets/release_invalid_checksum.tgz +0 -0
  81. data/spec/assets/release_invalid_jobs.tgz +0 -0
  82. data/spec/assets/release_no_name.tgz +0 -0
  83. data/spec/assets/release_no_version.tgz +0 -0
  84. data/spec/assets/stemcell/image +1 -0
  85. data/spec/assets/stemcell/stemcell.MF +6 -0
  86. data/spec/assets/stemcell_invalid_mf.tgz +0 -0
  87. data/spec/assets/stemcell_no_image.tgz +0 -0
  88. data/spec/assets/valid_release.tgz +0 -0
  89. data/spec/assets/valid_stemcell.tgz +0 -0
  90. data/spec/spec_helper.rb +25 -0
  91. data/spec/unit/base_command_spec.rb +66 -0
  92. data/spec/unit/biff_spec.rb +135 -0
  93. data/spec/unit/cache_spec.rb +36 -0
  94. data/spec/unit/cli_commands_spec.rb +481 -0
  95. data/spec/unit/config_spec.rb +139 -0
  96. data/spec/unit/core_ext_spec.rb +77 -0
  97. data/spec/unit/dependency_helper_spec.rb +52 -0
  98. data/spec/unit/deployment_manifest_compiler_spec.rb +63 -0
  99. data/spec/unit/director_spec.rb +511 -0
  100. data/spec/unit/director_task_spec.rb +48 -0
  101. data/spec/unit/event_log_renderer_spec.rb +171 -0
  102. data/spec/unit/hash_changeset_spec.rb +73 -0
  103. data/spec/unit/job_builder_spec.rb +454 -0
  104. data/spec/unit/package_builder_spec.rb +567 -0
  105. data/spec/unit/release_builder_spec.rb +65 -0
  106. data/spec/unit/release_spec.rb +66 -0
  107. data/spec/unit/release_tarball_spec.rb +33 -0
  108. data/spec/unit/runner_spec.rb +140 -0
  109. data/spec/unit/ssh_spec.rb +78 -0
  110. data/spec/unit/stemcell_spec.rb +17 -0
  111. data/spec/unit/version_calc_spec.rb +27 -0
  112. data/spec/unit/versions_index_spec.rb +132 -0
  113. metadata +338 -0
@@ -0,0 +1,139 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require "spec_helper"
4
+
5
+ describe Bosh::Cli::Config do
6
+ before :each do
7
+ @config = File.join(Dir.mktmpdir, "bosh_config")
8
+ @cache_dir = Dir.mktmpdir
9
+ end
10
+
11
+ def add_config(object)
12
+ File.open(@config, "w") do |f|
13
+ f.write(YAML.dump(object))
14
+ end
15
+ end
16
+
17
+ def create_config
18
+ Bosh::Cli::Config.new(@config)
19
+ end
20
+
21
+ def logged_in?(cfg)
22
+ cfg.username && cfg.password
23
+ end
24
+
25
+ it "should convert old deployment configs to the new config " +
26
+ "when set_deployment is called" do
27
+ add_config("target" => "localhost:8080", "deployment" => "test")
28
+
29
+ cfg = create_config
30
+ yaml_file = load_yaml_file(@config, nil)
31
+ yaml_file["deployment"].should == "test"
32
+ cfg.set_deployment("test2")
33
+ cfg.save
34
+ yaml_file = load_yaml_file(@config, nil)
35
+ yaml_file["deployment"].has_key?("localhost:8080").should be_true
36
+ yaml_file["deployment"]["localhost:8080"].should == "test2"
37
+ end
38
+
39
+ it "should convert old deployment configs to the new config " +
40
+ "when deployment is called" do
41
+ add_config("target" => "localhost:8080", "deployment" => "test")
42
+
43
+ cfg = create_config
44
+ yaml_file = load_yaml_file(@config, nil)
45
+ yaml_file["deployment"].should == "test"
46
+ cfg.deployment.should == "test"
47
+ yaml_file = load_yaml_file(@config, nil)
48
+ yaml_file["deployment"].has_key?("localhost:8080").should be_true
49
+ yaml_file["deployment"]["localhost:8080"].should == "test"
50
+ end
51
+
52
+ it "should save a deployment for each target" do
53
+ add_config({})
54
+ cfg = create_config
55
+ cfg.target = "localhost:1"
56
+ cfg.set_deployment("/path/to/deploy/1")
57
+ cfg.save
58
+ cfg.target = "localhost:2"
59
+ cfg.set_deployment("/path/to/deploy/2")
60
+ cfg.save
61
+
62
+ # Test that the file is written correctly.
63
+ yaml_file = load_yaml_file(@config, nil)
64
+ yaml_file["deployment"].has_key?("localhost:1").should be_true
65
+ yaml_file["deployment"].has_key?("localhost:2").should be_true
66
+ yaml_file["deployment"]["localhost:1"].should == "/path/to/deploy/1"
67
+ yaml_file["deployment"]["localhost:2"].should == "/path/to/deploy/2"
68
+
69
+ # Test that switching targets gives you the new deployment.
70
+ cfg.deployment.should == "/path/to/deploy/2"
71
+ cfg.target = "localhost:1"
72
+ cfg.deployment.should == "/path/to/deploy/1"
73
+ end
74
+
75
+ it "returns nil when the deployments key exists but has no value" do
76
+ add_config("target" => "localhost:8080", "deployment" => nil)
77
+
78
+ cfg = create_config
79
+ yaml_file = load_yaml_file(@config, nil)
80
+ yaml_file["deployment"].should == nil
81
+ cfg.deployment.should == nil
82
+ end
83
+
84
+ it "should throw MissingTarget when getting deployment without target set" do
85
+ add_config({})
86
+ cfg = create_config
87
+ expect { cfg.set_deployment("/path/to/deploy/1") }.
88
+ to raise_error(Bosh::Cli::MissingTarget)
89
+ end
90
+
91
+ it "whines on missing config file" do
92
+ lambda {
93
+ File.should_receive(:open).with(@config, "w").and_raise(Errno::EACCES)
94
+ create_config
95
+ }.should raise_error(Bosh::Cli::ConfigError)
96
+ end
97
+
98
+
99
+ it "effectively ignores config file if it is malformed" do
100
+ add_config([1, 2, 3])
101
+ cfg = create_config
102
+
103
+ cfg.target.should == nil
104
+ end
105
+
106
+ it "fetches auth information from the config file" do
107
+ config = {
108
+ "target" => "localhost:8080",
109
+ "deployment" => "test",
110
+ "auth" => {
111
+ "localhost:8080" => { "username" => "a", "password" => "b" },
112
+ "localhost:8081" => { "username" => "c", "password" => "d" }
113
+ }
114
+ }
115
+
116
+ add_config(config)
117
+ cfg = create_config
118
+
119
+ logged_in?(cfg).should be_true
120
+ cfg.username.should == "a"
121
+ cfg.password.should == "b"
122
+
123
+ config["target"] = "localhost:8081"
124
+ add_config(config)
125
+
126
+ cfg = create_config
127
+ logged_in?(cfg).should be_true
128
+ cfg.username.should == "c"
129
+ cfg.password.should == "d"
130
+
131
+ config["target"] = "localhost:8082"
132
+ add_config(config)
133
+ cfg = create_config
134
+ logged_in?(cfg).should be_false
135
+ cfg.username.should be_nil
136
+ cfg.password.should be_nil
137
+ end
138
+
139
+ end
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require "spec_helper"
4
+
5
+ describe String do
6
+
7
+ it "can tell valid bosh identifiers from invalid" do
8
+ %w(ruby ruby-1.8.7 mysql-2.3.5-alpha Apache_2.3).each do |id|
9
+ id.bosh_valid_id?.should be_true
10
+ end
11
+
12
+ ["ruby 1.8", "ruby-1.8@b29", "#!@", "db/2", "ruby(1.8)"].each do |id|
13
+ id.bosh_valid_id?.should be_false
14
+ end
15
+ end
16
+
17
+ it "can tell blank string from non-blank" do
18
+ [" ", "\t\t", "\n", ""].each do |string|
19
+ string.should be_blank
20
+ end
21
+
22
+ ["a", " a", "a ", " a ", "___", "z\tb"].each do |string|
23
+ string.should_not be_blank
24
+ end
25
+ end
26
+
27
+ it "has colorization helpers" do
28
+ Bosh::Cli::Config.colorize = false
29
+ "string".red.should == "string"
30
+ "string".green.should == "string"
31
+ "string".colorize("a").should == "string"
32
+ "string".colorize(:green).should == "string"
33
+
34
+ Bosh::Cli::Config.colorize = true
35
+ "string".red.should == "\e[0m\e[31mstring\e[0m"
36
+ "string".green.should == "\e[0m\e[32mstring\e[0m"
37
+ "string".colorize("a").should == "string"
38
+ "string".colorize(:green).should == "\e[0m\e[32mstring\e[0m"
39
+ end
40
+ end
41
+
42
+ describe Object do
43
+
44
+ it "has output helpers" do
45
+ s = StringIO.new
46
+ Bosh::Cli::Config.output = s
47
+ say("yea")
48
+ say("yea")
49
+ s.rewind
50
+ s.read.should == "yea\nyea\n"
51
+
52
+ s.rewind
53
+ header("test")
54
+ s.rewind
55
+ s.read.should == "\ntest\n----\n"
56
+
57
+ s.rewind
58
+ header("test", "a")
59
+ s.rewind
60
+ s.read.should == "\ntest\naaaa\n"
61
+ end
62
+
63
+ it "raises a special exception to signal a premature exit" do
64
+ lambda {
65
+ err("Done")
66
+ }.should raise_error(Bosh::Cli::CliExit, "Done")
67
+ end
68
+
69
+ it "can tell if object is blank" do
70
+ o = Object.new
71
+ o.stub!(:to_s).and_return(" ")
72
+ o.should be_blank
73
+ o.stub!(:to_s).and_return("Object 1")
74
+ o.should_not be_blank
75
+ end
76
+
77
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require "spec_helper"
4
+
5
+ describe Bosh::Cli::DependencyHelper do
6
+
7
+ def sorter
8
+ object = Object.new
9
+ class << object
10
+ include Bosh::Cli::DependencyHelper
11
+ end
12
+ object
13
+ end
14
+
15
+ def tsort_packages(*args)
16
+ sorter.tsort_packages(*args)
17
+ end
18
+
19
+ def partial_order_sort(*args)
20
+ sorter.partial_order_sort(*args)
21
+ end
22
+
23
+ it "resolves sorts simple dependencies" do
24
+ tsort_packages("A" => ["B"], "B" => ["C"], "C" => []).
25
+ should == ["C", "B", "A"]
26
+ end
27
+
28
+ it "whines on missing dependencies" do
29
+ lambda {
30
+ tsort_packages("A" => ["B"], "C" => ["D"])
31
+ }.should raise_error Bosh::Cli::MissingDependency,
32
+ "Package 'A' depends on missing package 'B'"
33
+ end
34
+
35
+ it "whines on circular dependencies" do
36
+ lambda {
37
+ tsort_packages("foo" => ["bar"], "bar" => ["baz"], "baz" => ["foo"])
38
+ }.should raise_error(Bosh::Cli::CircularDependency,
39
+ "Cannot resolve dependencies for 'bar': " +
40
+ "circular dependency with 'foo'")
41
+ end
42
+
43
+ it "can resolve nested dependencies" do
44
+ sorted = tsort_packages("A" => ["B", "C"], "B" => ["C", "D"],
45
+ "C" => ["D"], "D" => [], "E" => [])
46
+ sorted.index("B").should <= sorted.index("A")
47
+ sorted.index("C").should <= sorted.index("A")
48
+ sorted.index("D").should <= sorted.index("B")
49
+ sorted.index("D").should <= sorted.index("C")
50
+ end
51
+
52
+ end
@@ -0,0 +1,63 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require "spec_helper"
4
+
5
+ describe Bosh::Cli::DeploymentManifestCompiler do
6
+
7
+ def make_compiler(manifest, properties = {})
8
+ compiler = Bosh::Cli::DeploymentManifestCompiler.new(manifest)
9
+ compiler.properties = properties
10
+ compiler
11
+ end
12
+
13
+ it "substitutes properties in a raw manifest" do
14
+ raw_manifest = <<-MANIFEST.gsub(/^\s*/, "")
15
+ ---
16
+ name: mycloud
17
+ properties:
18
+ dea:
19
+ max_memory: <%= property("dea.max_memory") %>
20
+ MANIFEST
21
+
22
+ compiler = make_compiler(raw_manifest, { "dea.max_memory" => 8192 })
23
+
24
+ compiler.result.should == <<-MANIFEST.gsub(/^\s*/, "")
25
+ ---
26
+ name: mycloud
27
+ properties:
28
+ dea:
29
+ max_memory: 8192
30
+ MANIFEST
31
+ end
32
+
33
+ it "whines on missing deployment properties" do
34
+ raw_manifest = <<-MANIFEST.gsub(/^\s*/, "")
35
+ ---
36
+ name: mycloud
37
+ properties:
38
+ dea:
39
+ max_memory: <%= property("missing.property") %>
40
+ MANIFEST
41
+
42
+ compiler = make_compiler(raw_manifest, { "dea.max_memory" => 8192 })
43
+ error_msg = "Cannot resolve deployment property `missing.property'"
44
+
45
+ lambda {
46
+ compiler.result
47
+ }.should raise_error(Bosh::Cli::UndefinedProperty, error_msg)
48
+ end
49
+
50
+ it "whines if manifest has syntax error (from ERB's point of view)" do
51
+ raw_manifest = <<-MANIFEST.gsub(/^\s*/, "")
52
+ properties: <%=
53
+ dea:
54
+ max_memory: <%= property("missing.property") %>
55
+ MANIFEST
56
+
57
+ compiler = make_compiler(raw_manifest, { "dea.max_memory" => 8192 })
58
+
59
+ lambda {
60
+ compiler.result
61
+ }.should raise_error(Bosh::Cli::MalformedManifest)
62
+ end
63
+ end
@@ -0,0 +1,511 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require "spec_helper"
4
+
5
+ describe Bosh::Cli::Director do
6
+
7
+ DUMMY_TARGET = "http://target"
8
+
9
+ before do
10
+ @director = Bosh::Cli::Director.new(DUMMY_TARGET, "user", "pass")
11
+ end
12
+
13
+ describe "fetching status" do
14
+ it "tells if user is authenticated" do
15
+ @director.should_receive(:get).with("/info", "application/json").
16
+ and_return([200, JSON.generate("user" => "adam")])
17
+ @director.authenticated?.should == true
18
+ end
19
+
20
+ it "tells if user not authenticated" do
21
+ @director.should_receive(:get).with("/info", "application/json").
22
+ and_return([403, "Forbidden"])
23
+ @director.authenticated?.should == false
24
+
25
+ @director.should_receive(:get).with("/info", "application/json").
26
+ and_return([500, "Error"])
27
+ @director.authenticated?.should == false
28
+
29
+ @director.should_receive(:get).with("/info", "application/json").
30
+ and_return([404, "Not Found"])
31
+ @director.authenticated?.should == false
32
+
33
+ @director.should_receive(:get).with("/info", "application/json").
34
+ and_return([200, JSON.generate("user" => nil, "version" => 1)])
35
+ @director.authenticated?.should == false
36
+
37
+ # Backward compatibility
38
+ @director.should_receive(:get).with("/info", "application/json").
39
+ and_return([200, JSON.generate("status" => "ZB")])
40
+ @director.authenticated?.should == true
41
+ end
42
+ end
43
+
44
+ describe "interface REST API" do
45
+ it "has helper methods for HTTP verbs which delegate to generic request" do
46
+ [:get, :put, :post, :delete].each do |verb|
47
+ @director.should_receive(:request).with(verb, :arg1, :arg2)
48
+ @director.send(verb, :arg1, :arg2)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "API calls" do
54
+ it "creates user" do
55
+ @director.should_receive(:post).
56
+ with("/users", "application/json",
57
+ JSON.generate("username" => "joe", "password" => "pass")).
58
+ and_return(true)
59
+ @director.create_user("joe", "pass")
60
+ end
61
+
62
+ it "uploads stemcell" do
63
+ @director.should_receive(:upload_and_track).
64
+ with("/stemcells", "application/x-compressed",
65
+ "/path", :log_type=>"event").and_return(true)
66
+ @director.upload_stemcell("/path")
67
+ end
68
+
69
+ it "lists stemcells" do
70
+ @director.should_receive(:get).with("/stemcells", "application/json").
71
+ and_return([200, JSON.generate([]), {}])
72
+ @director.list_stemcells
73
+ end
74
+
75
+ it "lists releases" do
76
+ @director.should_receive(:get).with("/releases", "application/json").
77
+ and_return([200, JSON.generate([]), {}])
78
+ @director.list_releases
79
+ end
80
+
81
+ it "lists deployments" do
82
+ @director.should_receive(:get).with("/deployments", "application/json").
83
+ and_return([200, JSON.generate([]), {}])
84
+ @director.list_deployments
85
+ end
86
+
87
+ it "lists currently running tasks (director version < 0.3.5)" do
88
+ @director.should_receive(:get).with("/info", "application/json").
89
+ and_return([200, JSON.generate({ :version => "0.3.2"})])
90
+ @director.should_receive(:get).
91
+ with("/tasks?state=processing", "application/json").
92
+ and_return([200, JSON.generate([]), {}])
93
+ @director.list_running_tasks
94
+ end
95
+
96
+ it "lists currently running tasks (director version >= 0.3.5)" do
97
+ @director.should_receive(:get).
98
+ with("/info", "application/json").
99
+ and_return([200, JSON.generate({ :version => "0.3.5"})])
100
+ @director.should_receive(:get).
101
+ with("/tasks?state=processing,cancelling,queued", "application/json").
102
+ and_return([200, JSON.generate([]), {}])
103
+ @director.list_running_tasks
104
+ end
105
+
106
+ it "lists recent tasks" do
107
+ @director.should_receive(:get).
108
+ with("/tasks?limit=30", "application/json").
109
+ and_return([200, JSON.generate([]), {}])
110
+ @director.list_recent_tasks
111
+
112
+ @director.should_receive(:get).
113
+ with("/tasks?limit=100", "application/json").
114
+ and_return([200, JSON.generate([]), {}])
115
+ @director.list_recent_tasks(100000)
116
+ end
117
+
118
+ it "uploads release" do
119
+ @director.should_receive(:upload_and_track).
120
+ with("/releases", "application/x-compressed",
121
+ "/path", :log_type => "event").
122
+ and_return(true)
123
+ @director.upload_release("/path")
124
+ end
125
+
126
+ it "gets release info" do
127
+ @director.should_receive(:get).
128
+ with("/releases/foo", "application/json").
129
+ and_return([200, JSON.generate([]), { }])
130
+ @director.get_release("foo")
131
+ end
132
+
133
+ it "gets deployment info" do
134
+ @director.should_receive(:get).
135
+ with("/deployments/foo", "application/json").
136
+ and_return([200, JSON.generate([]), { }])
137
+ @director.get_deployment("foo")
138
+ end
139
+
140
+ it "deletes stemcell" do
141
+ @director.should_receive(:request_and_track).
142
+ with(:delete, "/stemcells/ubuntu/123",
143
+ nil, nil, :log_type => "event").
144
+ and_return(true)
145
+ @director.delete_stemcell("ubuntu", "123")
146
+ end
147
+
148
+ it "deletes deployment" do
149
+ @director.should_receive(:request_and_track).
150
+ with(:delete, "/deployments/foo",
151
+ nil, nil, :log_type => "event").
152
+ and_return(true)
153
+ @director.delete_deployment("foo")
154
+ end
155
+
156
+ it "deletes release (non-force)" do
157
+ @director.should_receive(:request_and_track).
158
+ with(:delete, "/releases/za",
159
+ nil, nil, :log_type => "event").
160
+ and_return(true)
161
+ @director.delete_release("za")
162
+ end
163
+
164
+ it "deletes release (force)" do
165
+ @director.should_receive(:request_and_track).
166
+ with(:delete, "/releases/zb?force=true",
167
+ nil, nil, :log_type => "event").
168
+ and_return(true)
169
+ @director.delete_release("zb", :force => true)
170
+ end
171
+
172
+ it "deploys" do
173
+ @director.should_receive(:request_and_track).
174
+ with(:post, "/deployments", "text/yaml",
175
+ "manifest", :log_type => "event").
176
+ and_return(true)
177
+ @director.deploy("manifest")
178
+ end
179
+
180
+ it "changes job state" do
181
+ @director.should_receive(:request_and_track).
182
+ with(:put, "/deployments/foo/jobs/dea?state=stopped",
183
+ "text/yaml", "manifest", :log_type => "event").
184
+ and_return(true)
185
+ @director.change_job_state("foo", "manifest", "dea", nil, "stopped")
186
+ end
187
+
188
+ it "changes job instance state" do
189
+ @director.should_receive(:request_and_track).
190
+ with(:put, "/deployments/foo/jobs/dea/0?state=detached",
191
+ "text/yaml", "manifest", :log_type => "event").
192
+ and_return(true)
193
+ @director.change_job_state("foo", "manifest", "dea", 0, "detached")
194
+ end
195
+
196
+ it "gets task state" do
197
+ @director.should_receive(:get).
198
+ with("/tasks/232").
199
+ and_return([200, JSON.generate({ "state" => "done" })])
200
+ @director.get_task_state(232).should == "done"
201
+ end
202
+
203
+ it "whines on missing task" do
204
+ @director.should_receive(:get).
205
+ with("/tasks/232").
206
+ and_return([404, "Not Found"])
207
+ lambda {
208
+ @director.get_task_state(232).should
209
+ }.should raise_error(Bosh::Cli::MissingTask)
210
+ end
211
+
212
+ it "gets task output" do
213
+ @director.should_receive(:get).
214
+ with("/tasks/232/output", nil,
215
+ nil, { "Range" => "bytes=42-" }).
216
+ and_return([206, "test", { :content_range => "bytes 42-56/100" }])
217
+ @director.get_task_output(232, 42).should == ["test", 57]
218
+ end
219
+
220
+ it "doesn't set task output new offset if it wasn't a partial response" do
221
+ @director.should_receive(:get).
222
+ with("/tasks/232/output", nil, nil,
223
+ { "Range" => "bytes=42-" }).
224
+ and_return([200, "test"])
225
+ @director.get_task_output(232, 42).should == ["test", nil]
226
+ end
227
+
228
+ it "know how to find time difference with director" do
229
+ now = Time.now
230
+ server_time = now - 100
231
+ Time.stub!(:now).and_return(now)
232
+
233
+ @director.should_receive(:get).with("/info").
234
+ and_return([200, JSON.generate("version" => 1),
235
+ { :date => server_time.rfc822 }])
236
+ @director.get_time_difference.to_i.should == 100
237
+ end
238
+
239
+ end
240
+
241
+ describe "checking status" do
242
+ it "considers target valid if it responds with 401 (for compatibility)" do
243
+ @director.stub(:get).
244
+ with("/info", "application/json").
245
+ and_return([401, "Not authorized"])
246
+ @director.exists?.should be_true
247
+ end
248
+
249
+ it "considers target valid if it responds with 200" do
250
+ @director.stub(:get).
251
+ with("/info", "application/json").
252
+ and_return([200, JSON.generate("name" => "Director is your friend")])
253
+ @director.exists?.should be_true
254
+ end
255
+ end
256
+
257
+ describe "tracking request" do
258
+ it "starts polling task if request responded with a redirect to task URL" do
259
+ @director.should_receive(:request).
260
+ with(:get, "/stuff", "text/plain", "abc").
261
+ and_return([302, "body", { :location => "/tasks/502" }])
262
+ @director.should_receive(:poll_task).
263
+ with("502", :arg1 => 1, :arg2 => 2).
264
+ and_return("polling result")
265
+ @director.request_and_track(:get, "/stuff", "text/plain",
266
+ "abc", :arg1 => 1, :arg2 => 2).
267
+ should == ["polling result", "502"]
268
+ end
269
+
270
+ it "considers all reponses but 302 a failure" do
271
+ [200, 404, 403].each do |code|
272
+ @director.should_receive(:request).
273
+ with(:get, "/stuff", "text/plain", "abc").
274
+ and_return([code, "body", { }])
275
+ @director.request_and_track(:get, "/stuff", "text/plain",
276
+ "abc", :arg1 => 1, :arg2 => 2).
277
+ should == [:failed, nil]
278
+ end
279
+ end
280
+
281
+ it "reports task as non trackable if its URL is unfamiliar" do
282
+ @director.should_receive(:request).
283
+ with(:get, "/stuff", "text/plain", "abc").
284
+ and_return([302, "body", { :location => "/track-task/502" }])
285
+ @director.request_and_track(:get, "/stuff", "text/plain",
286
+ "abc", :arg1 => 1, :arg2 => 2).
287
+ should == [:non_trackable, nil]
288
+ end
289
+
290
+ it "suppports uploading with progress bar" do
291
+ file = spec_asset("valid_release.tgz")
292
+ f = Bosh::Cli::FileWithProgressBar.open(file, "r")
293
+
294
+ Bosh::Cli::FileWithProgressBar.stub!(:open).with(file, "r").and_return(f)
295
+ @director.should_receive(:request_and_track).
296
+ with(:post, "/stuff", "application/x-compressed", f, { })
297
+ @director.upload_and_track("/stuff", "application/x-compressed", file)
298
+ f.progress_bar.finished?.should be_true
299
+ end
300
+ end
301
+
302
+ describe "performing HTTP requests" do
303
+ it "delegates to HTTPClient" do
304
+ headers = { "Content-Type" => "app/zb", "a" => "b", "c" => "d"}
305
+ user = "user"
306
+ password = "pass"
307
+ auth = "Basic " + Base64.encode64("#{user}:#{password}").strip
308
+
309
+ client = mock("httpclient")
310
+ client.should_receive(:send_timeout=).
311
+ with(Bosh::Cli::Director::API_TIMEOUT)
312
+ client.should_receive(:receive_timeout=).
313
+ with(Bosh::Cli::Director::API_TIMEOUT)
314
+ client.should_receive(:connect_timeout=).
315
+ with(Bosh::Cli::Director::CONNECT_TIMEOUT)
316
+ HTTPClient.stub!(:new).and_return(client)
317
+
318
+ client.should_receive(:request).
319
+ with(:get, "http://target/stuff", :body => "payload",
320
+ :header => headers.merge("Authorization" => auth))
321
+ @director.send(:perform_http_request, :get,
322
+ "http://target/stuff", "payload", headers)
323
+ end
324
+ end
325
+
326
+ describe "talking to REST API" do
327
+ it "performs HTTP request" do
328
+ mock_response = mock("response", :code => 200,
329
+ :body => "test", :headers => {})
330
+
331
+ @director.should_receive(:perform_http_request).
332
+ with(:get, "http://target/stuff", "payload", "h1" => "a",
333
+ "h2" => "b", "Content-Type" => "app/zb").
334
+ and_return(mock_response)
335
+
336
+ @director.request(:get, "/stuff", "app/zb", "payload",
337
+ { "h1" => "a", "h2" => "b"}).
338
+ should == [200, "test", {}]
339
+ end
340
+
341
+ it "nicely wraps director error response" do
342
+ [400, 403, 500].each do |code|
343
+ lambda {
344
+ # Familiar JSON
345
+ body = JSON.generate("code" => "40422",
346
+ "description" => "Weird stuff happened")
347
+
348
+ mock_response = mock("response",
349
+ :code => code,
350
+ :body => body,
351
+ :headers => {})
352
+
353
+ @director.should_receive(:perform_http_request).
354
+ and_return(mock_response)
355
+ @director.request(:get, "/stuff", "application/octet-stream",
356
+ "payload", { :hdr1 => "a", :hdr2 => "b"})
357
+ }.should raise_error(Bosh::Cli::DirectorError,
358
+ "Director error 40422: Weird stuff happened")
359
+
360
+ lambda {
361
+ # Not JSON
362
+ mock_response = mock("response", :code => code,
363
+ :body => "error message goes here",
364
+ :headers => {})
365
+ @director.should_receive(:perform_http_request).
366
+ and_return(mock_response)
367
+ @director.request(:get, "/stuff", "application/octet-stream",
368
+ "payload", { :hdr1 => "a", :hdr2 => "b"})
369
+ }.should raise_error(Bosh::Cli::DirectorError,
370
+ "Director error (HTTP #{code}): " +
371
+ "error message goes here")
372
+
373
+ lambda {
374
+ # JSON but weird
375
+ mock_response = mock("response", :code => code,
376
+ :body => '{"c":"d","a":"b"}',
377
+ :headers => {})
378
+ @director.should_receive(:perform_http_request).
379
+ and_return(mock_response)
380
+ @director.request(:get, "/stuff", "application/octet-stream",
381
+ "payload", { :hdr1 => "a", :hdr2 => "b"})
382
+ }.should raise_error(Bosh::Cli::DirectorError,
383
+ "Director error (HTTP #{code}): " +
384
+ %Q[{"c":"d","a":"b"}])
385
+ end
386
+ end
387
+
388
+ it "wraps director access exceptions" do
389
+ [URI::Error, SocketError, Errno::ECONNREFUSED].each do |err|
390
+ @director.should_receive(:perform_http_request).
391
+ and_raise(err.new("err message"))
392
+ lambda {
393
+ @director.request(:get, "/stuff", "app/zb", "payload", { })
394
+ }.should raise_error(Bosh::Cli::DirectorInaccessible)
395
+ end
396
+ @director.should_receive(:perform_http_request).
397
+ and_raise(SystemCallError.new("err message"))
398
+ lambda {
399
+ @director.request(:get, "/stuff", "app/zb", "payload", { })
400
+ }.should raise_error Bosh::Cli::DirectorError
401
+ end
402
+
403
+ it "streams file" do
404
+ mock_response = mock("response", :code => 200,
405
+ :body => "test body", :headers => { })
406
+ @director.should_receive(:perform_http_request).
407
+ and_yield("test body").and_return(mock_response)
408
+
409
+ code, filename, headers = @director.request(:get,
410
+ "/files/foo", nil, nil,
411
+ { }, { :file => true })
412
+
413
+ code.should == 200
414
+ File.read(filename).should == "test body"
415
+ headers.should == { }
416
+ end
417
+ end
418
+
419
+ describe "polling jobs" do
420
+ it "polls until success" do
421
+ n_calls = 0
422
+
423
+ @director.stub!(:get_time_difference).and_return(0)
424
+ @director.should_receive(:get).
425
+ with("/tasks/1").exactly(5).times.
426
+ and_return {
427
+ n_calls += 1;
428
+ [200,
429
+ JSON.generate("state" => n_calls == 5 ? "done" : "processing")
430
+ ]
431
+ }
432
+ @director.should_receive(:get).
433
+ with("/tasks/1/output",
434
+ nil, nil, "Range" => "bytes=0-").
435
+ exactly(5).times.and_return(nil)
436
+
437
+ @director.poll_task(1, :poll_interval => 0, :max_polls => 1000).
438
+ should == :done
439
+ end
440
+
441
+ it "respects max polls setting" do
442
+ @director.stub!(:get_time_difference).and_return(0)
443
+ @director.should_receive(:get).with("/tasks/1").
444
+ exactly(10).times.
445
+ and_return [200, JSON.generate("state" => "processing")]
446
+ @director.should_receive(:get).
447
+ with("/tasks/1/output",
448
+ nil, nil, "Range" => "bytes=0-").
449
+ exactly(10).times.and_return(nil)
450
+
451
+ @director.poll_task(1, :poll_interval => 0, :max_polls => 10).
452
+ should == :track_timeout
453
+ end
454
+
455
+ it "respects poll interval setting" do
456
+ @director.stub(:get).and_return([200, "processing"])
457
+
458
+ @director.should_receive(:get).with("/tasks/1").
459
+ exactly(10).times.
460
+ and_return([200, JSON.generate("state" => "processing")])
461
+ @director.should_receive(:get).
462
+ with("/tasks/1/output", nil, nil,
463
+ "Range" => "bytes=0-").
464
+ exactly(10).times.and_return(nil)
465
+ @director.should_receive(:sleep).with(5).exactly(9).times.and_return(nil)
466
+
467
+ @director.poll_task(1, :poll_interval => 5, :max_polls => 10).
468
+ should == :track_timeout
469
+ end
470
+
471
+ it "stops polling and returns error if status is not HTTP 200" do
472
+ @director.stub!(:get_time_difference).and_return(0)
473
+
474
+ @director.should_receive(:get).
475
+ with("/tasks/1").
476
+ and_return([500, JSON.generate("state" => "processing")])
477
+
478
+ lambda {
479
+ @director.poll_task(1, :poll_interval => 0, :max_polls => 10)
480
+ }.should raise_error(Bosh::Cli::TaskTrackError,
481
+ "Got HTTP 500 while tracking task state")
482
+ end
483
+
484
+ it "stops polling and returns error if task state is error" do
485
+ @director.stub!(:get_time_difference).and_return(0)
486
+
487
+ @director.stub(:get).
488
+ with("/tasks/1/output", nil, nil,
489
+ "Range" => "bytes=0-").
490
+ and_return([200, ""])
491
+
492
+ @director.stub(:get).
493
+ with("/tasks/1").
494
+ and_return([200, JSON.generate("state" => "error")])
495
+
496
+ @director.should_receive(:get).exactly(1).times
497
+
498
+ @director.poll_task(1, :poll_interval => 0, :max_polls => 10).
499
+ should == :error
500
+ end
501
+ end
502
+
503
+ it "calls cancel_task on the current task when cancel_current is called" do
504
+ task_num = 1
505
+ @director.stub(:cancel_task).and_return(["body", 200])
506
+ @director.should_receive(:cancel_task).once.with(task_num)
507
+ @director.should_receive(:say).once.with("Cancelling task ##{task_num}.")
508
+ @director.current_running_task = task_num
509
+ @director.cancel_current
510
+ end
511
+ end