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