bosh_cli 0.16

Sign up to get free protection for your applications and to get access to all the features.
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