migrant-boxes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ script/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in migrant.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/migrant/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
10
+ guard 'cucumber' do
11
+ watch(%r{^features/.+\.feature$})
12
+ watch(%r{^features/support/.+$}) { 'features' }
13
+ watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
14
+ end
data/README.markdown ADDED
@@ -0,0 +1,182 @@
1
+ Migrant
2
+ =======
3
+
4
+ Migrant is a tool to take your Vagrant boxes to the 'cloud'.
5
+
6
+ Migrant is designed to be a simple, focused tool to take boxes that are
7
+ working locally with Vagrant and deploy them remotely. Migrant reads
8
+ most configuration and provisioning information from Vagrant's
9
+ configuration files and then applies the same approach to remote 'cloud'
10
+ servers.
11
+
12
+ Installation
13
+ ------------
14
+
15
+ ```
16
+ $ gem install migrant-boxes
17
+ ```
18
+
19
+ Or in a `Gemfile`:
20
+
21
+ ```ruby
22
+ gem 'migrant-boxes'
23
+ ```
24
+
25
+ Examples
26
+ ========
27
+
28
+ For single box configurations, the minimum that needs to be specified
29
+ are provider access credentials and the location of the SSH keys to use
30
+ to access the box.
31
+
32
+ Minimal AWS Configuration
33
+ -------------------------
34
+
35
+ This sets up a 'micro' sized aws box.
36
+
37
+ ```ruby
38
+ Configuration.for('migrant') {
39
+ provider {
40
+ name 'aws'
41
+ access_key ENV['AWS_ACCESS_KEY']
42
+ secret_key ENV['AWS_SECRET_KEY']
43
+ }
44
+ ssh {
45
+ public_key '~/.ssh/id_rsa.pub'
46
+ private_key '~/.ssh/id_rsa'
47
+ }
48
+ }
49
+ ```
50
+
51
+ Minimal Rackspace Configuration
52
+ -------------------------------
53
+
54
+ This sets up an instance with 256MB ram
55
+
56
+ ```ruby
57
+ Configuration.for('migrant') {
58
+ provider {
59
+ name 'rackspace'
60
+ user_name ENV['RACKSPACE_ACCESS_KEY']
61
+ api_key ENV['AWS_SECRET_KEY']
62
+ }
63
+ ssh {
64
+ public_key '~/.ssh/id_rsa.pub'
65
+ private_key '~/.ssh/id_rsa'
66
+ }
67
+ }
68
+ ```
69
+
70
+ This will create the smallest possible server instance and run the provisioners
71
+ that are defined in the Vagrantfile. It will use your ssh keys to
72
+ access the box, and write some state information about instances to
73
+ `config/migrant.yml`
74
+
75
+ Migrant is highly opinionated: it will use the latest LTS Ubuntu
76
+ release. It will also download and install Ruby 1.9 from source in
77
+ `/usr/local`.
78
+
79
+ Customizing AWS instances
80
+ -------------------------
81
+
82
+ It's also possible to specify the size of the box and the AMI to use
83
+ when provisioning. Keep in mind that as of right now all the
84
+ bootstrapping and provisioning code is designed to work with recent
85
+ flavors of Ubuntu.
86
+
87
+ ```ruby
88
+
89
+ Configuration.for('migrant') {
90
+ provider {
91
+ name 'aws'
92
+ access_key ENV['AWS_ACCESS_KEY']
93
+ secret_key ENV['AWS_SECRET_KEY']
94
+ ip_address '1.2.3.4' # <-- Elastic IP address
95
+ flavor 'c1.medium' # <-- Any instance size
96
+ image_id 'ami-81b275e8' # <-- AMI ID to use (Lucid 32-bit)
97
+ }
98
+ ssh {
99
+ public_key '~/.ssh/id_rsa.pub'
100
+ private_key '~/.ssh/id_rsa'
101
+ }
102
+ }
103
+ ```
104
+ Customizing Rackspace Instances
105
+ -------------------------------
106
+
107
+ It's also possible to customize the server size with Rackspace.
108
+
109
+ ```ruby
110
+ Configuration.for('migrant') {
111
+ provider {
112
+ name 'rackspace'
113
+ user_name ENV['RACKSPACE_ACCESS_KEY']
114
+ api_key ENV['AWS_SECRET_KEY']
115
+ flavor_id 3 # <-- 1GB Server
116
+ }
117
+ ssh {
118
+ public_key '~/.ssh/id_rsa.pub'
119
+ private_key '~/.ssh/id_rsa'
120
+ }
121
+ }
122
+ ```
123
+
124
+ Configuring Multiple Environments
125
+ ---------------------------------
126
+
127
+ Most deployments have at least a staging and a production environment.
128
+ Migrant supports an arbitrary number of named environments.
129
+
130
+ ```ruby
131
+ Configuration.for('migrant') {
132
+ provider {
133
+ name 'rackspace'
134
+ user_name ENV['RACKSPACE_ACCESS_KEY']
135
+ api_key ENV['AWS_SECRET_KEY']
136
+ }
137
+ ssh {
138
+ public_key '~/.ssh/id_rsa.pub'
139
+ private_key '~/.ssh/id_rsa'
140
+ }
141
+
142
+ # Production environment settings. Defaults are inherited from above
143
+ production {
144
+ provider {
145
+ flavor_id 3 # <-- 1GB Server
146
+ }
147
+ }
148
+
149
+ # Staging environment settings. Defaults are inherited from above
150
+ staging {
151
+ provider {
152
+ flavor_id 1 # <-- 256MB Server
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ Usage
159
+ =====
160
+ ```
161
+ $ gem install migrant
162
+ $ migrant init
163
+ $ migrant up
164
+ ```
165
+
166
+ For a box in a particular 'environment':
167
+
168
+ ```
169
+ $ migrant production up
170
+ $ migrant staging up
171
+ $ migrant staging destroy
172
+ ```
173
+
174
+ Gotchas
175
+ =======
176
+
177
+ * Bootstrapping and provisioning only tested on Ubuntu 'Lucid Lynx'
178
+ * Chef-Solo provisioner the only one implemented
179
+ * Currently does not support multi-vm configurations. Eventually would
180
+ be nice to support load-balanced configurations, and have some ability
181
+ to start or stop any instances necessary to implement that
182
+ configuration (like PoolParty).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'cucumber/rake/task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ Cucumber::Rake::Task.new
9
+
10
+ task :default => :spec
11
+
data/bin/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application '.gitignore' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('vagrant', '.gitignore')
data/bin/migrant ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ require 'migrant'
3
+ require 'migrant/cli'
4
+
5
+ # Start the command line interface and let it process the commands
6
+ Migrant::CLI.start
7
+
@@ -0,0 +1,47 @@
1
+ Feature: Basic Configuration
2
+ In order to use Migrant to provision boxes remotely
3
+ I need to configure some minimal information
4
+
5
+ Scenario: Basic AWS Configuration
6
+ Given a Migrantfile with the body
7
+ """
8
+ Configuration.for('migrant') {
9
+ provider {
10
+ name 'aws'
11
+ access_key 'my_access_key'
12
+ secret_key 'my_secret_key'
13
+ }
14
+ ssh {
15
+ public_key '~/.ssh/id_rsa.pub'
16
+ private_key '~/.ssh/id_rsa'
17
+ }
18
+ }
19
+ """
20
+ Then the cloud should be "aws"
21
+ And my AWS access key should be "my_access_key"
22
+ And my AWS secret key should be "my_secret_key"
23
+ And my ssh public key should reside at "~/.ssh/id_rsa.pub"
24
+ And my ssh private key should reside at "~/.ssh/id_rsa"
25
+ And my AWS instance ID should be "t1.micro"
26
+ And my AWS AMI should be "Ubuntu Lucid Lynx" for the architecture "AMD64"
27
+
28
+ Scenario: Basic Rackspace Configuration
29
+ Given a Migrantfile with the body
30
+ """
31
+ Configuration.for('migrant') {
32
+ provider {
33
+ name 'rackspace'
34
+ user_name 'my_user_name'
35
+ api_key 'my_api_key'
36
+ }
37
+ ssh {
38
+ public_key '~/.ssh/id_rsa.pub'
39
+ private_key '~/.ssh/id_rsa'
40
+ }
41
+ }
42
+ """
43
+ Then the cloud should be "rackspace"
44
+ And my rackspace username should be "my_user_name"
45
+ And my rackspace api key should be "my_api_key"
46
+ And my image should be "Ubuntu Lucid Lynx"
47
+ And my flavor should be "256 MB Server"
@@ -0,0 +1,60 @@
1
+ require 'tempfile'
2
+ require 'configuration'
3
+ require 'pry'
4
+ require 'migrant'
5
+
6
+ Given /^a Migrantfile with the body$/ do |migrantfile_body|
7
+ tmpfile = Tempfile.open('migrant-config') do |f|
8
+ f.write migrantfile_body
9
+ f
10
+ end
11
+ Kernel.load(tmpfile.path)
12
+ @config = Configuration.for('migrant')
13
+ @environment = Migrant::Environment.new(nil,@config)
14
+ end
15
+
16
+ Then /^the cloud should be "([^"]*)"$/ do |cloud_provider_name|
17
+ # Make sure the environment is set up the right way
18
+ @environment.cloud.class.should == Migrant::Clouds::Base.registered(cloud_provider_name)
19
+ end
20
+
21
+ Then /^my AWS access key should be "([^"]*)"$/ do |arg1|
22
+ @config.provider.access_key.should == arg1
23
+ end
24
+
25
+ Then /^my AWS secret key should be "([^"]*)"$/ do |arg1|
26
+ @config.provider.secret_key.should == arg1
27
+ end
28
+
29
+ Then /^my ssh public key should reside at "([^"]*)"$/ do |arg1|
30
+ @config.ssh.public_key.should == arg1
31
+ end
32
+
33
+ Then /^my ssh private key should reside at "([^"]*)"$/ do |arg1|
34
+ @config.ssh.private_key.should == arg1
35
+ end
36
+
37
+ Then /^my AWS instance ID should be "([^"]*)"$/ do |arg1|
38
+ pending # express the regexp above with the code you wish you had
39
+ end
40
+
41
+ Then /^my AWS AMI should be "([^"]*)" for the architecture "([^"]*)"$/ do |arg1, arg2|
42
+ pending # express the regexp above with the code you wish you had
43
+ end
44
+
45
+ Then /^my rackspace username should be "([^"]*)"$/ do |arg1|
46
+ @config.provider.user_name.should == arg1
47
+ end
48
+
49
+ Then /^my rackspace api key should be "([^"]*)"$/ do |arg1|
50
+ @config.provider.api_key.should == arg1
51
+ end
52
+
53
+ Then /^my image should be "([^"]*)"$/ do |arg1|
54
+ pending # express the regexp above with the code you wish you had
55
+ end
56
+
57
+ Then /^my flavor should be "([^"]*)"$/ do |arg1|
58
+ pending # express the regexp above with the code you wish you had
59
+ end
60
+
@@ -0,0 +1,2 @@
1
+ require 'fog'
2
+ Fog.mock! # Make things run faster and try not to provision any real cloud resources
@@ -0,0 +1,38 @@
1
+ module Migrant
2
+ module Bootstrappers
3
+ # Base class for all boostrappers.
4
+ class Base
5
+
6
+ def self.register(shortcut)
7
+ @@bootstrappers ||= Hash.new
8
+ @@bootstrappers[shortcut] = self
9
+ end
10
+
11
+ def self.registered(shortcut)
12
+ @@bootstrappers[shortcut]
13
+ end
14
+
15
+ def self.default_bootstrapper
16
+ @@default = self
17
+ end
18
+
19
+ def self.default
20
+ @@default
21
+ end
22
+
23
+ def initialize(env)
24
+ @environment = env
25
+ end
26
+
27
+ # Run the bootstrapping process
28
+ def run
29
+ raise "Invalid Bootstrapper"
30
+ end
31
+
32
+ # Returns true if the box is already bootstrapped
33
+ def bootstrapped?
34
+ raise "Invalid Bootstrapper"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module Migrant
2
+ module Bootstrappers
3
+ # Installs Ruby 1.9.2 into /usr/local
4
+ class RubyLocal192 < Base
5
+ register(:rbenv_192)
6
+ default_bootstrapper
7
+
8
+ def initialize(env)
9
+ super
10
+ end
11
+
12
+ def run
13
+ commands = [
14
+ 'sudo apt-get update',
15
+ 'sudo apt-get install -y build-essential wget build-essential bison openssl libreadline5 libreadline-dev ' +
16
+ 'curl git-core zlib1g zlib1g-dev libssl-dev vim libsqlite3-0 libsqlite3-dev ' +
17
+ 'sqlite3 libreadline-dev libxml2-dev autoconf',
18
+ 'rm -rf /tmp/ruby-build',
19
+ 'git clone git://github.com/theaboutbox/ruby-build.git /tmp/ruby-build',
20
+ 'cd /tmp/ruby-build && sudo ./install.sh',
21
+ 'sudo ruby-build 1.9.2-p290 /usr/local',
22
+ 'sudo gem install bundler chef'
23
+ ]
24
+ @environment.cloud.execute commands
25
+ end
26
+
27
+ def bootstrapped?
28
+ result = @environment.server.ssh('/usr/local/bin/chef-solo --version')
29
+ result[0].status == 0
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,2 @@
1
+ require 'migrant/bootstrappers/base'
2
+ require 'migrant/bootstrappers/ruby_local_192'
@@ -0,0 +1,55 @@
1
+ module Migrant
2
+ # Represents a box managed by Migrant. We save this object in YAML format
3
+ # so that subsequent runs of Migrant can find them.
4
+ class Box
5
+ attr_accessor :provider
6
+ attr_accessor :id
7
+ attr_accessor :name
8
+
9
+ def initialize(box_provider=nil,box_id=nil,box_name=nil)
10
+ @id = box_id
11
+ @provider = box_provider
12
+ @name = box_name
13
+ end
14
+ end
15
+
16
+ # Loads and persists information about the boxes managed by this
17
+ # migrant configuration
18
+ class Boxes
19
+ # Returns an Boxes instance
20
+ def load
21
+ @boxes = YAML.load(File.read(@path)) if File.exists?(@path)
22
+ self
23
+ end
24
+
25
+ def initialize(path)
26
+ @path = path
27
+ @boxes = {}
28
+ @boxes['file_version'] = Migrant::VERSION
29
+ @boxes['boxes'] = Hash.new
30
+ load
31
+ end
32
+
33
+ def save()
34
+ File.open(@path,'w') do |f|
35
+ f.write(YAML.dump(@boxes))
36
+ end
37
+ end
38
+
39
+ def [](environment)
40
+ environment = 'default' if environment.nil?
41
+ @boxes['boxes'][environment]
42
+ end
43
+
44
+ def []=(environment,box)
45
+ environment = 'default' if environment.nil?
46
+ @boxes['boxes'][environment] = box
47
+ end
48
+
49
+ def add(environment,provider,id)
50
+ environment = 'default' if environment.nil?
51
+ box = Box.new(provider,id)
52
+ self[environment] = box
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ require 'thor'
2
+ require 'pp'
3
+ module Migrant
4
+ class CLI < Thor
5
+ desc 'init', 'Creates a Migrantfile with basic information'
6
+ def init
7
+
8
+ end
9
+
10
+ desc 'up', 'Starts the environment based on the information in the Migrantfile'
11
+ method_option :environment, :aliases => '-e', :desc => 'Specify an environment to launch boxes'
12
+ def up
13
+ load_config(options.environment)
14
+ @environment.up
15
+ end
16
+
17
+ desc 'destroy','Shut down the servers based on the information in this Migrantfile'
18
+ method_option :environment, :aliases => '-e', :desc => 'Specify an environment to destroy boxes'
19
+ def destroy
20
+ load_config(options.environment)
21
+ @environment.destroy
22
+ end
23
+
24
+ desc 'info','Describe any currently running instances'
25
+ method_option :environment, :aliases => '-e', :desc => 'Specify an environment to destroy boxes'
26
+ def info
27
+ load_config(options.environment)
28
+ @environment.info
29
+ end
30
+
31
+ no_tasks do
32
+ def load_config(environment_name)
33
+ @environment = Environment.new(environment_name)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,28 @@
1
+ module Migrant
2
+ module Clouds
3
+ class AWS < Base
4
+ register 'aws'
5
+
6
+ def initialize(env)
7
+ super
8
+ @server_def[:username] = 'ubuntu'
9
+ @server_def[:groups] = ['d2-server']
10
+
11
+ end
12
+
13
+ def connect
14
+ @connection = Fog::Compute.new(:provider => 'AWS',
15
+ :aws_access_key_id => @environment.setting('provider.access_key'),
16
+ :aws_secret_access_key => @environment.setting('provider.secret_key'))
17
+ end
18
+
19
+ def log_server_info
20
+ super
21
+ @environment.ui.notice " Flavor: #{@environment.server.flavor.name}"
22
+ @environment.ui.notice "DNS Name: #{@environment.server.dns_name}"
23
+ @environment.ui.notice " Zone: #{@environment.server.availability_zone}"
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,100 @@
1
+ require 'fog'
2
+ require 'pry'
3
+ module Migrant
4
+ module Clouds
5
+
6
+ # Base class for cloud service providers.
7
+ # All service providers use Fog to handle bootstrapping servers and running stuff
8
+ # Cloud service providers are responsible for starting up the server, setting up ssh
9
+ # access with the provided keypair, and (eventually) disabling the root user.
10
+ class Base
11
+
12
+ # Register a provider
13
+ def self.register(shortcut)
14
+ @@clouds ||= {}
15
+ @@clouds[shortcut] = self
16
+ end
17
+
18
+ # Get a registered provider
19
+ def self.registered(shortcut)
20
+ @@clouds[shortcut]
21
+ end
22
+
23
+ def initialize(env)
24
+ @environment = env
25
+ @server_def = {:private_key_path => @environment.setting('ssh.private_key'),
26
+ :public_key_path => @environment.setting('ssh.public_key')}
27
+ flavor_id = @environment.setting('provider.flavor_id')
28
+ @server_def[:flavor_id] = flavor_id unless flavor_id.nil?
29
+ image_id = @environment.setting('provider.image_id')
30
+ @server_def[:image_id] = image_id unless image_id.nil?
31
+ end
32
+
33
+ attr_accessor :connection
34
+
35
+ def connect
36
+ raise "Invalid Action for Base Class"
37
+ end
38
+
39
+ def bootstrap_server
40
+ @environment.ui.info "Launching Server..."
41
+ @environment.server = @connection.servers.bootstrap(@server_def)
42
+ @environment.ui.notice "Server Launched!"
43
+ ip_address = @environment.setting('provider.ip_address')
44
+ unless ip_address.nil?
45
+ @connection.associate_address(@environment.server.id,ip_address)
46
+ end
47
+ log_server_info
48
+ end
49
+
50
+ def log_server_info
51
+ @environment.ui.notice " ID: #{@environment.server.id}"
52
+ end
53
+
54
+ # Set up connection information for a server and set up SSH keypair access
55
+ #
56
+ # box - Migrant::Box object with information about the server to connect to
57
+ #
58
+ # Does not return anything but sets up @environment.server
59
+ def connect_to_server(box)
60
+ @environment.ui.notice "Connecting to #{box.provider.to_s} server: #{box.id}"
61
+ @environment.server = @connection.servers.get(box.id)
62
+ if (@environment.server.nil?)
63
+ @environment.ui.error "Cannot connect to server!"
64
+ raise "Cannot find server"
65
+ end
66
+ @environment.server.merge_attributes(@server_def)
67
+ end
68
+
69
+ def execute(commands)
70
+ server = @environment.server
71
+ commands.each do |cmd|
72
+ @environment.ui.info("$ #{cmd}")
73
+ result = server.ssh cmd
74
+ if (result[0].status != 0)
75
+ @environment.ui.info(result[0].stdout)
76
+ @environment.ui.error "Error executing command: #{cmd}\n#{result.inspect}"
77
+ raise "Remote Script Error"
78
+ end
79
+ @environment.ui.info(result[0].stdout)
80
+ @environment.ui.warn(result[0].stderr) if result[0].stderr
81
+ end
82
+ end
83
+
84
+ # Asks the user if they want to shut down a box
85
+ #
86
+ # box - A Migrant::Box with information about the box
87
+ #
88
+ # returns true if the user decided to shut down the box
89
+ def destroy(box)
90
+ if (@environment.ui.yes?("Are you sure you want to shut down box #{box.name}?",:red))
91
+ @environment.ui.notice "Shutting down box #{box.name} id: #{box.id}"
92
+ connect
93
+ @connection.servers.get(box.id).destroy
94
+ return true
95
+ end
96
+ return false
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,25 @@
1
+ module Migrant
2
+ module Clouds
3
+ class Rackspace < Base
4
+ register 'rackspace'
5
+
6
+ def initialize(env)
7
+ super
8
+ end
9
+
10
+ def connect
11
+ @connection = Fog::Compute.new(:provider => 'Rackspace',
12
+ :rackspace_api_key => @environment.setting('provider.api_key'),
13
+ :rackspace_username => @environment.setting('provider.user_name'))
14
+ end
15
+
16
+ def log_server_info
17
+ super
18
+ @environment.ui.notice " Name: #{@environment.server.name}"
19
+ @environment.ui.notice " IP Address: #{@environment.server.addresses['public']}"
20
+ @environment.ui.notice " Image ID: #{@environment.server.image.name}"
21
+ @environment.ui.notice " Flavor: #{@environment.server.flavor.name}"
22
+ end
23
+ end
24
+ end
25
+ end