vagrant-digitalocean 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # TODO switch on release
4
+ gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git"
5
+
6
+ # Specify your gem's dependencies in vagrant-digitalocean.gemspec
7
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 John Bender
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Vagrant Digital Ocean
2
+
3
+ `vagrant-digitalocean` is a provider plugin for Vagrant that allows the management of [Digital Ocean](https://www.digitalocean.com/) droplets (instances).
4
+
5
+ ## INSECURE
6
+
7
+ As of this writing there is no support for custom keys on the droplets created by this provider. That means anyone with the vagrant keys and the IP of your droplet has root on your machine.
8
+
9
+ *Do not use this plugin with sensitive projects*
10
+
11
+ ## Status
12
+
13
+ As of this writing the provider implementation is geared entirely toward a development workflow. That is, Digital Ocean droplets are meant to be used as a replacement for VirtualBox in a server developers workflow.
14
+
15
+ ## Supported Guests/Hosts
16
+
17
+ This project is primarily to support my workflow wich currently only involves Ubuntu and CentOS. It's likely that any unix host will work but I've not tested it. Guests require porting of the nfs, chef, and sudo setup scripts.
18
+
19
+ Hosts:
20
+
21
+ * Ubuntu 12.04
22
+
23
+ Guests:
24
+
25
+ * Ubuntu 12.04
26
+ * CentOS 6
27
+
28
+ ## Supported Provisioners
29
+
30
+ The shell provisioner is supported by default but other provisioners require bootstrapping on the server. Chef is currently the only supported provisioner. Adding support for puppet and others requires adding the install scripts.
31
+
32
+ ## Installation
33
+
34
+ Installation is performed in the prescribed manner for Vagrant 1.1 plugins.
35
+
36
+ vagrant plugin install vagrant-digitalocean
37
+
38
+ In addition to installing the plugin the default box associated with the provider needs to be installed.
39
+
40
+ vagrant box add digital_ocean https://raw.github.com/johnbender/vagrant-digitalocean/master/box/digital_ocean.box
41
+
42
+ ## Usage
43
+
44
+ To use the Digital Ocean provider you will need to visit the [API access page](https://www.digitalocean.com/api_access) to retrieve the client identifier and API key associated with your account.
45
+
46
+ ### Config
47
+
48
+ Supported provider configuration options are as follows:
49
+
50
+ ```ruby
51
+ Vagrant.configure("2") do |config|
52
+ config.vm.box = "digital_ocean"
53
+
54
+ config.vm.provider :digital_ocean do |vm|
55
+ vm.client_id = ENV["DO_CLIENT_ID"]
56
+ vm.api_key = ENV["DO_API_KEY"]
57
+ vm.image = "Ubuntu 12.04 x32 Server"
58
+ vm.region = "New York 1"
59
+ vm.size = "512MB"
60
+ end
61
+ end
62
+ ```
63
+
64
+ Note that the example contains the default value. The client identifier and API key are pulled from the environment and the other values are the string representations of the droplet configuration options as provided by the [Digital Ocean API](https://www.digitalocean.com/api).
65
+
66
+ ## Contributing
67
+
68
+ 1. Fork it
69
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
70
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
71
+ 4. Push to the branch (`git push origin my-new-feature`)
72
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ namespace "dev" do
4
+ task "build" do
5
+ system "bash bin/build.sh"
6
+ end
7
+ end
data/bin/build.sh ADDED
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+
3
+ # build the gem
4
+ gem build *.gemspec
5
+
6
+ # make the gem available for installation as a vagrant plugin
7
+ gem install *.gem
8
+
9
+ # make sure bsdtar is installed
10
+ if ! `which bsdtar > /dev/null`; then
11
+ echo "!! Install bsdtar"
12
+ exit 1
13
+ fi
14
+
15
+ # install the plugin
16
+ vagrant plugin install vagrant-digitalocean
17
+
18
+ # move into the dummy box dir
19
+ cd box
20
+
21
+ # create the dummy box
22
+ tar cvzf digital_ocean.box ./metadata.json
23
+
24
+ # remove an old version of the dummy box
25
+ if `vagrant box list | grep -q digital_ocean`; then
26
+ vagrant box remove digital_ocean digital_ocean
27
+ fi
28
+
29
+ # add the new version of the dummy box
30
+ vagrant box add digital_ocean digital_ocean.box
31
+
32
+ # back out of the box dir
33
+ cd -
data/box/Vagrantfile ADDED
@@ -0,0 +1,15 @@
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ Vagrant.configure("2") do |config|
5
+ config.vm.box = "digital_ocean"
6
+ config.vm.synced_folder ".", "/vagrant", :nfs => true
7
+
8
+ config.vm.provider :digital_ocean do |vm|
9
+ vm.client_id = ENV["DO_CLIENT_ID"]
10
+ vm.api_key = ENV["DO_API_KEY"]
11
+ vm.image = "Ubuntu 12.04 x32 Server"
12
+ vm.region = "New York 1"
13
+ vm.size = "512MB"
14
+ end
15
+ end
Binary file
data/box/metadata.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "provider": "digital_ocean"
3
+ }
@@ -0,0 +1,77 @@
1
+ require "vagrant-digitalocean/actions/destroy"
2
+ require "vagrant-digitalocean/actions/read_state"
3
+ require "vagrant-digitalocean/actions/setup_provisioner"
4
+ require "vagrant-digitalocean/actions/setup_nfs"
5
+ require "vagrant-digitalocean/actions/setup_sudo"
6
+ require "vagrant-digitalocean/actions/setup_user"
7
+ require "vagrant-digitalocean/actions/create"
8
+
9
+ module VagrantPlugins
10
+ module DigitalOcean
11
+ class Action
12
+ # Include the built-in callable actions, eg SSHExec
13
+ include Vagrant::Action::Builtin
14
+
15
+ def action(name)
16
+ send(name)
17
+ end
18
+
19
+ def destroy
20
+ return Vagrant::Action::Builder.new.tap do |builder|
21
+ builder.use ConfigValidate
22
+ builder.use Actions::Destroy
23
+ end
24
+ end
25
+
26
+ def read_state
27
+ return Vagrant::Action::Builder.new.tap do |builder|
28
+ builder.use ConfigValidate
29
+ builder.use Actions::ReadState
30
+ end
31
+ end
32
+
33
+ def ssh
34
+ return Vagrant::Action::Builder.new.tap do |builder|
35
+ builder.use ConfigValidate
36
+ builder.use SSHExec
37
+ end
38
+ end
39
+
40
+ def provision
41
+ return Vagrant::Action::Builder.new.tap do |builder|
42
+ builder.use ConfigValidate
43
+
44
+ # sort out sudo for redhat, etc
45
+ builder.use Actions::SetupSudo
46
+
47
+ # sort out sudo for redhat, etc
48
+ builder.use Actions::SetupUser
49
+
50
+ # execute provisioners
51
+ builder.use Provision
52
+
53
+ # setup provisioners, comes after Provision to force nfs folders
54
+ builder.use Actions::SetupProvisioner
55
+ end
56
+ end
57
+
58
+ def up
59
+ # TODO figure out when to exit if the vm is created
60
+ return Vagrant::Action::Builder.new.tap do |builder|
61
+ builder.use ConfigValidate
62
+
63
+ # build the vm if necessary
64
+ builder.use Actions::Create
65
+
66
+ builder.use provision
67
+
68
+ # set the host and remote ips for NFS
69
+ builder.use Actions::SetupNFS
70
+
71
+ # mount the nfs folders which should be all shared folders
72
+ builder.use NFS
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,103 @@
1
+ require "vagrant-digitalocean/helpers/client"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Actions
6
+ class Create
7
+ include Vagrant::Util::Retryable
8
+
9
+ def initialize(app, env)
10
+ @app, @env = app, env
11
+ @client = Helpers::Client.new
12
+ @translator = Helpers::Translator.new("actions.create")
13
+ end
14
+
15
+ def call(env)
16
+ # if the machine state is created skip
17
+ if env[:machine].state.id == :active
18
+ env[:ui].info @translator.t("skip")
19
+ return @app.call(env)
20
+ end
21
+
22
+ # TODO check the content of the key to see if it's changed
23
+ # TODO use the directory / vm name to qualify the key name
24
+ begin
25
+ ssh_key_id = @client
26
+ .request("/ssh_keys/")
27
+ .find_id(:ssh_keys, :name => "Vagrant Insecure")
28
+ rescue Errors::ResultMatchError
29
+ env[:ui].info @translator.t("create_key")
30
+
31
+ key = DigitalOcean.vagrant_key
32
+
33
+ result = @client.request("/ssh_keys/new", {
34
+ :name => "Vagrant Insecure",
35
+ :ssh_pub_key => key
36
+ })
37
+
38
+ ssh_key_id = result["ssh_key"]["id"]
39
+ end
40
+
41
+ size_id = @client
42
+ .request("/sizes")
43
+ .find_id(:sizes, :name => env[:machine].provider_config.size)
44
+
45
+ image_id = @client
46
+ .request("/images", { :filter => "global" })
47
+ .find_id(:images, :name => env[:machine].provider_config.image)
48
+
49
+ region_id = @client
50
+ .request("/regions")
51
+ .find_id(:regions, :name => env[:machine].provider_config.region)
52
+
53
+ env[:ui].info @translator.t("create_droplet")
54
+
55
+ result = @client.request("/droplets/new", {
56
+ :size_id => size_id,
57
+ :region_id => region_id,
58
+ :image_id => image_id,
59
+ # TODO use the current directory name as a post fix
60
+ :name => "vagrant",
61
+ :ssh_key_ids => ssh_key_id
62
+ })
63
+
64
+ # assign the machine id for reference in other commands
65
+ env[:machine].id = result["droplet"]["id"]
66
+
67
+ env[:ui].info @translator.t("wait_active")
68
+
69
+ retryable(:tries => 30, :sleep => 10) do
70
+ # If we're interrupted don't worry about waiting
71
+ next if env[:interrupted]
72
+
73
+ # Wait for the server to be ready
74
+ raise "not ready" if env[:machine].state.id != :active
75
+ end
76
+
77
+ # signal that the machine has just been created, used in ReadState
78
+ env[:machine_just_created] = true
79
+
80
+ @app.call(env)
81
+ end
82
+
83
+ # Both the recover and terminate are stolen almost verbatim from
84
+ # the Vagrant AWS provider up action
85
+ def recover(env)
86
+ return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError)
87
+
88
+ if env[:machine].state.id != :not_created
89
+ terminate(env)
90
+ end
91
+ end
92
+
93
+ def terminate(env)
94
+ destroy_env = env.dup
95
+ destroy_env.delete(:interrupted)
96
+ destroy_env[:config_validate] = false
97
+ destroy_env[:force_confirm_destroy] = true
98
+ env[:action_runner].run(ActionDispatch.new.destroy, destroy_env)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,44 @@
1
+ require "vagrant-digitalocean/helpers/client"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Actions
6
+ class Destroy
7
+ include Vagrant::Util::Retryable
8
+
9
+ def initialize(app, env)
10
+ @app, @env = app, env
11
+ @client = Helpers::Client.new
12
+ @translator = Helpers::Translator.new("actions.destroy")
13
+ end
14
+
15
+ def call(env)
16
+ # TODO remove the key associated with this machine
17
+ if [:active, :new].include?(env[:machine].state.id)
18
+ env[:ui].info @translator.t("destroying")
19
+ result = @client.request("/droplets/#{env[:machine].id}/destroy")
20
+
21
+ env[:ui].info @translator.t("wait_off")
22
+
23
+ retryable(:tries => 30, :sleep => 10) do
24
+ # If we're interrupted don't worry about waiting
25
+ next if env[:interrupted]
26
+
27
+ # Wait for the server to be ready
28
+ raise "not off" if env[:machine].state.id != :off
29
+ end
30
+ else
31
+ env[:ui].info @translator.t("not_active_or_new")
32
+ end
33
+
34
+ # make sure to remove the export when the machine is destroyed
35
+ # private in some hosts and requires a send
36
+ env[:ui].info @translator.t("clean_nfs")
37
+ env[:host].send(:nfs_cleanup, env[:machine].id)
38
+
39
+ @app.call(env)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ require "vagrant-digitalocean/helpers/client"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Actions
6
+ class ReadState
7
+ def initialize(app, env)
8
+ @app, @env = app, env
9
+ @client = Helpers::Client.new
10
+ end
11
+
12
+ def call(env)
13
+ # If we have a machine id ask the api what the state is
14
+ if env[:machine].id
15
+ droplet = @client.request("/droplets/#{env[:machine].id}")["droplet"]
16
+
17
+ env[:machine_state] = droplet
18
+ end
19
+
20
+ # no id signals that the machine hasn't yet been created
21
+ env[:machine_state] ||= {"status" => :not_created}
22
+
23
+ # TODO there has to be a better way, see UP for when
24
+ # :machine_just_created is set
25
+ env[:machine_state][:just_created] = env[:machine_just_created]
26
+ @app.call(env)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,55 @@
1
+ require "socket"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Actions
6
+ class SetupNFS
7
+ include Helpers::File
8
+
9
+ def initialize(app, env)
10
+ @app, @env = app, env
11
+ @translator = Helpers::Translator.new("actions.setup_nfs")
12
+ end
13
+
14
+ def call(env)
15
+ # set the nfs machine ip
16
+ env[:nfs_machine_ip] = env[:machine].provider.ssh_info[:host]
17
+ env[:ui].info @translator.t("machine_ip", :ip => env[:nfs_machine_ip])
18
+
19
+ # get the host ip from the local adapters
20
+ env[:nfs_host_ip] = determine_host_ip.ip_address
21
+ env[:ui].info @translator.t("host_ip", :ip => env[:nfs_host_ip])
22
+
23
+ # make sure the nfs server is setup
24
+ env[:ui].info @translator.t("install")
25
+ env[:machine].communicate.execute(nfs_install(env[:machine].guest))
26
+
27
+ vm = env[:machine].config.vm
28
+
29
+ # force all shard folders to use nfs
30
+ env[:ui].warn @translator.t("force_shared_folders")
31
+ folders = vm.synced_folders.keys.each do |key|
32
+ vm.synced_folders[key][:nfs] = true
33
+ end
34
+
35
+ @app.call(env)
36
+ end
37
+
38
+ # http://stackoverflow.com/questions/5029427/ruby-get-local-ip-nix
39
+ # TODO this is currently *nix only according to the above post
40
+ def determine_host_ip
41
+ Socket.ip_address_list.detect do |intf|
42
+ intf.ipv4? &&
43
+ !intf.ipv4_loopback? &&
44
+ !intf.ipv4_multicast? &&
45
+ !intf.ipv4_private?
46
+ end
47
+ end
48
+
49
+ def nfs_install(guest)
50
+ read_script("nfs", guest)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,30 @@
1
+ require "vagrant-digitalocean/helpers/file"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Actions
6
+ class SetupProvisioner
7
+ include Helpers::File
8
+
9
+ def initialize(app, env)
10
+ @app, @env = app, env
11
+ @translator = Helpers::Translator.new("actions.setup_provisioner")
12
+ end
13
+
14
+ def call(env)
15
+ # TODO prevent setup when no chef provisioner declared
16
+ # TODO catch ssh failure and report back on install issues
17
+ # TODO first check to see if it's installed and then skip the info
18
+ env[:ui].info @translator.t("install", :provisioner => "chef-solo")
19
+ env[:machine].communicate.execute(chef_install(env[:machine].guest))
20
+
21
+ @app.call(env)
22
+ end
23
+
24
+ def chef_install(guest)
25
+ read_script("chef", guest)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ require "vagrant-digitalocean/helpers/file"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Actions
6
+ class SetupSudo
7
+ include Helpers::File
8
+
9
+ def initialize(app, env)
10
+ @app, @env = app, env
11
+ @translator = Helpers::Translator.new("actions.setup_sudo")
12
+ end
13
+
14
+ def call(env)
15
+ env[:ui].info @translator.t("exec")
16
+ env[:machine].communicate.execute(fix_sudo(env[:machine].guest))
17
+
18
+ @app.call(env)
19
+ end
20
+
21
+ def fix_sudo(guest)
22
+ read_script("sudo", guest, false)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ module Actions
4
+ class SetupUser
5
+ def initialize(app, env)
6
+ @app, @env = app, env
7
+ @translator = Helpers::Translator.new("actions.setup_user")
8
+ end
9
+
10
+ def call(env)
11
+ # create the user, set the password to username, add to sudoers
12
+ # NOTE assumes group with username is created with useradd
13
+ env[:ui].info @translator.t("create", :user => user)
14
+ env[:machine].communicate.execute(<<-BASH)
15
+ if ! (grep #{user} /etc/passwd); then
16
+ useradd -m -s /bin/bash #{user};
17
+ echo -e "#{user}\n#{user}" | (passwd #{user});
18
+ fi
19
+ BASH
20
+
21
+ env[:ui].info @translator.t("sudo", :user => user)
22
+ env[:machine].communicate.execute(<<-BASH)
23
+ if ! (grep #{user} /etc/sudoers); then
24
+ echo "#{user} ALL=(ALL:ALL) ALL" >> /etc/sudoers;
25
+ fi
26
+ BASH
27
+
28
+ # create the .ssh directory in the users home
29
+ env[:machine].communicate.execute("su #{user} -c 'mkdir -p ~/.ssh'")
30
+
31
+ env[:ui].info @translator.t("key")
32
+ # add the specified key to the authorized keys file
33
+ env[:machine].communicate.execute(<<-BASH)
34
+ if ! grep '#{pub_key}' /home/#{user}/.ssh/authorized_keys; then
35
+ echo '#{pub_key}' >> /home/#{user}/.ssh/authorized_keys;
36
+ fi
37
+ BASH
38
+
39
+ env[:machine_state] ||= {}
40
+ env[:machine_state][:user] = user
41
+
42
+ @app.call(env)
43
+ end
44
+
45
+ private
46
+
47
+ # TODO use a config option to allow for alternate users
48
+ def user
49
+ "vagrant"
50
+ end
51
+
52
+ # TODO allow for a custom key to specified
53
+ def pub_key
54
+ @key ||= DigitalOcean.vagrant_key
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ class Config < Vagrant.plugin("2", :config)
4
+ attr_accessor :client_id, :api_key, :image, :region, :size
5
+
6
+ def initialize
7
+ @client_id = UNSET_VALUE
8
+ @api_key = UNSET_VALUE
9
+ @image = UNSET_VALUE
10
+ @region = UNSET_VALUE
11
+ @size = UNSET_VALUE
12
+ end
13
+
14
+ def finalize!
15
+ @client_id = ENV["DO_CLIENT_ID"] if @client_id == UNSET_VALUE
16
+ @api_key = ENV["DO_API_KEY"] if @api_key == UNSET_VALUE
17
+ @image = "Ubuntu 12.04 x32 Server" if @image == UNSET_VALUE
18
+ @region = "New York 1" if @region == UNSET_VALUE
19
+ @size = "512MB" if @size == UNSET_VALUE
20
+ end
21
+
22
+ def validate(machine)
23
+ errors = []
24
+ errors << "Client ID required" if !@client_id
25
+ errors << "API Key required" if !@api_key
26
+
27
+ { "Digital Ocean Provider" => errors }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ require "vagrant"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Errors
6
+ class DigitalOceanError < Vagrant::Errors::VagrantError
7
+ error_namespace("vagrant_digital_ocean.errors")
8
+ end
9
+
10
+ class APIStatusError < DigitalOceanError
11
+ error_key(:api_status)
12
+ end
13
+
14
+ class JSONError < DigitalOceanError
15
+ error_key(:json)
16
+ end
17
+
18
+ class ResultMatchError < DigitalOceanError
19
+ error_key(:result_match)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ require "vagrant-digitalocean/helpers/result"
2
+ require "faraday"
3
+ require "json"
4
+
5
+ module VagrantPlugins
6
+ module DigitalOcean
7
+ module Helpers
8
+ class Client
9
+ def initialize
10
+ @client = Faraday.new(:url => "https://api.digitalocean.com/")
11
+ end
12
+
13
+ def request(path, params = {})
14
+ # create the key
15
+ result = @client.get(path, params = params.merge({
16
+ :client_id => ENV["DO_CLIENT_ID"],
17
+ :api_key => ENV["DO_API_KEY"]
18
+ }))
19
+
20
+ # remove the api key in case an error gets dumped to the console
21
+ params[:api_key] = "REMOVED"
22
+
23
+ begin
24
+ body = JSON.parse(result.body)
25
+ rescue JSON::ParserError => e
26
+ raise(Errors::JSONError, {
27
+ :message => e.message,
28
+ :path => path,
29
+ :params => params,
30
+ :response => result.body
31
+ })
32
+ end
33
+
34
+ if body["status"] != "OK"
35
+ raise(Errors::APIStatusError, {
36
+ :path => path,
37
+ :params => params,
38
+ :status => body["status"],
39
+ :response => body.inspect
40
+ })
41
+ end
42
+
43
+ Result.new(body)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Helpers
6
+ module File
7
+ # TODO the optional exceptions are clumsy
8
+ def read_file(relative_path, except = true)
9
+ content = ""
10
+ path = ::File.join(DigitalOcean.source_root, relative_path)
11
+
12
+ begin
13
+ ::File.open(path) do |file|
14
+ content = file.read
15
+ end
16
+ rescue Errno::ENOENT => e
17
+ # ignore the missing file if except is false
18
+ raise(e) if except
19
+ end
20
+
21
+ content
22
+ end
23
+
24
+ # TODO the optional exceptions are clumsy
25
+ # read a script and match it to the guest operating system
26
+ def read_script(dir, guest, except = true)
27
+ script_dir = ::File.join("scripts", dir)
28
+ guest_name = guest.class.to_s
29
+
30
+ if guest_name =~ /Debian/ || guest_name =~ /Ubuntu/
31
+ read_file(::File.join(script_dir, "debian.sh"), except)
32
+ elsif guest_name =~ /RedHat/
33
+ read_file(::File.join(script_dir, "redhat.sh"), except)
34
+ else
35
+ raise "unsupported guest operating system" if except
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ module Helpers
4
+ class Result
5
+ def initialize(body)
6
+ @result = body
7
+ end
8
+
9
+ def [](key)
10
+ @result[key.to_s]
11
+ end
12
+
13
+ def find_id(sub_obj, search)
14
+ find(sub_obj, search)["id"]
15
+ end
16
+
17
+ def find(sub_obj, search)
18
+ key = search.keys.first
19
+ value = search[key].to_s
20
+ key = key.to_s
21
+
22
+ result = @result[sub_obj.to_s].inject(nil) do |result, obj|
23
+ obj[key] == value ? obj : result
24
+ end
25
+
26
+ result || error(sub_obj, key, value)
27
+ end
28
+
29
+ def error(sub_obj, key, value)
30
+ raise(Errors::ResultMatchError, {
31
+ :key => key,
32
+ :value => value,
33
+ :collection_name => sub_obj.to_s,
34
+ :sub_obj => @result[sub_obj.to_s]
35
+ })
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ module Helpers
4
+ class Translator
5
+ def self.plugin_namespace=(val)
6
+ @@plugin_namespace = val
7
+ end
8
+
9
+ def initialize(namespace)
10
+ @namespace = namespace
11
+ end
12
+
13
+ def t(keys, opts = {})
14
+ value = I18n.t("#{@@plugin_namespace}.#{@namespace}.#{keys}", opts)
15
+ opts[:progress] == false ? value : value + " ..."
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ require "i18n"
2
+ require "vagrant-digitalocean/helpers/translator"
3
+
4
+ module VagrantPlugins
5
+ module DigitalOcean
6
+ class Plugin < Vagrant.plugin("2")
7
+ name "DigitalOcean"
8
+ description <<-DESC
9
+ This plugin installs a provider that allows Vagrant to manage
10
+ machines using DigitalOcean's API.
11
+ DESC
12
+
13
+ config(:digital_ocean, :provider) do
14
+ require_relative "config"
15
+ Config
16
+ end
17
+
18
+ provider(:digital_ocean) do
19
+ # Return the provider
20
+ require_relative "provider"
21
+
22
+ I18n.load_path << File.expand_path("locales/en.yml", DigitalOcean.source_root)
23
+ I18n.reload!
24
+ Helpers::Translator.plugin_namespace = "vagrant_digital_ocean"
25
+
26
+ Provider
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,84 @@
1
+ require "vagrant-digitalocean/action"
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ class Provider < Vagrant.plugin("2", :provider)
6
+ # Initialize the provider to represent the given machine.
7
+ #
8
+ # @param [Vagrant::Machine] machine The machine that this provider
9
+ # is responsible for.
10
+ def initialize(machine)
11
+ @machine = machine
12
+ @dispatch = Action.new
13
+ end
14
+
15
+ # This should return an action callable for the given name.
16
+ #
17
+ # @param [Symbol] name Name of the action.
18
+ # @return [Object] A callable action sequence object, whether it
19
+ # is a proc, object, etc.
20
+ def action(name)
21
+ return @dispatch.action(name) if @dispatch.respond_to?(name)
22
+ nil
23
+ end
24
+
25
+ # This method is called if the underying machine ID changes. Providers
26
+ # can use this method to load in new data for the actual backing
27
+ # machine or to realize that the machine is now gone (the ID can
28
+ # become `nil`). No parameters are given, since the underlying machine
29
+ # is simply the machine instance given to this object. And no
30
+ # return value is necessary.
31
+ def machine_id_changed
32
+ end
33
+
34
+ # This should return a hash of information that explains how to
35
+ # SSH into the machine. If the machine is not at a point where
36
+ # SSH is even possible, then `nil` should be returned.
37
+ #
38
+ # The general structure of this returned hash should be the
39
+ # following:
40
+ #
41
+ # {
42
+ # :host => "1.2.3.4",
43
+ # :port => "22",
44
+ # :username => "mitchellh",
45
+ # :private_key_path => "/path/to/my/key"
46
+ # }
47
+ #
48
+ # **Note:** Vagrant only supports private key based authenticatonion,
49
+ # mainly for the reason that there is no easy way to exec into an
50
+ # `ssh` prompt with a password, whereas we can pass a private key
51
+ # via commandline.
52
+ #
53
+ # @return [Hash] SSH information. For the structure of this hash
54
+ # read the accompanying documentation for this method.
55
+ def ssh_info
56
+ state = @machine.action("read_state")[:machine_state]
57
+
58
+ return nil if state["status"] == :not_created
59
+
60
+ return {
61
+ :host => state["ip_address"],
62
+ :port => "22",
63
+ :username => "root",
64
+ :private_key_path => Vagrant.source_root + "keys/vagrant"
65
+ }
66
+ end
67
+
68
+ # This should return the state of the machine within this provider.
69
+ # The state must be an instance of {MachineState}. Please read the
70
+ # documentation of that class for more information.
71
+ #
72
+ # @return [MachineState]
73
+ def state
74
+ state_id = @machine.action("read_state")[:machine_state]["status"].to_sym
75
+
76
+ # TODO provide an actual description
77
+ long = short = state_id.to_s
78
+
79
+ # Return the MachineState object
80
+ Vagrant::MachineState.new(state_id, short, long)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ module VagrantPlugins
2
+ module Digitalocean
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ require "vagrant"
2
+ require "vagrant-digitalocean/version"
3
+ require "vagrant-digitalocean/plugin"
4
+ require "vagrant-digitalocean/errors"
5
+
6
+ module VagrantPlugins
7
+ module DigitalOcean
8
+ def self.source_root
9
+ @source_root ||= Pathname.new(File.expand_path('../../', __FILE__))
10
+ end
11
+
12
+ def self.vagrant_key
13
+ file = File.open(Vagrant.source_root + "keys/vagrant.pub")
14
+ key = file.read
15
+ file.close
16
+ key
17
+ end
18
+ end
19
+ end
data/locales/en.yml ADDED
@@ -0,0 +1,53 @@
1
+ en:
2
+ vagrant_digital_ocean:
3
+ errors:
4
+ api_status: |-
5
+ There was an issue with the request made to the Digital Ocean API at:
6
+
7
+ Path: %{path}
8
+ URI Params: %{params}
9
+
10
+ The response status from the API was:
11
+
12
+ Status: %{status}
13
+ Response: %{response}
14
+ :json: |-
15
+ There was an issue with the JSON response from the Digital Ocean API at:
16
+
17
+ Path: %{path}
18
+ URI Params: %{params}
19
+
20
+ The response JSON from the API was:
21
+
22
+ Response: %{response}
23
+ :result_match: |-
24
+ The result collection for %{collection_name}:
25
+
26
+ %{sub_obj}
27
+
28
+ Contained no object with the value "%{value}" for the the key "%{key}".
29
+ Please ensure that the configured value exists in the collection.
30
+ actions:
31
+ create:
32
+ skip: "Droplet is active, skipping creation"
33
+ create_key: "Adding key client account"
34
+ create_droplet: "Creating the droplet"
35
+ wait_active: "Waiting for the droplet to become active (>= 1 min)"
36
+ destroy:
37
+ destroying: "Destroying droplet"
38
+ not_active_or_new: "Droplet not in the `active` or `new` state"
39
+ clean_nfs: "Cleaning up NFS exports, may require sudo password"
40
+ wait_off: "Waiting for the droplet to be destroyed"
41
+ setup_sudo:
42
+ exec: "Making alterations to the sudoers file where necessary"
43
+ setup_nfs:
44
+ machine_ip: "Droplet IP: %{ip}"
45
+ host_ip: "Host IP: %{ip}"
46
+ install: "Installing NFS on the droplet"
47
+ force_shared_folders: "Forcing shared folders to use NFS where necessary"
48
+ setup_provisioner:
49
+ install: "Installing provisioner: %{provisioner} (>= 2 min)"
50
+ setup_user:
51
+ create: "Creating user '%{user}' and setting password"
52
+ sudo: "Enabling sudo for user '%{user}'"
53
+ key: "Adding public key to authorized_keys"
@@ -0,0 +1,20 @@
1
+ # cargo culted from OpsCode's guide at http://wiki.opscode.com/display/chef/Installing+Chef+Client+on+Ubuntu+or+Debian
2
+ # skip if the keys are present
3
+ if (which chef-solo); then exit 0; fi
4
+
5
+ # install the basics
6
+ apt-get install -y wget lsb-release;
7
+
8
+ # add the opscode repo to the sources
9
+ echo "deb http://apt.opscode.com/ `lsb_release -cs`-0.10 main" | sudo tee /etc/apt/sources.list.d/opscode.list;
10
+
11
+ # setup the gpg key
12
+ mkdir -p /etc/apt/trusted.gpg.d;
13
+ gpg --keyserver keys.gnupg.net --recv-keys 83EF826A;
14
+ gpg --export packages@opscode.com | sudo tee /etc/apt/trusted.gpg.d/opscode-keyring.gpg > /dev/null;
15
+
16
+ # update the repo to make sure that the package is available from the repo
17
+ apt-get update;
18
+
19
+ # avoid server url popup
20
+ echo "chef chef/chef_server_url string https://api.opscode.com/organizations/vagrant" | debconf-set-selections && apt-get install chef -y;
@@ -0,0 +1,21 @@
1
+ # cargo culted from OpsCode's guide at http://wiki.opscode.com/display/chef/Installing+Chef+Client+on+CentOS
2
+ # skip if chef-solo is present
3
+ if (which chef-solo); then exit 0; fi
4
+
5
+ # add the repo for ruby and other deps
6
+ rpm -Uvh http://rbel.frameos.org/rbel6
7
+
8
+ # install the pre-reqs
9
+ yum install -y ruby ruby-devel ruby-ri ruby-rdoc ruby-shadow gcc gcc-c++ automake autoconf make curl dmidecode
10
+
11
+ # install rubygems
12
+ if ! (which gem); then
13
+ cd /tmp
14
+ curl -O http://production.cf.rubygems.org/rubygems/rubygems-1.8.10.tgz
15
+ tar zxf rubygems-1.8.10.tgz
16
+ cd rubygems-1.8.10
17
+ ruby setup.rb --no-format-executable
18
+ fi
19
+
20
+ # install chef via rubygems
21
+ gem install chef --no-ri --no-rdoc
@@ -0,0 +1,4 @@
1
+ # install the nfs-kernel server
2
+ if !(which nfsstat); then
3
+ apt-get install -y nfs-kernel-server;
4
+ fi
@@ -0,0 +1,11 @@
1
+ if (service --status-all | grep nfs); then exit 0; fi;
2
+
3
+ # install the nfs-kernel server
4
+ yum install -y nfs-utils nfs-utils-lib;
5
+
6
+ # add to startup
7
+ chkconfig nfs on;
8
+
9
+ # make sure it's on after the install
10
+ service rpcbind start;
11
+ service nfs start;
@@ -0,0 +1,2 @@
1
+ # fix the default sudoers file to prevent the tty requirement
2
+ sed -i'.bk' -e 's/\(Defaults\s\+requiretty\)/# \1/' /etc/sudoers
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'vagrant-digitalocean/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "vagrant-digitalocean"
8
+ gem.version = VagrantPlugins::Digitalocean::VERSION
9
+ gem.authors = ["John Bender"]
10
+ gem.email = ["john.m.bender@gmail.com"]
11
+ gem.description = %q{Enables Vagrant to manage Digital Ocean droplets}
12
+ gem.summary = gem.description
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency "faraday", "~> 0.8.6"
20
+ gem.add_dependency "json", "~> 1.6.6"
21
+ gem.add_dependency "log4r", "~> 1.1.9"
22
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vagrant-digitalocean
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Bender
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.6
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.8.6
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.6.6
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.6.6
46
+ - !ruby/object:Gem::Dependency
47
+ name: log4r
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.1.9
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.9
62
+ description: Enables Vagrant to manage Digital Ocean droplets
63
+ email:
64
+ - john.m.bender@gmail.com
65
+ executables:
66
+ - build.sh
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - .gitignore
71
+ - Gemfile
72
+ - LICENSE.txt
73
+ - README.md
74
+ - Rakefile
75
+ - bin/build.sh
76
+ - box/Vagrantfile
77
+ - box/digital_ocean.box
78
+ - box/metadata.json
79
+ - lib/vagrant-digitalocean.rb
80
+ - lib/vagrant-digitalocean/action.rb
81
+ - lib/vagrant-digitalocean/actions/create.rb
82
+ - lib/vagrant-digitalocean/actions/destroy.rb
83
+ - lib/vagrant-digitalocean/actions/read_state.rb
84
+ - lib/vagrant-digitalocean/actions/setup_nfs.rb
85
+ - lib/vagrant-digitalocean/actions/setup_provisioner.rb
86
+ - lib/vagrant-digitalocean/actions/setup_sudo.rb
87
+ - lib/vagrant-digitalocean/actions/setup_user.rb
88
+ - lib/vagrant-digitalocean/config.rb
89
+ - lib/vagrant-digitalocean/errors.rb
90
+ - lib/vagrant-digitalocean/helpers/client.rb
91
+ - lib/vagrant-digitalocean/helpers/file.rb
92
+ - lib/vagrant-digitalocean/helpers/result.rb
93
+ - lib/vagrant-digitalocean/helpers/translator.rb
94
+ - lib/vagrant-digitalocean/plugin.rb
95
+ - lib/vagrant-digitalocean/provider.rb
96
+ - lib/vagrant-digitalocean/version.rb
97
+ - locales/en.yml
98
+ - scripts/chef/debian.sh
99
+ - scripts/chef/redhat.sh
100
+ - scripts/nfs/debian.sh
101
+ - scripts/nfs/redhat.sh
102
+ - scripts/sudo/redhat.sh
103
+ - vagrant-digitalocean.gemspec
104
+ homepage:
105
+ licenses: []
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 1.8.23
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: Enables Vagrant to manage Digital Ocean droplets
128
+ test_files: []