diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c05bf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.gem +*.rbc +.bundle +.config +.vagrant +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp +Vagrantfile diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..d9f185e --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +source 'https://rubygems.org' + +gemspec + +group :development do + # We depend on Vagrant for development, but we don't add it as a + # gem dependency because we expect to be installed within the + # Vagrant environment itself using `vagrant plugin`. + gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git" +end diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..b4ba2db --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 Mitchell Hashimoto + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2506fb --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# Vagrant RackSpace Cloud Provider + +This is a [Vagrant](http://www.vagrantup.com) 1.1+ plugin that adds a +[RackSpace Cloud](http://www.rackspace.com/cloud) provider to Vagrant, +allowing Vagrant to control and provision machines within RackSpace +cloud. + +## Features + +* Boot Rackspace Cloud instances. +* SSH into the instances. +* Provision the instances with any built-in Vagrant provisioner. +* Minimal synced folder support via `rsync`. + +## Usage + +Install using standard Vagrant 1.1+ plugin installation methods. After +installing, `vagrant up` and specify the `rackspace` provider. An example is +shown below. + +``` +$ vagrant plugin install vagrant-rackspace +... +$ vagrant up --provider=rackspace +... +``` + +Of course prior to doing this, you'll need to obtain an Rackspace-compatible +box file for Vagrant. + +## Quick Start + +After installing the plugin (instructions above), the quickest way to get +started is to actually use a dummy Rackspace box and specify all the details +manually within a `config.vm.provider` block. So first, add the dummy +box using any name you want: + +``` +$ vagrant box add dummy https://github.com/mitchellh/vagrant-rackspace/raw/master/dummy.box +... +``` + +And then make a Vagrantfile that looks like the following, filling in +your information where necessary. + +``` +Vagrant.configure("2") do |config| + config.vm.box = "dummy" + + config.vm.provider :rackspace do |rs| + rs.username = "YOUR USERNAME" + rs.api_key = "YOUR API KEY" + rs.flavor = /512MB/ + rs.image = /Ubuntu/ + end +end +``` + +And then run `vagrant up --provider=rackspace`. + +This will start an Ubuntu 12.04 instance in the DFW datacenter region within +your account. And assuming your SSH information was filled in properly +within your Vagrantfile, SSH and provisioning will work as well. + +Note that normally a lot of this boilerplate is encoded within the box +file, but the box file used for the quick start, the "dummy" box, has +no preconfigured defaults. + +## Box Format + +Every provider in Vagrant must introduce a custom box format. This +provider introduces `rackspace` boxes. You can view an example box in +the [example_box/ directory](https://github.com/mitchellh/vagrant-rackspace/tree/master/example_box). +That directory also contains instructions on how to build a box. + +The box format is basically just the required `metadata.json` file +along with a `Vagrantfile` that does default settings for the +provider-specific configuration for this provider. + +## Configuration + +This provider exposes quite a few provider-specific configuration options: + +* `api_key` - The API key for accessing Rackspace. +* `flavor` - The server flavor to boot. This can be a string matching + the exact ID or name of the server, or this can be a regular expression + to partially match some server flavor. +* `image` - The server image to boot. This can be a string matching the + exact ID or name of the image, or this can be a regular expression to + partially match some image. +* `endpoint` - The endpoint to hit. By default this is DFW. +* `username` - The username with which to access Rackspace. + +These can be set like typical provider-specific configuration: + +```ruby +Vagrant.configure("2") do |config| + # ... other stuff + + config.vm.provider :rackspace do |rs| + rs.username = "mitchellh" + rs.api_key = "foobarbaz" + end +end +``` + +## Networks + +Networking features in the form of `config.vm.network` are not +supported with `vagrant-rackspace`, currently. If any of these are +specified, Vagrant will emit a warning, but will otherwise boot +the Rackspace server. + +## Synced Folders + +There is minimal support for synced folders. Upon `vagrant up`, +`vagrant reload`, and `vagrant provision`, the Rackspace provider will use +`rsync` (if available) to uni-directionally sync the folder to +the remote machine over SSH. + +This is good enough for all built-in Vagrant provisioners (shell, +chef, and puppet) to work! + +## Development + +To work on the `vagrant-rackspace` plugin, clone this repository out, and use +[Bundler](http://gembundler.com) to get the dependencies: + +``` +$ bundle +``` + +Once you have the dependencies, verify the unit tests pass with `rake`: + +``` +$ bundle exec rake +``` + +If those pass, you're ready to start developing the plugin. You can test +the plugin without installing it into your Vagrant environment by just +creating a `Vagrantfile` in the top level of this directory (it is gitignored) +that uses it, and uses bundler to execute Vagrant: + +``` +$ bundle exec vagrant up --provider=rackspace +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d4347e1 --- /dev/null +++ b/Rakefile @@ -0,0 +1,21 @@ +require 'rubygems' +require 'bundler/setup' +require 'rspec/core/rake_task' + +# Immediately sync all stdout so that tools like buildbot can +# immediately load in the output. +$stdout.sync = true +$stderr.sync = true + +# Change to the directory of this file. +Dir.chdir(File.expand_path("../", __FILE__)) + +# This installs the tasks that help with gem creation and +# publishing. +Bundler::GemHelper.install_tasks + +# Install the `spec` task so that we can run tests. +RSpec::Core::RakeTask.new + +# Default task is to run the unit tests +task :default => "spec" diff --git a/dummy.box b/dummy.box new file mode 100644 index 0000000..48d62e3 Binary files /dev/null and b/dummy.box differ diff --git a/example_box/README.md b/example_box/README.md new file mode 100644 index 0000000..ad89531 --- /dev/null +++ b/example_box/README.md @@ -0,0 +1,13 @@ +# Vagrant RackSpace Cloud Example Box + +Vagrant providers each require a custom provider-specific box format. +This folder shows the example contents of a box for the `rackspace` provider. +To turn this into a box: + +``` +$ tar cvzf rackspace.box ./metadata.json ./Vagrantfile +``` + +This box works by using Vagrant's built-in Vagrantfile merging to setup +defaults for RackSpace. These defaults can easily be overwritten by higher-level +Vagrantfiles (such as project root Vagrantfiles). diff --git a/example_box/metadata.json b/example_box/metadata.json new file mode 100644 index 0000000..491922b --- /dev/null +++ b/example_box/metadata.json @@ -0,0 +1,3 @@ +{ + "provider": "rackspace" +} diff --git a/lib/vagrant-rackspace.rb b/lib/vagrant-rackspace.rb new file mode 100644 index 0000000..19c1588 --- /dev/null +++ b/lib/vagrant-rackspace.rb @@ -0,0 +1,53 @@ +require "pathname" + +require "vagrant-rackspace/plugin" + +module VagrantPlugins + module Rackspace + lib_path = Pathname.new(File.expand_path("../vagrant-rackspace", __FILE__)) + autoload :Errors, lib_path.join("errors") + + # This initializes the i18n load path so that the plugin-specific + # translations work. + def self.init_i18n + I18n.load_path << File.expand_path("locales/en.yml", source_root) + I18n.reload! + end + + # This initializes the logging so that our logs are outputted at + # the same level as Vagrant core logs. + def self.init_logging + # Initialize logging + level = nil + begin + level = Log4r.const_get(ENV["VAGRANT_LOG"].upcase) + rescue NameError + # This means that the logging constant wasn't found, + # which is fine. We just keep `level` as `nil`. But + # we tell the user. + level = nil + end + + # Some constants, such as "true" resolve to booleans, so the + # above error checking doesn't catch it. This will check to make + # sure that the log level is an integer, as Log4r requires. + level = nil if !level.is_a?(Integer) + + # Set the logging level on all "vagrant" namespaced + # logs as long as we have a valid level. + if level + logger = Log4r::Logger.new("vagrant_rackspace") + logger.outputters = Log4r::Outputter.stderr + logger.level = level + logger = nil + end + end + + # This returns the path to the source of this plugin. + # + # @return [Pathname] + def self.source_root + @source_root ||= Pathname.new(File.expand_path("../../", __FILE__)) + end + end +end diff --git a/lib/vagrant-rackspace/action.rb b/lib/vagrant-rackspace/action.rb new file mode 100644 index 0000000..2e2e2f0 --- /dev/null +++ b/lib/vagrant-rackspace/action.rb @@ -0,0 +1,95 @@ +require "pathname" + +require "vagrant/action/builder" + +module VagrantPlugins + module Rackspace + module Action + # Include the built-in modules so we can use them as top-level things. + include Vagrant::Action::Builtin + + # This action is called to destroy the remote machine. + def self.action_destroy + Vagrant::Action::Builder.new.tap do |b| + b.use ConfigValidate + b.use Call, IsCreated do |env, b2| + if !env[:result] + b2.use MessageNotCreated + next + end + + b2.use ConnectRackspace + b2.use DeleteServer + end + end + end + + # This action is called to read the SSH info of the machine. The + # resulting state is expected to be put into the `:machine_ssh_info` + # key. + def self.action_read_ssh_info + Vagrant::Action::Builder.new.tap do |b| + b.use ConfigValidate + b.use ConnectRackspace + b.use ReadSSHInfo + end + end + + # This action is called to read the state of the machine. The + # resulting state is expected to be put into the `:machine_state_id` + # key. + def self.action_read_state + Vagrant::Action::Builder.new.tap do |b| + b.use ConfigValidate + b.use ConnectRackspace + b.use ReadState + end + end + + def self.action_ssh + Vagrant::Action::Builder.new.tap do |b| + b.use ConfigValidate + b.use Call, IsCreated do |env, b2| + if !env[:result] + b2.use MessageNotCreated + next + end + + b2.use SSHExec + end + end + end + + def self.action_up + Vagrant::Action::Builder.new.tap do |b| + b.use ConfigValidate + b.use Call, IsCreated do |env, b2| + if env[:result] + b2.use MessageAlreadyCreated + next + end + + b2.use ConnectRackspace + b2.use Provision + b2.use SyncFolders + b2.use WarnNetworks + b2.use CreateServer + end + end + end + + # The autoload farm + action_root = Pathname.new(File.expand_path("../action", __FILE__)) + autoload :ConnectRackspace, action_root.join("connect_rackspace") + autoload :CreateServer, action_root.join("create_server") + autoload :DeleteServer, action_root.join("delete_server") + autoload :IsCreated, action_root.join("is_created") + autoload :MessageAlreadyCreated, action_root.join("message_already_created") + autoload :MessageNotCreated, action_root.join("message_not_created") + autoload :ReadSSHInfo, action_root.join("read_ssh_info") + autoload :ReadState, action_root.join("read_state") + autoload :SyncFolders, action_root.join("sync_folders") + autoload :WarnNetworks, action_root.join("warn_networks") + end + end +end diff --git a/lib/vagrant-rackspace/action/connect_rackspace.rb b/lib/vagrant-rackspace/action/connect_rackspace.rb new file mode 100644 index 0000000..be66734 --- /dev/null +++ b/lib/vagrant-rackspace/action/connect_rackspace.rb @@ -0,0 +1,37 @@ +require "fog" +require "log4r" + +module VagrantPlugins + module Rackspace + module Action + # This action connects to Rackspace, verifies credentials work, and + # puts the Rackspace connection object into the `:rackspace_compute` key + # in the environment. + class ConnectRackspace + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant_rackspace::action::connect_rackspace") + end + + def call(env) + # Get the configs + config = env[:machine].provider_config + api_key = config.api_key + endpoint = config.endpoint + username = config.username + + @logger.info("Connecting to Rackspace...") + env[:rackspace_compute] = Fog::Compute.new({ + :provider => :rackspace, + :version => :v2, + :rackspace_api_key => api_key, + :rackspace_endpoint => endpoint, + :rackspace_username => username + }) + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/create_server.rb b/lib/vagrant-rackspace/action/create_server.rb new file mode 100644 index 0000000..0463899 --- /dev/null +++ b/lib/vagrant-rackspace/action/create_server.rb @@ -0,0 +1,115 @@ +require "fog" +require "log4r" + +require 'vagrant/util/retryable' + +module VagrantPlugins + module Rackspace + module Action + # This creates the Rackspace server. + class CreateServer + include Vagrant::Util::Retryable + + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant_rackspace::action::create_server") + end + + def call(env) + # Get the configs + config = env[:machine].provider_config + + # Find the flavor + env[:ui].info(I18n.t("vagrant_rackspace.finding_flavor")) + flavor = find_matching(env[:rackspace_compute].flavors.all, config.flavor) + raise Errors::NoMatchingFlavor if !flavor + + # Find the image + env[:ui].info(I18n.t("vagrant_rackspace.finding_image")) + image = find_matching(env[:rackspace_compute].images.all, config.image) + raise Errors::NoMatchingImage if !image + + # Output the settings we're going to use to the user + env[:ui].info(I18n.t("vagrant_rackspace.launching_server")) + env[:ui].info(" -- Flavor: #{flavor.name}") + env[:ui].info(" -- Image: #{image.name}") + + # Build the options for launching... + options = { + :flavor_id => flavor.id, + :image_id => image.id, + :name => env[:machine].name, + :personality => [ + { + :path => "/root/.ssh/authorized_keys", + :contents => Base64.encode64(Vagrant.source_root.join("keys/vagrant.pub").read) + } + ] + } + + # Create the server + server = env[:rackspace_compute].servers.create(options) + p server.password + + # Store the ID right away so we can track it + env[:machine].id = server.id + + # Wait for the server to finish building + env[:ui].info(I18n.t("vagrant_rackspace.waiting_for_build")) + retryable(:on => Fog::Errors::TimeoutError, :tries => 200) do + # If we're interrupted don't worry about waiting + next if env[:interrupted] + + # Set the progress + env[:ui].clear_line + env[:ui].report_progress(server.progress, 100, false) + + # Wait for the server to be ready + begin + server.wait_for(5) { ready? } + rescue RuntimeError => e + # If we don't have an error about a state transition, then + # we just move on. + raise if e.message !~ /should have transitioned/ + raise Errors::CreateBadState, :state => server.state + end + end + + if !env[:interrupted] + # Clear the line one more time so the progress is removed + env[:ui].clear_line + + # Wait for SSH to become available + env[:ui].info(I18n.t("vagrant_rackspace.waiting_for_ssh")) + while true + # If we're interrupted then just back out + break if env[:interrupted] + break if env[:machine].communicate.ready? + sleep 2 + end + + env[:ui].info(I18n.t("vagrant_rackspace.ready")) + end + + @app.call(env) + end + + protected + + # This method finds a matching _thing_ in a collection of + # _things_. This works matching if the ID or NAME equals to + # `name`. Or, if `name` is a regexp, a partial match is chosen + # as well. + def find_matching(collection, name) + collection.each do |single| + return single if single.id == name + return single if single.name == name + return single if name.is_a?(Regexp) && name =~ single.name + end + + nil + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/delete_server.rb b/lib/vagrant-rackspace/action/delete_server.rb new file mode 100644 index 0000000..3b565eb --- /dev/null +++ b/lib/vagrant-rackspace/action/delete_server.rb @@ -0,0 +1,26 @@ +require "log4r" + +module VagrantPlugins + module Rackspace + module Action + # This deletes the running server, if there is one. + class DeleteServer + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant_rackspace::action::delete_server") + end + + def call(env) + if env[:machine].id + env[:ui].info(I18n.t("vagrant_rackspace.deleting_server")) + server = env[:rackspace_compute].servers.get(env[:machine].id) + server.destroy + env[:machine].id = nil + end + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/is_created.rb b/lib/vagrant-rackspace/action/is_created.rb new file mode 100644 index 0000000..be69738 --- /dev/null +++ b/lib/vagrant-rackspace/action/is_created.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module Rackspace + module Action + class IsCreated + def initialize(app, env) + @app = app + end + + def call(env) + env[:result] = env[:machine].state.id != :not_created + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/message_already_created.rb b/lib/vagrant-rackspace/action/message_already_created.rb new file mode 100644 index 0000000..00b8028 --- /dev/null +++ b/lib/vagrant-rackspace/action/message_already_created.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module Rackspace + module Action + class MessageAlreadyCreated + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t("vagrant_rackspace.already_created")) + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/message_not_created.rb b/lib/vagrant-rackspace/action/message_not_created.rb new file mode 100644 index 0000000..9bf6bf9 --- /dev/null +++ b/lib/vagrant-rackspace/action/message_not_created.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module Rackspace + module Action + class MessageNotCreated + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t("vagrant_rackspace.not_created")) + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/read_ssh_info.rb b/lib/vagrant-rackspace/action/read_ssh_info.rb new file mode 100644 index 0000000..6a2c000 --- /dev/null +++ b/lib/vagrant-rackspace/action/read_ssh_info.rb @@ -0,0 +1,42 @@ +require "log4r" + +module VagrantPlugins + module Rackspace + module Action + # This action reads the SSH info for the machine and puts it into the + # `:machine_ssh_info` key in the environment. + class ReadSSHInfo + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant_rackspace::action::read_ssh_info") + end + + def call(env) + env[:machine_ssh_info] = read_ssh_info(env[:rackspace_compute], env[:machine]) + + @app.call(env) + end + + def read_ssh_info(rackspace, machine) + return nil if machine.id.nil? + + # Find the machine + server = rackspace.servers.get(machine.id) + if server.nil? + # The machine can't be found + @logger.info("Machine couldn't be found, assuming it got destroyed.") + machine.id = nil + return nil + end + + # Read the DNS info + return { + :host => server.ipv4_address, + :port => 22, + :username => "root" + } + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/read_state.rb b/lib/vagrant-rackspace/action/read_state.rb new file mode 100644 index 0000000..77d99a3 --- /dev/null +++ b/lib/vagrant-rackspace/action/read_state.rb @@ -0,0 +1,38 @@ +require "log4r" + +module VagrantPlugins + module Rackspace + module Action + # This action reads the state of the machine and puts it in the + # `:machine_state_id` key in the environment. + class ReadState + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant_rackspace::action::read_state") + end + + def call(env) + env[:machine_state_id] = read_state(env[:rackspace_compute], env[:machine]) + + @app.call(env) + end + + def read_state(rackspace, machine) + return :not_created if machine.id.nil? + + # Find the machine + server = rackspace.servers.get(machine.id) + if server.nil? || server.state == "DELETED" + # The machine can't be found + @logger.info("Machine not found or deleted, assuming it got destroyed.") + machine.id = nil + return :not_created + end + + # Return the state + return server.state.downcase.to_sym + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/sync_folders.rb b/lib/vagrant-rackspace/action/sync_folders.rb new file mode 100644 index 0000000..7098d4d --- /dev/null +++ b/lib/vagrant-rackspace/action/sync_folders.rb @@ -0,0 +1,57 @@ +require "log4r" + +require "vagrant/util/subprocess" + +module VagrantPlugins + module Rackspace + module Action + # This middleware uses `rsync` to sync the folders over to the + # remote instance. + class SyncFolders + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant_rackspace::action::sync_folders") + end + + def call(env) + @app.call(env) + + ssh_info = env[:machine].ssh_info + + env[:machine].config.vm.synced_folders.each do |id, data| + hostpath = File.expand_path(data[:hostpath], env[:root_path]) + guestpath = data[:guestpath] + + # Make sure there is a trailing slash on the host path to + # avoid creating an additional directory with rsync + hostpath = "#{hostpath}/" if hostpath !~ /\/$/ + + env[:ui].info(I18n.t("vagrant_rackspace.rsync_folder", + :hostpath => hostpath, + :guestpath => guestpath)) + + # Create the guest path + env[:machine].communicate.sudo("mkdir -p '#{guestpath}'") + env[:machine].communicate.sudo( + "chown #{ssh_info[:username]} '#{guestpath}'") + + # Rsync over to the guest path using the SSH info + command = [ + "rsync", "--verbose", "--archive", "-z", + "-e", "ssh -p #{ssh_info[:port]} -i '#{ssh_info[:private_key_path]}'", + hostpath, + "#{ssh_info[:username]}@#{ssh_info[:host]}:#{guestpath}"] + + r = Vagrant::Util::Subprocess.execute(*command) + if r.exit_code != 0 + raise Errors::RsyncError, + :guestpath => guestpath, + :hostpath => hostpath, + :stderr => r.stderr + end + end + end + end + end + end +end diff --git a/lib/vagrant-rackspace/action/warn_networks.rb b/lib/vagrant-rackspace/action/warn_networks.rb new file mode 100644 index 0000000..1f2a72c --- /dev/null +++ b/lib/vagrant-rackspace/action/warn_networks.rb @@ -0,0 +1,19 @@ +module VagrantPlugins + module Rackspace + module Action + class WarnNetworks + def initialize(app, env) + @app = app + end + + def call(env) + if env[:machine].config.vm.networks.length > 0 + env[:ui].warn(I18n.t("vagrant_rackspace.warn_networks")) + end + + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant-rackspace/config.rb b/lib/vagrant-rackspace/config.rb new file mode 100644 index 0000000..b839b7f --- /dev/null +++ b/lib/vagrant-rackspace/config.rb @@ -0,0 +1,47 @@ +require "vagrant" + +module VagrantPlugins + module Rackspace + class Config < Vagrant.plugin("2", :config) + # The API key to access RackSpace. + # + # @return [String] + attr_accessor :api_key + + # The endpoint to access RackSpace. If nil, it will default + # to DFW. + # + # @return [String] + attr_accessor :endpoint + + # The flavor of server to launch, either the ID or name. This + # can also be a regular expression to partially match a name. + attr_accessor :flavor + + # The name or ID of the image to use. This can also be a regular + # expression to partially match a name. + attr_accessor :image + + # The username to access RackSpace. + # + # @return [String] + attr_accessor :username + + def initialize + @api_key = UNSET_VALUE + @endpoint = UNSET_VALUE + @flavor = UNSET_VALUE + @image = UNSET_VALUE + @username = UNSET_VALUE + end + + def finalize! + @api_key = nil if @api_key == UNSET_VALUE + @endpoint = nil if @endpoint == UNSET_VALUE + @flavor = nil if @flavor == UNSET_VALUE + @image = nil if @image == UNSET_VALUE + @username = nil if @username == UNSET_VALUE + end + end + end +end diff --git a/lib/vagrant-rackspace/errors.rb b/lib/vagrant-rackspace/errors.rb new file mode 100644 index 0000000..38d2149 --- /dev/null +++ b/lib/vagrant-rackspace/errors.rb @@ -0,0 +1,27 @@ +require "vagrant" + +module VagrantPlugins + module Rackspace + module Errors + class VagrantRackspaceError < Vagrant::Errors::VagrantError + error_namespace("vagrant_rackspace.errors") + end + + class CreateBadState < VagrantRackspaceError + error_key(:create_bad_state) + end + + class NoMatchingFlavor < VagrantRackspaceError + error_key(:no_matching_flavor) + end + + class NoMatchingImage < VagrantRackspaceError + error_key(:no_matching_image) + end + + class RsyncError < VagrantRackspaceError + error_key(:rsync_error) + end + end + end +end diff --git a/lib/vagrant-rackspace/plugin.rb b/lib/vagrant-rackspace/plugin.rb new file mode 100644 index 0000000..addd84d --- /dev/null +++ b/lib/vagrant-rackspace/plugin.rb @@ -0,0 +1,37 @@ +begin + require "vagrant" +rescue LoadError + raise "The RackSpace Cloud provider must be run within Vagrant." +end + +# This is a sanity check to make sure no one is attempting to install +# this into an early Vagrant version. +if Vagrant::VERSION < "1.1.0" + raise "RackSpace Cloud provider is only compatible with Vagrant 1.1+" +end + +module VagrantPlugins + module Rackspace + class Plugin < Vagrant.plugin("2") + name "RackSpace Cloud" + description <<-DESC + This plugin enables Vagrant to manage machines in RackSpace Cloud. + DESC + + config(:rackspace, :provider) do + require_relative "config" + Config + end + + provider(:rackspace) do + # Setup some things + Rackspace.init_i18n + Rackspace.init_logging + + # Load the actual provider + require_relative "provider" + Provider + end + end + end +end diff --git a/lib/vagrant-rackspace/provider.rb b/lib/vagrant-rackspace/provider.rb new file mode 100644 index 0000000..29b8fb6 --- /dev/null +++ b/lib/vagrant-rackspace/provider.rb @@ -0,0 +1,50 @@ +require "vagrant" + +require "vagrant-rackspace/action" + +module VagrantPlugins + module Rackspace + class Provider < Vagrant.plugin("2", :provider) + def initialize(machine) + @machine = machine + end + + def action(name) + # Attempt to get the action method from the Action class if it + # exists, otherwise return nil to show that we don't support the + # given action. + action_method = "action_#{name}" + return Action.send(action_method) if Action.respond_to?(action_method) + nil + end + + def ssh_info + # Run a custom action called "read_ssh_info" which does what it + # says and puts the resulting SSH info into the `:machine_ssh_info` + # key in the environment. + env = @machine.action("read_ssh_info") + env[:machine_ssh_info] + end + + def state + # Run a custom action we define called "read_state" which does + # what it says. It puts the state in the `:machine_state_id` + # key in the environment. + env = @machine.action("read_state") + + state_id = env[:machine_state_id] + + # Get the short and long description + short = I18n.t("vagrant_rackspace.states.short_#{state_id}") + long = I18n.t("vagrant_rackspace.states.long_#{state_id}") + + # Return the MachineState object + Vagrant::MachineState.new(state_id, short, long) + end + + def to_s + "RackSpace Cloud" + end + end + end +end diff --git a/lib/vagrant-rackspace/version.rb b/lib/vagrant-rackspace/version.rb new file mode 100644 index 0000000..d8e9c7c --- /dev/null +++ b/lib/vagrant-rackspace/version.rb @@ -0,0 +1,5 @@ +module VagrantPlugins + module Rackspace + VERSION = "0.0.1" + end +end diff --git a/locales/en.yml b/locales/en.yml new file mode 100644 index 0000000..b5b159d --- /dev/null +++ b/locales/en.yml @@ -0,0 +1,67 @@ +en: + vagrant_rackspace: + already_created: |- + The server is already created. + deleting_server: |- + Deleting server... + finding_flavor: |- + Finding flavor for server... + finding_image: |- + Finding image for server... + launching_server: |- + Launching a server with the following settings... + not_created: |- + The server hasn't been created yet. Run `vagrant up` first. + ready: |- + The server is ready! + rsync_folder: |- + Rsyncing folder: %{hostpath} => %{guestpath} + waiting_for_build: |- + Waiting for the server to be built... + waiting_for_ssh: |- + Waiting for SSH to become available... + warn_networks: |- + Warning! The Rackspace provider doesn't support any of the Vagrant + high-level network configurations (`config.vm.network`). They + will be silently ignored. + + errors: + create_bad_state: |- + While creating the server, it transitioned to an unexpected + state: '%{state}', instead of properly booting. Run `vagrant status` + to find out what can be done about this state, or `vagrant destroy` + if you want to start over. + no_matching_flavor: |- + No matching flavor was found! Please check your flavor setting + to make sure you have a valid flavor chosen. + no_matching_image: |- + No matching image was found! Please check your image setting to + make sure you have a valid image chosen. + rsync_error: |- + There was an error when attemping to rsync a share folder. + Please inspect the error message below for more info. + + Host path: %{hostpath} + Guest path: %{guestpath} + Error: %{stderr} + + states: + short_active: |- + active + long_active: |- + The server is up and running. Run `vagrant ssh` to access it. + short_build: |- + building + long_build: |- + The server is currently being built. You must wait for this to + complete before you can access it. You can delete the server, however, + by running `vagrant destroy`. + short_error: |- + error + long_error: |- + The server is in an erroneous state. Contact RackSpace support + or destroy the machine with `vagrant destroy`. + short_not_created: |- + not created + long_not_created: |- + The server is not created. Run `vagrant up` to create it. diff --git a/spec/vagrant-rackspace/config_spec.rb b/spec/vagrant-rackspace/config_spec.rb new file mode 100644 index 0000000..947698a --- /dev/null +++ b/spec/vagrant-rackspace/config_spec.rb @@ -0,0 +1,31 @@ +require "vagrant-rackspace/config" + +describe VagrantPlugins::Rackspace::Config do + describe "defaults" do + subject do + super().tap do |o| + o.finalize! + end + end + + its(:api_key) { should be_nil } + its(:endpoint) { should be_nil } + its(:flavor) { should be_nil } + its(:image) { should be_nil } + its(:username) { should be_nil } + end + + describe "overriding defaults" do + [:api_key, + :endpoint, + :flavor, + :image, + :username].each do |attribute| + it "should not default #{attribute} if overridden" do + subject.send("#{attribute}=".to_sym, "foo") + subject.finalize! + subject.send(attribute).should == "foo" + end + end + end +end diff --git a/vagrant-rackspace.gemspec b/vagrant-rackspace.gemspec new file mode 100644 index 0000000..3d4ae6f --- /dev/null +++ b/vagrant-rackspace.gemspec @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'vagrant-rackspace/version' + +Gem::Specification.new do |gem| + gem.name = "vagrant-rackspace" + gem.version = VagrantPlugins::Rackspace::VERSION + gem.authors = ["Mitchell Hashimoto"] + gem.email = ["mitchell@hashicorp.com"] + gem.description = "Enables Vagrant to manage machines in RackSpace Cloud." + gem.summary = "Enables Vagrant to manage machines in RackSpace Cloud." + gem.homepage = "http://www.vagrantup.com" + + gem.add_runtime_dependency "fog", "~> 1.9.0" + + gem.add_development_dependency "rake" + gem.add_development_dependency "rspec", "~> 2.13.0" + + gem.files = `git ls-files`.split($/) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.require_paths = ["lib"] +end