github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/provider/local/environprovider.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package local
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"net/url"
    10  	"os"
    11  	"os/user"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  	"syscall"
    16  
    17  	"github.com/juju/errors"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/utils"
    20  	"github.com/juju/utils/proxy"
    21  	"gopkg.in/juju/environschema.v1"
    22  
    23  	"github.com/juju/juju/environs"
    24  	"github.com/juju/juju/environs/config"
    25  	"github.com/juju/juju/instance"
    26  	"github.com/juju/juju/juju/osenv"
    27  	"github.com/juju/juju/version"
    28  )
    29  
    30  var logger = loggo.GetLogger("juju.provider.local")
    31  
    32  var _ environs.EnvironProvider = (*environProvider)(nil)
    33  
    34  type environProvider struct{}
    35  
    36  var providerInstance = &environProvider{}
    37  
    38  var userCurrent = user.Current
    39  
    40  // Open implements environs.EnvironProvider.Open.
    41  func (environProvider) Open(cfg *config.Config) (environs.Environ, error) {
    42  	logger.Infof("opening environment %q", cfg.Name())
    43  	// Do the initial validation on the config.
    44  	localConfig, err := providerInstance.newConfig(cfg)
    45  	if err != nil {
    46  		return nil, errors.Trace(err)
    47  	}
    48  	if err := VerifyPrerequisites(localConfig.container()); err != nil {
    49  		return nil, errors.Annotate(err, "failed verification of local provider prerequisites")
    50  	}
    51  	if cfg, err = providerInstance.correctLocalhostURLs(cfg, localConfig); err != nil {
    52  		return nil, errors.Annotate(err, "failed to replace localhost references in loopback URLs specified in proxy config settings")
    53  	}
    54  	environ := &localEnviron{name: cfg.Name()}
    55  	if err := environ.SetConfig(cfg); err != nil {
    56  		return nil, errors.Annotate(err, "failure setting config")
    57  	}
    58  	return environ, nil
    59  }
    60  
    61  // Schema returns the configuration schema for an environment.
    62  func (environProvider) Schema() environschema.Fields {
    63  	fields, err := config.Schema(configSchema)
    64  	if err != nil {
    65  		panic(err)
    66  	}
    67  	return fields
    68  }
    69  
    70  // correctLocalhostURLs exams proxy attributes and changes URL values pointing to localhost to use bridge IP.
    71  func (p environProvider) correctLocalhostURLs(cfg *config.Config, providerCfg *environConfig) (*config.Config, error) {
    72  	attrs := cfg.AllAttrs()
    73  	updatedAttrs := make(map[string]interface{})
    74  	for _, key := range config.ProxyAttributes {
    75  		anAttr := attrs[key]
    76  		if anAttr == nil {
    77  			continue
    78  		}
    79  		var attrStr string
    80  		var isString bool
    81  		if attrStr, isString = anAttr.(string); !isString || attrStr == "" {
    82  			continue
    83  		}
    84  		newValue, err := p.swapLocalhostForBridgeIP(attrStr, providerCfg)
    85  		if err != nil {
    86  			return nil, errors.Trace(err)
    87  		}
    88  		updatedAttrs[key] = newValue
    89  		logger.Infof("\nAttribute %q is set to (%v)\n", key, newValue)
    90  	}
    91  	// Update desired attributes on current configuration
    92  	return cfg.Apply(updatedAttrs)
    93  }
    94  
    95  // RestrictedConfigAttributes is specified in the EnvironProvider interface.
    96  func (p environProvider) RestrictedConfigAttributes() []string {
    97  	return []string{ContainerKey, NetworkBridgeKey, RootDirKey, "proxy-ssh"}
    98  }
    99  
   100  // PrepareForCreateEnvironment is specified in the EnvironProvider interface.
   101  func (p environProvider) PrepareForCreateEnvironment(cfg *config.Config) (*config.Config, error) {
   102  	return cfg, nil
   103  }
   104  
   105  // PrepareForBootstrap implements environs.EnvironProvider.PrepareForBootstrap.
   106  func (p environProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) {
   107  	attrs := map[string]interface{}{
   108  		// We must not proxy SSH through the API server in a
   109  		// local provider environment. Besides not being useful,
   110  		// it may not work; there is no requirement for sshd to
   111  		// be available on machine-0.
   112  		"proxy-ssh": false,
   113  	}
   114  	if _, ok := cfg.AgentVersion(); !ok {
   115  		attrs["agent-version"] = version.Current.Number.String()
   116  	}
   117  	if namespace, _ := cfg.UnknownAttrs()["namespace"].(string); namespace == "" {
   118  		username := os.Getenv("USER")
   119  		if username == "" {
   120  			u, err := userCurrent()
   121  			if err != nil {
   122  				return nil, errors.Annotate(err, "failed to determine username for namespace")
   123  			}
   124  			username = u.Username
   125  		}
   126  		attrs["namespace"] = fmt.Sprintf("%s-%s", username, cfg.Name())
   127  	}
   128  
   129  	setIfNotBlank := func(key, value string) {
   130  		if value != "" {
   131  			attrs[key] = value
   132  		}
   133  	}
   134  	// If the user has specified no values for any of the four normal
   135  	// proxies, then look in the environment and set them.
   136  	logger.Tracef("Look for proxies?")
   137  	if cfg.HttpProxy() == "" &&
   138  		cfg.HttpsProxy() == "" &&
   139  		cfg.FtpProxy() == "" &&
   140  		cfg.NoProxy() == "" {
   141  		proxySettings := proxy.DetectProxies()
   142  		logger.Tracef("Proxies detected %#v", proxySettings)
   143  		setIfNotBlank(config.HttpProxyKey, proxySettings.Http)
   144  		setIfNotBlank(config.HttpsProxyKey, proxySettings.Https)
   145  		setIfNotBlank(config.FtpProxyKey, proxySettings.Ftp)
   146  		setIfNotBlank(config.NoProxyKey, proxySettings.NoProxy)
   147  	}
   148  	if version.Current.OS == version.Ubuntu {
   149  		if cfg.AptHttpProxy() == "" &&
   150  			cfg.AptHttpsProxy() == "" &&
   151  			cfg.AptFtpProxy() == "" {
   152  			proxySettings, err := detectPackageProxies()
   153  			if err != nil {
   154  				return nil, errors.Trace(err)
   155  			}
   156  			setIfNotBlank(config.AptHttpProxyKey, proxySettings.Http)
   157  			setIfNotBlank(config.AptHttpsProxyKey, proxySettings.Https)
   158  			setIfNotBlank(config.AptFtpProxyKey, proxySettings.Ftp)
   159  		}
   160  	}
   161  
   162  	cfg, err := cfg.Apply(attrs)
   163  	if err != nil {
   164  		return nil, errors.Trace(err)
   165  	}
   166  	// Make sure everything is valid.
   167  	cfg, err = p.Validate(cfg, nil)
   168  	if err != nil {
   169  		return nil, errors.Trace(err)
   170  	}
   171  
   172  	// The user must not set bootstrap-ip; this is determined by the provider,
   173  	// and its presence used to determine whether the environment has yet been
   174  	// bootstrapped.
   175  	if _, ok := cfg.UnknownAttrs()["bootstrap-ip"]; ok {
   176  		return nil, errors.Errorf("bootstrap-ip must not be specified")
   177  	}
   178  	err = checkLocalPort(cfg.StatePort(), "state port")
   179  	if err != nil {
   180  		return nil, errors.Trace(err)
   181  	}
   182  	err = checkLocalPort(cfg.APIPort(), "API port")
   183  	if err != nil {
   184  		return nil, errors.Trace(err)
   185  	}
   186  
   187  	return p.Open(cfg)
   188  }
   189  
   190  // swapLocalhostForBridgeIP substitutes bridge ip for localhost. Non-localhost values are not modified.
   191  func (p environProvider) swapLocalhostForBridgeIP(originalURL string, providerConfig *environConfig) (string, error) {
   192  	// TODO(anastasia) 2014-10-31 Bug#1385277 Parse method does not cater for malformed URL, eg. localhost:8080
   193  	parsedUrl, err := url.Parse(originalURL)
   194  	if err != nil {
   195  		return "", errors.Trace(err)
   196  	}
   197  
   198  	isLoopback, _, port := isLoopback(parsedUrl.Host)
   199  	if !isLoopback {
   200  		// If not loopback host address, return current attribute value
   201  		return originalURL, nil
   202  	}
   203  	//If localhost is specified, use its network bridge ip
   204  	bridgeAddress, nwerr := getAddressForInterface(providerConfig.networkBridge())
   205  	if nwerr != nil {
   206  		return "", errors.Trace(nwerr)
   207  	}
   208  	parsedUrl.Host = bridgeAddress + port
   209  	return parsedUrl.String(), nil
   210  }
   211  
   212  // isLoopback returns whether given url is a loopback url.
   213  // The argument to the method is expected to be in the form of
   214  // host:port where host and port are also returned as distinct values.
   215  func isLoopback(hostAndPort string) (isLoopback bool, host, port string) {
   216  	host, port = getHostAndPort(hostAndPort)
   217  	isLoopback = strings.ToLower(host) == "localhost"
   218  	if !isLoopback {
   219  		ip := net.ParseIP(host)
   220  		isLoopback = ip != nil && ip.IsLoopback()
   221  	}
   222  	return
   223  }
   224  
   225  // getHostAndPort expects argument in the form host:port and
   226  // returns host and port as distinctive strings.
   227  func getHostAndPort(original string) (host, port string) {
   228  	// Host and post specification is host:port
   229  	hostAndPortRegexp := regexp.MustCompile(`(?P<host>\[?[::]*[^:]+)(?P<port>$|:[^:]+$)`)
   230  
   231  	matched := hostAndPortRegexp.FindStringSubmatch(original)
   232  	if len(matched) == 0 {
   233  		// Passed in parameter is not in the form host:port.
   234  		// Let's not mess with it.
   235  		return original, ""
   236  	}
   237  
   238  	// For the string in the form host:port, FindStringSubmatch above
   239  	// will return {host:port, host, :port}
   240  	host = matched[1]
   241  	port = matched[2]
   242  
   243  	// For hosts like [::1], remove brackets
   244  	if strings.Contains(host, "[") {
   245  		host = host[1 : len(host)-1]
   246  	}
   247  
   248  	// For hosts like ::1, substring :1 is not a port!
   249  	if strings.Contains(host, port) {
   250  		port = ""
   251  	}
   252  	return
   253  }
   254  
   255  // checkLocalPort checks that the passed port is not used so far.
   256  var checkLocalPort = func(port int, description string) error {
   257  	logger.Infof("checking %s", description)
   258  	// Try to connect the port on localhost.
   259  	address := net.JoinHostPort("localhost", strconv.Itoa(port))
   260  	// TODO(mue) Add a timeout?
   261  	conn, err := net.Dial("tcp", address)
   262  	if err != nil {
   263  		if isConnectionRefused(err) {
   264  			// we're expecting to get conn refused
   265  			return nil
   266  		}
   267  		// some other error happened
   268  		return errors.Trace(err)
   269  	}
   270  	// Connected, so port is in use.
   271  	err = conn.Close()
   272  	if err != nil {
   273  		return err
   274  	}
   275  	return errors.Errorf("cannot use %d as %s, already in use", port, description)
   276  }
   277  
   278  // isConnectionRefused indicates if the err was caused by a refused connection.
   279  func isConnectionRefused(err error) bool {
   280  	if err, ok := err.(*net.OpError); ok {
   281  		// go 1.4 and earlier
   282  		if err.Err == syscall.ECONNREFUSED {
   283  			return true
   284  		}
   285  		// go 1.5 and later
   286  		if err, ok := err.Err.(*os.SyscallError); ok {
   287  			return err.Err == syscall.ECONNREFUSED
   288  		}
   289  	}
   290  	return false
   291  }
   292  
   293  // Validate implements environs.EnvironProvider.Validate.
   294  func (provider environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
   295  	// Check for valid changes for the base config values.
   296  	if err := config.Validate(cfg, old); err != nil {
   297  		return nil, err
   298  	}
   299  	validated, err := cfg.ValidateUnknownAttrs(configFields, configDefaults)
   300  	if err != nil {
   301  		return nil, errors.Annotatef(err, "failed to validate unknown attrs")
   302  	}
   303  	localConfig := newEnvironConfig(cfg, validated)
   304  	// Set correct default network bridge if needed
   305  	// fix for http://pad.lv/1394450
   306  	localConfig.setDefaultNetworkBridge()
   307  	// Before potentially creating directories, make sure that the
   308  	// root directory has not changed.
   309  	if localConfig.namespace() == "" {
   310  		return nil, errors.New("missing namespace, config not prepared")
   311  	}
   312  	containerType := localConfig.container()
   313  	if old != nil {
   314  		oldLocalConfig, err := provider.newConfig(old)
   315  		if err != nil {
   316  			return nil, errors.Annotatef(err, "old config is not a valid local config: %v", old)
   317  		}
   318  		if containerType != oldLocalConfig.container() {
   319  			return nil, errors.Errorf("cannot change container from %q to %q",
   320  				oldLocalConfig.container(), containerType)
   321  		}
   322  		if localConfig.rootDir() != oldLocalConfig.rootDir() {
   323  			return nil, errors.Errorf("cannot change root-dir from %q to %q",
   324  				oldLocalConfig.rootDir(),
   325  				localConfig.rootDir())
   326  		}
   327  		if localConfig.networkBridge() != oldLocalConfig.networkBridge() {
   328  			return nil, errors.Errorf("cannot change network-bridge from %q to %q",
   329  				oldLocalConfig.rootDir(),
   330  				localConfig.rootDir())
   331  		}
   332  		if localConfig.storagePort() != oldLocalConfig.storagePort() {
   333  			return nil, errors.Errorf("cannot change storage-port from %v to %v",
   334  				oldLocalConfig.storagePort(),
   335  				localConfig.storagePort())
   336  		}
   337  		if localConfig.namespace() != oldLocalConfig.namespace() {
   338  			return nil, errors.Errorf("cannot change namespace from %v to %v",
   339  				oldLocalConfig.namespace(),
   340  				localConfig.namespace())
   341  		}
   342  	}
   343  	// Currently only supported containers are "lxc" and "kvm".
   344  	if containerType != instance.LXC && containerType != instance.KVM {
   345  		return nil, errors.Errorf("unsupported container type: %q", containerType)
   346  	}
   347  	dir, err := utils.NormalizePath(localConfig.rootDir())
   348  	if err != nil {
   349  		return nil, err
   350  	}
   351  	if dir == "." {
   352  		dir = osenv.JujuHomePath(cfg.Name())
   353  	}
   354  	// Always assign the normalized path.
   355  	localConfig.attrs["root-dir"] = dir
   356  
   357  	// If the user hasn't already specified a value, set it to the
   358  	// given value.
   359  	defineIfNot := func(keyName string, value interface{}) {
   360  		if _, defined := cfg.AllAttrs()[keyName]; !defined {
   361  			logger.Infof("lxc-clone is enabled. Switching %s to %v", keyName, value)
   362  			localConfig.attrs[keyName] = value
   363  		}
   364  	}
   365  
   366  	// If we're cloning, and the user hasn't specified otherwise,
   367  	// prefer to skip update logic.
   368  	if useClone, _ := localConfig.LXCUseClone(); useClone && containerType == instance.LXC {
   369  		defineIfNot("enable-os-refresh-update", true)
   370  		defineIfNot("enable-os-upgrade", false)
   371  	}
   372  
   373  	// Apply the coerced unknown values back into the config.
   374  	return cfg.Apply(localConfig.attrs)
   375  }
   376  
   377  // BoilerplateConfig implements environs.EnvironProvider.BoilerplateConfig.
   378  func (environProvider) BoilerplateConfig() string {
   379  	return `
   380  # https://juju.ubuntu.com/docs/config-local.html
   381  local:
   382      type: local
   383  
   384      # root-dir holds the directory that is used for the storage files and
   385      # database. The default location is $JUJU_HOME/<env-name>.
   386      # $JUJU_HOME defaults to ~/.juju. Override if needed.
   387      #
   388      # root-dir: ~/.juju/local
   389  
   390      # storage-port holds the port where the local provider starts the
   391      # HTTP file server. Override the value if you have multiple local
   392      # providers, or if the default port is used by another program.
   393      #
   394      # storage-port: 8040
   395  
   396      # network-bridge holds the name of the LXC network bridge to use.
   397      # Override if the default LXC network bridge is different.
   398      #
   399      #
   400      # network-bridge: lxcbr0
   401  
   402      # The default series to deploy the state-server and charms on.
   403      # Make sure to uncomment the following option and set the value to
   404      # precise or trusty as desired.
   405      #
   406      # default-series: trusty
   407  
   408      # Whether or not to refresh the list of available updates for an
   409      # OS. The default option of true is recommended for use in
   410      # production systems, but disabling this can speed up local
   411      # deployments for development or testing.
   412      #
   413      # enable-os-refresh-update: true
   414  
   415      # Whether or not to perform OS upgrades when machines are
   416      # provisioned. The default option of true is recommended for use
   417      # in production systems, but disabling this can speed up local
   418      # deployments for development or testing.
   419      #
   420      # enable-os-upgrade: true
   421  
   422  `[1:]
   423  }
   424  
   425  // SecretAttrs implements environs.EnvironProvider.SecretAttrs.
   426  func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
   427  	// don't have any secret attrs
   428  	return nil, nil
   429  }
   430  
   431  func (p environProvider) newConfig(cfg *config.Config) (*environConfig, error) {
   432  	valid, err := p.Validate(cfg, nil)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	return newEnvironConfig(valid, valid.UnknownAttrs()), nil
   437  }