tools-cf-plugin 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,180 @@
1
+ require "spec_helper"
2
+
3
+ module CFTools::Tunnel
4
+ describe Base do
5
+ before do
6
+ stub(subject).input { { :quiet => true } }
7
+ end
8
+
9
+ describe "#director" do
10
+ context "when the given director is accessible" do
11
+ before do
12
+ stub(subject).address_reachable?("some-director.com", 25555) { true }
13
+ end
14
+
15
+ it "returns the given director" do
16
+ director = subject.director("some-director.com", nil)
17
+ expect(director.director_uri.host).to eq("some-director.com")
18
+ expect(director.director_uri.port).to eq(25555)
19
+ end
20
+ end
21
+
22
+ context "when the given director is inaccessible" do
23
+ before do
24
+ stub(subject).address_reachable?("some-director.com", 25555) { false }
25
+ end
26
+
27
+ it "opens a tunnel through the gateway" do
28
+ mock(subject).tunnel_to("some-director.com", 25555, "user@some-gateway") do
29
+ 1234
30
+ end
31
+
32
+ director = subject.director("some-director.com", "user@some-gateway")
33
+ expect(director.director_uri.host).to eq("127.0.0.1")
34
+ expect(director.director_uri.port).to eq(1234)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "#login_to_director" do
40
+ let(:director) { stub }
41
+
42
+ before do
43
+ stub(director).user = anything
44
+ stub(director).password = anything
45
+ stub(director).authenticated? { true }
46
+ end
47
+
48
+ it "assigns the given user/pass on the director" do
49
+ mock(director).user = "user"
50
+ mock(director).password = "pass"
51
+ subject.login_to_director(director, "user", "pass")
52
+ end
53
+
54
+ it "returns true iff director.authenticated?" do
55
+ mock(director).authenticated? { true }
56
+ expect(subject.login_to_director(director, "user", "pass")).to be_true
57
+ end
58
+
59
+ it "returns false iff !director.authenticated?" do
60
+ mock(director).authenticated? { false }
61
+ expect(subject.login_to_director(director, "user", "pass")).to be_false
62
+ end
63
+ end
64
+
65
+ describe "#tunnel_to" do
66
+ let(:gateway) { stub }
67
+
68
+ before do
69
+ stub(gateway).open
70
+ end
71
+
72
+ it "creates a gateway using the given user/host" do
73
+ mock(Net::SSH::Gateway).new("ghost", "guser") { gateway }
74
+ subject.tunnel_to("1.2.3.4", 1234, "guser@ghost")
75
+ end
76
+
77
+ it "opens a local tunnel and returns its port" do
78
+ stub(Net::SSH::Gateway).new("ghost", "guser") { gateway }
79
+ mock(gateway).open("1.2.3.4", 1234) { 5678 }
80
+ expect(subject.tunnel_to("1.2.3.4", 1234, "guser@ghost")).to eq(5678)
81
+ end
82
+ end
83
+
84
+ describe "#authenticate_with_director" do
85
+ let(:director) { stub }
86
+
87
+ def self.it_asks_interactively
88
+ it "asks for the credentials interactively" do
89
+ if saved_credentials
90
+ mock(subject).login_to_director(director, "user", "pass") { false }.ordered
91
+ end
92
+
93
+ mock_ask("Director Username") { "fizz" }.ordered
94
+ mock_ask("Director Password", anything) { "buzz" }.ordered
95
+
96
+ mock(subject).login_to_director(director, "fizz", "buzz") { true }.ordered
97
+
98
+ subject.authenticate_with_director(director, "foo", saved_credentials)
99
+ end
100
+
101
+ context "when the interactive user/pass is valid" do
102
+ it "returns true" do
103
+ if saved_credentials
104
+ mock(subject).login_to_director(director, "user", "pass") { false }.ordered
105
+ end
106
+
107
+ mock_ask("Director Username") { "fizz" }.ordered
108
+ mock_ask("Director Password", anything) { "buzz" }.ordered
109
+
110
+ mock(subject).login_to_director(director, "fizz", "buzz") { true }.ordered
111
+
112
+ expect(
113
+ subject.authenticate_with_director(director, "foo", saved_credentials)
114
+ ).to be_true
115
+ end
116
+
117
+ it "saves them to the bosh config" do
118
+ if saved_credentials
119
+ mock(subject).login_to_director(director, "user", "pass") { false }.ordered
120
+ end
121
+
122
+ mock_ask("Director Username") { "fizz" }.ordered
123
+ mock_ask("Director Password", anything) { "buzz" }.ordered
124
+
125
+ mock(subject).login_to_director(director, "fizz", "buzz") { true }.ordered
126
+
127
+ mock(subject).save_auth("foo", "username" => "fizz", "password" => "buzz")
128
+
129
+ subject.authenticate_with_director(director, "foo", saved_credentials)
130
+ end
131
+ end
132
+
133
+ context "when the interactive user/pass is invalid" do
134
+ it "asks again" do
135
+ if saved_credentials
136
+ mock(subject).login_to_director(director, "user", "pass") { false }.ordered
137
+ end
138
+
139
+ mock_ask("Director Username") { "fizz" }.ordered
140
+ mock_ask("Director Password", anything) { "buzz" }.ordered
141
+
142
+ mock(subject).login_to_director(director, "fizz", "buzz") { false }.ordered
143
+
144
+ mock_ask("Director Username") { "a" }.ordered
145
+ mock_ask("Director Password", anything) { "b" }.ordered
146
+
147
+ mock(subject).login_to_director(director, "a", "b") { true }.ordered
148
+
149
+ subject.authenticate_with_director(director, "foo", saved_credentials)
150
+ end
151
+ end
152
+ end
153
+
154
+ context "when saved credentials are given" do
155
+ let(:saved_credentials) { { "username" => "user", "password" => "pass" } }
156
+
157
+ context "and they are valid" do
158
+ it "returns true" do
159
+ mock(subject).login_to_director(director, "user", "pass") { true }
160
+
161
+ expect(
162
+ subject.authenticate_with_director(
163
+ director, "foo", "username" => "user", "password" => "pass")
164
+ ).to be_true
165
+ end
166
+ end
167
+
168
+ context "and they are NOT valid" do
169
+ it_asks_interactively
170
+ end
171
+ end
172
+
173
+ context "when auth credentials are NOT given" do
174
+ let(:saved_credentials) { nil }
175
+
176
+ it_asks_interactively
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,109 @@
1
+ require "spec_helper"
2
+
3
+ describe CFTools::Tunnel::LogEntry do
4
+ let(:label) { "some-label" }
5
+ let(:line) { "something happened!" }
6
+ let(:stream) { :stdout }
7
+
8
+ subject { described_class.new(label, line, stream) }
9
+
10
+ describe "#label" do
11
+ let(:label) { "some-label" }
12
+
13
+ it "returns the label of the entry" do
14
+ expect(subject.label).to eq("some-label")
15
+ end
16
+ end
17
+
18
+ describe "#line" do
19
+ let(:line) { "something happened!" }
20
+
21
+ it "returns the line of the entry" do
22
+ expect(subject.line).to eq("something happened!")
23
+ end
24
+ end
25
+
26
+ describe "#stream" do
27
+ let(:stream) { :stdout }
28
+
29
+ it "returns the stream of the entry" do
30
+ expect(subject.stream).to eq(:stdout)
31
+ end
32
+ end
33
+
34
+ describe "#message" do
35
+ context "when the line is JSON" do
36
+ let(:line) { '{"message":"foo"}' }
37
+
38
+ it "return its 'message' field" do
39
+ expect(subject.message).to eq("foo")
40
+ end
41
+ end
42
+
43
+ context "when the line is NOT JSON" do
44
+ let(:line) { "bar" }
45
+
46
+ it "returns the line" do
47
+ expect(subject.message).to eq("bar")
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#log_level" do
53
+ context "when the line is JSON" do
54
+ let(:line) { '{"log_level":"debug"}' }
55
+
56
+ it "return its 'log_level' field" do
57
+ expect(subject.log_level).to eq("debug")
58
+ end
59
+ end
60
+
61
+ context "when the line is NOT JSON" do
62
+ let(:line) { "bar" }
63
+
64
+ it "returns nil" do
65
+ expect(subject.log_level).to be_nil
66
+ end
67
+ end
68
+ end
69
+
70
+ describe "#timestamp" do
71
+ let(:time) { Time.now }
72
+
73
+ around { |example| Timecop.freeze(&example) }
74
+
75
+ context "when the line is JSON" do
76
+ context "and the timestamp is a string" do
77
+ let(:line) { %Q[{"timestamp":"#{time.strftime("%F %T")}"}] }
78
+
79
+ it "return its parsed 'timestamp' field" do
80
+ expect(subject.timestamp.to_s).to eq(time.to_s)
81
+ end
82
+ end
83
+
84
+ context "and the timestamp is numeric" do
85
+ let(:line) { %Q[{"timestamp":#{time.to_f}}] }
86
+
87
+ it "interprets it as time since UNIX epoch" do
88
+ expect(subject.timestamp.to_s).to eq(time.to_s)
89
+ end
90
+ end
91
+
92
+ context "and the timestamp is missing" do
93
+ let(:line) { %Q[{}] }
94
+
95
+ it "returns the time at which the entry was created" do
96
+ expect(subject.timestamp).to eq(time)
97
+ end
98
+ end
99
+ end
100
+
101
+ context "when the line is NOT JSON" do
102
+ let(:line) { "bar" }
103
+
104
+ it "returns the time at which the entry was created" do
105
+ expect(subject.timestamp).to eq(time)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,80 @@
1
+ require "spec_helper"
2
+
3
+ module CFTools::Tunnel
4
+ describe MultiLineStream do
5
+ let(:director) { stub }
6
+ let(:deployment) { "some-deployment" }
7
+ let(:gateway_user) { "vcap" }
8
+ let(:gateway_host) { "vcap.me" }
9
+
10
+ let(:gateway) { stub }
11
+ let(:entries) { Queue.new }
12
+
13
+ subject do
14
+ described_class.new(director, deployment, gateway_user, gateway_host)
15
+ end
16
+
17
+ before do
18
+ stub(subject).create_ssh_user { ["1.2.3.4", "some_user_1"] }
19
+ stub(subject).gateway { gateway }
20
+ stub(subject).entry_queue { entries }
21
+ stub(Thread).new { |blk| blk.call }
22
+ end
23
+
24
+ describe "#stream" do
25
+ it "yields entries as they come through the queue" do
26
+ entries << :a
27
+ entries << :b
28
+ entries << nil
29
+
30
+ seen = []
31
+ subject.stream({}) do |e|
32
+ seen << e
33
+ end
34
+
35
+ expect(seen).to eq([:a, :b])
36
+ end
37
+
38
+ it "spawns a SSH tunnel for each location" do
39
+ mock(Thread).new { |blk| blk.call }.ordered
40
+ mock(Thread).new { |blk| blk.call }.ordered
41
+
42
+ mock(subject).create_ssh_user("foo", 0, entries) { ["1.2.3.4", "some_user_1"] }
43
+ mock(subject).create_ssh_user("bar", 0, entries) { ["1.2.3.5", "some_user_2"] }
44
+
45
+ mock(gateway).ssh("1.2.3.4", "some_user_1")
46
+ mock(gateway).ssh("1.2.3.5", "some_user_2")
47
+
48
+ entries << nil
49
+
50
+ subject.stream(["foo", 0] => [], ["bar", 0]=> [])
51
+ end
52
+
53
+ it "streams from each location" do
54
+ ssh = stub
55
+
56
+ locations = {
57
+ "1.2.3.4" => [
58
+ StreamLocation.new("some/path", "some-label"),
59
+ StreamLocation.new("some/other/path", "some-label")
60
+ ],
61
+ "1.2.3.5" => [
62
+ StreamLocation.new("some/path", "other-label"),
63
+ ]
64
+ }
65
+
66
+ locations.each do |_, locs|
67
+ locs.each do |loc|
68
+ mock(loc).stream_lines(ssh)
69
+ end
70
+ end
71
+
72
+ stub(gateway).ssh { |_, _, blk| blk.call(ssh) }
73
+
74
+ entries << nil
75
+
76
+ subject.stream(locations)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,59 @@
1
+ require "spec_helper"
2
+
3
+ describe CFTools::Tunnel::StreamLocation do
4
+ let(:path) { "some/file" }
5
+ let(:label) { "some_component/0" }
6
+
7
+ subject { described_class.new(path, label) }
8
+
9
+ describe "#path" do
10
+ let(:path) { "some/path" }
11
+
12
+ it "returns the path of the entry" do
13
+ expect(subject.path).to eq("some/path")
14
+ end
15
+ end
16
+
17
+ describe "#label" do
18
+ let(:label) { "some-label" }
19
+
20
+ it "returns the label of the entry" do
21
+ expect(subject.label).to eq("some-label")
22
+ end
23
+ end
24
+
25
+ describe "#stream_lines" do
26
+ let(:ssh) { stub }
27
+
28
+ it "tails the file under /var/vcap/sys/log" do
29
+ mock(ssh).exec("tail -f /var/vcap/sys/log/#{path}")
30
+ subject.stream_lines(ssh)
31
+ end
32
+
33
+ it "yields log entries as lines come through the channel" do
34
+ stub(ssh).exec { |_, blk| blk.call({}, :stdout, "foo\nbar\n") }
35
+
36
+ lines = []
37
+ subject.stream_lines(ssh) do |entry|
38
+ lines << entry.message
39
+ end
40
+
41
+ expect(lines).to eq(["foo\n", "bar\n"])
42
+ end
43
+
44
+ it "merges chunks that form a complete line" do
45
+ channel = {}
46
+ stub(ssh).exec do |_, blk|
47
+ blk.call(channel, :stdout, "fo")
48
+ blk.call(channel, :stdout, "o\nbar\n")
49
+ end
50
+
51
+ lines = []
52
+ subject.stream_lines(ssh) do |entry|
53
+ lines << entry.message
54
+ end
55
+
56
+ expect(lines).to eq(["foo\n", "bar\n"])
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,117 @@
1
+ require "spec_helper"
2
+
3
+ module CFTools::Tunnel
4
+ describe WatchLogs do
5
+ let(:director_uri) { "https://some-director.com:25555" }
6
+
7
+ let(:director) { Bosh::Cli::Director.new(director_uri) }
8
+
9
+ let(:stream) { stub }
10
+
11
+ let(:vms) do
12
+ [ { "ips" => ["1.2.3.4"], "job_name" => "cloud_controller", "index" => 0 },
13
+ { "ips" => ["1.2.3.5"], "job_name" => "dea_next", "index" => 0 },
14
+ { "ips" => ["1.2.3.6"], "job_name" => "dea_next", "index" => 1 }
15
+ ]
16
+ end
17
+
18
+ let(:deployments) do
19
+ [{ "name" => "some-deployment", "releases" => [{ "name" => "cf-release" }] }]
20
+ end
21
+
22
+ def mock_cli
23
+ any_instance_of(described_class) do |cli|
24
+ mock(cli)
25
+ end
26
+ end
27
+
28
+ def stub_cli
29
+ any_instance_of(described_class) do |cli|
30
+ stub(cli)
31
+ end
32
+ end
33
+
34
+ before do
35
+ stub(director).list_deployments { deployments }
36
+ stub(director).fetch_vm_state { vms }
37
+ stub_cli.connected_director { director }
38
+
39
+ stub(stream).stream
40
+ stub_cli.stream_for { stream }
41
+ end
42
+
43
+ it "connects to the given director" do
44
+ mock_cli.connected_director(
45
+ "some-director.com", "someuser@somehost.com") do
46
+ director
47
+ end
48
+
49
+ cf %W[watch-logs some-director.com --gateway someuser@somehost.com]
50
+ end
51
+
52
+ context "when no gateway user/host is specified" do
53
+ it "defaults to vcap@director" do
54
+ mock_cli.connected_director(
55
+ "some-director.com", "vcap@some-director.com") do
56
+ director
57
+ end
58
+
59
+ cf %W[watch-logs some-director.com]
60
+ end
61
+ end
62
+
63
+ context "when there are no jobs to log" do
64
+ let(:vms) { [] }
65
+
66
+ it "fails with a message" do
67
+ cf %W[watch-logs some-director.com]
68
+ expect(error_output).to say("No locations found.")
69
+ end
70
+ end
71
+
72
+ context "when there are jobs to log" do
73
+ it "streams their locations" do
74
+ mock(stream).stream(anything) do |locations, blk|
75
+ expect(locations).to include(["cloud_controller", 0])
76
+ expect(locations).to include(["dea_next", 0])
77
+ expect(locations).to include(["dea_next", 1])
78
+ end
79
+
80
+ cf %W[watch-logs some-director.com]
81
+ end
82
+
83
+ it "pretty-prints their log entries" do
84
+ entry1_time = Time.new(2011, 06, 21, 1, 2, 3)
85
+ entry2_time = Time.new(2011, 06, 21, 1, 2, 4)
86
+ entry3_time = Time.new(2011, 06, 21, 1, 2, 5)
87
+
88
+ entry1 = LogEntry.new(
89
+ "cloud_controller/0",
90
+ %Q[{"message":"a","timestamp":#{entry1_time.to_f},"log_level":"info"}],
91
+ :stdout)
92
+
93
+ entry2 = LogEntry.new(
94
+ "dea_next/1",
95
+ %Q[{"message":"b","timestamp":#{entry2_time.to_f},"log_level":"warn"}],
96
+ :stdout)
97
+
98
+ entry3 = LogEntry.new(
99
+ "dea_next/0",
100
+ %Q[{"message":"c","timestamp":#{entry3_time.to_f},"log_level":"error"}],
101
+ :stdout)
102
+
103
+ mock(stream).stream(anything) do |locations, blk|
104
+ blk.call(entry1)
105
+ blk.call(entry2)
106
+ blk.call(entry3)
107
+ end
108
+
109
+ cf %W[watch-logs some-director.com]
110
+
111
+ expect(output).to say("cloud_controller/0 01:02:03 AM info a\n")
112
+ expect(output).to say("dea_next/1 01:02:04 AM warn b\n")
113
+ expect(output).to say("dea_next/0 01:02:05 AM error c\n")
114
+ end
115
+ end
116
+ end
117
+ end