asteroid 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +9 -0
- data/Rakefile +11 -0
- data/asteroid.gemspec +33 -0
- data/bin/asteroid +17 -0
- data/generator/README.md +1 -0
- data/generator/Rakefile +0 -0
- data/generator/asteroid/servers/default.yml +3 -0
- data/generator/config/asteroid.rb +9 -0
- data/generator/gitignore +2 -0
- data/generator/secrets/.keep +0 -0
- data/generator/secrets/config/providers.rb +7 -0
- data/generator/secrets/config/secrets.yml +5 -0
- data/lib/asteroid.rb +22 -0
- data/lib/asteroid/application.rb +32 -0
- data/lib/asteroid/config.rb +103 -0
- data/lib/asteroid/config_file.rb +46 -0
- data/lib/asteroid/generator.rb +96 -0
- data/lib/asteroid/instance.rb +113 -0
- data/lib/asteroid/instance/aster.rb +0 -0
- data/lib/asteroid/instance/command.rb +146 -0
- data/lib/asteroid/instance/scp.rb +17 -0
- data/lib/asteroid/instance/ssh.rb +29 -0
- data/lib/asteroid/instance/vars.rb +45 -0
- data/lib/asteroid/provider.rb +36 -0
- data/lib/asteroid/provider/abstract.rb +37 -0
- data/lib/asteroid/provider/digital_ocean.rb +66 -0
- data/lib/asteroid/provider/mock.rb +51 -0
- data/lib/asteroid/provider/virtual_box.rb +43 -0
- data/lib/asteroid/script.rb +64 -0
- data/lib/asteroid/server.rb +96 -0
- data/lib/asteroid/ssh_key.rb +41 -0
- data/lib/asteroid/template.rb +25 -0
- data/lib/asteroid/version.rb +3 -0
- data/test/helper.rb +8 -0
- data/test/unit/test_instance.rb +18 -0
- data/test/unit/test_server.rb +19 -0
- metadata +241 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
require_relative './instance/vars'
|
4
|
+
require_relative './instance/scp'
|
5
|
+
require_relative './instance/ssh'
|
6
|
+
require_relative './instance/command'
|
7
|
+
|
8
|
+
|
9
|
+
module Asteroid
|
10
|
+
|
11
|
+
class MissingAttributeError < StandardError
|
12
|
+
def initialize(name)
|
13
|
+
@attribute_name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute
|
17
|
+
@attribute_name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Instance
|
22
|
+
|
23
|
+
attr_accessor :logger
|
24
|
+
|
25
|
+
attr_reader :server,
|
26
|
+
:provider,
|
27
|
+
:type
|
28
|
+
|
29
|
+
|
30
|
+
def initialize(options = {})
|
31
|
+
@provider = options.delete :provider
|
32
|
+
@server = options.delete :server
|
33
|
+
@type = options.delete :type
|
34
|
+
|
35
|
+
@attributes = options
|
36
|
+
|
37
|
+
# Validate Attributes
|
38
|
+
require_attribute :name
|
39
|
+
require_attribute :id
|
40
|
+
|
41
|
+
provider.required_instance_attributes.each do |att|
|
42
|
+
require_attribute att
|
43
|
+
end
|
44
|
+
|
45
|
+
# The type is added on the name
|
46
|
+
@attributes[:type] ||= if @attributes[:name]
|
47
|
+
@attributes[:name].match(/[a-z]+/)[0].to_sym
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.all
|
52
|
+
instances = Provider.all.instances
|
53
|
+
instances.map do |info|
|
54
|
+
new info
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.first(type)
|
59
|
+
all_with_type(type).first
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.all_with_type(type)
|
63
|
+
all.select{|i| i.type == type}
|
64
|
+
end
|
65
|
+
|
66
|
+
def password(username = nil)
|
67
|
+
username ||= self["login.username"]
|
68
|
+
password_filename = File.join(Asteroid::Config.password_dir, "/instance_#{@id}-user_#{username}")
|
69
|
+
if File.exists? password_filename
|
70
|
+
File.read(password_filename)
|
71
|
+
else
|
72
|
+
password = SecureRandom.hex(64)
|
73
|
+
File.open(password_filename, 'w'){|f| f.write password}
|
74
|
+
password
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def server_defaults
|
79
|
+
server.instance_config
|
80
|
+
end
|
81
|
+
|
82
|
+
def server
|
83
|
+
@server ||= Server.named self.type.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.find(id)
|
87
|
+
all.select{|i| i.id == id}.first
|
88
|
+
end
|
89
|
+
|
90
|
+
def env
|
91
|
+
{
|
92
|
+
"INSTANCE_PRIVATE_KEY" => self.server.ssh_key_filename,
|
93
|
+
"INSTANCE_SSH_PORT" => self["ssh.port"].to_i,
|
94
|
+
"INSTANCE_SSH_LOGIN" => self["login.username"],
|
95
|
+
"INSTANCE_IP_ADDRESS" => self.ip_address,
|
96
|
+
"INSTANCE_ID" => self.id,
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def destroy
|
101
|
+
@provider.destroy_instance self
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def require_attribute(name)
|
107
|
+
if @attributes[name].nil?
|
108
|
+
raise MissingAttributeError.new(name)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
File without changes
|
@@ -0,0 +1,146 @@
|
|
1
|
+
|
2
|
+
module Asteroid
|
3
|
+
class Instance
|
4
|
+
|
5
|
+
def eval_command(cmd, env = nil)
|
6
|
+
env ||= template_data
|
7
|
+
cmd = Template.new(:erb).render(cmd, env)
|
8
|
+
cmd, *rest = cmd.split(" ")
|
9
|
+
case cmd.to_sym
|
10
|
+
when :run
|
11
|
+
self.ssh # reconnect
|
12
|
+
command_run(rest.first, env)
|
13
|
+
when :upload
|
14
|
+
from, to, _ = rest
|
15
|
+
command_upload(from, to, env)
|
16
|
+
when :"upload-private-key"
|
17
|
+
name, _ = rest
|
18
|
+
command_upload_private_key(name, env)
|
19
|
+
when :exec
|
20
|
+
script = rest.join(" ")
|
21
|
+
command_exec(script, env)
|
22
|
+
when :config
|
23
|
+
set_or_get, var, val, _ = rest
|
24
|
+
command_config(set_or_get, var, val)
|
25
|
+
else
|
26
|
+
if server.commands[cmd]
|
27
|
+
command_command(cmd, rest, env)
|
28
|
+
else
|
29
|
+
binding.pry
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def template_data
|
35
|
+
{
|
36
|
+
instance: self, server: server
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def command_command(cmd, args, env)
|
41
|
+
env ||= template_data
|
42
|
+
command = server.commands[cmd]
|
43
|
+
|
44
|
+
env = if command["args"]
|
45
|
+
command["args"].inject({}) do |r, k|
|
46
|
+
r[k] = args.shift
|
47
|
+
r
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
env = template_data.merge({
|
52
|
+
args: env
|
53
|
+
})
|
54
|
+
|
55
|
+
command["steps"].map do |step|
|
56
|
+
eval_command step, env
|
57
|
+
end.last
|
58
|
+
end
|
59
|
+
|
60
|
+
def command_run_yml_script(yml_script, env = nil)
|
61
|
+
env ||= template_data
|
62
|
+
filename = File.join(Asteroid::Config.script_dir, '/', yml_script)
|
63
|
+
script = File.read(filename)
|
64
|
+
yml = Template.new(:erb).render(script, env)
|
65
|
+
data = YAML::load yml
|
66
|
+
unless data["steps"]
|
67
|
+
raise "No steps in #{filenbame}"
|
68
|
+
else
|
69
|
+
data["steps"].each do |step|
|
70
|
+
eval_command step, env
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def aster_environment
|
76
|
+
@aster_environment ||= Aster::Environment.new.tap do |e|
|
77
|
+
e.define_function :run, [] do |arguments|
|
78
|
+
binding.pry
|
79
|
+
end
|
80
|
+
|
81
|
+
e.define_function :exec, [] do |arguments|
|
82
|
+
binding.pry
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def command_run_aster_script(aster_script, env = nil)
|
88
|
+
filename = File.join(Asteroid::Config.script_dir, '/', aster_script)
|
89
|
+
script = File.read(filename)
|
90
|
+
aster_environment.eval script
|
91
|
+
end
|
92
|
+
|
93
|
+
def command_run(script_name, env = nil)
|
94
|
+
env ||= template_data
|
95
|
+
script = Script.named(script_name)
|
96
|
+
|
97
|
+
if script.nil?
|
98
|
+
raise "No script named #{script_name}"
|
99
|
+
end
|
100
|
+
|
101
|
+
# Return early if we're running a script
|
102
|
+
if script.yml?
|
103
|
+
# TODO: send the fucking script obj
|
104
|
+
command_run_yml_script(script_name)
|
105
|
+
return
|
106
|
+
elsif script.aster?
|
107
|
+
command_run_aster_script(script_name)
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
if script.template?
|
113
|
+
script.set_data env
|
114
|
+
end
|
115
|
+
|
116
|
+
rendered_script = script.render
|
117
|
+
puts rendered_script
|
118
|
+
puts self.ssh_exec rendered_script
|
119
|
+
end
|
120
|
+
|
121
|
+
def command_upload(from, to, env = nil)
|
122
|
+
env ||= template_data
|
123
|
+
from = ConfigFile.new(from)
|
124
|
+
|
125
|
+
if from.template?
|
126
|
+
from.set_data env
|
127
|
+
end
|
128
|
+
|
129
|
+
puts from.filename
|
130
|
+
self.scp.upload! from.filename, to
|
131
|
+
end
|
132
|
+
|
133
|
+
def command_exec(command, env = nil)
|
134
|
+
self.ssh_exec command
|
135
|
+
end
|
136
|
+
|
137
|
+
def command_config(set_or_get, var, val)
|
138
|
+
if set_or_get.to_s == "set"
|
139
|
+
self.config_set(var, val)
|
140
|
+
elsif set_or_get == "get"
|
141
|
+
self.config_get(var)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'net/scp'
|
2
|
+
|
3
|
+
module Asteroid
|
4
|
+
class Instance
|
5
|
+
def scp
|
6
|
+
if @scp && (@scp.session.transport.port.to_s != self["ssh.port"] || (@scp.session.transport.options[:user] != self["login.username"]))
|
7
|
+
@scp = nil
|
8
|
+
end
|
9
|
+
@scp ||= Net::SCP.start(
|
10
|
+
ip_address,
|
11
|
+
self["login.username"],
|
12
|
+
port: self["ssh.port"].to_i,
|
13
|
+
:keys => [server.ssh_key_filename]
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Asteroid
|
4
|
+
class Instance
|
5
|
+
|
6
|
+
def ssh_exec(command)
|
7
|
+
ssh.exec!(command) do |channel, stream, data|
|
8
|
+
if stream == :stdout
|
9
|
+
puts data
|
10
|
+
else
|
11
|
+
puts data
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def ssh
|
17
|
+
if @ssh && (@ssh.transport.port.to_s != self["ssh.port"] || (@ssh.transport.options[:user] != self["login.username"]))
|
18
|
+
@ssh = nil
|
19
|
+
end
|
20
|
+
@ssh ||= Net::SSH.start(
|
21
|
+
ip_address,
|
22
|
+
self["login.username"],
|
23
|
+
port: self["ssh.port"].to_i,
|
24
|
+
:keys => [server.ssh_key_filename]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Asteroid
|
4
|
+
|
5
|
+
class Instance
|
6
|
+
|
7
|
+
def [](k)
|
8
|
+
val = config_get k
|
9
|
+
val.nil? ? server_defaults[k] : val
|
10
|
+
end
|
11
|
+
|
12
|
+
def []=(k, v)
|
13
|
+
config_set k, v
|
14
|
+
end
|
15
|
+
|
16
|
+
def config_get(k)
|
17
|
+
data = load_config_data
|
18
|
+
data && data[k]
|
19
|
+
end
|
20
|
+
|
21
|
+
def config_set(k, v)
|
22
|
+
data = load_config_data
|
23
|
+
data[k] = v
|
24
|
+
save_config_data data
|
25
|
+
v
|
26
|
+
end
|
27
|
+
|
28
|
+
def config_filename
|
29
|
+
File.join(Asteroid::Config.instance_config_dir, "/instance_#{@id}.yml")
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_config_data
|
33
|
+
begin
|
34
|
+
YAML::load_file config_filename
|
35
|
+
rescue
|
36
|
+
{}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def save_config_data(data)
|
41
|
+
File.open(config_filename, 'w') {|f| f.write data.to_yaml }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require_relative './provider/abstract'
|
3
|
+
require_relative './provider/digital_ocean'
|
4
|
+
require_relative './provider/virtual_box'
|
5
|
+
require_relative './provider/mock'
|
6
|
+
|
7
|
+
module Asteroid
|
8
|
+
|
9
|
+
class ProviderProxy
|
10
|
+
|
11
|
+
def method_missing(m, *args, &block)
|
12
|
+
Config.providers.map do |p|
|
13
|
+
p.send(m, *args, &block)
|
14
|
+
end.flatten
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
module Provider
|
20
|
+
|
21
|
+
def self.all
|
22
|
+
ProviderProxy.new
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class Config
|
28
|
+
class << self
|
29
|
+
attr_accessor :providers
|
30
|
+
def providers
|
31
|
+
@providers ||= []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# :name=>"web-1c5c2972",
|
2
|
+
# :image_id=>3101918,
|
3
|
+
# :size_id=>66,
|
4
|
+
# :region_id=>4,
|
5
|
+
# :backups_active=>false,
|
6
|
+
# :ip_address=>"107.170.109.79",
|
7
|
+
# :private_ip_address=>"10.128.197.52",
|
8
|
+
# :locked=>false,
|
9
|
+
# :status=>"active",
|
10
|
+
# :created_at=>"2014-04-12T20:44:53Z"}
|
11
|
+
|
12
|
+
module Asteroid
|
13
|
+
module Provider
|
14
|
+
class Abstract
|
15
|
+
|
16
|
+
def required_instance_attributes
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(config = {})
|
21
|
+
@config = config
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.type
|
25
|
+
self.to_s.split('::').last.underscore.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
def instances
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy_instance(instance)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Asteroid
|
2
|
+
module Provider
|
3
|
+
class DigitalOcean < Abstract
|
4
|
+
|
5
|
+
def initialize(config = {})
|
6
|
+
@config = config
|
7
|
+
Digitalocean.client_id = config[:client_id]
|
8
|
+
Digitalocean.api_key = config[:api_key]
|
9
|
+
end
|
10
|
+
|
11
|
+
def sizes
|
12
|
+
Digitalocean::Size.all.sizes.map do |d|
|
13
|
+
d.marshal_dump.merge(
|
14
|
+
provider: self
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def instances
|
20
|
+
response = Digitalocean::Droplet.all
|
21
|
+
response.droplets.map do |d|
|
22
|
+
d.marshal_dump.merge(
|
23
|
+
provider: self
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_instance(server)
|
29
|
+
instance = Digitalocean::Droplet.create(
|
30
|
+
name: server.instance_name,
|
31
|
+
size_id: server.attributes[:size_id],
|
32
|
+
image_id: server.attributes[:image_id],
|
33
|
+
region_id: server.attributes[:region_id],
|
34
|
+
ssh_key_ids: server.attributes[:ssh_key_ids],
|
35
|
+
private_networking: true
|
36
|
+
)
|
37
|
+
|
38
|
+
if instance.status == "OK"
|
39
|
+
Instance.new instance.droplet.merge(provider: self)
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy_instance(instance)
|
46
|
+
Digitalocean::Droplet.destroy instance.id
|
47
|
+
end
|
48
|
+
|
49
|
+
def ssh_keys
|
50
|
+
response = Digitalocean::SshKey.all
|
51
|
+
response.ssh_keys.map do |d|
|
52
|
+
d.marshal_dump.merge(
|
53
|
+
provider: self
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_ssh_key(config)
|
59
|
+
end
|
60
|
+
|
61
|
+
def reboot_instance(instance)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|