github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/provider/local/environ.go (about)

     1  // Copyright 2013, 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package local
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"sync"
    15  	"syscall"
    16  
    17  	"github.com/juju/errors"
    18  	"github.com/juju/utils/arch"
    19  	"github.com/juju/utils/proxy"
    20  	"github.com/juju/utils/series"
    21  	"github.com/juju/utils/shell"
    22  	"github.com/juju/utils/symlink"
    23  
    24  	"github.com/juju/juju/agent"
    25  	"github.com/juju/juju/cloudconfig"
    26  	"github.com/juju/juju/cloudconfig/cloudinit"
    27  	"github.com/juju/juju/cloudconfig/instancecfg"
    28  	"github.com/juju/juju/constraints"
    29  	"github.com/juju/juju/container"
    30  	"github.com/juju/juju/container/factory"
    31  	"github.com/juju/juju/environs"
    32  	"github.com/juju/juju/environs/config"
    33  	"github.com/juju/juju/environs/filestorage"
    34  	"github.com/juju/juju/environs/httpstorage"
    35  	"github.com/juju/juju/environs/storage"
    36  	"github.com/juju/juju/instance"
    37  	"github.com/juju/juju/juju/osenv"
    38  	"github.com/juju/juju/mongo"
    39  	"github.com/juju/juju/network"
    40  	"github.com/juju/juju/provider/common"
    41  	"github.com/juju/juju/service"
    42  	servicecommon "github.com/juju/juju/service/common"
    43  	"github.com/juju/juju/state/multiwatcher"
    44  	"github.com/juju/juju/tools"
    45  	"github.com/juju/juju/worker/terminationworker"
    46  )
    47  
    48  // boostrapInstanceId is just the name we give to the bootstrap machine.
    49  // Using "localhost" because it is, and it makes sense.
    50  const bootstrapInstanceId instance.Id = "localhost"
    51  
    52  // localEnviron implements Environ.
    53  var _ environs.Environ = (*localEnviron)(nil)
    54  
    55  type localEnviron struct {
    56  	common.SupportsUnitPlacementPolicy
    57  
    58  	localMutex       sync.Mutex
    59  	config           *environConfig
    60  	name             string
    61  	bridgeAddress    string
    62  	localStorage     storage.Storage
    63  	storageListener  net.Listener
    64  	containerManager container.Manager
    65  }
    66  
    67  // SupportedArchitectures is specified on the EnvironCapability interface.
    68  func (*localEnviron) SupportedArchitectures() ([]string, error) {
    69  	localArch := arch.HostArch()
    70  	return []string{localArch}, nil
    71  }
    72  
    73  func (*localEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error {
    74  	if placement != "" {
    75  		return fmt.Errorf("unknown placement directive: %s", placement)
    76  	}
    77  	return nil
    78  }
    79  
    80  func (env *localEnviron) machineAgentServiceName() string {
    81  	return "juju-agent-" + env.config.namespace()
    82  }
    83  
    84  func ensureNotRoot() error {
    85  	if checkIfRoot() {
    86  		return fmt.Errorf("bootstrapping a local environment must not be done as root")
    87  	}
    88  	return nil
    89  }
    90  
    91  // Bootstrap is specified in the Environ interface.
    92  func (env *localEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (string, string, environs.BootstrapFinalizer, error) {
    93  	if err := ensureNotRoot(); err != nil {
    94  		return "", "", nil, err
    95  	}
    96  
    97  	// Make sure there are tools available for the
    98  	// host's architecture and series.
    99  	if _, err := args.AvailableTools.Match(tools.Filter{
   100  		Arch:   arch.HostArch(),
   101  		Series: series.HostSeries(),
   102  	}); err != nil {
   103  		return "", "", nil, err
   104  	}
   105  
   106  	cfg, err := env.Config().Apply(map[string]interface{}{
   107  		// Record the bootstrap IP, so the containers know where to go for storage.
   108  		"bootstrap-ip": env.bridgeAddress,
   109  	})
   110  	if err == nil {
   111  		err = env.SetConfig(cfg)
   112  	}
   113  	if err != nil {
   114  		logger.Errorf("failed to apply bootstrap-ip to config: %v", err)
   115  		return "", "", nil, err
   116  	}
   117  	return arch.HostArch(), series.HostSeries(), env.finishBootstrap, nil
   118  }
   119  
   120  // finishBootstrap converts the machine config to cloud-config,
   121  // converts that to a script, and then executes it locally.
   122  func (env *localEnviron) finishBootstrap(ctx environs.BootstrapContext, icfg *instancecfg.InstanceConfig) error {
   123  	icfg.InstanceId = bootstrapInstanceId
   124  	icfg.DataDir = env.config.rootDir()
   125  	icfg.LogDir = fmt.Sprintf("/var/log/juju-%s", env.config.namespace())
   126  	icfg.CloudInitOutputLog = filepath.Join(icfg.DataDir, "cloud-init-output.log")
   127  
   128  	// No JobManageNetworking added in order not to change the network
   129  	// configuration of the user's machine.
   130  	icfg.Jobs = []multiwatcher.MachineJob{multiwatcher.JobManageEnviron}
   131  
   132  	icfg.MachineAgentServiceName = env.machineAgentServiceName()
   133  	icfg.AgentEnvironment = map[string]string{
   134  		agent.Namespace:   env.config.namespace(),
   135  		agent.StorageDir:  env.config.storageDir(),
   136  		agent.StorageAddr: env.config.storageAddr(),
   137  		agent.LxcBridge:   env.config.networkBridge(),
   138  
   139  		// The local provider only supports a single state server,
   140  		// so we make the oplog size to a small value. This makes
   141  		// the preallocation faster with no disadvantage.
   142  		agent.MongoOplogSize: "1", // 1MB
   143  	}
   144  
   145  	if err := instancecfg.FinishInstanceConfig(icfg, env.Config()); err != nil {
   146  		return errors.Trace(err)
   147  	}
   148  
   149  	// Since Juju's state machine is currently the host machine
   150  	// for local providers, don't stomp on it.
   151  	cfgAttrs := env.config.AllAttrs()
   152  	if val, ok := cfgAttrs["enable-os-refresh-update"].(bool); !ok {
   153  		logger.Infof("local provider; disabling refreshing OS updates.")
   154  		icfg.EnableOSRefreshUpdate = false
   155  	} else {
   156  		icfg.EnableOSRefreshUpdate = val
   157  	}
   158  	if val, ok := cfgAttrs["enable-os-upgrade"].(bool); !ok {
   159  		logger.Infof("local provider; disabling OS upgrades.")
   160  		icfg.EnableOSUpgrade = false
   161  	} else {
   162  		icfg.EnableOSUpgrade = val
   163  	}
   164  
   165  	// don't write proxy or mirror settings for local machine
   166  	icfg.AptProxySettings = proxy.Settings{}
   167  	icfg.ProxySettings = proxy.Settings{}
   168  	icfg.AptMirror = ""
   169  
   170  	cloudcfg, err := cloudinit.New(icfg.Series)
   171  	if err != nil {
   172  		return errors.Trace(err)
   173  	}
   174  	cloudcfg.SetSystemUpdate(icfg.EnableOSRefreshUpdate)
   175  	cloudcfg.SetSystemUpgrade(icfg.EnableOSUpgrade)
   176  
   177  	// Since rsyslogd is restricted by apparmor to only write to /var/log/**
   178  	// we now provide a symlink to the written file in the local log dir.
   179  	// Also, we leave the old all-machines.log file in
   180  	// /var/log/juju-{{namespace}} until we start the environment again. So
   181  	// potentially remove it at the start of the cloud-init.
   182  	localLogDir := filepath.Join(icfg.DataDir, "log")
   183  	if err := os.RemoveAll(localLogDir); err != nil {
   184  		return errors.Trace(err)
   185  	}
   186  	if err := symlink.New(icfg.LogDir, localLogDir); err != nil {
   187  		return errors.Trace(err)
   188  	}
   189  	if err := os.Remove(icfg.CloudInitOutputLog); err != nil && !os.IsNotExist(err) {
   190  		return errors.Trace(err)
   191  	}
   192  	cloudcfg.AddScripts(
   193  		fmt.Sprintf("rm -fr %s", icfg.LogDir),
   194  		fmt.Sprintf("rm -f /var/spool/rsyslog/machine-0-%s", env.config.namespace()),
   195  	)
   196  	udata, err := cloudconfig.NewUserdataConfig(icfg, cloudcfg)
   197  	if err != nil {
   198  		return errors.Trace(err)
   199  	}
   200  	if err := udata.ConfigureJuju(); err != nil {
   201  		return errors.Trace(err)
   202  	}
   203  	return executeCloudConfig(ctx, icfg, cloudcfg)
   204  }
   205  
   206  var executeCloudConfig = func(ctx environs.BootstrapContext, icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) error {
   207  	// Finally, convert cloud-config to a script and execute it.
   208  	configScript, err := cloudcfg.RenderScript()
   209  	if err != nil {
   210  		return nil
   211  	}
   212  	script := shell.DumpFileOnErrorScript(icfg.CloudInitOutputLog) + configScript
   213  	cmd := exec.Command("sudo", "/bin/bash", "-s")
   214  	cmd.Stdin = strings.NewReader(script)
   215  	cmd.Stdout = ctx.GetStdout()
   216  	cmd.Stderr = ctx.GetStderr()
   217  	return cmd.Run()
   218  }
   219  
   220  // StateServerInstances is specified in the Environ interface.
   221  func (env *localEnviron) StateServerInstances() ([]instance.Id, error) {
   222  	agentsDir := filepath.Join(env.config.rootDir(), "agents")
   223  	_, err := os.Stat(agentsDir)
   224  	if os.IsNotExist(err) {
   225  		return nil, environs.ErrNotBootstrapped
   226  	}
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  	return []instance.Id{bootstrapInstanceId}, nil
   231  }
   232  
   233  // Config is specified in the Environ interface.
   234  func (env *localEnviron) Config() *config.Config {
   235  	env.localMutex.Lock()
   236  	defer env.localMutex.Unlock()
   237  	return env.config.Config
   238  }
   239  
   240  // SetConfig is specified in the Environ interface.
   241  func (env *localEnviron) SetConfig(cfg *config.Config) error {
   242  	ecfg, err := providerInstance.newConfig(cfg)
   243  	if err != nil {
   244  		logger.Errorf("failed to create new environ config: %v", err)
   245  		return errors.Trace(err)
   246  	}
   247  	env.localMutex.Lock()
   248  	defer env.localMutex.Unlock()
   249  	env.config = ecfg
   250  	env.name = ecfg.Name()
   251  	containerType := ecfg.container()
   252  	managerConfig := container.ManagerConfig{
   253  		container.ConfigName:   env.config.namespace(),
   254  		container.ConfigLogDir: env.config.logDir(),
   255  	}
   256  	var imageURLGetter container.ImageURLGetter
   257  	if containerType == instance.LXC {
   258  		if useLxcClone, ok := cfg.LXCUseClone(); ok {
   259  			managerConfig["use-clone"] = fmt.Sprint(useLxcClone)
   260  		}
   261  		if useLxcCloneAufs, ok := cfg.LXCUseCloneAUFS(); ok {
   262  			managerConfig["use-aufs"] = fmt.Sprint(useLxcCloneAufs)
   263  		}
   264  		// For lxc containers, we cache image tarballs in the environment storage, so here
   265  		// we construct a URL getter.
   266  		if uuid, ok := ecfg.UUID(); ok {
   267  			var caCert []byte = nil
   268  			if cert, ok := cfg.CACert(); ok {
   269  				caCert = []byte(cert)
   270  			}
   271  			imageURLGetter = container.NewImageURLGetter(ecfg.stateServerAddr(), uuid, caCert)
   272  		}
   273  	}
   274  	env.containerManager, err = factory.NewContainerManager(
   275  		containerType, managerConfig, imageURLGetter)
   276  	if err != nil {
   277  		return errors.Trace(err)
   278  	}
   279  
   280  	// When the localEnviron value is created on the client
   281  	// side, the bootstrap-ip attribute will not exist,
   282  	// because it is only set *within* the running
   283  	// environment, not in the configuration created by
   284  	// Prepare.
   285  	//
   286  	// When bootstrapIPAddress returns a non-empty string,
   287  	// we know we are running server-side and thus must use
   288  	// httpstorage.
   289  	if addr := ecfg.bootstrapIPAddress(); addr != "" {
   290  		env.bridgeAddress = addr
   291  		return nil
   292  	}
   293  	// If we get to here, it is because we haven't yet bootstrapped an
   294  	// environment, and saved the config in it, or we are running a command
   295  	// from the command line, so it is ok to work on the assumption that we
   296  	// have direct access to the directories.
   297  	if err := env.config.createDirs(); err != nil {
   298  		return errors.Trace(err)
   299  	}
   300  	// Record the network bridge address and create a filestorage.
   301  	if err := env.resolveBridgeAddress(cfg); err != nil {
   302  		return errors.Trace(err)
   303  	}
   304  	return env.setLocalStorage()
   305  }
   306  
   307  // resolveBridgeAddress finishes up the setup of the environment in
   308  // situations where there is no machine agent running yet.
   309  func (env *localEnviron) resolveBridgeAddress(cfg *config.Config) error {
   310  	// We need the provider config to get the network bridge.
   311  	config, err := providerInstance.newConfig(cfg)
   312  	if err != nil {
   313  		logger.Errorf("failed to create new environ config: %v", err)
   314  		return err
   315  	}
   316  	networkBridge := config.networkBridge()
   317  	bridgeAddress, err := getAddressForInterface(networkBridge)
   318  	if err != nil {
   319  		logger.Infof("configure a different bridge using 'network-bridge' in the config file")
   320  		return fmt.Errorf("cannot find address of network-bridge: %q: %v", networkBridge, err)
   321  	}
   322  	logger.Debugf("found %q as address for %q", bridgeAddress, networkBridge)
   323  	env.bridgeAddress = bridgeAddress
   324  	return nil
   325  }
   326  
   327  // setLocalStorage creates a filestorage so tools can
   328  // be synced and so forth without having a machine agent
   329  // running.
   330  func (env *localEnviron) setLocalStorage() error {
   331  	storage, err := filestorage.NewFileStorageWriter(env.config.storageDir())
   332  	if err != nil {
   333  		return err
   334  	}
   335  	env.localStorage = storage
   336  	return nil
   337  }
   338  
   339  var unsupportedConstraints = []string{
   340  	constraints.CpuCores,
   341  	constraints.CpuPower,
   342  	constraints.InstanceType,
   343  	constraints.Tags,
   344  }
   345  
   346  // ConstraintsValidator is defined on the Environs interface.
   347  func (env *localEnviron) ConstraintsValidator() (constraints.Validator, error) {
   348  	validator := constraints.NewValidator()
   349  	validator.RegisterUnsupported(unsupportedConstraints)
   350  	supportedArches, err := env.SupportedArchitectures()
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  	validator.RegisterVocabulary(constraints.Arch, supportedArches)
   355  	return validator, nil
   356  }
   357  
   358  // MaintainInstance is specified in the InstanceBroker interface.
   359  func (*localEnviron) MaintainInstance(args environs.StartInstanceParams) error {
   360  	return nil
   361  }
   362  
   363  // StartInstance is specified in the InstanceBroker interface.
   364  func (env *localEnviron) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
   365  	if args.InstanceConfig.HasNetworks() {
   366  		return nil, fmt.Errorf("starting instances with networks is not supported yet.")
   367  	}
   368  	series := args.Tools.OneSeries()
   369  	logger.Debugf("StartInstance: %q, %s", args.InstanceConfig.MachineId, series)
   370  	args.InstanceConfig.Tools = args.Tools[0]
   371  
   372  	args.InstanceConfig.MachineContainerType = env.config.container()
   373  	logger.Debugf("tools: %#v", args.InstanceConfig.Tools)
   374  	if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.config.Config); err != nil {
   375  		return nil, err
   376  	}
   377  	// TODO: evaluate the impact of setting the constraints on the
   378  	// instanceConfig for all machines rather than just state server nodes.
   379  	// This limitation is why the constraints are assigned directly here.
   380  	args.InstanceConfig.Constraints = args.Constraints
   381  	args.InstanceConfig.AgentEnvironment[agent.Namespace] = env.config.namespace()
   382  	inst, hardware, err := createContainer(env, args)
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  	return &environs.StartInstanceResult{
   387  		Instance: inst,
   388  		Hardware: hardware,
   389  	}, nil
   390  }
   391  
   392  // Override for testing.
   393  var createContainer = func(env *localEnviron, args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) {
   394  	series := args.Tools.OneSeries()
   395  	network := container.BridgeNetworkConfig(env.config.networkBridge(), 0, args.NetworkInfo)
   396  	allowLoopMounts, _ := env.config.AllowLXCLoopMounts()
   397  	isLXC := env.config.container() == instance.LXC
   398  	storage := &container.StorageConfig{
   399  		AllowMount: !isLXC || allowLoopMounts,
   400  	}
   401  	inst, hardware, err := env.containerManager.CreateContainer(args.InstanceConfig, series, network, storage)
   402  	if err != nil {
   403  		return nil, nil, err
   404  	}
   405  	return inst, hardware, nil
   406  }
   407  
   408  // StopInstances is specified in the InstanceBroker interface.
   409  func (env *localEnviron) StopInstances(ids ...instance.Id) error {
   410  	for _, id := range ids {
   411  		if id == bootstrapInstanceId {
   412  			return fmt.Errorf("cannot stop the bootstrap instance")
   413  		}
   414  		if err := env.containerManager.DestroyContainer(id); err != nil {
   415  			return err
   416  		}
   417  	}
   418  	return nil
   419  }
   420  
   421  // Instances is specified in the Environ interface.
   422  func (env *localEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
   423  	if len(ids) == 0 {
   424  		return nil, nil
   425  	}
   426  	insts, err := env.AllInstances()
   427  	if err != nil {
   428  		return nil, err
   429  	}
   430  	allInstances := make(map[instance.Id]instance.Instance)
   431  	for _, inst := range insts {
   432  		allInstances[inst.Id()] = inst
   433  	}
   434  	var found int
   435  	insts = make([]instance.Instance, len(ids))
   436  	for i, id := range ids {
   437  		if inst, ok := allInstances[id]; ok {
   438  			insts[i] = inst
   439  			found++
   440  		}
   441  	}
   442  	if found == 0 {
   443  		insts, err = nil, environs.ErrNoInstances
   444  	} else if found < len(ids) {
   445  		err = environs.ErrPartialInstances
   446  	} else {
   447  		err = nil
   448  	}
   449  	return insts, err
   450  }
   451  
   452  // AllInstances is specified in the InstanceBroker interface.
   453  func (env *localEnviron) AllInstances() (instances []instance.Instance, err error) {
   454  	instances = append(instances, &localInstance{bootstrapInstanceId, env})
   455  	// Add in all the containers as well.
   456  	lxcInstances, err := env.containerManager.ListContainers()
   457  	if err != nil {
   458  		return nil, err
   459  	}
   460  	for _, inst := range lxcInstances {
   461  		instances = append(instances, &localInstance{inst.Id(), env})
   462  	}
   463  	return instances, nil
   464  }
   465  
   466  // Storage is specified in the Environ interface.
   467  func (env *localEnviron) Storage() storage.Storage {
   468  	// localStorage is non-nil if we're running from the CLI
   469  	if env.localStorage != nil {
   470  		return env.localStorage
   471  	}
   472  	return httpstorage.Client(env.config.storageAddr())
   473  }
   474  
   475  // Destroy is specified in the Environ interface.
   476  func (env *localEnviron) Destroy() error {
   477  	// If bootstrap failed, for example because the user
   478  	// lacks sudo rights, then the agents won't have been
   479  	// installed. If that's the case, we can just remove
   480  	// the data-dir and exit.
   481  	agentsDir := filepath.Join(env.config.rootDir(), "agents")
   482  	if _, err := os.Stat(agentsDir); os.IsNotExist(err) {
   483  		// If we can't remove the root dir, then continue
   484  		// and attempt as root anyway.
   485  		if os.RemoveAll(env.config.rootDir()) == nil {
   486  			return nil
   487  		}
   488  	}
   489  	if !checkIfRoot() {
   490  		juju, err := exec.LookPath(os.Args[0])
   491  		if err != nil {
   492  			return err
   493  		}
   494  		args := []string{
   495  			"env", osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(),
   496  			juju, "destroy-environment", "-y", "--force", env.Config().Name(),
   497  		}
   498  		cmd := exec.Command("sudo", args...)
   499  		cmd.Stdout = os.Stdout
   500  		cmd.Stderr = os.Stderr
   501  		return cmd.Run()
   502  	}
   503  	// Kill all running instances. This must be done as
   504  	// root, or listing/stopping containers will fail.
   505  	containers, err := env.containerManager.ListContainers()
   506  	if err != nil {
   507  		return err
   508  	}
   509  	for _, inst := range containers {
   510  		if err := env.containerManager.DestroyContainer(inst.Id()); err != nil {
   511  			return err
   512  		}
   513  	}
   514  	cmd := exec.Command(
   515  		"pkill",
   516  		fmt.Sprintf("-%d", terminationworker.TerminationSignal),
   517  		"-f", filepath.Join(regexp.QuoteMeta(env.config.rootDir()), ".*", "jujud"),
   518  	)
   519  	if err := cmd.Run(); err != nil {
   520  		if err, ok := err.(*exec.ExitError); ok {
   521  			// Exit status 1 means no processes were matched:
   522  			// we don't consider this an error here.
   523  			if err.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() != 1 {
   524  				return errors.Annotate(err, "failed to kill jujud")
   525  			}
   526  		}
   527  	}
   528  	// Stop the mongo database and machine agent. We log any errors but
   529  	// do not fail, so that remaining "destroy" steps will still happen.
   530  	//
   531  	// We run through twice, since this races with the agent's teardown.
   532  	// We only log errors on the second time through, since if an error
   533  	// occurred, we sould expect it to be due to the service no longer
   534  	// existing.
   535  	for i := 0; i < 2; i++ {
   536  		err = mongoRemoveService(env.config.namespace())
   537  		if err != nil && !errors.IsNotFound(err) && i > 0 {
   538  			logger.Errorf("while stopping mongod: %v", err)
   539  		}
   540  		svc, err := discoverService(env.machineAgentServiceName())
   541  		if err == nil {
   542  			if err := svc.Stop(); err != nil && i > 0 {
   543  				logger.Errorf("while stopping machine agent: %v", err)
   544  			}
   545  			if err := svc.Remove(); err != nil && i > 0 {
   546  				logger.Errorf("while disabling machine agent: %v", err)
   547  			}
   548  		}
   549  	}
   550  
   551  	// Finally, remove the data-dir.
   552  	if err := os.RemoveAll(env.config.rootDir()); err != nil && !os.IsNotExist(err) {
   553  		// Before we return the error, just check to see if the directory is
   554  		// there. There is a race condition with the agent with the removing
   555  		// of the directory, and due to a bug
   556  		// (https://code.google.com/p/go/issues/detail?id=7776) the
   557  		// os.IsNotExist error isn't always returned.
   558  		if _, statErr := os.Stat(env.config.rootDir()); os.IsNotExist(statErr) {
   559  			return nil
   560  		}
   561  		return err
   562  	}
   563  	return nil
   564  }
   565  
   566  type agentService interface {
   567  	Stop() error
   568  	Remove() error
   569  }
   570  
   571  var mongoRemoveService = func(namespace string) error {
   572  	return mongo.RemoveService(namespace)
   573  }
   574  
   575  var discoverService = func(name string) (agentService, error) {
   576  	return service.DiscoverService(name, servicecommon.Conf{})
   577  }
   578  
   579  // OpenPorts is specified in the Environ interface.
   580  func (env *localEnviron) OpenPorts(ports []network.PortRange) error {
   581  	return fmt.Errorf("open ports not implemented")
   582  }
   583  
   584  // ClosePorts is specified in the Environ interface.
   585  func (env *localEnviron) ClosePorts(ports []network.PortRange) error {
   586  	return fmt.Errorf("close ports not implemented")
   587  }
   588  
   589  // Ports is specified in the Environ interface.
   590  func (env *localEnviron) Ports() ([]network.PortRange, error) {
   591  	return nil, nil
   592  }
   593  
   594  // Provider is specified in the Environ interface.
   595  func (env *localEnviron) Provider() environs.EnvironProvider {
   596  	return providerInstance
   597  }