Files
virtual-kubelet/vendor/github.com/vmware/vic/cmd/vic-machine/create/create.go
Loc Nguyen 513cebe7b7 VMware vSphere Integrated Containers provider (#206)
* Add Virtual Kubelet provider for VIC

Initial virtual kubelet provider for VMware VIC.  This provider currently
handles creating and starting of a pod VM via the VIC portlayer and persona
server.  Image store handling via the VIC persona server.  This provider
currently requires the feature/wolfpack branch of VIC.

* Added pod stop and delete.  Also added node capacity.

Added the ability to stop and delete pod VMs via VIC.  Also retrieve
node capacity information from the VCH.

* Cleanup and readme file

Some file clean up and added a Readme.md markdown file for the VIC
provider.

* Cleaned up errors, added function comments, moved operation code

1. Cleaned up error handling.  Set standard for creating errors.
2. Added method prototype comments for all interface functions.
3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder.

* Add mocking code and unit tests for podcache, podcreator, and podstarter

Used the unit test framework used in VIC to handle assertions in the provider's
unit test.  Mocking code generated using OSS project mockery, which is compatible
with the testify assertion framework.

* Vendored packages for the VIC provider

Requires feature/wolfpack branch of VIC and a few specific commit sha of
projects used within VIC.

* Implementation of POD Stopper and Deleter unit tests (#4)

* Updated files for initial PR
2018-06-04 15:41:32 -07:00

790 lines
22 KiB
Go

// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package create
import (
"context"
"fmt"
"io"
"net"
"net/url"
"path"
"reflect"
"strings"
"time"
"gopkg.in/urfave/cli.v1"
"github.com/vmware/vic/cmd/vic-machine/common"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/install/data"
"github.com/vmware/vic/lib/install/management"
"github.com/vmware/vic/lib/install/validate"
"github.com/vmware/vic/lib/install/vchlog"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
)
const (
// Max permitted length of Virtual Machine name
MaxVirtualMachineNameLen = 80
// Max permitted length of Virtual Switch name
MaxDisplayNameLen = 31
)
var EntireOptionHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{end}}
`
// Create has all input parameters for vic-machine create command
type Create struct {
common.Networks
*data.Data
Certs common.CertFactory
containerNetworks common.CNetworks
Registries common.Registries
volumeStores common.VolumeStores
Nameservers common.DNS
memoryReservLimits string
cpuReservLimits string
advancedOptions bool
BridgeIPRange string
Proxies common.Proxies
SyslogAddr string
executor *management.Dispatcher
}
func NewCreate() *Create {
create := &Create{}
create.Data = data.NewData()
return create
}
// SetFields iterates through the fields in the Create struct, searching for fields
// tagged with the `arg` key. If the value of that tag matches the supplied `flag`
// string, a nil check is performed. If the field is not nil, then the user supplied
// this flag on the command line and we need to persist it.
// This is a workaround for cli.Context.IsSet() returning false when
// the short option for a cli.StringSlice is supplied instead of the long option.
// See https://github.com/urfave/cli/issues/314
func (c *Create) SetFields() map[string]struct{} {
result := make(map[string]struct{})
create := reflect.ValueOf(c).Elem()
for i := 0; i < create.NumField(); i++ {
t := create.Type().Field(i)
if tag := t.Tag.Get("arg"); tag != "" {
ss := create.Field(i)
if !ss.IsNil() {
result[tag] = struct{}{}
}
}
}
return result
}
// Flags return all cli flags for create
func (c *Create) Flags() []cli.Flag {
create := []cli.Flag{
// images
cli.StringFlag{
Name: "image-store, i",
Value: "",
Usage: "Image datastore path in format \"datastore/path\"",
Destination: &c.ImageDatastorePath,
},
cli.StringFlag{
Name: "base-image-size",
Value: constants.DefaultBaseImageScratchSize,
Usage: "Specify the size of the base image from which all other images are created e.g. 8GB/8000MB",
Destination: &c.ScratchSize,
Hidden: true,
},
}
networks := []cli.Flag{
// bridge
cli.StringFlag{
Name: "bridge-network, b",
Value: "",
Usage: "The bridge network port group name (private port group for containers). Defaults to the Virtual Container Host name",
Destination: &c.BridgeNetworkName,
},
cli.StringFlag{
Name: "bridge-network-range, bnr",
Value: "172.16.0.0/12",
Usage: "The IP range from which bridge networks are allocated",
Destination: &c.BridgeIPRange,
Hidden: true,
},
// client
cli.StringFlag{
Name: "client-network, cln",
Value: "",
Usage: "The client network port group name (restricts DOCKER_API access to this network). Defaults to DCHP - see advanced help (-x)",
Destination: &c.ClientNetworkName,
},
cli.StringFlag{
Name: "client-network-gateway",
Value: "",
Usage: "Gateway for the VCH on the client network, including one or more routing destinations in a comma separated list, e.g. 10.1.0.0/16,10.2.0.0/16:10.0.0.1",
Destination: &c.ClientNetworkGateway,
Hidden: true,
},
cli.StringFlag{
Name: "client-network-ip",
Value: "",
Usage: "IP address with a network mask for the VCH on the client network, e.g. 10.0.0.2/24",
Destination: &c.ClientNetworkIP,
Hidden: true,
},
// public
cli.StringFlag{
Name: "public-network, pn",
Value: "VM Network",
Usage: "The public network port group name (port forwarding and default route). Defaults to 'VM Network' and DHCP -- see advanced help (-x)",
Destination: &c.PublicNetworkName,
},
cli.StringFlag{
Name: "public-network-gateway",
Value: "",
Usage: "Gateway for the VCH on the public network, e.g. 10.0.0.1",
Destination: &c.PublicNetworkGateway,
Hidden: true,
},
cli.StringFlag{
Name: "public-network-ip",
Value: "",
Usage: "IP address with a network mask for the VCH on the public network, e.g. 10.0.1.2/24",
Destination: &c.PublicNetworkIP,
Hidden: true,
},
// management
cli.StringFlag{
Name: "management-network, mn",
Value: "",
Usage: "The management network port group name (provides route to target hosting vSphere). Defaults to DCHP - see advanced help (-x)",
Destination: &c.ManagementNetworkName,
},
cli.StringFlag{
Name: "management-network-gateway",
Value: "",
Usage: "Gateway for the VCH on the management network, including one or more routing destinations in a comma separated list, e.g. 10.1.0.0/16,10.2.0.0/16:10.0.0.1",
Destination: &c.ManagementNetworkGateway,
Hidden: true,
},
cli.StringFlag{
Name: "management-network-ip",
Value: "",
Usage: "IP address with a network mask for the VCH on the management network, e.g. 10.0.2.2/24",
Destination: &c.ManagementNetworkIP,
Hidden: true,
},
}
var memory, cpu []cli.Flag
memory = append(memory, c.VCHMemoryLimitFlags(true)...)
memory = append(memory,
cli.IntFlag{
Name: "endpoint-memory",
Value: constants.DefaultEndpointMemoryMB,
Usage: "Memory for the VCH endpoint VM, in MB. Does not impact resources allocated per container.",
Hidden: true,
Destination: &c.MemoryMB,
})
cpu = append(cpu, c.VCHCPULimitFlags(true)...)
cpu = append(cpu,
cli.IntFlag{
Name: "endpoint-cpu",
Value: 1,
Usage: "vCPUs for the VCH endpoint VM. Does not impact resources allocated per container.",
Hidden: true,
Destination: &c.NumCPUs,
})
tls := c.Certs.CertFlags()
tls = append(tls, cli.BoolFlag{
Name: "no-tls, k",
Usage: "Disable TLS support completely",
Destination: &c.Certs.NoTLS,
Hidden: true,
})
registries := c.Registries.Flags()
registries = append(registries,
cli.StringSliceFlag{
Name: "insecure-registry, dir",
Value: &c.Registries.InsecureRegistriesArg,
Usage: "Specify a list of permitted insecure registry server addresses",
})
registries = append(registries,
cli.StringSliceFlag{
Name: "whitelist-registry, wr",
Value: &c.Registries.WhitelistRegistriesArg,
Usage: "Specify a list of permitted whitelist registry server addresses (insecure addresses still require the --insecure-registry option in addition)",
})
syslog := []cli.Flag{
cli.StringFlag{
Name: "syslog-address",
Value: "",
Usage: "Address of the syslog server to send Virtual Container Host logs to. Must be in the format transport://host[:port], where transport is udp or tcp. port defaults to 514 if not specified",
Destination: &c.SyslogAddr,
Hidden: true,
},
}
util := []cli.Flag{
// miscellaneous
cli.BoolFlag{
Name: "force, f",
Usage: "Ignore error messages and proceed",
Destination: &c.Force,
},
cli.DurationFlag{
Name: "timeout",
Value: 3 * time.Minute,
Usage: "Time to wait for create",
Destination: &c.Timeout,
},
cli.BoolFlag{
Name: "asymmetric-routes, ar",
Usage: "Set up the Virtual Container Host for asymmetric routing",
Destination: &c.AsymmetricRouting,
Hidden: true,
},
}
help := []cli.Flag{
// help options
cli.BoolFlag{
Name: "extended-help, x",
Usage: "Show all options - this must be specified instead of --help",
Destination: &c.advancedOptions,
},
}
target := c.TargetFlags()
ops := c.OpsCredentials.Flags(true)
compute := c.ComputeFlags()
container := c.ContainerFlags()
volume := c.volumeStores.Flags()
iso := c.ImageFlags(true)
cNetwork := c.containerNetworks.CNetworkFlags(true)
dns := c.Nameservers.DNSFlags(true)
proxies := c.Proxies.ProxyFlags(true)
kubelet := c.Kubelet.Flags(true)
debug := c.DebugFlags(true)
// flag arrays are declared, now combined
var flags []cli.Flag
for _, f := range [][]cli.Flag{target, compute, ops, create, container, volume, dns, networks, cNetwork, memory, cpu, tls, registries, proxies, syslog, iso, util, kubelet, debug, help} {
flags = append(flags, f...)
}
return flags
}
func (c *Create) ProcessParams(op trace.Operation) error {
defer trace.End(trace.Begin("", op))
if err := c.HasCredentials(op); err != nil {
return err
}
// prevent usage of special characters for certain user provided values
if err := common.CheckUnsupportedChars(c.DisplayName); err != nil {
return cli.NewExitError(fmt.Sprintf("--name contains unsupported characters: %s Allowed characters are alphanumeric, space and symbols - _ ( )", err), 1)
}
if len(c.DisplayName) > MaxDisplayNameLen {
return cli.NewExitError(fmt.Sprintf("Display name %s exceeds the permitted 31 characters limit. Please use a shorter -name parameter", c.DisplayName), 1)
}
if c.BridgeNetworkName == "" {
c.BridgeNetworkName = c.DisplayName
}
// Pass admin credentials for use as ops credentials if ops credentials are not supplied.
if err := c.OpsCredentials.ProcessOpsCredentials(op, true, c.Target.User, c.Target.Password); err != nil {
return err
}
if err := c.Kubelet.ProcessKubelet(op, true); err != nil {
return err
}
var err error
c.ContainerNetworks, err = c.containerNetworks.ProcessContainerNetworks(op)
if err != nil {
return err
}
if err = c.ProcessBridgeNetwork(); err != nil {
return err
}
if err = c.ProcessNetwork(op, &c.Data.ClientNetwork, "client", c.ClientNetworkName,
c.ClientNetworkIP, c.ClientNetworkGateway); err != nil {
return err
}
if err = c.ProcessNetwork(op, &c.Data.PublicNetwork, "public", c.PublicNetworkName,
c.PublicNetworkIP, c.PublicNetworkGateway); err != nil {
return err
}
if err = c.ProcessNetwork(op, &c.Data.ManagementNetwork, "management", c.ManagementNetworkName,
c.ManagementNetworkIP, c.ManagementNetworkGateway); err != nil {
return err
}
if c.DNS, err = c.Nameservers.ProcessDNSServers(op); err != nil {
return err
}
// must come after client network processing as it checks for static IP on that interface
if err = c.processCertificates(op); err != nil {
return err
}
if err = common.CheckUnsupportedCharsDatastore(c.ImageDatastorePath); err != nil {
return cli.NewExitError(fmt.Sprintf("--image-store contains unsupported characters: %s Allowed characters are alphanumeric, space and symbols - _ ( ) / :", err), 1)
}
c.VolumeLocations, err = c.volumeStores.ProcessVolumeStores()
if err != nil {
return err
}
if err = c.Registries.ProcessRegistries(op); err != nil {
return err
}
c.InsecureRegistries = c.Registries.InsecureRegistries
c.WhitelistRegistries = c.Registries.WhitelistRegistries
c.RegistryCAs = c.Registries.RegistryCAs
hproxy, sproxy, err := c.Proxies.ProcessProxies()
if err != nil {
return err
}
c.HTTPProxy = hproxy
c.HTTPSProxy = sproxy
if err = c.ProcessSyslog(); err != nil {
return err
}
return nil
}
func (c *Create) processCertificates(op trace.Operation) error {
// debuglevel is a pointer now so we have to do this song and dance
var debug int
if c.Debug.Debug == nil {
debug = 0
} else {
debug = *c.Debug.Debug
}
c.Certs.Networks = c.Networks
if err := c.Certs.ProcessCertificates(op, c.DisplayName, c.Force, debug); err != nil {
return err
}
// copy a few things out of seed because ProcessCertificates has side effects
c.KeyPEM = c.Certs.KeyPEM
c.CertPEM = c.Certs.CertPEM
c.ClientCAs = c.Certs.ClientCAs
return nil
}
func (c *Create) ProcessBridgeNetwork() error {
// bridge network params
var err error
_, c.Data.BridgeIPRange, err = net.ParseCIDR(c.BridgeIPRange)
if err != nil {
return cli.NewExitError(fmt.Sprintf("Error parsing bridge network ip range: %s. Range must be in CIDR format, e.g., 172.16.0.0/12", err), 1)
}
return nil
}
func parseGatewaySpec(gw string) (cidrs []net.IPNet, gwIP net.IPNet, err error) {
ss := strings.Split(gw, ":")
if len(ss) > 2 {
err = fmt.Errorf("gateway %s specified incorrectly", gw)
return
}
gwStr := ss[0]
cidrsStr := ""
if len(ss) > 1 {
gwStr = ss[1]
cidrsStr = ss[0]
}
if gwIP.IP = net.ParseIP(gwStr); gwIP.IP == nil {
err = fmt.Errorf("Provided gateway IP address is not valid: %s", gwStr)
}
if err != nil {
return
}
if cidrsStr != "" {
for _, c := range strings.Split(cidrsStr, ",") {
var ipnet *net.IPNet
_, ipnet, err = net.ParseCIDR(c)
if err != nil {
err = fmt.Errorf("invalid CIDR in gateway specification: %s", err)
return
}
cidrs = append(cidrs, *ipnet)
}
}
return
}
// ProcessNetwork parses network args if present
func (c *Create) ProcessNetwork(op trace.Operation, network *data.NetworkConfig, netName, pgName, staticIP, gateway string) error {
var err error
network.Name = pgName
if gateway != "" && staticIP == "" {
return fmt.Errorf("Gateway provided without static IP for %s network", netName)
}
defer func(net *data.NetworkConfig) {
if err == nil {
op.Debugf("%s network: IP %s gateway %s dest: %s", netName, net.IP, net.Gateway.IP, net.Destinations)
}
}(network)
var ipNet *net.IPNet
if staticIP != "" {
var ipAddr net.IP
ipAddr, ipNet, err = net.ParseCIDR(staticIP)
if err != nil {
return fmt.Errorf("Failed to parse the provided %s network IP address %s: %s", netName, staticIP, err)
}
network.IP.IP = ipAddr
network.IP.Mask = ipNet.Mask
}
if gateway != "" {
network.Destinations, network.Gateway, err = parseGatewaySpec(gateway)
if err != nil {
return fmt.Errorf("Invalid %s network gateway: %s", netName, err)
}
if !network.IP.Contains(network.Gateway.IP) {
return fmt.Errorf("%s gateway with IP %s is not reachable from %s", netName, network.Gateway.IP, ipNet.String())
}
// TODO(vburenin): this seems ugly, and it actually is. The reason is that a gateway required to specify
// a network mask for it, which is just not how network configuration should be done. Network mask has to
// be provided separately or with the IP address. It is hard to change all dependencies to keep mask
// with IP address, so it will be stored with network gateway as it was previously.
network.Gateway.Mask = network.IP.Mask
}
return nil
}
func (c *Create) ProcessSyslog() error {
if len(c.SyslogAddr) == 0 {
return nil
}
u, err := url.Parse(c.SyslogAddr)
if err != nil {
return err
}
c.SyslogConfig.Addr = u
return nil
}
func (c *Create) logArguments(op trace.Operation, cliContext *cli.Context) []string {
args := []string{}
sf := c.SetFields() // StringSlice options set by the user
for _, f := range cliContext.FlagNames() {
_, ok := sf[f]
if !cliContext.IsSet(f) && !ok {
continue
}
// avoid logging sensitive data
if f == "user" || f == "password" || f == "ops-password" {
op.Debugf("--%s=<censored>", f)
continue
}
if f == "tls-server-cert" || f == "tls-cert-path" || f == "tls-server-key" || f == "registry-ca" || f == "tls-ca" {
continue
}
if f == "target" {
url, err := url.Parse(cliContext.String(f))
if err != nil {
op.Debugf("Unable to re-parse target url for logging")
continue
}
url.User = nil
flag := fmt.Sprintf("--target=%s", url.String())
op.Debug(flag)
args = append(args, flag)
continue
}
i := cliContext.Int(f)
if i != 0 {
flag := fmt.Sprintf("--%s=%d", f, i)
op.Debug(flag)
args = append(args, flag)
continue
}
d := cliContext.Duration(f)
if d != 0 {
flag := fmt.Sprintf("--%s=%s", f, d.String())
op.Debug(flag)
args = append(args, flag)
continue
}
x := cliContext.Float64(f)
if x != 0 {
flag := fmt.Sprintf("--%s=%f", f, x)
op.Debug(flag)
args = append(args, flag)
continue
}
// check for StringSlice before String as the cli String checker
// will mistake a StringSlice for a String and jackaroo the formatting
match := func() (result bool) {
result = false
defer func() { recover() }()
ss := cliContext.StringSlice(f)
if ss != nil {
for _, o := range ss {
flag := fmt.Sprintf("--%s=%s", f, o)
op.Debug(flag)
args = append(args, flag)
}
}
return ss != nil
}()
if match {
continue
}
s := cliContext.String(f)
if s != "" {
flag := fmt.Sprintf("--%s=%s", f, s)
op.Debug(flag)
args = append(args, flag)
continue
}
b := cliContext.Bool(f)
bT := cliContext.BoolT(f)
if b && !bT {
flag := fmt.Sprintf("--%s=%t", f, true)
op.Debug(flag)
args = append(args, flag)
continue
}
match = func() (result bool) {
result = false
defer func() { recover() }()
is := cliContext.IntSlice(f)
if is != nil {
flag := fmt.Sprintf("--%s=%#v", f, is)
op.Debug(flag)
args = append(args, flag)
}
return is != nil
}()
if match {
continue
}
// generic last because it matches everything
g := cliContext.Generic(f)
if g != nil {
flag := fmt.Sprintf("--%s=%#v", f, g)
op.Debug(flag)
args = append(args, flag)
}
}
return args
}
func (c *Create) Run(clic *cli.Context) (err error) {
if c.advancedOptions {
cli.HelpPrinter(clic.App.Writer, EntireOptionHelpTemplate, clic.Command)
return nil
}
// create the logger for streaming VCH log messages
datastoreLog := vchlog.New()
defer func(old io.Writer) {
trace.Logger.Out = old
datastoreLog.Close()
}(trace.Logger.Out)
trace.Logger.Out = io.MultiWriter(trace.Logger.Out, datastoreLog.GetPipe())
go datastoreLog.Run()
// These operations will be executed without timeout
op := common.NewOperation(clic, c.Debug.Debug)
op.Infof("### Installing VCH ####")
defer func() {
// urfave/cli will print out exit in error handling, so no more information in main method can be printed out.
err = common.LogErrorIfAny(op, clic, err)
}()
if err = c.ProcessParams(op); err != nil {
return err
}
args := c.logArguments(op, clic)
var images map[string]string
if images, err = c.CheckImagesFiles(op, c.Force); err != nil {
return err
}
if len(clic.Args()) > 0 {
op.Errorf("Unknown argument: %s", clic.Args()[0])
return errors.New("invalid CLI arguments")
}
validator, err := validate.NewValidator(op, c.Data)
if err != nil {
op.Error("Create cannot continue: failed to create validator")
return err
}
defer validator.Session.Logout(op)
vchConfig, err := validator.Validate(op, c.Data)
if err != nil {
op.Error("Create cannot continue: configuration validation failed")
return err
}
// persist cli args used to create the VCH
vchConfig.VicMachineCreateOptions = args
vConfig := validator.AddDeprecatedFields(op, vchConfig, c.Data)
vConfig.ImageFiles = images
vConfig.ApplianceISO = path.Base(c.ApplianceISO)
vConfig.BootstrapISO = path.Base(c.BootstrapISO)
vConfig.HTTPProxy = c.HTTPProxy
vConfig.HTTPSProxy = c.HTTPSProxy
vConfig.Timeout = c.Data.Timeout
// separate initial validation from dispatch of creation task
op.Info("")
executor := management.NewDispatcher(op, validator.Session, vchConfig, c.Force)
if err = executor.CreateVCH(vchConfig, vConfig, datastoreLog); err != nil {
executor.CollectDiagnosticLogs()
op.Error(err)
return err
}
// Perform the remaining work using a context with a timeout to ensure the user does not wait forever
op, cancel := trace.WithTimeout(&op, c.Timeout, "Create")
defer cancel()
defer func() {
if op.Err() == context.DeadlineExceeded {
//context deadline exceeded, replace returned error message
err = errors.Errorf("Creating VCH exceeded time limit of %s. Please increase the timeout using --timeout to accommodate for a busy vSphere target", c.Timeout)
}
}()
if err = executor.CheckServiceReady(op, vchConfig, c.Certs.ClientCert); err != nil {
executor.CollectDiagnosticLogs()
cmd, _ := executor.GetDockerAPICommand(vchConfig, c.Certs.Ckey, c.Certs.Ccert, c.Certs.Cacert, c.Certs.CertPath)
op.Info("\tAPI may be slow to start - try to connect to API after a few minutes:")
if cmd != "" {
op.Infof("\t\tRun command: %s", cmd)
} else {
op.Infof("\t\tRun %s inspect to find API connection command and run the command if ip address is ready", clic.App.Name)
}
op.Info("\t\tIf command succeeds, VCH is started. If command fails, VCH failed to install - see documentation for troubleshooting.")
return err
}
op.Info("Initialization of appliance successful")
// We must check for the volume stores that are present after the portlayer presents.
executor.ShowVCH(vchConfig, c.Certs.Ckey, c.Certs.Ccert, c.Certs.Cacert, c.Certs.EnvFile, c.Certs.CertPath)
op.Info("Installer completed successfully")
go func() {
select {
case <-time.After(3 * time.Second):
op.Infof("Waiting for log upload to complete") // tell the user if the wait causes noticeable delay
case <-op.Done():
return
}
}()
// wait on the logger to finish streaming
datastoreLog.Close()
datastoreLog.Wait(op)
return nil
}