tools-cf-plugin 1.0.0 → 1.1.0

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.
@@ -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