asteroid 0.0.1
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.
- 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
|