vagrant-chassis-digitalocean 1.0.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.
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.