534 lines
13 KiB
Go
534 lines
13 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"os/user"
|
|
"reflect"
|
|
"sort"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/component"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/core"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/types"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
|
|
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
|
|
)
|
|
|
|
type Machine struct {
|
|
*Target
|
|
box *Box
|
|
machine *vagrant_server.Target_Machine
|
|
logger hclog.Logger
|
|
cache cacher.Cache
|
|
vagrantfile *Vagrantfile
|
|
}
|
|
|
|
// Close implements core.Machine
|
|
func (m *Machine) Close() (err error) {
|
|
return
|
|
}
|
|
|
|
// ID implements core.Machine
|
|
func (m *Machine) ID() (id string, err error) {
|
|
return m.machine.Id, nil
|
|
}
|
|
|
|
// SetID implements core.Machine
|
|
func (m *Machine) SetID(value string) (err error) {
|
|
m.machine.Id = value
|
|
|
|
// Also set uid
|
|
user, err := user.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.machine.Uid = user.Uid
|
|
|
|
// Persist changes
|
|
if value == "" {
|
|
m.target.Record = nil
|
|
err = m.Destroy()
|
|
} else {
|
|
err = m.SaveMachine()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (m *Machine) Box() (b core.Box, err error) {
|
|
if m.box == nil {
|
|
boxes, _ := m.project.Boxes()
|
|
boxName, err := m.vagrantfile.GetValue("vm", "box")
|
|
if err != nil {
|
|
m.logger.Error("failed to get machine box name from config",
|
|
"error", err,
|
|
)
|
|
|
|
return nil, err
|
|
}
|
|
provider, err := m.ProviderName()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := boxes.Find(boxName.(string), "", provider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if b == nil {
|
|
return &Box{
|
|
basis: m.project.basis,
|
|
box: &vagrant_server.Box{
|
|
Name: boxName.(string),
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
m.machine.Box = b.(*Box).ToProto()
|
|
m.SaveMachine()
|
|
m.box = b.(*Box)
|
|
}
|
|
|
|
return m.box, nil
|
|
}
|
|
|
|
// Guest implements core.Machine
|
|
func (m *Machine) Guest() (g core.Guest, err error) {
|
|
comm, err := m.Communicate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isReady, err := comm.Ready(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isReady {
|
|
return nil, fmt.Errorf("unable to communicate with guest")
|
|
}
|
|
defer func() {
|
|
if g != nil {
|
|
err = seedPlugin(g, m)
|
|
if err == nil {
|
|
m.cache.Register("guest", g)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Note that if we get the guest value from the
|
|
// local cache, we return it directly to prevent
|
|
// the seeding and cache registration from happening
|
|
// again.
|
|
i := m.cache.Get("guest")
|
|
if i != nil {
|
|
return i.(core.Guest), nil
|
|
}
|
|
|
|
// Check if a guest is provided by the Vagrantfile. If it is, then try
|
|
// to use that guest
|
|
vg, err := m.vagrantfile.GetValue("vm", "guest")
|
|
if err != nil {
|
|
m.logger.Trace("failed to get guest value from vagrantfile",
|
|
"error", err,
|
|
)
|
|
} else {
|
|
guestName, ok := vg.(string)
|
|
if ok {
|
|
var guest *Component
|
|
guest, err = m.project.basis.component(m.ctx, component.GuestType, guestName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g = guest.Value.(core.Guest)
|
|
return
|
|
} else {
|
|
m.logger.Debug("guest name was not a valid string value",
|
|
"guest", vg,
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
// Try to detect a guest
|
|
guests, err := m.project.basis.typeComponents(m.ctx, component.GuestType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
names := make([]string, 0, len(guests))
|
|
pcount := map[string]int{}
|
|
|
|
for name, g := range guests {
|
|
names = append(names, name)
|
|
pcount[name] = g.plugin.ParentCount()
|
|
}
|
|
|
|
sort.Slice(names, func(i, j int) bool {
|
|
in := names[i]
|
|
jn := names[j]
|
|
// TODO check values exist before use
|
|
return pcount[in] > pcount[jn]
|
|
})
|
|
|
|
for _, name := range names {
|
|
guest := guests[name].Value.(core.Guest)
|
|
detected, gerr := guest.Detect(m.toTarget())
|
|
if gerr != nil {
|
|
m.logger.Error("guest error on detection check",
|
|
"plugin", name,
|
|
"type", "Guest",
|
|
"error", err)
|
|
|
|
continue
|
|
}
|
|
if detected {
|
|
m.logger.Info("guest detection complete",
|
|
"name", name,
|
|
)
|
|
g = guest
|
|
return
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to detect guest plugin for current platform")
|
|
}
|
|
|
|
func (m *Machine) Inspect() (printable string, err error) {
|
|
name, err := m.Name()
|
|
provider, err := m.Provider()
|
|
printable = "#<" + reflect.TypeOf(m).String() + ": " + name + " (" + reflect.TypeOf(provider).String() + ")>"
|
|
return
|
|
}
|
|
|
|
// ConnectionInfo implements core.Machine
|
|
func (m *Machine) ConnectionInfo() (info *core.ConnectionInfo, err error) {
|
|
// TODO: need Vagrantfile
|
|
return
|
|
}
|
|
|
|
// MachineState implements core.Machine
|
|
func (m *Machine) MachineState() (state *core.MachineState, err error) {
|
|
p, err := m.Provider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.State()
|
|
}
|
|
|
|
// SetMachineState implements core.Machine
|
|
func (m *Machine) SetMachineState(state *core.MachineState) (err error) {
|
|
var st *vagrant_plugin_sdk.Args_Target_Machine_State
|
|
mapstructure.Decode(state, &st)
|
|
m.machine.State = st
|
|
|
|
switch st.Id {
|
|
case "not_created":
|
|
m.target.State = vagrant_server.Operation_UNKNOWN
|
|
case "running":
|
|
m.target.State = vagrant_server.Operation_CREATED
|
|
case "poweroff":
|
|
m.target.State = vagrant_server.Operation_DESTROYED
|
|
case "pending":
|
|
m.target.State = vagrant_server.Operation_PENDING
|
|
default:
|
|
m.target.State = vagrant_server.Operation_UNKNOWN
|
|
}
|
|
|
|
return m.SaveMachine()
|
|
}
|
|
|
|
func (m *Machine) UID() (userId string, err error) {
|
|
return m.machine.Uid, nil
|
|
}
|
|
|
|
func StringToPathFunc() mapstructure.DecodeHookFunc {
|
|
return func(
|
|
f reflect.Type,
|
|
t reflect.Type,
|
|
data interface{}) (interface{}, error) {
|
|
if f.Kind() != reflect.String {
|
|
return data, nil
|
|
}
|
|
if !t.Implements(reflect.TypeOf((*path.Path)(nil)).Elem()) {
|
|
return data, nil
|
|
}
|
|
|
|
// Convert it
|
|
return path.NewPath(data.(string)), nil
|
|
}
|
|
}
|
|
|
|
func (m *Machine) defaultSyncedFolderType() (folderType *string, err error) {
|
|
logger := m.logger.Named("default-synced-folder-type")
|
|
|
|
// Get all available synced folder plugins
|
|
sfPlugins, err := m.project.basis.plugins.ListPlugins("synced_folder")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Load full plugins so we can read options
|
|
for i, sfp := range sfPlugins {
|
|
fullPlugin, err := m.project.basis.plugins.GetPlugin(sfp.Name, sfp.Type)
|
|
if err != nil {
|
|
logger.Error("error while loading synced folder plugin: name", sfp.Name, "err", err)
|
|
return nil, fmt.Errorf("error while loading synced folder plugin: name = %s, err = %s", sfp.Name, err)
|
|
}
|
|
sfPlugins[i] = fullPlugin
|
|
}
|
|
|
|
// Sort by plugin priority. Higher is first
|
|
sort.SliceStable(sfPlugins, func(i, j int) bool {
|
|
iPriority := sfPlugins[i].Options.(*component.SyncedFolderOptions).Priority
|
|
jPriority := sfPlugins[j].Options.(*component.SyncedFolderOptions).Priority
|
|
return iPriority > jPriority
|
|
})
|
|
|
|
logger.Debug("sorted synced folder plugins", "names", sfPlugins)
|
|
|
|
allowedTypesRaw, err := m.vagrantfile.GetValue("vm", "allowed_synced_folder_types")
|
|
if err != nil {
|
|
m.logger.Warn("failed to fetch allowed synced folder types, ignoring",
|
|
"error", err,
|
|
)
|
|
err = nil
|
|
} else {
|
|
allowedTypes, ok := allowedTypesRaw.([]interface{})
|
|
if !ok {
|
|
m.logger.Warn("unexpected type for allowed synced folder types",
|
|
"type", hclog.Fmt("%T", allowedTypesRaw),
|
|
)
|
|
}
|
|
// Remove unallowed types
|
|
if len(allowedTypes) > 0 {
|
|
allowed := make(map[string]struct{})
|
|
for _, a := range allowedTypes {
|
|
typ, err := optionToString(a)
|
|
if err != nil {
|
|
m.logger.Warn("failed to convert synced folder type to string",
|
|
"type", hclog.Fmt("%T", a),
|
|
)
|
|
}
|
|
allowed[typ] = struct{}{}
|
|
}
|
|
k := 0
|
|
for _, sfp := range sfPlugins {
|
|
if _, ok := allowed[sfp.Name]; ok {
|
|
sfPlugins[k] = sfp
|
|
k++
|
|
} else {
|
|
logger.Debug("removing disallowed plugin", "type", sfp.Name)
|
|
}
|
|
}
|
|
sfPlugins = sfPlugins[:k]
|
|
}
|
|
}
|
|
// Check for first usable plugin
|
|
for _, sfp := range sfPlugins {
|
|
syncedFolder := sfp.Plugin.(core.SyncedFolder)
|
|
usable, err := syncedFolder.Usable(m)
|
|
if err != nil {
|
|
logger.Error("error on usable check", "plugin", sfp.Name, "error", err)
|
|
continue
|
|
}
|
|
if usable {
|
|
logger.Info("returning default", "name", sfp.Name)
|
|
return &sfp.Name, nil
|
|
} else {
|
|
logger.Debug("skipping unusable plugin", "name", sfp.Name)
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to detect guest plugin for current platform")
|
|
}
|
|
|
|
// SyncedFolders implements core.Machine
|
|
func (m *Machine) SyncedFolders() (folders []*core.MachineSyncedFolder, err error) {
|
|
syncedFoldersRaw, err := m.vagrantfile.GetValue("vm", "__synced_folders")
|
|
if err != nil {
|
|
m.logger.Error("failed to load synced folders",
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
tmpFolders, ok := syncedFoldersRaw.(map[interface{}]interface{})
|
|
if !ok {
|
|
m.logger.Error("synced folders configuration is unexpected type",
|
|
"type", hclog.Fmt("%T", syncedFoldersRaw),
|
|
)
|
|
return nil, fmt.Errorf("invalid configuration type for synced folders")
|
|
}
|
|
|
|
syncedFolders := map[string]map[interface{}]interface{}{}
|
|
|
|
for k, v := range tmpFolders {
|
|
var key string
|
|
var ok bool
|
|
if key, ok = k.(string); !ok {
|
|
if skey, ok := k.(types.Symbol); ok {
|
|
key = string(skey)
|
|
} else {
|
|
m.logger.Error("invalid key type for synced folders",
|
|
"key", k,
|
|
"type", hclog.Fmt("%T", k),
|
|
)
|
|
|
|
return nil, fmt.Errorf("invalid configuration type for synced folder key")
|
|
}
|
|
}
|
|
value, ok := v.(map[interface{}]interface{})
|
|
if !ok {
|
|
m.logger.Error("invalid value type for synced folders",
|
|
"type", hclog.Fmt("%T", v),
|
|
)
|
|
}
|
|
|
|
syncedFolders[key] = value
|
|
}
|
|
|
|
for _, options := range syncedFolders {
|
|
var ftype string
|
|
typeRaw, ok := getOptionValue("type", options)
|
|
if ok {
|
|
if ftype, err = optionToString(typeRaw); err != nil {
|
|
m.logger.Debug("failed to convert folder type to string",
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
if ftype == "" {
|
|
ftype, err = m.project.DefaultProvider(
|
|
&core.DefaultProviderOptions{
|
|
CheckUsable: false,
|
|
MachineName: m.target.Name,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
lookup := "syncedfolder_" + ftype
|
|
v := m.cache.Get(lookup)
|
|
if v == nil {
|
|
plg, err := m.project.basis.component(m.ctx, component.SyncedFolderType, ftype)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v = plg.Value.(core.SyncedFolder)
|
|
m.cache.Register(lookup, v)
|
|
}
|
|
|
|
if err = seedPlugin(v, m); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var guestPath, hostPath path.Path
|
|
guestPathRaw, ok := getOptionValue("guestpath", options)
|
|
if !ok {
|
|
return nil, fmt.Errorf("synced folder options do not include guest path value")
|
|
}
|
|
hostPathRaw, ok := getOptionValue("hostpath", options)
|
|
if !ok {
|
|
return nil, fmt.Errorf("synced folder options do not include host path value")
|
|
}
|
|
if gps, err := optionToString(guestPathRaw); err == nil {
|
|
guestPath = path.NewPath(gps)
|
|
} else {
|
|
return nil, err
|
|
}
|
|
if hps, err := optionToString(hostPathRaw); err == nil {
|
|
hostPath = path.NewPath(hps)
|
|
} else {
|
|
return nil, err
|
|
}
|
|
|
|
opts := map[string]interface{}{}
|
|
for k, v := range options {
|
|
key, err := optionToString(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts[key] = v
|
|
}
|
|
|
|
f := &core.Folder{
|
|
Source: hostPath,
|
|
Destination: guestPath,
|
|
Options: opts,
|
|
}
|
|
|
|
folders = append(folders, &core.MachineSyncedFolder{
|
|
Plugin: v.(core.SyncedFolder),
|
|
Folder: f,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
func (m *Machine) SaveMachine() (err error) {
|
|
m.logger.Debug("saving machine to db", "machine", m.machine.Id)
|
|
// Update the target record and uuid to match the machine's new state
|
|
m.target.Record, err = anypb.New(m.machine)
|
|
m.target.Uuid = m.machine.Id
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return m.Save()
|
|
}
|
|
|
|
func (m *Machine) toTarget() core.Target {
|
|
return m
|
|
}
|
|
|
|
// Get option value from config map. Since keys in the config
|
|
// can be either string or types.Symbol, this helper function
|
|
// will check for either type being set
|
|
func getOptionValue(
|
|
name string, // name of option
|
|
options map[interface{}]interface{}, // options map from config
|
|
) (interface{}, bool) {
|
|
var key interface{}
|
|
key = name
|
|
result, ok := options[key]
|
|
if ok {
|
|
return result, true
|
|
}
|
|
key = types.Symbol(name)
|
|
result, ok = options[key]
|
|
if ok {
|
|
return result, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// Option values from the config which are expected to be string
|
|
// values may be a string or types.Symbol. This helper function
|
|
// will take the value and convert it into a string if possible.
|
|
func optionToString(
|
|
opt interface{}, // value to convert
|
|
) (result string, err error) {
|
|
result, ok := opt.(string)
|
|
if ok {
|
|
return
|
|
}
|
|
|
|
sym, ok := opt.(types.Symbol)
|
|
if !ok {
|
|
return result, fmt.Errorf("option value is not string type (%T)", opt)
|
|
}
|
|
result = string(sym)
|
|
|
|
return
|
|
}
|
|
|
|
var _ core.Machine = (*Machine)(nil)
|
|
var _ core.Target = (*Machine)(nil)
|