2015-08-25 23:13:33 +00:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
2015-11-17 03:30:37 +00:00
|
|
|
"fmt"
|
2015-08-25 23:13:33 +00:00
|
|
|
"io"
|
2016-05-31 18:58:02 +00:00
|
|
|
"os"
|
2015-11-17 03:30:37 +00:00
|
|
|
"strconv"
|
2015-11-24 15:18:49 +00:00
|
|
|
"strings"
|
2015-12-23 00:10:30 +00:00
|
|
|
"time"
|
2015-08-25 23:13:33 +00:00
|
|
|
|
2018-09-13 17:43:40 +00:00
|
|
|
log "github.com/hashicorp/go-hclog"
|
2019-01-09 18:57:56 +00:00
|
|
|
"github.com/hashicorp/nomad/client/state"
|
2017-01-18 23:55:14 +00:00
|
|
|
"github.com/hashicorp/nomad/helper"
|
2019-01-23 14:27:14 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/pluginutils/loader"
|
2015-08-25 23:13:33 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2016-05-24 04:28:12 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs/config"
|
2018-10-17 02:21:15 +00:00
|
|
|
"github.com/hashicorp/nomad/plugins/base"
|
2017-08-16 22:42:15 +00:00
|
|
|
"github.com/hashicorp/nomad/version"
|
2015-08-25 23:13:33 +00:00
|
|
|
)
|
|
|
|
|
2016-03-23 18:45:03 +00:00
|
|
|
var (
|
|
|
|
// DefaultEnvBlacklist is the default set of environment variables that are
|
|
|
|
// filtered when passing the environment variables of the host to a task.
|
|
|
|
DefaultEnvBlacklist = strings.Join([]string{
|
|
|
|
"CONSUL_TOKEN",
|
|
|
|
"VAULT_TOKEN",
|
|
|
|
"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN",
|
|
|
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
|
|
}, ",")
|
2016-03-24 17:55:14 +00:00
|
|
|
|
2018-03-11 17:52:58 +00:00
|
|
|
// DefaultUserBlacklist is the default set of users that tasks are not
|
2016-03-24 17:55:14 +00:00
|
|
|
// allowed to run as when using a driver in "user.checked_drivers"
|
|
|
|
DefaultUserBlacklist = strings.Join([]string{
|
|
|
|
"root",
|
|
|
|
"Administrator",
|
|
|
|
}, ",")
|
|
|
|
|
|
|
|
// DefaultUserCheckedDrivers is the set of drivers we apply the user
|
|
|
|
// blacklist onto. For virtualized drivers it often doesn't make sense to
|
|
|
|
// make this stipulation so by default they are ignored.
|
|
|
|
DefaultUserCheckedDrivers = strings.Join([]string{
|
|
|
|
"exec",
|
2016-03-25 19:43:50 +00:00
|
|
|
"qemu",
|
2016-03-24 17:55:14 +00:00
|
|
|
"java",
|
|
|
|
}, ",")
|
2016-12-03 01:04:07 +00:00
|
|
|
|
|
|
|
// A mapping of directories on the host OS to attempt to embed inside each
|
|
|
|
// task's chroot.
|
|
|
|
DefaultChrootEnv = map[string]string{
|
|
|
|
"/bin": "/bin",
|
|
|
|
"/etc": "/etc",
|
|
|
|
"/lib": "/lib",
|
|
|
|
"/lib32": "/lib32",
|
|
|
|
"/lib64": "/lib64",
|
|
|
|
"/run/resolvconf": "/run/resolvconf",
|
|
|
|
"/sbin": "/sbin",
|
|
|
|
"/usr": "/usr",
|
|
|
|
}
|
2016-03-23 18:45:03 +00:00
|
|
|
)
|
|
|
|
|
2015-08-25 23:13:33 +00:00
|
|
|
// RPCHandler can be provided to the Client if there is a local server
|
|
|
|
// to avoid going over the network. If not provided, the Client will
|
|
|
|
// maintain a connection pool to the servers
|
|
|
|
type RPCHandler interface {
|
|
|
|
RPC(method string, args interface{}, reply interface{}) error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Config is used to parameterize and configure the behavior of the client
|
|
|
|
type Config struct {
|
|
|
|
// DevMode controls if we are in a development mode which
|
|
|
|
// avoids persistent storage.
|
|
|
|
DevMode bool
|
|
|
|
|
|
|
|
// StateDir is where we store our state
|
|
|
|
StateDir string
|
|
|
|
|
|
|
|
// AllocDir is where we store data for allocations
|
|
|
|
AllocDir string
|
|
|
|
|
|
|
|
// LogOutput is the destination for logs
|
|
|
|
LogOutput io.Writer
|
|
|
|
|
2018-09-13 17:43:40 +00:00
|
|
|
// Logger provides a logger to thhe client
|
|
|
|
Logger log.Logger
|
|
|
|
|
2015-08-25 23:13:33 +00:00
|
|
|
// Region is the clients region
|
|
|
|
Region string
|
|
|
|
|
2015-10-01 15:31:47 +00:00
|
|
|
// Network interface to be used in network fingerprinting
|
2015-10-02 07:29:18 +00:00
|
|
|
NetworkInterface string
|
2015-10-01 15:31:47 +00:00
|
|
|
|
2015-10-03 00:32:11 +00:00
|
|
|
// Network speed is the default speed of network interfaces if they can not
|
|
|
|
// be determined dynamically.
|
|
|
|
NetworkSpeed int
|
|
|
|
|
2017-03-14 19:56:31 +00:00
|
|
|
// CpuCompute is the default total CPU compute if they can not be determined
|
|
|
|
// dynamically. It should be given as Cores * MHz (2 Cores * 2 Ghz = 4000)
|
|
|
|
CpuCompute int
|
|
|
|
|
2018-01-22 20:28:29 +00:00
|
|
|
// MemoryMB is the default node total memory in megabytes if it cannot be
|
|
|
|
// determined dynamically.
|
|
|
|
MemoryMB int
|
|
|
|
|
2015-12-23 00:10:30 +00:00
|
|
|
// MaxKillTimeout allows capping the user-specifiable KillTimeout. If the
|
|
|
|
// task's KillTimeout is greater than the MaxKillTimeout, MaxKillTimeout is
|
|
|
|
// used.
|
|
|
|
MaxKillTimeout time.Duration
|
|
|
|
|
2015-08-25 23:13:33 +00:00
|
|
|
// Servers is a list of known server addresses. These are as "host:port"
|
|
|
|
Servers []string
|
|
|
|
|
|
|
|
// RPCHandler can be provided to avoid network traffic if the
|
|
|
|
// server is running locally.
|
|
|
|
RPCHandler RPCHandler
|
|
|
|
|
|
|
|
// Node provides the base node
|
|
|
|
Node *structs.Node
|
2015-09-01 02:48:59 +00:00
|
|
|
|
2016-02-08 21:29:53 +00:00
|
|
|
// ClientMaxPort is the upper range of the ports that the client uses for
|
2016-03-14 02:05:41 +00:00
|
|
|
// communicating with plugin subsystems over loopback
|
2016-02-08 21:29:53 +00:00
|
|
|
ClientMaxPort uint
|
2016-02-05 23:17:15 +00:00
|
|
|
|
2016-02-08 21:29:53 +00:00
|
|
|
// ClientMinPort is the lower range of the ports that the client uses for
|
2016-03-14 02:05:41 +00:00
|
|
|
// communicating with plugin subsystems over loopback
|
2016-02-08 21:29:53 +00:00
|
|
|
ClientMinPort uint
|
2016-02-05 23:17:15 +00:00
|
|
|
|
2016-08-02 02:58:32 +00:00
|
|
|
// A mapping of directories on the host OS to attempt to embed inside each
|
|
|
|
// task's chroot.
|
|
|
|
ChrootEnv map[string]string
|
|
|
|
|
2015-09-01 02:48:59 +00:00
|
|
|
// Options provides arbitrary key-value configuration for nomad internals,
|
|
|
|
// like fingerprinters and drivers. The format is:
|
|
|
|
//
|
|
|
|
// namespace.option = value
|
|
|
|
Options map[string]string
|
2016-02-25 03:06:30 +00:00
|
|
|
|
|
|
|
// Version is the version of the Nomad client
|
2017-08-16 22:42:15 +00:00
|
|
|
Version *version.VersionInfo
|
2016-05-11 22:24:37 +00:00
|
|
|
|
2016-05-24 04:28:12 +00:00
|
|
|
// ConsulConfig is this Agent's Consul configuration
|
|
|
|
ConsulConfig *config.ConsulConfig
|
2016-05-25 05:30:10 +00:00
|
|
|
|
2016-08-09 22:00:50 +00:00
|
|
|
// VaultConfig is this Agent's Vault configuration
|
|
|
|
VaultConfig *config.VaultConfig
|
|
|
|
|
2016-05-25 05:30:10 +00:00
|
|
|
// StatsCollectionInterval is the interval at which the Nomad client
|
|
|
|
// collects resource usage stats
|
|
|
|
StatsCollectionInterval time.Duration
|
2016-08-02 02:49:01 +00:00
|
|
|
|
|
|
|
// PublishNodeMetrics determines whether nomad is going to publish node
|
|
|
|
// level metrics to remote Telemetry sinks
|
|
|
|
PublishNodeMetrics bool
|
|
|
|
|
|
|
|
// PublishAllocationMetrics determines whether nomad is going to publish
|
|
|
|
// allocation metrics to remote Telemetry sinks
|
|
|
|
PublishAllocationMetrics bool
|
2016-10-24 05:22:00 +00:00
|
|
|
|
2016-10-25 22:57:38 +00:00
|
|
|
// TLSConfig holds various TLS related configurations
|
|
|
|
TLSConfig *config.TLSConfig
|
2017-01-09 19:21:51 +00:00
|
|
|
|
2017-01-31 23:32:20 +00:00
|
|
|
// GCInterval is the time interval at which the client triggers garbage
|
|
|
|
// collection
|
|
|
|
GCInterval time.Duration
|
|
|
|
|
2017-03-11 00:27:00 +00:00
|
|
|
// GCParallelDestroys is the number of parallel destroys the garbage
|
|
|
|
// collector will allow.
|
|
|
|
GCParallelDestroys int
|
|
|
|
|
|
|
|
// GCDiskUsageThreshold is the disk usage threshold given as a percent
|
|
|
|
// beyond which the Nomad client triggers GC of terminal allocations
|
2017-01-31 23:32:20 +00:00
|
|
|
GCDiskUsageThreshold float64
|
|
|
|
|
2017-03-11 00:27:00 +00:00
|
|
|
// GCInodeUsageThreshold is the inode usage threshold given as a percent
|
|
|
|
// beyond which the Nomad client triggers GC of the terminal allocations
|
2017-01-31 23:32:20 +00:00
|
|
|
GCInodeUsageThreshold float64
|
|
|
|
|
2017-05-11 00:39:45 +00:00
|
|
|
// GCMaxAllocs is the maximum number of allocations a node can have
|
|
|
|
// before garbage collection is triggered.
|
|
|
|
GCMaxAllocs int
|
|
|
|
|
2017-01-09 19:21:51 +00:00
|
|
|
// LogLevel is the level of the logs to putout
|
|
|
|
LogLevel string
|
2017-02-27 21:42:37 +00:00
|
|
|
|
|
|
|
// NoHostUUID disables using the host's UUID and will force generation of a
|
|
|
|
// random UUID.
|
|
|
|
NoHostUUID bool
|
2017-08-20 00:19:38 +00:00
|
|
|
|
|
|
|
// ACLEnabled controls if ACL enforcement and management is enabled.
|
|
|
|
ACLEnabled bool
|
|
|
|
|
|
|
|
// ACLTokenTTL is how long we cache token values for
|
|
|
|
ACLTokenTTL time.Duration
|
|
|
|
|
|
|
|
// ACLPolicyTTL is how long we cache policy values for
|
|
|
|
ACLPolicyTTL time.Duration
|
2017-08-30 21:18:42 +00:00
|
|
|
|
|
|
|
// DisableTaggedMetrics determines whether metrics will be displayed via a
|
|
|
|
// key/value/tag format, or simply a key/value format
|
|
|
|
DisableTaggedMetrics bool
|
|
|
|
|
2019-06-03 19:31:39 +00:00
|
|
|
// DisableRemoteExec disables remote exec targeting tasks on this client
|
|
|
|
DisableRemoteExec bool
|
|
|
|
|
2017-08-30 21:18:42 +00:00
|
|
|
// BackwardsCompatibleMetrics determines whether to show methods of
|
2018-03-11 19:13:25 +00:00
|
|
|
// displaying metrics for older versions, or to only show the new format
|
2017-08-30 21:18:42 +00:00
|
|
|
BackwardsCompatibleMetrics bool
|
2018-01-10 19:28:44 +00:00
|
|
|
|
|
|
|
// RPCHoldTimeout is how long an RPC can be "held" before it is errored.
|
|
|
|
// This is used to paper over a loss of leadership by instead holding RPCs,
|
|
|
|
// so that the caller experiences a slow response rather than an error.
|
|
|
|
// This period is meant to be long enough for a leader election to take
|
|
|
|
// place, and a small jitter is applied to avoid a thundering herd.
|
|
|
|
RPCHoldTimeout time.Duration
|
2018-09-27 01:14:36 +00:00
|
|
|
|
|
|
|
// PluginLoader is used to load plugins.
|
|
|
|
PluginLoader loader.PluginCatalog
|
|
|
|
|
|
|
|
// PluginSingletonLoader is a plugin loader that will returns singleton
|
|
|
|
// instances of the plugins.
|
|
|
|
PluginSingletonLoader loader.PluginCatalog
|
2019-01-09 18:57:56 +00:00
|
|
|
|
|
|
|
// StateDBFactory is used to override stateDB implementations,
|
|
|
|
StateDBFactory state.NewStateDBFunc
|
2019-06-14 03:05:57 +00:00
|
|
|
|
|
|
|
// CNIPath is the path used to search for CNI plugins. Multiple paths can
|
|
|
|
// be specified with colon delimited
|
|
|
|
CNIPath string
|
|
|
|
|
|
|
|
// BridgeNetworkName is the name to use for the bridge created in bridge
|
|
|
|
// networking mode. This defaults to 'nomad' if not set
|
|
|
|
BridgeNetworkName string
|
|
|
|
|
|
|
|
// BridgeNetworkAllocSubnet is the IP subnet to use for address allocation
|
|
|
|
// for allocations in bridge networking mode. Subnet must be in CIDR
|
|
|
|
// notation
|
|
|
|
BridgeNetworkAllocSubnet string
|
2019-07-25 14:45:41 +00:00
|
|
|
|
2019-08-12 14:22:27 +00:00
|
|
|
// HostVolumes is a map of the configured host volumes by name.
|
2019-07-25 14:45:41 +00:00
|
|
|
HostVolumes map[string]*structs.ClientHostVolumeConfig
|
2015-09-01 02:48:59 +00:00
|
|
|
}
|
|
|
|
|
2016-02-10 21:44:53 +00:00
|
|
|
func (c *Config) Copy() *Config {
|
2016-02-11 01:54:43 +00:00
|
|
|
nc := new(Config)
|
|
|
|
*nc = *c
|
|
|
|
nc.Node = nc.Node.Copy()
|
2017-01-18 23:55:14 +00:00
|
|
|
nc.Servers = helper.CopySliceString(nc.Servers)
|
|
|
|
nc.Options = helper.CopyMapStringString(nc.Options)
|
2019-07-25 14:45:41 +00:00
|
|
|
nc.HostVolumes = structs.CopyMapStringClientHostVolumeConfig(nc.HostVolumes)
|
2016-08-09 22:00:50 +00:00
|
|
|
nc.ConsulConfig = c.ConsulConfig.Copy()
|
|
|
|
nc.VaultConfig = c.VaultConfig.Copy()
|
2016-02-11 01:54:43 +00:00
|
|
|
return nc
|
2016-02-10 21:44:53 +00:00
|
|
|
}
|
|
|
|
|
2016-05-31 18:58:02 +00:00
|
|
|
// DefaultConfig returns the default configuration
|
|
|
|
func DefaultConfig() *Config {
|
|
|
|
return &Config{
|
2017-08-30 21:18:42 +00:00
|
|
|
Version: version.GetVersion(),
|
|
|
|
VaultConfig: config.DefaultVaultConfig(),
|
|
|
|
ConsulConfig: config.DefaultConsulConfig(),
|
|
|
|
LogOutput: os.Stderr,
|
|
|
|
Region: "global",
|
|
|
|
StatsCollectionInterval: 1 * time.Second,
|
|
|
|
TLSConfig: &config.TLSConfig{},
|
|
|
|
LogLevel: "DEBUG",
|
|
|
|
GCInterval: 1 * time.Minute,
|
|
|
|
GCParallelDestroys: 2,
|
|
|
|
GCDiskUsageThreshold: 80,
|
|
|
|
GCInodeUsageThreshold: 70,
|
|
|
|
GCMaxAllocs: 50,
|
|
|
|
NoHostUUID: true,
|
|
|
|
DisableTaggedMetrics: false,
|
2019-06-03 19:31:39 +00:00
|
|
|
DisableRemoteExec: false,
|
2017-08-30 21:18:42 +00:00
|
|
|
BackwardsCompatibleMetrics: false,
|
2018-01-10 19:28:44 +00:00
|
|
|
RPCHoldTimeout: 5 * time.Second,
|
2016-05-31 18:58:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-01 02:54:49 +00:00
|
|
|
// Read returns the specified configuration value or "".
|
2015-09-01 02:48:59 +00:00
|
|
|
func (c *Config) Read(id string) string {
|
2016-03-24 17:55:14 +00:00
|
|
|
return c.Options[id]
|
2015-08-25 23:13:33 +00:00
|
|
|
}
|
2015-09-01 02:54:49 +00:00
|
|
|
|
|
|
|
// ReadDefault returns the specified configuration value, or the specified
|
|
|
|
// default value if none is set.
|
|
|
|
func (c *Config) ReadDefault(id string, defaultValue string) string {
|
2016-03-24 17:55:14 +00:00
|
|
|
val, ok := c.Options[id]
|
|
|
|
if !ok {
|
|
|
|
return defaultValue
|
2015-09-01 02:54:49 +00:00
|
|
|
}
|
2016-03-24 17:55:14 +00:00
|
|
|
return val
|
2015-09-01 02:54:49 +00:00
|
|
|
}
|
2015-11-17 03:30:37 +00:00
|
|
|
|
|
|
|
// ReadBool parses the specified option as a boolean.
|
|
|
|
func (c *Config) ReadBool(id string) (bool, error) {
|
|
|
|
val, ok := c.Options[id]
|
|
|
|
if !ok {
|
2015-11-17 03:55:08 +00:00
|
|
|
return false, fmt.Errorf("Specified config is missing from options")
|
2015-11-17 03:30:37 +00:00
|
|
|
}
|
|
|
|
bval, err := strconv.ParseBool(val)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Failed to parse %s as bool: %s", val, err)
|
|
|
|
}
|
|
|
|
return bval, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReadBoolDefault tries to parse the specified option as a boolean. If there is
|
|
|
|
// an error in parsing, the default option is returned.
|
|
|
|
func (c *Config) ReadBoolDefault(id string, defaultValue bool) bool {
|
2015-11-17 03:55:08 +00:00
|
|
|
val, err := c.ReadBool(id)
|
2015-11-17 03:30:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
2015-11-24 15:18:49 +00:00
|
|
|
|
2017-03-14 19:56:31 +00:00
|
|
|
// ReadInt parses the specified option as a int.
|
|
|
|
func (c *Config) ReadInt(id string) (int, error) {
|
|
|
|
val, ok := c.Options[id]
|
|
|
|
if !ok {
|
|
|
|
return 0, fmt.Errorf("Specified config is missing from options")
|
|
|
|
}
|
|
|
|
ival, err := strconv.Atoi(val)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("Failed to parse %s as int: %s", val, err)
|
|
|
|
}
|
|
|
|
return ival, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReadIntDefault tries to parse the specified option as a int. If there is
|
|
|
|
// an error in parsing, the default option is returned.
|
|
|
|
func (c *Config) ReadIntDefault(id string, defaultValue int) int {
|
|
|
|
val, err := c.ReadInt(id)
|
|
|
|
if err != nil {
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
2017-02-24 21:20:40 +00:00
|
|
|
// ReadDuration parses the specified option as a duration.
|
|
|
|
func (c *Config) ReadDuration(id string) (time.Duration, error) {
|
|
|
|
val, ok := c.Options[id]
|
|
|
|
if !ok {
|
|
|
|
return time.Duration(0), fmt.Errorf("Specified config is missing from options")
|
|
|
|
}
|
|
|
|
dval, err := time.ParseDuration(val)
|
|
|
|
if err != nil {
|
|
|
|
return time.Duration(0), fmt.Errorf("Failed to parse %s as time duration: %s", val, err)
|
|
|
|
}
|
|
|
|
return dval, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReadDurationDefault tries to parse the specified option as a duration. If there is
|
|
|
|
// an error in parsing, the default option is returned.
|
|
|
|
func (c *Config) ReadDurationDefault(id string, defaultValue time.Duration) time.Duration {
|
|
|
|
val, err := c.ReadDuration(id)
|
|
|
|
if err != nil {
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
2016-05-15 16:41:34 +00:00
|
|
|
// ReadStringListToMap tries to parse the specified option as a comma separated list.
|
2015-11-24 15:18:49 +00:00
|
|
|
// If there is an error in parsing, an empty list is returned.
|
|
|
|
func (c *Config) ReadStringListToMap(key string) map[string]struct{} {
|
|
|
|
s := strings.TrimSpace(c.Read(key))
|
|
|
|
list := make(map[string]struct{})
|
|
|
|
if s != "" {
|
|
|
|
for _, e := range strings.Split(s, ",") {
|
|
|
|
trimmed := strings.TrimSpace(e)
|
|
|
|
list[trimmed] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return list
|
|
|
|
}
|
2016-03-24 17:55:14 +00:00
|
|
|
|
2016-05-15 16:41:34 +00:00
|
|
|
// ReadStringListToMap tries to parse the specified option as a comma separated list.
|
2016-03-24 17:55:14 +00:00
|
|
|
// If there is an error in parsing, an empty list is returned.
|
|
|
|
func (c *Config) ReadStringListToMapDefault(key, defaultValue string) map[string]struct{} {
|
|
|
|
val, ok := c.Options[key]
|
|
|
|
if !ok {
|
|
|
|
val = defaultValue
|
|
|
|
}
|
|
|
|
|
|
|
|
list := make(map[string]struct{})
|
|
|
|
if val != "" {
|
|
|
|
for _, e := range strings.Split(val, ",") {
|
|
|
|
trimmed := strings.TrimSpace(e)
|
|
|
|
list[trimmed] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return list
|
|
|
|
}
|
2018-10-17 02:21:15 +00:00
|
|
|
|
|
|
|
// NomadPluginConfig produces the NomadConfig struct which is sent to Nomad plugins
|
2018-12-18 00:40:58 +00:00
|
|
|
func (c *Config) NomadPluginConfig() *base.AgentConfig {
|
|
|
|
return &base.AgentConfig{
|
2018-10-30 01:34:34 +00:00
|
|
|
Driver: &base.ClientDriverConfig{
|
2018-10-19 03:31:01 +00:00
|
|
|
ClientMinPort: c.ClientMinPort,
|
|
|
|
ClientMaxPort: c.ClientMaxPort,
|
|
|
|
},
|
2018-10-17 02:21:15 +00:00
|
|
|
}
|
|
|
|
}
|