triton-internal 0.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,53 @@
1
+ module Triton
2
+
3
+ # This module "magically" defines any constants as
4
+ # RemoteExceptions, so when the server send back
5
+ # error structures such as:
6
+ #
7
+ # {"code" => "WidgetInvalid", ...}
8
+ #
9
+ # it means you can:
10
+ #
11
+ # begin
12
+ # ...
13
+ # rescue Triton::RemoteException::WidgetInvalid
14
+ # end
15
+ #
16
+ # without having to exhaustively define all such exceptions beforehand
17
+ module RemoteExceptions
18
+ def self.const_missing(name)
19
+ klass = Class.new(RemoteException)
20
+ RemoteExceptions.const_set(name, klass)
21
+ end
22
+ end
23
+
24
+ #
25
+ # In tests, you can also `raise Triton::RemoteExceptions::WidgetFailure, 'the-message'
26
+ # and also `raise Triton::RemoteException, { "code" => 'WidgetFailure', 'message' => 'the-message' }
27
+ # with the same behaviour
28
+ #
29
+ class RemoteException < RuntimeError
30
+ def self.exception(payload)
31
+ if payload.is_a?(Hash) && payload.keys.include?('code')
32
+ const = Triton::RemoteExceptions.const_get(payload['code'].intern)
33
+ const.new(payload)
34
+ else
35
+ self.new(payload)
36
+ end
37
+ end
38
+
39
+ attr_reader :errors, :body, :code
40
+
41
+ def initialize(hash_or_string)
42
+ if hash_or_string.is_a?(Hash)
43
+ @errors = hash_or_string['errors']
44
+ @code = hash_or_string['code']
45
+ @body = hash_or_string
46
+ super(hash_or_string.fetch('message') { body.to_s })
47
+ else
48
+ super(hash_or_string)
49
+ end
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,3 @@
1
+ module Triton
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,50 @@
1
+ module Triton
2
+ class Vmapi < ApiBase
3
+
4
+ # The commit used to compile this itnerface:
5
+ # https://github.com/joyent/sdc-vmapi/blob/590a4e09c0c76093f66f4ff69d6b4fd66de09bb6/docs/index.md
6
+ #
7
+
8
+ self.name = "vmapi"
9
+
10
+ call('Ping', :method => :get, :path => '/ping')
11
+ call('ListVms', :method => :get, :path => '/vms' )
12
+ call('GetVm', :method => :get, :path => '/vms/:uuid')
13
+ call('CreateVm', :method => :post, :path => '/vms')
14
+ call("DeleteVm", :method => :delete, :path => '/vms/:uuid')
15
+
16
+ {
17
+ 'StartVm' => 'start',
18
+ 'StopVm' => 'stop',
19
+ 'RebootVm' => 'reboot',
20
+ 'ReprovisionVm' => 'reprovision',
21
+ 'UpdateVm' => 'update',
22
+ 'AddNics' => 'add_nics',
23
+ 'UpdateNics' => 'update_nics',
24
+ 'RemoveNics' => 'remove_nics',
25
+ 'CreateSnapshot' => 'create_snapshot',
26
+ 'DeleteSnapshot' => 'delete_snapshot',
27
+ 'RollbackSnapshot' => 'rollback_snapshot'
28
+ }.each do |api_name, action_name|
29
+ call(api_name, :method => :post, :path => "/vms/:uuid?action=#{action_name}")
30
+ end
31
+
32
+ # :type in the following calls must be one of 'tags', 'customer_metadata', or 'internal_metadata'
33
+ call("ListMetadata", :method => :get, :path => '/vms/:uuid/:type')
34
+ call("GetMetadata", :method => :get, :path => '/vms/:uuid/:type/:key')
35
+ call("AddMetadata", :method => :post, :path => '/vms/:uuid/:type', :body_param => 'metadata')
36
+ call("SetMetadata", :method => :put, :path => '/vms/:uuid/:type', :body_param => 'metadata')
37
+ call("DeleteMetadata", :method => :delete, :path => '/vms/:uuid/:type/:key')
38
+ call("DeleteAllMetadata", :method => :delete, :path => '/vms/:uuid/:type')
39
+ call("AddRoleTags", :method => :post, :path => '/vms/:uuid/role_tags')
40
+ call("SetRoleTags", :method => :put, :path => '/vms/:uuid/role_tags')
41
+ call("DeleteRoleTag", :method => :delete, :path => '/vms/:uuid/role_tags/:role_tag')
42
+ call("DeleteAllRoleTags", :method => :delete, :path => '/vms/:uuid/role_tags')
43
+
44
+ call("GetJobs", :method => :get, :path => '/jobs')
45
+ call("ListVmJobs", :method => :get, :path => '/vms/:uuid/jobs')
46
+ call("GetJob", :method => :get, :path => '/jobs/:uuid')
47
+ call("GetStatuses", :method => :get, :path => '/statuses')
48
+
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ require 'triton/version'
2
+ require 'rest-client'
3
+ require 'cgi'
4
+
5
+ module Triton
6
+
7
+ def self.suffix=(new_suffix)
8
+ @suffix = new_suffix
9
+ end
10
+ def self.suffix
11
+ @suffix
12
+ end
13
+
14
+
15
+ def self.socks=(new_socks)
16
+ if new_socks
17
+ server,port = new_socks.to_s.split(":")
18
+ if port.nil?
19
+ port = server
20
+ server = "127.0.0.1"
21
+ end
22
+ begin
23
+ require 'socksify'
24
+ TCPSocket::socks_server = server
25
+ TCPSocket::socks_port = port.to_i
26
+ rescue LoadError
27
+ $stderr.puts "Could not load 'socksify' gem. Socks forwarding won't work."
28
+ end
29
+
30
+ end
31
+ end
32
+
33
+ TestModeLeak = Class.new(RuntimeError)
34
+ def self.test_mode=(value)
35
+ @test_mode = value
36
+ end
37
+ def self.test_mode
38
+ @test_mode
39
+ end
40
+
41
+ def self.logger=(logger)
42
+ @logger = logger
43
+ end
44
+ def self.logger
45
+ @logger ||= Logger.new("/dev/null")
46
+ end
47
+
48
+ require 'triton/remote_exception'
49
+ require 'triton/api_base'
50
+ require 'triton/vmapi'
51
+ require 'triton/cnapi'
52
+ require 'triton/napi'
53
+ require 'triton/papi'
54
+ require 'triton/imgapi'
55
+ require 'triton/indifferent_hash'
56
+
57
+ end
@@ -0,0 +1,288 @@
1
+ require 'spec_helper'
2
+
3
+ describe "ApiMapping" do
4
+
5
+ let(:fakeapi) do
6
+ Class.new(Triton::ApiBase) do
7
+ self.name = "fakeapi"
8
+ call("TestMethod", {
9
+ :method => :post,
10
+ :path => "/testmethod/path"
11
+ })
12
+
13
+ call("GetMethod", {
14
+ :path => "/testmethod/path"
15
+ })
16
+
17
+ call("GetThing", {
18
+ :path => "/things/:thing/foo"
19
+ })
20
+
21
+ call("PostThing", {
22
+ :method => :post,
23
+ :path => "/things/:thing"
24
+ })
25
+
26
+ call("GetThingWithQueryString", {
27
+ :path => "/things/:thing/foo?a=b"
28
+ })
29
+
30
+ call("SetTags", {
31
+ :method => :post,
32
+ :path => "/thing/:id/tag",
33
+ :body_param => :metadata
34
+ })
35
+
36
+ end
37
+ end
38
+
39
+ describe "instance behaviour" do
40
+
41
+ subject { fakeapi.new("TestMethod") }
42
+
43
+ it "should memoise the request" do
44
+ first_request = subject.request
45
+ expect(subject.request.object_id).to eq(first_request.object_id)
46
+ end
47
+
48
+ it "should pass execute to the request" do
49
+ expect(subject.request).to receive(:execute).and_return("{}")
50
+ subject.execute
51
+ end
52
+
53
+ it "should have a shortcut to execute the method from the class" do
54
+ expect(fakeapi).to respond_to(:execute)
55
+ end
56
+ end
57
+
58
+ describe "init - when the call isn't defined" do
59
+ it "should raise an exception if the call isn't recognised" do
60
+ expect { fakeapi.new("Unknown") }.to raise_exception(Triton::ApiBase::UnknownCall)
61
+ end
62
+
63
+ it "should be a NoMethodError" do
64
+ expect(Triton::ApiBase::UnknownCall.new).to be_a(NoMethodError)
65
+ end
66
+ end
67
+
68
+ describe "response handling" do
69
+ subject { fakeapi.new("TestMethod") }
70
+
71
+ it "should decode the payload as JSON" do
72
+ allow(subject.request).to receive(:execute).and_return('{"something": "here"}')
73
+ expect(subject.execute).to eq({"something" => "here"})
74
+ end
75
+
76
+ it "should return a hash with indifferent key access" do
77
+ allow(subject.request).to receive(:execute).and_return('{"something": "here", "nested": {"something": "inner"}}')
78
+ expect(subject.execute[:something]).to eq("here")
79
+ expect(subject.execute['something']).to eq("here")
80
+ expect(subject.execute[:nested][:something]).to eq("inner")
81
+ expect(subject.execute[:nested]['something']).to eq("inner")
82
+ end
83
+ end
84
+
85
+ describe "response exception handling" do
86
+ subject { fakeapi.new("TestMethod") }
87
+
88
+ let(:exception) { RestClient::Exception.new(double("Response", :body => "body"))}
89
+
90
+ before do
91
+ # Simulate a RestClient exception when we execute
92
+ allow(subject.request).to receive(:execute).and_raise(exception)
93
+ end
94
+
95
+ it "should re-raise the original RestClient exception if the body doesn't contain valid JSON" do
96
+ expect { subject.execute }.to raise_exception(exception)
97
+ end
98
+
99
+ it "should raise a Triton::RemoteException if the JSON decoded, but doesn't contain a 'code' field" do
100
+ allow(exception.response).to receive(:body).and_return("{}")
101
+ expect { subject.execute }.to raise_exception(Triton::RemoteException)
102
+ end
103
+
104
+ it "should synthesize an exception class if a 'code' is present" do
105
+ allow(exception.response).to receive(:body).and_return('{"code": "ItScrewedUp"}')
106
+ expect { subject.execute }.to raise_exception(Triton::RemoteExceptions::ItScrewedUp)
107
+ end
108
+
109
+ it "should be possible to raise a RemoteException with a struct" do
110
+ expect do
111
+ raise Triton::RemoteExceptions::RemoteException, { 'code' => 'ResourceNotFound', 'message' => 'VM not found'}
112
+ end.to raise_exception(Triton::RemoteExceptions::ResourceNotFound, 'VM not found')
113
+ end
114
+
115
+ it "should be possible to raise a RemoteException with a message directly" do
116
+ expect do
117
+ raise Triton::RemoteExceptions::ResourceNotFound, 'VM not found'
118
+ end.to raise_exception(Triton::RemoteExceptions::ResourceNotFound, 'VM not found')
119
+ end
120
+
121
+ it "should be possible to raise a RemoteException from a mock with a message in a test" do
122
+ object = Object.new
123
+ allow(object).to receive(:crash).and_raise(Triton::RemoteExceptions::ResourceNotFound, 'VM not found')
124
+
125
+ expect do
126
+ object.crash
127
+ end.to raise_exception(Triton::RemoteExceptions::ResourceNotFound, 'VM not found')
128
+ end
129
+ end
130
+
131
+ describe "request" do
132
+ subject { fakeapi.new("TestMethod").request }
133
+
134
+ it "should build a RestClient request" do
135
+ expect(subject).to be_a(RestClient::Request)
136
+ end
137
+
138
+ it "should use the specified name as the first part of the hostname" do
139
+ expect(subject.url).to match(%r{^http://fakeapi})
140
+ end
141
+
142
+ it "should append the configured suffix to the hostname" do
143
+ expect(subject.url).to match(%r{^http://fakeapi.test/})
144
+ end
145
+
146
+ it "should append the path to the url" do
147
+ expect(URI.parse(subject.url).path).to eq("/testmethod/path")
148
+ end
149
+
150
+
151
+ it "should set the method" do
152
+ expect(subject.method).to eq(:post)
153
+ end
154
+
155
+ it "should default the method to :get" do
156
+ expect(fakeapi.new("GetMethod").request.method).to eq(:get)
157
+ end
158
+ end
159
+
160
+ describe "path parametisation" do
161
+ it "should replace any :<word> values with the associated argument" do
162
+ expect(URI.parse(fakeapi.new("GetThing", :thing => "IDENT").request.url).path).to eq("/things/IDENT/foo")
163
+ end
164
+ it "should URL encode any value" do
165
+ expect(URI.parse(fakeapi.new("GetThing", :thing => "IDE/NT").request.url).path).to eq("/things/IDE%2FNT/foo")
166
+ end
167
+
168
+ it "should exclude any arguments used to build the URL from the query string of GET requests" do
169
+ request = fakeapi.new("GetThing", 'thing' => "123", :foo => "bar").request
170
+ expect(URI.parse(request.url).query).to eq("foo=bar")
171
+ end
172
+ it "should exclude any arguments used to build the URL from the payload of non-get requests" do
173
+ request = fakeapi.new("PostThing", 'thing' => "123", :foo => "bar").request
174
+ expect(URI.parse(request.url).path).to eq("/things/123")
175
+ expect(JSON.load(request.payload)).to eq("foo" => "bar")
176
+ end
177
+ end
178
+
179
+ it "should accept application/json" do
180
+ expect(fakeapi.new("GetThing", :thing => "IDENT").request.headers['Accept']).to eq("application/json")
181
+ expect(fakeapi.new("TestMethod").request.headers['Accept']).to eq("application/json")
182
+ end
183
+
184
+ describe "arguments - GET requests" do
185
+ subject { fakeapi.new("GetMethod", :foo=>"bar/baz", :bar=>"baz").request }
186
+ it "should urlencode and append to the URL as a query string" do
187
+ expect(URI.parse(subject.url).query).to eq("foo=bar%2Fbaz&bar=baz")
188
+ end
189
+
190
+ it "should append carefully if there is already a query string" do
191
+ url = fakeapi.new("GetThingWithQueryString", :thing => 1, :foo=>"bar", :bar=>"foo/bar").request.url
192
+ expect(URI.parse(url).query).to eq("a=b&foo=bar&bar=foo%2Fbar")
193
+
194
+ end
195
+
196
+ it "should not set a payload" do
197
+ expect(subject.payload).to be_nil
198
+ end
199
+
200
+ it "should not set a Content-Type header" do
201
+ expect(subject.headers["Content-Type"]).to be_nil
202
+ end
203
+ end
204
+
205
+ describe "arguments - non-GET" do
206
+ subject { fakeapi.new("TestMethod", 'some' => 'args', :live => :here).request }
207
+
208
+ it "should set the Content-Body to application/json" do
209
+ expect(subject.headers["Content-Type"]).to eq("application/json")
210
+ end
211
+ it "should serialize the arguments to JSON as the payload" do
212
+ expect(JSON.load(subject.payload)).to eq({'some' => 'args', 'live' => 'here'})
213
+ end
214
+
215
+ end
216
+
217
+ it "should generate a snake-case function call from the Api name" do
218
+ expect(fakeapi).to respond_to(:get_method)
219
+ end
220
+
221
+
222
+ describe "camelise" do
223
+ it "should return a camel_case string" do
224
+ expect("foo".camelise).to eq("Foo")
225
+ expect("foo_bar".camelise).to eq("FooBar")
226
+ expect("foo_bar_baz".camelise).to eq("FooBarBaz")
227
+ expect("foo______bar_baz".camelise).to eq("FooBarBaz")
228
+ end
229
+ it "should return nil if an unexpected string is encountered" do
230
+ expect("something-wrong".camelise).to eq(nil)
231
+ expect("Capsyousay".camelise).to eq(nil)
232
+ expect("wow!".camelise).to eq(nil)
233
+ end
234
+ end
235
+
236
+ describe "test_mode" do
237
+ before do
238
+ Triton.test_mode = true
239
+ end
240
+ it "should raise an exception if a request is attempted when test_mode is set" do
241
+ expect do
242
+ fakeapi.test_method()
243
+ end.to raise_exception(Triton::TestModeLeak)
244
+ end
245
+
246
+ it "should include the Api call name in the exception" do
247
+ expect do
248
+ fakeapi.test_method()
249
+ end.to raise_exception do |ex|
250
+ expect(ex.message).to include("TestMethod")
251
+ end
252
+ end
253
+
254
+ it "should include the call route in the exception" do
255
+ expect do
256
+ fakeapi.test_method()
257
+ end.to raise_exception do |ex|
258
+ expect(ex.message).to include("POST /testmethod/path")
259
+ end
260
+ end
261
+
262
+ it "should include the parameters in the exception" do
263
+ expect do
264
+ fakeapi.test_method("param" => "value")
265
+ end.to raise_exception do |ex|
266
+ expect(ex.message).to include(%{"param": "value"})
267
+ end
268
+ end
269
+ end
270
+
271
+
272
+ describe "body_param option" do
273
+
274
+ subject { fakeapi.new('SetTags', :id => "the-id", :other_param => "other_value", :key => 'value', :metadata => { "tag1" => "value1", "tag2" => "value2" })}
275
+
276
+ it "should write the value of the body_param to the body" do
277
+ expect(JSON.load(subject.request.payload)).to eq({ "tag1" => "value1", "tag2" => "value2" })
278
+ end
279
+
280
+ it "should set any additional parameters on the query-string" do
281
+ expect(URI.parse(subject.request.url).query).to eq("other_param=other_value&key=value")
282
+ end
283
+
284
+ end
285
+
286
+ end
287
+
288
+
@@ -0,0 +1,16 @@
1
+ require "bundler/setup"
2
+ require "triton/internal"
3
+
4
+ RSpec.configure do |config|
5
+ # Enable flags like --only-failures and --next-failure
6
+ config.example_status_persistence_file_path = ".rspec_status"
7
+
8
+ config.before do
9
+ Triton.test_mode = false
10
+ Triton.suffix = "test"
11
+ end
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Triton do
4
+ it "has a version number" do
5
+ expect(Triton::VERSION).not_to be nil
6
+ end
7
+
8
+ it "should have a configurable suffix" do
9
+ expect(Triton.suffix).to eq("test")
10
+ Triton.suffix = "foo"
11
+ expect(Triton.suffix).to eq("foo")
12
+ end
13
+
14
+ describe "socks config" do
15
+ it "should accept host:port" do
16
+ Triton.socks = "1.2.3.4:5678"
17
+ expect(TCPSocket.socks_server).to eq("1.2.3.4")
18
+ expect(TCPSocket.socks_port).to eq(5678)
19
+ end
20
+ it "should accept string 'port' and assume localhost" do
21
+ Triton.socks = "5678"
22
+ expect(TCPSocket.socks_server).to eq("127.0.0.1")
23
+ expect(TCPSocket.socks_port).to eq(5678)
24
+ end
25
+ it "should accept an integer port, and assume localhost" do
26
+ Triton.socks = 5678
27
+ expect(TCPSocket.socks_server).to eq("127.0.0.1")
28
+ expect(TCPSocket.socks_port).to eq(5678)
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'triton/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "triton-internal"
8
+ spec.version = Triton::VERSION
9
+ spec.authors = ["Thomas Haggett"]
10
+ spec.email = ["thomas-tritongem@haggett.org"]
11
+ spec.licenses = ['MIT']
12
+
13
+ spec.summary = %q{Library to wrap the Triton internal APIs}
14
+ spec.description = %q{Library that wraps all of the Joyent Triton Internal APIs in a consistent ruby interface allowing easier calling and mocking.}
15
+ spec.homepage = "http://thomas.haggett.org/"
16
+
17
+ spec.files = Dir.glob(File.expand_path("../**/*", __FILE__)).reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.14"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+
28
+ spec.add_dependency 'rest-client', "~> 1.8"
29
+ spec.add_dependency 'socksify', "~> 1.7"
30
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: triton-internal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Thomas Haggett
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rest-client
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: socksify
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ description: Library that wraps all of the Joyent Triton Internal APIs in a consistent
84
+ ruby interface allowing easier calling and mocking.
85
+ email:
86
+ - thomas-tritongem@haggett.org
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - "/Users/thomas/tdh/triton/Gemfile"
92
+ - "/Users/thomas/tdh/triton/Gemfile.lock"
93
+ - "/Users/thomas/tdh/triton/LICENSE"
94
+ - "/Users/thomas/tdh/triton/README.md"
95
+ - "/Users/thomas/tdh/triton/Rakefile"
96
+ - "/Users/thomas/tdh/triton/bin/console"
97
+ - "/Users/thomas/tdh/triton/bin/setup"
98
+ - "/Users/thomas/tdh/triton/lib/triton.rb"
99
+ - "/Users/thomas/tdh/triton/lib/triton/api_base.rb"
100
+ - "/Users/thomas/tdh/triton/lib/triton/cnapi.rb"
101
+ - "/Users/thomas/tdh/triton/lib/triton/imgapi.rb"
102
+ - "/Users/thomas/tdh/triton/lib/triton/indifferent_hash.rb"
103
+ - "/Users/thomas/tdh/triton/lib/triton/internal.rb"
104
+ - "/Users/thomas/tdh/triton/lib/triton/napi.rb"
105
+ - "/Users/thomas/tdh/triton/lib/triton/papi.rb"
106
+ - "/Users/thomas/tdh/triton/lib/triton/remote_exception.rb"
107
+ - "/Users/thomas/tdh/triton/lib/triton/version.rb"
108
+ - "/Users/thomas/tdh/triton/lib/triton/vmapi.rb"
109
+ - "/Users/thomas/tdh/triton/pkg/triton-internal-0.1.0.gem"
110
+ - "/Users/thomas/tdh/triton/spec/api_base_spec.rb"
111
+ - "/Users/thomas/tdh/triton/spec/spec_helper.rb"
112
+ - "/Users/thomas/tdh/triton/spec/triton_spec.rb"
113
+ - "/Users/thomas/tdh/triton/triton-internal.gemspec"
114
+ homepage: http://thomas.haggett.org/
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.5.1
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Library to wrap the Triton internal APIs
138
+ test_files: []