vagrant-chassis-digitalocean 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +19 -0
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE.txt +23 -0
  5. data/README.md +95 -0
  6. data/Rakefile +22 -0
  7. data/box/digital_ocean.box +0 -0
  8. data/box/metadata.json +3 -0
  9. data/lib/vagrant-chassis-digitalocean.rb +20 -0
  10. data/lib/vagrant-chassis-digitalocean/actions.rb +160 -0
  11. data/lib/vagrant-chassis-digitalocean/actions/check_state.rb +19 -0
  12. data/lib/vagrant-chassis-digitalocean/actions/create.rb +96 -0
  13. data/lib/vagrant-chassis-digitalocean/actions/destroy.rb +35 -0
  14. data/lib/vagrant-chassis-digitalocean/actions/modify_provision_path.rb +38 -0
  15. data/lib/vagrant-chassis-digitalocean/actions/power_off.rb +33 -0
  16. data/lib/vagrant-chassis-digitalocean/actions/power_on.rb +34 -0
  17. data/lib/vagrant-chassis-digitalocean/actions/rebuild.rb +52 -0
  18. data/lib/vagrant-chassis-digitalocean/actions/reload.rb +31 -0
  19. data/lib/vagrant-chassis-digitalocean/actions/setup_key.rb +58 -0
  20. data/lib/vagrant-chassis-digitalocean/actions/setup_sudo.rb +41 -0
  21. data/lib/vagrant-chassis-digitalocean/actions/setup_user.rb +64 -0
  22. data/lib/vagrant-chassis-digitalocean/actions/sync_folders.rb +91 -0
  23. data/lib/vagrant-chassis-digitalocean/commands/rebuild.rb +23 -0
  24. data/lib/vagrant-chassis-digitalocean/config.rb +62 -0
  25. data/lib/vagrant-chassis-digitalocean/errors.rb +37 -0
  26. data/lib/vagrant-chassis-digitalocean/helpers/client.rb +88 -0
  27. data/lib/vagrant-chassis-digitalocean/helpers/result.rb +40 -0
  28. data/lib/vagrant-chassis-digitalocean/plugin.rb +26 -0
  29. data/lib/vagrant-chassis-digitalocean/provider.rb +100 -0
  30. data/lib/vagrant-chassis-digitalocean/version.rb +5 -0
  31. data/locales/en.yml +84 -0
  32. data/test/Vagrantfile +38 -0
  33. data/test/cookbooks/test/recipes/default.rb +1 -0
  34. data/test/scripts/provision.sh +3 -0
  35. data/test/test.sh +14 -0
  36. data/test/test_id_rsa +27 -0
  37. data/test/test_id_rsa.pub +1 -0
  38. data/vagrant-chassis-digitalocean.gemspec +21 -0
  39. metadata +137 -0
@@ -0,0 +1,23 @@
1
+ require 'optparse'
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ module Commands
6
+ class Rebuild < Vagrant.plugin('2', :command)
7
+ def execute
8
+ opts = OptionParser.new do |o|
9
+ o.banner = 'Usage: vagrant rebuild [vm-name]'
10
+ end
11
+
12
+ argv = parse_options(opts)
13
+
14
+ with_target_vms(argv) do |machine|
15
+ machine.action(:rebuild)
16
+ end
17
+
18
+ 0
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,62 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ class Config < Vagrant.plugin('2', :config)
4
+ attr_accessor :client_id
5
+ attr_accessor :api_key
6
+ attr_accessor :image
7
+ attr_accessor :region
8
+ attr_accessor :size
9
+ attr_accessor :private_networking
10
+ attr_accessor :backups_enabled
11
+ attr_accessor :ca_path
12
+ attr_accessor :ssh_key_name
13
+ attr_accessor :setup
14
+
15
+ alias_method :setup?, :setup
16
+
17
+ def initialize
18
+ @client_id = UNSET_VALUE
19
+ @api_key = UNSET_VALUE
20
+ @image = UNSET_VALUE
21
+ @region = UNSET_VALUE
22
+ @size = UNSET_VALUE
23
+ @private_networking = UNSET_VALUE
24
+ @backups_enable = UNSET_VALUE
25
+ @ca_path = UNSET_VALUE
26
+ @ssh_key_name = UNSET_VALUE
27
+ @setup = UNSET_VALUE
28
+ end
29
+
30
+ def finalize!
31
+ @client_id = ENV['DO_CLIENT_ID'] if @client_id == UNSET_VALUE
32
+ @api_key = ENV['DO_API_KEY'] if @api_key == UNSET_VALUE
33
+ @image = 'Ubuntu 12.04.3 x64' if @image == UNSET_VALUE
34
+ @region = 'New York 2' if @region == UNSET_VALUE
35
+ @size = '512MB' if @size == UNSET_VALUE
36
+ @private_networking = false if @private_networking == UNSET_VALUE
37
+ @backups_enabled = false if @backups_enabled == UNSET_VALUE
38
+ @ca_path = nil if @ca_path == UNSET_VALUE
39
+ @ssh_key_name = 'Vagrant' if @ssh_key_name == UNSET_VALUE
40
+ @setup = true if @setup == UNSET_VALUE
41
+ end
42
+
43
+ def validate(machine)
44
+ errors = []
45
+ errors << I18n.t('vagrant_digital_ocean.config.client_id') if !@client_id
46
+ errors << I18n.t('vagrant_digital_ocean.config.api_key') if !@api_key
47
+
48
+ key = machine.config.ssh.private_key_path
49
+ key = key[0] if key.is_a?(Array)
50
+ if !key
51
+ errors << I18n.t('vagrant_digital_ocean.config.private_key')
52
+ elsif !File.file?(File.expand_path("#{key}.pub", machine.env.root_path))
53
+ errors << I18n.t('vagrant_digital_ocean.config.public_key', {
54
+ :key => "#{key}.pub"
55
+ })
56
+ end
57
+
58
+ { 'Digital Ocean Provider' => errors }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,37 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ module Errors
4
+ class DigitalOceanError < Vagrant::Errors::VagrantError
5
+ error_namespace("vagrant_digital_ocean.errors")
6
+ end
7
+
8
+ class APIStatusError < DigitalOceanError
9
+ error_key(:api_status)
10
+ end
11
+
12
+ class JSONError < DigitalOceanError
13
+ error_key(:json)
14
+ end
15
+
16
+ class ResultMatchError < DigitalOceanError
17
+ error_key(:result_match)
18
+ end
19
+
20
+ class CertificateError < DigitalOceanError
21
+ error_key(:certificate)
22
+ end
23
+
24
+ class LocalIPError < DigitalOceanError
25
+ error_key(:local_ip)
26
+ end
27
+
28
+ class PublicKeyError < DigitalOceanError
29
+ error_key(:public_key)
30
+ end
31
+
32
+ class RsyncError < DigitalOceanError
33
+ error_key(:rsync)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,88 @@
1
+ require 'vagrant-digitalocean/helpers/result'
2
+ require 'faraday'
3
+ require 'json'
4
+
5
+ module VagrantPlugins
6
+ module DigitalOcean
7
+ module Helpers
8
+ module Client
9
+ def client
10
+ @client ||= ApiClient.new(@machine)
11
+ end
12
+ end
13
+
14
+ class ApiClient
15
+ include Vagrant::Util::Retryable
16
+
17
+ def initialize(machine)
18
+ @logger = Log4r::Logger.new('vagrant::digitalocean::apiclient')
19
+ @config = machine.provider_config
20
+ @client = Faraday.new({
21
+ :url => 'https://api.digitalocean.com/',
22
+ :ssl => {
23
+ :ca_file => @config.ca_path
24
+ }
25
+ })
26
+ end
27
+
28
+ def request(path, params = {})
29
+ begin
30
+ @logger.info "Request: #{path}"
31
+ result = @client.get(path, params = params.merge({
32
+ :client_id => @config.client_id,
33
+ :api_key => @config.api_key
34
+ }))
35
+ rescue Faraday::Error::ConnectionFailed => e
36
+ # TODO this is suspect but because farady wraps the exception
37
+ # in something generic there doesn't appear to be another
38
+ # way to distinguish different connection errors :(
39
+ if e.message =~ /certificate verify failed/
40
+ raise Errors::CertificateError
41
+ end
42
+
43
+ raise e
44
+ end
45
+
46
+ # remove the api key in case an error gets dumped to the console
47
+ params[:api_key] = 'REMOVED'
48
+
49
+ begin
50
+ body = JSON.parse(result.body)
51
+ @logger.info "Response: #{body}"
52
+ rescue JSON::ParserError => e
53
+ raise(Errors::JSONError, {
54
+ :message => e.message,
55
+ :path => path,
56
+ :params => params,
57
+ :response => result.body
58
+ })
59
+ end
60
+
61
+ if body['status'] != 'OK'
62
+ raise(Errors::APIStatusError, {
63
+ :path => path,
64
+ :params => params,
65
+ :status => body['status'],
66
+ :response => body.inspect
67
+ })
68
+ end
69
+
70
+ Result.new(body)
71
+ end
72
+
73
+ def wait_for_event(env, id)
74
+ retryable(:tries => 120, :sleep => 10) do
75
+ # stop waiting if interrupted
76
+ next if env[:interrupted]
77
+
78
+ # check event status
79
+ result = self.request("/events/#{id}")
80
+
81
+ yield result if block_given?
82
+ raise 'not ready' if result['event']['action_status'] != 'done'
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ 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,26 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ class Plugin < Vagrant.plugin('2')
4
+ name 'DigitalOcean'
5
+ description <<-DESC
6
+ This plugin installs a provider that allows Vagrant to manage
7
+ machines using DigitalOcean's API.
8
+ DESC
9
+
10
+ config(:digital_ocean, :provider) do
11
+ require_relative 'config'
12
+ Config
13
+ end
14
+
15
+ provider(:digital_ocean) do
16
+ require_relative 'provider'
17
+ Provider
18
+ end
19
+
20
+ command(:rebuild) do
21
+ require_relative 'commands/rebuild'
22
+ Commands::Rebuild
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,100 @@
1
+ require 'vagrant-digitalocean/actions'
2
+
3
+ module VagrantPlugins
4
+ module DigitalOcean
5
+ class Provider < Vagrant.plugin('2', :provider)
6
+
7
+ # This class method caches status for all droplets within
8
+ # the Digital Ocean account. A specific droplet's status
9
+ # may be refreshed by passing :refresh => true as an option.
10
+ def self.droplet(machine, opts = {})
11
+ client = Helpers::ApiClient.new(machine)
12
+
13
+ # load status of droplets if it has not been done before
14
+ if !@droplets
15
+ result = client.request('/droplets')
16
+ @droplets = result['droplets']
17
+ end
18
+
19
+ if opts[:refresh] && machine.id
20
+ # refresh the droplet status for the given machine
21
+ @droplets.delete_if { |d| d['id'].to_s == machine.id }
22
+ result = client.request("/droplets/#{machine.id}")
23
+ @droplets << droplet = result['droplet']
24
+ else
25
+ # lookup droplet status for the given machine
26
+ droplet = @droplets.find { |d| d['id'].to_s == machine.id }
27
+ end
28
+
29
+ # if lookup by id failed, check for a droplet with a matching name
30
+ # and set the id to ensure vagrant stores locally
31
+ # TODO allow the user to configure this behavior
32
+ if !droplet
33
+ name = machine.config.vm.hostname || machine.name
34
+ droplet = @droplets.find { |d| d['name'] == name.to_s }
35
+ machine.id = droplet['id'].to_s if droplet
36
+ end
37
+
38
+ droplet ||= {'status' => 'not_created'}
39
+ end
40
+
41
+ def initialize(machine)
42
+ @machine = machine
43
+ end
44
+
45
+ def action(name)
46
+ return Actions.send(name) if Actions.respond_to?(name)
47
+ nil
48
+ end
49
+
50
+ # This method is called if the underying machine ID changes. Providers
51
+ # can use this method to load in new data for the actual backing
52
+ # machine or to realize that the machine is now gone (the ID can
53
+ # become `nil`). No parameters are given, since the underlying machine
54
+ # is simply the machine instance given to this object. And no
55
+ # return value is necessary.
56
+ def machine_id_changed
57
+ end
58
+
59
+ # This should return a hash of information that explains how to
60
+ # SSH into the machine. If the machine is not at a point where
61
+ # SSH is even possible, then `nil` should be returned.
62
+ #
63
+ # The general structure of this returned hash should be the
64
+ # following:
65
+ #
66
+ # {
67
+ # :host => "1.2.3.4",
68
+ # :port => "22",
69
+ # :username => "mitchellh",
70
+ # :private_key_path => "/path/to/my/key"
71
+ # }
72
+ #
73
+ # **Note:** Vagrant only supports private key based authenticatonion,
74
+ # mainly for the reason that there is no easy way to exec into an
75
+ # `ssh` prompt with a password, whereas we can pass a private key
76
+ # via commandline.
77
+ def ssh_info
78
+ droplet = Provider.droplet(@machine)
79
+
80
+ return nil if droplet['status'].to_sym != :active
81
+
82
+ return {
83
+ :host => droplet['ip_address'],
84
+ :port => '22',
85
+ :username => 'root',
86
+ :private_key_path => nil
87
+ }
88
+ end
89
+
90
+ # This should return the state of the machine within this provider.
91
+ # The state must be an instance of {MachineState}. Please read the
92
+ # documentation of that class for more information.
93
+ def state
94
+ state = Provider.droplet(@machine)['status'].to_sym
95
+ long = short = state.to_s
96
+ Vagrant::MachineState.new(state, short, long)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,5 @@
1
+ module VagrantPlugins
2
+ module DigitalOcean
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
data/locales/en.yml ADDED
@@ -0,0 +1,84 @@
1
+ en:
2
+ vagrant_digital_ocean:
3
+ info:
4
+ off: "Droplet is off"
5
+ not_created: "Droplet has not been created"
6
+ already_active: "Droplet is already active"
7
+ already_off: "Droplet is already off"
8
+ creating: "Creating a new droplet..."
9
+ droplet_ip: "Assigned IP address: %{ip}"
10
+ droplet_private_ip: "Private IP address: %{ip}"
11
+ destroying: "Destroying the droplet..."
12
+ powering_off: "Powering off the droplet..."
13
+ powering_on: "Powering on the droplet..."
14
+ rebuilding: "Rebuilding the droplet..."
15
+ reloading: "Rebooting the droplet..."
16
+ creating_user: "Creating user account: %{user}..."
17
+ modifying_sudo: "Modifying sudoers file to remove tty requirement..."
18
+ using_key: "Using existing SSH key: %{name}"
19
+ creating_key: "Creating new SSH key: %{name}..."
20
+ trying_rsync_install: "Rsync not found, attempting to install with yum..."
21
+ rsyncing: "Rsyncing folder: %{hostpath} => %{guestpath}..."
22
+ config:
23
+ client_id: "Client ID is required"
24
+ api_key: "API key is required"
25
+ private_key: "SSH private key path is required"
26
+ public_key: "SSH public key not found: %{key}"
27
+ errors:
28
+ public_key: |-
29
+ There was an issue reading the public key at:
30
+
31
+ Path: %{path}
32
+
33
+ Please check the file's permissions.
34
+ api_status: |-
35
+ There was an issue with the request made to the Digital Ocean
36
+ API at:
37
+
38
+ Path: %{path}
39
+ URI Params: %{params}
40
+
41
+ The response status from the API was:
42
+
43
+ Status: %{status}
44
+ Response: %{response}
45
+ rsync: |-
46
+ There was an error when attemping to rsync a share folder.
47
+ Please inspect the error message below for more info.
48
+
49
+ Host path: %{hostpath}
50
+ Guest path: %{guestpath}
51
+ Error: %{stderr}
52
+ json: |-
53
+ There was an issue with the JSON response from the Digital Ocean
54
+ API at:
55
+
56
+ Path: %{path}
57
+ URI Params: %{params}
58
+
59
+ The response JSON from the API was:
60
+
61
+ Response: %{response}
62
+ result_match: |-
63
+ The result collection for %{collection_name}:
64
+
65
+ %{sub_obj}
66
+
67
+ Contained no object with the value "%{value}" for the the
68
+ key "%{key}".
69
+
70
+ Please ensure that the configured value exists in the collection.
71
+ certificate: |-
72
+ The secure connection to the Digital Ocean API has failed. Please
73
+ ensure that your local certificates directory is defined in the
74
+ provider config.
75
+
76
+ config.vm.provider :digital_ocean do |vm|
77
+ vm.ca_path = "/path/to/ssl/ca/cert.crt"
78
+ end
79
+
80
+ This is generally caused by the OpenSSL configuration associated
81
+ with the Ruby install being unaware of the system specific ca
82
+ certs.
83
+ local_ip: |-
84
+ The Digital Ocean provider was unable to determine the host's IP.