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