aws_minecraft 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.
- 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
|