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 }