migrant-boxes 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.
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