Compare commits

..

25 Commits

Author SHA1 Message Date
Chris Roberts 9f1914642f When class name doesn't start with vagrant, prepend vagrant::root 2019-09-09 20:32:30 -07:00
Chris Roberts 2ad8abd57c Allow sets used within configuration to be dumped as an array 2019-09-09 20:32:08 -07:00
Chris Roberts c789fbf440 Set GRPC to use our logger 2019-09-09 20:31:26 -07:00
Chris Roberts 5c09116f53 Include parent name within config instance. Always pass all variables. 2019-09-09 20:30:55 -07:00
Chris Roberts 1651269333 Include extra error information on timeout 2019-09-09 20:30:06 -07:00
Chris Roberts 0d9ad25d69 Finish up test coverage and cleanup 2019-09-06 16:26:33 -07:00
Chris Roberts f4420f6b81 Clean tests for plugin client refactor 2019-09-06 16:26:33 -07:00
Chris Roberts fe7ac740df Remove proxy usage. Use binding for management only. 2019-09-06 16:26:33 -07:00
Chris Roberts 5605a6e843 Remove context logic from server implementations 2019-09-06 16:26:33 -07:00
Chris Roberts dcac2a167f Pass subcontext to call 2019-09-06 16:26:33 -07:00
Chris Roberts 6e9fc12b08 Remove extraneous contexts from tests 2019-09-06 16:26:33 -07:00
Chris Roberts 248f902345 Update to use errgroup for cleaner context usage 2019-09-06 16:26:33 -07:00
Chris Roberts 7b8b75593b Clean up in the plugin client and server implementations 2019-09-06 16:26:33 -07:00
Chris Roberts ca58714a03 Refactor proto usage 2019-09-06 16:26:33 -07:00
Chris Roberts a04e24378b Provide context when calling and update error handling 2019-09-06 16:26:33 -07:00
Chris Roberts bac7689b36 Clean up synced folder implementation to use updated plugin fetch 2019-09-06 16:26:33 -07:00
Chris Roberts 338cf4cc0b Ensure array type is processed when loading actions 2019-09-06 16:26:33 -07:00
Chris Roberts c441fac0db Clean up plugin lookup usage 2019-09-06 16:26:33 -07:00
Chris Roberts db249d58ac Do not re-write plugin state file
Maintain existing structure for backwards compatibility and
isolate new plugin information which can be ignored when
using previous versions.
2019-09-06 16:26:33 -07:00
Chris Roberts 8952168480 Fix up failing tests 2019-09-06 16:26:33 -07:00
Chris Roberts 893771e535 Configure travis for test coverage and configure for modules 2019-09-06 16:26:33 -07:00
Chris Roberts 9499553706 Adding test coverage 2019-09-06 16:26:33 -07:00
Chris Roberts 8aa0fd9445 Add Experimental to Util autoload entries 2019-09-06 16:26:33 -07:00
Chris Roberts 9f8d883852 Add more ignores and cleanup Vagrantfile adjustments 2019-09-06 16:26:33 -07:00
Chris Roberts b11c86528a Add basic support for go-plugin 2019-09-06 16:26:33 -07:00
304 changed files with 14687 additions and 4890 deletions

View File

@ -1,20 +0,0 @@
#!/usr/bin/env bash
csource="${BASH_SOURCE[0]}"
while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done
root="$( cd -P "$( dirname "$csource" )/../" && pwd )"
. "${root}/.ci/init.sh"
pushd "${root}" > "${output}"
# Build our gem
wrap gem build *.gemspec \
"Failed to build Vagrant RubyGem"
# Get the path of our new gem
g=(vagrant*.gem)
gem=$(printf "%s" "${g}")
wrap aws s3 cp "${gem}" "${ASSETS_PRIVATE_BUCKET}/${repository}/vagrant-master.gem" \
"Failed to store Vagrant RubyGem master build"

View File

@ -1,432 +0,0 @@
# last-modified: Tue Jan 14 20:37:58 UTC 2020
#!/usr/bin/env bash
# Path to file used for output redirect
# and extracting messages for warning and
# failure information sent to slack
function output_file() {
printf "/tmp/.ci-output"
}
# Write failure message, send error to configured
# slack, and exit with non-zero status. If an
# "$(output_file)" file exists, the last 5 lines will be
# included in the slack message.
#
# $1: Failure message
function fail() {
(>&2 echo "ERROR: ${1}")
if [ -f ""$(output_file)"" ]; then
slack -s error -m "ERROR: ${1}" -f "$(output_file)" -T 5
else
slack -s error -m "ERROR: ${1}"
fi
exit 1
}
# Write warning message, send warning to configured
# slack
#
# $1: Warning message
function warn() {
(>&2 echo "WARN: ${1}")
if [ -f ""$(output_file)"" ]; then
slack -s warn -m "WARNING: ${1}" -f "$(output_file)"
else
slack -s warn -m "WARNING: ${1}"
fi
}
# Execute command while redirecting all output to
# a file (file is used within fail mesage on when
# command is unsuccessful). Final argument is the
# error message used when the command fails.
#
# $@{1:$#-1}: Command to execute
# $@{$#}: Failure message
function wrap() {
i=$(("${#}" - 1))
wrap_raw "${@:1:$i}"
if [ $? -ne 0 ]; then
cat "$(output_file)"
fail "${@:$#}"
fi
rm "$(output_file)"
}
# Execute command while redirecting all output to
# a file. Exit status is returned.
function wrap_raw() {
rm -f "$(output_file)"
"${@}" > "$(output_file)" 2>&1
return $?
}
# Execute command while redirecting all output to
# a file (file is used within fail mesage on when
# command is unsuccessful). Command output will be
# streamed during execution. Final argument is the
# error message used when the command fails.
#
# $@{1:$#-1}: Command to execute
# $@{$#}: Failure message
function wrap_stream() {
i=$(("${#}" - 1))
wrap_stream_raw "${@:1:$i}"
if [ $? -ne 0 ]; then
fail "${@:$#}"
fi
rm "$(output_file)"
}
# Execute command while redirecting all output
# to a file. Command output will be streamed
# during execution. Exit status is returned
function wrap_stream_raw() {
rm -f "$(output_file)"
"${@}" > "$(output_file)" 2>&1 &
pid=$!
until [ -f "$(output_file)" ]; do
sleep 0.1
done
tail -f --quiet --pid "${pid}" "$(output_file)"
wait "${pid}"
return $?
}
# Send command to packet device and wrap
# execution
# $@{1:$#-1}: Command to execute
# $@{$#}: Failure message
function pkt_wrap() {
wrap packet-exec run -quiet -- "${@}"
}
# Send command to packet device and wrap
# execution
# $@: Command to execute
function pkt_wrap_raw() {
wrap_raw packet-exec run -quiet -- "${@}"
}
# Send command to packet device and wrap
# execution with output streaming
# $@{1:$#-1}: Command to execute
# $@{$#}: Failure message
function pkt_wrap_stream() {
wrap_stream packet-exec run -quiet -- "${@}"
}
# Send command to packet device and wrap
# execution with output streaming
# $@: Command to execute
function pkt_wrap_stream_raw() {
wrap_stream_raw packet-exec run -quiet -- "${@}"
}
# Generates location within the asset storage
# bucket to retain built assets.
function asset_location() {
if [ "${tag}" = "" ]; then
dst="${ASSETS_PRIVATE_LONGTERM}/${repository}/${ident_ref}/${short_sha}"
else
if [[ "${tag}" = *"+"* ]]; then
dst="${ASSETS_PRIVATE_LONGTERM}/${repository}/${tag}"
else
dst="${ASSETS_PRIVATE_BUCKET}/${repository}/${tag}"
fi
fi
echo -n "${dst}"
}
# Upload assets to the asset storage bucket.
#
# $1: Path to asset file or directory to upload
function upload_assets() {
if [ "${1}" = "" ]; then
fail "Parameter required for asset upload"
fi
if [ -d "${1}" ]; then
wrap aws s3 cp --recursive "${1}" "$(asset_location)/" \
"Upload to asset storage failed"
else
wrap aws s3 cp "${1}" "$(asset_location)/" \
"Upload to asset storage failed"
fi
}
# Download assets from the asset storage bucket. If
# destination is not provided, remote path will be
# used locally.
#
# $1: Path to asset or directory to download
# $2: Optional destination for downloaded assets
function download_assets() {
if [ "${1}" = "" ]; then
fail "At least one parameter required for asset download"
fi
if [ "${2}" = "" ]; then
dst="${1#/}"
else
dst="${2}"
fi
mkdir -p "${dst}"
src="$(asset_location)/${1#/}"
remote=$(aws s3 ls "${src}")
if [[ "${remote}" = *" PRE "* ]]; then
mkdir -p "${dst}"
wrap aws s3 cp --recursive "${src%/}/" "${dst}" \
"Download from asset storage failed"
else
mkdir -p "$(dirname "${dst}")"
wrap aws s3 cp "${src}" "${dst}" \
"Download from asset storage failed"
fi
}
# Upload assets to the cache storage bucket.
#
# $1: Path to asset file or directory to upload
function upload_cache() {
if [ "${1}" = "" ]; then
fail "Parameter required for cache upload"
fi
if [ -d "${1}" ]; then
wrap aws s3 cp --recursive "${1}" "${asset_cache}/" \
"Upload to cache failed"
else
wrap aws s3 cp "${1}" "${asset_cache}/" \
"Upload to cache failed"
fi
}
# Download assets from the cache storage bucket. If
# destination is not provided, remote path will be
# used locally.
#
# $1: Path to asset or directory to download
# $2: Optional destination for downloaded assets
function download_cache() {
if [ "${1}" = "" ]; then
fail "At least one parameter required for cache download"
fi
if [ "${2}" = "" ]; then
dst="${1#/}"
else
dst="${2}"
fi
mkdir -p "${dst}"
src="${asset_cache}/${1#/}"
remote=$(aws s3 ls "${src}")
if [[ "${remote}" = *" PRE "* ]]; then
mkdir -p "${dst}"
wrap aws s3 cp --recursive "${src%/}/" "${dst}" \
"Download from cache storage failed"
else
mkdir -p "$(dirname "${dst}")"
wrap aws s3 cp "${src}" "${dst}" \
"Download from cache storage failed"
fi
}
# Validate arguments for GitHub release. Checks for
# two arguments and that second argument is an exiting
# file asset, or directory.
#
# $1: GitHub tag name
# $2: Asset file or directory of assets
function release_validate() {
if [ "${1}" = "" ]; then
fail "Missing required position 1 argument (TAG) for release"
fi
if [ "${2}" = "" ]; then
fail "Missing required position 2 argument (PATH) for release"
fi
if [ ! -e "${2}" ]; then
fail "Path provided for release (${2}) does not exist"
fi
}
# Generate a GitHub release
#
# $1: GitHub tag name
# $2: Asset file or directory of assets
function release() {
release_validate "${@}"
wrap_raw ghr -u "${repo_owner}" -r "${repo_name}" -c "${full_sha}" -n "${1}" -delete
if [ $? -ne 0 ]; then
wrap ghr -u "${repo_owner}" -r "${repo_name}" -c "${full_sha}" -n "${1}" \
"${1}" "${2}" "Failed to create release for version ${1}"
fi
}
# Generate a GitHub prerelease
#
# $1: GitHub tag name
# $2: Asset file or directory of assets
function prerelease() {
release_validate "${@}"
if [[ "${1}" != *"+"* ]]; then
ptag="${1}+${short_sha}"
else
ptag="${1}"
fi
wrap_raw ghr -u "${repo_owner}" -r "${repo_name}" -c "${full_sha}" -n "${ptag}" \
-delete -prerelease "${ptag}" "${2}"
if [ $? -ne 0 ]; then
wrap ghr -u "${repo_owner}" -r "${repo_name}" -c "${full_sha}" -n "${ptag}" \
-prerelease "${ptag}" "${2}" \
"Failed to create prerelease for version ${1}"
fi
echo -n "${ptag}"
}
# Check if version string is valid for release
#
# $1: Version
# Returns: 0 if valid, 1 if invalid
function valid_release_version() {
if [[ "${1}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
return 0
else
return 1
fi
}
# Validate arguments for HashiCorp release. Ensures asset
# directory exists, and checks that the SHASUMS and SHASUM.sig
# files are present.
#
# $1: Asset directory
function hashicorp_release_validate() {
directory="${1}"
# Directory checks
if [ "${directory}" = "" ]; then
fail "No asset directory was provided for HashiCorp release"
fi
if [ ! -d "${directory}" ]; then
fail "Asset directory for HashiCorp release does not exist"
fi
# SHASUMS checks
if [ ! -e "${directory}/"*SHASUMS ]; then
fail "Asset directory is missing SHASUMS file"
fi
if [ ! -e "${directory}/"*SHASUMS.sig ]; then
fail "Asset directory is missing SHASUMS signature file"
fi
}
# Verify release assets by validating checksum properly match
# and that signature file is valid
#
# $1: Asset directory
function hashicorp_release_verify() {
directory="${1}"
pushd "${directory}" > "${output}"
# First do a checksum validation
wrap shasum -a 256 -c *_SHA256SUMS \
"Checksum validation of release assets failed"
# Next check that the signature is valid
gpghome=$(mktemp -qd)
export GNUPGHOME="${gpghome}"
wrap gpg --import "${HASHICORP_PUBLIC_GPG_KEY}" \
"Failed to import HashiCorp public GPG key"
wrap gpg --verify *SHA256SUMS.sig *SHA256SUMS \
"Validation of SHA256SUMS signature failed"
rm -rf "${gpghome}" > "${output}" 2>&1
popd > "${output}"
}
# Generate a HashiCorp release
#
# $1: Asset directory
function hashicorp_release() {
directory="${1}"
hashicorp_release_validate "${directory}"
hashicorp_release_verify "${directory}"
oid="${AWS_ACCESS_KEY_ID}"
okey="${AWS_SECRET_ACCESS_KEY}"
export AWS_ACCESS_KEY_ID="${RELEASE_AWS_ACCESS_KEY_ID}"
export AWS_SECRET_ACCESS_KEY="${RELEASE_AWS_SECRET_ACCESS_KEY}"
wrap_stream hc-releases upload "${directory}" \
"Failed to upload HashiCorp release assets"
wrap_stream hc-releases publish \
"Failed to publish HashiCorp release"
export AWS_ACCESS_KEY_ID="${oid}"
export AWS_SECRET_ACCESS_KEY="${okey}"
}
# Configures git for hashibot usage
function hashibot_git() {
wrap git config user.name "${HASHIBOT_USERNAME}" \
"Failed to setup git for hashibot usage (username)"
wrap git config user.email "${HASHIBOT_EMAIL}" \
"Failed to setup git for hashibot usage (email)"
wrap git remote set-url origin "https://${HASHIBOT_USERNAME}:${HASHIBOT_TOKEN}@github.com/${repository}" \
"Failed to setup git for hashibot usage (remote)"
}
# Stub cleanup method which can be redefined
# within actual script
function cleanup() {
(>&2 echo "** No cleanup tasks defined")
}
trap cleanup EXIT
# Enable debugging. This needs to be enabled with
# extreme caution when used on public repositories.
# Output with debugging enabled will likely include
# secret values which should not be publicly exposed.
#
# If repository is public, FORCE_PUBLIC_DEBUG environment
# variable must also be set.
is_private=$(curl -H "Authorization: token ${HASHIBOT_TOKEN}" -s "https://api.github.com/repos/${GITHUB_REPOSITORY}" | jq .private)
if [ "${DEBUG}" != "" ]; then
if [ "${is_private}" = "false" ]; then
if [ "${FORCE_PUBLIC_DEBUG}" != "" ]; then
set -x
output="/dev/stdout"
else
fail "Cannot enable debug mode on public repository unless forced"
fi
else
set -x
output="/dev/stdout"
fi
else
output="/dev/null"
fi
# Check if we are running a public repository on private runners
if [ "${VAGRANT_PRIVATE}" != "" ] && [ "${is_private}" = "false" ]; then
fail "Cannot run public repositories on private Vagrant runners. Disable runners now!"
fi
# Common variables
full_sha="${GITHUB_SHA}"
short_sha="${full_sha:0:8}"
ident_ref="${GITHUB_REF#*/*/}"
if [[ "${GITHUB_REF}" == *"refs/tags/"* ]]; then
tag="${GITHUB_REF##*tags/}"
valid_release_version "${tag}"
if [ $? -eq 0 ]; then
release=1
fi
fi
repository="${GITHUB_REPOSITORY}"
repo_owner="${repository%/*}"
repo_name="${repository#*/}"
asset_cache="${ASSETS_PRIVATE_SHORTTERM}/${repository}/${GITHUB_ACTION}"
job_id="${GITHUB_ACTION}"

View File

@ -1,6 +0,0 @@
#!/usr/bin/env bash
. "${root}/.ci/common.sh"
export DEBIAN_FRONTEND="noninteractive"
export PATH="${PATH}:${root}/.ci"

View File

@ -1,62 +0,0 @@
#!/usr/bin/env bash
ghr_version="0.13.0"
# NOTE: This release will generate a new release on the installers
# repository which in turn triggers a full package build
target_owner="hashicorp"
target_repository="vagrant-builders"
csource="${BASH_SOURCE[0]}"
while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done
root="$( cd -P "$( dirname "$csource" )/../" && pwd )"
. "${root}/.ci/init.sh"
pushd "${root}" > "${output}"
# Install ghr
wrap curl -Lso /tmp/ghr.tgz "https://github.com/tcnksm/ghr/releases/download/v${ghr_version}/ghr_v${ghr_version}_linux_amd64.tar.gz" \
"Failed to download ghr utility"
wrap tar -C /tmp/ -xf /tmp/ghr.tgz \
"Failed to unpack ghr archive"
wrap mv "/tmp/ghr_v${ghr_version}_linux_amd64/ghr" "${root}/.ci/" \
"Failed to install ghr utility"
# Build our gem
wrap gem build *.gemspec \
"Failed to build Vagrant RubyGem"
# Get the path of our new gem
g=(vagrant*.gem)
gem=$(printf "%s" "${g}")
# Determine the version of the release
vagrant_version="$(gem specification "${gem}" version)"
vagrant_version="${vagrant_version##*version: }"
# We want to release into the builders repository so
# update the repository variable with the desired destination
repo_owner="${target_owner}"
repo_name="${target_repository}"
full_sha="master"
export GITHUB_TOKEN="${HASHIBOT_TOKEN}"
if [ "${tag}" = "" ]; then
echo "Generating Vagrant RubyGem pre-release... "
version="v${vagrant_version}+${short_sha}"
prerelease "${version}" "${gem}"
else
# Validate this is a proper release version
valid_release_version "${vagrant_version}"
if [ $? -ne 0 ]; then
fail "Invalid version format for Vagrant release: ${vagrant_version}"
fi
echo "Generating Vagrant RubyGem release... "
version="v${vagrant_version}"
release "${version}" "${gem}"
fi
slack -m "New Vagrant installers release triggered: *${version}*"

176
.ci/slack
View File

@ -1,176 +0,0 @@
#!/usr/bin/env ruby
require "optparse"
require "net/https"
require "uri"
require "json"
OPTIONS = [:channel, :username, :icon, :state, :message,
:message_file, :file, :title, :tail, :webhook].freeze
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
opts.on("-c", "--channel CHAN", "Send to channel") do |c|
options[:channel] = c
end
opts.on("-u", "--username USER", "Send as username") do |u|
options[:username] = u
end
opts.on("-i", "--icon URL", "User icon image") do |i|
options[:icon] = i
end
opts.on("-s", "--state STATE", "Message state (success, warn, error, or color code)") do |s|
options[:state] = s
end
opts.on("-m", "--message MESSAGE", "Message to send") do |m|
options[:message] = m
end
opts.on("-M", "--message-file MESSAGE_FILE", "Use file contents as message") do |m|
options[:message_file] = m
end
opts.on("-f", "--file MESSAGE_FILE", "Send raw contents of file in message") do |f|
options[:file] = f
end
opts.on("-t", "--title TITLE", "Message title") do |t|
options[:title] = t
end
opts.on("-T", "--tail N", "Send last N lines of content from raw message file") do |t|
options[:tail] = t
end
opts.on("-w", "--webhook HOOK", "Slack webhook") do |w|
options[:webhook] = w
end
opts.on("-h", "--help", "Print help") do
puts opts
exit
end
end.parse!
OPTIONS.each do |key|
if !options.key?(key)
env_key = "SLACK_#{key.to_s.upcase}"
if ENV[env_key]
options[key] = ENV[env_key]
end
end
end
if !options[:webhook]
$stderr.puts "ERROR: Webhook is required!"
exit 1
end
if ENV["CIRCLECI"]
options[:icon] = "https://emoji.slack-edge.com/TF1GCKJNM/circleci/054b58d488e65138.png" unless options[:icon]
options[:username] = "circleci" unless options[:username]
options[:footer] = "CircleCI - <#{ENV["CIRCLE_BUILD_URL"]}|#{ENV["CIRCLE_PROJECT_USERNAME"]}/#{ENV["CIRCLE_PROJECT_REPONAME"]}>"
options[:footer_icon] = "https://emoji.slack-edge.com/TF1GCKJNM/circleci/054b58d488e65138.png"
end
if ENV["GITHUB_ACTIONS"]
options[:icon] = "https://ca.slack-edge.com/T024UT03C-WG8NDATGT-f82ae03b9fca-48" unless options[:icon]
options[:username] = "github" unless options[:username]
options[:footer] = "Actions - <https://github.com/#{ENV["GITHUB_REPOSITORY"]}/commit/#{ENV["GITHUB_SHA"]}/checks|#{ENV["GITHUB_REPOSITORY"]}>"
options[:footer_icon] = "https://ca.slack-edge.com/T024UT03C-WG8NDATGT-f82ae03b9fca-48"
end
options[:state] = "success" unless options[:state]
case options[:state]
when "success", "good"
options[:state] = "good"
when "warn", "warning"
options[:state] = "warning"
when "error", "danger"
options[:state] = "danger"
else
if !options[:state].start_with?("#")
$stderr.puts "ERROR: Invalid value for `state` (#{options[:state]})"
exit 1
end
end
msg = options[:message]
# NOTE: Message provided from CLI argument will end up with
# double escaped newlines so remove one
msg.gsub!("\\n", "\n") if msg
if options[:message_file]
if !File.exist?(options[:message_file])
$stderr.puts "ERROR: Message file does not exist `#{options[:message_file]}`"
exit 1
end
msg_c = File.read(options[:message_file])
msg = msg ? "#{msg}\n\n#{msg_c}" : msg_c
end
if options[:file]
if !File.exist?(options[:file])
$stderr.puts "ERROR: Message file does not exist `#{options[:file]}`"
exit 1
end
if (tail = options[:tail].to_i) > 0
content = ""
buffer = 0
File.open(options[:file], "r") do |f|
until (content.split("\n").size > tail) || buffer >= f.size
buffer += 1000
buffer = f.size if buffer > f.size
f.seek(f.size - buffer)
content = f.read
end
end
parts = content.split("\n")
if parts.size > tail
parts = parts.slice(-tail, tail)
end
fmsg = parts ? parts.join("\n") : ""
else
fmsg = File.read(options[:file])
end
fmsg = "```\n#{fmsg}\n```"
if msg
msg = msg << "\n\n" << fmsg
end
end
if msg.to_s.empty?
$stderr.puts "ERROR: No message content provided!"
exit 1
end
attach = {text: msg, fallback: msg, color: options[:state], mrkdn: true}
attach[:title] = options[:title] if options[:title]
attach[:footer] = options[:footer] if options[:footer]
attach[:footer_icon] = options[:footer_icon] if options[:footer_icon]
attach[:ts] = Time.now.to_i
payload = {}.tap do |pd|
pd[:username] = options.fetch(:username, "packet-exec")
pd[:channel] = options[:channel] if options[:channel]
pd[:icon_url] = options[:icon] if options[:icon]
pd[:attachments] = [attach]
end
result = Net::HTTP.post(URI(options[:webhook]), payload.to_json, "Content-Type" => "application/json")
if !result.code.start_with?("2")
$stderr.puts "Failed to send slack message"
exit 1
else
$stdout.puts "ok"
end

View File

@ -1,27 +0,0 @@
#!/usr/bin/env bash
csource="${BASH_SOURCE[0]}"
while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done
root="$( cd -P "$( dirname "$csource" )/../" && pwd )"
pushd "${root}" > /dev/null
export DEBIAN_FRONTEND="noninteractive"
# Install required dependencies
sudo apt-get update || exit 1
sudo apt-get install -yq bsdtar || exit 1
# Ensure bundler is installed
gem install --no-document bundler || exit 1
# Install the bundle
bundle install || exit 1
# Run tests
bundle exec rake test:unit
result=$?
popd > /dev/null
exit $result

View File

@ -1,6 +1,133 @@
version: 2
reference:
environment: &ENVIRONMENT
SLACK_TITLE: Vagrant CI
RELEASE_TARGET_REPONAME: vagrant-installers
images:
ruby23: &ruby23
docker:
- image: circleci/ruby:2.3
ruby24: &ruby24
docker:
- image: circleci/ruby:2.4
ruby25: &ruby25
docker:
- image: circleci/ruby:2.5
ruby26: &ruby26
docker:
- image: circleci/ruby:2.6
builder: &builder
environment:
<<: *ENVIRONMENT
docker:
- image: $BUILDER_IMAGE
auth:
username: $BUILDER_USERNAME
password: $BUILDER_PASSWORD
workflows:
public: &PUBLIC_WORKFLOW
filters:
branches:
only: /^pull\/.*/
master: &MASTER_WORKFLOW
filters:
branches:
only: master
private_build: &PRIVATE_WORKFLOW_BUILD
context: vagrant
filters:
branches:
only:
- /^build-.*/
tags:
only: /.*/
jobs:
private_failure: &PRIVATE_FAILURE
run:
name: Failure handler
command: |
if [ -f .output ]; then
slack -m "Vagrant job has failed: *${CIRCLE_JOB}*" -s error -f .output -T 5
else
slack -m "Vagrant job has failed: *${CIRCLE_JOB}*" -s error
fi
when: on_fail
unit_tests: &unit_tests
steps:
- run: sudo apt-get update ; sudo apt-get -yq install bsdtar
- checkout
- restore_cache:
key: static-site-gems-v1-{{ checksum "Gemfile.lock" }}
- run:
command: bundle check || bundle install --path vendor/bundle
- save_cache:
key: static-site-gems-v1-{{ checksum "Gemfile.lock" }}
paths:
- ./vendor/bundle
- run: bundle exec rake test:unit
jobs:
build:
<<: *builder
steps:
- checkout
- run: gem build vagrant.gemspec
- *PRIVATE_FAILURE
- persist_to_workspace:
root: .
paths:
- ./*.gem
store:
<<: *builder
steps:
- attach_workspace:
at: .
- run: |
gem_name=(vagrant-*.gem)
if [ "${CIRCLE_TAG}" == "" ]; then
remote_gem_name="vagrant-master.gem"
else
remote_gem_name="vagrant.gem"
fi
if [[ "${CIRCLE_BRANCH}" = "build-"* ]]; then
s3_dst="${ASSETS_PRIVATE_LONGTERM}/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BRANCH##build-}/"
else
s3_dst="${ASSETS_PRIVATE_BUCKET}/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/"
fi
aws s3 cp "${gem_name}" "${s3_dst}${remote_gem_name}" > .output 2>&1
- *PRIVATE_FAILURE
release:
<<: *builder
steps:
- checkout
- attach_workspace:
at: .
- run: |
set +e
gem=(vagrant-*.gem)
gem_version="${gem##vagrant-}"
gem_version="${gem_version%%.gem}"
export GITHUB_TOKEN="${HASHIBOT_TOKEN}"
if [ "${CIRCLE_TAG}" = "" ]; then
version="v${gem_version}+$(git rev-parse --short "${CIRCLE_SHA1}")"
ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${RELEASE_TARGET_REPONAME} -c master -prerelease -delete -replace ${version} ${gem} > .output 2>&1
else
version="${CIRCLE_TAG}"
ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${RELEASE_TARGET_REPONAME} -c master -delete -replace ${version} ${gem} > .output 2>&1
fi
slack -m "New Vagrant installers release triggered: *${version}*"
- *PRIVATE_FAILURE
test_ruby23:
<<: *ruby23
<<: *unit_tests
test_ruby24:
<<: *ruby24
<<: *unit_tests
test_ruby25:
<<: *ruby25
<<: *unit_tests
test_ruby26:
<<: *ruby26
<<: *unit_tests
build-website:
# setting the working_directory along with the checkout path allows us to not have
# to cd into the website/ directory for commands
@ -24,6 +151,51 @@ jobs:
command: ./scripts/deploy.sh
workflows:
version: 2
builds:
jobs:
- build:
<<: *PRIVATE_WORKFLOW_BUILD
- store:
<<: *PRIVATE_WORKFLOW_BUILD
requires:
- build
- release:
<<: *PRIVATE_WORKFLOW_BUILD
requires:
- build
pull_requests:
jobs:
- test_ruby23:
<<: *PUBLIC_WORKFLOW
- test_ruby24:
<<: *PUBLIC_WORKFLOW
- test_ruby25:
<<: *PUBLIC_WORKFLOW
- test_ruby26:
<<: *PUBLIC_WORKFLOW
master:
jobs:
- test_ruby23:
<<: *MASTER_WORKFLOW
- test_ruby24:
<<: *MASTER_WORKFLOW
- test_ruby25:
<<: *MASTER_WORKFLOW
- test_ruby26:
<<: *MASTER_WORKFLOW
- build:
<<: *MASTER_WORKFLOW
context: vagrant
requires:
- test_ruby23
- test_ruby24
- test_ruby25
- test_ruby26
- store:
<<: *MASTER_WORKFLOW
context: vagrant
requires:
- build
website:
jobs:
- build-website:

View File

@ -1,37 +0,0 @@
on:
push:
branches:
- master
paths-ignore:
- 'CHANGELOG.md'
- 'website/**'
jobs:
build-gem:
name: Build Vagrant RubyGem
runs-on: ubuntu-18.04
steps:
- name: Code Checkout
uses: actions/checkout@v1
- name: Set Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: '2.6'
- name: Build RubyGem
run: ./.ci/build.sh
working-directory: ${{github.workspace}}
env:
ASSETS_LONGTERM_PREFIX: elt
ASSETS_PRIVATE_BUCKET: ${{ secrets.ASSETS_PRIVATE_BUCKET }}
ASSETS_PRIVATE_LONGTERM: ${{ secrets.ASSETS_PRIVATE_LONGTERM }}
ASSETS_PRIVATE_SHORTTERM: ${{ secrets.ASSETS_PRIVATE_SHORTTERM }}
ASSETS_PUBLIC_BUCKET: ${{ secrets.ASSETS_PUBLIC_BUCKET }}
ASSETS_PUBLIC_LONGTERM: ${{ secrets.ASSETS_PUBLIC_LONGTERM }}
ASSETS_PUBLIC_SHORTTERM: ${{ secrets.ASSETS_PUBLIC_SHORTTERM }}
ASSETS_SHORTTERM_PREFIX: est
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
HASHIBOT_EMAIL: ${{ secrets.HASHIBOT_EMAIL }}
HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }}
HASHIBOT_USERNAME: ${{ secrets.HASHIBOT_USERNAME }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

View File

@ -1,38 +0,0 @@
on:
push:
branches:
- 'build-*'
tags: '*'
paths-ignore:
- 'CHANGELOG.md'
- 'website/**'
jobs:
trigger-release:
name: Trigger Installers Build
runs-on: ubuntu-18.04
steps:
- name: Code Checkout
uses: actions/checkout@v1
- name: Set Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: '2.6'
- name: Create Builders Release
run: ./.ci/release.sh
working-directory: ${{github.workspace}}
env:
ASSETS_LONGTERM_PREFIX: elt
ASSETS_PRIVATE_BUCKET: est
ASSETS_PRIVATE_LONGTERM: ${{ secrets.ASSETS_PRIVATE_LONGTERM }}
ASSETS_PRIVATE_SHORTTERM: ${{ secrets.ASSETS_PRIVATE_SHORTTERM }}
ASSETS_PUBLIC_BUCKET: ${{ secrets.ASSETS_PUBLIC_BUCKET }}
ASSETS_PUBLIC_LONGTERM: ${{ secrets.ASSETS_PUBLIC_LONGTERM }}
ASSETS_PUBLIC_SHORTTERM: ${{ secrets.ASSETS_PUBLIC_SHORTTERM }}
ASSETS_SHORTTERM_PREFIX: ${{ secrets.ASSETS_SHORTTERM_PREFIX }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
HASHIBOT_EMAIL: ${{ secrets.HASHIBOT_EMAIL }}
HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }}
HASHIBOT_USERNAME: ${{ secrets.HASHIBOT_USERNAME }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

View File

@ -1,29 +0,0 @@
on:
push:
branches:
- master
- 'test-*'
paths-ignore:
- 'CHANGELOG.md'
- 'website/**'
pull_request:
branches:
- master
jobs:
unit-tests:
runs-on: ubuntu-18.04
strategy:
matrix:
ruby: [ '2.4.x', '2.5.x', '2.6.x' ]
name: Vagrant unit tests on Ruby ${{ matrix.ruby }}
steps:
- name: Code Checkout
uses: actions/checkout@v1
- name: Setup Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: ${{matrix.ruby}}
architecture: 'x64'
- name: Run Tests
run: .ci/test.sh

5
.gitignore vendored
View File

@ -48,5 +48,10 @@ doc/
.ruby-version
.rvmrc
# Extensions
*.so
*.bundle
tmp/*
# Box storage for spec
test/vagrant-spec/boxes/*.box

View File

@ -1,12 +0,0 @@
poll "closed_issue_locker" "locker" {
schedule = "0 50 1 * * *"
closed_for = "720h" # 30 days
max_issues = 500
sleep_between_issues = "5s"
message = <<-EOF
I'm going to lock this issue because it has been closed for _30 days_ . This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
EOF
}

41
.travis.yml Normal file
View File

@ -0,0 +1,41 @@
language: ruby
sudo: false
cache: bundler
before_install:
- which go
- sudo apt-get remove --purge golang-go
- sudo add-apt-repository ppa:gophers/archive -y
- sudo apt-get update -q
- sudo apt-get install golang-1.11-go -yq
addons:
apt:
packages:
- bsdtar
rvm:
- 2.3.8
- 2.4.5
- 2.5.3
- 2.6.1
branches:
only:
- master
env:
global:
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
- GO111MODULE=on
- GOPATH=$HOME/go
- GOROOT=/usr/lib/go-1.11
- PATH=/usr/lib/go-1.11/bin:$PATH
script:
- go version
- go test ./...
- bundle exec rake compile
- bundle exec rake test:unit

View File

@ -2,81 +2,18 @@
FEATURES:
IMPROVEMENTS:
BUG FIXES:
## 2.2.7 (January 27, 2020)
IMPROVEMENTS:
- guest/opensuse: Check for basename hostname prior to setting hostname [GH-11170]
- host/linux: Check for modinfo in /sbin if it's not on PATH [GH-11178]
- core: Show guest name in hostname error message [GH-11175]
- provisioners/shell: Linux guests now support `reboot` option [GH-11194]
- darwin/nfs: Put each NFS export on its own line [GH-11216]
- contrib/bash: Add more completion flags to up command [GH-11223]
- provider/virtualbox: Add VirtualBox provider support for version 6.1.x [GH-11250]
- box/outdated: Allow to force check for box updates and ignore cached check [GH-11231]
- guest/alpine: Update apk cache when installing rsync [GH-11220]
- provider/virtualbox: Improve error message when machine folder is inaccessible [GH-11239]
- provisioner/ansible_local: Add pip install method for arch guests [GH-11265]
- communicators/winssh: Use Windows shell for `vagrant ssh -c` [GH-11258]
BUG FIXES:
- command/snapshot/save: Fix regression that prevented snapshot of all guests in environment [GH-11152]
- core: Update UI to properly retain newlines when adding prefix [GH-11126]
- core: Check if box update is available locally [GH-11188]
- core: Ensure Vagrant::Errors are loaded in file_checksum util [GH-11183]
- cloud/publish: Improve argument handling for missing arguments to command [GH-11184]
- core: Get latest version for current provider during outdated check [GH-11192]
- linux/nfs: avoid adding extra newlines to /etc/exports [GH-11201]
- guest/darwin: Fix VMware synced folders on APFS [GH-11267]
- guest/redhat: Ensure `nfs-server` is restarted when installing nfs client [GH-11212]
- core: Do not validate checksums if options are empty string [GH-11211]
- provider/docker: Enhance docker build method to match against buildkit output [GH-11205]
- provisioner/ansible_local: Don't prompt for input when installing Ansible on Ubuntu and Debian [GH-11191]
- provisioner/ansible_local: Ensure all guest caps accept all passed in arguments [GH-11265]
- host/windows: Fix regression that prevented port collisions from being detected [GH-11244]
- core/provisioner: Set top level provisioner name if set in a provisioner config [GH-11295]
## 2.2.6 (October 14, 2019)
FEATURES:
- core/provisioners: Introduce new Provisioner options: before and after [GH-11043]
- guest/alpine: Integrate the vagrant-alpine plugin into Vagrant core [GH-10975]
- core/provisioners: Introduce new Provisioner options: before and after [GH-11043]
IMPROVEMENTS:
- command/box/prune: Allow prompt skip while preserving actively in use boxes [GH-10908]
- command/cloud: Support providing checksum information with boxes [GH-11101]
- dev: Fixed Vagrantfile for Vagrant development [GH-11012]
- guest/alt: Improve handling for using network tools when setting hostname [GH-11000]
- command/box/prune: Allow prompt skip while preserving actively in use boxes [GH-10908]
- guest/suse: Add ipv6 network config templates for SUSE based distributions [GH-11013]
- guest/windows: Retry on connection timeout errors for the reboot capability [GH-11093]
- host/bsd: Use host resolve path capability to modify local paths if required [GH-11108]
- host/darwin: Add host resolve path capability to provide real paths for firmlinks [GH-11108]
- provisioners/chef: Update pkg install flags for chef on FreeBSD guests [GH-11075]
- provider/hyperv: Improve error message when VMMS is not running [GH-10978]
- provider/virtualbox: Raise additional errors for incomplete virtualbox installation on usable check [GH-10938]
- util/filechecksum: Add support for more checksum types [GH-11101]
BUG FIXES:
- command/rsync-auto: Fix path watcher bug so that all subdirectories are synced when changed [GH-11089]
- command/snapshot/save: Ensure VM id is passed to list snapshots for hyper-v provider [GH-11097]
- core: Ensure proper paths are shown in config loading exceptions [GH-11056]
- guest/suse: Use hostnamectl instead of hostname to set the hostname under SUSE [GH-11100]
- provider/docker: Fix default provider validation if password is used [GH-11053]
- provider/docker: Fix Docker providers usable? check [GH-11068]
- provisioner/ansible_local: Ensure pip_install_cmd is finalized to emptry string [GH-11098]
- provisioner/file: Ensure relative path for file provisioner source is relative to guest machines cwd [GH-11099]
- provider/docker: Ensure docker build_args option is properly set in docker compose config yaml [GH-11106]
- guest/suse: Update nfs & service daemon names for suse based hosts and guests [GH-11076]
- provider/docker: Determine ip address prefix workaround for docker public networks [GH-11111]
- provider/docker: Only return interfaces where addr is not nil for networks [GH-11116]
## 2.2.5 (June 19, 2019)
@ -552,7 +489,7 @@ BUG FIXES:
- core: Rescue more exceptions when checking if port is open [GH-8517]
- guests/solaris11: Inherit from Solaris guest and keep solaris11 specific methods [GH-9034]
- guests/windows: Split out cygwin path helper for msys2/cygwin paths and ensure cygpath exists [GH-8972]
- guests/windows: Specify expected shell when executing on guest (fixes einssh communicator usage) [GH-9012]
- guests/windows: Specify expected shell when executing on guest (fixes winssh communicator usage) [GH-9012]
- guests/windows: Include WinSSH Communicator when using insert_public_key [GH-9105]
- hosts/windows: Check for vagrant.exe when validating versions within WSL [GH-9107, GH-8962]
- providers/docker: Isolate windows check within executor to handle running through VM [GH-8921]

View File

@ -1,5 +1,10 @@
require 'rubygems'
require 'bundler/setup'
require 'rake/extensiontask'
Rake::ExtensionTask.new "go-plugin" do |ext|
ext.lib_dir = "lib/vagrant/go_plugin"
end
# Immediately sync all stdout so that tools like buildbot can
# immediately load in the output.

39
Vagrantfile vendored
View File

@ -4,13 +4,13 @@
# Ruby, run unit tests, etc.
Vagrant.configure("2") do |config|
config.vm.box = "hashicorp/bionic64"
config.vm.box = "hashicorp/precise64"
config.vm.hostname = "vagrant"
config.ssh.shell = "bash -c 'BASH_ENV=/etc/profile exec bash'"
["vmware_desktop", "virtualbox", "hyperv"].each do |provider|
["vmware_fusion", "vmware_workstation", "virtualbox"].each do |provider|
config.vm.provider provider do |v, override|
v.memory = "2048"
v.memory = "1024"
end
end
@ -29,32 +29,32 @@ $shell = <<-'CONTENTS'
export DEBIAN_FRONTEND=noninteractive
MARKER_FILE="/usr/local/etc/vagrant_provision_marker"
RUBY_VER_REQ=$(awk '$1 == "s.required_ruby_version" { print $4 }' /vagrant/vagrant.gemspec | tr -d '"')
BUNDLER_VER_REQ=$(awk '/s.add_dependency "bundler"/ { print $4 }' /vagrant/vagrant.gemspec | tr -d '"')
# Only provision once
if [ -f "${MARKER_FILE}" ]; then
exit 0
fi
# Add ubuntu_rvm repo
apt-add-repository -y ppa:rael-gc/rvm
# Update apt
apt-get update --quiet
# Add vagrant user to sudo group:
# ubuntu_rvm only adds users in group sudo to group rvm
usermod -a -G sudo vagrant
# Install basic dependencies and RVM
apt-get install -qy build-essential bsdtar rvm
# Install basic dependencies
apt-get install -qy build-essential bsdtar curl
# Import the mpapis public key to verify downloaded releases
su -l -c 'gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3' vagrant
# Install next-to-last Ruby that complies with Vagrant's version constraint
RUBY_VER=$(su -l -c 'rvm list known' vagrant | tr '[]-' ' ' | awk "/^ ruby ${RUBY_VER_REQ:0:1}\./ { print \$2 }" | sort -r | sed -n '2p')
su -l -c "rvm install ${RUBY_VER}" vagrant
su -l -c "rvm --default use ${RUBY_VER}" vagrant
# Install RVM
su -l -c 'curl -sL https://get.rvm.io | bash -s stable' vagrant
# Add the vagrant user to the RVM group
#usermod -a -G rvm vagrant
# Install latest Ruby that complies with Vagrant's version constraint
RUBY_VER_LATEST=$(su -l -c 'rvm list known' vagrant | tr '[]-' ' ' | awk "/^ ruby ${RUBY_VER_REQ:0:1}\./ { print \$2 }" | sort | tail -n1)
su -l -c "rvm install ${RUBY_VER_LATEST}" vagrant
su -l -c "rvm --default use ${RUBY_VER_LATEST}" vagrant
# Output the Ruby version (for sanity)
su -l -c 'ruby --version' vagrant
@ -63,14 +63,15 @@ su -l -c 'ruby --version' vagrant
apt-get install -qy git
# Upgrade Rubygems
su -l -c "rvm ${RUBY_VER} do gem update --system" vagrant
su -l -c "rvm ${RUBY_VER_LATEST} do gem update --system" vagrant
# Prepare to run unit tests
# Install bundler and prepare to run unit tests
su -l -c "gem install bundler -v ${BUNDLER_VER_REQ}" vagrant
su -l -c 'cd /vagrant; bundle install' vagrant
# Automatically move into the shared folder, but only add the command
# if it's not already there.
grep -q 'cd /vagrant' /home/vagrant/.bash_profile 2>/dev/null || echo 'cd /vagrant' >> /home/vagrant/.bash_profile
grep -q 'cd /vagrant' /home/vagrant/.bash_profile || echo 'cd /vagrant' >> /home/vagrant/.bash_profile
# Touch the marker file so we don't do this again
touch ${MARKER_FILE}

View File

@ -75,19 +75,7 @@ _vagrant() {
then
local vm_list=$(find "${vagrant_state_file}/machines" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
fi
local up_commands="\
--provision \
--no-provision \
--provision-with \
--destroy-on-error \
--no-destroy-on-error \
--parallel \
--no-parallel
--provider \
--install-provider \
--no-install-provider \
-h \
--help"
local up_commands="--no-provision"
COMPREPLY=($(compgen -W "${up_commands} ${vm_list}" -- ${cur}))
return 0
;;

68
ext/go-plugin/extconf.rb Normal file
View File

@ -0,0 +1,68 @@
lib = File.expand_path('./../../../lib', File.expand_path(__FILE__))
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'mkmf'
require 'time'
find_executable('go')
go_version = /go version go(\d+\.\d+)/.match(`go version`).captures.first
raise "'go' version >=1.5.0 is required, found go #{go_version}" unless Gem::Dependency.new('', '>=1.5.0').match?('', go_version)
makefile = "Makefile"
makefile_content = <<MFEND
NAME := #{File.basename(File.dirname(File.expand_path(__FILE__)))}
BINARY := ${NAME}.so
V = 0
Q1 = $(V:1=)
Q = $(Q1:0=@)
ECHO1 = $(V:1=@:)
ECHO = $(ECHO1:0=@echo)
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -maxdepth 0 -name '*.go')
VERSION=1.0
BUILD_DATE=#{Time.now.iso8601}
cflags= $(optflags) $(warnflags)
optflags= -O3 -fno-fast-math
warnflags= -Wall -Wextra -Wno-unused-parameter -Wno-parentheses -Wno-long-long -Wno-missing-field-initializers -Wunused-variable -Wpointer-arith -Wwrite-strings -Wimplicit-function-declaration -Wdiv-by-zero -Wdeprecated-declarations
CCDLFLAGS= -fno-common
INCFLAGS= -I#{RbConfig::CONFIG['rubyhdrdir']}/ -I#{RbConfig::CONFIG['rubyarchhdrdir']}/ -I$(SOURCEDIR)
CFLAGS= $(CCDLFLAGS) $(cflags) -fno-common -pipe $(INCFLAGS)
LDFLAGS=-L#{RbConfig::CONFIG['libdir']} #{RbConfig::CONFIG['LIBRUBYARG']}
.DEFAULT_GOAL := $(BINARY)
.PHONY: help
help:
${ECHO} ${VERSION}
${ECHO} ${BUILD_DATE}
all:
make clean
$(BINARY)
$(BINARY): $(SOURCES)
CGO_CFLAGS="${CFLAGS}" CGO_LDFLAGS="${LDFLAGS}" go build -buildmode=c-shared -o ${BINARY} #{File.dirname(__FILE__)}/../../ext/${NAME}/
.PHONY: install
install:
# go install ${LDFLAGS} ./...
.PHONY: clean
clean:
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
if [ -f lib/${BINARY} ] ; then rm lib/${BINARY} ; fi
MFEND
puts "creating Makefile"
File.open(makefile, 'w') do |f|
f.write(makefile_content.gsub!(/(?:^|\G) {2}/m,"\t"))
end
$makefile_created = true

139
ext/go-plugin/go-plugin.go Normal file
View File

@ -0,0 +1,139 @@
package main
import (
"C"
"io/ioutil"
"os"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin"
)
var Plugins *plugin.VagrantPlugin
//export Setup
func Setup(enableLogger, timestamps bool, logLevel *C.char) bool {
lvl := to_gs(logLevel)
lopts := &hclog.LoggerOptions{Name: "vagrant"}
if enableLogger {
lopts.Output = os.Stderr
} else {
lopts.Output = ioutil.Discard
}
if !timestamps {
lopts.TimeFormat = " "
}
lopts.Level = hclog.LevelFromString(lvl)
vagrant.SetDefaultLogger(hclog.New(lopts))
if Plugins != nil {
Plugins.Logger.Error("plugins setup failure", "error", "already setup")
return false
}
Plugins = plugin.VagrantPluginInit()
return true
}
//export LoadPlugins
func LoadPlugins(plgpath *C.char) bool {
if Plugins == nil {
vagrant.DefaultLogger().Error("cannot load plugins", "error", "not setup")
return false
}
p := to_gs(plgpath)
err := Plugins.LoadPlugins(p)
if err != nil {
Plugins.Logger.Error("failed loading plugins",
"path", p, "error", err)
return false
}
Plugins.Logger.Info("plugins successfully loaded", "path", p)
return true
}
//export Reset
func Reset() {
if Plugins != nil {
Plugins.Logger.Info("resetting loaded plugins")
Teardown()
dirs := Plugins.PluginDirectories
Plugins.PluginDirectories = []string{}
for _, p := range dirs {
Plugins.LoadPlugins(p)
}
} else {
Plugins.Logger.Warn("plugin reset failure", "error", "not setup")
}
}
//export Teardown
func Teardown() {
// only teardown if setup
if Plugins == nil {
vagrant.DefaultLogger().Error("cannot teardown plugins", "error", "not setup")
return
}
Plugins.Logger.Debug("tearing down any active plugins")
Plugins.Kill()
Plugins.Logger.Info("plugins have been halted")
}
//export ListProviders
func ListProviders() *C.char {
list := map[string]interface{}{}
r := &Response{Result: list}
if Plugins == nil {
return r.Dump()
}
for n, p := range Plugins.Providers {
info := p.Provider.Info()
c := p.Client.ReattachConfig()
data := map[string]interface{}{
"network": c.Addr.Network(),
"address": c.Addr.String(),
"description": info.Description,
"priority": info.Priority,
}
list[n] = data
}
r.Result = list
return r.Dump()
}
//export ListSyncedFolders
func ListSyncedFolders() *C.char {
list := map[string]interface{}{}
r := &Response{Result: list}
if Plugins == nil {
return r.Dump()
}
for n, p := range Plugins.SyncedFolders {
info := p.SyncedFolder.Info()
c := p.Client.ReattachConfig()
data := map[string]interface{}{
"network": c.Addr.Network(),
"address": c.Addr.String(),
"description": info.Description,
"priority": info.Priority,
}
list[n] = data
}
r.Result = list
return r.Dump()
}
// stub required for build
func main() {}
// helper to convert c string to go string
func to_gs(s *C.char) string {
return C.GoString(s)
}
// helper to convert go string to c string
func to_cs(s string) *C.char {
return C.CString(s)
}

48
ext/go-plugin/response.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"C"
"encoding/json"
"errors"
"fmt"
)
type Response struct {
Error error `json:"error"`
Result interface{} `json:"result"`
}
// Serialize the response into a JSON C string
func (r *Response) Dump() *C.char {
tmp := map[string]interface{}{}
if r.Error != nil {
tmp["error"] = r.Error.Error()
} else {
tmp["error"] = nil
}
tmp["result"] = r.Result
result, err := json.Marshal(tmp)
if err != nil {
return to_cs(fmt.Sprintf(`{"error": "failed to encode response - %s"}`, err))
}
return to_cs(string(result[:]))
}
// Load a new response from a JSON C string
func LoadResponse(s *C.char) (r *Response, err error) {
tmp := map[string]interface{}{}
st := []byte(to_gs(s))
r = &Response{}
err = json.Unmarshal(st, &tmp)
if tmp["error"] != nil {
e, ok := tmp["error"].(string)
if !ok {
err = errors.New(
fmt.Sprintf("cannot load error content - %s", tmp["error"]))
return
}
r.Error = errors.New(e)
}
r.Result = tmp["result"]
return
}

View File

@ -0,0 +1,10 @@
package vagrant
type Box struct {
Name string `json:"name"`
Provider string `json:"provider"`
Version string `json:"version"`
Directory string `json:"directory"`
Metadata map[string]string `json:"metadata"`
MetadataURL string `json:"metadata_url"`
}

View File

@ -0,0 +1,61 @@
package vagrant
import (
"context"
)
type SystemCapability struct {
Name string `json:"name"`
Platform string `json:"platform"`
}
type ProviderCapability struct {
Name string `json:"name"`
Provider string `json:"provider"`
}
type GuestCapabilities interface {
GuestCapabilities() (caps []SystemCapability, err error)
GuestCapability(ctx context.Context, cap *SystemCapability, args interface{}, machine *Machine) (result interface{}, err error)
}
type HostCapabilities interface {
HostCapabilities() (caps []SystemCapability, err error)
HostCapability(ctx context.Context, cap *SystemCapability, args interface{}, env *Environment) (result interface{}, err error)
}
type ProviderCapabilities interface {
ProviderCapabilities() (caps []ProviderCapability, err error)
ProviderCapability(ctx context.Context, cap *ProviderCapability, args interface{}, machine *Machine) (result interface{}, err error)
}
type NoGuestCapabilities struct{}
type NoHostCapabilities struct{}
type NoProviderCapabilities struct{}
func (g *NoGuestCapabilities) GuestCapabilities() (caps []SystemCapability, err error) {
caps = make([]SystemCapability, 0)
return
}
func (g *NoGuestCapabilities) GuestCapability(x context.Context, c *SystemCapability, a interface{}, m *Machine) (r interface{}, err error) {
return
}
func (h *NoHostCapabilities) HostCapabilities() (caps []SystemCapability, err error) {
caps = make([]SystemCapability, 0)
return
}
func (h *NoHostCapabilities) HostCapability(x context.Context, c *SystemCapability, a interface{}, e *Environment) (r interface{}, err error) {
return
}
func (p *NoProviderCapabilities) ProviderCapabilities() (caps []ProviderCapability, err error) {
caps = make([]ProviderCapability, 0)
return
}
func (p *NoProviderCapabilities) ProviderCapability(x context.Context, cap *ProviderCapability, args interface{}, machine *Machine) (result interface{}, err error) {
return
}

View File

@ -0,0 +1,78 @@
package vagrant
import (
"context"
"testing"
)
func TestNoGuestCapabilities(t *testing.T) {
g := NoGuestCapabilities{}
caps, err := g.GuestCapabilities()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(caps) != 0 {
t.Fatalf("guest capabilities should be empty")
}
}
func TestNoGuestCapability(t *testing.T) {
g := NoGuestCapabilities{}
m := &Machine{}
cap := &SystemCapability{"Test", "Test"}
r, err := g.GuestCapability(context.Background(), cap, "args", m)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if r != nil {
t.Fatalf("capability returned unexpected result")
}
}
func TestNoHostCapabilities(t *testing.T) {
h := NoHostCapabilities{}
caps, err := h.HostCapabilities()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(caps) != 0 {
t.Fatalf("host capabilities should be empty")
}
}
func TestNoHostCapability(t *testing.T) {
h := NoHostCapabilities{}
e := &Environment{}
cap := &SystemCapability{"Test", "Test"}
r, err := h.HostCapability(context.Background(), cap, "args", e)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if r != nil {
t.Fatalf("capability returned unexpected result")
}
}
func TestNoProviderCapabilities(t *testing.T) {
p := NoProviderCapabilities{}
caps, err := p.ProviderCapabilities()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(caps) != 0 {
t.Fatalf("provider capabilities should be empty")
}
}
func TestNoProviderCapability(t *testing.T) {
p := NoProviderCapabilities{}
m := &Machine{}
cap := &ProviderCapability{"Test", "Test"}
r, err := p.ProviderCapability(context.Background(), cap, "args", m)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if r != nil {
t.Fatalf("capability returned unexpected result")
}
}

View File

@ -0,0 +1,202 @@
package communicator
import (
"fmt"
"io"
"strings"
"sync"
"unicode"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/mitchellh/iochan"
)
// CmdDisconnect is a sentinel value to indicate a RemoteCmd
// exited because the remote side disconnected us.
const CmdDisconnect int = 2300218
// Cmd represents a remote command being prepared or run.
type Cmd struct {
// Command is the command to run remotely. This is executed as if
// it were a shell command, so you are expected to do any shell escaping
// necessary.
Command string
// Stdin specifies the process's standard input. If Stdin is
// nil, the process reads from an empty bytes.Buffer.
Stdin io.Reader
// Stdout and Stderr represent the process's standard output and
// error.
//
// If either is nil, it will be set to ioutil.Discard.
Stdout io.Writer
Stderr io.Writer
// Once Wait returns, his will contain the exit code of the process.
exitStatus int
// Internal fields
exitCh chan struct{}
// err is used to store any error reported by the Communicator during
// execution.
err error
// This thing is a mutex, lock when making modifications concurrently
sync.Mutex
}
// Init must be called by the Communicator before executing the command.
func (c *Cmd) Init() {
c.Lock()
defer c.Unlock()
c.exitCh = make(chan struct{})
}
// SetExitStatus stores the exit status of the remote command as well as any
// communicator related error. SetExitStatus then unblocks any pending calls
// to Wait.
// This should only be called by communicators executing the remote.Cmd.
func (c *Cmd) SetExitStatus(status int, err error) {
c.Lock()
defer c.Unlock()
c.exitStatus = status
c.err = err
close(c.exitCh)
}
// StartWithUi runs the remote command and streams the output to any
// configured Writers for stdout/stderr, while also writing each line
// as it comes to a Ui.
func (r *Cmd) StartWithUi(c Communicator, ui vagrant.Ui) error {
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
defer stdout_w.Close()
defer stderr_w.Close()
// Retain the original stdout/stderr that we can replace back in.
originalStdout := r.Stdout
originalStderr := r.Stderr
defer func() {
r.Lock()
defer r.Unlock()
r.Stdout = originalStdout
r.Stderr = originalStderr
}()
// Set the writers for the output so that we get it streamed to us
if r.Stdout == nil {
r.Stdout = stdout_w
} else {
r.Stdout = io.MultiWriter(r.Stdout, stdout_w)
}
if r.Stderr == nil {
r.Stderr = stderr_w
} else {
r.Stderr = io.MultiWriter(r.Stderr, stderr_w)
}
// Start the command
if err := c.Start(r); err != nil {
return err
}
// Create the channels we'll use for data
exitCh := make(chan struct{})
stdoutCh := iochan.DelimReader(stdout_r, '\n')
stderrCh := iochan.DelimReader(stderr_r, '\n')
// Start the goroutine to watch for the exit
go func() {
defer close(exitCh)
defer stdout_w.Close()
defer stderr_w.Close()
r.Wait()
}()
// Loop and get all our output
OutputLoop:
for {
select {
case output := <-stderrCh:
if output != "" {
ui.Say(r.cleanOutputLine(output))
}
case output := <-stdoutCh:
if output != "" {
ui.Say(r.cleanOutputLine(output))
}
case <-exitCh:
break OutputLoop
}
}
// Make sure we finish off stdout/stderr because we may have gotten
// a message from the exit channel before finishing these first.
for output := range stdoutCh {
ui.Say(r.cleanOutputLine(output))
}
for output := range stderrCh {
ui.Say(r.cleanOutputLine(output))
}
return nil
}
// Wait waits for the remote command to complete.
// Wait may return an error from the communicator, or an ExitError if the
// process exits with a non-zero exit status.
func (c *Cmd) Wait() error {
<-c.exitCh
c.Lock()
defer c.Unlock()
if c.err != nil || c.exitStatus != 0 {
return &ExitError{
Command: c.Command,
ExitStatus: c.exitStatus,
Err: c.err,
}
}
return nil
}
// cleanOutputLine cleans up a line so that '\r' don't muck up the
// UI output when we're reading from a remote command.
func (r *Cmd) cleanOutputLine(line string) string {
// Trim surrounding whitespace
line = strings.TrimRightFunc(line, unicode.IsSpace)
// Trim up to the first carriage return, since that text would be
// lost anyways.
idx := strings.LastIndex(line, "\r")
if idx > -1 {
line = line[idx+1:]
}
return line
}
// ExitError is returned by Wait to indicate and error executing the remote
// command, or a non-zero exit status.
type ExitError struct {
Command string
ExitStatus int
Err error
}
func (e *ExitError) Error() string {
if e.Err != nil {
return fmt.Sprintf("error executing %q: %v", e.Command, e.Err)
}
return fmt.Sprintf("%q exit status: %d", e.Command, e.ExitStatus)
}

View File

@ -0,0 +1 @@
package communicator

View File

@ -0,0 +1,117 @@
package communicator
import (
"context"
"fmt"
"io"
"os"
"sync/atomic"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
type Communicator interface {
Connect() error
Disconnect() error
Timeout() time.Duration
Start(*Cmd) error
Download(path string, output io.Writer) error
DownloadDir(dst, src string, excludes []string) error
Upload(dst string, src io.Reader, srcinfo *os.FileInfo) error
UploadDir(dst, src string, excludes []string) error
}
// maxBackoffDelay is the maximum delay between retry attempts
var maxBackoffDelay = 20 * time.Second
var initialBackoffDelay = time.Second
var logger = vagrant.DefaultLogger().Named("communicator")
// Fatal is an interface that error values can return to halt Retry
type Fatal interface {
FatalError() error
}
// Retry retries the function f until it returns a nil error, a Fatal error, or
// the context expires.
func Retry(ctx context.Context, f func() error) error {
// container for atomic error value
type errWrap struct {
E error
}
// Try the function in a goroutine
var errVal atomic.Value
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
delay := time.Duration(0)
for {
// If our context ended, we want to exit right away.
select {
case <-ctx.Done():
return
case <-time.After(delay):
}
// Try the function call
err := f()
// return if we have no error, or a FatalError
done := false
switch e := err.(type) {
case nil:
done = true
case Fatal:
err = e.FatalError()
done = true
}
errVal.Store(errWrap{err})
if done {
return
}
logger.Warn("retryable error", "error", err)
delay *= 2
if delay == 0 {
delay = initialBackoffDelay
}
if delay > maxBackoffDelay {
delay = maxBackoffDelay
}
logger.Info("sleeping for retry", "duration", delay)
}
}()
// Wait for completion
select {
case <-ctx.Done():
case <-doneCh:
}
var lastErr error
// Check if we got an error executing
if ev, ok := errVal.Load().(errWrap); ok {
lastErr = ev.E
}
// Check if we have a context error to check if we're interrupted or timeout
switch ctx.Err() {
case context.Canceled:
return fmt.Errorf("interrupted - last error: %v", lastErr)
case context.DeadlineExceeded:
return fmt.Errorf("timeout - last error: %v", lastErr)
}
if lastErr != nil {
return lastErr
}
return nil
}

View File

@ -0,0 +1,59 @@
package none
import (
"errors"
"io"
"os"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
)
type Communicator struct {
config string
}
// Creates a null vagrant.Communicator implementation. This takes
// an already existing configuration.
func New(config string) (result *Communicator, err error) {
// Establish an initial connection and connect
result = &Communicator{
config: config,
}
return
}
func (c *Communicator) Connect() (err error) {
return
}
func (c *Communicator) Disconnect() (err error) {
return
}
func (c *Communicator) Start(cmd *communicator.Cmd) (err error) {
cmd.Init()
cmd.SetExitStatus(0, nil)
return
}
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
return errors.New("Upload is not implemented when communicator = 'none'")
}
func (c *Communicator) UploadDir(dst string, src string, excl []string) error {
return errors.New("UploadDir is not implemented when communicator = 'none'")
}
func (c *Communicator) Download(path string, output io.Writer) error {
return errors.New("Download is not implemented when communicator = 'none'")
}
func (c *Communicator) DownloadDir(dst string, src string, excl []string) error {
return errors.New("DownloadDir is not implemented when communicator = 'none'")
}
func (c *Communicator) Timeout() time.Duration {
return 0
}

View File

@ -0,0 +1,12 @@
package none
import (
"testing"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
)
func TestCommIsCommunicator(t *testing.T) {
// Force failure with explanation of why it's not valid
var _ communicator.Communicator = new(Communicator)
}

View File

@ -0,0 +1,949 @@
package ssh
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
// ErrHandshakeTimeout is returned from New() whenever we're unable to establish
// an ssh connection within a certain timeframe. By default the handshake time-
// out period is 1 minute. You can change it with Config.HandshakeTimeout.
var ErrHandshakeTimeout = fmt.Errorf("Timeout during SSH handshake")
var logger = vagrant.DefaultLogger().Named("communicator.ssh")
type Communicator struct {
client *ssh.Client
config *Config
conn net.Conn
address string
}
// Config is the structure used to configure the SSH communicator.
type Config struct {
// The configuration of the Go SSH connection
SSHConfig *ssh.ClientConfig
// Connection returns a new connection. The current connection
// in use will be closed as part of the Close method, or in the
// case an error occurs.
Connection func() (net.Conn, error)
// Pty, if true, will request a pty from the remote end.
Pty bool
// DisableAgentForwarding, if true, will not forward the SSH agent.
DisableAgentForwarding bool
// HandshakeTimeout limits the amount of time we'll wait to handshake before
// saying the connection failed.
HandshakeTimeout time.Duration
// UseSftp, if true, sftp will be used instead of scp for file transfers
UseSftp bool
// KeepAliveInterval sets how often we send a channel request to the
// server. A value < 0 disables.
KeepAliveInterval time.Duration
// Timeout is how long to wait for a read or write to succeed.
Timeout time.Duration
}
// Creates a new vagrant.Communicator implementation over SSH. This takes
// an already existing TCP connection and SSH configuration.
func New(address string, config *Config) (result *Communicator, err error) {
// Establish an initial connection and connect
result = &Communicator{
config: config,
address: address,
}
// reset the logger in case custom default has been set
logger = vagrant.DefaultLogger().Named("communicator.ssh")
return
}
func (c *Communicator) Connect() error {
return c.reconnect()
}
func (c *Communicator) Disconnect() (err error) {
if c.conn != nil {
logger.Info("closing connection")
err = c.conn.Close()
} else {
err = errors.New("No connection currently established to close")
}
return
}
func (c *Communicator) Timeout() time.Duration {
return c.config.Timeout
}
func (c *Communicator) Start(cmd *communicator.Cmd) (err error) {
session, err := c.newSession()
if err != nil {
return
}
// Setup our session
session.Stdin = cmd.Stdin
session.Stdout = cmd.Stdout
session.Stderr = cmd.Stderr
if c.config.Pty {
// Request a PTY
termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err = session.RequestPty("xterm", 40, 80, termModes); err != nil {
return
}
}
logger.Debug("starting remote command", "command", cmd.Command)
err = session.Start(cmd.Command + "\n")
if err != nil {
return
}
go func() {
if c.config.KeepAliveInterval <= 0 {
return
}
c := time.NewTicker(c.config.KeepAliveInterval)
defer c.Stop()
for range c.C {
_, err := session.SendRequest("keepalive@vagrantup.com", true, nil)
if err != nil {
return
}
}
}()
// Start a goroutine to wait for the session to end and set the
// exit boolean and status.
go func() {
defer session.Close()
err := session.Wait()
exitStatus := 0
if err != nil {
switch err.(type) {
case *ssh.ExitError:
exitStatus = err.(*ssh.ExitError).ExitStatus()
logger.Error("remote command exited non-zero",
"exitcode", exitStatus, "command", cmd.Command)
case *ssh.ExitMissingError:
logger.Error("remote command exited without exit status or exit signal",
"command", cmd.Command)
exitStatus = communicator.CmdDisconnect
default:
logger.Error("error waiting for ssh session", "error", err)
}
}
cmd.SetExitStatus(exitStatus, err)
}()
return
}
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
if c.config.UseSftp {
return c.sftpUploadSession(path, input, fi)
} else {
return c.scpUploadSession(path, input, fi)
}
}
func (c *Communicator) UploadDir(dst string, src string, excl []string) error {
logger.Debug("uploading directory", "source", src, "destination", dst)
if c.config.UseSftp {
return c.sftpUploadDirSession(dst, src, excl)
} else {
return c.scpUploadDirSession(dst, src, excl)
}
}
func (c *Communicator) DownloadDir(src string, dst string, excl []string) error {
logger.Debug("downloading directory", "source", src, "destination", dst)
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
dirStack := []string{dst}
for {
fmt.Fprint(w, "\x00")
// read file info
fi, err := stdoutR.ReadString('\n')
if err != nil {
return err
}
if len(fi) < 0 {
return fmt.Errorf("empty response from server")
}
switch fi[0] {
case '\x01', '\x02':
return fmt.Errorf("%s", fi[1:])
case 'C', 'D':
break
case 'E':
dirStack = dirStack[:len(dirStack)-1]
if len(dirStack) == 0 {
fmt.Fprint(w, "\x00")
return nil
}
continue
default:
return fmt.Errorf("unexpected server response (%x)", fi[0])
}
var mode int64
var size int64
var name string
logger.Debug("download directory", "str", fi)
n, err := fmt.Sscanf(fi[1:], "%o %d %s", &mode, &size, &name)
if err != nil || n != 3 {
return fmt.Errorf("can't parse server response (%s)", fi)
}
if size < 0 {
return fmt.Errorf("negative file size")
}
logger.Debug("download directory", "mode", mode, "size", size, "name", name)
dst = filepath.Join(dirStack...)
switch fi[0] {
case 'D':
err = os.MkdirAll(filepath.Join(dst, name), os.FileMode(mode))
if err != nil {
return err
}
dirStack = append(dirStack, name)
continue
case 'C':
fmt.Fprint(w, "\x00")
err = scpDownloadFile(filepath.Join(dst, name), stdoutR, size, os.FileMode(mode))
if err != nil {
return err
}
}
if err := checkSCPStatus(stdoutR); err != nil {
return err
}
}
}
return c.scpSession("scp -vrf "+src, scpFunc)
}
func (c *Communicator) Download(path string, output io.Writer) error {
if c.config.UseSftp {
return c.sftpDownloadSession(path, output)
}
return c.scpDownloadSession(path, output)
}
func (c *Communicator) newSession() (session *ssh.Session, err error) {
logger.Debug("opening new ssh session")
if c.client == nil {
err = errors.New("client not available")
} else {
session, err = c.client.NewSession()
}
if err != nil {
logger.Error("ssh session open failure", "error", err)
if err := c.reconnect(); err != nil {
return nil, err
}
if c.client == nil {
return nil, errors.New("client not available")
} else {
return c.client.NewSession()
}
}
return session, nil
}
func (c *Communicator) reconnect() (err error) {
// Ignore errors here because we don't care if it fails
c.Disconnect()
// Set the conn and client to nil since we'll recreate it
c.conn = nil
c.client = nil
logger.Debug("reconnection to tcp connection for ssh")
c.conn, err = c.config.Connection()
if err != nil {
// Explicitly set this to the REAL nil. Connection() can return
// a nil implementation of net.Conn which will make the
// "if c.conn == nil" check fail above. Read here for more information
// on this psychotic language feature:
//
// http://golang.org/doc/faq#nil_error
c.conn = nil
logger.Error("reconnection failure", "error", err)
return
}
if c.config.Timeout > 0 {
c.conn = &timeoutConn{c.conn, c.config.Timeout, c.config.Timeout}
}
logger.Debug("handshaking with ssh")
// Default timeout to 1 minute if it wasn't specified (zero value). For
// when you need to handshake from low orbit.
var duration time.Duration
if c.config.HandshakeTimeout == 0 {
duration = 1 * time.Minute
} else {
duration = c.config.HandshakeTimeout
}
connectionEstablished := make(chan struct{}, 1)
var sshConn ssh.Conn
var sshChan <-chan ssh.NewChannel
var req <-chan *ssh.Request
go func() {
sshConn, sshChan, req, err = ssh.NewClientConn(c.conn, c.address, c.config.SSHConfig)
close(connectionEstablished)
}()
select {
case <-connectionEstablished:
// We don't need to do anything here. We just want select to block until
// we connect or timeout.
case <-time.After(duration):
if c.conn != nil {
c.conn.Close()
}
if sshConn != nil {
sshConn.Close()
}
return ErrHandshakeTimeout
}
if err != nil {
return
}
logger.Debug("handshake complete")
if sshConn != nil {
c.client = ssh.NewClient(sshConn, sshChan, req)
}
c.connectToAgent()
return
}
func (c *Communicator) connectToAgent() {
if c.client == nil {
return
}
if c.config.DisableAgentForwarding {
logger.Info("SSH agent forwarding is disabled")
return
}
// open connection to the local agent
socketLocation := os.Getenv("SSH_AUTH_SOCK")
if socketLocation == "" {
logger.Info("no local agent socket, will not connect agent")
return
}
agentConn, err := net.Dial("unix", socketLocation)
if err != nil {
logger.Error("could not connect to local agent socket", "path", socketLocation)
return
}
// create agent and add in auth
forwardingAgent := agent.NewClient(agentConn)
if forwardingAgent == nil {
logger.Error("could not create agent client")
agentConn.Close()
return
}
// add callback for forwarding agent to SSH config
// XXX - might want to handle reconnects appending multiple callbacks
auth := ssh.PublicKeysCallback(forwardingAgent.Signers)
c.config.SSHConfig.Auth = append(c.config.SSHConfig.Auth, auth)
agent.ForwardToAgent(c.client, forwardingAgent)
// Setup a session to request agent forwarding
session, err := c.newSession()
if err != nil {
return
}
defer session.Close()
err = agent.RequestAgentForwarding(session)
if err != nil {
logger.Error("request agent forwarding failed", "error", err)
return
}
logger.Info("agent forwarding enabled")
return
}
func (c *Communicator) sftpUploadSession(path string, input io.Reader, fi *os.FileInfo) error {
sftpFunc := func(client *sftp.Client) error {
return c.sftpUploadFile(path, input, client, fi)
}
return c.sftpSession(sftpFunc)
}
func (c *Communicator) sftpUploadFile(path string, input io.Reader, client *sftp.Client, fi *os.FileInfo) error {
logger.Debug("sftp uploading", "path", path)
f, err := client.Create(path)
if err != nil {
return err
}
defer f.Close()
if _, err = io.Copy(f, input); err != nil {
return err
}
if fi != nil && (*fi).Mode().IsRegular() {
mode := (*fi).Mode().Perm()
err = client.Chmod(path, mode)
if err != nil {
return err
}
}
return nil
}
func (c *Communicator) sftpUploadDirSession(dst string, src string, excl []string) error {
sftpFunc := func(client *sftp.Client) error {
rootDst := dst
if src[len(src)-1] != '/' {
logger.Debug("no trailing slash, creating the source directory name")
rootDst = filepath.Join(dst, filepath.Base(src))
}
walkFunc := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Calculate the final destination using the
// base source and root destination
relSrc, err := filepath.Rel(src, path)
if err != nil {
return err
}
finalDst := filepath.Join(rootDst, relSrc)
// In Windows, Join uses backslashes which we don't want to get
// to the sftp server
finalDst = filepath.ToSlash(finalDst)
// Skip the creation of the target destination directory since
// it should exist and we might not even own it
if finalDst == dst {
return nil
}
return c.sftpVisitFile(finalDst, path, info, client)
}
return filepath.Walk(src, walkFunc)
}
return c.sftpSession(sftpFunc)
}
func (c *Communicator) sftpMkdir(path string, client *sftp.Client, fi os.FileInfo) error {
logger.Debug("sftp create directory", "path", path)
if err := client.Mkdir(path); err != nil {
// Do not consider it an error if the directory existed
remoteFi, fiErr := client.Lstat(path)
if fiErr != nil || !remoteFi.IsDir() {
return err
}
}
mode := fi.Mode().Perm()
if err := client.Chmod(path, mode); err != nil {
return err
}
return nil
}
func (c *Communicator) sftpVisitFile(dst string, src string, fi os.FileInfo, client *sftp.Client) error {
if !fi.IsDir() {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
return c.sftpUploadFile(dst, f, client, &fi)
} else {
err := c.sftpMkdir(dst, client, fi)
return err
}
}
func (c *Communicator) sftpDownloadSession(path string, output io.Writer) error {
sftpFunc := func(client *sftp.Client) error {
f, err := client.Open(path)
if err != nil {
return err
}
defer f.Close()
if _, err = io.Copy(output, f); err != nil {
return err
}
return nil
}
return c.sftpSession(sftpFunc)
}
func (c *Communicator) sftpSession(f func(*sftp.Client) error) error {
client, err := c.newSftpClient()
if err != nil {
return fmt.Errorf("sftpSession error: %s", err.Error())
}
defer client.Close()
return f(client)
}
func (c *Communicator) newSftpClient() (*sftp.Client, error) {
session, err := c.newSession()
if err != nil {
return nil, err
}
if err := session.RequestSubsystem("sftp"); err != nil {
return nil, err
}
pw, err := session.StdinPipe()
if err != nil {
return nil, err
}
pr, err := session.StdoutPipe()
if err != nil {
return nil, err
}
// Capture stdout so we can return errors to the user
var stdout bytes.Buffer
tee := io.TeeReader(pr, &stdout)
client, err := sftp.NewClientPipe(tee, pw)
if err != nil && stdout.Len() > 0 {
logger.Error("upload failed", "error", stdout.Bytes())
}
return client, err
}
func (c *Communicator) scpUploadSession(path string, input io.Reader, fi *os.FileInfo) error {
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash separators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
// Escape spaces in remote directory
target_dir = strings.Replace(target_dir, " ", "\\ ", -1)
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
return scpUploadFile(target_file, input, w, stdoutR, fi)
}
return c.scpSession("scp -vt "+target_dir, scpFunc)
}
func (c *Communicator) scpUploadDirSession(dst string, src string, excl []string) error {
scpFunc := func(w io.Writer, r *bufio.Reader) error {
uploadEntries := func() error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
entries, err := f.Readdir(-1)
if err != nil {
return err
}
return scpUploadDir(src, entries, w, r)
}
if src[len(src)-1] != '/' {
logger.Debug("no trailing slash, creating the source directory name")
fi, err := os.Stat(src)
if err != nil {
return err
}
return scpUploadDirProtocol(filepath.Base(src), w, r, uploadEntries, fi)
} else {
// Trailing slash, so only upload the contents
return uploadEntries()
}
}
return c.scpSession("scp -rvt "+dst, scpFunc)
}
func (c *Communicator) scpDownloadSession(path string, output io.Writer) error {
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
fmt.Fprint(w, "\x00")
// read file info
fi, err := stdoutR.ReadString('\n')
if err != nil {
return err
}
if len(fi) < 0 {
return fmt.Errorf("empty response from server")
}
switch fi[0] {
case '\x01', '\x02':
return fmt.Errorf("%s", fi[1:])
case 'C':
case 'D':
return fmt.Errorf("remote file is directory")
default:
return fmt.Errorf("unexpected server response (%x)", fi[0])
}
var mode string
var size int64
n, err := fmt.Sscanf(fi, "%6s %d ", &mode, &size)
if err != nil || n != 2 {
return fmt.Errorf("can't parse server response (%s)", fi)
}
if size < 0 {
return fmt.Errorf("negative file size")
}
fmt.Fprint(w, "\x00")
if _, err := io.CopyN(output, stdoutR, size); err != nil {
return err
}
fmt.Fprint(w, "\x00")
return checkSCPStatus(stdoutR)
}
if !strings.Contains(path, " ") {
return c.scpSession("scp -vf "+path, scpFunc)
}
return c.scpSession("scp -vf "+strconv.Quote(path), scpFunc)
}
func (c *Communicator) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
session, err := c.newSession()
if err != nil {
return err
}
defer session.Close()
// Get a pipe to stdin so that we can send data down
stdinW, err := session.StdinPipe()
if err != nil {
return err
}
// We only want to close once, so we nil w after we close it,
// and only close in the defer if it hasn't been closed already.
defer func() {
if stdinW != nil {
stdinW.Close()
}
}()
// Get a pipe to stdout so that we can get responses back
stdoutPipe, err := session.StdoutPipe()
if err != nil {
return err
}
stdoutR := bufio.NewReader(stdoutPipe)
// Set stderr to a bytes buffer
stderr := new(bytes.Buffer)
session.Stderr = stderr
// Start the sink mode on the other side
// TODO(mitchellh): There are probably issues with shell escaping the path
logger.Debug("starting remote scp process", "command", scpCommand)
if err := session.Start(scpCommand); err != nil {
return err
}
// Call our callback that executes in the context of SCP. We ignore
// EOF errors if they occur because it usually means that SCP prematurely
// ended on the other side.
logger.Debug("started scp session, beginning transfers")
if err := f(stdinW, stdoutR); err != nil && err != io.EOF {
return err
}
// Close the stdin, which sends an EOF, and then set w to nil so that
// our defer func doesn't close it again since that is unsafe with
// the Go SSH package.
logger.Debug("scp sessiono complete, closing stdin pipe")
stdinW.Close()
stdinW = nil
// Wait for the SCP connection to close, meaning it has consumed all
// our data and has completed. Or has errored.
logger.Debug("waiting for ssh session to complete")
err = session.Wait()
if err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
// Otherwise, we have an ExitError, meaning we can just read
// the exit status
logger.Debug("non-zero exit status", "exitcode", exitErr.ExitStatus())
stdoutB, err := ioutil.ReadAll(stdoutR)
if err != nil {
return err
}
logger.Debug("scp output", "output", stdoutB)
// If we exited with status 127, it means SCP isn't available.
// Return a more descriptive error for that.
if exitErr.ExitStatus() == 127 {
return errors.New(
"SCP failed to start. This usually means that SCP is not\n" +
"properly installed on the remote system.")
}
}
return err
}
logger.Debug("scp stderr", "length", stderr.Len(), "content", stderr.String())
return nil
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
func checkSCPStatus(r *bufio.Reader) error {
code, err := r.ReadByte()
if err != nil {
return err
}
if code != 0 {
// Treat any non-zero (really 1 and 2) as fatal errors
message, _, err := r.ReadLine()
if err != nil {
return fmt.Errorf("Error reading error message: %s", err)
}
return errors.New(string(message))
}
return nil
}
func scpDownloadFile(dst string, src io.Reader, size int64, mode os.FileMode) error {
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return err
}
defer f.Close()
if _, err := io.CopyN(f, src, size); err != nil {
return err
}
return nil
}
func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader, fi *os.FileInfo) error {
var mode os.FileMode
var size int64
if fi != nil && (*fi).Mode().IsRegular() {
mode = (*fi).Mode().Perm()
size = (*fi).Size()
} else {
// Create a temporary file where we can copy the contents of the src
// so that we can determine the length, since SCP is length-prefixed.
tf, err := ioutil.TempFile("", "vagrant-upload")
if err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
mode = 0644
logger.Debug("copying input data to temporary file to read length")
if _, err := io.Copy(tf, src); err != nil {
return fmt.Errorf("Error copying input data into local temporary "+
"file. Check that TEMPDIR has enough space. Error: %s", err)
}
// Sync the file so that the contents are definitely on disk, then
// read the length of it.
if err := tf.Sync(); err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
// Seek the file to the beginning so we can re-read all of it
if _, err := tf.Seek(0, 0); err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
tfi, err := tf.Stat()
if err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
size = tfi.Size()
src = tf
}
// Start the protocol
perms := fmt.Sprintf("C%04o", mode)
logger.Debug("scp uploading", "path", dst, "perms", perms, "size", size)
fmt.Fprintln(w, perms, size, dst)
if err := checkSCPStatus(r); err != nil {
return err
}
if _, err := io.CopyN(w, src, size); err != nil {
return err
}
fmt.Fprint(w, "\x00")
return checkSCPStatus(r)
}
func scpUploadDirProtocol(name string, w io.Writer, r *bufio.Reader, f func() error, fi os.FileInfo) error {
logger.Debug("scp directory upload", "path", name)
mode := fi.Mode().Perm()
perms := fmt.Sprintf("D%04o 0", mode)
fmt.Fprintln(w, perms, name)
err := checkSCPStatus(r)
if err != nil {
return err
}
if err := f(); err != nil {
return err
}
fmt.Fprintln(w, "E")
return err
}
func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) error {
for _, fi := range fs {
realPath := filepath.Join(root, fi.Name())
// Track if this is actually a symlink to a directory. If it is
// a symlink to a file we don't do any special behavior because uploading
// a file just works. If it is a directory, we need to know so we
// treat it as such.
isSymlinkToDir := false
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
symPath, err := filepath.EvalSymlinks(realPath)
if err != nil {
return err
}
symFi, err := os.Lstat(symPath)
if err != nil {
return err
}
isSymlinkToDir = symFi.IsDir()
}
if !fi.IsDir() && !isSymlinkToDir {
// It is a regular file (or symlink to a file), just upload it
f, err := os.Open(realPath)
if err != nil {
return err
}
err = func() error {
defer f.Close()
return scpUploadFile(fi.Name(), f, w, r, &fi)
}()
if err != nil {
return err
}
continue
}
// It is a directory, recursively upload
err := scpUploadDirProtocol(fi.Name(), w, r, func() error {
f, err := os.Open(realPath)
if err != nil {
return err
}
defer f.Close()
entries, err := f.Readdir(-1)
if err != nil {
return err
}
return scpUploadDir(realPath, entries, w, r)
}, fi)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,235 @@
// +build !race
package ssh
import (
"bytes"
"fmt"
"net"
"testing"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
"golang.org/x/crypto/ssh"
)
// private key for mock server
const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA19lGVsTqIT5iiNYRgnoY1CwkbETW5cq+Rzk5v/kTlf31XpSU
70HVWkbTERECjaYdXM2gGcbb+sxpq6GtXf1M3kVomycqhxwhPv4Cr6Xp4WT/jkFx
9z+FFzpeodGJWjOH6L2H5uX1Cvr9EDdQp9t9/J32/qBFntY8GwoUI/y/1MSTmMiF
tupdMODN064vd3gyMKTwrlQ8tZM6aYuyOPsutLlUY7M5x5FwMDYvnPDSeyT/Iw0z
s3B+NCyqeeMd2T7YzQFnRATj0M7rM5LoSs7DVqVriOEABssFyLj31PboaoLhOKgc
qoM9khkNzr7FHVvi+DhYM2jD0DwvqZLN6NmnLwIDAQABAoIBAQCGVj+kuSFOV1lT
+IclQYA6bM6uY5mroqcSBNegVxCNhWU03BxlW//BE9tA/+kq53vWylMeN9mpGZea
riEMIh25KFGWXqXlOOioH8bkMsqA8S7sBmc7jljyv+0toQ9vCCtJ+sueNPhxQQxH
D2YvUjfzBQ04I9+wn30BByDJ1QA/FoPsunxIOUCcRBE/7jxuLYcpR+JvEF68yYIh
atXRld4W4in7T65YDR8jK1Uj9XAcNeDYNpT/M6oFLx1aPIlkG86aCWRO19S1jLPT
b1ZAKHHxPMCVkSYW0RqvIgLXQOR62D0Zne6/2wtzJkk5UCjkSQ2z7ZzJpMkWgDgN
ifCULFPBAoGBAPoMZ5q1w+zB+knXUD33n1J+niN6TZHJulpf2w5zsW+m2K6Zn62M
MXndXlVAHtk6p02q9kxHdgov34Uo8VpuNjbS1+abGFTI8NZgFo+bsDxJdItemwC4
KJ7L1iz39hRN/ZylMRLz5uTYRGddCkeIHhiG2h7zohH/MaYzUacXEEy3AoGBANz8
e/msleB+iXC0cXKwds26N4hyMdAFE5qAqJXvV3S2W8JZnmU+sS7vPAWMYPlERPk1
D8Q2eXqdPIkAWBhrx4RxD7rNc5qFNcQWEhCIxC9fccluH1y5g2M+4jpMX2CT8Uv+
3z+NoJ5uDTXZTnLCfoZzgZ4nCZVZ+6iU5U1+YXFJAoGBANLPpIV920n/nJmmquMj
orI1R/QXR9Cy56cMC65agezlGOfTYxk5Cfl5Ve+/2IJCfgzwJyjWUsFx7RviEeGw
64o7JoUom1HX+5xxdHPsyZ96OoTJ5RqtKKoApnhRMamau0fWydH1yeOEJd+TRHhc
XStGfhz8QNa1dVFvENczja1vAoGABGWhsd4VPVpHMc7lUvrf4kgKQtTC2PjA4xoc
QJ96hf/642sVE76jl+N6tkGMzGjnVm4P2j+bOy1VvwQavKGoXqJBRd5Apppv727g
/SM7hBXKFc/zH80xKBBgP/i1DR7kdjakCoeu4ngeGywvu2jTS6mQsqzkK+yWbUxJ
I7mYBsECgYB/KNXlTEpXtz/kwWCHFSYA8U74l7zZbVD8ul0e56JDK+lLcJ0tJffk
gqnBycHj6AhEycjda75cs+0zybZvN4x65KZHOGW/O/7OAWEcZP5TPb3zf9ned3Hl
NsZoFj52ponUM6+99A2CmezFCN16c4mbA//luWF+k3VVqR6BpkrhKw==
-----END RSA PRIVATE KEY-----`
var serverConfig = &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if c.User() == "user" && string(pass) == "pass" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
}
func init() {
// Parse and set the private key of the server, required to accept connections
signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
if err != nil {
panic("unable to parse private key: " + err.Error())
}
serverConfig.AddHostKey(signer)
}
func newMockLineServer(t *testing.T) string {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to listen for connection: %s", err)
}
go func() {
defer l.Close()
c, err := l.Accept()
if err != nil {
t.Errorf("Unable to accept incoming connection: %s", err)
}
defer c.Close()
conn, chans, _, err := ssh.NewServerConn(c, serverConfig)
if err != nil {
t.Logf("Handshaking error: %v", err)
}
t.Log("Accepted SSH connection")
for newChannel := range chans {
channel, _, err := newChannel.Accept()
if err != nil {
t.Errorf("Unable to accept channel.")
}
t.Log("Accepted channel")
go func(channelType string) {
defer channel.Close()
conn.OpenChannel(channelType, nil)
}(newChannel.ChannelType())
}
conn.Close()
}()
return l.Addr().String()
}
func newMockBrokenServer(t *testing.T) string {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable tp listen for connection: %s", err)
}
go func() {
defer l.Close()
c, err := l.Accept()
if err != nil {
t.Errorf("Unable to accept incoming connection: %s", err)
}
defer c.Close()
// This should block for a period of time longer than our timeout in
// the test case. That way we invoke a failure scenario.
t.Log("Block on handshaking for SSH connection")
time.Sleep(5 * time.Second)
}()
return l.Addr().String()
}
func TestCommIsCommunicator(t *testing.T) {
var raw interface{}
raw = &Communicator{}
if _, ok := raw.(communicator.Communicator); !ok {
t.Fatalf("Communicator must be a communicator")
}
}
func TestNew_Invalid(t *testing.T) {
clientConfig := &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{
ssh.Password("i-am-invalid"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := newMockLineServer(t)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
t.Errorf("Unable to accept incoming connection: %v", err)
}
return conn, err
}
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
}
comm, err := New(address, config)
if err != nil {
t.Fatalf("Failed to setup communicator: %s", err)
}
err = comm.Connect()
if err == nil {
t.Fatal("should have had an error connecting")
}
}
func TestStart(t *testing.T) {
clientConfig := &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{
ssh.Password("pass"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := newMockLineServer(t)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
}
return conn, err
}
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
}
client, err := New(address, config)
if err != nil {
t.Fatalf("error connecting to SSH: %s", err)
}
cmd := &communicator.Cmd{
Command: "echo foo",
Stdout: new(bytes.Buffer),
}
client.Start(cmd)
}
func TestHandshakeTimeout(t *testing.T) {
clientConfig := &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{
ssh.Password("pass"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := newMockBrokenServer(t)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
}
return conn, err
}
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
HandshakeTimeout: 50 * time.Millisecond,
}
comm, err := New(address, config)
if err != nil {
t.Fatalf("Failed to setup communicator: %s", err)
}
err = comm.Connect()
if err != ErrHandshakeTimeout {
// Note: there's another error that can come back from this call:
// ssh: handshake failed: EOF
// This should appear in cases where the handshake fails because of
// malformed (or no) data sent back by the server, but should not happen
// in a timeout scenario.
t.Fatalf("Expected handshake timeout, got: %s", err)
}
}

View File

@ -0,0 +1,88 @@
package ssh
import (
"fmt"
"net"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/net/proxy"
)
// ConnectFunc is a convenience method for returning a function
// that just uses net.Dial to communicate with the remote end that
// is suitable for use with the SSH communicator configuration.
func ConnectFunc(network, addr string) func() (net.Conn, error) {
return func() (net.Conn, error) {
c, err := net.DialTimeout(network, addr, 15*time.Second)
if err != nil {
return nil, err
}
if tcpConn, ok := c.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(5 * time.Second)
}
return c, nil
}
}
// ProxyConnectFunc is a convenience method for returning a function
// that connects to a host using SOCKS5 proxy
func ProxyConnectFunc(socksProxy string, socksAuth *proxy.Auth, network, addr string) func() (net.Conn, error) {
return func() (net.Conn, error) {
// create a socks5 dialer
dialer, err := proxy.SOCKS5("tcp", socksProxy, socksAuth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("Can't connect to the proxy: %s", err)
}
c, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
return c, nil
}
}
// BastionConnectFunc is a convenience method for returning a function
// that connects to a host over a bastion connection.
func BastionConnectFunc(
bProto string,
bAddr string,
bConf *ssh.ClientConfig,
proto string,
addr string) func() (net.Conn, error) {
return func() (net.Conn, error) {
// Connect to the bastion
bastion, err := ssh.Dial(bProto, bAddr, bConf)
if err != nil {
return nil, fmt.Errorf("Error connecting to bastion: %s", err)
}
// Connect through to the end host
conn, err := bastion.Dial(proto, addr)
if err != nil {
bastion.Close()
return nil, err
}
// Wrap it up so we close both things properly
return &bastionConn{
Conn: conn,
Bastion: bastion,
}, nil
}
}
type bastionConn struct {
net.Conn
Bastion *ssh.Client
}
func (c *bastionConn) Close() error {
c.Conn.Close()
return c.Bastion.Close()
}

View File

@ -0,0 +1,30 @@
package ssh
import (
"net"
"time"
)
// timeoutConn wraps a net.Conn, and sets a deadline for every read
// and write operation.
type timeoutConn struct {
net.Conn
ReadTimeout time.Duration
WriteTimeout time.Duration
}
func (c *timeoutConn) Read(b []byte) (int, error) {
err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
if err != nil {
return 0, err
}
return c.Conn.Read(b)
}
func (c *timeoutConn) Write(b []byte) (int, error) {
err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
if err != nil {
return 0, err
}
return c.Conn.Write(b)
}

View File

@ -0,0 +1,25 @@
package ssh
import (
"golang.org/x/crypto/ssh"
)
// An implementation of ssh.KeyboardInteractiveChallenge that simply sends
// back the password for all questions. The questions are logged.
func PasswordKeyboardInteractive(password string) ssh.KeyboardInteractiveChallenge {
return func(user, instruction string, questions []string, echos []bool) ([]string, error) {
logger.Info("keyboard interactive challenge", "user", user,
"instructions", instruction)
for i, question := range questions {
logger.Info("challenge question", "number", i+1, "question", question)
}
// Just send the password back for all questions
answers := make([]string, len(questions))
for i := range answers {
answers[i] = password
}
return answers, nil
}
}

View File

@ -0,0 +1,28 @@
package ssh
import (
"reflect"
"testing"
"golang.org/x/crypto/ssh"
)
func TestPasswordKeyboardInteractive_Impl(t *testing.T) {
var raw interface{}
raw = PasswordKeyboardInteractive("foo")
if _, ok := raw.(ssh.KeyboardInteractiveChallenge); !ok {
t.Fatal("PasswordKeyboardInteractive must implement KeyboardInteractiveChallenge")
}
}
func TestPasswordKeyboardInteractive_Challenge(t *testing.T) {
p := PasswordKeyboardInteractive("foo")
result, err := p("foo", "bar", []string{"one", "two"}, nil)
if err != nil {
t.Fatalf("err not nil: %s", err)
}
if !reflect.DeepEqual(result, []string{"foo", "foo"}) {
t.Fatalf("invalid password: %#v", result)
}
}

View File

@ -0,0 +1,257 @@
package winrm
import (
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
"github.com/masterzen/winrm"
"github.com/packer-community/winrmcp/winrmcp"
)
var logger = vagrant.DefaultLogger().Named("communicator.winrm")
// Communicator represents the WinRM communicator
type Communicator struct {
config *Config
client *winrm.Client
endpoint *winrm.Endpoint
}
// New creates a new communicator implementation over WinRM.
func New(config *Config) (*Communicator, error) {
endpoint := &winrm.Endpoint{
Host: config.Host,
Port: config.Port,
HTTPS: config.Https,
Insecure: config.Insecure,
/*
TODO
HTTPS: connInfo.HTTPS,
Insecure: connInfo.Insecure,
CACert: connInfo.CACert,
*/
}
// Create the client
params := *winrm.DefaultParameters
if config.TransportDecorator != nil {
params.TransportDecorator = config.TransportDecorator
}
params.Timeout = formatDuration(config.Timeout)
client, err := winrm.NewClientWithParameters(
endpoint, config.Username, config.Password, &params)
if err != nil {
return nil, err
}
return &Communicator{
config: config,
client: client,
endpoint: endpoint,
}, nil
}
func (c *Communicator) Connect() (err error) {
// Create the shell to verify the connection
logger.Debug("connecting to remote shell")
shell, err := c.client.CreateShell()
if err != nil {
logger.Error("connection failure", "error", err)
return
}
if err = shell.Close(); err != nil {
logger.Error("connection close failure", "error", err)
}
return
}
// Start implementation of communicator.Communicator interface
func (c *Communicator) Start(rc *communicator.Cmd) error {
shell, err := c.client.CreateShell()
if err != nil {
return err
}
logger.Info("starting remote command", "commmand", rc.Command)
rc.Init()
cmd, err := shell.Execute(rc.Command)
if err != nil {
return err
}
go runCommand(shell, cmd, rc)
return nil
}
func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *communicator.Cmd) {
defer shell.Close()
var wg sync.WaitGroup
copyFunc := func(w io.Writer, r io.Reader) {
defer wg.Done()
io.Copy(w, r)
}
if rc.Stdout != nil && cmd.Stdout != nil {
wg.Add(1)
go copyFunc(rc.Stdout, cmd.Stdout)
} else {
logger.Warn("failed to read stdout", "command", rc.Command)
}
if rc.Stderr != nil && cmd.Stderr != nil {
wg.Add(1)
go copyFunc(rc.Stderr, cmd.Stderr)
} else {
logger.Warn("failed to read stderr", "command", rc.Command)
}
cmd.Wait()
wg.Wait()
code := cmd.ExitCode()
logger.Info("command complete", "exitcode", code, "command", rc.Command)
rc.SetExitStatus(code, nil)
}
// Upload implementation of communicator.Communicator interface
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
wcp, err := c.newCopyClient()
if err != nil {
return fmt.Errorf("Was unable to create winrm client: %s", err)
}
if strings.HasSuffix(path, `\`) {
// path is a directory
path += filepath.Base((*fi).Name())
}
logger.Info("uploading file", "path", path)
return wcp.Write(path, input)
}
// UploadDir implementation of communicator.Communicator interface
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
if !strings.HasSuffix(src, "/") {
dst = fmt.Sprintf("%s\\%s", dst, filepath.Base(src))
}
logger.Info("uploading directory", "source", src, "destination", dst)
wcp, err := c.newCopyClient()
if err != nil {
return err
}
return wcp.Copy(src, dst)
}
func (c *Communicator) Download(src string, dst io.Writer) error {
client, err := c.newWinRMClient()
if err != nil {
return err
}
encodeScript := `$file=[System.IO.File]::ReadAllBytes("%s"); Write-Output $([System.Convert]::ToBase64String($file))`
base64DecodePipe := &Base64Pipe{w: dst}
cmd := winrm.Powershell(fmt.Sprintf(encodeScript, src))
_, err = client.Run(cmd, base64DecodePipe, ioutil.Discard)
return err
}
func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
return fmt.Errorf("WinRM doesn't support download dir.")
}
func (c *Communicator) getClientConfig() *winrmcp.Config {
return &winrmcp.Config{
Auth: winrmcp.Auth{
User: c.config.Username,
Password: c.config.Password,
},
Https: c.config.Https,
Insecure: c.config.Insecure,
OperationTimeout: c.config.Timeout,
MaxOperationsPerShell: 15, // lowest common denominator
TransportDecorator: c.config.TransportDecorator,
}
}
func (c *Communicator) newCopyClient() (*winrmcp.Winrmcp, error) {
addr := fmt.Sprintf("%s:%d", c.endpoint.Host, c.endpoint.Port)
clientConfig := c.getClientConfig()
return winrmcp.New(addr, clientConfig)
}
func (c *Communicator) newWinRMClient() (*winrm.Client, error) {
conf := c.getClientConfig()
// Shamelessly borrowed from the winrmcp client to ensure
// that the client is configured using the same defaulting behaviors that
// winrmcp uses even we we aren't using winrmcp. This ensures similar
// behavior between upload, download, and copy functions. We can't use the
// one generated by winrmcp because it isn't exported.
var endpoint *winrm.Endpoint
endpoint = &winrm.Endpoint{
Host: c.endpoint.Host,
Port: c.endpoint.Port,
HTTPS: conf.Https,
Insecure: conf.Insecure,
TLSServerName: conf.TLSServerName,
CACert: conf.CACertBytes,
Timeout: conf.ConnectTimeout,
}
params := winrm.NewParameters(
winrm.DefaultParameters.Timeout,
winrm.DefaultParameters.Locale,
winrm.DefaultParameters.EnvelopeSize,
)
params.TransportDecorator = conf.TransportDecorator
params.Timeout = "PT3M"
client, err := winrm.NewClientWithParameters(
endpoint, conf.Auth.User, conf.Auth.Password, params)
return client, err
}
type Base64Pipe struct {
w io.Writer // underlying writer (file, buffer)
}
func (d *Base64Pipe) ReadFrom(r io.Reader) (int64, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return 0, err
}
var i int
i, err = d.Write(b)
if err != nil {
return 0, err
}
return int64(i), err
}
func (d *Base64Pipe) Write(p []byte) (int, error) {
dst := make([]byte, base64.StdEncoding.DecodedLen(len(p)))
decodedBytes, err := base64.StdEncoding.Decode(dst, p)
if err != nil {
return 0, err
}
return d.w.Write(dst[0:decodedBytes])
}

View File

@ -0,0 +1,123 @@
package winrm
import (
"bytes"
"io"
"strings"
"testing"
"time"
"github.com/dylanmei/winrmtest"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
)
const PAYLOAD = "stuff"
const BASE64_ENCODED_PAYLOAD = "c3R1ZmY="
func newMockWinRMServer(t *testing.T) *winrmtest.Remote {
wrm := winrmtest.NewRemote()
wrm.CommandFunc(
winrmtest.MatchText("echo foo"),
func(out, err io.Writer) int {
out.Write([]byte("foo"))
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^echo c29tZXRoaW5n >> ".*"$`),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^echo `+BASE64_ENCODED_PAYLOAD+` >> ".*"$`),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^powershell.exe -EncodedCommand .*$`),
func(out, err io.Writer) int {
out.Write([]byte(BASE64_ENCODED_PAYLOAD))
return 0
})
wrm.CommandFunc(
winrmtest.MatchText("powershell"),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchText(`powershell -Command "(Get-Item C:/Temp/vagrant.cmd) -is [System.IO.DirectoryInfo]"`),
func(out, err io.Writer) int {
out.Write([]byte("False"))
return 0
})
return wrm
}
func TestStart(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
c, err := New(&Config{
Host: wrm.Host,
Port: wrm.Port,
Username: "user",
Password: "pass",
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
var cmd communicator.Cmd
stdout := new(bytes.Buffer)
cmd.Command = "echo foo"
cmd.Stdout = stdout
err = c.Start(&cmd)
if err != nil {
t.Fatalf("error executing remote command: %s", err)
}
cmd.Wait()
if stdout.String() != "foo" {
t.Fatalf("bad command response: expected %q, got %q", "foo", stdout.String())
}
}
func TestUpload(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
c, err := New(&Config{
Host: wrm.Host,
Port: wrm.Port,
Username: "user",
Password: "pass",
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
file := "C:/Temp/vagrant.cmd"
err = c.Upload(file, strings.NewReader(PAYLOAD), nil)
if err != nil {
t.Fatalf("error uploading file: %s", err)
}
dest := new(bytes.Buffer)
err = c.Download(file, dest)
if err != nil {
t.Fatalf("error downloading file: %s", err)
}
downloadedPayload := dest.String()
if downloadedPayload != PAYLOAD {
t.Fatalf("files are not equal: expected [%s] length: %v, got [%s] length %v", PAYLOAD, len(PAYLOAD), downloadedPayload, len(downloadedPayload))
}
}

View File

@ -0,0 +1,19 @@
package winrm
import (
"time"
"github.com/masterzen/winrm"
)
// Config is used to configure the WinRM connection
type Config struct {
Host string
Port int
Username string
Password string
Timeout time.Duration
Https bool
Insecure bool
TransportDecorator func() winrm.Transporter
}

View File

@ -0,0 +1,32 @@
package winrm
import (
"fmt"
"time"
)
// formatDuration formats the given time.Duration into an ISO8601
// duration string.
func formatDuration(duration time.Duration) string {
// We're not supporting negative durations
if duration.Seconds() <= 0 {
return "PT0S"
}
h := int(duration.Hours())
m := int(duration.Minutes()) - (h * 60)
s := int(duration.Seconds()) - (h*3600 + m*60)
res := "PT"
if h > 0 {
res = fmt.Sprintf("%s%dH", res, h)
}
if m > 0 {
res = fmt.Sprintf("%s%dM", res, m)
}
if s > 0 {
res = fmt.Sprintf("%s%dS", res, s)
}
return res
}

View File

@ -0,0 +1,36 @@
package winrm
import (
"testing"
"time"
)
func TestFormatDuration(t *testing.T) {
// Test complex duration with hours, minutes, seconds
d := time.Duration(3701) * time.Second
s := formatDuration(d)
if s != "PT1H1M41S" {
t.Fatalf("bad ISO 8601 duration string: %s", s)
}
// Test only minutes duration
d = time.Duration(20) * time.Minute
s = formatDuration(d)
if s != "PT20M" {
t.Fatalf("bad ISO 8601 duration string for 20M: %s", s)
}
// Test only seconds
d = time.Duration(1) * time.Second
s = formatDuration(d)
if s != "PT1S" {
t.Fatalf("bad ISO 8601 duration string for 1S: %s", s)
}
// Test negative duration (unsupported)
d = time.Duration(-1) * time.Second
s = formatDuration(d)
if s != "PT0S" {
t.Fatalf("bad ISO 8601 duration string for negative: %s", s)
}
}

View File

@ -0,0 +1,25 @@
package vagrant
import (
"context"
)
type Config interface {
ConfigAttributes() (attrs []string, err error)
ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error)
ConfigValidate(ctx context.Context, data map[string]interface{}, m *Machine) (errors []string, err error)
ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error)
}
type NoConfig struct{}
func (c *NoConfig) ConfigAttributes() (a []string, e error) { return }
func (c *NoConfig) ConfigLoad(context.Context, map[string]interface{}) (d map[string]interface{}, e error) {
return
}
func (c *NoConfig) ConfigValidate(context.Context, map[string]interface{}, *Machine) (es []string, e error) {
return
}
func (c *NoConfig) ConfigFinalize(context.Context, map[string]interface{}) (f map[string]interface{}, e error) {
return
}

View File

@ -0,0 +1,64 @@
package vagrant
import (
"encoding/json"
"io"
"os"
)
type Environment struct {
ActiveMachines map[string]string `json:"active_machines,omitempty"`
AliasesPath string `json:"aliases_path,omitempty"`
BoxesPath string `json:"boxes_path,omitempty"`
CWD string `json:"cwd,omitempty"`
DataDir string `json:"data_dir,omitempty"`
DefaultPrivateKeyPath string `json:"default_private_key_path,omitempty"`
GemsPath string `json:"gems_path,omitempty"`
HomePath string `json:"home_path,omitempty"`
LocalDataPath string `json:"local_data_path,omitempty"`
MachineNames []string `json:"machine_names,omitempty"`
PrimaryMachineName string `json:"primary_machine_name,omitempty"`
RootPath string `json:"root_path,omitempty"`
TmpPath string `json:"tmp_path,omitempty"`
VagrantfileName string `json:"vagrantfile_name,omitempty"`
UI Ui `json:"-"`
}
func DumpEnvironment(e *Environment) (s string, err error) {
DefaultLogger().Debug("dumping environment to serialized data")
b, err := json.Marshal(e)
if err != nil {
DefaultLogger().Error("environment dump failure", "error", err)
return
}
s = string(b)
return
}
func LoadEnvironment(edata string, ios IOServer) (e *Environment, err error) {
DefaultLogger().Debug("loading environment from serialized data")
e = &Environment{}
err = json.Unmarshal([]byte(edata), e)
if err != nil {
return
}
var stdout io.Writer
var stderr io.Writer
if ios == nil {
stdout = os.Stdout
stderr = os.Stderr
} else {
stdout = &IOWriter{target: "stdout", srv: ios}
stderr = &IOWriter{target: "stderr", srv: ios}
}
e.UI = &TargetedUi{
Target: "vagrant",
Ui: &ColoredUi{
ErrorColor: UiColorRed,
Ui: &BasicUi{
Reader: os.Stdin,
Writer: stdout,
ErrorWriter: stderr},
}}
return
}

View File

@ -0,0 +1,63 @@
package vagrant
import (
"strings"
"testing"
)
func TestLoadEnvironment(t *testing.T) {
env, err := LoadEnvironment("{}", nil)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
if env.UI == nil {
t.Fatalf("no UI configured for environment")
}
}
func TestBadLoadEnvironment(t *testing.T) {
_, err := LoadEnvironment("ack", nil)
if err == nil {
t.Fatalf("expected load error but none provided")
}
}
func TestLoadEnvironmentUIStdout(t *testing.T) {
iosrv := buildio()
env, err := LoadEnvironment("{}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { env.UI.Info("test string") }()
str := <-iosrv.Streams()["stdout"]
if !strings.Contains(str, "test string") {
t.Fatalf("unexpected output: %s", str)
}
}
func TestLoadEnvironmentUIStderr(t *testing.T) {
iosrv := buildio()
env, err := LoadEnvironment("{}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { env.UI.Error("test string") }()
str, err := iosrv.Read("stderr")
if !strings.Contains(str, "test string") {
t.Fatalf("unexpected output: %s", str)
}
}
func TestDumpEnvironment(t *testing.T) {
env, err := LoadEnvironment("{}", nil)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
d, err := DumpEnvironment(env)
if err != nil {
t.Fatalf("unexpected dump error: %s", err)
}
if d != "{}" {
t.Fatalf("unexpected dump information: %s", d)
}
}

View File

@ -0,0 +1,17 @@
package vagrant
import (
hclog "github.com/hashicorp/go-hclog"
)
var GlobalIOServer *IOServer
var defaultLogger = hclog.Default().Named("vagrant")
func DefaultLogger() hclog.Logger {
return defaultLogger
}
func SetDefaultLogger(l hclog.Logger) {
defaultLogger = l
}

View File

@ -0,0 +1,53 @@
package vagrant
import (
"errors"
)
type StreamIO interface {
Read(target string) (content string, err error)
Write(content, target string) (n int, err error)
}
type IOServer interface {
Streams() map[string]chan (string)
StreamIO
}
type IOSrv struct {
Targets map[string]chan (string)
}
func (i *IOSrv) Streams() map[string]chan (string) {
return i.Targets
}
type IOWriter struct {
target string
srv IOServer
}
func (i *IOWriter) Write(b []byte) (n int, err error) {
content := string(b)
n, err = i.srv.Write(content, i.target)
return
}
func (i *IOSrv) Read(target string) (content string, err error) {
if _, ok := i.Streams()[target]; !ok {
err = errors.New("Unknown target defined")
return
}
content = <-i.Streams()[target]
return
}
func (i *IOSrv) Write(content, target string) (n int, err error) {
if _, ok := i.Streams()[target]; !ok {
err = errors.New("Unknown target defined")
return
}
i.Streams()[target] <- content
n = len(content)
return
}

View File

@ -0,0 +1,48 @@
package vagrant
import (
"testing"
)
func buildio() IOServer {
return &IOSrv{
Targets: map[string]chan (string){
"stdout": make(chan string),
"stderr": make(chan string)}}
}
func TestIOSrvWrite(t *testing.T) {
iosrv := buildio()
var i int
go func() { i, _ = iosrv.Write("test string", "stdout") }()
_, _ = iosrv.Read("stdout")
if i != len("test string") {
t.Fatalf("unexpected write bytes %d != %d",
len("test string"), i)
}
}
func TestIOSrvRead(t *testing.T) {
iosrv := buildio()
go func() { _, _ = iosrv.Write("test string", "stdout") }()
r, _ := iosrv.Read("stdout")
if r != "test string" {
t.Fatalf("unexpected read result: %s", r)
}
}
func TestIOSrvWriteBadTarget(t *testing.T) {
iosrv := buildio()
_, err := iosrv.Write("test string", "stdno")
if err == nil {
t.Fatalf("expected error on write")
}
}
func TestIOSrvReadBadTarget(t *testing.T) {
iosrv := buildio()
_, err := iosrv.Read("stdno")
if err == nil {
t.Fatalf("expected error on read")
}
}

View File

@ -0,0 +1,59 @@
package vagrant
import (
"encoding/json"
"io"
"os"
)
type Machine struct {
Box Box `json:"box"`
Config map[string]interface{} `json:"config"`
DataDir string `json:"data_dir,omitempty"`
Env Environment `json:"environment"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
ProviderConfig map[string]interface{} `json:"provider_config"`
ProviderName string `json:"provider_name,omitempty"`
ProviderOptions map[string]string `json:"provider_options"`
UI Ui `json:"-"`
}
func DumpMachine(m *Machine) (s string, err error) {
DefaultLogger().Debug("dumping machine to serialized data")
b, err := json.Marshal(m)
if err != nil {
DefaultLogger().Debug("machine dump failure", "error", err)
return
}
s = string(b)
return
}
func LoadMachine(mdata string, ios IOServer) (m *Machine, err error) {
DefaultLogger().Debug("loading machine from serialized data")
m = &Machine{}
err = json.Unmarshal([]byte(mdata), m)
if err != nil {
return
}
var stdout io.Writer
var stderr io.Writer
if ios == nil {
stdout = os.Stdout
stderr = os.Stderr
} else {
stdout = &IOWriter{target: "stdout", srv: ios}
stderr = &IOWriter{target: "stderr", srv: ios}
}
m.UI = &TargetedUi{
Target: m.Name,
Ui: &ColoredUi{
ErrorColor: UiColorRed,
Ui: &BasicUi{
Reader: os.Stdin,
Writer: stdout,
ErrorWriter: stderr},
}}
return
}

View File

@ -0,0 +1,7 @@
package vagrant
type MachineState struct {
Id string `json:"id"`
ShortDesc string `json:"short_description"`
LongDesc string `json:"long_description"`
}

View File

@ -0,0 +1,53 @@
package vagrant
import (
"strings"
"testing"
)
func TestMachineLoad(t *testing.T) {
_, err := LoadMachine("{}", nil)
if err != nil {
t.Fatalf("failed to load machine: %s", err)
}
}
func TestMachineDump(t *testing.T) {
m, err := LoadMachine("{}", nil)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
_, err = DumpMachine(m)
if err != nil {
t.Fatalf("failed to dump machine: %s", err)
}
}
func TestMachineUI(t *testing.T) {
iosrv := buildio()
m, err := LoadMachine("{}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { m.UI.Info("test string") }()
r, _ := iosrv.Read("stdout")
if !strings.Contains(r, "test string") {
t.Fatalf("unexpected read result: %s", r)
}
}
func TestMachineUINamed(t *testing.T) {
iosrv := buildio()
m, err := LoadMachine("{\"name\":\"plugintest\"}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { m.UI.Info("test string") }()
r, _ := iosrv.Read("stdout")
if !strings.Contains(r, "test string") {
t.Fatalf("unexpected read result: %s", r)
}
if !strings.Contains(r, "plugintest") {
t.Fatalf("output does not contain name: %s", r)
}
}

View File

@ -0,0 +1,294 @@
package plugin
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
hclog "github.com/hashicorp/go-hclog"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
var (
Handshake = go_plugin.HandshakeConfig{
MagicCookieKey: "VAGRANT_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "1561a662a76642f98df77ad025aa13a9b16225d93f90475e91090fbe577317ed",
ProtocolVersion: 1}
ErrPluginShutdown = errors.New("plugin has shutdown")
)
type RemotePlugin interface {
Impl() interface{}
}
type RemoteConfig struct {
Client *go_plugin.Client
Config vagrant.Config
}
func (r *RemoteConfig) Impl() interface{} {
return r.Config
}
type RemoteProvider struct {
Client *go_plugin.Client
Provider Provider
}
func (r *RemoteProvider) Impl() interface{} {
return r.Provider
}
type RemoteGuestCapabilities struct {
Client *go_plugin.Client
GuestCapabilities vagrant.GuestCapabilities
}
func (r *RemoteGuestCapabilities) Impl() interface{} {
return r.GuestCapabilities
}
type RemoteHostCapabilities struct {
Client *go_plugin.Client
HostCapabilities vagrant.HostCapabilities
}
func (r *RemoteHostCapabilities) Impl() interface{} {
return r.HostCapabilities
}
type RemoteProviderCapabilities struct {
Client *go_plugin.Client
ProviderCapabilities vagrant.ProviderCapabilities
}
func (r *RemoteProviderCapabilities) Impl() interface{} {
return r.ProviderCapabilities
}
type RemoteSyncedFolder struct {
Client *go_plugin.Client
SyncedFolder vagrant.SyncedFolder
}
func (r *RemoteSyncedFolder) Impl() interface{} {
return r.SyncedFolder
}
type VagrantPlugin struct {
Providers map[string]*RemoteProvider
SyncedFolders map[string]*RemoteSyncedFolder
PluginDirectories []string
PluginLookup func(name, kind string) (p interface{}, err error)
Logger hclog.Logger
}
func VagrantPluginInit() *VagrantPlugin {
v := &VagrantPlugin{
PluginDirectories: []string{},
Providers: map[string]*RemoteProvider{},
SyncedFolders: map[string]*RemoteSyncedFolder{},
Logger: vagrant.DefaultLogger().Named("go-plugin")}
v.PluginLookup = v.DefaultPluginLookup
return v
}
func (v *VagrantPlugin) DefaultPluginLookup(name, kind string) (p interface{}, err error) {
switch kind {
case "provider":
p = v.Providers[name].Impl()
case "synced_folder":
p = v.SyncedFolders[name].Impl()
default:
err = errors.New("invalid plugin type")
return
}
if p == nil {
err = errors.New(fmt.Sprintf("Failed to locate %s plugin of type %s", name, kind))
}
return
}
func (v *VagrantPlugin) LoadPlugins(pluginPath string) error {
for _, p := range v.PluginDirectories {
if p == pluginPath {
v.Logger.Error("plugin directory path already loaded", "path", pluginPath)
return errors.New("plugin directory already loaded")
}
}
v.PluginDirectories = append(v.PluginDirectories, pluginPath)
if err := v.LoadProviders(pluginPath); err != nil {
return err
}
if err := v.LoadSyncedFolders(pluginPath); err != nil {
return err
}
return nil
}
func (v *VagrantPlugin) LoadProviders(pluginPath string) error {
providerPaths, err := go_plugin.Discover("*_provider", pluginPath)
if err != nil {
v.Logger.Error("error during plugin discovery", "type", "provider",
"error", err, "path", pluginPath)
return err
}
for _, providerPath := range providerPaths {
v.Logger.Info("loading provider plugin", "path", providerPath)
client := go_plugin.NewClient(&go_plugin.ClientConfig{
AllowedProtocols: []go_plugin.Protocol{go_plugin.ProtocolGRPC},
Logger: v.Logger,
HandshakeConfig: Handshake,
Cmd: exec.Command(providerPath),
VersionedPlugins: map[int]go_plugin.PluginSet{
2: {"provider": &ProviderPlugin{}}}})
gclient, err := client.Client()
if err != nil {
v.Logger.Error("error loading provider client", "error", err, "path", providerPath)
return err
}
raw, err := gclient.Dispense("provider")
if err != nil {
v.Logger.Error("error loading provider plugin", "error", err, "path", providerPath)
return err
}
prov := raw.(Provider)
n := prov.Name()
v.Providers[n] = &RemoteProvider{
Client: client,
Provider: prov}
v.Logger.Info("plugin loaded", "type", "provider", "name", n, "path", providerPath)
go v.StreamIO("stdout", prov, n, "provider")
go v.StreamIO("stderr", prov, n, "provider")
}
return nil
}
func (v *VagrantPlugin) LoadSyncedFolders(pluginPath string) error {
folderPaths, err := go_plugin.Discover("*_synced_folder", pluginPath)
if err != nil {
v.Logger.Error("error during plugin discovery", "type", "synced_folder",
"error", err, "path", pluginPath)
return err
}
for _, folderPath := range folderPaths {
v.Logger.Info("loading synced_folder plugin", "path", folderPath)
client := go_plugin.NewClient(&go_plugin.ClientConfig{
AllowedProtocols: []go_plugin.Protocol{go_plugin.ProtocolGRPC},
Logger: v.Logger,
HandshakeConfig: Handshake,
Cmd: exec.Command(folderPath),
VersionedPlugins: map[int]go_plugin.PluginSet{
2: {"synced_folders": &SyncedFolderPlugin{}}}})
gclient, err := client.Client()
if err != nil {
v.Logger.Error("error loading synced_folder client", "error", err, "path", folderPath)
return err
}
raw, err := gclient.Dispense("synced_folder")
if err != nil {
v.Logger.Error("error loading synced_folder plugin", "error", err, "path", folderPath)
return err
}
fold := raw.(SyncedFolder)
n := fold.Name()
v.SyncedFolders[n] = &RemoteSyncedFolder{
Client: client,
SyncedFolder: fold}
v.Logger.Info("plugin loaded", "type", "synced_folder", "name", n, "path", folderPath)
go v.StreamIO("stdout", fold, n, "synced_folder")
go v.StreamIO("stderr", fold, n, "synced_folder")
}
return nil
}
func (v *VagrantPlugin) StreamIO(target string, i vagrant.IOServer, name, kind string) {
v.Logger.Info("starting plugin IO streaming", "target", target, "plugin", name, "type", kind)
for {
str, err := i.Read(target)
if err != nil {
v.Logger.Error("plugin IO streaming failure", "target", target, "plugin", name,
"type", kind, "error", err)
break
}
v.Logger.Debug("received plugin IO content", "target", target, "plugin", name,
"type", kind, "content", str)
if target == "stdout" {
os.Stdout.Write([]byte(str))
} else if target == "stderr" {
os.Stderr.Write([]byte(str))
}
}
v.Logger.Info("completed plugin IO streaming", "target", target, "plugin", name, "type", kind)
}
func (v *VagrantPlugin) Kill() {
v.Logger.Debug("killing all running plugins")
for n, p := range v.Providers {
v.Logger.Debug("killing plugin", "name", n, "type", "provider")
p.Client.Kill()
v.Logger.Info("plugin killed", "name", n, "type", "provider")
}
for n, p := range v.SyncedFolders {
v.Logger.Debug("killing plugin", "name", n, "type", "synced_folder")
p.Client.Kill()
v.Logger.Info("plugin killed", "name", n, "type", "synced_folder")
}
}
// Helper used for inspect GRPC related errors and providing "correct"
// error message
func handleGrpcError(err error, pluginCtx context.Context, reqCtx context.Context) error {
// If there was no error then nothing to process
if err == nil {
return nil
}
// If a request context is provided, check that it
// was not canceled or timed out. If no context
// provided, stub one for later.
if reqCtx != nil {
s := status.FromContextError(reqCtx.Err())
switch s.Code() {
case codes.Canceled:
return context.Canceled
case codes.DeadlineExceeded:
return context.DeadlineExceeded
}
} else {
reqCtx = context.Background()
}
s, ok := status.FromError(err)
if ok && (s.Code() == codes.Unavailable || s.Code() == codes.Canceled) {
select {
case <-pluginCtx.Done():
err = ErrPluginShutdown
case <-reqCtx.Done():
err = reqCtx.Err()
select {
case <-pluginCtx.Done():
err = ErrPluginShutdown
default:
}
case <-time.After(5):
return errors.New("exceeded context check timeout - " + err.Error())
}
return err
} else if s != nil && s.Message() != "" {
// Extract actual error message received
// and create new error
return errors.New(s.Message())
}
return err
}

View File

@ -0,0 +1,365 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type GuestCapabilities interface {
vagrant.GuestCapabilities
Meta
}
type GuestCapabilitiesPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl GuestCapabilities
}
func (g *GuestCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
g.Impl.Init()
vagrant_proto.RegisterGuestCapabilitiesServer(s, &GRPCGuestCapabilitiesServer{
Impl: g.Impl,
GRPCIOServer: GRPCIOServer{
Impl: g.Impl}})
return nil
}
func (g *GuestCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewGuestCapabilitiesClient(c)
return &GRPCGuestCapabilitiesClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCGuestCapabilitiesServer struct {
GRPCIOServer
Impl GuestCapabilities
}
func (s *GRPCGuestCapabilitiesServer) GuestCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.SystemCapabilityList, err error) {
resp = &vagrant_proto.SystemCapabilityList{}
r, err := s.Impl.GuestCapabilities()
if err != nil {
return
}
for _, cap := range r {
rcap := &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform}
resp.Capabilities = append(resp.Capabilities, rcap)
}
return
}
func (s *GRPCGuestCapabilitiesServer) GuestCapability(ctx context.Context, req *vagrant_proto.GuestCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
return
}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
cap := &vagrant.SystemCapability{
Name: req.Capability.Name,
Platform: req.Capability.Platform}
r, err := s.Impl.GuestCapability(ctx, cap, args, machine)
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
type GRPCGuestCapabilitiesClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.GuestCapabilitiesClient
doneCtx context.Context
}
func (c *GRPCGuestCapabilitiesClient) GuestCapabilities() (caps []vagrant.SystemCapability, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.GuestCapabilities(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
caps = make([]vagrant.SystemCapability, len(resp.Capabilities))
for i := 0; i < len(resp.Capabilities); i++ {
cap := vagrant.SystemCapability{
Name: resp.Capabilities[i].Name,
Platform: resp.Capabilities[i].Platform}
caps[i] = cap
}
return
}
func (c *GRPCGuestCapabilitiesClient) GuestCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, machine *vagrant.Machine) (result interface{}, err error) {
a, err := json.Marshal(args)
if err != nil {
return
}
m, err := vagrant.DumpMachine(machine)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.GuestCapability(jctx, &vagrant_proto.GuestCapabilityRequest{
Capability: &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform},
Machine: m,
Arguments: string(a)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &result)
return
}
type HostCapabilities interface {
vagrant.HostCapabilities
Meta
}
type HostCapabilitiesPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl HostCapabilities
}
func (h *HostCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
h.Impl.Init()
vagrant_proto.RegisterHostCapabilitiesServer(s, &GRPCHostCapabilitiesServer{
Impl: h.Impl,
GRPCIOServer: GRPCIOServer{
Impl: h.Impl}})
return nil
}
func (h *HostCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewHostCapabilitiesClient(c)
return &GRPCHostCapabilitiesClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCHostCapabilitiesServer struct {
GRPCIOServer
Impl HostCapabilities
}
func (s *GRPCHostCapabilitiesServer) HostCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.SystemCapabilityList, err error) {
resp = &vagrant_proto.SystemCapabilityList{}
r, err := s.Impl.HostCapabilities()
if err != nil {
return
}
for _, cap := range r {
rcap := &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform}
resp.Capabilities = append(resp.Capabilities, rcap)
}
return
}
func (s *GRPCHostCapabilitiesServer) HostCapability(ctx context.Context, req *vagrant_proto.HostCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
return
}
env, err := vagrant.LoadEnvironment(req.Environment, s.Impl)
if err != nil {
return
}
cap := &vagrant.SystemCapability{
Name: req.Capability.Name,
Platform: req.Capability.Platform}
r, err := s.Impl.HostCapability(ctx, cap, args, env)
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
type GRPCHostCapabilitiesClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.HostCapabilitiesClient
doneCtx context.Context
}
func (c *GRPCHostCapabilitiesClient) HostCapabilities() (caps []vagrant.SystemCapability, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.HostCapabilities(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
caps = make([]vagrant.SystemCapability, len(resp.Capabilities))
for i := 0; i < len(resp.Capabilities); i++ {
cap := vagrant.SystemCapability{
Name: resp.Capabilities[i].Name,
Platform: resp.Capabilities[i].Platform}
caps[i] = cap
}
return
}
func (c *GRPCHostCapabilitiesClient) HostCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, env *vagrant.Environment) (result interface{}, err error) {
a, err := json.Marshal(args)
if err != nil {
return
}
e, err := vagrant.DumpEnvironment(env)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.HostCapability(jctx, &vagrant_proto.HostCapabilityRequest{
Capability: &vagrant_proto.SystemCapability{
Name: cap.Name,
Platform: cap.Platform},
Environment: e,
Arguments: string(a)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &result)
return
}
type ProviderCapabilities interface {
vagrant.ProviderCapabilities
Meta
}
type ProviderCapabilitiesPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl ProviderCapabilities
}
func (p *ProviderCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
p.Impl.Init()
vagrant_proto.RegisterProviderCapabilitiesServer(s, &GRPCProviderCapabilitiesServer{
Impl: p.Impl,
GRPCIOServer: GRPCIOServer{
Impl: p.Impl}})
return nil
}
func (p *ProviderCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewProviderCapabilitiesClient(c)
return &GRPCProviderCapabilitiesClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCProviderCapabilitiesServer struct {
GRPCIOServer
Impl ProviderCapabilities
}
func (s *GRPCProviderCapabilitiesServer) ProviderCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.ProviderCapabilityList, err error) {
resp = &vagrant_proto.ProviderCapabilityList{}
r, err := s.Impl.ProviderCapabilities()
if err != nil {
return
}
for _, cap := range r {
rcap := &vagrant_proto.ProviderCapability{Name: cap.Name, Provider: cap.Provider}
resp.Capabilities = append(resp.Capabilities, rcap)
}
return
}
func (s *GRPCProviderCapabilitiesServer) ProviderCapability(ctx context.Context, req *vagrant_proto.ProviderCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
return
}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
cap := &vagrant.ProviderCapability{
Name: req.Capability.Name,
Provider: req.Capability.Provider}
r, err := s.Impl.ProviderCapability(ctx, cap, args, m)
if err != nil {
return
}
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
type GRPCProviderCapabilitiesClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.ProviderCapabilitiesClient
doneCtx context.Context
}
func (c *GRPCProviderCapabilitiesClient) ProviderCapabilities() (caps []vagrant.ProviderCapability, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ProviderCapabilities(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
caps = make([]vagrant.ProviderCapability, len(resp.Capabilities))
for i := 0; i < len(resp.Capabilities); i++ {
cap := vagrant.ProviderCapability{
Name: resp.Capabilities[i].Name,
Provider: resp.Capabilities[i].Provider}
caps[i] = cap
}
return
}
func (c *GRPCProviderCapabilitiesClient) ProviderCapability(ctx context.Context, cap *vagrant.ProviderCapability, args interface{}, machine *vagrant.Machine) (result interface{}, err error) {
a, err := json.Marshal(args)
if err != nil {
return
}
m, err := vagrant.DumpMachine(machine)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ProviderCapability(jctx, &vagrant_proto.ProviderCapabilityRequest{
Capability: &vagrant_proto.ProviderCapability{
Name: cap.Name,
Provider: cap.Provider},
Machine: m,
Arguments: string(a)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &result)
return
}

View File

@ -0,0 +1,499 @@
package plugin
import (
"context"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestCapabilities_GuestCapabilities(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.GuestCapabilities()
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("length %d != 1", len(resp))
}
if resp[0].Name != "test_cap" {
t.Errorf("name - %s != test_cap", resp[0].Name)
}
if resp[0].Platform != "testOS" {
t.Errorf("platform - %s != testOS", resp[0].Platform)
}
}
func TestCapabilities_GuestCapability(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
args := []string{"test_value", "next_test_value"}
resp, err := impl.GuestCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
if result[1] != "test_value" {
t.Errorf("%s != test_value", result[1])
}
}
func TestCapabilities_GuestCapability_noargs(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
var args interface{}
args = nil
resp, err := impl.GuestCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
}
func TestCapabilities_GuestCapability_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.GuestCapability(ctx, cap, args, m)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_GuestCapability_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.GuestCapability(ctx, cap, args, m)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_HostCapabilities(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.HostCapabilities()
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("length %d != 1", len(resp))
}
if resp[0].Name != "test_cap" {
t.Errorf("name - %s != test_cap", resp[0].Name)
}
if resp[0].Platform != "testOS" {
t.Errorf("platform - %s != testOS", resp[0].Platform)
}
}
func TestCapabilities_HostCapability(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
args := []string{"test_value", "next_test_value"}
resp, err := impl.HostCapability(context.Background(), cap, args, e)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
if result[1] != "test_value" {
t.Errorf("%s != test_value", result[1])
}
}
func TestCapabilities_HostCapability_noargs(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
var args interface{}
args = nil
resp, err := impl.HostCapability(context.Background(), cap, args, e)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
}
func TestCapabilities_HostCapability_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.HostCapability(ctx, cap, args, e)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_HostCapability_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.HostCapability(ctx, cap, args, e)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_ProviderCapabilities(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.ProviderCapabilities()
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("length %d != 1", len(resp))
}
if resp[0].Name != "test_cap" {
t.Errorf("name - %s != test_cap", resp[0].Name)
}
if resp[0].Provider != "testProvider" {
t.Errorf("provider - %s != testProvdier", resp[0].Provider)
}
}
func TestCapabilities_ProviderCapability(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
args := []string{"test_value", "next_test_value"}
resp, err := impl.ProviderCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
if result[1] != "test_value" {
t.Errorf("%s != test_value", result[1])
}
}
func TestCapabilities_ProviderCapability_noargs(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
var args interface{}
args = nil
resp, err := impl.ProviderCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
}
func TestCapabilities_ProviderCapability_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ProviderCapability(ctx, cap, args, m)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_ProviderCapability_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.ProviderCapability(ctx, cap, args, m)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}

View File

@ -0,0 +1,175 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type Config interface {
vagrant.Config
Meta
}
type ConfigPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl Config
}
func (c *ConfigPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
c.Impl.Init()
vagrant_proto.RegisterConfigServer(s, &GRPCConfigServer{
Impl: c.Impl,
GRPCIOServer: GRPCIOServer{
Impl: c.Impl}})
return nil
}
func (c *ConfigPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, con *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewConfigClient(con)
return &GRPCConfigClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCConfigServer struct {
GRPCIOServer
Impl Config
}
func (s *GRPCConfigServer) ConfigAttributes(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.ListResponse, err error) {
resp = &vagrant_proto.ListResponse{}
resp.Items, err = s.Impl.ConfigAttributes()
return
}
func (s *GRPCConfigServer) ConfigLoad(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.Configuration, err error) {
resp = &vagrant_proto.Configuration{}
var data map[string]interface{}
err = json.Unmarshal([]byte(req.Data), &data)
if err != nil {
return
}
r, err := s.Impl.ConfigLoad(ctx, data)
if err != nil {
return
}
mdata, err := json.Marshal(r)
if err != nil {
return
}
resp.Data = string(mdata)
return
}
func (s *GRPCConfigServer) ConfigValidate(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.ListResponse, err error) {
resp = &vagrant_proto.ListResponse{}
var data map[string]interface{}
err = json.Unmarshal([]byte(req.Data), &data)
if err != nil {
return
}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
resp.Items, err = s.Impl.ConfigValidate(ctx, data, m)
return
}
func (s *GRPCConfigServer) ConfigFinalize(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.Configuration, err error) {
resp = &vagrant_proto.Configuration{}
var data map[string]interface{}
err = json.Unmarshal([]byte(req.Data), &data)
if err != nil {
return
}
r, err := s.Impl.ConfigFinalize(ctx, data)
if err != nil {
return
}
mdata, err := json.Marshal(r)
if err != nil {
return
}
resp.Data = string(mdata)
return
}
type GRPCConfigClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.ConfigClient
doneCtx context.Context
}
func (c *GRPCConfigClient) ConfigAttributes() (attrs []string, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigAttributes(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, nil)
}
attrs = resp.Items
return
}
func (c *GRPCConfigClient) ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error) {
mdata, err := json.Marshal(data)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigLoad(jctx, &vagrant_proto.Configuration{
Data: string(mdata)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Data), &loaddata)
return
}
func (c *GRPCConfigClient) ConfigValidate(ctx context.Context, data map[string]interface{}, m *vagrant.Machine) (errs []string, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
mdata, err := json.Marshal(data)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigValidate(jctx, &vagrant_proto.Configuration{
Data: string(mdata),
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
errs = resp.Items
return
}
func (c *GRPCConfigClient) ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error) {
mdata, err := json.Marshal(data)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigFinalize(jctx, &vagrant_proto.Configuration{
Data: string(mdata)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Data), &finaldata)
return
}

View File

@ -0,0 +1,246 @@
package plugin
import (
"context"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestConfigPlugin_Attributes(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.ConfigAttributes()
if err != nil {
t.Fatalf("bad resp %s", err)
}
if resp[0] != "fubar" {
t.Errorf("%s != fubar", resp[0])
}
if resp[1] != "foobar" {
t.Errorf("%s != foobar", resp[1])
}
}
func TestConfigPlugin_Load(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{}
var resp map[string]interface{}
resp, err = impl.ConfigLoad(context.Background(), data)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if _, ok := resp["test_key"]; !ok {
t.Fatalf("bad resp content %#v", resp)
}
v := resp["test_key"].(string)
if v != "test_val" {
t.Errorf("%s != test_val", v)
}
}
func TestConfigPlugin_Load_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{"pause": true}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.ConfigLoad(ctx, data)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestConfigPlugin_Load_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{"pause": true}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ConfigLoad(ctx, data)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestConfigPlugin_Validate(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{}
machine := &vagrant.Machine{}
resp, err := impl.ConfigValidate(context.Background(), data, machine)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("bad size %d != 1", len(resp))
}
if resp[0] != "test error" {
t.Errorf("%s != test error", resp[0])
}
}
func TestConfigPlugin_Validate_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{"pause": true}
machine := &vagrant.Machine{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ConfigValidate(ctx, data, machine)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestConfigPlugin_Finalize(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{
"test_key": "test_val",
"other_key": "other_val"}
resp, err := impl.ConfigFinalize(context.Background(), data)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if _, ok := resp["test_key"]; !ok {
t.Fatalf("bad resp content %#v", resp)
}
v := resp["test_key"].(string)
if v != "test_val-updated" {
t.Errorf("%s != test_val-updated", v)
}
v = resp["other_key"].(string)
if v != "other_val-updated" {
t.Errorf("%s != other_val-updated", v)
}
}
func TestConfigPlugin_Finalize_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{
"pause": true,
"test_key": "test_val",
"other_key": "other_val"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ConfigFinalize(ctx, data)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}

View File

@ -0,0 +1,84 @@
package plugin
import (
"context"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type IO interface {
vagrant.StreamIO
}
type IOPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl vagrant.StreamIO
}
type GRPCIOServer struct {
Impl vagrant.StreamIO
}
func (s *GRPCIOServer) Read(ctx context.Context, req *vagrant_proto.Identifier) (r *vagrant_proto.Content, err error) {
r = &vagrant_proto.Content{}
r.Value, err = s.Impl.Read(req.Name)
return
}
func (s *GRPCIOServer) Write(ctx context.Context, req *vagrant_proto.Content) (r *vagrant_proto.WriteResponse, err error) {
r = &vagrant_proto.WriteResponse{}
bytes := 0
bytes, err = s.Impl.Write(req.Value, req.Target)
if err != nil {
return
}
r.Length = int32(bytes)
return
}
type GRPCIOClient struct {
client vagrant_proto.IOClient
doneCtx context.Context
}
func (c *GRPCIOClient) Read(target string) (content string, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Read(jctx, &vagrant_proto.Identifier{
Name: target})
if err != nil {
return content, handleGrpcError(err, c.doneCtx, ctx)
}
content = resp.Value
return
}
func (c *GRPCIOClient) Write(content, target string) (length int, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Write(jctx, &vagrant_proto.Content{
Value: content,
Target: target})
if err != nil {
return length, handleGrpcError(err, c.doneCtx, ctx)
}
length = int(resp.Length)
return
}
func (i *IOPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
vagrant_proto.RegisterIOServer(s, &GRPCIOServer{Impl: i.Impl})
return nil
}
func (i *IOPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCIOClient{
client: vagrant_proto.NewIOClient(c),
doneCtx: ctx}, nil
}

View File

@ -0,0 +1,89 @@
package plugin
import (
"testing"
"github.com/hashicorp/go-plugin"
)
type MockIO struct {
Core
}
func TestIO_ReadWrite(t *testing.T) {
ioplugin := &MockIO{}
ioplugin.Init()
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"io": &IOPlugin{Impl: ioplugin}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("io")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(IO)
if !ok {
t.Fatalf("bad %#v", raw)
}
go func() {
length, err := impl.Write("test_message", "stdout")
if err != nil {
t.Fatalf("bad write: %s", err)
}
if length != len("test_message") {
t.Fatalf("bad length %d != %d", length, len("test_message"))
}
}()
resp, err := impl.Read("stdout")
if err != nil {
t.Fatalf("bad read: %s", err)
}
if resp != "test_message" {
t.Errorf("%s != test_message", resp)
}
}
func TestIO_Write_bad(t *testing.T) {
ioplugin := &MockIO{}
ioplugin.Init()
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"io": &IOPlugin{Impl: ioplugin}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("io")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(IO)
if !ok {
t.Fatalf("bad %#v", raw)
}
_, err = impl.Write("test_message", "bad-target")
if err == nil {
t.Fatalf("illegal write")
}
}
func TestIO_Read_bad(t *testing.T) {
ioplugin := &MockIO{}
ioplugin.Init()
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"io": &IOPlugin{Impl: ioplugin}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("io")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(IO)
if !ok {
t.Fatalf("bad %#v", raw)
}
_, err = impl.Read("bad-target")
if err == nil {
t.Fatalf("illegal read")
}
}

View File

@ -0,0 +1,39 @@
package plugin
import (
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
type Meta interface {
Init()
vagrant.IOServer
}
type GRPCCoreClient struct{}
func (c *GRPCCoreClient) Init() {}
func (c *GRPCCoreClient) Streams() (s map[string]chan (string)) { return }
type Core struct {
vagrant.IOServer
io vagrant.StreamIO
}
func (c *Core) Init() {
if c.io == nil {
c.io = &vagrant.IOSrv{
map[string]chan (string){
"stdout": make(chan string),
"stderr": make(chan string),
},
}
}
}
func (c *Core) Read(target string) (string, error) {
return c.io.Read(target)
}
func (c *Core) Write(content, target string) (int, error) {
return c.io.Write(content, target)
}

View File

@ -0,0 +1,300 @@
package plugin
import (
"context"
"errors"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
type MockGuestCapabilities struct{ Core }
func (g *MockGuestCapabilities) GuestCapabilities() (caps []vagrant.SystemCapability, err error) {
caps = []vagrant.SystemCapability{
vagrant.SystemCapability{Name: "test_cap", Platform: "testOS"}}
return
}
func (g *MockGuestCapabilities) GuestCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, m *vagrant.Machine) (result interface{}, err error) {
if args != nil {
arguments := args.([]interface{})
if arguments[0] == "pause" {
time.Sleep(1 * time.Second)
}
if len(arguments) > 0 {
result = []string{
cap.Name,
arguments[0].(string)}
return
}
}
result = []string{cap.Name}
return
}
type MockHostCapabilities struct{ Core }
func (h *MockHostCapabilities) HostCapabilities() (caps []vagrant.SystemCapability, err error) {
caps = []vagrant.SystemCapability{
vagrant.SystemCapability{Name: "test_cap", Platform: "testOS"}}
return
}
func (h *MockHostCapabilities) HostCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, e *vagrant.Environment) (result interface{}, err error) {
if args != nil {
arguments := args.([]interface{})
if arguments[0] == "pause" {
time.Sleep(1 * time.Second)
}
if len(arguments) > 0 {
result = []string{
cap.Name,
arguments[0].(string)}
return
}
}
result = []string{cap.Name}
return
}
type MockProviderCapabilities struct{ Core }
func (p *MockProviderCapabilities) ProviderCapabilities() (caps []vagrant.ProviderCapability, err error) {
caps = []vagrant.ProviderCapability{
vagrant.ProviderCapability{Name: "test_cap", Provider: "testProvider"}}
return
}
func (p *MockProviderCapabilities) ProviderCapability(ctx context.Context, cap *vagrant.ProviderCapability, args interface{}, m *vagrant.Machine) (result interface{}, err error) {
if args != nil {
arguments := args.([]interface{})
if arguments[0] == "pause" {
time.Sleep(1 * time.Second)
}
if len(arguments) > 0 {
result = []string{
cap.Name,
arguments[0].(string)}
return
}
}
result = []string{cap.Name}
return
}
type MockConfig struct {
Core
}
func (c *MockConfig) ConfigAttributes() (attrs []string, err error) {
attrs = []string{"fubar", "foobar"}
return
}
func (c *MockConfig) ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error) {
if data["pause"] == true {
time.Sleep(1 * time.Second)
}
loaddata = map[string]interface{}{
"test_key": "test_val"}
if data["test_key"] != nil {
loaddata["sent_key"] = data["test_key"]
}
return
}
func (c *MockConfig) ConfigValidate(ctx context.Context, data map[string]interface{}, m *vagrant.Machine) (errors []string, err error) {
errors = []string{"test error"}
if data["pause"] == true {
time.Sleep(1 * time.Second)
}
return
}
func (c *MockConfig) ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error) {
finaldata = make(map[string]interface{})
for key, tval := range data {
val, ok := tval.(string)
if !ok {
continue
}
finaldata[key] = val + "-updated"
}
if data["pause"] == true {
time.Sleep(1 * time.Second)
}
return
}
type MockProvider struct {
Core
vagrant.NoConfig
vagrant.NoGuestCapabilities
vagrant.NoHostCapabilities
vagrant.NoProviderCapabilities
}
func (c *MockProvider) Action(ctx context.Context, actionName string, m *vagrant.Machine) (actions []string, err error) {
switch actionName {
case "valid":
actions = []string{"self::DoTask"}
case "pause":
time.Sleep(1 * time.Second)
default:
err = errors.New("Unknown action requested")
}
return
}
func (c *MockProvider) IsInstalled(ctx context.Context, m *vagrant.Machine) (bool, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return true, nil
}
func (c *MockProvider) IsUsable(ctx context.Context, m *vagrant.Machine) (bool, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return true, nil
}
func (c *MockProvider) MachineIdChanged(ctx context.Context, m *vagrant.Machine) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return nil
}
func (c *MockProvider) Name() string {
return "mock_provider"
}
func (c *MockProvider) RunAction(ctx context.Context, actionName string, args interface{}, m *vagrant.Machine) (r interface{}, err error) {
switch actionName {
case "send_output":
m.UI.Say("test_output_p")
case "pause":
time.Sleep(1 * time.Second)
case "valid":
default:
return nil, errors.New("invalid action name")
}
var arguments []interface{}
if args != nil {
arguments = args.([]interface{})
} else {
arguments = []interface{}{"unset"}
}
r = []string{
actionName,
arguments[0].(string)}
return
}
func (c *MockProvider) SshInfo(ctx context.Context, m *vagrant.Machine) (*vagrant.SshInfo, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return &vagrant.SshInfo{
Host: "localhost",
Port: 2222}, nil
}
func (c *MockProvider) State(ctx context.Context, m *vagrant.Machine) (*vagrant.MachineState, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return &vagrant.MachineState{
Id: "default",
ShortDesc: "running"}, nil
}
func (c *MockProvider) Info() *vagrant.ProviderInfo {
return &vagrant.ProviderInfo{
Description: "Custom",
Priority: 10}
}
type MockSyncedFolder struct {
Core
vagrant.NoGuestCapabilities
vagrant.NoHostCapabilities
}
func (s *MockSyncedFolder) Cleanup(ctx context.Context, m *vagrant.Machine, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil {
err, _ := opts["error"].(bool)
ui, _ := opts["ui"].(bool)
if err {
return errors.New("cleanup error")
}
if ui {
m.UI.Say("test_output_sf")
return nil
}
}
return nil
}
func (s *MockSyncedFolder) Disable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil && opts["error"].(bool) {
return errors.New("disable error")
}
return nil
}
func (s *MockSyncedFolder) Enable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil && opts["error"].(bool) {
return errors.New("enable error")
}
return nil
}
func (s *MockSyncedFolder) Info() *vagrant.SyncedFolderInfo {
return &vagrant.SyncedFolderInfo{
Description: "mock_folder",
Priority: 100}
}
func (s *MockSyncedFolder) IsUsable(ctx context.Context, m *vagrant.Machine) (bool, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return true, nil
}
func (s *MockSyncedFolder) Name() string {
return "mock_folder"
}
func (s *MockSyncedFolder) Prepare(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil && opts["error"].(bool) {
return errors.New("prepare error")
}
return nil
}

View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
echo -n "Parsing proto files and generating go output... "
for i in *
do
if [ -d "${i}" ]; then
protoc --proto_path=`go env GOPATH`/src --proto_path=. --go_out=plugins=grpc:. "${i}"/*.proto;
if [ $? -ne 0 ]; then
echo "failed!"
exit 1
fi
fi
done
protoc --proto_path=`go env GOPATH`/src --proto_path=. --go_out=plugins=grpc:. *.proto;
if [ $? -ne 0 ]; then
echo "failed!"
exit 1
fi
echo "done!"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,196 @@
syntax = "proto3";
package vagrant.proto;
message Empty{}
message Machine {
string machine = 1;
}
message Valid {
bool result = 1;
}
message Identifier {
string name = 1;
}
message PluginInfo {
string description = 1;
int64 priority = 2;
}
message Content {
string target = 1;
string value = 2;
}
message WriteResponse {
int32 length = 1;
}
service IO {
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
message SystemCapability {
string name = 1;
string platform = 2;
}
message ProviderCapability {
string name = 1;
string provider = 2;
}
message SystemCapabilityList {
repeated SystemCapability capabilities = 1;
}
message ProviderCapabilityList {
repeated ProviderCapability capabilities = 1;
}
message GenericResponse {
string result = 1;
}
message GuestCapabilityRequest {
SystemCapability capability = 1;
string machine = 2;
string arguments = 3;
}
message HostCapabilityRequest {
SystemCapability capability = 1;
string environment = 2;
string arguments = 3;
}
message ProviderCapabilityRequest {
ProviderCapability capability = 1;
string machine = 2;
string arguments = 3;
}
service GuestCapabilities {
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
service HostCapabilities {
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
service ProviderCapabilities {
rpc ProviderCapabilities (Empty) returns (ProviderCapabilityList);
rpc ProviderCapability (ProviderCapabilityRequest) returns (GenericResponse);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
message Configuration {
string data = 1;
string machine = 2;
}
message ListResponse {
repeated string items = 1;
}
service Config {
rpc ConfigAttributes(Empty) returns (ListResponse);
rpc ConfigLoad(Configuration) returns (Configuration);
rpc ConfigValidate(Configuration) returns (ListResponse);
rpc ConfigFinalize(Configuration) returns (Configuration);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
message SyncedFolders {
string machine = 1;
string folders = 2;
string options = 3;
}
service SyncedFolder {
rpc Cleanup(SyncedFolders) returns (Empty);
rpc Disable(SyncedFolders) returns (Empty);
rpc Enable(SyncedFolders) returns (Empty);
rpc Info(Empty) returns (PluginInfo);
rpc IsUsable(Machine) returns (Valid);
rpc Name(Empty) returns (Identifier);
rpc Prepare(SyncedFolders) returns (Empty);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
// Guest capabilities helpers (copied from GuestCapabilities service)
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
// Host capabilities helpers (copied from GuestCapabilities service)
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
}
message GenericAction {
string name = 1;
string machine = 2;
}
message ExecuteAction {
string name = 1;
string data = 2;
string machine = 3;
}
message MachineSshInfo {
string host = 1;
int64 port = 2;
string private_key_path = 3;
string username = 4;
}
message MachineState {
string id = 1;
string short_description = 2;
string long_description = 3;
}
service Provider {
rpc Action(GenericAction) returns (ListResponse);
rpc Info(Empty) returns (PluginInfo);
rpc IsInstalled(Machine) returns (Valid);
rpc IsUsable(Machine) returns (Valid);
rpc MachineIdChanged(Machine) returns (Machine);
rpc Name(Empty) returns (Identifier);
rpc RunAction(ExecuteAction) returns (GenericResponse);
rpc SshInfo(Machine) returns (MachineSshInfo);
rpc State(Machine) returns (MachineState);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
// Config helpers (copied from Config service)
rpc ConfigAttributes(Empty) returns (ListResponse);
rpc ConfigLoad(Configuration) returns (Configuration);
rpc ConfigValidate(Configuration) returns (ListResponse);
rpc ConfigFinalize(Configuration) returns (Configuration);
// Guest capabilities helpers (copied from GuestCapabilities service)
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
// Host capabilities helpers (copied from HostCapabilities service)
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
// Provider capabilities helpers (copied from ProviderCapabilities service)
rpc ProviderCapabilities (Empty) returns (ProviderCapabilityList);
rpc ProviderCapability (ProviderCapabilityRequest) returns (GenericResponse);
}

View File

@ -0,0 +1,346 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type Provider interface {
vagrant.Provider
Meta
}
type ProviderPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl Provider
}
type GRPCProviderClient struct {
GRPCCoreClient
GRPCConfigClient
GRPCGuestCapabilitiesClient
GRPCHostCapabilitiesClient
GRPCProviderCapabilitiesClient
GRPCIOClient
client vagrant_proto.ProviderClient
doneCtx context.Context
}
func (c *GRPCProviderClient) Action(ctx context.Context, actionName string, m *vagrant.Machine) (r []string, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Action(jctx, &vagrant_proto.GenericAction{
Name: actionName,
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
r = resp.Items
return
}
func (c *GRPCProviderClient) Info() *vagrant.ProviderInfo {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Info(jctx, &vagrant_proto.Empty{})
if err != nil {
return &vagrant.ProviderInfo{}
}
return &vagrant.ProviderInfo{
Description: resp.Description,
Priority: resp.Priority}
}
func (c *GRPCProviderClient) IsInstalled(ctx context.Context, m *vagrant.Machine) (r bool, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.IsInstalled(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return false, handleGrpcError(err, c.doneCtx, ctx)
}
r = resp.Result
return
}
func (c *GRPCProviderClient) IsUsable(ctx context.Context, m *vagrant.Machine) (r bool, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.IsUsable(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return false, handleGrpcError(err, c.doneCtx, ctx)
}
r = resp.Result
return
}
func (c *GRPCProviderClient) MachineIdChanged(ctx context.Context, m *vagrant.Machine) (err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.MachineIdChanged(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return handleGrpcError(err, c.doneCtx, ctx)
}
return
}
func (c *GRPCProviderClient) RunAction(ctx context.Context, actName string, args interface{}, m *vagrant.Machine) (r interface{}, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
runData, err := json.Marshal(args)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.RunAction(jctx, &vagrant_proto.ExecuteAction{
Name: actName,
Data: string(runData),
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &r)
if err != nil {
return
}
return
}
func (c *GRPCProviderClient) SshInfo(ctx context.Context, m *vagrant.Machine) (r *vagrant.SshInfo, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.SshInfo(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
r = &vagrant.SshInfo{
Host: resp.Host,
Port: resp.Port,
PrivateKeyPath: resp.PrivateKeyPath,
Username: resp.Username}
return
}
func (c *GRPCProviderClient) State(ctx context.Context, m *vagrant.Machine) (r *vagrant.MachineState, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.State(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
r = &vagrant.MachineState{
Id: resp.Id,
ShortDesc: resp.ShortDescription,
LongDesc: resp.LongDescription}
return
}
func (c *GRPCProviderClient) Name() string {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Name(jctx, &vagrant_proto.Empty{})
if err != nil {
return ""
}
return resp.Name
}
func (p *ProviderPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewProviderClient(c)
return &GRPCProviderClient{
GRPCConfigClient: GRPCConfigClient{
client: client,
doneCtx: ctx},
GRPCGuestCapabilitiesClient: GRPCGuestCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCHostCapabilitiesClient: GRPCHostCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCProviderCapabilitiesClient: GRPCProviderCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx},
client: client,
doneCtx: ctx,
}, nil
}
func (p *ProviderPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
p.Impl.Init()
vagrant_proto.RegisterProviderServer(s, &GRPCProviderServer{
Impl: p.Impl,
GRPCConfigServer: GRPCConfigServer{
Impl: p.Impl},
GRPCGuestCapabilitiesServer: GRPCGuestCapabilitiesServer{
Impl: p.Impl},
GRPCHostCapabilitiesServer: GRPCHostCapabilitiesServer{
Impl: p.Impl},
GRPCProviderCapabilitiesServer: GRPCProviderCapabilitiesServer{
Impl: p.Impl},
GRPCIOServer: GRPCIOServer{
Impl: p.Impl}})
return nil
}
type GRPCProviderServer struct {
GRPCIOServer
GRPCConfigServer
GRPCGuestCapabilitiesServer
GRPCHostCapabilitiesServer
GRPCProviderCapabilitiesServer
Impl Provider
}
func (s *GRPCProviderServer) Action(ctx context.Context, req *vagrant_proto.GenericAction) (resp *vagrant_proto.ListResponse, err error) {
resp = &vagrant_proto.ListResponse{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.Action(ctx, req.Name, m)
if err != nil {
return
}
resp.Items = r
return
}
func (s *GRPCProviderServer) RunAction(ctx context.Context, req *vagrant_proto.ExecuteAction) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
if err = json.Unmarshal([]byte(req.Data), &args); err != nil {
return
}
r, err := s.Impl.RunAction(ctx, req.Name, args, m)
if err != nil {
return
}
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
func (s *GRPCProviderServer) Info(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.PluginInfo, err error) {
resp = &vagrant_proto.PluginInfo{}
r := s.Impl.Info()
resp.Description = r.Description
resp.Priority = r.Priority
return
}
func (s *GRPCProviderServer) IsInstalled(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
resp = &vagrant_proto.Valid{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
resp.Result, err = s.Impl.IsInstalled(ctx, m)
return
}
func (s *GRPCProviderServer) IsUsable(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
resp = &vagrant_proto.Valid{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
resp.Result, err = s.Impl.IsUsable(ctx, m)
return
}
func (s *GRPCProviderServer) SshInfo(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.MachineSshInfo, err error) {
resp = &vagrant_proto.MachineSshInfo{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.SshInfo(ctx, m)
if err != nil {
return
}
resp.Host = r.Host
resp.Port = r.Port
resp.Username = r.Username
resp.PrivateKeyPath = r.PrivateKeyPath
return
}
func (s *GRPCProviderServer) State(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.MachineState, err error) {
resp = &vagrant_proto.MachineState{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.State(ctx, m)
if err != nil {
return
}
resp.Id = r.Id
resp.ShortDescription = r.ShortDesc
resp.LongDescription = r.LongDesc
return
}
func (s *GRPCProviderServer) MachineIdChanged(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Machine, err error) {
resp = &vagrant_proto.Machine{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
if err = s.Impl.MachineIdChanged(ctx, m); err != nil {
return
}
mdata, err := vagrant.DumpMachine(m)
if err != nil {
return
}
resp = &vagrant_proto.Machine{Machine: mdata}
return
}
func (s *GRPCProviderServer) Name(ctx context.Context, req *vagrant_proto.Empty) (*vagrant_proto.Identifier, error) {
return &vagrant_proto.Identifier{Name: s.Impl.Name()}, nil
}

View File

@ -0,0 +1,667 @@
package plugin
import (
"context"
"strings"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestProvider_Action(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.Action(context.Background(), "valid", &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if resp[0] != "self::DoTask" {
t.Errorf("%s != self::DoTask", resp[0])
}
}
func TestProvider_Action_invalid(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
_, err = impl.Action(context.Background(), "invalid", &vagrant.Machine{})
if err == nil {
t.Errorf("illegal action")
}
}
func TestProvider_Action_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.Action(ctx, "pause", &vagrant.Machine{})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_Action_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.Action(ctx, "pause", &vagrant.Machine{})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsInstalled(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
installed, err := impl.IsInstalled(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !installed {
t.Errorf("bad result")
}
}
func TestProvider_IsInstalled_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.IsInstalled(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsInstalled_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.IsInstalled(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsUsable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
usable, err := impl.IsUsable(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !usable {
t.Errorf("bad result")
}
}
func TestProvider_IsUsable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsUsable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_MachineIdChanged(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.MachineIdChanged(context.Background(), &vagrant.Machine{})
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestProvider_MachineIdChanged_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.MachineIdChanged(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_MachineIdChanged_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.MachineIdChanged(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_Name(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Name()
if resp != "mock_provider" {
t.Errorf("%s != mock_provider", resp)
}
}
func TestProvider_RunAction(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
resp, err := impl.RunAction(context.Background(), "valid", args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result := resp.([]interface{})
if result[0] != "valid" {
t.Errorf("%s != valid", result[0])
}
if result[1] != "test_arg" {
t.Errorf("%s != test_arg", result[1])
}
}
func TestProvider_RunAction_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.RunAction(ctx, "pause", args, m)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_RunAction_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.RunAction(ctx, "pause", args, m)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_RunAction_invalid(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
_, err = impl.RunAction(context.Background(), "invalid", args, m)
if err == nil {
t.Fatalf("illegal action run")
}
}
func TestProvider_SshInfo(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.SshInfo(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("invalid resp: %s", err)
}
if resp.Host != "localhost" {
t.Errorf("%s != localhost", resp.Host)
}
if resp.Port != 2222 {
t.Errorf("%d != 2222", resp.Port)
}
}
func TestProvider_SshInfo_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.SshInfo(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_SshInfo_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.SshInfo(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_State(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.State(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("invalid resp: %s", err)
}
if resp.Id != "default" {
t.Errorf("%s != default", resp.Id)
}
if resp.ShortDesc != "running" {
t.Errorf("%s != running", resp.ShortDesc)
}
}
func TestProvider_State_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.State(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_State_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.State(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_Info(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Info()
if resp.Description != "Custom" {
t.Errorf("%s != Custom", resp.Description)
}
if resp.Priority != 10 {
t.Errorf("%d != 10", resp.Priority)
}
}
func TestProvider_MachineUI_output(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx := context.Background()
go func() {
_, err = impl.RunAction(ctx, "send_output", nil, &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}()
resp, err := impl.Read("stdout")
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !strings.Contains(resp, "test_output_p") {
t.Errorf("%s !~ test_output_p", resp)
}
}

View File

@ -0,0 +1,283 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type SyncedFolder interface {
vagrant.SyncedFolder
Meta
}
type SyncedFolderPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl SyncedFolder
}
type GRPCSyncedFolderClient struct {
GRPCCoreClient
GRPCGuestCapabilitiesClient
GRPCHostCapabilitiesClient
GRPCIOClient
client vagrant_proto.SyncedFolderClient
doneCtx context.Context
}
func (c *GRPCSyncedFolderClient) Cleanup(ctx context.Context, m *vagrant.Machine, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Cleanup(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
func (c *GRPCSyncedFolderClient) Disable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
folders, err := json.Marshal(f)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Disable(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Folders: string(folders),
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
func (c *GRPCSyncedFolderClient) Enable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
folders, err := json.Marshal(f)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Enable(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Folders: string(folders),
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
func (c *GRPCSyncedFolderClient) Info() *vagrant.SyncedFolderInfo {
resp, err := c.client.Info(context.Background(), &vagrant_proto.Empty{})
if err != nil {
return &vagrant.SyncedFolderInfo{}
}
return &vagrant.SyncedFolderInfo{
Description: resp.Description,
Priority: resp.Priority}
}
func (c *GRPCSyncedFolderClient) IsUsable(ctx context.Context, m *vagrant.Machine) (u bool, err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.IsUsable(jctx, &vagrant_proto.Machine{
Machine: machine})
if err != nil {
return false, handleGrpcError(err, c.doneCtx, ctx)
}
u = resp.Result
return
}
func (c *GRPCSyncedFolderClient) Name() string {
resp, err := c.client.Name(context.Background(), &vagrant_proto.Empty{})
if err != nil {
return ""
}
return resp.Name
}
func (c *GRPCSyncedFolderClient) Prepare(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
folders, err := json.Marshal(f)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Prepare(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Folders: string(folders),
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
type GRPCSyncedFolderServer struct {
GRPCGuestCapabilitiesServer
GRPCHostCapabilitiesServer
GRPCIOServer
Impl SyncedFolder
}
func (s *GRPCSyncedFolderServer) Cleanup(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Cleanup(ctx, machine, options)
return
}
func (s *GRPCSyncedFolderServer) Disable(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var folders vagrant.FolderList
err = json.Unmarshal([]byte(req.Folders), &folders)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Disable(ctx, machine, folders, options)
return
}
func (s *GRPCSyncedFolderServer) Enable(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var folders vagrant.FolderList
err = json.Unmarshal([]byte(req.Folders), &folders)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Enable(ctx, machine, folders, options)
return
}
func (s *GRPCSyncedFolderServer) Info(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.PluginInfo, err error) {
resp = &vagrant_proto.PluginInfo{}
r := s.Impl.Info()
resp.Description = r.Description
resp.Priority = r.Priority
return
}
func (s *GRPCSyncedFolderServer) IsUsable(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
resp = &vagrant_proto.Valid{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.IsUsable(ctx, machine)
if err != nil {
return
}
resp.Result = r
return
}
func (s *GRPCSyncedFolderServer) Name(_ context.Context, req *vagrant_proto.Empty) (*vagrant_proto.Identifier, error) {
return &vagrant_proto.Identifier{Name: s.Impl.Name()}, nil
}
func (s *GRPCSyncedFolderServer) Prepare(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var folders vagrant.FolderList
err = json.Unmarshal([]byte(req.Folders), &folders)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Prepare(ctx, machine, folders, options)
return
}
func (f *SyncedFolderPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
f.Impl.Init()
vagrant_proto.RegisterSyncedFolderServer(s,
&GRPCSyncedFolderServer{
Impl: f.Impl,
GRPCIOServer: GRPCIOServer{
Impl: f.Impl},
GRPCGuestCapabilitiesServer: GRPCGuestCapabilitiesServer{
Impl: f.Impl},
GRPCHostCapabilitiesServer: GRPCHostCapabilitiesServer{
Impl: f.Impl}})
return nil
}
func (f *SyncedFolderPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewSyncedFolderClient(c)
return &GRPCSyncedFolderClient{
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx},
GRPCGuestCapabilitiesClient: GRPCGuestCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCHostCapabilitiesClient: GRPCHostCapabilitiesClient{
client: client,
doneCtx: ctx},
client: client,
doneCtx: ctx}, nil
}

View File

@ -0,0 +1,562 @@
package plugin
import (
"context"
"strings"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestSyncedFolder_Cleanup(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Cleanup(context.Background(), &vagrant.Machine{}, nil)
if err != nil {
t.Fatalf("bad resp: %#v", err)
}
}
func TestSyncedFolder_Cleanup_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := map[string]interface{}{
"error": true}
err = impl.Cleanup(context.Background(), &vagrant.Machine{}, args)
if err == nil {
t.Fatalf("illegal cleanup")
}
if err.Error() != "cleanup error" {
t.Errorf("%s != cleanup error", err.Error())
}
}
func TestSyncedFolder_Cleanup_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Cleanup(ctx, &vagrant.Machine{Name: "pause"}, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Cleanup_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Cleanup(ctx, &vagrant.Machine{Name: "pause"}, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Disable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Disable(context.Background(), &vagrant.Machine{}, nil, nil)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Disable_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
folders := map[string]interface{}{
"folder_name": "options"}
args := map[string]interface{}{
"error": true}
err = impl.Disable(context.Background(), &vagrant.Machine{}, folders, args)
if err == nil {
t.Fatalf("illegal disable")
}
if err.Error() != "disable error" {
t.Errorf("%s != disable error", err.Error())
}
}
func TestSyncedFolder_Disable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Disable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Disable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Disable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Enable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Enable(context.Background(), &vagrant.Machine{}, nil, nil)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Enable_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
folders := map[string]interface{}{
"folder_name": "options"}
args := map[string]interface{}{
"error": true}
err = impl.Enable(context.Background(), &vagrant.Machine{}, folders, args)
if err == nil {
t.Fatalf("illegal enable")
}
if err.Error() != "enable error" {
t.Errorf("%s != enable error", err.Error())
}
}
func TestSyncedFolder_Enable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Enable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Enable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Enable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Prepare(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Prepare(context.Background(), &vagrant.Machine{}, nil, nil)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Prepare_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
folders := map[string]interface{}{
"folder_name": "options"}
args := map[string]interface{}{
"error": true}
err = impl.Prepare(context.Background(), &vagrant.Machine{}, folders, args)
if err == nil {
t.Fatalf("illegal prepare")
}
if err.Error() != "prepare error" {
t.Errorf("%s != prepare error", err.Error())
}
}
func TestSyncedFolder_Prepare_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Prepare(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Prepare_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Prepare(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Info(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Info()
if resp == nil {
t.Fatalf("bad resp")
}
if resp.Description != "mock_folder" {
t.Errorf("%s != mock_folder", resp.Description)
}
if resp.Priority != 100 {
t.Errorf("%d != 100", resp.Priority)
}
}
func TestSyncedFolder_IsUsable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.IsUsable(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !resp {
t.Errorf("bad result")
}
}
func TestSyncedFolder_IsUsable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_IsUsable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Name(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Name()
if resp != "mock_folder" {
t.Errorf("%s != mock_folder", resp)
}
}
func TestSyncedFolder_MachineUI_output(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
go func() {
err := impl.Cleanup(context.Background(), &vagrant.Machine{}, map[string]interface{}{"ui": true})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}()
resp, err := impl.Read("stdout")
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !strings.Contains(resp, "test_output") {
t.Errorf("%s !~ test_output", resp)
}
}

View File

@ -0,0 +1,27 @@
package vagrant
import (
"context"
)
type Provider interface {
Info() *ProviderInfo
Action(ctx context.Context, actionName string, machData *Machine) ([]string, error)
IsInstalled(ctx context.Context, machData *Machine) (bool, error)
IsUsable(ctx context.Context, machData *Machine) (bool, error)
MachineIdChanged(ctx context.Context, machData *Machine) error
Name() string
RunAction(ctx context.Context, actionName string, args interface{}, machData *Machine) (interface{}, error)
SshInfo(ctx context.Context, machData *Machine) (*SshInfo, error)
State(ctx context.Context, machData *Machine) (*MachineState, error)
Config
GuestCapabilities
HostCapabilities
ProviderCapabilities
}
type ProviderInfo struct {
Description string `json:"description"`
Priority int64 `json:"priority"`
}

View File

@ -0,0 +1,8 @@
package vagrant
type SshInfo struct {
Host string `json:"host"`
Port int64 `json:"port"`
Username string `json:"username"`
PrivateKeyPath string `json:"private_key_path"`
}

View File

@ -0,0 +1,26 @@
package vagrant
import (
"context"
)
type FolderList map[string]interface{}
type FolderOptions map[string]interface{}
type SyncedFolderInfo struct {
Description string `json:"description"`
Priority int64 `json:"priority"`
}
type SyncedFolder interface {
Cleanup(ctx context.Context, m *Machine, opts FolderOptions) error
Disable(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
Enable(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
Info() *SyncedFolderInfo
IsUsable(ctx context.Context, m *Machine) (bool, error)
Name() string
Prepare(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
GuestCapabilities
HostCapabilities
}

472
ext/go-plugin/vagrant/ui.go Normal file
View File

@ -0,0 +1,472 @@
package vagrant
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
"time"
"unicode"
)
type UiColor uint
const (
UiColorRed UiColor = 31
UiColorGreen = 32
UiColorYellow = 33
UiColorBlue = 34
UiColorMagenta = 35
UiColorCyan = 36
)
type UiChannel uint
const (
UiOutput UiChannel = 1
UiError = 2
)
var logger = DefaultLogger().Named("ui")
type Options struct {
Channel UiChannel
NewLine bool
}
var defaultOptions = &Options{
Channel: UiOutput,
NewLine: true,
}
// The Ui interface handles all communication for Vagrant with the outside
// world. This sort of control allows us to strictly control how output
// is formatted and various levels of output.
type Ui interface {
Ask(string) (string, error)
Detail(string)
Info(string)
Error(string)
Machine(string, ...string)
Message(string, *Options)
Output(string)
Say(string)
Success(string)
Warn(string)
}
// The BasicUI is a UI that reads and writes from a standard Go reader
// and writer. It is safe to be called from multiple goroutines. Machine
// readable output is simply logged for this UI.
type BasicUi struct {
Reader io.Reader
Writer io.Writer
ErrorWriter io.Writer
l sync.Mutex
interrupted bool
scanner *bufio.Scanner
}
var _ Ui = new(BasicUi)
func (rw *BasicUi) Ask(query string) (string, error) {
rw.l.Lock()
defer rw.l.Unlock()
if rw.interrupted {
return "", errors.New("interrupted")
}
if rw.scanner == nil {
rw.scanner = bufio.NewScanner(rw.Reader)
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(sigCh)
logger.Info("ask", query)
if query != "" {
if _, err := fmt.Fprint(rw.Writer, query+" "); err != nil {
return "", err
}
}
result := make(chan string, 1)
go func() {
var line string
if rw.scanner.Scan() {
line = rw.scanner.Text()
}
if err := rw.scanner.Err(); err != nil {
logger.Error("scan failure", "error", err)
return
}
result <- line
}()
select {
case line := <-result:
return line, nil
case <-sigCh:
// Print a newline so that any further output starts properly
// on a new line.
fmt.Fprintln(rw.Writer)
// Mark that we were interrupted so future Ask calls fail.
rw.interrupted = true
return "", errors.New("interrupted")
}
}
func (rw *BasicUi) Detail(message string) { rw.Say(message) }
func (rw *BasicUi) Info(message string) { rw.Say(message) }
func (rw *BasicUi) Output(message string) { rw.Say(message) }
func (rw *BasicUi) Success(message string) { rw.Say(message) }
func (rw *BasicUi) Warn(message string) { rw.Say(message) }
func (rw *BasicUi) Say(message string) {
rw.Message(message, nil)
}
func (rw *BasicUi) Message(message string, opts *Options) {
rw.l.Lock()
defer rw.l.Unlock()
if opts == nil {
opts = &Options{Channel: UiOutput, NewLine: true}
}
logger.Debug("write message", "content", message, "options", opts)
target := rw.Writer
if opts.Channel == UiError {
if rw.ErrorWriter == nil {
logger.Error("error writer unset using writer")
} else {
target = rw.ErrorWriter
}
}
suffix := ""
if opts.NewLine {
suffix = "\n"
}
_, err := fmt.Fprint(target, message+suffix)
if err != nil {
logger.Error("write failure", "error", err)
}
}
func (rw *BasicUi) Error(message string) {
rw.Message(message, &Options{Channel: UiError, NewLine: true})
}
func (rw *BasicUi) Machine(t string, args ...string) {
logger.Info("machine readable", "category", t, "args", args)
}
// MachineReadableUi is a UI that only outputs machine-readable output
// to the given Writer.
type MachineReadableUi struct {
Writer io.Writer
}
var _ Ui = new(MachineReadableUi)
func (u *MachineReadableUi) Ask(query string) (string, error) {
return "", errors.New("machine-readable UI can't ask")
}
func (u *MachineReadableUi) Detail(message string) {
u.Machine("ui", "detail", message)
}
func (u *MachineReadableUi) Info(message string) {
u.Machine("ui", "info", message)
}
func (u *MachineReadableUi) Output(message string) {
u.Machine("ui", "output", message)
}
func (u *MachineReadableUi) Success(message string) {
u.Machine("ui", "success", message)
}
func (u *MachineReadableUi) Warn(message string) {
u.Machine("ui", "warn", message)
}
func (u *MachineReadableUi) Say(message string) {
u.Machine("ui", "say", message)
}
func (u *MachineReadableUi) Message(message string, opts *Options) {
u.Machine("ui", "message", message)
}
func (u *MachineReadableUi) Error(message string) {
u.Machine("ui", "error", message)
}
// TODO: Do we want to update this to match Vagrant machine style?
func (u *MachineReadableUi) Machine(category string, args ...string) {
now := time.Now().UTC()
// Determine if we have a target, and set it
target := ""
commaIdx := strings.Index(category, ",")
if commaIdx > -1 {
target = category[0:commaIdx]
category = category[commaIdx+1:]
}
// Prepare the args
for i, v := range args {
args[i] = strings.Replace(v, ",", "%!(VAGRANT_COMMA)", -1)
args[i] = strings.Replace(args[i], "\r", "\\r", -1)
args[i] = strings.Replace(args[i], "\n", "\\n", -1)
}
argsString := strings.Join(args, ",")
_, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString)
if err != nil {
if err == syscall.EPIPE || strings.Contains(err.Error(), "broken pipe") {
// Ignore epipe errors because that just means that the file
// is probably closed or going to /dev/null or something.
} else {
panic(err)
}
}
}
type NoopUi struct{}
var _ Ui = new(NoopUi)
func (*NoopUi) Ask(string) (string, error) { return "", errors.New("this is a noop ui") }
func (*NoopUi) Detail(string) { return }
func (*NoopUi) Info(string) { return }
func (*NoopUi) Error(string) { return }
func (*NoopUi) Machine(string, ...string) { return }
func (*NoopUi) Message(string, *Options) { return }
func (*NoopUi) Output(string) { return }
func (*NoopUi) Say(string) { return }
func (*NoopUi) Success(string) { return }
func (*NoopUi) Warn(string) { return }
// ColoredUi is a UI that is colored using terminal colors.
type ColoredUi struct {
Color UiColor
ErrorColor UiColor
SuccessColor UiColor
WarnColor UiColor
Ui Ui
}
var _ Ui = new(ColoredUi)
func (u *ColoredUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.colorize(query, u.Color, true))
}
func (u *ColoredUi) Detail(message string) {
u.Say(message)
}
func (u *ColoredUi) Info(message string) {
u.Say(message)
}
func (u *ColoredUi) Error(message string) {
color := u.ErrorColor
if color == 0 {
color = UiColorRed
}
u.Ui.Error(u.colorize(message, color, true))
}
func (u *ColoredUi) Machine(t string, args ...string) {
// Don't colorize machine-readable output
u.Ui.Machine(t, args...)
}
func (u *ColoredUi) Message(message string, opts *Options) {
u.Ui.Message(u.colorize(message, u.Color, false), opts)
}
func (u *ColoredUi) Output(message string) {
u.Say(message)
}
func (u *ColoredUi) Say(message string) {
u.Ui.Say(u.colorize(message, u.Color, true))
}
func (u *ColoredUi) Success(message string) {
u.Ui.Say(u.colorize(message, u.SuccessColor, true))
}
func (u *ColoredUi) Warn(message string) {
u.Ui.Say(u.colorize(message, u.WarnColor, true))
}
func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string {
if !u.supportsColors() {
return message
}
attr := 0
if bold {
attr = 1
}
return fmt.Sprintf("\033[%d;%dm%s\033[0m", attr, color, message)
}
func (u *ColoredUi) supportsColors() bool {
// Never use colors if we have this environmental variable
if os.Getenv("VAGRANT_NO_COLOR") != "" {
return false
}
// For now, on non-Windows machine, just assume it does
if runtime.GOOS != "windows" {
return true
}
// On Windows, if we appear to be in Cygwin, then it does
cygwin := os.Getenv("CYGWIN") != "" ||
os.Getenv("OSTYPE") == "cygwin" ||
os.Getenv("TERM") == "cygwin"
return cygwin
}
// TargetedUi is a UI that wraps another UI implementation and modifies
// the output to indicate a specific target. Specifically, all Say output
// is prefixed with the target name. Message output is not prefixed but
// is offset by the length of the target so that output is lined up properly
// with Say output. Machine-readable output has the proper target set.
type TargetedUi struct {
Target string
Ui Ui
}
var _ Ui = new(TargetedUi)
func (u *TargetedUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.prefixLines(true, query))
}
func (u *TargetedUi) Detail(message string) {
u.Ui.Detail(u.prefixLines(true, message))
}
func (u *TargetedUi) Info(message string) {
u.Ui.Info(u.prefixLines(true, message))
}
func (u *TargetedUi) Output(message string) {
u.Ui.Output(u.prefixLines(true, message))
}
func (u *TargetedUi) Success(message string) {
u.Ui.Success(u.prefixLines(true, message))
}
func (u *TargetedUi) Warn(message string) {
u.Ui.Warn(u.prefixLines(true, message))
}
func (u *TargetedUi) Say(message string) {
u.Ui.Say(u.prefixLines(true, message))
}
func (u *TargetedUi) Message(message string, opts *Options) {
u.Ui.Message(u.prefixLines(false, message), opts)
}
func (u *TargetedUi) Error(message string) {
u.Ui.Error(u.prefixLines(true, message))
}
func (u *TargetedUi) Machine(t string, args ...string) {
// Prefix in the target, then pass through
u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...)
}
func (u *TargetedUi) prefixLines(arrow bool, message string) string {
arrowText := "==>"
if !arrow {
arrowText = strings.Repeat(" ", len(arrowText))
}
var result bytes.Buffer
for _, line := range strings.Split(message, "\n") {
result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line))
}
return strings.TrimRightFunc(result.String(), unicode.IsSpace)
}
// TimestampedUi is a UI that wraps another UI implementation and prefixes
// prefixes each message with an RFC3339 timestamp
type TimestampedUi struct {
Ui Ui
}
var _ Ui = new(TimestampedUi)
func (u *TimestampedUi) Ask(query string) (string, error) {
return u.Ui.Ask(query)
}
func (u *TimestampedUi) Detail(message string) {
u.Ui.Detail(u.timestampLine(message))
}
func (u *TimestampedUi) Info(message string) {
u.Ui.Info(u.timestampLine(message))
}
func (u *TimestampedUi) Output(message string) {
u.Ui.Output(u.timestampLine(message))
}
func (u *TimestampedUi) Success(message string) {
u.Ui.Success(u.timestampLine(message))
}
func (u *TimestampedUi) Warn(message string) {
u.Ui.Warn(u.timestampLine(message))
}
func (u *TimestampedUi) Say(message string) {
u.Ui.Say(u.timestampLine(message))
}
func (u *TimestampedUi) Message(message string, opts *Options) {
u.Ui.Message(u.timestampLine(message), opts)
}
func (u *TimestampedUi) Error(message string) {
u.Ui.Error(u.timestampLine(message))
}
func (u *TimestampedUi) Machine(message string, args ...string) {
u.Ui.Machine(message, args...)
}
func (u *TimestampedUi) timestampLine(string string) string {
return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string)
}

View File

@ -0,0 +1,290 @@
package vagrant
import (
"bytes"
"os"
"strings"
"testing"
)
// This reads the output from the bytes.Buffer in our test object
// and then resets the buffer.
func readWriter(ui *BasicUi) (result string) {
buffer := ui.Writer.(*bytes.Buffer)
result = buffer.String()
buffer.Reset()
return
}
// Reset the input Reader then add some input to it.
func writeReader(ui *BasicUi, input string) {
buffer := ui.Reader.(*bytes.Buffer)
buffer.WriteString(input)
}
func readErrorWriter(ui *BasicUi) (result string) {
buffer := ui.ErrorWriter.(*bytes.Buffer)
result = buffer.String()
buffer.Reset()
return
}
func testUi() *BasicUi {
return &BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
ErrorWriter: new(bytes.Buffer),
}
}
func TestColoredUi(t *testing.T) {
bufferUi := testUi()
ui := &ColoredUi{UiColorBlue, UiColorRed, UiColorGreen,
UiColorYellow, bufferUi}
if !ui.supportsColors() {
t.Skip("skipping for ui without color support")
}
ui.Say("foo")
result := readWriter(bufferUi)
if result != "\033[1;34mfoo\033[0m\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Message("foo", nil)
result = readWriter(bufferUi)
if result != "\033[0;34mfoo\033[0m\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Error("foo")
result = readWriter(bufferUi)
if result != "" {
t.Fatalf("invalid output: %s", result)
}
result = readErrorWriter(bufferUi)
if result != "\033[1;31mfoo\033[0m\n" {
t.Fatalf("invalid output: %s", result)
}
}
func TestColoredUi_noColorEnv(t *testing.T) {
bufferUi := testUi()
ui := &ColoredUi{UiColorBlue, UiColorRed, UiColorGreen,
UiColorYellow, bufferUi}
// Set the env var to get rid of the color
oldenv := os.Getenv("VAGRANT_NO_COLOR")
os.Setenv("VAGRANT_NO_COLOR", "1")
defer os.Setenv("VAGRANT_NO_COLOR", oldenv)
ui.Say("foo")
result := readWriter(bufferUi)
if result != "foo\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Message("foo", nil)
result = readWriter(bufferUi)
if result != "foo\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Error("foo")
result = readErrorWriter(bufferUi)
if result != "foo\n" {
t.Fatalf("invalid output: %s", result)
}
}
func TestTargetedUi(t *testing.T) {
bufferUi := testUi()
targetedUi := &TargetedUi{
Target: "foo",
Ui: bufferUi,
}
var actual, expected string
targetedUi.Say("foo")
actual = readWriter(bufferUi)
expected = "==> foo: foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
targetedUi.Message("foo", nil)
actual = readWriter(bufferUi)
expected = " foo: foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
targetedUi.Error("bar")
actual = readErrorWriter(bufferUi)
expected = "==> foo: bar\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
targetedUi.Say("foo\nbar")
actual = readWriter(bufferUi)
expected = "==> foo: foo\n==> foo: bar\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
}
func TestColoredUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &ColoredUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("ColoredUi must implement Ui")
}
}
func TestTargetedUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &TargetedUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("TargetedUi must implement Ui")
}
}
func TestBasicUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &BasicUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("BasicUi must implement Ui")
}
}
func TestBasicUi_Error(t *testing.T) {
bufferUi := testUi()
var actual, expected string
bufferUi.Error("foo")
actual = readErrorWriter(bufferUi)
expected = "foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
bufferUi.ErrorWriter = nil
bufferUi.Error("5")
actual = readWriter(bufferUi)
expected = "5\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
}
func TestBasicUi_Say(t *testing.T) {
bufferUi := testUi()
var actual, expected string
bufferUi.Say("foo")
actual = readWriter(bufferUi)
expected = "foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
bufferUi.Say("5")
actual = readWriter(bufferUi)
expected = "5\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
}
func TestBasicUi_Ask(t *testing.T) {
var actual, expected string
var err error
var testCases = []struct {
Prompt, Input, Answer string
}{
{"[c]ontinue or [a]bort", "c\n", "c"},
{"[c]ontinue or [a]bort", "c", "c"},
// Empty input shouldn't give an error
{"Name", "Joe Bloggs\n", "Joe Bloggs"},
{"Name", "Joe Bloggs", "Joe Bloggs"},
{"Name", "\n", ""},
}
for _, testCase := range testCases {
// Because of the internal bufio we can't easily reset the input, so create a new one each time
bufferUi := testUi()
writeReader(bufferUi, testCase.Input)
actual, err = bufferUi.Ask(testCase.Prompt)
if err != nil {
t.Fatal(err)
}
if actual != testCase.Answer {
t.Fatalf("bad answer: %#v", actual)
}
actual = readWriter(bufferUi)
expected = testCase.Prompt + " "
if actual != expected {
t.Fatalf("bad prompt: %#v", actual)
}
}
}
func TestMachineReadableUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &MachineReadableUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("MachineReadableUi must implement Ui")
}
}
func TestMachineReadableUi(t *testing.T) {
var data, expected string
buf := new(bytes.Buffer)
ui := &MachineReadableUi{Writer: buf}
// No target
ui.Machine("foo", "bar", "baz")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,bar,baz\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// Target
buf.Reset()
ui.Machine("mitchellh,foo", "bar", "baz")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = "mitchellh,foo,bar,baz\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// Commas
buf.Reset()
ui.Machine("foo", "foo,bar")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,foo%!(VAGRANT_COMMA)bar\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// New lines
buf.Reset()
ui.Machine("foo", "foo\n")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,foo\\n\n"
if data != expected {
t.Fatalf("bad: %#v", data)
}
}

21
go.mod Normal file
View File

@ -0,0 +1,21 @@
module github.com/hashicorp/vagrant
require (
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5
github.com/dylanmei/iso8601 v0.1.0 // indirect
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1
github.com/golang/protobuf v1.3.0
github.com/hashicorp/go-hclog v0.8.0
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104
github.com/kr/fs v0.1.0 // indirect
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943
github.com/mitchellh/iochan v1.0.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db
github.com/pkg/errors v0.8.1 // indirect
github.com/pkg/sftp v1.10.0
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
google.golang.org/grpc v1.19.0
)

91
go.sum Normal file
View File

@ -0,0 +1,91 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4 h1:pSm8mp0T2OH2CPmPDPtwHPr3VAQaOwVF/JbllOPP4xA=
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022 h1:y8Gs8CzNfDF5AZvjr+5UyGQvQEBL7pwo+v+wX6q9JI8=
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5 h1:U7q69tqXiCf6m097GRlNQB0/6SI1qWIOHYHhCEvDxF4=
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5/go.mod h1:nxQPcNPR/34g+HcK2hEsF99O+GJgIkW/OmPl8wtzhmk=
github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e h1:ptBAamGVd6CfRsUtyHD+goy2JGhv1QC32v3gqM8mYAM=
github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0 h1:JaCC8jz0zdMLk2m+qCCVLLLM/PL93p84w4pK3aJWj60=
github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dylanmei/iso8601 v0.1.0 h1:812NGQDBcqquTfH5Yeo7lwR0nzx/cKdsmf3qMjPURUI=
github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ=
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1 h1:r1oACdS2XYiAWcfF8BJXkoU8l1J71KehGR+d99yWEDA=
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0 h1:z3ollgGRg8RjfJH6UVBaG54R70GFd++QOkvnJH3VSBY=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104 h1:9iQ/zrTOJqzP+kH37s6xNb6T1RysiT7fnDD3DJbspVw=
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9 h1:SmVbOZFWAlyQshuMfOkiAx1f5oUTsOGG5IXplAEYeeM=
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943 h1:Bteu9XN1gkBePnKr0v1edkUo2LJRsmK5ne2FrC6yVW4=
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943/go.mod h1:bsMsaiOA3CXjbJxW0a94G4PfPDj9zUmH5JoFuJ9P4o0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU=
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.0 h1:DGA1KlA9esU6WcicH+P8PxFZOl15O6GYtab1cIJdOlE=
github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,4 +1,5 @@
require "log4r"
require "log4r/logger"
require "vagrant/util/credential_scrubber"
# Update the default formatter within the log4r library to ensure
# sensitive values are being properly scrubbed from logger data
@ -53,6 +54,8 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
# rest-client to write using the `<<` operator.
# See https://github.com/rest-client/rest-client/issues/34#issuecomment-290858
# for more information
Log4r::PatternFormatter::DirectiveTable["l"] =
'"[" + ' + Log4r::PatternFormatter::DirectiveTable["l"] + ' + "]"'
class VagrantLogger < Log4r::Logger
def << (msg)
debug(msg.strip)
@ -61,13 +64,10 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
logger = VagrantLogger.new("vagrant")
logger.outputters = Log4r::Outputter.stderr
logger.level = level
base_formatter = Log4r::BasicFormatter.new
if ENV["VAGRANT_LOG_TIMESTAMP"]
base_formatter = Log4r::PatternFormatter.new(
pattern: "%d [%5l] %m",
date_pattern: "%F %T"
)
end
base_formatter = Log4r::PatternFormatter.new(
pattern: "%d %-7l %C: %m",
date_pattern: ENV["VAGRANT_LOG_TIMESTAMP"] ? "%FT%T.%L%z" : " "
)
# Vagrant Cloud gem uses RestClient to make HTTP requests, so
# log them if debug is enabled and use Vagrants logger
require 'rest_client'
@ -118,6 +118,7 @@ module Vagrant
autoload :Driver, 'vagrant/driver'
autoload :Environment, 'vagrant/environment'
autoload :Errors, 'vagrant/errors'
autoload :GoPlugin, 'vagrant/go_plugin'
autoload :Guest, 'vagrant/guest'
autoload :Host, 'vagrant/host'
autoload :Machine, 'vagrant/machine'

View File

@ -15,7 +15,6 @@ module Vagrant
autoload :Confirm, "vagrant/action/builtin/confirm"
autoload :ConfigValidate, "vagrant/action/builtin/config_validate"
autoload :DestroyConfirm, "vagrant/action/builtin/destroy_confirm"
autoload :Disk, "vagrant/action/builtin/disk"
autoload :EnvSet, "vagrant/action/builtin/env_set"
autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt"
autoload :HandleBox, "vagrant/action/builtin/handle_box"

View File

@ -348,15 +348,9 @@ module Vagrant
end
if opts[:checksum] && opts[:checksum_type]
if opts[:checksum].to_s.strip.empty?
@logger.warn("Given checksum is empty, cannot validate checksum for box")
elsif opts[:checksum_type].to_s.strip.empty?
@logger.warn("Given checksum type is empty, cannot validate checksum for box")
else
env[:ui].detail(I18n.t("vagrant.actions.box.add.checksumming"))
validate_checksum(
opts[:checksum_type], opts[:checksum], box_url)
end
env[:ui].detail(I18n.t("vagrant.actions.box.add.checksumming"))
validate_checksum(
opts[:checksum_type], opts[:checksum], box_url)
end
# Add the box!
@ -533,11 +527,22 @@ module Vagrant
end
def validate_checksum(checksum_type, checksum, path)
@logger.info("Validating checksum with #{checksum_type}")
checksum_klass = case checksum_type.to_sym
when :md5
Digest::MD5
when :sha1
Digest::SHA1
when :sha256
Digest::SHA2
else
raise Errors::BoxChecksumInvalidType,
type: checksum_type.to_s
end
@logger.info("Validating checksum with #{checksum_klass}")
@logger.info("Expected checksum: #{checksum}")
actual = FileChecksum.new(path, checksum_type).checksum
@logger.info("Actual checksum: #{actual}")
actual = FileChecksum.new(path, checksum_klass).checksum
if actual.casecmp(checksum) != 0
raise Errors::BoxChecksumMismatch,
actual: actual,

View File

@ -40,7 +40,7 @@ module Vagrant
# Have download options specified in the environment override
# options specified for the machine.
download_options = {
automatic_check: !env[:box_outdated_force],
automatic_check: true,
ca_cert: env[:ca_cert] || machine.config.vm.box_download_ca_cert,
ca_path: env[:ca_path] || machine.config.vm.box_download_ca_path,
client_cert: env[:client_cert] ||
@ -70,23 +70,15 @@ module Vagrant
message: e.message))
end
env[:box_outdated] = update != nil
local_update = check_outdated_local(env)
if update && (local_update.nil? || (local_update.version < update[1].version))
if update
env[:ui].warn(I18n.t(
"vagrant.box_outdated_single",
name: update[0].name,
provider: box.provider,
current: box.version,
latest: update[1].version))
elsif local_update
env[:ui].warn(I18n.t(
"vagrant.box_outdated_local",
name: local_update.name,
old: box.version,
new: local_update.version))
env[:box_outdated] = true
else
env[:box_outdated] = false
check_outdated_local(env)
end
@app.call(env)
@ -101,8 +93,19 @@ module Vagrant
version ||= ""
version += "> #{machine.box.version}"
env[:box_collection].find(
box = env[:box_collection].find(
machine.box.name, machine.box.provider, version)
if box
env[:ui].warn(I18n.t(
"vagrant.box_outdated_local",
name: box.name,
old: machine.box.version,
new: box.version))
env[:box_outdated] = true
return
end
env[:box_outdated] = false
end
end
end

View File

@ -1,39 +0,0 @@
module Vagrant
module Action
module Builtin
class Disk
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant::action::builtin::disk")
end
def call(env)
machine = env[:machine]
defined_disks = get_disks(machine, env)
# Call into providers machine implementation for disk management
if !defined_disks.empty?
if machine.provider.capability?(:configure_disks)
machine.provider.capability(:configure_disks, defined_disks)
else
env[:ui].warn(I18n.t("vagrant.actions.disk.provider_unsupported",
provider: machine.provider_name))
end
end
# Continue On
@app.call(env)
end
def get_disks(machine, env)
return @_disks if @_disks
@_disks = []
@_disks = machine.config.vm.disks
@_disks
end
end
end
end
end

View File

@ -25,27 +25,9 @@ module Vagrant
# Store in the type map so that --provision-with works properly
@_provisioner_types[result] = provisioner.type
# Set top level provisioner name to provisioner configs name if top level name not set.
# This is mostly for handling the shell provisioner, if a user has set its name like:
#
# config.vm.provision "shell", name: "my_provisioner"
#
# Where `name` is a shell config option, not a top level provisioner class option
#
# Note: `name` is set to a symbol, since it is converted to one via #Config::VM.provision
provisioner_name = provisioner.name
if !provisioner_name
if provisioner.config.respond_to?(:name) &&
provisioner.config.name
provisioner_name = provisioner.config.name.to_sym
end
else
provisioner_name = provisioner_name.to_sym
end
# Build up the options
options = {
name: provisioner_name,
name: provisioner.name,
run: provisioner.run,
before: provisioner.before,
after: provisioner.after,

View File

@ -36,35 +36,17 @@ module Vagrant
# Get the command and wrap it in a login shell
command = ShellQuote.escape(env[:ssh_run_command], "'")
if env[:machine].config.vm.communicator == :winssh
shell = env[:machine].config.winssh.shell
else
shell = env[:machine].config.ssh.shell
end
if shell == "cmd"
# Add an extra space to the command so cmd.exe quoting works
# properly
command = "#{shell} /C #{command} "
elsif shell == "powershell"
command = "$ProgressPreference = \"SilentlyContinue\"; #{command}"
command = Base64.strict_encode64(command.encode("UTF-16LE", "UTF-8"))
command = "#{shell} -encodedCommand #{command}"
else
command = "#{shell} -c '#{command}'"
end
command = "#{env[:machine].config.ssh.shell} -c '#{command}'"
# Execute!
opts = env[:ssh_opts] || {}
opts[:extra_args] ||= []
# Allow the user to specify a tty or non-tty manually, but if they
# don't then we default to a TTY unless they are using WinSSH
# don't then we default to a TTY
if !opts[:extra_args].include?("-t") &&
!opts[:extra_args].include?("-T") &&
env[:tty] &&
env[:machine].config.vm.communicator != :winssh
env[:tty]
opts[:extra_args] << "-t"
end

View File

@ -47,16 +47,7 @@ module Vagrant
raise Errors::VagrantInterrupt if env[:interrupted]
action = @actions.shift
@logger.info("Calling IN action: #{action}")
if !action.is_a?(Proc) && env[:hook]
hook_name = action.class.name.split("::").last.
gsub(/([a-z])([A-Z])/, '\1_\2').gsub('-', '_').downcase
end
env[:hook].call("before_#{hook_name}".to_sym) if hook_name
@stack.unshift(action).first.call(env)
env[:hook].call("after_#{hook_name}".to_sym) if hook_name
raise Errors::VagrantInterrupt if env[:interrupted]
@logger.info("Calling OUT action: #{action}")
rescue SystemExit

View File

@ -221,5 +221,17 @@ module Vagrant
"#{@name}-#{@version}-#{@provider}" <=>
"#{other.name}-#{other.version}-#{other.provider}"
end
# @return [String]
def to_json(*args)
{
name: name,
provider: provider,
version: version,
directory: directory.to_s,
metadata: metadata,
metadata_url: metadata_url
}.to_json(*args)
end
end
end

View File

@ -68,25 +68,11 @@ module Vagrant
# Returns all the versions supported by this metadata. These
# versions are sorted so the last element of the list is the
# latest version. Optionally filter versions by a matching
# provider.
# latest version.
#
# @return[Array<String>]
def versions(**opts)
provider = nil
provider = opts[:provider].to_sym if opts[:provider]
if provider
@version_map.select do |version, raw|
if raw["providers"]
raw["providers"].detect do |p|
p["name"].to_sym == provider
end
end
end.keys.sort.map(&:to_s)
else
@version_map.keys.sort.map(&:to_s)
end
def versions
@version_map.keys.sort.map(&:to_s)
end
# Represents a single version within the metadata.

View File

@ -129,7 +129,7 @@ module Vagrant
path = "(unknown)"
if e.backtrace && e.backtrace[0]
backtrace_tokens = e.backtrace[0].split(":")
path = e.backtrace.first.slice(0, e.backtrace.first.rindex(':')).rpartition(':').first
path = backtrace_tokens[0]
backtrace_tokens.each do |part|
if part =~ /\d+/
line = part.to_i

View File

@ -7,6 +7,10 @@ module Vagrant
def method_missing(name, *args, &block)
DummyConfig.new
end
def to_json(*_)
"null"
end
end
end
end

View File

@ -112,6 +112,14 @@ module Vagrant
@keys = state["keys"] if state.key?("keys")
@missing_key_calls = state["missing_key_calls"] if state.key?("missing_key_calls")
end
def to_json(*args)
Hash[
@keys.find_all { |k,v|
!k.to_s.start_with?("_")
}
].to_json(*args)
end
end
end
end

View File

@ -175,6 +175,15 @@ module Vagrant
# Load any global plugins
Vagrant::Plugin::Manager.instance.load_plugins(plugins)
# Load any available go-plugins
Util::Experimental.guard_with(:go_plugin) do
begin
Vagrant::GoPlugin::Manager.instance.globalize!
rescue LoadError => err
@logger.warn("go plugin support is not available: #{err}")
end
end
plugins = process_configured_plugins
# Call the hooks that does not require configurations to be loaded
@ -918,6 +927,26 @@ module Vagrant
end
end
# @return [String]
def to_json(*args)
{
cwd: cwd,
data_dir: data_dir,
vagrantfile_name: vagrantfile_name,
home_path: home_path,
local_data_path: local_data_path,
tmp_path: tmp_path,
aliases_path: aliases_path,
boxes_path: boxes_path,
gems_path: gems_path,
default_private_key_path: default_private_key_path,
root_path: root_path,
primary_machine_name: primary_machine_name,
machine_names: machine_names,
active_machines: Hash[active_machines]
}.to_json(*args)
end
protected
# Attempt to guess the configured provider in use. Will fallback

View File

@ -436,10 +436,6 @@ module Vagrant
error_key(:machine_action_locked)
end
class MachineFolderNotAccessible < VagrantError
error_key(:machine_folder_not_accessible)
end
class MachineGuestNotReady < VagrantError
error_key(:machine_guest_not_ready)
end

26
lib/vagrant/go_plugin.rb Normal file
View File

@ -0,0 +1,26 @@
module Vagrant
autoload :Proto, "vagrant/go_plugin/vagrant_proto/vagrant_services_pb"
module GoPlugin
# @return [String]
INSTALL_DIRECTORY = Vagrant.user_data_path.join("go-plugins").to_s.freeze
autoload :CapabilityPlugin, "vagrant/go_plugin/capability_plugin"
autoload :ConfigPlugin, "vagrant/go_plugin/config_plugin"
autoload :Core, "vagrant/go_plugin/core"
autoload :GRPCPlugin, "vagrant/go_plugin/core"
autoload :Interface, "vagrant/go_plugin/interface"
autoload :Manager, "vagrant/go_plugin/manager"
autoload :ProviderPlugin, "vagrant/go_plugin/provider_plugin"
autoload :SyncedFolderPlugin, "vagrant/go_plugin/synced_folder_plugin"
# @return [Interface]
def self.interface
unless @_interface
@_interface = Interface.new
end
@_interface
end
end
end

View File

@ -0,0 +1,108 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
# Contains all capability functionality for go-plugin
module CapabilityPlugin
extend Vagrant::Util::Logger
# Wrapper class for go-plugin defined capabilities
class Capability
include GRPCPlugin
end
# Fetch any defined guest capabilites for given plugin and register
# capabilities within given plugin class
#
# @param [Vagrant::Proto::GuestCapabilities::Stub] client Plugin client
# @param [Class] plugin_klass Plugin class to register capabilities
# @param [Symbol] plugin_type Type of plugin
def self.generate_guest_capabilities(client, plugin_klass, plugin_type)
logger.debug("checking for guest capabilities in #{plugin_type} plugin #{plugin_klass}")
result = client.guest_capabilities(Vagrant::Proto::Empty.new)
return if result.capabilities.empty?
logger.debug("guest capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
result.capabilities.each do |cap|
cap_klass = Class.new(Capability).tap do |k|
k.define_singleton_method(cap.name) { |machine, *args|
response = plugin_client.guest_capability(
Vagrant::Proto::GuestCapabilityRequest.new(
machine: JSON.dump(machine), arguments: JSON.dump(args),
capability: Vagrant::Proto::SystemCapability.new(
name: cap.name, platform: cap.platform))).result
result = JSON.load(response)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
}
end
cap_klass.plugin_client = client
plugin_klass.guest_capability(cap.platform.to_sym, cap.name.to_sym) { cap_klass }
end
end
# Fetch any defined host capabilites for given plugin and register
# capabilities within given plugin class
#
# @param [Vagrant::Proto::HostCapabilities::Stub] client Plugin client
# @param [Class] plugin_klass Plugin class to register capabilities
# @param [Symbol] plugin_type Type of plugin
def self.generate_host_capabilities(client, plugin_klass, plugin_type)
logger.debug("checking for host capabilities in #{plugin_type} plugin #{plugin_klass}")
result = client.host_capabilities(Vagrant::Proto::Empty.new)
return if result.capabilities.empty?
logger.debug("host capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
result.capabilities.each do |cap|
cap_klass = Class.new(Capability).tap do |k|
k.define_singleton_method(cap.name) { |environment, *args|
response = plugin_client.host_capability(
Vagrant::Proto::HostCapabilityRequest.new(
environment: JSON.dump(environment), arguments: JSON.dump(args),
capability: Vagrant::Proto::SystemCapability.new(
name: cap.name, platform: cap.platform))).result
result = JSON.load(response)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
}
end
cap_klass.plugin_client = client
plugin_klass.host_capability(cap.platform.to_sym, cap.name.to_sym) { cap_klass }
end
end
# Fetch any defined provider capabilites for given plugin and register
# capabilities within given plugin class
#
# @param [Vagrant::Proto::ProviderCapabilities::Stub] client Plugin client
# @param [Class] plugin_klass Plugin class to register capabilities
# @param [Symbol] plugin_type Type of plugin
def self.generate_provider_capabilities(client, plugin_klass, plugin_type)
logger.debug("checking for provider capabilities in #{plugin_type} plugin #{plugin_klass}")
result = client.provider_capabilities(Vagrant::Proto::Empty.new)
return if result.capabilities.empty?
logger.debug("provider capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
result.capabilities.each do |cap|
cap_klass = Class.new(Capability).tap do |k|
k.define_singleton_method(cap.name) { |machine, *args|
response = plugin_client.provider_capability(
Vagrant::Proto::ProviderCapabilityRequest.new(
machine: JSON.dump(machine), arguments: JSON.dump(args),
capability: Vagrant::Proto::ProviderCapability.new(
name: cap.name, provider: cap.provider))).result
result = JSON.load(response)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
}
end
cap_klass.plugin_client = client
plugin_klass.provider_capability(cap.provider.to_sym, cap.name.to_sym) { cap_klass }
end
end
end
end
end

View File

@ -0,0 +1,64 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
# Contains all configuration functionality for go-plugin
module ConfigPlugin
# Generate configuration for the parent class
#
# @param [Vagrant::Proto::Config::Stub] client Plugin client
# @param [String] parent_name Parent plugin name
# @param [Class] parent_klass Parent class to register config
# @param [Symbol] parent_type Type of parent class (:provider, :synced_folder, etc)
def self.generate_config(client, parent_name, parent_klass, parent_type)
config_attrs = client.config_attributes(Vagrant::Proto::Empty.new).items
config_klass = Class.new(Config).tap do |c|
c.class_eval("def parent_name; '#{parent_name}'; end")
end
config_klass.plugin_client = client
Array(config_attrs).each do |att|
config_klass.instance_eval("attr_accessor :#{att}")
end
parent_klass.config(parent_name, parent_type) { config_klass }
end
# Config plugin class used with go-plugin
class Config < Vagrant.plugin("2", :config)
include GRPCPlugin
# Finalize the current configuration
def finalize!
data = local_data
response = plugin_client.config_finalize(Vagrant::Proto::Configuration.new(
data: JSON.dump(data)))
result = JSON.load(response.data)
if result && result.is_a?(Hash)
new_data = Vagrant::Util::HashWithIndifferentAccess.new(result)
new_data.each do |key, value|
next if data[key] == value
instance_variable_set("@#{key}", value)
end
end
self
end
# Validate configuration
#
# @param [Vagrant::Machine] machine Guest machine
# @return [Array<String>] list of errors
def validate(machine)
result = plugin_client.config_validate(Vagrant::Proto::Configuration.new(
machine: JSON.dump(machine),
data: JSON.dump(local_data)))
{parent_name => result.items}
end
# @return [Hash] currently defined instance variables
def local_data
Vagrant::Util::HashWithIndifferentAccess.
new(instance_variables_hash)
end
end
end
end
end

View File

@ -0,0 +1,82 @@
require "ffi"
module Vagrant
module GoPlugin
# Base module for generic setup of module/class
module Core
# Loads FFI and core helpers into given module/class
def self.included(const)
const.class_eval do
include Vagrant::Util::Logger
extend FFI::Library
ffi_lib FFI::Platform::LIBC
ffi_lib File.expand_path("./go-plugin.so", File.dirname(__FILE__))
typedef :strptr, :plugin_result
# stdlib functions
if FFI::Platform.windows?
attach_function :free, :_free, [:pointer], :void
else
attach_function :free, [:pointer], :void
end
# Load the result received from the extension. This will load
# the JSON result, raise an error if detected, and properly
# free the memory associated with the result.
def load_result(*args)
val, ptr = block_given? ? yield : args
FFI::AutoPointer.new(ptr, self.method(:free))
begin
result = JSON.load(val)
if !result.is_a?(Hash)
raise TypeError.new "Expected Hash but received `#{result.class}`"
end
if !result["error"].to_s.empty?
raise ArgumentError.new result["error"].to_s
end
result = result["result"]
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
rescue => e
# TODO: Customize to provide formatted output on error
raise
end
end
end
end
end
# Simple module to load into plugin wrapper classes
# to provide expected functionality
module GRPCPlugin
module ClassMethods
def plugin_client
@_plugin_client
end
def plugin_client=(c)
if @_plugin_client
raise ArgumentError, "Plugin client has already been set"
end
@_plugin_client = c
end
end
module InstanceMethods
def plugin_client
self.class.plugin_client
end
end
def self.included(klass)
klass.include(Vagrant::Util::Logger)
klass.include(InstanceMethods)
klass.extend(ClassMethods)
end
end
end
end

View File

@ -0,0 +1,187 @@
require "log4r"
require "vagrant/go_plugin/core"
require "vagrant/go_plugin/vagrant_proto/vagrant_services_pb"
module RubyLogger
include Vagrant::Util::Logger
end
module GRPC
extend Vagrant::Util::Logger
end
module Vagrant
module GoPlugin
# Interface for go-plugin integration
class Interface
include Core
# go plugin functions
typedef :bool, :enable_logger
typedef :bool, :timestamps
typedef :string, :log_level
typedef :string, :plugin_directory
attach_function :_setup, :Setup, [:enable_logger, :timestamps, :log_level], :bool
attach_function :_teardown, :Teardown, [], :void
attach_function :_reset, :Reset, [], :void
attach_function :_load_plugins, :LoadPlugins, [:plugin_directory], :bool
attach_function :_list_providers, :ListProviders, [], :plugin_result
attach_function :_list_synced_folders, :ListSyncedFolders, [], :plugin_result
def initialize
Vagrant::Proto.instance_eval do
::GRPC.extend(Vagrant::Util::Logger)
end
setup
end
# List of provider plugins currently available
#
# @return [Hash<String,Hash<Info>>]
def list_providers
load_result { _list_providers } || {}
end
# List of synced folder plugins currently available
#
# @return [Hash<String,Hash<Info>>]
def list_synced_folders
load_result { _list_synced_folders } || {}
end
# Load any plugins found at the given directory
#
# @param [String, Pathname] path Directory to load
def load_plugins(path)
logger.debug("loading plugins from path: #{path}")
if !File.directory?(path.to_s)
raise ArgumentError, "Directory expected for plugin loading"
end
_load_plugins(path.to_s)
end
# Register all available plugins
def register_plugins
logger.debug("registering provider plugins")
load_providers
logger.debug("registering synced folder plugins")
load_synced_folders
end
# Load the plugins found at the given directory
#
# @param [String] plugin_directory Directory containing go-plugins
def setup
if !@setup
@setup = true
Kernel.at_exit { Vagrant::GoPlugin.interface.teardown }
logger.debug("running go-plugin interface setup")
_setup(!Vagrant.log_level.to_s.empty?,
!!ENV["VAGRANT_LOG_TIMESTAMP"],
Vagrant.log_level.to_s)
else
logger.warn("go-plugin interface already setup")
end
end
def reset
logger.debug("running go-plugin interface reset")
_reset
logger.debug("completed go-plugin interface reset")
end
# Teardown any plugins that may be currently active
def teardown
logger.debug("starting teardown of go-plugin interface")
_teardown
logger.debug("teardown of go-plugin interface complete")
end
# @return [Boolean] go plugins have been setup
def configured?
!!@setup
end
# Load any detected provider plugins
def load_providers
if !@_providers_loaded
@_providers_loaded
logger.debug("provider go-plugins have not been loaded... loading")
list_providers.each do |p_name, p_details|
logger.debug("loading go-plugin provider #{p_name}. details - #{p_details}")
client = Vagrant::Proto::Provider::Stub.new(
"#{p_details[:network]}://#{p_details[:address]}",
:this_channel_is_insecure)
# Create new provider class wrapper
provider_klass = Class.new(ProviderPlugin::Provider)
provider_klass.plugin_client = client
# Create new plugin to register the provider
plugin_klass = Class.new(Vagrant.plugin("2"))
# Define the plugin
plugin_klass.class_eval do
name "#{p_name} Provider"
description p_details[:description]
end
# Register the provider
plugin_klass.provider(p_name.to_sym, priority: p_details.fetch(:priority, 0)) do
provider_klass
end
# Setup any configuration support
ConfigPlugin.generate_config(client, p_name, plugin_klass, :provider)
# Register any guest capabilities
CapabilityPlugin.generate_guest_capabilities(client, plugin_klass, :provider)
# Register any host capabilities
CapabilityPlugin.generate_host_capabilities(client, plugin_klass, :provider)
# Register any provider capabilities
CapabilityPlugin.generate_provider_capabilities(client, plugin_klass, :provider)
logger.debug("completed loading provider go-plugin #{p_name}")
logger.info("loaded go-plugin provider - #{p_name}")
end
else
logger.warn("provider go-plugins have already been loaded. ignoring load request.")
end
end
# Load any detected synced folder plugins
def load_synced_folders
if !@_synced_folders_loaded
@_synced_folders_loaded = true
logger.debug("synced folder go-plugins have not been loaded... loading")
Array(list_synced_folders).each do |f_name, f_details|
logger.debug("loading go-plugin synced folder #{f_name}. details - #{f_details}")
client = Vagrant::Proto::SyncedFolder::Stub.new(
"#{p_details[:network]}://#{p_details[:address]}",
:this_channel_is_insecure)
# Create new synced folder class wrapper
folder_klass = Class.new(SyncedFolderPlugin::SyncedFolder)
folder_klass.plugin_client = client
# Create new plugin to register the synced folder
plugin_klass = Class.new(Vagrant.plugin("2"))
# Define the plugin
plugin_klass.class_eval do
name "#{f_name} Synced Folder"
description f_details[:description]
end
# Register the synced folder
plugin_klass.synced_folder(f_name.to_sym, priority: f_details.fetch(:priority, 10)) do
folder_klass
end
# Register any guest capabilities
CapabilityPlugin.generate_guest_capabilities(client, plugin_klass, :synced_folder)
# Register any host capabilities
CapabilityPlugin.generate_host_capabilities(client, plugin_klass, :synced_folder)
# Register any provider capabilities
CapabilityPlugin.generate_provider_capabilities(client, plugin_klass, :synced_folder)
logger.debug("completed loading synced folder go-plugin #{f_name}")
logger.info("loaded go-plugin synced folder - #{f_name}")
end
else
logger.warn("synced folder go-plugins have already been loaded. ignoring load request.")
end
end
end
end
end

View File

@ -0,0 +1,87 @@
require "zip"
require "vagrant/plugin/state_file"
module Vagrant
module GoPlugin
class Manager
include Util::Logger
# @return [Manager]
def self.instance
if !@instance
@instance = self.new
end
@instance
end
# @return [StateFile] user defined plugins
attr_reader :user_file
# @return [StateFile, nil] project local defined plugins
attr_reader :local_file
def initialize
FileUtils.mkdir_p(INSTALL_DIRECTORY)
FileUtils.mkdir_p(Vagrant.user_data_path.join("tmp").to_s)
@user_file = Plugin::StateFile.new(Vagrant.user_data_path.join("plugins.json"))
end
# Load global plugins
def globalize!
Dir.glob(File.join(INSTALL_DIRECTORY, "*")).each do |entry|
next if !File.directory?(entry)
logger.debug("loading go plugins from directory: #{entry}")
GoPlugin.interface.load_plugins(entry)
end
GoPlugin.interface.register_plugins
end
# Load local plugins
def localize!
raise NotImplementedError
end
# Install a go plugin
#
# @param [String] plugin_name Name of plugin
# @param [String] remote_source Location of plugin for download
# @param [Hash] options Currently unused
def install_plugin(plugin_name, remote_source, options={})
install_dir = File.join(INSTALL_DIRECTORY, plugin_name)
FileUtils.mkdir_p(install_dir)
Dir.mktmpdir("go-plugin", Vagrant.user_data_path.join("tmp").to_s) do |dir|
dest_file = File.join(dir, "plugin.zip")
logger.debug("downloading go plugin #{plugin_name} from #{remote_source}")
Util::Downloader.new(remote_source, dest_file).download!
logger.debug("extracting go plugin #{plugin_name} from #{dest_file}")
Zip::File.open(dest_file) do |zfile|
zfile.each do |entry|
install_path = File.join(install_dir, entry.name)
if File.file?(install_path)
logger.warn("removing existing plugin path for unpacking - #{install_path}")
File.delete(install_path)
end
entry.extract(install_path)
FileUtils.chmod(0755, install_path)
end
end
end
user_file.add_go_plugin(plugin_name, source: remote_source)
end
# Uninstall a go plugin
#
# @param [String] plugin_name Name of plugin
# @param [Hash] options Currently unused
def uninstall_plugin(plugin_name, options={})
plugin_path = File.join(INSTALL_DIRECTORY, plugin_name)
if !File.directory?(plugin_path)
logger.warn("Plugin directory does not exist for #{plugin_name} - #{plugin_path}")
else
logger.debug("deleting go plugin from path #{plugin_path}")
FileUtils.rm_rf(plugin_path)
end
user_file.remove_go_plugin(plugin_name)
end
end
end
end

View File

@ -0,0 +1,171 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
module ProviderPlugin
# Helper class for wrapping actions in a go-plugin into
# something which can be used by Vagrant::Action::Builder
class Action
include GRPCPlugin
# @return [String] action name associated to this class
def self.action_name
@action_name
end
# Set the action name for this class
#
# @param [String] n action name
# @return [String]
# @note can only be set once
def self.action_name=(n)
if @action_name
raise ArgumentError.new("Class action name has already been set")
end
@action_name = n.to_s.dup.freeze
end
def initialize(app, env)
@app = app
end
# Run the action
def call(env)
if env.is_a?(Hash) && !env.is_a?(Vagrant::Util::HashWithIndifferentAccess)
env = Vagrant::Util::HashWithIndifferentAccess.new(env)
end
machine = env.fetch(:machine, {})
response = plugin_client.run_action(
Vagrant::Proto::ExecuteAction.new(
name: self.class.action_name,
data: JSON.dump(env),
machine: JSON.dump(machine)))
result = JSON.load(response.result)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
result.each_pair do |k, v|
env[k] = v
end
end
@app.call(env)
end
end
# Helper class used to provide a wrapper around a go-plugin
# provider so that it can be interacted with normally within
# Vagrant
class Provider < Vagrant.plugin("2", :provider)
include GRPCPlugin
# @return [Vagrant::Machine]
attr_reader :machine
def initialize(machine)
@machine = machine
end
# @return [String] name of the provider plugin for this class
def name
if !@_name
@_name = plugin_client.name(Vagrant::Proto::Empty.new).name
end
@_name
end
# Get callable action by name
#
# @param [Symbol] name name of the action
# @return [Class] callable action class
def action(name)
result = plugin_client.action(
Vagrant::Proto::GenericAction.new(
name: name.to_s,
machine: JSON.dump(machine)))
klasses = result.items.map do |klass_name|
if klass_name.start_with?("self::")
action_name = klass_name.split("::", 2).last
klass = Class.new(Action)
klass.plugin_client = plugin_client
klass.action_name = action_name
klass.class_eval do
def self.name
action_name.capitalize.tr("_", "")
end
end
klass
else
klass_name.split("::").inject(Object) do |memo, const|
if memo.const_defined?(const)
memo.const_get(const)
else
raise NameError, "Unknown action class `#{klass_name}`"
end
end
end
end
Vagrant::Action::Builder.new.tap do |builder|
klasses.each do |action_class|
builder.use action_class
end
end
end
# Execute capability with given name
#
# @param [Symbol] cap_name Name of the capability
# @return [Object]
def capability(cap_name, *args)
r = plugin_client.provider_capability(
Vagrant::Proto::ProviderCapabilityRequest.new(
capability: Vagrant::Proto::ProviderCapability.new(
name: cap_name.to_s,
provider: name
),
machine: JSON.dump(machine),
arguments: JSON.dump(args)
)
)
result = JSON.load(r.result)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
end
# @return [Boolean] provider is installed
def is_installed?
plugin_client.is_installed(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).result
end
# @return [Boolean] provider is usable
def is_usable?
plugin_client.is_usable(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).result
end
# @return [nil]
def machine_id_changed
plugin_client.machine_id_changed(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine)))
nil
end
# @return [Hash] SSH information
def ssh_info
result = plugin_client.ssh_info(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).to_hash
Vagrant::Util::HashWithIndifferentAccess.new(result)
end
# @return [Vagrant::MachineState]
def state
result = plugin_client.state(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine)))
Vagrant::MachineState.new(result.id,
result.short_description, result.long_description)
end
end
end
end
end

View File

@ -0,0 +1,89 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
# Contains all synced folder functionality for go-plugin
module SyncedFolderPlugin
# Helper class used to provide a wrapper around a go-plugin
# synced folder so that it can be interacted with normally
# within Vagrant
class SyncedFolder < Vagrant.plugin("2", :synced_folder)
include GRPCPlugin
# Cleanup synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] opts Folder options
def cleanup(machine, opts)
plugin_client.cleanup(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
options: JSON.dump(opts),
folders: JSON.dump({})))
nil
end
# Disable synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] folders Folders to enable
# @param [Hash] opts Folder options
def disable(machine, folders, opts)
plugin_client.disable(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
folders: JSON.dump(folders),
options: JSON.dump(opts)))
nil
end
# Enable synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] folders Folders to enable
# @param [Hash] opts Folder options
def enable(machine, folders, opts)
plugin_client.enable(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
folders: JSON.dump(folders),
options: JSON.dump(options)))
nil
end
# Prepare synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] folders Folders to enable
# @param [Hash] opts Folder options
def prepare(machine, folders, opts)
plugin_client.prepare(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
folders: JSON.dump(folders),
options: JSON.dump(options)))
nil
end
# Check if plugin is usable
#
# @param [Vagrant::Machine] machine Vagrant guest
# @return [Boolean]
def usable?(machine, raise_error=false)
plugin_client.is_usable(
Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).result
end
# @return [String]
def name
if !@_name
@_name = plugin_client.name(Vagrant::Proto::Empty.new).name
end
@_name
end
end
end
end
end

View File

@ -0,0 +1,122 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: vagrant.proto
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("vagrant.proto", :syntax => :proto3) do
add_message "vagrant.proto.Empty" do
end
add_message "vagrant.proto.Machine" do
optional :machine, :string, 1
end
add_message "vagrant.proto.Valid" do
optional :result, :bool, 1
end
add_message "vagrant.proto.Identifier" do
optional :name, :string, 1
end
add_message "vagrant.proto.PluginInfo" do
optional :description, :string, 1
optional :priority, :int64, 2
end
add_message "vagrant.proto.Content" do
optional :target, :string, 1
optional :value, :string, 2
end
add_message "vagrant.proto.WriteResponse" do
optional :length, :int32, 1
end
add_message "vagrant.proto.SystemCapability" do
optional :name, :string, 1
optional :platform, :string, 2
end
add_message "vagrant.proto.ProviderCapability" do
optional :name, :string, 1
optional :provider, :string, 2
end
add_message "vagrant.proto.SystemCapabilityList" do
repeated :capabilities, :message, 1, "vagrant.proto.SystemCapability"
end
add_message "vagrant.proto.ProviderCapabilityList" do
repeated :capabilities, :message, 1, "vagrant.proto.ProviderCapability"
end
add_message "vagrant.proto.GenericResponse" do
optional :result, :string, 1
end
add_message "vagrant.proto.GuestCapabilityRequest" do
optional :capability, :message, 1, "vagrant.proto.SystemCapability"
optional :machine, :string, 2
optional :arguments, :string, 3
end
add_message "vagrant.proto.HostCapabilityRequest" do
optional :capability, :message, 1, "vagrant.proto.SystemCapability"
optional :environment, :string, 2
optional :arguments, :string, 3
end
add_message "vagrant.proto.ProviderCapabilityRequest" do
optional :capability, :message, 1, "vagrant.proto.ProviderCapability"
optional :machine, :string, 2
optional :arguments, :string, 3
end
add_message "vagrant.proto.Configuration" do
optional :data, :string, 1
optional :machine, :string, 2
end
add_message "vagrant.proto.ListResponse" do
repeated :items, :string, 1
end
add_message "vagrant.proto.SyncedFolders" do
optional :machine, :string, 1
optional :folders, :string, 2
optional :options, :string, 3
end
add_message "vagrant.proto.GenericAction" do
optional :name, :string, 1
optional :machine, :string, 2
end
add_message "vagrant.proto.ExecuteAction" do
optional :name, :string, 1
optional :data, :string, 2
optional :machine, :string, 3
end
add_message "vagrant.proto.MachineSshInfo" do
optional :host, :string, 1
optional :port, :int64, 2
optional :private_key_path, :string, 3
optional :username, :string, 4
end
add_message "vagrant.proto.MachineState" do
optional :id, :string, 1
optional :short_description, :string, 2
optional :long_description, :string, 3
end
end
end
module Vagrant
module Proto
Empty = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Empty").msgclass
Machine = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Machine").msgclass
Valid = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Valid").msgclass
Identifier = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Identifier").msgclass
PluginInfo = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.PluginInfo").msgclass
Content = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Content").msgclass
WriteResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.WriteResponse").msgclass
SystemCapability = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SystemCapability").msgclass
ProviderCapability = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapability").msgclass
SystemCapabilityList = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SystemCapabilityList").msgclass
ProviderCapabilityList = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapabilityList").msgclass
GenericResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GenericResponse").msgclass
GuestCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GuestCapabilityRequest").msgclass
HostCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.HostCapabilityRequest").msgclass
ProviderCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapabilityRequest").msgclass
Configuration = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Configuration").msgclass
ListResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ListResponse").msgclass
SyncedFolders = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SyncedFolders").msgclass
GenericAction = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GenericAction").msgclass
ExecuteAction = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ExecuteAction").msgclass
MachineSshInfo = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.MachineSshInfo").msgclass
MachineState = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.MachineState").msgclass
end
end

View File

@ -0,0 +1,223 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# Source: vagrant.proto for package 'vagrant.proto'
require 'grpc'
require_relative 'vagrant_pb'
module Vagrant
module Proto
module IO
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.IO'
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module GuestCapabilities
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.GuestCapabilities'
rpc :GuestCapabilities, Empty, SystemCapabilityList
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module HostCapabilities
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.HostCapabilities'
rpc :HostCapabilities, Empty, SystemCapabilityList
rpc :HostCapability, HostCapabilityRequest, GenericResponse
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module ProviderCapabilities
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.ProviderCapabilities'
rpc :ProviderCapabilities, Empty, ProviderCapabilityList
rpc :ProviderCapability, ProviderCapabilityRequest, GenericResponse
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module Config
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.Config'
rpc :ConfigAttributes, Empty, ListResponse
rpc :ConfigLoad, Configuration, Configuration
rpc :ConfigValidate, Configuration, ListResponse
rpc :ConfigFinalize, Configuration, Configuration
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module SyncedFolder
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.SyncedFolder'
rpc :Cleanup, SyncedFolders, Empty
rpc :Disable, SyncedFolders, Empty
rpc :Enable, SyncedFolders, Empty
rpc :Info, Empty, PluginInfo
rpc :IsUsable, Machine, Valid
rpc :Name, Empty, Identifier
rpc :Prepare, SyncedFolders, Empty
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
# Guest capabilities helpers (copied from GuestCapabilities service)
rpc :GuestCapabilities, Empty, SystemCapabilityList
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
# Host capabilities helpers (copied from GuestCapabilities service)
rpc :HostCapabilities, Empty, SystemCapabilityList
rpc :HostCapability, HostCapabilityRequest, GenericResponse
end
Stub = Service.rpc_stub_class
end
module Provider
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.Provider'
rpc :Action, GenericAction, ListResponse
rpc :Info, Empty, PluginInfo
rpc :IsInstalled, Machine, Valid
rpc :IsUsable, Machine, Valid
rpc :MachineIdChanged, Machine, Machine
rpc :Name, Empty, Identifier
rpc :RunAction, ExecuteAction, GenericResponse
rpc :SshInfo, Machine, MachineSshInfo
rpc :State, Machine, MachineState
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
# Config helpers (copied from Config service)
rpc :ConfigAttributes, Empty, ListResponse
rpc :ConfigLoad, Configuration, Configuration
rpc :ConfigValidate, Configuration, ListResponse
rpc :ConfigFinalize, Configuration, Configuration
# Guest capabilities helpers (copied from GuestCapabilities service)
rpc :GuestCapabilities, Empty, SystemCapabilityList
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
# Host capabilities helpers (copied from HostCapabilities service)
rpc :HostCapabilities, Empty, SystemCapabilityList
rpc :HostCapability, HostCapabilityRequest, GenericResponse
# Provider capabilities helpers (copied from ProviderCapabilities service)
rpc :ProviderCapabilities, Empty, ProviderCapabilityList
rpc :ProviderCapability, ProviderCapabilityRequest, GenericResponse
end
Stub = Service.rpc_stub_class
end
end
end
require 'logger'
# DebugIsTruncated extends the default Logger to truncate debug messages
class DebugIsTruncated < Logger
def debug(s)
super(truncate(s, 1024))
end
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
#
# 'Once upon a time in a world far far away'.truncate(27)
# # => "Once upon a time in a wo..."
#
# Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
#
# 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
# # => "Once upon a time in a..."
#
# 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
# # => "Once upon a time in a..."
#
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
# for a total length not exceeding <tt>length</tt>:
#
# 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
# # => "And they f... (continued)"
def truncate(s, truncate_at, options = {})
return s unless s.length > truncate_at
omission = options[:omission] || '...'
with_extra_room = truncate_at - omission.length
stop = \
if options[:separator]
rindex(options[:separator], with_extra_room) || with_extra_room
else
with_extra_room
end
"#{s[0, stop]}#{omission}"
end
end
# RubyLogger defines a logger for gRPC based on the standard ruby logger.
module RubyLogger
def logger
LOGGER
end
LOGGER = DebugIsTruncated.new(STDOUT)
LOGGER.level = Logger::DEBUG
end
# GRPC is the general RPC module
module GRPC
# Inject the noop #logger if no module-level logger method has been injected.
extend RubyLogger
end

View File

@ -578,6 +578,20 @@ module Vagrant
end
end
# @return [String]
def to_json(*args)
{
box: box,
config: config,
data_dir: data_dir,
environment: env,
id: id,
name: name,
provider_config: provider_config,
provider_name: provider_name
}.to_json(*args)
end
protected
# Returns the path to the file that stores the UID.

View File

@ -28,6 +28,7 @@ module Vagrant
@data["version"] ||= "1"
@data["installed"] ||= {}
@data["go_plugin"] ||= {}
end
# Add a plugin that is installed to the state file.
@ -47,6 +48,43 @@ module Vagrant
save!
end
# Add a go plugin that is installed to the state file
#
# @param [String, Symbol] name Plugin name
def add_go_plugin(name, **opts)
@data["go_plugin"][name] = {
"source" => opts[:source]
}
save!
end
# Remove a go plugin that is installed from the state file
#
# @param [String, Symbol] name Name of the plugin
def remove_go_plugin(name)
@data["go_plugin"].delete(name.to_s)
save!
end
# Check if go plugin is installed from the state file
#
# @param [String, Symbol] name Plugin name
# @return [Boolean]
def has_go_plugin?(name)
@data["go_plugin"].key?(name.to_s)
end
# This returns a hash of installed go plugins according to the state
# file. Note that this may _not_ directly match over to actually
# installed plugins
#
# @return [Hash]
def installed_go_plugins
@data["go_plugin"]
end
# Adds a RubyGems index source to look up gems.
#
# @param [String] url URL of the source.

View File

@ -138,6 +138,12 @@ module Vagrant
@__finalized = true
end
end
class Set < ::Set
def to_json(*args)
self.to_a.to_json(*args)
end
end
end
end
end

View File

@ -329,15 +329,10 @@ module Vagrant
target = opts[:target] if opts.key?(:target)
target = "#{target}:" if target != ""
# Get the lines. The first default is because if the message
# is an empty string, then we want to still use the empty string.
lines = [message]
if message != ""
lines = [].tap do |l|
message.scan(/(.*?)(\n|$)/).each do |m|
l << m.first if m.first != "" || (m.first == "" && m.last == "\n")
end
end
lines << "" if message.end_with?("\n")
end
lines = message.split("\n") if message != ""
# Otherwise, make sure to prefix every line properly
lines.map do |line|

Some files were not shown because too many files have changed in this diff Show More