github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/provider/ec2/environ_vpc.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package ec2
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/utils/set"
    12  	"gopkg.in/amz.v3/ec2"
    13  
    14  	"github.com/juju/juju/environs"
    15  	"github.com/juju/juju/network"
    16  )
    17  
    18  const (
    19  	activeState           = "active"
    20  	availableState        = "available"
    21  	localRouteGatewayID   = "local"
    22  	defaultRouteCIDRBlock = "0.0.0.0/0"
    23  	vpcIDNone             = "none"
    24  )
    25  
    26  var (
    27  	vpcNotUsableForBootstrapErrorPrefix = `
    28  Juju cannot use the given vpc-id for bootstrapping a controller
    29  instance. Please, double check the given VPC ID is correct, and that
    30  the VPC contains at least one subnet.
    31  
    32  Error details`[1:]
    33  
    34  	vpcNotUsableForModelErrorPrefix = `
    35  Juju cannot use the given vpc-id for the model being added.
    36  Please double check the given VPC ID is correct, and that
    37  the VPC contains at least one subnet.
    38  
    39  Error details`[1:]
    40  
    41  	vpcNotRecommendedErrorPrefix = `
    42  The given vpc-id does not meet one or more of the following minimum
    43  Juju requirements:
    44  
    45  1. VPC should be in "available" state and contain one or more subnets.
    46  2. An Internet Gateway (IGW) should be attached to the VPC.
    47  3. The main route table of the VPC should have both a default route
    48     to the attached IGW and a local route matching the VPC CIDR block.
    49  4. At least one of the VPC subnets should have MapPublicIPOnLaunch
    50     attribute enabled (i.e. at least one subnet needs to be 'public').
    51  5. All subnets should be implicitly associated to the VPC main route
    52     table, rather than explicitly to per-subnet route tables.
    53  
    54  A default VPC already satisfies all of the requirements above. If you
    55  still want to use the VPC, try running 'juju bootstrap' again with:
    56  
    57    --config vpc-id=%s --config vpc-id-force=true
    58  
    59  to force Juju to bypass the requirements check (NOT recommended unless
    60  you understand the implications: most importantly, not being able to
    61  access the Juju controller, likely causing bootstrap to fail, or trying
    62  to deploy exposed workloads on instances started in private or isolated
    63  subnets).
    64  
    65  Error details`[1:]
    66  
    67  	cannotValidateVPCErrorPrefix = `
    68  Juju could not verify whether the given vpc-id meets the minumum Juju
    69  connectivity requirements. Please, double check the VPC ID is correct,
    70  you have a working connection to the Internet, your AWS credentials are
    71  sufficient to access VPC features, or simply retry bootstrapping again.
    72  
    73  Error details`[1:]
    74  
    75  	vpcNotRecommendedButForcedWarning = `
    76  WARNING! The specified vpc-id does not satisfy the minimum Juju requirements,
    77  but will be used anyway because vpc-id-force=true is also specified.
    78  
    79  `[1:]
    80  )
    81  
    82  // vpcNotUsableError indicates a user-specified VPC cannot be used either
    83  // because it is missing or because it contains no subnets.
    84  type vpcNotUsableError struct {
    85  	errors.Err
    86  }
    87  
    88  // vpcNotUsablef returns an error which satisfies isVPCNotUsableError().
    89  func vpcNotUsablef(optionalCause error, format string, args ...interface{}) error {
    90  	outerErr := errors.Errorf(format, args...)
    91  	if optionalCause != nil {
    92  		outerErr = errors.Maskf(optionalCause, format, args...)
    93  	}
    94  
    95  	innerErr, _ := outerErr.(*errors.Err) // cannot fail.
    96  	return &vpcNotUsableError{*innerErr}
    97  }
    98  
    99  // isVPCNotUsableError reports whether err was created with vpcNotUsablef().
   100  func isVPCNotUsableError(err error) bool {
   101  	err = errors.Cause(err)
   102  	_, ok := err.(*vpcNotUsableError)
   103  	return ok
   104  }
   105  
   106  // vpcNotRecommendedError indicates a user-specified VPC is unlikely to be
   107  // suitable for hosting a Juju controller instance and/or exposed workloads, due
   108  // to not satisfying the mininum requirements described in validateVPC()'s doc
   109  // comment. Users can still force Juju to use such a VPC by passing
   110  // 'vpc-id-force=true' setting.
   111  type vpcNotRecommendedError struct {
   112  	errors.Err
   113  }
   114  
   115  // vpcNotRecommendedf returns an error which satisfies isVPCNotRecommendedError().
   116  func vpcNotRecommendedf(format string, args ...interface{}) error {
   117  	outerErr := errors.Errorf(format, args...)
   118  	innerErr, _ := outerErr.(*errors.Err) // cannot fail.
   119  	return &vpcNotRecommendedError{*innerErr}
   120  }
   121  
   122  // isVPCNotRecommendedError reports whether err was created with vpcNotRecommendedf().
   123  func isVPCNotRecommendedError(err error) bool {
   124  	err = errors.Cause(err)
   125  	_, ok := err.(*vpcNotRecommendedError)
   126  	return ok
   127  }
   128  
   129  // vpcAPIClient defines a subset of the goamz API calls needed to validate a VPC.
   130  type vpcAPIClient interface {
   131  	// AccountAttributes, called with the "default-vpc" attribute. is used to
   132  	// find the ID of the region's default VPC (if any).
   133  	AccountAttributes(attributeNames ...string) (*ec2.AccountAttributesResp, error)
   134  
   135  	// VPCs is used to get details for the VPC being validated, including
   136  	// whether it exists, is available, and its CIDRBlock and IsDefault fields.
   137  	VPCs(ids []string, filter *ec2.Filter) (*ec2.VPCsResp, error)
   138  
   139  	// Subnets is used to get a list of all subnets of the validated VPC (if
   140  	// any),including their Id, AvailZone, and MapPublicIPOnLaunch fields.
   141  	Subnets(ids []string, filter *ec2.Filter) (*ec2.SubnetsResp, error)
   142  
   143  	// InternetGateways is used to get details of the Internet Gateway (IGW)
   144  	// attached to the validated VPC (if any), its Id to check against routes,
   145  	// and whether it's available.
   146  	InternetGateways(ids []string, filter *ec2.Filter) (*ec2.InternetGatewaysResp, error)
   147  
   148  	// RouteTables is used to find the main route table of the VPC (if any),
   149  	// whether it includes a default route to the attached IGW, a local route to
   150  	// the VPC CIDRBlock, and any per-subnet route tables.
   151  	RouteTables(ids []string, filter *ec2.Filter) (*ec2.RouteTablesResp, error)
   152  }
   153  
   154  // validateVPC requires both arguments to be set and validates that vpcID refers
   155  // to an existing AWS VPC (default or non-default) for the current region.
   156  // Returns an error satifying isVPCNotUsableError() when the VPC with the given
   157  // vpcID cannot be found, or when the VPC exists but contains no subnets.
   158  // Returns an error satisfying isVPCNotRecommendedError() in the following
   159  // cases:
   160  //
   161  // 1. The VPC's state is not "available".
   162  // 2. The VPC does not have an Internet Gateway (IGW) attached.
   163  // 3. A main route table is not associated with the VPC.
   164  // 4. The main route table lacks both a default route via the IGW and a local
   165  //    route matching the VPC's CIDR block.
   166  // 5. One or more of the VPC's subnets are not associated with the main route
   167  //    table of the VPC.
   168  // 6. None of the the VPC's subnets have the MapPublicIPOnLaunch attribute set.
   169  //
   170  // With the vpc-id-force config setting set to true, the provider can ignore a
   171  // vpcNotRecommendedError. A vpcNotUsableError cannot be ignored, while
   172  // unexpected API responses and errors could be retried.
   173  //
   174  // The above minimal requirements allow Juju to work out-of-the-box with most
   175  // common (and officially documented by AWS) VPC setups, easy try out with AWS
   176  // Console / VPC Wizard / CLI. Detecting VPC setups indicating intentional
   177  // customization by experienced users, protecting beginners from bad Juju-UX due
   178  // to broken VPC setup, while still allowing power users to override that and
   179  // continue (but knowing what that implies).
   180  func validateVPC(apiClient vpcAPIClient, vpcID string) error {
   181  	if vpcID == "" || apiClient == nil {
   182  		return errors.Errorf("invalid arguments: empty VPC ID or nil client")
   183  	}
   184  
   185  	vpc, err := getVPCByID(apiClient, vpcID)
   186  	if err != nil {
   187  		return errors.Trace(err)
   188  	}
   189  
   190  	if err := checkVPCIsAvailable(vpc); err != nil {
   191  		return errors.Trace(err)
   192  	}
   193  
   194  	subnets, err := getVPCSubnets(apiClient, vpc)
   195  	if err != nil {
   196  		return errors.Trace(err)
   197  	}
   198  
   199  	publicSubnet, err := findFirstPublicSubnet(subnets)
   200  	if err != nil {
   201  		return errors.Trace(err)
   202  	}
   203  
   204  	// TODO(dimitern): Rather than just logging that, use publicSubnet.Id or
   205  	// even publicSubnet.AvailZone as default bootstrap placement directive, so
   206  	// the controller would be reachable.
   207  	logger.Infof(
   208  		"found subnet %q (%s) in AZ %q, suitable for a Juju controller instance",
   209  		publicSubnet.Id, publicSubnet.CIDRBlock, publicSubnet.AvailZone,
   210  	)
   211  
   212  	gateway, err := getVPCInternetGateway(apiClient, vpc)
   213  	if err != nil {
   214  		return errors.Trace(err)
   215  	}
   216  
   217  	if err := checkInternetGatewayIsAvailable(gateway); err != nil {
   218  		return errors.Trace(err)
   219  	}
   220  
   221  	routeTables, err := getVPCRouteTables(apiClient, vpc)
   222  	if err != nil {
   223  		return errors.Trace(err)
   224  	}
   225  
   226  	mainRouteTable, err := findVPCMainRouteTable(routeTables)
   227  	if err != nil {
   228  		return errors.Trace(err)
   229  	}
   230  
   231  	if err := checkVPCRouteTableRoutes(vpc, mainRouteTable, gateway); err != nil {
   232  		return errors.Annotatef(err, "VPC %q main route table %q", vpcID, mainRouteTable.Id)
   233  	}
   234  
   235  	logger.Infof("VPC %q is suitable for Juju controllers and expose-able workloads", vpc.Id)
   236  	return nil
   237  }
   238  
   239  func getVPCByID(apiClient vpcAPIClient, vpcID string) (*ec2.VPC, error) {
   240  	response, err := apiClient.VPCs([]string{vpcID}, nil)
   241  	if isVPCNotFoundError(err) {
   242  		return nil, vpcNotUsablef(err, "")
   243  	} else if err != nil {
   244  		return nil, errors.Annotatef(err, "unexpected AWS response getting VPC %q", vpcID)
   245  	}
   246  
   247  	if numResults := len(response.VPCs); numResults == 0 {
   248  		return nil, vpcNotUsablef(nil, "VPC %q not found", vpcID)
   249  	} else if numResults > 1 {
   250  		logger.Debugf("VPCs() returned %#v", response)
   251  		return nil, errors.Errorf("expected 1 result from AWS, got %d", numResults)
   252  	}
   253  
   254  	vpc := response.VPCs[0]
   255  	return &vpc, nil
   256  }
   257  
   258  func isVPCNotFoundError(err error) bool {
   259  	return err != nil && ec2ErrCode(err) == "InvalidVpcID.NotFound"
   260  }
   261  
   262  func checkVPCIsAvailable(vpc *ec2.VPC) error {
   263  	if vpc.State != availableState {
   264  		return vpcNotRecommendedf("VPC has unexpected state %q", vpc.State)
   265  	}
   266  
   267  	if vpc.IsDefault {
   268  		logger.Infof("VPC %q is the default VPC for the region", vpc.Id)
   269  	}
   270  
   271  	return nil
   272  }
   273  
   274  func getVPCSubnets(apiClient vpcAPIClient, vpc *ec2.VPC) ([]ec2.Subnet, error) {
   275  	filter := ec2.NewFilter()
   276  	filter.Add("vpc-id", vpc.Id)
   277  	response, err := apiClient.Subnets(nil, filter)
   278  	if err != nil {
   279  		return nil, errors.Annotatef(err, "unexpected AWS response getting subnets of VPC %q", vpc.Id)
   280  	}
   281  
   282  	if len(response.Subnets) == 0 {
   283  		return nil, vpcNotUsablef(nil, "no subnets found for VPC %q", vpc.Id)
   284  	}
   285  
   286  	return response.Subnets, nil
   287  }
   288  
   289  func findFirstPublicSubnet(subnets []ec2.Subnet) (*ec2.Subnet, error) {
   290  	for _, subnet := range subnets {
   291  		// TODO(dimitern): goamz's AddDefaultVPCAndSubnets() does not set
   292  		// MapPublicIPOnLaunch only DefaultForAZ, but in reality the former is
   293  		// always set when the latter is. Until this is fixed in goamz, we check
   294  		// for both below to allow testing the behavior.
   295  		if subnet.MapPublicIPOnLaunch || subnet.DefaultForAZ {
   296  			logger.Debugf(
   297  				"VPC %q subnet %q has MapPublicIPOnLaunch=%v, DefaultForAZ=%v",
   298  				subnet.VPCId, subnet.Id, subnet.MapPublicIPOnLaunch, subnet.DefaultForAZ,
   299  			)
   300  			return &subnet, nil
   301  		}
   302  
   303  	}
   304  	return nil, vpcNotRecommendedf("VPC contains no public subnets")
   305  }
   306  
   307  func getVPCInternetGateway(apiClient vpcAPIClient, vpc *ec2.VPC) (*ec2.InternetGateway, error) {
   308  	filter := ec2.NewFilter()
   309  	filter.Add("attachment.vpc-id", vpc.Id)
   310  	response, err := apiClient.InternetGateways(nil, filter)
   311  	if err != nil {
   312  		return nil, errors.Annotatef(err, "unexpected AWS response getting Internet Gateway of VPC %q", vpc.Id)
   313  	}
   314  
   315  	if numResults := len(response.InternetGateways); numResults == 0 {
   316  		return nil, vpcNotRecommendedf("VPC has no Internet Gateway attached")
   317  	} else if numResults > 1 {
   318  		logger.Debugf("InternetGateways() returned %#v", response)
   319  		return nil, errors.Errorf("expected 1 result from AWS, got %d", numResults)
   320  	}
   321  
   322  	gateway := response.InternetGateways[0]
   323  	return &gateway, nil
   324  }
   325  
   326  func checkInternetGatewayIsAvailable(gateway *ec2.InternetGateway) error {
   327  	if state := gateway.AttachmentState; state != availableState {
   328  		return vpcNotRecommendedf("VPC has Internet Gateway %q in unexpected state %q", gateway.Id, state)
   329  	}
   330  
   331  	return nil
   332  }
   333  
   334  func getVPCRouteTables(apiClient vpcAPIClient, vpc *ec2.VPC) ([]ec2.RouteTable, error) {
   335  	filter := ec2.NewFilter()
   336  	filter.Add("vpc-id", vpc.Id)
   337  	response, err := apiClient.RouteTables(nil, filter)
   338  	if err != nil {
   339  		return nil, errors.Annotatef(err, "unexpected AWS response getting route tables of VPC %q", vpc.Id)
   340  	}
   341  
   342  	if len(response.Tables) == 0 {
   343  		return nil, vpcNotRecommendedf("VPC has no route tables")
   344  	}
   345  	logger.Tracef("RouteTables() returned %#v", response)
   346  
   347  	return response.Tables, nil
   348  }
   349  
   350  func findVPCMainRouteTable(routeTables []ec2.RouteTable) (*ec2.RouteTable, error) {
   351  	var mainTable *ec2.RouteTable
   352  	for i, table := range routeTables {
   353  		if len(table.Associations) < 1 {
   354  			logger.Tracef("ignoring VPC %q route table %q with no associations", table.VPCId, table.Id)
   355  			continue
   356  		}
   357  
   358  		for _, association := range table.Associations {
   359  			// TODO(dimitern): Of all the requirements, this seems like the most
   360  			// strict and likely to push users to use vpc-id-force=true. On the
   361  			// other hand, having to deal with more than the main route table's
   362  			// routes will likely overcomplicate the routes checks that follow.
   363  			if subnetID := association.SubnetId; subnetID != "" {
   364  				return nil, vpcNotRecommendedf("subnet %q not associated with VPC %q main route table", subnetID, table.VPCId)
   365  			}
   366  
   367  			if association.IsMain && mainTable == nil {
   368  				logger.Tracef("main route table of VPC %q has ID %q", table.VPCId, table.Id)
   369  				mainTable = &routeTables[i]
   370  			}
   371  		}
   372  	}
   373  
   374  	if mainTable == nil {
   375  		return nil, vpcNotRecommendedf("VPC has no associated main route table")
   376  	}
   377  
   378  	return mainTable, nil
   379  }
   380  
   381  func checkVPCRouteTableRoutes(vpc *ec2.VPC, routeTable *ec2.RouteTable, gateway *ec2.InternetGateway) error {
   382  	hasDefaultRoute := false
   383  	hasLocalRoute := false
   384  
   385  	logger.Tracef("checking route table %+v routes", routeTable)
   386  	for _, route := range routeTable.Routes {
   387  		if route.State != activeState {
   388  			logger.Tracef("skipping inactive route %+v", route)
   389  			continue
   390  		}
   391  
   392  		switch route.DestinationCIDRBlock {
   393  		case defaultRouteCIDRBlock:
   394  			if route.GatewayId == gateway.Id {
   395  				logger.Tracef("default route uses expected gateway %q", gateway.Id)
   396  				hasDefaultRoute = true
   397  			}
   398  		case vpc.CIDRBlock:
   399  			if route.GatewayId == localRouteGatewayID {
   400  				logger.Tracef("local route uses expected CIDR %q", vpc.CIDRBlock)
   401  				hasLocalRoute = true
   402  			}
   403  		default:
   404  			logger.Tracef("route %+v is neither local nor default (skipping)", route)
   405  		}
   406  	}
   407  
   408  	if hasDefaultRoute && hasLocalRoute {
   409  		return nil
   410  	}
   411  
   412  	if !hasDefaultRoute {
   413  		return vpcNotRecommendedf("missing default route via gateway %q", gateway.Id)
   414  	}
   415  	return vpcNotRecommendedf("missing local route with destination %q", vpc.CIDRBlock)
   416  }
   417  
   418  func findDefaultVPCID(apiClient vpcAPIClient) (string, error) {
   419  	response, err := apiClient.AccountAttributes("default-vpc")
   420  	if err != nil {
   421  		return "", errors.Annotate(err, "unexpected AWS response getting default-vpc account attribute")
   422  	}
   423  
   424  	if len(response.Attributes) == 0 ||
   425  		len(response.Attributes[0].Values) == 0 ||
   426  		response.Attributes[0].Name != "default-vpc" {
   427  		// No value for the requested "default-vpc" attribute, all bets are off.
   428  		return "", errors.NotFoundf("default-vpc account attribute")
   429  	}
   430  
   431  	firstAttributeValue := response.Attributes[0].Values[0]
   432  	if firstAttributeValue == vpcIDNone {
   433  		return "", errors.NotFoundf("default VPC")
   434  	}
   435  
   436  	return firstAttributeValue, nil
   437  }
   438  
   439  // getVPCSubnetIDsForAvailabilityZone returns a sorted list of subnet IDs, which
   440  // are both in the given vpcID and the given zoneName. If allowedSubnetIDs is
   441  // not empty, the returned list will only contain IDs present there. Returns an
   442  // error satisfying errors.IsNotFound() when no results match.
   443  func getVPCSubnetIDsForAvailabilityZone(
   444  	apiClient vpcAPIClient,
   445  	vpcID, zoneName string,
   446  	allowedSubnetIDs []string,
   447  ) ([]string, error) {
   448  	allowedSubnets := set.NewStrings(allowedSubnetIDs...)
   449  	vpc := &ec2.VPC{Id: vpcID}
   450  	subnets, err := getVPCSubnets(apiClient, vpc)
   451  	if err != nil && !isVPCNotUsableError(err) {
   452  		return nil, errors.Annotatef(err, "cannot get VPC %q subnets", vpcID)
   453  	} else if isVPCNotUsableError(err) {
   454  		// We're reusing getVPCSubnets(), but not while validating a VPC
   455  		// pre-bootstrap, so we should change vpcNotUsableError to a simple
   456  		// NotFoundError.
   457  		message := fmt.Sprintf("VPC %q has no subnets in AZ %q", vpcID, zoneName)
   458  		return nil, errors.NewNotFound(err, message)
   459  	}
   460  
   461  	matchingSubnetIDs := set.NewStrings()
   462  	for _, subnet := range subnets {
   463  		if subnet.AvailZone != zoneName {
   464  			logger.Infof("skipping subnet %q (in VPC %q): not in the chosen AZ %q", subnet.Id, vpcID, zoneName)
   465  			continue
   466  		}
   467  		if !allowedSubnets.IsEmpty() && !allowedSubnets.Contains(subnet.Id) {
   468  			logger.Infof("skipping subnet %q (in VPC %q, AZ %q): not matching spaces constraints", subnet.Id, vpcID, zoneName)
   469  			continue
   470  		}
   471  		matchingSubnetIDs.Add(subnet.Id)
   472  	}
   473  
   474  	if matchingSubnetIDs.IsEmpty() {
   475  		message := fmt.Sprintf("VPC %q has no subnets in AZ %q", vpcID, zoneName)
   476  		return nil, errors.NewNotFound(nil, message)
   477  	}
   478  
   479  	sortedIDs := matchingSubnetIDs.SortedValues()
   480  	logger.Infof("found %d subnets in VPC %q matching AZ %q and constraints: %v", len(sortedIDs), vpcID, zoneName, sortedIDs)
   481  	return sortedIDs, nil
   482  }
   483  
   484  func findSubnetIDsForAvailabilityZone(zoneName string, subnetsToZones map[network.Id][]string) ([]string, error) {
   485  	matchingSubnetIDs := set.NewStrings()
   486  	for subnetID, zones := range subnetsToZones {
   487  		zonesSet := set.NewStrings(zones...)
   488  		if zonesSet.Contains(zoneName) {
   489  			matchingSubnetIDs.Add(string(subnetID))
   490  		}
   491  	}
   492  
   493  	if matchingSubnetIDs.IsEmpty() {
   494  		return nil, errors.NotFoundf("subnets in AZ %q", zoneName)
   495  	}
   496  
   497  	return matchingSubnetIDs.SortedValues(), nil
   498  }
   499  
   500  func isVPCIDSetButInvalid(vpcID string) bool {
   501  	return isVPCIDSet(vpcID) && !strings.HasPrefix(vpcID, "vpc-")
   502  }
   503  
   504  func isVPCIDSet(vpcID string) bool {
   505  	return vpcID != "" && vpcID != vpcIDNone
   506  }
   507  
   508  func validateBootstrapVPC(apiClient vpcAPIClient, region, vpcID string, forceVPCID bool, ctx environs.BootstrapContext) error {
   509  	if vpcID == vpcIDNone {
   510  		ctx.Infof("Using EC2-classic features or default VPC in region %q", region)
   511  	}
   512  	if !isVPCIDSet(vpcID) {
   513  		return nil
   514  	}
   515  
   516  	err := validateVPC(apiClient, vpcID)
   517  	switch {
   518  	case isVPCNotUsableError(err):
   519  		// VPC missing or has no subnets at all.
   520  		return errors.Annotate(err, vpcNotUsableForBootstrapErrorPrefix)
   521  	case isVPCNotRecommendedError(err):
   522  		// VPC does not meet minumum validation criteria.
   523  		if !forceVPCID {
   524  			return errors.Annotatef(err, vpcNotRecommendedErrorPrefix, vpcID)
   525  		}
   526  		ctx.Infof(vpcNotRecommendedButForcedWarning)
   527  	case err != nil:
   528  		// Anything else unexpected while validating the VPC.
   529  		return errors.Annotate(err, cannotValidateVPCErrorPrefix)
   530  	}
   531  
   532  	ctx.Infof("Using VPC %q in region %q", vpcID, region)
   533  
   534  	return nil
   535  }
   536  
   537  func validateModelVPC(apiClient vpcAPIClient, modelName, vpcID string) error {
   538  	if !isVPCIDSet(vpcID) {
   539  		return nil
   540  	}
   541  
   542  	err := validateVPC(apiClient, vpcID)
   543  	switch {
   544  	case isVPCNotUsableError(err):
   545  		// VPC missing or has no subnets at all.
   546  		return errors.Annotate(err, vpcNotUsableForModelErrorPrefix)
   547  	case isVPCNotRecommendedError(err):
   548  		// VPC does not meet minumum validation criteria, but that's less
   549  		// important for hosted models, as the controller is already accessible.
   550  		logger.Infof(
   551  			"Juju will use, but does not recommend using VPC %q: %v",
   552  			vpcID, err.Error(),
   553  		)
   554  	case err != nil:
   555  		// Anything else unexpected while validating the VPC.
   556  		return errors.Annotate(err, cannotValidateVPCErrorPrefix)
   557  	}
   558  	logger.Infof("Using VPC %q for model %q", vpcID, modelName)
   559  
   560  	return nil
   561  }