aws_minecraft 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ loglevel: INFO
2
+ upload_path: /Users/hannibal/RubyProjects/aws_minecraft/drop
@@ -0,0 +1,11 @@
1
+ {
2
+ "dry_run": false,
3
+ "image_id": "ami-ea26ce85",
4
+ "key_name": "minecraft_keys",
5
+ "min_count": 1,
6
+ "max_count": 1,
7
+ "instance_type": "t2.nano",
8
+ "monitoring": {
9
+ "enabled": true
10
+ }
11
+ }
@@ -0,0 +1,5 @@
1
+ create table instances (
2
+ ip varchar(100),
3
+ id varchar(100),
4
+ PRIMARY KEY (id)
5
+ );
@@ -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
+ }
@@ -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
Binary file
Binary file
Binary file
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