Permalink
Browse files

- simulate user server with docker to test setup server worker and au…

…toshutdown minecraft worker (related to github issue #21)

- update readme
- refactored env.sh
  • Loading branch information...
Raekye committed Feb 3, 2015
1 parent 324eb99 commit 17a61455c988423aa6029b3e8526f46a9b249c82
@@ -16,4 +16,6 @@
/tmp

env.sh
coverage/
/coverage
/test-docker/id_rsa
/test-docker/id_rsa.pub
@@ -6,30 +6,19 @@ Digital Ocean is used as the backend/hosting service, due to cost, reliability,
Gamocosm works well for friends who play together, but not 24/7.
Running a server 14 hours a week (2 hours every day) may cost 40 cents a month, instead of $5.

### Minecraft Server Wrapper
## Minecraft Server Wrapper
The [Minecraft Server Wrapper][4] (for lack of a better name) is a light python webserver.
It provides an HTTP API for starting and stopping Minecraft servers, downloading the world, etc.
Please check it out and help improve it too!

### Gamocosm Minecraft Flavours
## Gamocosm Minecraft Flavours
The [gamocosm-minecraft-flavours][10] repository includes the setup scripts used to install different flavours of Minecraft on a new server.
Read this [wiki page][11] for adding support for new flavours, or manually installing something yourself.

### Contributing
## Contributing
Pull requests are welcome!

#### Tests

- `./env.sh rake test` for everything (uses API token from "env.sh")
- `./env.sh rake test:functionals test:units` for local tests
- If nothing fails tests should delete everything they create.

- Because there's a lot of infrastructure (see below, "Technical details"), sometimes tests will fail for random reasons
- I have not found a good workaround for this
- Run `RAILS_ENV=test ./env.sh rails [s|c]` to run the server or console (respectively) in test mode
- Note: the test server does not automatically reload source files when you edit them. You must restart the server

#### Setting up your development environment
### Setting up your development environment
You should have a Unix/Linux system.
The following instructions were made for Fedora 20, but the steps should be similar on other distributions.

@@ -39,15 +28,16 @@ The following instructions were made for Fedora 20, but the steps should be simi
1. Install Bundler: `gem install bundler`
1. Install gem dependencies: `bundle install`
1. Run `cp env.sh.template env.sh`
1. Run `chmod u+x env.sh`
1. Enter config in `env.sh`
1. Initialize postgresql: `(sudo) postgresql-setup initdb`
1. Start postgresql, memcached, and redis manually: `(sudo) service start postgresql/memcached/redis`, or enable them to start at boot time: `(sudo) service enable postgresql/memcached/redis`
1. After configuring the database, run `./env.sh rake db:setup`
1. Start the server: `./env.sh rails s`
1. Start the Sidekiq worker: `./env.sh sidekiq`
1. After configuring the database, run `./run.sh rake db:setup`
1. Start the server: `./run.sh rails s`
1. Start the Sidekiq worker: `./run.sh sidekiq`

##### env.sh options
### run.sh and env.sh options
`run.sh` and `tests.sh` both source `env.sh` for environment variables/configuration.
`run.sh` also does `bundle exec` for you, so you just do `./run.sh GEM ARGS ...`.

- `DIGITAL_OCEAN_API_KEY`: your Digital Ocean api token
- `DIGITAL_OCEAN_SSH_PUBLIC_KEY_PATH`: ssh key to be added to new servers to SSH into
@@ -69,7 +59,7 @@ The following instructions were made for Fedora 20, but the steps should be simi
- `DEVELOPER_EMAILS`: comma separated list of emails to send exceptions to
- `BADNESS_SECRET`: secret to protect `/badness` endpoint

##### Database configuration
### Database configuration
Locate `pg_hba.conf`. On Fedora this is in `/var/lib/pgsql/data/`.
This file tells postgresql how to authenticate users. Read about it on the [PostgreSQL docs][1].
To restart postgresql: `(sudo) service postgresql restart`
@@ -106,23 +96,22 @@ Depending on what method you want to use, add the following under the line that

Example: `local postgres,gamocosm_development,gamocosm_test,gamocosm_production gamocosm md5`

#### Technical details
### Technical details
Hmmmm.

##### Data
#### Data
- Gamocosm has a lot of infrastructure: Digital Ocean's API, Digital Ocean servers/droplets, Minecraft and the server wrapper, the Gamocosm rails server, and the Gamocosm sidekiq worker
- Avoid state whenever possible; less chance of corruption with less data
- Idempotency is good

##### Error handling

#### Error handling
- Methods that "do things" should return nil on success, or a message or object on error.
- Methods that "return things" should use `.error!` to mark a return value is an error. These errors should always be strings.
- You can use `.error?` to check if a return value is an error. `nil` cannot be made an error.
- These methods are defined on `Object` in `config/initializers/my_extensions.rb`
- I prefer only throwing exceptions in "exceptional cases", not when I expect something to go wrong (e.g. user input).

##### Important checks
#### Important checks
- `server.remote.exists?`: `!server.remote_id.nil?`
- `server.remote.error?`: whether there was an error or not retrieving info about a droplet from Digital Ocean
- true if the user is missing his Digital Ocean API token, or if it's invalid
@@ -133,7 +122,7 @@ Hmmmm.
- `minecraft.node.error?`: error communicating with Minecraft wrapper on server
- `minecraft.running?`: `server.running? && !node.error? && node.pid > 0` (notice symmetry with `server.running?`)

##### Background workers
#### Background workers
- Idempotent
- Use `ActiveRecord::Base.connection_pool.with_connection do |conn|` if threads (e.g. teimout) access the database
- Run finite amount of times (keep track of how many times looped)
@@ -144,16 +133,35 @@ Hmmmm.

#### Other useful stuff
- Development/test user (from `db/seed.rb`): email "test@test.com", password "1234test", has the Digital Ocean api token from `env.sh`
- the current tests don't use this, and mock all HTTP requests/responses
- The Sidekiq web interface is mounted at `/sidekiq`
- Sidekiq doesn't automatically reload source files when you edit them. You must restart it for changes to take effect
- New Relic RPM is available in developer mode at `/newrelic`
- Run the console: `./env.sh rails c`
- Reset the database: `./env.sh rake db:reset`
- Run the console: `./run.sh rails c`
- Reset the database: `./run.sh rake db:reset`
- Reset Sidekiq jobs: `Sidekiq::Queue.new.each { |job| job.delete }` in the rails console
- Reset Sidekiq stats: `Sidekiq::Stats.new.reset` in the rails console
- The deployment scripts and configuration are in the `sysadmin/` directory
- List of `rake db` commands: [Stack Overflow][3]

## Tests
- `./run.sh rake test` or `./tests.sh`
- tests use WebMock to mock http requests (no external requests)
- `RAILS_ENV=test ./run.sh rails [s|c]` to run the server or console (respectively) in test mode
- Note: the test server, unlike the dev server, does not automatically reload source files when you change them

### More testing by simulating a user server with Docker
Without a server to connect to, Gamocosm can't try SetupServerWorker or AutoshutdownMinecraftWorker.
"test-docker/" contains a Dockerfile for building a basic Fedora container with an SSH server (simulating a bare Digital Ocean server).
If you set `$TEST_DOCKER` to "true", the tests will assume there is a running Docker Gamocosm container to connect to.

`tests.sh` will build the image, start the container, and delete the container for you if you specify to use Docker.
Otherwise, it will run the tests normally (equivalent to `./run.sh rake test`).
You should have non-root access to Docker.
You could also manage Docker yourself; you can look at the `tests.sh` file for reference.

Example: `TEST_DOCKER=true ./tests.sh`

### Credits
- Special thanks to [geetfun][2] who helped with the original development
- [SuperMarioBro][7] for helping iron out some initial bugs, adding support for more Minecraft flavours
@@ -5,6 +5,17 @@ class SetupServerWorker
include Sidekiq::Worker
sidekiq_options retry: 0

SYSTEM_PACKAGES = [
'yum-plugin-security',
'firewalld',
'java-1.7.0-openjdk-headless',
'python3',
'python3-devel',
'python3-pip',
'git',
'tmux',
]

def perform(user_id, server_id, times = 0)
user = User.find(user_id)
server = Server.find(server_id)
@@ -89,7 +100,7 @@ def base_install(user, server, host)
execute :echo, '/swapfile none swap defaults 0 0', '>>', '/etc/fstab'
end
server.update_columns(remote_setup_stage: 2)
execute :yum, '-y', 'install', 'yum-plugin-security', 'java-1.7.0-openjdk-headless', 'python3', 'python3-devel', 'python3-pip', 'git', 'tmux'
execute :yum, '-y', 'install', *SYSTEM_PACKAGES
execute :yum, '-y', 'update', '--security'
execute 'firewall-cmd', '--add-port=5000/tcp'
execute 'firewall-cmd', '--permanent', '--add-port=5000/tcp'
@@ -26,16 +26,3 @@ export DEVISE_SECRET_KEY=1234abc
export SECRET_KEY_BASE=
export DEVELOPER_EMAILS=
export BADNESS_SECRET=

if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then
PATH="$PATH:$HOME/bin"
fi

if [[ "$RAILS_ENV" == "production" ]] && [[ "$1" != "--bundler" ]]; then
ruby "$@"
else
if [[ "$1" == "--bundler" ]]; then
shift
fi
bundle exec "$@"
fi
16 run.sh
@@ -0,0 +1,16 @@
#!/bin/bash

source env.sh

if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then
PATH="$PATH:$HOME/bin"
fi

if [[ "$RAILS_ENV" == "production" ]] && [[ "$1" != "--bundler" ]]; then
ruby "$@"
else
if [[ "$1" == "--bundler" ]]; then
shift
fi
bundle exec "$@"
fi
@@ -49,7 +49,6 @@
mkdir tmp
touch tmp/restart.txt
cp env.sh.template env.sh
chmod u+x env.sh
chown -R http:http .

sudo -u http gem install bundler
@@ -64,9 +63,9 @@
vi env.sh
# no more sed -i "/SIDEKIQ_ADMIN_PASSWORD/ s/=.*$/=$SIDEKIQ_ADMIN_PASSWORD/" env.sh :(

su - http -c "cd $(pwd) && RAILS_ENV=production ./env.sh --bundler rake db:setup"
su - http -c "cd $(pwd) && RAILS_ENV=production ./run.sh --bundler rake db:setup"

su - http -c "cd $(pwd) && RAILS_ENV=production ./env.sh --bundler rake assets:precompile"
su - http -c "cd $(pwd) && RAILS_ENV=production ./run.sh --bundler rake assets:precompile"

OUTDOORS_IP_ADDRESS=$(ifconfig | grep -m 1 "inet" | awk "{ print \$2 }")
echo "$OUTDOORS_IP_ADDRESS gamocosm.com" >> /etc/hosts
@@ -65,7 +65,6 @@ git checkout release
mkdir tmp
touch tmp/restart.txt
cp env.sh.template env.sh
chmod u+x env.sh
chown -R http:http .

sudo -u http gem install bundler
@@ -80,9 +79,9 @@ read -p "Please fill in the information in env.sh (press any key to continue)...
vi env.sh
# no more sed -i "/SIDEKIQ_ADMIN_PASSWORD/ s/=.*$/=$SIDEKIQ_ADMIN_PASSWORD/" env.sh :(

su - http -c "cd $(pwd) && RAILS_ENV=production ./env.sh --bundler rake db:setup"
su - http -c "cd $(pwd) && RAILS_ENV=production ./run.sh --bundler rake db:setup"

su - http -c "cd $(pwd) && RAILS_ENV=production ./env.sh --bundler rake assets:precompile"
su - http -c "cd $(pwd) && RAILS_ENV=production ./run.sh --bundler rake assets:precompile"

OUTDOORS_IP_ADDRESS=$(ifconfig | grep -m 1 "inet" | awk "{ print \$2 }")
echo "$OUTDOORS_IP_ADDRESS gamocosm.com" >> /etc/hosts
@@ -1,5 +1,5 @@
server {
passenger_ruby /var/www/gamocosm/env.sh;
passenger_ruby /var/www/gamocosm/run.sh;
listen 80;
server_name gamocosm.com;
passenger_enabled on;
@@ -13,10 +13,10 @@ git checkout release
git pull origin release

bundle install
RAILS_ENV=production ./env.sh --bundler rake assets:precompile
RAILS_ENV=production ./env.sh --bundler rake db:migrate
RAILS_ENV=test ./env.sh rake db:migrate
./env.sh rake db:migrate
RAILS_ENV=production ./run.sh --bundler rake assets:precompile
RAILS_ENV=production ./run.sh --bundler rake db:migrate
RAILS_ENV=test ./run.sh rake db:migrate
./run.sh rake db:migrate

touch tmp/restart.txt

@@ -1,13 +1,24 @@
FROM fedora:20
MAINTAINER Raekye

RUN yum -y update
RUN yum -y install openssh-server
RUN yum -y install wget
RUN yum -y install yum-plugin-security firewalld java-1.7.0-openjdk-headless python3 python3-devel python3-pip git tmux
RUN ssh-keygen -t rsa -N '' -f /etc/ssh/ssh_host_rsa_key

RUN mkdir /root/.ssh
RUN chmod 700 /root/.ssh

ADD id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 644 /root/.ssh/authorized_keys

RUN echo 'PATH="$HOME/bin:$PATH"' >> /root/.bashrc
RUN mkdir /root/bin
ADD swapon.sh /root/bin/swapon
RUN chmod u+x /root/bin/swapon
ADD firewall-cmd.sh /root/bin/firewall-cmd
RUN chmod u+x /root/bin/firewall-cmd
ADD systemctl.sh /root/bin/systemctl
RUN chmod u+x /root/bin/systemctl

ENTRYPOINT ["/usr/sbin/sshd", "-D"]
@@ -0,0 +1,2 @@
#!/bin/bash
echo 'In Docker container; skipping firewall-cmd.'
@@ -0,0 +1,2 @@
#!/bin/bash
echo 'In Docker container; skipping swapon.'
@@ -0,0 +1,8 @@
#!/bin/bash
echo 'In Docker container; patching systemctl.'
if [[ "$1" == "start" ]]; then
cd /home/mcuser/minecraft
echo | python3 /opt/gamocosm/minecraft-server_wrapper.py daemonize mcsw.pid --auth=/opt/gamocosm/mcsw-auth.txt > /dev/null 2>&1
sleep 2
curl -d '{"ram": "512M"}' "http://gamocosm-mothership:$(sed -n 2p /opt/gamocosm/mcsw-auth.txt)@localhost:5000/start"
fi
@@ -31,19 +31,6 @@ def user_digital_ocean_after!
end

test "a lot of things (\"test everything\" - so it goes)" do
if ENV['TEST_REAL'] == 'true'
begin
WebMock.allow_net_connect!
do_a_lot_of_things
ensure
WebMock.disable_net_connect!
end
else
do_a_lot_of_things
end
end

def do_a_lot_of_things
mock_digital_ocean_base(200, [], [], [])
user_digital_ocean_before!
login_user('test@test.com', '1234test')
@@ -210,8 +197,11 @@ def track_sidekiq_worker(worker, perform_in, max_times)
end

def wait_for_autoshutdown_server(minecraft)
if have_user_server_for_test?
if have_user_server?
sleep 64
track_sidekiq_worker('AutoshutdownMinecraftWorker', 0, 16)
# workers do Server.find, here uses minecraft.server
minecraft.reload
else
assert_equal 1, AutoshutdownMinecraftWorker.jobs.count, "Bad number of AutoshutdownMinecraftWorker jobs: #{AutoshutdownMinecraftWorker.jobs}"
AutoshutdownMinecraftWorker.jobs.clear
@@ -230,15 +220,15 @@ def wait_for_starting_server(minecraft)
track_sidekiq_worker('WaitForStartingServerWorker', 0, 32)
mock_minecraft_running(200, minecraft, 1)
mock_minecraft_properties(200, minecraft, { })
if have_user_server_for_test?
if have_user_server?
track_sidekiq_worker('SetupServerWorker', 0, 16)
else
assert_equal 1, SetupServerWorker.jobs.count, "More than 1 SetupServerWorker jobs: #{SetupServerWorker.jobs}"
SetupServerWorker.jobs.clear
StartMinecraftWorker.perform_in(0.seconds, minecraft.server.id)
end
track_sidekiq_worker('StartMinecraftWorker', 0, 1)
# workers do Server.find, here uses minceraft.server
# workers do Server.find, here uses minecraft.server
minecraft.reload
assert minecraft.server.remote.exists?, 'Minecraft server remote does not exist'
assert_not minecraft.server.remote.error?, "Minecraft server remote error: #{minecraft.server.remote.error}"
@@ -256,7 +246,7 @@ def wait_for_stopping_server(minecraft)
track_sidekiq_worker('WaitForStoppingServerWorker', 0, 16)
mock_digital_ocean_action_after(mock_digital_ocean_action(200, 1, 1, 'in-progress').times(2).then, 200, 'completed')
track_sidekiq_worker('WaitForSnapshottingServerWorker', 0, 32)
# workers do Server.find, here uses minceraft.server
# workers do Server.find, here uses minecraft.server
minecraft.reload
assert_not minecraft.server.remote.exists?, 'Minecraft server remote exists'
assert_not minecraft.server.busy?, "Minecraft server busy: #{minecraft.inspect}, #{minecraft.server.inspect}"
@@ -24,8 +24,8 @@ class ActiveSupport::TestCase

# Add more helper methods to be used by all tests here...

def have_user_server_for_test?
return ENV['TEST_REAL'] == 'true' || ENV['TEST_DOCKER'] == 'true'
def have_user_server?
return ENV['TEST_DOCKER'] == 'true'
end

# WebMock base helpers
Oops, something went wrong.

0 comments on commit 17a6145

Please sign in to comment.