aws_minecraft 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/.gitignore +7 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +61 -0
- data/LICENSE +21 -0
- data/README.md +171 -0
- data/ROADMAP.md +46 -0
- data/Rakefile +10 -0
- data/aws_minecraft.gemspec +32 -0
- data/bin/aws_minecraft.rb +70 -0
- data/cfg/config.yml +2 -0
- data/cfg/ec2_conf.json +11 -0
- data/cfg/instances.sql +5 -0
- data/cfg/sg_config.json +20 -0
- data/cfg/user_data.sh +21 -0
- data/img/attach-detach.png +0 -0
- data/img/attach_no_session.png +0 -0
- data/img/attached_session.png +0 -0
- data/img/create.png +0 -0
- data/img/create_in_progress.png +0 -0
- data/img/stop_server.png +0 -0
- data/img/terminate.png +0 -0
- data/img/upload.png +0 -0
- data/lib/aws_minecraft.rb +117 -0
- data/lib/aws_minecraft/aws_helper.rb +150 -0
- data/lib/aws_minecraft/db_helper.rb +48 -0
- data/lib/aws_minecraft/mine_config.rb +15 -0
- data/lib/aws_minecraft/ssh_helper.rb +52 -0
- data/lib/aws_minecraft/upload_helper.rb +23 -0
- data/lib/aws_minecraft/version.rb +5 -0
- data/spec/aws_helper_spec.rb +98 -0
- data/spec/awsmine_spec.rb +134 -0
- data/spec/config_spec.rb +18 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/ssh_helper_spec.rb +0 -0
- data/spec/upload_helper_spec.rb +0 -0
- metadata +140 -0
data/cfg/config.yml
ADDED
data/cfg/ec2_conf.json
ADDED
data/cfg/instances.sql
ADDED
data/cfg/sg_config.json
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"ip_permissions": [
|
3
|
+
{
|
4
|
+
"ip_protocol": "tcp",
|
5
|
+
"from_port": 22,
|
6
|
+
"to_port": 22,
|
7
|
+
"ip_ranges": [{
|
8
|
+
"cidr_ip": "0.0.0.0/0"
|
9
|
+
}]
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"ip_protocol": "tcp",
|
13
|
+
"from_port": 25565,
|
14
|
+
"to_port": 25565,
|
15
|
+
"ip_ranges": [{
|
16
|
+
"cidr_ip": "0.0.0.0/0"
|
17
|
+
}]
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
data/cfg/user_data.sh
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
yum update -y
|
3
|
+
yum install git -y
|
4
|
+
yum install libevent-devel -y
|
5
|
+
yum install ncurses-devel -y
|
6
|
+
yum install glibc-static -y
|
7
|
+
yum install java-1.8.0-openjdk -y
|
8
|
+
yum groupinstall "Development tools" -y
|
9
|
+
cd ~
|
10
|
+
wget https://github.com/downloads/libevent/libevent/libevent-2.0.21-stable.tar.gz
|
11
|
+
tar xzvf libevent-2.0.21-stable.tar.gz
|
12
|
+
cd libevent-2.0.21-stable
|
13
|
+
./configure && make
|
14
|
+
make install
|
15
|
+
cd /home/ec2-user
|
16
|
+
wget https://github.com/tmux/tmux/releases/download/2.2/tmux-2.2.tar.gz
|
17
|
+
tar xfvz tmux-2.2.tar.gz
|
18
|
+
cd tmux-2.2
|
19
|
+
./configure && make
|
20
|
+
cd /home/ec2-user
|
21
|
+
chown -R ec2-user:ec2-user tmux-2.2
|
Binary file
|
Binary file
|
Binary file
|
data/img/create.png
ADDED
Binary file
|
Binary file
|
data/img/stop_server.png
ADDED
Binary file
|
data/img/terminate.png
ADDED
Binary file
|
data/img/upload.png
ADDED
Binary file
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'aws_minecraft/aws_helper'
|
2
|
+
require 'aws_minecraft/db_helper'
|
3
|
+
require 'aws_minecraft/upload_helper'
|
4
|
+
require 'aws_minecraft/mine_config'
|
5
|
+
require 'aws_minecraft/ssh_helper'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module AWSMine
|
9
|
+
# Main class for AWS Minecraft
|
10
|
+
class AWSMine
|
11
|
+
MINECRAFT_SESSION_NAME = 'minecraft'.freeze
|
12
|
+
attr_accessor :aws_helper, :db_helper, :upload_helper, :ssh_helper
|
13
|
+
def initialize
|
14
|
+
@aws_helper = AWSHelper.new
|
15
|
+
@db_helper = DBHelper.new
|
16
|
+
@upload_helper = UploadHelper.new
|
17
|
+
@ssh_helper = SSHHelper.new
|
18
|
+
@logger = Logger.new(STDOUT)
|
19
|
+
@logger.level = Logger.const_get(MineConfig.new.loglevel)
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_instance
|
23
|
+
if @db_helper.instance_exists?
|
24
|
+
ip, id = @db_helper.instance_details
|
25
|
+
@logger.info 'Instance already exists.'
|
26
|
+
state = @aws_helper.state(id)
|
27
|
+
@logger.info "State is: #{state}"
|
28
|
+
@logger.info "Public ip | id: #{ip} | #{id}"
|
29
|
+
return
|
30
|
+
end
|
31
|
+
ip, id = @aws_helper.create_ec2
|
32
|
+
@db_helper.store_instance(ip, id)
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_instance
|
36
|
+
unless @db_helper.instance_exists?
|
37
|
+
@logger.info 'No instances found. Nothing to do.'
|
38
|
+
return
|
39
|
+
end
|
40
|
+
ip, id = @db_helper.instance_details
|
41
|
+
@logger.info("Starting instance #{ip} | #{id}.")
|
42
|
+
new_ip = @aws_helper.start_ec2(id)
|
43
|
+
@db_helper.update_instance(new_ip, id)
|
44
|
+
end
|
45
|
+
|
46
|
+
def stop_instance
|
47
|
+
unless @db_helper.instance_exists?
|
48
|
+
@logger.info 'No running instances found. Nothing to do.'
|
49
|
+
return
|
50
|
+
end
|
51
|
+
ip, id = @db_helper.instance_details
|
52
|
+
@logger.info("Stopping instance #{ip} | #{id}.")
|
53
|
+
@aws_helper.stop_ec2(id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def terminate_instance
|
57
|
+
unless @db_helper.instance_exists?
|
58
|
+
@logger.info 'No running instances found. Nothing to do.'
|
59
|
+
return
|
60
|
+
end
|
61
|
+
@logger.info('WARNING! Terminating an instance will result in dataloss. Make ' \
|
62
|
+
'sure everything is backed up. Do you want to continue? (Y/n):')
|
63
|
+
answer = $stdin.gets.chomp
|
64
|
+
return if answer == 'n'
|
65
|
+
ip, id = @db_helper.instance_details
|
66
|
+
@logger.info("Terminating instance #{ip} | #{id}.")
|
67
|
+
@aws_helper.terminate_ec2(id)
|
68
|
+
@db_helper.remove_instance
|
69
|
+
end
|
70
|
+
|
71
|
+
def start_server(name)
|
72
|
+
@logger.info("Starting server: #{name}.")
|
73
|
+
cmd = "cd /home/ec2-user/data && ../tmux-2.2/tmux new -d -s #{MINECRAFT_SESSION_NAME} " \
|
74
|
+
"'echo eula=true > eula.txt && java -jar #{name} nogui'"
|
75
|
+
@logger.info("Running command: '#{cmd}'")
|
76
|
+
remote_exec(cmd)
|
77
|
+
ip, = @db_helper.instance_details
|
78
|
+
@logger.info("Server URL is: #{ip}:25565")
|
79
|
+
end
|
80
|
+
|
81
|
+
def stop_server
|
82
|
+
@logger.info('Stopping server')
|
83
|
+
ip, = @db_helper.instance_details
|
84
|
+
@ssh_helper.stop_server(ip)
|
85
|
+
end
|
86
|
+
|
87
|
+
def attach_to_server
|
88
|
+
@logger.info("Attaching to server: #{MINECRAFT_SESSION_NAME}.")
|
89
|
+
ip, = @db_helper.instance_details
|
90
|
+
@ssh_helper.attach_to_server(ip)
|
91
|
+
end
|
92
|
+
|
93
|
+
def init_db
|
94
|
+
@logger.info 'Creating db.'
|
95
|
+
@db_helper.init_db
|
96
|
+
@logger.info 'Done.'
|
97
|
+
end
|
98
|
+
|
99
|
+
def upload_world
|
100
|
+
end
|
101
|
+
|
102
|
+
def upload_files
|
103
|
+
ip, = @db_helper.instance_details
|
104
|
+
@upload_helper.upload_files(ip)
|
105
|
+
end
|
106
|
+
|
107
|
+
def remote_exec(cmd)
|
108
|
+
ip, = @db_helper.instance_details
|
109
|
+
@ssh_helper.remote_exec(ip, cmd)
|
110
|
+
end
|
111
|
+
|
112
|
+
def ssh
|
113
|
+
ip, = @db_helper.instance_details
|
114
|
+
@ssh_helper.ssh(ip)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'aws-sdk'
|
3
|
+
require 'logger'
|
4
|
+
require_relative 'mine_config'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module AWSMine
|
8
|
+
# Main wrapper for AWS commands
|
9
|
+
class AWSHelper
|
10
|
+
attr_accessor :ec2_client, :ec2_resource
|
11
|
+
def initialize
|
12
|
+
# region: AWS_REGION
|
13
|
+
# Credentials are loaded from environment properties
|
14
|
+
config = MineConfig.new
|
15
|
+
credentials = Aws::SharedCredentials.new(profile_name: config.profile)
|
16
|
+
@ec2_client = Aws::EC2::Client.new(credentials: credentials)
|
17
|
+
@ec2_resource = Aws::EC2::Resource.new(client: @ec2_client)
|
18
|
+
@logger = Logger.new(STDOUT)
|
19
|
+
@logger.level = Logger.const_get(config.loglevel)
|
20
|
+
end
|
21
|
+
|
22
|
+
# rubocop:disable Metrics/MethodLength
|
23
|
+
# rubocop:disable Metrics/AbcSize
|
24
|
+
def create_ec2
|
25
|
+
@logger.info('Creating new EC2 instance.')
|
26
|
+
config = File.open(File.join(__dir__, '../../cfg/ec2_conf.json'),
|
27
|
+
'rb', &:read).chop
|
28
|
+
@logger.debug("Configuration loaded: #{config}.")
|
29
|
+
ec2_config = symbolize(JSON.parse(config))
|
30
|
+
@logger.debug("Configuration symbolized: #{ec2_config}.")
|
31
|
+
@logger.info('Importing keys.')
|
32
|
+
import_keypair
|
33
|
+
@logger.info('Creating security group.')
|
34
|
+
sg_id = create_security_group
|
35
|
+
ec2_config[:security_group_ids] = [sg_id]
|
36
|
+
ec2_config[:user_data] = retrieve_user_data
|
37
|
+
@logger.info('Creating instance.')
|
38
|
+
instance = @ec2_resource.create_instances(ec2_config)[0]
|
39
|
+
@logger.info('Instance created. Waiting for it to become available.')
|
40
|
+
@ec2_resource.client.wait_until(:instance_status_ok,
|
41
|
+
instance_ids: [instance.id]) do |w|
|
42
|
+
w.before_wait do |_, _|
|
43
|
+
@logger << '.'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
@logger.info("\n")
|
47
|
+
@logger.info('Instance in running state.')
|
48
|
+
pub_ip = @ec2_resource.instances(instance_ids: [instance.id]).first
|
49
|
+
@logger.info('Instance started with ip | id: ' \
|
50
|
+
"#{pub_ip.public_ip_address} | #{instance.id}.")
|
51
|
+
[pub_ip.public_ip_address, instance.id]
|
52
|
+
end
|
53
|
+
|
54
|
+
def terminate_ec2(id)
|
55
|
+
@ec2_client.terminate_instances(dry_run: false, instance_ids: [id])
|
56
|
+
@ec2_resource.client.wait_until(:instance_terminated,
|
57
|
+
instance_ids: [id]) do |w|
|
58
|
+
w.before_wait do |_, _|
|
59
|
+
@logger << '.'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
@logger.info("\n")
|
63
|
+
@logger.info('Instance terminated. Goodbye.')
|
64
|
+
end
|
65
|
+
|
66
|
+
def stop_ec2(id)
|
67
|
+
@ec2_client.stop_instances(dry_run: false, instance_ids: [id])
|
68
|
+
@ec2_resource.client.wait_until(:instance_stopped,
|
69
|
+
instance_ids: [id]) do |w|
|
70
|
+
w.before_wait do |_, _|
|
71
|
+
@logger << '.'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
@logger.info("\n")
|
75
|
+
@logger.info('Instance stopped. Goodbye.')
|
76
|
+
end
|
77
|
+
|
78
|
+
def start_ec2(id)
|
79
|
+
@ec2_client.start_instances(dry_run: false, instance_ids: [id])
|
80
|
+
@ec2_resource.client.wait_until(:instance_running,
|
81
|
+
instance_ids: [id]) do |w|
|
82
|
+
w.before_wait do |_, _|
|
83
|
+
@logger << '.'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
pub_ip = @ec2_resource.instances(instance_ids: [id]).first
|
87
|
+
@logger.info("Instance started. New ip is:#{pub_ip.public_ip_address}.")
|
88
|
+
pub_ip.public_ip_address
|
89
|
+
end
|
90
|
+
|
91
|
+
def state(id)
|
92
|
+
instance = @ec2_resource.instances(instance_ids: [id]).first
|
93
|
+
@logger.debug("Response from describe_instances: #{instance}.")
|
94
|
+
instance.state.name
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def symbolize(obj)
|
100
|
+
case obj
|
101
|
+
when Hash
|
102
|
+
return obj.inject({}) do |memo, (k, v)|
|
103
|
+
memo.tap { |m| m[k.to_sym] = symbolize(v) }
|
104
|
+
end
|
105
|
+
when Array
|
106
|
+
return obj.map { |memo| symbolize(memo) }
|
107
|
+
else
|
108
|
+
obj
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def import_keypair
|
113
|
+
key = Base64.decode64(File.open(File.join(__dir__, '../../cfg/minecraft.key'),
|
114
|
+
'rb', &:read).chop)
|
115
|
+
begin
|
116
|
+
@ec2_client.describe_key_pairs(key_names: ['minecraft_keys'])
|
117
|
+
key_exists = true
|
118
|
+
rescue Aws::EC2::Errors::InvalidKeyPairNotFound
|
119
|
+
key_exists = false
|
120
|
+
end
|
121
|
+
return if key_exists
|
122
|
+
resp = @ec2_client.import_key_pair(dry_run: false,
|
123
|
+
key_name: 'minecraft_keys',
|
124
|
+
public_key_material: key)
|
125
|
+
@logger.debug("Response from import_key_pair: #{resp}.")
|
126
|
+
end
|
127
|
+
|
128
|
+
def create_security_group
|
129
|
+
config = File.open(File.join(__dir__, '../../cfg/sg_config.json'),
|
130
|
+
'rb', &:read).chop
|
131
|
+
sg_config = symbolize(JSON.parse(config))
|
132
|
+
begin
|
133
|
+
sg = @ec2_resource.create_security_group(dry_run: false,
|
134
|
+
group_name: 'mine_group',
|
135
|
+
description: 'minecraft_group')
|
136
|
+
sg.authorize_ingress(sg_config)
|
137
|
+
sg.id
|
138
|
+
rescue Aws::EC2::Errors::InvalidGroupDuplicate
|
139
|
+
@logger.info('Security Group already exists. Returning id.')
|
140
|
+
@ec2_resource.security_groups(group_names: ['mine_group']).first.id
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def retrieve_user_data
|
145
|
+
user_data = File.open(File.join(__dir__, '../../cfg/user_data.sh'),
|
146
|
+
'rb', &:read).chop
|
147
|
+
Base64.encode64(user_data)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
module AWSMine
|
3
|
+
# Initializes the database. The format to use if there are more tables
|
4
|
+
# is simple. Just create a SQL file corresponding to the table name
|
5
|
+
# and add that table to the @tables instance variable.
|
6
|
+
class DBHelper
|
7
|
+
def initialize
|
8
|
+
@db = SQLite3::Database.new 'minecraft.db'
|
9
|
+
@tables = %w(instances)
|
10
|
+
end
|
11
|
+
|
12
|
+
def table_exists?(table)
|
13
|
+
retrieved = @db.execute <<-SQL
|
14
|
+
SELECT name FROM sqlite_master WHERE type='table' AND name='#{table}';
|
15
|
+
SQL
|
16
|
+
return false if retrieved.nil? || retrieved.empty?
|
17
|
+
retrieved.first.first == table
|
18
|
+
end
|
19
|
+
|
20
|
+
def init_db
|
21
|
+
@tables.each do |table|
|
22
|
+
sql = File.open(File.join(__dir__, "../../cfg/#{table}.sql"),
|
23
|
+
'rb', &:read).chop
|
24
|
+
@db.execute sql unless table_exists? table
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def instance_details
|
29
|
+
@db.execute('SELECT ip, id FROM instances;').first
|
30
|
+
end
|
31
|
+
|
32
|
+
def instance_exists?
|
33
|
+
!@db.execute('SELECT id FROM instances;').empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def store_instance(ip, id)
|
37
|
+
@db.execute "INSERT INTO instances VALUES ('#{ip}', '#{id}');"
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_instance(ip, id)
|
41
|
+
@db.execute "UPDATE instances SET ip='#{ip}' WHERE id='#{id}';"
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_instance
|
45
|
+
@db.execute 'DELETE FROM instances;'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module AWSMine
|
4
|
+
# MineConfig is a configuration loader
|
5
|
+
class MineConfig
|
6
|
+
attr_reader :loglevel, :profile, :upload_path
|
7
|
+
def initialize
|
8
|
+
config = YAML.load_file(File.join(__dir__, '../../cfg/config.yml'))
|
9
|
+
@loglevel = config['loglevel']
|
10
|
+
@profile = ENV.fetch('AWS_DEFAULT_PROFILE', 'default')
|
11
|
+
# Upload path is used so files can sit anywhere.
|
12
|
+
@upload_path = config['upload_path']
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require_relative 'mine_config'
|
3
|
+
|
4
|
+
module AWSMine
|
5
|
+
# Main wrapper for SSH commands
|
6
|
+
class SSHHelper
|
7
|
+
def initialize
|
8
|
+
config = MineConfig.new
|
9
|
+
@logger = Logger.new(STDOUT)
|
10
|
+
@logger.level = Logger.const_get(config.loglevel)
|
11
|
+
end
|
12
|
+
|
13
|
+
def attach_to_server(ip)
|
14
|
+
exec("ssh ec2-user@#{ip} -t 'cd /home/ec2-user && ./tmux-2.2/tmux attach -t #{AWSMine::MINECRAFT_SESSION_NAME}'")
|
15
|
+
end
|
16
|
+
|
17
|
+
def ssh(ip)
|
18
|
+
exec("ssh ec2-user@#{ip}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def remote_exec(host, cmd)
|
22
|
+
@logger.debug("Executing '#{cmd}' on '#{host}'.")
|
23
|
+
# This should work if ssh key is loaded and AgentFrowarding is set to yes.
|
24
|
+
Net::SSH.start(host, 'ec2-user', config: true) do |ssh|
|
25
|
+
output = ssh.exec!(cmd)
|
26
|
+
@logger.info output
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop_server(host)
|
31
|
+
Net::SSH.start(host, 'ec2-user', config: true) do |ssh|
|
32
|
+
@logger.info('Opening channel to host.')
|
33
|
+
channel = ssh.open_channel do |ch|
|
34
|
+
@logger.info('Channel opened. Opening pty.')
|
35
|
+
ch.request_pty do |c, success|
|
36
|
+
unless success
|
37
|
+
@logger.info('Failed to request channel.')
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
c.on_data do |_, data|
|
41
|
+
puts "Received data: #{data}."
|
42
|
+
end
|
43
|
+
c.exec("cd /home/ec2-user && ./tmux-2.2/tmux attach -t #{AWSMine::MINECRAFT_SESSION_NAME}")
|
44
|
+
@logger.info('Sending stop signal...')
|
45
|
+
c.send_data("stop\n")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
channel.wait
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|