pe-razor-client 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ ---
2
+
3
+ # This file helps Razor::CLI::Navigate with a few things by annotating the
4
+ # API. Ultimately, this should be something the server provides. But until
5
+ # we better understand what sort of annotation we need from the server,
6
+ # we'll keep this with the client.
7
+ #
8
+ # Possible argument types are
9
+ # - json: parse the argument value as a JSON document and use that
10
+ # - boolean: if the argument has no value associated with it, or the value
11
+ # is "true", convert to +true+; everything else gets converted
12
+ # to +false+
13
+ commands:
14
+ create-broker:
15
+ args:
16
+ configuration: json
17
+ create-tag:
18
+ args:
19
+ rule: json
20
+ add-policy-tag:
21
+ args:
22
+ rule: json
23
+ update-tag-rule:
24
+ args:
25
+ rule: json
26
+ reboot-node:
27
+ args:
28
+ hard: boolean
@@ -0,0 +1,86 @@
1
+ require 'uri'
2
+ require 'optparse'
3
+
4
+ module Razor::CLI
5
+
6
+ class Parse
7
+ DEFAULT_RAZOR_API = "http://localhost:8080/api"
8
+
9
+ def get_optparse
10
+ @optparse ||= OptionParser.new do |opts|
11
+ opts.banner = "Usage: razor [FLAGS] NAVIGATION\n"
12
+
13
+ opts.on "-d", "--dump", "Dumps API output to the screen" do
14
+ @dump = true
15
+ end
16
+
17
+ opts.on "-u", "--url URL",
18
+ "The full Razor API URL, can also be set\n" + " "*37 +
19
+ "with the RAZOR_API environment variable\n" + " "*37 +
20
+ "(default #{DEFAULT_RAZOR_API})" do |url|
21
+ parse_and_set_api_url(url, :opts)
22
+ end
23
+
24
+ opts.on "-h", "--help", "Show this screen" do
25
+ @option_help = true
26
+ end
27
+
28
+ end
29
+ end
30
+
31
+ def list_things(name, items)
32
+ "\n #{name}:\n" +
33
+ items.map {|x| x["name"]}.compact.sort.map do |name|
34
+ " #{name}"
35
+ end.join("\n")
36
+ end
37
+
38
+ def help
39
+ output = get_optparse.to_s
40
+ begin
41
+ output << list_things("Collections", navigate.collections)
42
+ output << "\n\n Navigate to entries of a collection using COLLECTION NAME, for example,\n 'nodes node15' for the details of a node or 'nodes node15 log' to see\n the log for node15\n"
43
+ output << list_things("Commands", navigate.commands)
44
+ output << "\n\n Pass arguments to commands either directly by name ('--name=NAME')\n or save the JSON body for the command in a file and pass it with\n '--json FILE'. Using --json is the only way to pass arguments in\n nested structures such as the configuration for a broker.\n"
45
+ rescue
46
+ output << "\nCould not connect to the server at #{@api_url}. More help is available after "
47
+ output << "pointing\nthe client to a Razor server"
48
+ end
49
+ output
50
+ end
51
+
52
+ def show_help?
53
+ !!@option_help
54
+ end
55
+
56
+ def dump_response?
57
+ !!@dump
58
+ end
59
+
60
+ attr_reader :api_url
61
+
62
+ def initialize(args)
63
+ parse_and_set_api_url(ENV["RAZOR_API"] || DEFAULT_RAZOR_API, :env)
64
+ @args = args.dup
65
+ rest = get_optparse.order(args)
66
+ if rest.any?
67
+ @navigation = rest
68
+ else
69
+ @option_help = true
70
+ end
71
+ end
72
+
73
+ def navigate
74
+ @navigate ||=Navigate.new(self, @navigation)
75
+ end
76
+
77
+ private
78
+ def parse_and_set_api_url(url, source)
79
+ begin
80
+ @api_url = URI.parse(url)
81
+ rescue URI::InvalidURIError => e
82
+ raise Razor::CLI::InvalidURIError.new(url, source)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pe-razor-client"
7
+ spec.version = "0.14.0"
8
+ spec.authors = ["Puppet Labs"]
9
+ spec.email = ["info@puppetlabs.com"]
10
+ spec.description = "The client for the Razor server"
11
+ spec.summary = "The client for everybody's favorite provisioning tool"
12
+ spec.homepage = "https://github.com/puppetlabs/razor-client"
13
+ spec.license = "ASL2"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.bindir = "bin"
17
+ spec.executables = ['razor']
18
+ spec.test_files = spec.files.grep(%r{^spec/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version
22
+
23
+ # mime-types is a dependency of rest-client. We need to explicitly depend
24
+ # on it and pin its version to make sure the gem works with Ruby 1.8.7
25
+ spec.add_dependency "mime-types", '< 2.0'
26
+ spec.add_dependency "multi_json"
27
+ spec.add_dependency "rest-client"
28
+ spec.add_dependency "terminal-table"
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.3"
31
+ spec.add_development_dependency "rake"
32
+ end
@@ -0,0 +1,52 @@
1
+ # Needed to make the client work on Ruby 1.8.7
2
+ unless Kernel.respond_to?(:require_relative)
3
+ module Kernel
4
+ def require_relative(path)
5
+ require File.join(File.dirname(caller[0]), path.to_str)
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative '../spec_helper'
11
+
12
+ describe Razor::CLI::Navigate do
13
+ context "with no path", :vcr do
14
+ subject(:nav) {Razor::CLI::Parse.new([]).navigate}
15
+ it do
16
+ nav.get_document.should_not be_nil
17
+ nav.get_document.should == nav.entrypoint
18
+ end
19
+ end
20
+
21
+ context "with a single item path", :vcr do
22
+ subject(:nav) {Razor::CLI::Parse.new(["tags"]).navigate}
23
+ it { nav.get_document.should == []}
24
+
25
+ it do
26
+ nav.get_document;
27
+ nav.last_url.to_s.should =~ %r{/api/collections/tags$}
28
+ end
29
+ end
30
+
31
+ context "with an invalid path", :vcr do
32
+ subject(:nav) {Razor::CLI::Parse.new(["going","nowhere"]).navigate}
33
+
34
+ it {expect{nav.get_document}.to raise_error Razor::CLI::NavigationError}
35
+ end
36
+
37
+ context "with authentication", :vcr do
38
+ AuthArg = %w[-u http://fred:dead@localhost:8080/api].freeze
39
+
40
+ it "should supply that to the API service" do
41
+ nav = Razor::CLI::Parse.new(AuthArg).navigate
42
+ nav.get_document.should be_an_instance_of Hash
43
+ URI.parse(nav.last_url.to_s).userinfo.should == "fred:dead"
44
+ end
45
+
46
+ it "should preserve that across navigation" do
47
+ nav = Razor::CLI::Parse.new(AuthArg + ['tags']).navigate
48
+ nav.get_document.should == []
49
+ URI.parse(nav.last_url.to_s).userinfo.should == "fred:dead"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ # Needed to make the client work on Ruby 1.8.7
2
+ unless Kernel.respond_to?(:require_relative)
3
+ module Kernel
4
+ def require_relative(path)
5
+ require File.join(File.dirname(caller[0]), path.to_str)
6
+ end
7
+ end
8
+ end
9
+
10
+ require "rspec/expectations"
11
+ require_relative '../spec_helper'
12
+
13
+ describe Razor::CLI::Parse do
14
+
15
+ def parse(*args)
16
+ Razor::CLI::Parse.new(args)
17
+ end
18
+
19
+ describe "#new" do
20
+ context "with no arguments" do
21
+ it {parse.show_help?.should be true}
22
+ end
23
+
24
+ context "with a '-h'" do
25
+ it {parse("-h").show_help?.should be true}
26
+ end
27
+
28
+ context "with a '-d'" do
29
+ it {parse("-d").dump_response?.should be true}
30
+ end
31
+
32
+ context "with a '-u'" do
33
+ it "should use the given URL" do
34
+ url = 'http://razor.example.com:2150/path/to/api'
35
+ parse('-u',url).api_url.to_s.should == url
36
+ end
37
+
38
+ it "should terminate with an error if an invalid URL is provided" do
39
+ expect{parse('-u','not valid url')}.to raise_error(Razor::CLI::InvalidURIError)
40
+ end
41
+ end
42
+
43
+ context "with ENV RAZOR_API set" do
44
+ it "should use the given URL" do
45
+ url = 'http://razor.example.com:2150/env/path/to/api'
46
+ ENV["RAZOR_API"] = url
47
+ parse.api_url.to_s.should == url
48
+ end
49
+
50
+ it "should use -u before ENV" do
51
+ env_url = 'http://razor.example.com:2150/env/path/to/api'
52
+ url = 'http://razor.example.com:2150/path/to/api'
53
+ ENV["RAZOR_API"] = env_url
54
+ parse('-u',url).api_url.to_s.should == url
55
+ end
56
+
57
+ it "should terminate with an error if an invalid URL is provided" do
58
+ ENV["RAZOR_API"] = 'not valid url'
59
+ expect{parse}.to raise_error(Razor::CLI::InvalidURIError)
60
+ end
61
+ end
62
+
63
+ describe "#help", :vcr do
64
+ subject(:p) {parse}
65
+ it { should respond_to :help}
66
+
67
+ it { p.help.should be_a String}
68
+
69
+ it "should print a list of known endpoints" do
70
+ p.navigate.should_receive(:collections).and_return([])
71
+ p.navigate.should_receive(:commands).and_return([])
72
+ p.help
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,69 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: http://localhost:8080/api
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept:
11
+ - application/json
12
+ Accept-Encoding:
13
+ - gzip, deflate
14
+ User-Agent:
15
+ - Ruby
16
+ response:
17
+ status:
18
+ code: 200
19
+ message: OK
20
+ headers:
21
+ Server:
22
+ - Apache-Coyote/1.1
23
+ X-Content-Type-Options:
24
+ - nosniff
25
+ Content-Type:
26
+ - application/json;charset=utf-8
27
+ Content-Length:
28
+ - '1566'
29
+ Date:
30
+ - Wed, 28 Aug 2013 21:46:05 GMT
31
+ body:
32
+ encoding: US-ASCII
33
+ string: ! '{"commands":[{"name":"create-image","rel":"http://api.puppetlabs.com/razor/v1/commands/create-image","id":"http://localhost:8080/api/commands/create-image"},{"name":"delete-image","rel":"http://api.puppetlabs.com/razor/v1/commands/delete-image","id":"http://localhost:8080/api/commands/delete-image"},{"name":"create-installer","rel":"http://api.puppetlabs.com/razor/v1/commands/create-installer","id":"http://localhost:8080/api/commands/create-installer"},{"name":"create-tag","rel":"http://api.puppetlabs.com/razor/v1/commands/create-tag","id":"http://localhost:8080/api/commands/create-tag"},{"name":"create-broker","rel":"http://api.puppetlabs.com/razor/v1/commands/create-broker","id":"http://localhost:8080/api/commands/create-broker"},{"name":"create-policy","rel":"http://api.puppetlabs.com/razor/v1/commands/create-policy","id":"http://localhost:8080/api/commands/create-policy"}],"collections":[{"name":"brokers","rel":"http://api.puppetlabs.com/razor/v1/collections/brokers","id":"http://localhost:8080/api/collections/brokers"},{"name":"images","rel":"http://api.puppetlabs.com/razor/v1/collections/images","id":"http://localhost:8080/api/collections/images"},{"name":"tags","rel":"http://api.puppetlabs.com/razor/v1/collections/tags","id":"http://localhost:8080/api/collections/tags"},{"name":"policies","rel":"http://api.puppetlabs.com/razor/v1/collections/policies","id":"http://localhost:8080/api/collections/policies"},{"name":"nodes","rel":"http://api.puppetlabs.com/razor/v1/collections/nodes","id":"http://localhost:8080/api/collections/nodes"}]}'
34
+ http_version:
35
+ recorded_at: Wed, 28 Aug 2013 21:46:05 GMT
36
+ - request:
37
+ method: get
38
+ uri: http://localhost:8080/api/collections/tags
39
+ body:
40
+ encoding: US-ASCII
41
+ string: ''
42
+ headers:
43
+ Accept:
44
+ - application/json
45
+ Accept-Encoding:
46
+ - gzip, deflate
47
+ User-Agent:
48
+ - Ruby
49
+ response:
50
+ status:
51
+ code: 200
52
+ message: OK
53
+ headers:
54
+ Server:
55
+ - Apache-Coyote/1.1
56
+ X-Content-Type-Options:
57
+ - nosniff
58
+ Content-Type:
59
+ - application/json;charset=utf-8
60
+ Content-Length:
61
+ - '2'
62
+ Date:
63
+ - Wed, 28 Aug 2013 21:46:05 GMT
64
+ body:
65
+ encoding: US-ASCII
66
+ string: ! '[]'
67
+ http_version:
68
+ recorded_at: Wed, 28 Aug 2013 21:46:05 GMT
69
+ recorded_with: VCR 2.5.0
@@ -0,0 +1,36 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: http://localhost:8080/api
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept:
11
+ - application/json
12
+ Accept-Encoding:
13
+ - gzip, deflate
14
+ User-Agent:
15
+ - Ruby
16
+ response:
17
+ status:
18
+ code: 200
19
+ message: OK
20
+ headers:
21
+ Server:
22
+ - Apache-Coyote/1.1
23
+ X-Content-Type-Options:
24
+ - nosniff
25
+ Content-Type:
26
+ - application/json;charset=utf-8
27
+ Content-Length:
28
+ - '1566'
29
+ Date:
30
+ - Wed, 28 Aug 2013 21:46:05 GMT
31
+ body:
32
+ encoding: US-ASCII
33
+ string: ! '{"commands":[{"name":"create-image","rel":"http://api.puppetlabs.com/razor/v1/commands/create-image","id":"http://localhost:8080/api/commands/create-image"},{"name":"delete-image","rel":"http://api.puppetlabs.com/razor/v1/commands/delete-image","id":"http://localhost:8080/api/commands/delete-image"},{"name":"create-installer","rel":"http://api.puppetlabs.com/razor/v1/commands/create-installer","id":"http://localhost:8080/api/commands/create-installer"},{"name":"create-tag","rel":"http://api.puppetlabs.com/razor/v1/commands/create-tag","id":"http://localhost:8080/api/commands/create-tag"},{"name":"create-broker","rel":"http://api.puppetlabs.com/razor/v1/commands/create-broker","id":"http://localhost:8080/api/commands/create-broker"},{"name":"create-policy","rel":"http://api.puppetlabs.com/razor/v1/commands/create-policy","id":"http://localhost:8080/api/commands/create-policy"}],"collections":[{"name":"brokers","rel":"http://api.puppetlabs.com/razor/v1/collections/brokers","id":"http://localhost:8080/api/collections/brokers"},{"name":"images","rel":"http://api.puppetlabs.com/razor/v1/collections/images","id":"http://localhost:8080/api/collections/images"},{"name":"tags","rel":"http://api.puppetlabs.com/razor/v1/collections/tags","id":"http://localhost:8080/api/collections/tags"},{"name":"policies","rel":"http://api.puppetlabs.com/razor/v1/collections/policies","id":"http://localhost:8080/api/collections/policies"},{"name":"nodes","rel":"http://api.puppetlabs.com/razor/v1/collections/nodes","id":"http://localhost:8080/api/collections/nodes"}]}'
34
+ http_version:
35
+ recorded_at: Wed, 28 Aug 2013 21:46:05 GMT
36
+ recorded_with: VCR 2.5.0
@@ -0,0 +1,102 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: http://localhost:8080/api/collections/tags
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept:
11
+ - application/json
12
+ Accept-Encoding:
13
+ - gzip, deflate
14
+ User-Agent:
15
+ - Ruby
16
+ response:
17
+ status:
18
+ code: 200
19
+ message: OK
20
+ headers:
21
+ Server:
22
+ - Apache-Coyote/1.1
23
+ X-Content-Type-Options:
24
+ - nosniff
25
+ Content-Type:
26
+ - application/json;charset=utf-8
27
+ Content-Length:
28
+ - '73'
29
+ Date:
30
+ - Wed, 29 Jan 2014 21:06:11 GMT
31
+ body:
32
+ encoding: US-ASCII
33
+ string: ! '{"spec":"http://api.puppetlabs.com/razor/v1/collections/tags","items":[]}'
34
+ http_version:
35
+ recorded_at: Wed, 29 Jan 2014 21:06:11 GMT
36
+ - request:
37
+ method: get
38
+ uri: http://fred:dead@localhost:8080/api
39
+ body:
40
+ encoding: US-ASCII
41
+ string: ''
42
+ headers:
43
+ Accept:
44
+ - application/json
45
+ Accept-Encoding:
46
+ - gzip, deflate
47
+ User-Agent:
48
+ - Ruby
49
+ response:
50
+ status:
51
+ code: 200
52
+ message: OK
53
+ headers:
54
+ Server:
55
+ - Apache-Coyote/1.1
56
+ X-Content-Type-Options:
57
+ - nosniff
58
+ Content-Type:
59
+ - application/json;charset=utf-8
60
+ Content-Length:
61
+ - '4491'
62
+ Date:
63
+ - Wed, 29 Jan 2014 21:14:20 GMT
64
+ body:
65
+ encoding: US-ASCII
66
+ string: ! '{"commands":[{"name":"create-repo","rel":"http://api.puppetlabs.com/razor/v1/commands/create-repo","id":"http://localhost:8080/api/commands/create-repo"},{"name":"delete-repo","rel":"http://api.puppetlabs.com/razor/v1/commands/delete-repo","id":"http://localhost:8080/api/commands/delete-repo"},{"name":"delete-node","rel":"http://api.puppetlabs.com/razor/v1/commands/delete-node","id":"http://localhost:8080/api/commands/delete-node"},{"name":"delete-policy","rel":"http://api.puppetlabs.com/razor/v1/commands/delete-policy","id":"http://localhost:8080/api/commands/delete-policy"},{"name":"update-node-metadata","rel":"http://api.puppetlabs.com/razor/v1/commands/update-node-metadata","id":"http://localhost:8080/api/commands/update-node-metadata"},{"name":"remove-node-metadata","rel":"http://api.puppetlabs.com/razor/v1/commands/remove-node-metadata","id":"http://localhost:8080/api/commands/remove-node-metadata"},{"name":"modify-node-metadata","rel":"http://api.puppetlabs.com/razor/v1/commands/modify-node-metadata","id":"http://localhost:8080/api/commands/modify-node-metadata"},{"name":"reinstall-node","rel":"http://api.puppetlabs.com/razor/v1/commands/reinstall-node","id":"http://localhost:8080/api/commands/reinstall-node"},{"name":"set-node-ipmi-credentials","rel":"http://api.puppetlabs.com/razor/v1/commands/set-node-ipmi-credentials","id":"http://localhost:8080/api/commands/set-node-ipmi-credentials"},{"name":"reboot-node","rel":"http://api.puppetlabs.com/razor/v1/commands/reboot-node","id":"http://localhost:8080/api/commands/reboot-node"},{"name":"set-node-desired-power-state","rel":"http://api.puppetlabs.com/razor/v1/commands/set-node-desired-power-state","id":"http://localhost:8080/api/commands/set-node-desired-power-state"},{"name":"create-task","rel":"http://api.puppetlabs.com/razor/v1/commands/create-task","id":"http://localhost:8080/api/commands/create-task"},{"name":"create-tag","rel":"http://api.puppetlabs.com/razor/v1/commands/create-tag","id":"http://localhost:8080/api/commands/create-tag"},{"name":"delete-tag","rel":"http://api.puppetlabs.com/razor/v1/commands/delete-tag","id":"http://localhost:8080/api/commands/delete-tag"},{"name":"update-tag-rule","rel":"http://api.puppetlabs.com/razor/v1/commands/update-tag-rule","id":"http://localhost:8080/api/commands/update-tag-rule"},{"name":"create-broker","rel":"http://api.puppetlabs.com/razor/v1/commands/create-broker","id":"http://localhost:8080/api/commands/create-broker"},{"name":"delete-broker","rel":"http://api.puppetlabs.com/razor/v1/commands/delete-broker","id":"http://localhost:8080/api/commands/delete-broker"},{"name":"create-policy","rel":"http://api.puppetlabs.com/razor/v1/commands/create-policy","id":"http://localhost:8080/api/commands/create-policy"},{"name":"move-policy","rel":"http://api.puppetlabs.com/razor/v1/commands/move-policy","id":"http://localhost:8080/api/commands/move-policy"},{"name":"enable-policy","rel":"http://api.puppetlabs.com/razor/v1/commands/enable-policy","id":"http://localhost:8080/api/commands/enable-policy"},{"name":"disable-policy","rel":"http://api.puppetlabs.com/razor/v1/commands/disable-policy","id":"http://localhost:8080/api/commands/disable-policy"},{"name":"add-policy-tag","rel":"http://api.puppetlabs.com/razor/v1/commands/add-policy-tag","id":"http://localhost:8080/api/commands/add-policy-tag"},{"name":"remove-policy-tag","rel":"http://api.puppetlabs.com/razor/v1/commands/remove-policy-tag","id":"http://localhost:8080/api/commands/remove-policy-tag"},{"name":"modify-policy-max-count","rel":"http://api.puppetlabs.com/razor/v1/commands/modify-policy-max-count","id":"http://localhost:8080/api/commands/modify-policy-max-count"}],"collections":[{"name":"brokers","rel":"http://api.puppetlabs.com/razor/v1/collections/brokers","id":"http://localhost:8080/api/collections/brokers"},{"name":"repos","rel":"http://api.puppetlabs.com/razor/v1/collections/repos","id":"http://localhost:8080/api/collections/repos"},{"name":"tags","rel":"http://api.puppetlabs.com/razor/v1/collections/tags","id":"http://localhost:8080/api/collections/tags"},{"name":"policies","rel":"http://api.puppetlabs.com/razor/v1/collections/policies","id":"http://localhost:8080/api/collections/policies"},{"name":"nodes","rel":"http://api.puppetlabs.com/razor/v1/collections/nodes","id":"http://localhost:8080/api/collections/nodes"},{"name":"tasks","rel":"http://api.puppetlabs.com/razor/v1/collections/tasks","id":"http://localhost:8080/api/collections/tasks"}]}'
67
+ http_version:
68
+ recorded_at: Wed, 29 Jan 2014 21:14:20 GMT
69
+ - request:
70
+ method: get
71
+ uri: http://fred:dead@localhost:8080/api/collections/tags
72
+ body:
73
+ encoding: US-ASCII
74
+ string: ''
75
+ headers:
76
+ Accept:
77
+ - application/json
78
+ Accept-Encoding:
79
+ - gzip, deflate
80
+ User-Agent:
81
+ - Ruby
82
+ response:
83
+ status:
84
+ code: 200
85
+ message: OK
86
+ headers:
87
+ Server:
88
+ - Apache-Coyote/1.1
89
+ X-Content-Type-Options:
90
+ - nosniff
91
+ Content-Type:
92
+ - application/json;charset=utf-8
93
+ Content-Length:
94
+ - '73'
95
+ Date:
96
+ - Wed, 29 Jan 2014 21:14:20 GMT
97
+ body:
98
+ encoding: US-ASCII
99
+ string: ! '{"spec":"http://api.puppetlabs.com/razor/v1/collections/tags","items":[]}'
100
+ http_version:
101
+ recorded_at: Wed, 29 Jan 2014 21:14:21 GMT
102
+ recorded_with: VCR 2.5.0