quandl_sandbox 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +48 -0
- data/lib/quandl/sandbox/attributes.rb +52 -0
- data/lib/quandl/sandbox/configuration.rb +44 -0
- data/lib/quandl/sandbox/dataset.rb +45 -0
- data/lib/quandl/sandbox/ec2.rb +49 -0
- data/lib/quandl/sandbox/job.rb +37 -0
- data/lib/quandl/sandbox/repository.rb +67 -0
- data/lib/quandl/sandbox/server/attributes.rb +57 -0
- data/lib/quandl/sandbox/server/instance.rb +116 -0
- data/lib/quandl/sandbox/server/ssh.rb +64 -0
- data/lib/quandl/sandbox/server.rb +9 -0
- data/lib/quandl/sandbox/version.rb +5 -0
- data/lib/quandl/sandbox.rb +34 -0
- data/spec/lib/quandl/sandbox/configuration_spec.rb +26 -0
- data/spec/lib/quandl/sandbox/ec2_spec.rb +8 -0
- data/spec/lib/quandl/sandbox/job_spec.rb +29 -0
- data/spec/lib/quandl/sandbox/repository_spec.rb +21 -0
- data/spec/lib/quandl/sandbox/server_spec.rb +66 -0
- data/spec/spec_helper.rb +38 -0
- metadata +225 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 191b24f362d9b5c0144dd44ffcf008ae39d84cad
|
4
|
+
data.tar.gz: 9c33e859a5685e26b2d28e2f0b95d5b5d967b697
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8b309b71cccc432a123e7025a4a0d5a7a8217d84f1d62ea9cdb41e1f4b23b98e031038722e92e3a245490df825c926dfa293aabb5758a05193539bc772b5d921
|
7
|
+
data.tar.gz: 6d802be7e7936959ef883453ed55a1e868ea020c63197f59d144103a8624d2cd154aa58ffabbe1c94c0608404be835c216ca327b02fa12f27991ad1673eb3078
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012-2013 Blake Hilscher
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Installation
|
2
|
+
|
3
|
+
```ruby
|
4
|
+
|
5
|
+
gem 'quandl_sandbox'
|
6
|
+
|
7
|
+
```
|
8
|
+
|
9
|
+
|
10
|
+
# Configuration
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
|
14
|
+
require 'quandl/sandbox'
|
15
|
+
|
16
|
+
Quandl::Sandbox.config do |c|
|
17
|
+
c.gateway = '10.110.110.10'
|
18
|
+
c.gateway_user = 'ubuntu'
|
19
|
+
c.subnet_id = 'subnet-a1a345b7'
|
20
|
+
c.environment = 'staging'
|
21
|
+
c.webhoook = 'http://example.com/api/v1/execute/callback'
|
22
|
+
end
|
23
|
+
|
24
|
+
```
|
25
|
+
|
26
|
+
|
27
|
+
# Execution
|
28
|
+
|
29
|
+
#### build and execute job in sandbox
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
|
33
|
+
sw = Quandl::Sandbox::Repository.new(
|
34
|
+
job_uid: 'xyz',
|
35
|
+
repository: 'http://github.com/blakehilscher/goog_scraper/',
|
36
|
+
reference: 'master' )
|
37
|
+
|
38
|
+
sw.execute
|
39
|
+
|
40
|
+
```
|
41
|
+
|
42
|
+
j = Job.find_by_uid('xyz').status
|
43
|
+
=> 'executing
|
44
|
+
|
45
|
+
sleep(10)
|
46
|
+
|
47
|
+
Job.find_by_uid('xyz').status
|
48
|
+
=> 'retrieved'
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Quandl
|
2
|
+
module Sandbox
|
3
|
+
|
4
|
+
module Attributes
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
|
10
|
+
@attributes = []
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def attributes
|
17
|
+
@attributes
|
18
|
+
end
|
19
|
+
|
20
|
+
def define_attributes(*names)
|
21
|
+
names.each do |name|
|
22
|
+
name = name.to_sym
|
23
|
+
next if @attributes.include?(name)
|
24
|
+
@attributes << name
|
25
|
+
attr_accessor(name)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(*args)
|
32
|
+
self.attributes = args.extract_options!
|
33
|
+
super if defined?(super)
|
34
|
+
end
|
35
|
+
|
36
|
+
def attributes
|
37
|
+
self.class.attributes.inject({}){|m,name| m.merge({ name.to_sym => self.send(name) }) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def attributes=(pairs)
|
41
|
+
# ensure symbolic keys
|
42
|
+
pairs.symbolize_keys!
|
43
|
+
# assign each acceptable pair
|
44
|
+
self.class.attributes.each do |name|
|
45
|
+
self.send("#{name}=", pairs[name] ) if pairs[name]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Quandl::Sandbox
|
2
|
+
|
3
|
+
class << self
|
4
|
+
attr_accessor :configuration
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.configuration
|
8
|
+
@configuration ||= Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.configure
|
12
|
+
yield(configuration)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
class Configuration
|
17
|
+
|
18
|
+
include Quandl::Sandbox::Attributes
|
19
|
+
|
20
|
+
define_attributes :image, :instance_type, :availability_zone, :ssh_gateway,
|
21
|
+
:ssh_user, :subnet_id, :environment, :verbose, :webhoook, :tmp_dir,
|
22
|
+
:aws_access_key_id, :aws_secret_access_key, :key_name, :key_file
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@instance_type = 'm1.small'
|
26
|
+
@availability_zone = 'us-east-1c'
|
27
|
+
@ssh_user = 'ubuntu'
|
28
|
+
@environment ||= ( ENV['APP_ENV'] || ENV['RAILS_ENV'] || 'development' )
|
29
|
+
@verbose = ( @environment == 'production' ) ? false : true
|
30
|
+
@key_name = 'quandl_3'
|
31
|
+
@tmp_dir = File.join( Quandl::Sandbox.root, 'tmp/' )
|
32
|
+
end
|
33
|
+
|
34
|
+
def key_data
|
35
|
+
File.read(key_file)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_h
|
39
|
+
self.attributes
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Quandl::Sandbox::Dataset
|
2
|
+
|
3
|
+
attr_accessor :source_code, :code, :name, :description, :data, :column_names
|
4
|
+
|
5
|
+
def self.from_output(outputs)
|
6
|
+
outputs.split("----\n").collect do |output|
|
7
|
+
self.new(output)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(output)
|
12
|
+
rows = output.to_s.rstrip.split("\n")
|
13
|
+
self.full_code = rows.shift.strip
|
14
|
+
self.name = rows.shift.strip
|
15
|
+
self.description = rows.shift.strip
|
16
|
+
self.column_names = rows.shift
|
17
|
+
self.data = rows.join("\n").strip
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"<##{self.class.name}" +
|
22
|
+
[:full_code, :name, :description, :column_names].inject({}){|m,k| m[k] = self.send(k); m }.to_s +
|
23
|
+
" >"
|
24
|
+
end
|
25
|
+
|
26
|
+
def column_names=(names)
|
27
|
+
names = names.split(",").collect(&:strip) if names.is_a?(String)
|
28
|
+
@column_names = Array(names).flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
def full_code=(value)
|
32
|
+
value = value.split('/')
|
33
|
+
self.source_code = value[0]
|
34
|
+
self.code = value[1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def full_code
|
38
|
+
[source_code, code].join('/')
|
39
|
+
end
|
40
|
+
|
41
|
+
def data=(rows)
|
42
|
+
@data = Quandl::Data.new(rows)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Quandl::Sandbox::EC2
|
2
|
+
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def auto_retry(attempt = 3, &block)
|
6
|
+
block.call
|
7
|
+
rescue => e
|
8
|
+
Quandl::Logger.debug("auto_retry #{e} #{attempt}")
|
9
|
+
Quandl::Sandbox::EC2.gateway_reset!
|
10
|
+
attempt -= 1
|
11
|
+
auto_retry(attempt, &block) if attempt > 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def find(uid)
|
15
|
+
instances.filter('instance-id', uid ).collect{ |i| i }.first
|
16
|
+
end
|
17
|
+
|
18
|
+
def instances
|
19
|
+
interface.instances.filter('subnet-id', Sandbox.configuration.subnet_id )
|
20
|
+
end
|
21
|
+
|
22
|
+
def gateway_reset!
|
23
|
+
@gateway.shutdown!
|
24
|
+
@gateway = nil
|
25
|
+
rescue => err
|
26
|
+
Quandl::Logger.error("gateway_reset! #{err}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def gateway
|
30
|
+
@gateway ||= Net::SSH::Gateway.new( gateway_host, gateway_user )
|
31
|
+
end
|
32
|
+
|
33
|
+
def gateway_user
|
34
|
+
@gateway_user ||= Sandbox.configuration.ssh_gateway.split("@").first
|
35
|
+
end
|
36
|
+
|
37
|
+
def gateway_host
|
38
|
+
@gateway_host ||= Sandbox.configuration.ssh_gateway.split("@").last
|
39
|
+
end
|
40
|
+
|
41
|
+
def interface
|
42
|
+
@interface ||= AWS::EC2.new(
|
43
|
+
access_key_id: Sandbox.configuration.aws_access_key_id,
|
44
|
+
secret_access_key: Sandbox.configuration.aws_secret_access_key)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Quandl::Sandbox::Job
|
2
|
+
|
3
|
+
attr_accessor :repository, :server, :server_id, :output, :attempts
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
options = args.extract_options!.symbolize_keys!
|
7
|
+
self.repository = Quandl::Sandbox::Repository.new(options[:repository]) if options[:repository].present?
|
8
|
+
self.server = Quandl::Sandbox::Server.find( options[:server_id] ) if options[:server_id].present?
|
9
|
+
end
|
10
|
+
|
11
|
+
def datasets
|
12
|
+
@datasets ||= Quandl::Sandbox::Dataset.from_output( output[:stdout] )
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
# ensure the ssh daemon has started
|
17
|
+
server.ensure_connection_established!
|
18
|
+
# run commands that need to happen before importer
|
19
|
+
server.exec( repository.script.before_execute )
|
20
|
+
# execute importer
|
21
|
+
self.output = server.exec( repository.script.execute )
|
22
|
+
rescue => err
|
23
|
+
Quandl::Logger.error( err )
|
24
|
+
ensure
|
25
|
+
server.delete
|
26
|
+
end
|
27
|
+
|
28
|
+
def output=(value)
|
29
|
+
@datasets = nil
|
30
|
+
@output = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def server
|
34
|
+
@server ||= Quandl::Sandbox::Server.create
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class Quandl::Sandbox::Repository
|
2
|
+
|
3
|
+
include Quandl::Sandbox::Attributes
|
4
|
+
|
5
|
+
define_attributes :job_uid, :git, :ref
|
6
|
+
|
7
|
+
def git=(value)
|
8
|
+
@full_path = nil
|
9
|
+
@git = URI.escape( value.to_s )
|
10
|
+
end
|
11
|
+
|
12
|
+
def ref=(value)
|
13
|
+
@ref = URI.escape( value.to_s )
|
14
|
+
end
|
15
|
+
|
16
|
+
def full_path
|
17
|
+
@full_path ||= File.join( '/home/ubuntu', dirname )
|
18
|
+
end
|
19
|
+
|
20
|
+
def dirname
|
21
|
+
@dirname ||= git.to_s.parameterize
|
22
|
+
end
|
23
|
+
|
24
|
+
def script
|
25
|
+
@script ||= Script.new(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
class Script
|
29
|
+
|
30
|
+
attr_accessor :repository
|
31
|
+
|
32
|
+
def initialize(repo)
|
33
|
+
self.repository = repo
|
34
|
+
end
|
35
|
+
|
36
|
+
def before_execute
|
37
|
+
[ clone, checkout, pull, bundle ].join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute
|
41
|
+
"cd #{r.full_path}; bundle exec ruby import.rb"
|
42
|
+
end
|
43
|
+
|
44
|
+
def clone
|
45
|
+
%Q{git clone "#{r.git}" "#{r.full_path}"}
|
46
|
+
end
|
47
|
+
|
48
|
+
def checkout
|
49
|
+
%Q{ cd #{r.full_path}
|
50
|
+
git checkout -b "#{r.ref}"}
|
51
|
+
end
|
52
|
+
|
53
|
+
def pull
|
54
|
+
%Q{ cd #{r.full_path}; git pull origin "#{r.ref}" }
|
55
|
+
end
|
56
|
+
|
57
|
+
def bundle
|
58
|
+
"cd #{r.full_path}; bundle update"
|
59
|
+
end
|
60
|
+
|
61
|
+
def r
|
62
|
+
repository
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Quandl
|
2
|
+
module Sandbox
|
3
|
+
|
4
|
+
class Server
|
5
|
+
module Attributes
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Quandl::Sandbox::Attributes
|
11
|
+
|
12
|
+
define_attributes :image, :instance_type, :availability_zone,
|
13
|
+
:ssh_gateway, :ssh_user, :subnet_id, :environment, :node_name,
|
14
|
+
:key_name
|
15
|
+
|
16
|
+
attr_reader :new_record
|
17
|
+
|
18
|
+
def node_name
|
19
|
+
@node_name ||= "#{environment}-sandbox-worker-#{uid}"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(*args)
|
25
|
+
# is a new record
|
26
|
+
self.new_record = true
|
27
|
+
# default attributes
|
28
|
+
self.attributes = Quandl::Sandbox.configuration.to_h
|
29
|
+
# onwards
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def uid
|
34
|
+
@uid ||= SecureRandom.hex(6)
|
35
|
+
end
|
36
|
+
|
37
|
+
def tags
|
38
|
+
{
|
39
|
+
Name: node_name,
|
40
|
+
cluster_type: 'quandl_sandbox',
|
41
|
+
instance_type: 'quandl_ugc_worker',
|
42
|
+
instance_environment: environment,
|
43
|
+
vpc: ssh_gateway.present? ? 'true' : 'false'
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def new_record=(value)
|
50
|
+
@new_record = (value == true)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Quandl::Sandbox::Server::Instance
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
|
7
|
+
TIMEOUT = 60
|
8
|
+
|
9
|
+
delegate :status, :delete, :exists?, :subnet,
|
10
|
+
to: :instance, allow_nil: true
|
11
|
+
|
12
|
+
attr_reader :instance
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
def find_or_create(uid)
|
19
|
+
find(uid) || create
|
20
|
+
end
|
21
|
+
|
22
|
+
def find(uid)
|
23
|
+
# lookup instance
|
24
|
+
instance = Sandbox::EC2.find(uid)
|
25
|
+
# build server given instance
|
26
|
+
instance.present? ? self.new( instance: instance ) : nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def create(*args)
|
30
|
+
s = self.new(*args)
|
31
|
+
s.launch!
|
32
|
+
s
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(id)
|
36
|
+
i = Sandbox::EC2.find(id)
|
37
|
+
i.present? ? i.delete : false
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(*args)
|
43
|
+
# grab instance if present
|
44
|
+
instance = args.try(:last).try(:[], :instance)
|
45
|
+
# upwards
|
46
|
+
super
|
47
|
+
# assign instance if given
|
48
|
+
self.instance = instance if instance.present?
|
49
|
+
end
|
50
|
+
|
51
|
+
def private_ip_address
|
52
|
+
@private_ip_address ||= instance.try(:private_ip_address)
|
53
|
+
end
|
54
|
+
|
55
|
+
def instance_id
|
56
|
+
@instance_id ||= instance.try(:id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def running?
|
60
|
+
@running ||= (status == :running) ? true : nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def launch!
|
64
|
+
# only new servers can be launched
|
65
|
+
return false unless new_record
|
66
|
+
# create, tag, wait
|
67
|
+
self.instance = create_instance!
|
68
|
+
add_tags_to_instance
|
69
|
+
end
|
70
|
+
|
71
|
+
def await_instance_uninterruptedly
|
72
|
+
t1 = Time.now
|
73
|
+
Quandl::Logger.debug "#{instance_id}: Waiting for instance to start up ... "
|
74
|
+
Timeout::timeout(TIMEOUT) {
|
75
|
+
sleep 0.5 while status == :pending
|
76
|
+
}
|
77
|
+
Quandl::Logger.debug "#{instance_id}: Instance started. (#{t1.elapsed_ms})"
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
|
85
|
+
def instance=(instance)
|
86
|
+
raise( ArgumentError, "instance must be of class AWS::EC2::Instance") unless instance.kind_of?(AWS::EC2::Instance)
|
87
|
+
return false unless new_record
|
88
|
+
# assign instance
|
89
|
+
@instance = instance
|
90
|
+
assign_attributes_using_instance
|
91
|
+
self.new_record = false
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_instance!
|
95
|
+
Sandbox::EC2.instances.create({
|
96
|
+
image_id: image,
|
97
|
+
subnet_id: subnet_id,
|
98
|
+
availability_zone: availability_zone,
|
99
|
+
key_name: key_name,
|
100
|
+
instance_type: instance_type
|
101
|
+
})
|
102
|
+
end
|
103
|
+
|
104
|
+
def assign_attributes_using_instance
|
105
|
+
self.class.attributes.each do |name|
|
106
|
+
self.send("#{name}=", instance.send(name)) if instance.respond_to?(name)
|
107
|
+
end
|
108
|
+
@node_name = instance.tags['Name']
|
109
|
+
@uid = @node_name.to_s.split('-').last
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_tags_to_instance
|
113
|
+
tags.each{|key, value| instance.add_tag( key.to_s, value: value.to_s ) }
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Quandl::Sandbox::Server::SSH
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def ensure_connection_established!
|
6
|
+
Sandbox::EC2.auto_retry(3){ exec("hostname") }
|
7
|
+
end
|
8
|
+
|
9
|
+
def upload!(local_path, remote_path="/home/ubuntu/")
|
10
|
+
ssh do |tunnel|
|
11
|
+
tunnel.scp.upload!( local_path, remote_path, recursive: true ) do |ch, name, sent, total|
|
12
|
+
Quandl::Logger.debug("#{name}: #{sent}/#{total}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def exec(command)
|
18
|
+
exec_through_channel!(command)
|
19
|
+
end
|
20
|
+
|
21
|
+
def exec_through_channel!(command)
|
22
|
+
t1 = Time.now
|
23
|
+
result = { stdout: '', stderr: '' }
|
24
|
+
self.ssh do |ssh|
|
25
|
+
# run event loop until channel closes
|
26
|
+
channel = ssh.open_channel do |ch|
|
27
|
+
# command to execute
|
28
|
+
ch.exec command do |ch, success|
|
29
|
+
# fail fast
|
30
|
+
raise "could not execute command" unless success
|
31
|
+
# on_data is stdout
|
32
|
+
ch.on_data do |c, data|
|
33
|
+
$stdout.print data
|
34
|
+
result[:stdout] += data
|
35
|
+
end
|
36
|
+
# on_extended_data is stderr
|
37
|
+
ch.on_extended_data do |c, type, data|
|
38
|
+
$stderr.print data
|
39
|
+
result[:stderr] += data
|
40
|
+
end
|
41
|
+
# when command completes
|
42
|
+
ch.on_close { Quandl::Logger.debug("#{instance_id}: Hasta La Vista! (#{t1.elapsed_ms})") }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
channel.wait
|
46
|
+
end
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def ssh(&block)
|
51
|
+
# wait for server to be ready
|
52
|
+
await_instance_uninterruptedly unless running?
|
53
|
+
# establish connection
|
54
|
+
Quandl::Logger.debug "#{instance_id}: Waiting for sshd to start ... "
|
55
|
+
# execute block through tunnel
|
56
|
+
output = nil
|
57
|
+
Sandbox::EC2.gateway.ssh( private_ip_address, ssh_user,
|
58
|
+
key_data: Sandbox.configuration.key_data, keys_only: true ) do |ssh|
|
59
|
+
output = block.call(ssh)
|
60
|
+
end
|
61
|
+
output
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'quandl/sandbox/server/attributes'
|
2
|
+
require 'quandl/sandbox/server/instance'
|
3
|
+
require 'quandl/sandbox/server/ssh'
|
4
|
+
|
5
|
+
class Quandl::Sandbox::Server
|
6
|
+
include Quandl::Sandbox::Server::Attributes
|
7
|
+
include Quandl::Sandbox::Server::Instance
|
8
|
+
include Quandl::Sandbox::Server::SSH
|
9
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'quandl/sandbox/version'
|
2
|
+
require 'quandl/logger'
|
3
|
+
require 'quandl/operation'
|
4
|
+
|
5
|
+
require 'aws-sdk'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
|
9
|
+
require 'net/ssh/gateway'
|
10
|
+
require 'net/scp'
|
11
|
+
|
12
|
+
require "active_support"
|
13
|
+
require "active_support/inflector"
|
14
|
+
require "active_support/core_ext/hash"
|
15
|
+
require "active_support/core_ext/object"
|
16
|
+
|
17
|
+
require 'timeout'
|
18
|
+
require 'securerandom'
|
19
|
+
|
20
|
+
require 'quandl/sandbox/attributes'
|
21
|
+
require 'quandl/sandbox/configuration'
|
22
|
+
require 'quandl/sandbox/ec2'
|
23
|
+
require 'quandl/sandbox/job'
|
24
|
+
require 'quandl/sandbox/repository'
|
25
|
+
require 'quandl/sandbox/server'
|
26
|
+
require 'quandl/sandbox/dataset'
|
27
|
+
|
28
|
+
module Quandl
|
29
|
+
module Sandbox
|
30
|
+
def self.root
|
31
|
+
@root ||= File.expand_path(File.join(__FILE__, '../../../'))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Quandl::Sandbox::Configuration do
|
5
|
+
subject{ Quandl::Sandbox::Configuration.new }
|
6
|
+
|
7
|
+
[ :image, :instance_type, :availability_zone, :ssh_gateway,
|
8
|
+
:ssh_user, :subnet_id, :environment, :verbose, :webhoook ].each do |name|
|
9
|
+
|
10
|
+
it { should respond_to name }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#to_h" do
|
14
|
+
subject{ Quandl::Sandbox::Configuration.new.to_h }
|
15
|
+
its([:instance_type]){ should eq 'm1.small' }
|
16
|
+
its([:availability_zone]){ should eq 'us-east-1c' }
|
17
|
+
its([:ssh_user]){ should eq 'ubuntu' }
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#configuration" do
|
21
|
+
subject{ Quandl::Sandbox.configuration }
|
22
|
+
its(:aws_access_key_id){ should eq ENV['AWS_ACCESS_KEY_ID'] }
|
23
|
+
its(:aws_secret_access_key){ should eq ENV['AWS_SECRET_ACCESS_KEY'] }
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Quandl::Sandbox::Job do
|
5
|
+
let(:job){ Quandl::Sandbox::Job.new }
|
6
|
+
subject{ job }
|
7
|
+
describe "#datasets" do
|
8
|
+
before(:each){ job.stub(:output).and_return(stubbed_job_output) }
|
9
|
+
|
10
|
+
subject{ job.datasets }
|
11
|
+
|
12
|
+
its(:count){ should eq 4 }
|
13
|
+
|
14
|
+
describe "#first" do
|
15
|
+
subject{ job.datasets.first }
|
16
|
+
|
17
|
+
its(:source_code){ should eq "QUANDL_SANDBOX_TEST" }
|
18
|
+
its(:code){ should eq "DATASET_0"}
|
19
|
+
its(:name){ should eq "Test Dataset 0" }
|
20
|
+
its(:description){ should eq "Description of the dataset 0"}
|
21
|
+
its(:column_names){ should eq ['Date', 'High', 'Low', 'Average'] }
|
22
|
+
its(:full_code){ should eq "QUANDL_SANDBOX_TEST/DATASET_0"}
|
23
|
+
its('data.count'){ should eq 10 }
|
24
|
+
its('data.first.count'){ should eq 4 }
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Quandl::Sandbox::Repository do
|
5
|
+
|
6
|
+
let(:config){ Quandl::Sandbox.configuration }
|
7
|
+
let(:worker){ Quandl::Sandbox::Repository.new }
|
8
|
+
|
9
|
+
context "without arguments" do
|
10
|
+
subject{ worker }
|
11
|
+
its(:git){ should be_blank }
|
12
|
+
its(:ref){ should be_blank }
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with arguments" do
|
16
|
+
subject{ Quandl::Sandbox::Repository.new( git: 'github.com/test42/test84', ref: 'master') }
|
17
|
+
its(:git){ should eq 'github.com/test42/test84'}
|
18
|
+
its(:ref){ should eq 'master'}
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Quandl::Sandbox::Server do
|
5
|
+
|
6
|
+
let(:config){ Quandl::Sandbox.configuration }
|
7
|
+
let(:server){ Quandl::Sandbox::Server.new }
|
8
|
+
|
9
|
+
subject{ server }
|
10
|
+
|
11
|
+
Quandl::Sandbox::Server.attributes.each do |name|
|
12
|
+
its(name){ should eq config.send(name) } if Quandl::Sandbox.configuration.respond_to?(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#uid" do
|
16
|
+
subject{ server.uid }
|
17
|
+
its(:length){ should eq 12 }
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#node_name" do
|
21
|
+
subject{ server.node_name }
|
22
|
+
it{ should match /#{server.environment}/ }
|
23
|
+
it{ should match /sandbox-worker/ }
|
24
|
+
it{ should match /#{server.uid}/ }
|
25
|
+
end
|
26
|
+
|
27
|
+
context "with created server" do
|
28
|
+
|
29
|
+
before(:all) do
|
30
|
+
@server = Quandl::Sandbox::Server.create
|
31
|
+
end
|
32
|
+
|
33
|
+
subject{ @server }
|
34
|
+
|
35
|
+
its(:exists?){ should be_true }
|
36
|
+
its(:launch!){ should be_false }
|
37
|
+
its(:instance_id){ should be_present }
|
38
|
+
its(:subnet_id){ should eq Quandl::Sandbox.configuration.subnet_id }
|
39
|
+
|
40
|
+
describe ".find" do
|
41
|
+
it "should find the server" do
|
42
|
+
s = Quandl::Sandbox::Server.find(subject.instance_id)
|
43
|
+
s.exists?.should be_true
|
44
|
+
end
|
45
|
+
it "should not find servers outside of the subnet" do
|
46
|
+
s = Quandl::Sandbox::Server.find("i-a4002dc5")
|
47
|
+
s.should be_nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#exec" do
|
52
|
+
it "should get the hostname" do
|
53
|
+
# get internal_ip through exec('hostname')
|
54
|
+
internal_ip = subject.exec('hostname').rstrip.split('-')[1..-1].join('.')
|
55
|
+
# compare against private ip address
|
56
|
+
subject.private_ip_address.should eq internal_ip
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
after(:all) do
|
61
|
+
@server.delete rescue nil
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
2
|
+
|
3
|
+
require "rspec"
|
4
|
+
require 'factory_girl'
|
5
|
+
require 'quandl/data'
|
6
|
+
require 'quandl/fabricate'
|
7
|
+
require 'pry'
|
8
|
+
|
9
|
+
factory_dir = File.join( File.dirname(__FILE__), 'factories/**/*.rb' )
|
10
|
+
Dir.glob( factory_dir ).each{|f| require(f); puts f }
|
11
|
+
|
12
|
+
require 'quandl/sandbox'
|
13
|
+
|
14
|
+
Quandl::Logger.use File.join( File.dirname(__FILE__), '../log/test.log' )
|
15
|
+
|
16
|
+
Quandl::Sandbox.configure do |c|
|
17
|
+
c.ssh_gateway = ENV['QUANDL_SANDBOX_GATEWAY']
|
18
|
+
c.subnet_id = ENV['QUANDL_SANDBOX_SUBNET']
|
19
|
+
c.image = ENV['QUANDL_SANDBOX_IMAGE']
|
20
|
+
c.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
|
21
|
+
c.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
|
22
|
+
c.key_file = "/Users/blake/.ssh/quandl_3.pem"
|
23
|
+
c.environment = 'test'
|
24
|
+
end
|
25
|
+
|
26
|
+
RSpec.configure do |config|
|
27
|
+
config.include FactoryGirl::Syntax::Methods
|
28
|
+
end
|
29
|
+
|
30
|
+
def stubbed_job_output
|
31
|
+
4.times.collect do |i|
|
32
|
+
"QUANDL_SANDBOX_TEST/DATASET_#{i}\n" +
|
33
|
+
"Test Dataset #{i}\n" +
|
34
|
+
"Description of the dataset #{i}\n" +
|
35
|
+
"Date, High, Low, Average\n" +
|
36
|
+
"#{Quandl::Fabricate::Data.rand(rows: 10, columns: 3).to_csv}}"
|
37
|
+
end.join("----\n")
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: quandl_sandbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Blake Hilscher
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.14'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.14'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: factory_girl_rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: fivemat
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.2'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.2'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activemodel
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.2'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.2'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: aws-sdk
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.18'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.18'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: net-scp
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ~>
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 1.1.2
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ~>
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 1.1.2
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: net-ssh-gateway
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ~>
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.2.0
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ~>
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 1.2.0
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: quandl_data
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ~>
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '1.1'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ~>
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '1.1'
|
167
|
+
description: Launch and execute code inside an ec2 instance. Supports vpc private
|
168
|
+
subnet via ssh gateway. Use webhooks to notify success or failure.
|
169
|
+
email:
|
170
|
+
- blake@hilscher.ca
|
171
|
+
executables: []
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- lib/quandl/sandbox/attributes.rb
|
176
|
+
- lib/quandl/sandbox/configuration.rb
|
177
|
+
- lib/quandl/sandbox/dataset.rb
|
178
|
+
- lib/quandl/sandbox/ec2.rb
|
179
|
+
- lib/quandl/sandbox/job.rb
|
180
|
+
- lib/quandl/sandbox/repository.rb
|
181
|
+
- lib/quandl/sandbox/server/attributes.rb
|
182
|
+
- lib/quandl/sandbox/server/instance.rb
|
183
|
+
- lib/quandl/sandbox/server/ssh.rb
|
184
|
+
- lib/quandl/sandbox/server.rb
|
185
|
+
- lib/quandl/sandbox/version.rb
|
186
|
+
- lib/quandl/sandbox.rb
|
187
|
+
- LICENSE
|
188
|
+
- README.md
|
189
|
+
- spec/lib/quandl/sandbox/configuration_spec.rb
|
190
|
+
- spec/lib/quandl/sandbox/ec2_spec.rb
|
191
|
+
- spec/lib/quandl/sandbox/job_spec.rb
|
192
|
+
- spec/lib/quandl/sandbox/repository_spec.rb
|
193
|
+
- spec/lib/quandl/sandbox/server_spec.rb
|
194
|
+
- spec/spec_helper.rb
|
195
|
+
homepage: http://blake.hilscher.ca/
|
196
|
+
licenses:
|
197
|
+
- MIT
|
198
|
+
metadata: {}
|
199
|
+
post_install_message:
|
200
|
+
rdoc_options: []
|
201
|
+
require_paths:
|
202
|
+
- lib
|
203
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - '>='
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0'
|
208
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - '>='
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '0'
|
213
|
+
requirements: []
|
214
|
+
rubyforge_project:
|
215
|
+
rubygems_version: 2.1.10
|
216
|
+
signing_key:
|
217
|
+
specification_version: 4
|
218
|
+
summary: Launch and execute code inside an ec2 instance.
|
219
|
+
test_files:
|
220
|
+
- spec/lib/quandl/sandbox/configuration_spec.rb
|
221
|
+
- spec/lib/quandl/sandbox/ec2_spec.rb
|
222
|
+
- spec/lib/quandl/sandbox/job_spec.rb
|
223
|
+
- spec/lib/quandl/sandbox/repository_spec.rb
|
224
|
+
- spec/lib/quandl/sandbox/server_spec.rb
|
225
|
+
- spec/spec_helper.rb
|