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