vagrant-haipa 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.vscode/launch.json +29 -0
- data/Gemfile +20 -0
- data/LICENSE +373 -0
- data/README.md +111 -0
- data/Rakefile +24 -0
- data/box/haipa.box +0 -0
- data/box/metadata.json +3 -0
- data/lib/vagrant-haipa.rb +20 -0
- data/lib/vagrant-haipa/actions.rb +209 -0
- data/lib/vagrant-haipa/actions/check_state.rb +19 -0
- data/lib/vagrant-haipa/actions/connect_haipa.rb +24 -0
- data/lib/vagrant-haipa/actions/create_machine.rb +56 -0
- data/lib/vagrant-haipa/actions/delete_machine.rb +31 -0
- data/lib/vagrant-haipa/actions/is_created.rb +18 -0
- data/lib/vagrant-haipa/actions/is_stopped.rb +18 -0
- data/lib/vagrant-haipa/actions/message_already_created.rb +16 -0
- data/lib/vagrant-haipa/actions/message_not_created.rb +16 -0
- data/lib/vagrant-haipa/actions/message_will_not_destroy.rb +16 -0
- data/lib/vagrant-haipa/actions/modify_provision_path.rb +38 -0
- data/lib/vagrant-haipa/actions/set_name.rb +53 -0
- data/lib/vagrant-haipa/actions/shut_down.rb +33 -0
- data/lib/vagrant-haipa/actions/start_machine.rb +34 -0
- data/lib/vagrant-haipa/actions/stop_machine.rb +33 -0
- data/lib/vagrant-haipa/actions/wait_for_ip_address.rb +32 -0
- data/lib/vagrant-haipa/config.rb +54 -0
- data/lib/vagrant-haipa/errors.rb +41 -0
- data/lib/vagrant-haipa/helpers/client.rb +111 -0
- data/lib/vagrant-haipa/helpers/result.rb +40 -0
- data/lib/vagrant-haipa/plugin.rb +22 -0
- data/lib/vagrant-haipa/provider.rb +118 -0
- data/lib/vagrant-haipa/version.rb +5 -0
- data/locales/en.yml +95 -0
- data/test/Vagrantfile +43 -0
- data/test/cookbooks/test/recipes/default.rb +1 -0
- data/test/scripts/provision.sh +3 -0
- data/test/test.sh +13 -0
- data/test/test_id_rsa +27 -0
- data/test/test_id_rsa.pub +1 -0
- data/vagrant +27 -0
- data/vagrant-haipa.gemspec +21 -0
- metadata +132 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'vagrant-haipa/helpers/result'
|
2
|
+
require 'faraday'
|
3
|
+
require 'json'
|
4
|
+
module VagrantPlugins
|
5
|
+
module Haipa
|
6
|
+
module Helpers
|
7
|
+
module Client
|
8
|
+
def client
|
9
|
+
@client ||= ApiClient.new(@machine)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ApiClient
|
14
|
+
include Vagrant::Util::Retryable
|
15
|
+
|
16
|
+
def initialize(machine)
|
17
|
+
@logger = Log4r::Logger.new('vagrant::haipa::apiclient')
|
18
|
+
@config = machine.provider_config
|
19
|
+
@client = Faraday.new({
|
20
|
+
:url => 'http://localhost:62189/',
|
21
|
+
:ssl => {
|
22
|
+
:ca_file => @config.ca_path
|
23
|
+
}
|
24
|
+
})
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(path, params = {})
|
28
|
+
request(path, params, :delete)
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(path, params = {})
|
32
|
+
@client.headers['Content-Type'] = 'application/json'
|
33
|
+
request(path, params, :post)
|
34
|
+
end
|
35
|
+
|
36
|
+
def request(path, params = {}, method = :get)
|
37
|
+
begin
|
38
|
+
@logger.info "Request: #{path}"
|
39
|
+
result = @client.send(method) do |req|
|
40
|
+
req.url path
|
41
|
+
req.body = params.to_json unless method == :get
|
42
|
+
req.params = params if method == :get
|
43
|
+
|
44
|
+
req.headers['Authorization'] = "Bearer #{@config.token}"
|
45
|
+
end
|
46
|
+
rescue Faraday::Error::ConnectionFailed => e
|
47
|
+
# TODO this is suspect but because farady wraps the exception
|
48
|
+
# in something generic there doesn't appear to be another
|
49
|
+
# way to distinguish different connection errors :(
|
50
|
+
if e.message =~ /certificate verify failed/
|
51
|
+
raise Errors::CertificateError
|
52
|
+
end
|
53
|
+
|
54
|
+
raise e
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
body = JSON.parse(result.body)
|
59
|
+
body.delete_if { |key, _| key == '@odata.context' }
|
60
|
+
|
61
|
+
@logger.info "Response: #{body}"
|
62
|
+
rescue JSON::ParserError => e
|
63
|
+
raise(Errors::JSONError, {
|
64
|
+
:message => e.message,
|
65
|
+
:path => path,
|
66
|
+
:params => params,
|
67
|
+
:response => result.body
|
68
|
+
})
|
69
|
+
end
|
70
|
+
|
71
|
+
unless /^2\d\d$/ =~ result.status.to_s
|
72
|
+
raise(Errors::APIStatusError, {
|
73
|
+
:path => path,
|
74
|
+
:params => params,
|
75
|
+
:status => result.status,
|
76
|
+
:response => body.inspect
|
77
|
+
})
|
78
|
+
end
|
79
|
+
Result.new(body)
|
80
|
+
end
|
81
|
+
|
82
|
+
def wait_for_event(env, id)
|
83
|
+
timestamp = '2018-09-01T23:47:17.50094+02:00'
|
84
|
+
|
85
|
+
operation_error = nil
|
86
|
+
retryable(:tries => 20, :sleep => 5) do
|
87
|
+
# stop waiting if interrupted
|
88
|
+
next if env[:interrupted]
|
89
|
+
|
90
|
+
# check action status
|
91
|
+
result = request("odata/Operations(#{id})", '$expand' => "LogEntries($filter=Timestamp gt #{timestamp})")
|
92
|
+
|
93
|
+
result['LogEntries'].each do |entry|
|
94
|
+
env[:ui].info(entry['Message'])
|
95
|
+
|
96
|
+
timestamp = entry['Timestamp']
|
97
|
+
end
|
98
|
+
|
99
|
+
yield result if block_given?
|
100
|
+
|
101
|
+
raise 'Operation not completed' if result['Status'] == 'Running' || result['Status'] == 'Queued'
|
102
|
+
operation_error = result['StatusMessage'] if result['Status'] == 'Failed'
|
103
|
+
end
|
104
|
+
|
105
|
+
raise "Operation failed: #{operation_error}" if operation_error
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module VagrantPlugins
|
2
|
+
module Haipa
|
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) #:ssh_keys, {:name => 'ijin (vagrant)'}
|
14
|
+
find(sub_obj, search)['id']
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(sub_obj, search)
|
18
|
+
key = search.keys.first #:slug
|
19
|
+
value = search[key].to_s #sfo1
|
20
|
+
key = key.to_s #slug
|
21
|
+
|
22
|
+
result = @result[sub_obj.to_s].inject(nil) do |result2, obj|
|
23
|
+
obj[key] == value ? obj : result2
|
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,22 @@
|
|
1
|
+
module VagrantPlugins
|
2
|
+
module Haipa
|
3
|
+
class Plugin < Vagrant.plugin('2')
|
4
|
+
name 'Haipa'
|
5
|
+
description <<-DESC
|
6
|
+
This plugin installs a provider that allows Vagrant to manage
|
7
|
+
machines using Haipa's API.
|
8
|
+
DESC
|
9
|
+
|
10
|
+
config(:haipa, :provider) do
|
11
|
+
require_relative 'config'
|
12
|
+
Config
|
13
|
+
end
|
14
|
+
|
15
|
+
provider(:haipa, parallel: true, defaultable: false, box_optional: true) do
|
16
|
+
require_relative 'provider'
|
17
|
+
Provider
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'vagrant-haipa/actions'
|
2
|
+
require 'vagrant-haipa/helpers/client'
|
3
|
+
|
4
|
+
module VagrantPlugins
|
5
|
+
module Haipa
|
6
|
+
class Provider < Vagrant.plugin('2', :provider)
|
7
|
+
|
8
|
+
def self.haipa_machines(machine)
|
9
|
+
client = Helpers::ApiClient.new(machine)
|
10
|
+
|
11
|
+
unless @haipa_machines
|
12
|
+
result = client.request('/odata/Machines', {'$expand' => 'Networks' })
|
13
|
+
@haipa_machines = result['value']
|
14
|
+
end
|
15
|
+
return @haipa_machines
|
16
|
+
end
|
17
|
+
|
18
|
+
# This class method caches status for all machines within
|
19
|
+
# the Haipa account. A specific machine's status
|
20
|
+
# may be refreshed by passing :refresh => true as an option.
|
21
|
+
def self.haipa_machine(machine, opts = {})
|
22
|
+
client = Helpers::ApiClient.new(machine)
|
23
|
+
|
24
|
+
# load status of machines if it has not been done before
|
25
|
+
haipa_machines(machine)
|
26
|
+
|
27
|
+
if opts[:refresh] && machine.id
|
28
|
+
# refresh the machine status for the given machine
|
29
|
+
@haipa_machines.delete_if { |d| d['Id'].to_s == machine.id }
|
30
|
+
result = client.request("/odata/Machines(#{machine.id})", {'$expand' => 'Networks' })
|
31
|
+
@haipa_machines << haipa_machine = result
|
32
|
+
else
|
33
|
+
# lookup machine status for the given machine
|
34
|
+
haipa_machine = @haipa_machines.find { |d| d['Id'].to_s == machine.id }
|
35
|
+
end
|
36
|
+
|
37
|
+
# if lookup by id failed, check for a machine with a matching name
|
38
|
+
# and set the id to ensure vagrant stores locally
|
39
|
+
# TODO allow the user to configure this behavior
|
40
|
+
unless haipa_machine
|
41
|
+
name = machine.config.vm.hostname || machine.name
|
42
|
+
haipa_machine = @haipa_machines.find { |d| d['Name'] == name.to_s }
|
43
|
+
machine.id = haipa_machine['Id'].to_s if haipa_machine
|
44
|
+
end
|
45
|
+
|
46
|
+
haipa_machine || { 'Status' => 'not_created' }
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(machine)
|
50
|
+
@machine = machine
|
51
|
+
end
|
52
|
+
|
53
|
+
def action(name)
|
54
|
+
# Attempt to get the action method from the Action class if it
|
55
|
+
# exists, otherwise return nil to show that we don't support the
|
56
|
+
# given action.
|
57
|
+
action_method = "action_#{name}"
|
58
|
+
return Actions.send(action_method) if Actions.respond_to?(action_method)
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# This method is called if the underying machine ID changes. Providers
|
63
|
+
# can use this method to load in new data for the actual backing
|
64
|
+
# machine or to realize that the machine is now gone (the ID can
|
65
|
+
# become `nil`). No parameters are given, since the underlying machine
|
66
|
+
# is simply the machine instance given to this object. And no
|
67
|
+
# return value is necessary.
|
68
|
+
def machine_id_changed
|
69
|
+
end
|
70
|
+
|
71
|
+
# This should return a hash of information that explains how to
|
72
|
+
# SSH into the machine. If the machine is not at a point where
|
73
|
+
# SSH is even possible, then `nil` should be returned.
|
74
|
+
#
|
75
|
+
# The general structure of this returned hash should be the
|
76
|
+
# following:
|
77
|
+
#
|
78
|
+
# {
|
79
|
+
# :host => "1.2.3.4",
|
80
|
+
# :port => "22",
|
81
|
+
# :username => "mitchellh",
|
82
|
+
# :private_key_path => "/path/to/my/key"
|
83
|
+
# }
|
84
|
+
#
|
85
|
+
# **Note:** Vagrant only supports private key based authenticatonion,
|
86
|
+
# mainly for the reason that there is no easy way to exec into an
|
87
|
+
# `ssh` prompt with a password, whereas we can pass a private key
|
88
|
+
# via commandline.
|
89
|
+
def ssh_info
|
90
|
+
machine = Provider.haipa_machine(@machine)
|
91
|
+
|
92
|
+
return nil if machine['Status'].to_sym != :Running
|
93
|
+
|
94
|
+
|
95
|
+
# Run a custom action called "ssh_ip" which does what it says and puts
|
96
|
+
# the IP found into the `:machine_ip` key in the environment.
|
97
|
+
env = @machine.action("ssh_ip")
|
98
|
+
|
99
|
+
# If we were not able to identify the machine IP, we return nil
|
100
|
+
# here and we let Vagrant core deal with it ;)
|
101
|
+
return nil unless env[:machine_ip]
|
102
|
+
|
103
|
+
return {
|
104
|
+
:host => env[:machine_ip],
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
# This should return the state of the machine within this provider.
|
109
|
+
# The state must be an instance of {MachineState}. Please read the
|
110
|
+
# documentation of that class for more information.
|
111
|
+
def state
|
112
|
+
state = Provider.haipa_machine(@machine)['Status'].downcase.to_sym
|
113
|
+
long = short = state.to_s
|
114
|
+
Vagrant::MachineState.new(state, short, long)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/locales/en.yml
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
en:
|
2
|
+
vagrant_haipa:
|
3
|
+
info:
|
4
|
+
off: "Machine is off"
|
5
|
+
not_created: "Machine has not been created"
|
6
|
+
already_active: "Machine is already active"
|
7
|
+
already_off: "Machine is already off"
|
8
|
+
creating: "Creating a new machine..."
|
9
|
+
machine_ip: "Assigned IP address: %{ip}"
|
10
|
+
destroying: "Destroying the machine..."
|
11
|
+
shutting_down: "Shutting down the machine..."
|
12
|
+
powering_off: "Powering off the machine..."
|
13
|
+
powering_on: "Powering on the machine..."
|
14
|
+
rebuilding: "Rebuilding the machine..."
|
15
|
+
reloading: "Rebooting the machine..."
|
16
|
+
creating_user: "Creating user account: %{user}..."
|
17
|
+
late_sudo_install_deb8: "Haipa's debian-8 image lacks sudo. Installing now."
|
18
|
+
modifying_sudo: "Modifying sudoers file to remove tty requirement..."
|
19
|
+
using_key: "Using existing SSH key: %{name}"
|
20
|
+
creating_key: "Creating new SSH key: %{name}..."
|
21
|
+
trying_rsync_install: "Rsync not found, attempting to install with yum..."
|
22
|
+
rsyncing: "Rsyncing folder: %{hostpath} => %{guestpath}..."
|
23
|
+
rsync_missing: "The rsync executable was not found in the current path."
|
24
|
+
images: "Description Slug ID\n\n%{images}"
|
25
|
+
images_with_regions: "Description Slug ID Regions\n\n%{images}"
|
26
|
+
regions: "Description Slug\n\n%{regions}"
|
27
|
+
sizes: "Memory CPUs Slug\n\n%{sizes}"
|
28
|
+
list_error: 'Could not contact the Haipa API: %{message}'
|
29
|
+
will_not_destroy: |-
|
30
|
+
The instance '%{name}' will not be destroyed, since the confirmation
|
31
|
+
was declined.
|
32
|
+
already_status: |-
|
33
|
+
The machine is already %{status}.
|
34
|
+
config:
|
35
|
+
token: "Token is required"
|
36
|
+
private_key: "SSH private key path is required"
|
37
|
+
public_key: "SSH public key not found: %{key}"
|
38
|
+
errors:
|
39
|
+
public_key: |-
|
40
|
+
There was an issue reading the public key at:
|
41
|
+
|
42
|
+
Path: %{path}
|
43
|
+
|
44
|
+
Please check the file's permissions.
|
45
|
+
api_status: |-
|
46
|
+
There was an issue with the request made to the Haipa
|
47
|
+
API at:
|
48
|
+
|
49
|
+
Path: %{path}
|
50
|
+
URI Params: %{params}
|
51
|
+
|
52
|
+
The response status from the API was:
|
53
|
+
|
54
|
+
Status: %{status}
|
55
|
+
Response: %{response}
|
56
|
+
rsync: |-
|
57
|
+
There was an error when attemping to rsync a share folder.
|
58
|
+
Please inspect the error message below for more info.
|
59
|
+
|
60
|
+
Host path: %{hostpath}
|
61
|
+
Guest path: %{guestpath}
|
62
|
+
Error: %{stderr}
|
63
|
+
json: |-
|
64
|
+
There was an issue with the JSON response from the Haipa
|
65
|
+
API at:
|
66
|
+
|
67
|
+
Path: %{path}
|
68
|
+
URI Params: %{params}
|
69
|
+
|
70
|
+
The response JSON from the API was:
|
71
|
+
|
72
|
+
Response: %{response}
|
73
|
+
result_match: |-
|
74
|
+
The result collection for %{collection_name}:
|
75
|
+
|
76
|
+
%{sub_obj}
|
77
|
+
|
78
|
+
Contained no object with the value "%{value}" for the the
|
79
|
+
key "%{key}".
|
80
|
+
|
81
|
+
Please ensure that the configured value exists in the collection.
|
82
|
+
certificate: |-
|
83
|
+
The secure connection to the Haipa API has failed. Please
|
84
|
+
ensure that your local certificates directory is defined in the
|
85
|
+
provider config.
|
86
|
+
|
87
|
+
config.vm.provider :haipa do |vm|
|
88
|
+
vm.ca_path = "/path/to/ssl/ca/cert.crt"
|
89
|
+
end
|
90
|
+
|
91
|
+
This is generally caused by the OpenSSL configuration associated
|
92
|
+
with the Ruby install being unaware of the system specific ca
|
93
|
+
certs.
|
94
|
+
local_ip: |-
|
95
|
+
The Haipa provider was unable to determine the host's IP.
|
data/test/Vagrantfile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
REQUIRED_PLUGINS = %w(vagrant-haipa)
|
2
|
+
|
3
|
+
Vagrant.configure('2') do |config|
|
4
|
+
|
5
|
+
config.ssh.username = 'ubuntu'
|
6
|
+
config.ssh.password = 'vagrant'
|
7
|
+
|
8
|
+
config.vm.synced_folder ".", "/vagrant", disabled: true
|
9
|
+
|
10
|
+
#config.vm.provision :shell, :path => 'scripts/provision.sh'
|
11
|
+
|
12
|
+
config.vm.define :ubuntu do |ubuntu|
|
13
|
+
ubuntu.vm.provider :haipa do |provider|
|
14
|
+
provider.vm_config = {
|
15
|
+
'Memory' => {
|
16
|
+
'Startup' => 2048
|
17
|
+
},
|
18
|
+
'Disks' => [
|
19
|
+
{
|
20
|
+
"Template" => 'T:\openstack\ubuntu-xenial.vhdx',
|
21
|
+
"Size" => 20
|
22
|
+
}
|
23
|
+
],
|
24
|
+
'NetworkAdapters' => [
|
25
|
+
{
|
26
|
+
"Name" => "eth0",
|
27
|
+
"SwitchName" => "Default Switch",
|
28
|
+
}
|
29
|
+
] ,
|
30
|
+
}
|
31
|
+
|
32
|
+
provider.provision = {
|
33
|
+
'Hostname' => 'basic',
|
34
|
+
"UserData" => {
|
35
|
+
"password" => config.ssh.password,
|
36
|
+
"chpasswd" => {
|
37
|
+
"expire"=> "False"
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
log 'Testing 1 2 3!'
|