github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/openstack/firewaller.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package openstack 5 6 import ( 7 "fmt" 8 "regexp" 9 "strings" 10 "sync" 11 "time" 12 13 gooseerrors "github.com/go-goose/goose/v5/errors" 14 "github.com/go-goose/goose/v5/neutron" 15 "github.com/juju/clock" 16 "github.com/juju/errors" 17 "github.com/juju/retry" 18 19 "github.com/juju/juju/core/instance" 20 corenetwork "github.com/juju/juju/core/network" 21 "github.com/juju/juju/core/network/firewall" 22 "github.com/juju/juju/environs" 23 "github.com/juju/juju/environs/config" 24 "github.com/juju/juju/environs/context" 25 "github.com/juju/juju/environs/instances" 26 "github.com/juju/juju/provider/common" 27 ) 28 29 const ( 30 validUUID = `[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}` 31 GroupControllerPattern = `^(?P<prefix>juju-)(?P<controllerUUID>` + validUUID + `)(?P<suffix>-.*)$` 32 ) 33 34 var extractControllerRe = regexp.MustCompile(GroupControllerPattern) 35 36 var shortRetryStrategy = retry.CallArgs{ 37 Clock: clock.WallClock, 38 MaxDuration: 5 * time.Second, 39 Delay: 200 * time.Millisecond, 40 } 41 42 // FirewallerFactory for obtaining firewaller object. 43 type FirewallerFactory interface { 44 GetFirewaller(env environs.Environ) Firewaller 45 } 46 47 // Firewaller allows custom openstack provider behaviour. 48 // This is used in other providers that embed the openstack provider. 49 type Firewaller interface { 50 // OpenPorts opens the given port ranges for the whole environment. 51 OpenPorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error 52 53 // ClosePorts closes the given port ranges for the whole environment. 54 ClosePorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error 55 56 // IngressRules returns the ingress rules applied to the whole environment. 57 // It is expected that there be only one ingress rule result for a given 58 // port range - the rule's SourceCIDRs will contain all applicable source 59 // address rules for that port range. 60 IngressRules(ctx context.ProviderCallContext) (firewall.IngressRules, error) 61 62 // OpenModelPorts opens the given port ranges on the model firewall 63 OpenModelPorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error 64 65 // CloseModelPorts Closes the given port ranges on the model firewall 66 CloseModelPorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error 67 68 // ModelIngressRules returns the set of ingress rules on the model firewall. 69 // The rules are returned as sorted by network.SortIngressRules(). 70 // It is expected that there be only one ingress rule result for a given 71 // port range - the rule's SourceCIDRs will contain all applicable source 72 // address rules for that port range. 73 // If the model security group doesn't exist, return a NotFound error 74 ModelIngressRules(ctx context.ProviderCallContext) (firewall.IngressRules, error) 75 76 // DeleteAllModelGroups deletes all security groups for the 77 // model. 78 DeleteAllModelGroups(ctx context.ProviderCallContext) error 79 80 // DeleteAllControllerGroups deletes all security groups for the 81 // controller, ie those for all hosted models. 82 DeleteAllControllerGroups(ctx context.ProviderCallContext, controllerUUID string) error 83 84 // DeleteGroups deletes the security groups with the specified names. 85 DeleteGroups(ctx context.ProviderCallContext, names ...string) error 86 87 // UpdateGroupController updates all of the security groups for 88 // this model to refer to the specified controller, such that 89 // DeleteAllControllerGroups will remove them only when called 90 // with the specified controller ID. 91 UpdateGroupController(ctx context.ProviderCallContext, controllerUUID string) error 92 93 // GetSecurityGroups returns a list of the security groups that 94 // belong to given instances. 95 GetSecurityGroups(ctx context.ProviderCallContext, ids ...instance.Id) ([]string, error) 96 97 // SetUpGroups sets up initial security groups, if any, and returns 98 // their names. 99 SetUpGroups(ctx context.ProviderCallContext, controllerUUID, machineID string) ([]string, error) 100 101 // OpenInstancePorts opens the given port ranges for the specified instance. 102 OpenInstancePorts(ctx context.ProviderCallContext, inst instances.Instance, machineID string, rules firewall.IngressRules) error 103 104 // CloseInstancePorts closes the given port ranges for the specified instance. 105 CloseInstancePorts(ctx context.ProviderCallContext, inst instances.Instance, machineID string, rules firewall.IngressRules) error 106 107 // InstanceIngressRules returns the ingress rules applied to the specified instance. 108 InstanceIngressRules(ctx context.ProviderCallContext, inst instances.Instance, machineID string) (firewall.IngressRules, error) 109 } 110 111 type firewallerFactory struct{} 112 113 // GetFirewaller implements FirewallerFactory 114 func (f *firewallerFactory) GetFirewaller(env environs.Environ) Firewaller { 115 return &neutronFirewaller{firewallerBase{environ: env.(*Environ)}} 116 } 117 118 type firewallerBase struct { 119 environ *Environ 120 ensureGroupMutex sync.Mutex 121 } 122 123 // GetSecurityGroups implements Firewaller interface. 124 func (c *firewallerBase) GetSecurityGroups(ctx context.ProviderCallContext, ids ...instance.Id) ([]string, error) { 125 var securityGroupNames []string 126 if c.environ.Config().FirewallMode() == config.FwInstance { 127 instances, err := c.environ.Instances(ctx, ids) 128 if err != nil { 129 return nil, errors.Trace(err) 130 } 131 novaClient := c.environ.nova() 132 securityGroupNames = make([]string, 0, len(ids)) 133 for _, inst := range instances { 134 if inst == nil { 135 continue 136 } 137 serverID, err := instServerID(inst) 138 if err != nil { 139 return nil, errors.Trace(err) 140 } 141 groups, err := novaClient.GetServerSecurityGroups(string(inst.Id())) 142 if err != nil { 143 handleCredentialError(err, ctx) 144 return nil, errors.Trace(err) 145 } 146 for _, group := range groups { 147 // We only include the group specifically tied to the instance, not 148 // any group global to the model itself. 149 suffix := fmt.Sprintf("%s-%s", c.environ.Config().UUID(), serverID) 150 if strings.HasSuffix(group.Name, suffix) { 151 securityGroupNames = append(securityGroupNames, group.Name) 152 } 153 } 154 } 155 } 156 return securityGroupNames, nil 157 } 158 159 func instServerID(inst instances.Instance) (string, error) { 160 openstackName := inst.(*openstackInstance).getServerDetail().Name 161 lastDashPos := strings.LastIndex(openstackName, "-") 162 if lastDashPos == -1 { 163 return "", errors.Errorf("cannot identify machine ID in openstack server name %q", openstackName) 164 } 165 return openstackName[lastDashPos+1:], nil 166 } 167 168 func deleteSecurityGroupsMatchingName( 169 ctx context.ProviderCallContext, 170 deleteSecurityGroups func(ctx context.ProviderCallContext, match func(name string) bool) error, 171 prefix string, 172 ) error { 173 re, err := regexp.Compile("^" + prefix) 174 if err != nil { 175 return errors.Trace(err) 176 } 177 return deleteSecurityGroups(ctx, re.MatchString) 178 } 179 180 func deleteSecurityGroupsOneOfNames( 181 ctx context.ProviderCallContext, 182 deleteSecurityGroups func(ctx context.ProviderCallContext, match func(name string) bool) error, 183 names ...string, 184 ) error { 185 match := func(check string) bool { 186 for _, name := range names { 187 if check == name { 188 return true 189 } 190 } 191 return false 192 } 193 return deleteSecurityGroups(ctx, match) 194 } 195 196 // deleteSecurityGroup attempts to delete the security group. Should it fail, 197 // the deletion is retried due to timing issues in openstack. A security group 198 // cannot be deleted while it is in use. Theoretically we terminate all the 199 // instances before we attempt to delete the associated security groups, but 200 // in practice neutron hasn't always finished with the instance before it 201 // returns, so there is a race condition where we think the instance is 202 // terminated and hence attempt to delete the security groups but nova still 203 // has it around internally. To attempt to catch this timing issue, deletion 204 // of the groups is tried multiple times. 205 func deleteSecurityGroup( 206 ctx context.ProviderCallContext, 207 deleteSecurityGroupByID func(string) error, 208 name, id string, 209 clock clock.Clock, 210 ) { 211 logger.Debugf("deleting security group %q", name) 212 err := retry.Call(retry.CallArgs{ 213 Func: func() error { 214 if err := deleteSecurityGroupByID(id); err != nil { 215 if gooseerrors.IsNotFound(err) { 216 return nil 217 } 218 handleCredentialError(err, ctx) 219 return errors.Trace(err) 220 } 221 return nil 222 }, 223 NotifyFunc: func(err error, attempt int) { 224 if attempt%4 == 0 { 225 message := fmt.Sprintf("waiting to delete security group %q", name) 226 if attempt != 4 { 227 message = "still " + message 228 } 229 logger.Debugf(message) 230 } 231 }, 232 Attempts: 30, 233 Delay: time.Second, 234 Clock: clock, 235 }) 236 if err != nil { 237 logger.Warningf("cannot delete security group %q. Used by another model?", name) 238 } 239 } 240 241 func (c *firewallerBase) globalGroupName(controllerUUID string) string { 242 return fmt.Sprintf("%s-global", c.jujuGroupName(controllerUUID)) 243 } 244 245 func (c *firewallerBase) machineGroupName(controllerUUID, machineID string) string { 246 return fmt.Sprintf("%s-%s", c.jujuGroupName(controllerUUID), machineID) 247 } 248 249 func (c *firewallerBase) jujuGroupName(controllerUUID string) string { 250 cfg := c.environ.Config() 251 return fmt.Sprintf("juju-%v-%v", controllerUUID, cfg.UUID()) 252 } 253 254 func (c *firewallerBase) jujuControllerGroupPrefix(controllerUUID string) string { 255 return fmt.Sprintf("juju-%v-", controllerUUID) 256 } 257 258 func (c *firewallerBase) jujuGroupPrefixRegexp() string { 259 cfg := c.environ.Config() 260 return fmt.Sprintf("juju-.*-%v", cfg.UUID()) 261 } 262 263 func (c *firewallerBase) jujuGroupRegexp() string { 264 return fmt.Sprintf("%s$", c.jujuGroupPrefixRegexp()) 265 } 266 267 func (c *firewallerBase) globalGroupRegexp() string { 268 return fmt.Sprintf("%s-global", c.jujuGroupPrefixRegexp()) 269 } 270 271 func (c *firewallerBase) machineGroupRegexp(machineID string) string { 272 // we are only looking to match 1 machine 273 return fmt.Sprintf("%s-%s$", c.jujuGroupPrefixRegexp(), machineID) 274 } 275 276 type neutronFirewaller struct { 277 firewallerBase 278 } 279 280 // SetUpGroups creates the security groups for the new machine, and 281 // returns them. 282 // 283 // Instances are tagged with a group so they can be distinguished from 284 // other instances that might be running on the same OpenStack account. 285 // In addition, a specific machine security group is created for each 286 // machine, so that its firewall rules can be configured per machine. 287 // 288 // Note: ideally we'd have a better way to determine group membership so that 2 289 // people that happen to share an openstack account and name their environment 290 // "openstack" don't end up destroying each other's machines. 291 func (c *neutronFirewaller) SetUpGroups(ctx context.ProviderCallContext, controllerUUID, machineID string) ([]string, error) { 292 jujuGroup, err := c.ensureGroup(c.jujuGroupName(controllerUUID), true) 293 if err != nil { 294 return nil, errors.Trace(err) 295 } 296 var machineGroup neutron.SecurityGroupV2 297 switch c.environ.Config().FirewallMode() { 298 case config.FwInstance: 299 machineGroup, err = c.ensureGroup(c.machineGroupName(controllerUUID, machineID), false) 300 case config.FwGlobal: 301 machineGroup, err = c.ensureGroup(c.globalGroupName(controllerUUID), false) 302 } 303 if err != nil { 304 handleCredentialError(err, ctx) 305 return nil, errors.Trace(err) 306 } 307 groups := []string{jujuGroup.Name, machineGroup.Name} 308 if c.environ.ecfg().useDefaultSecurityGroup() { 309 groups = append(groups, "default") 310 } 311 return groups, nil 312 } 313 314 // zeroGroup holds the zero security group. 315 var zeroGroup neutron.SecurityGroupV2 316 317 // ensureGroup returns the security group with name and rules. 318 // If a group with name does not exist, one will be created. 319 // If it exists, its permissions are set to rules. 320 func (c *neutronFirewaller) ensureGroup(name string, isModelGroup bool) (neutron.SecurityGroupV2, error) { 321 // Due to parallelization of the provisioner, it's possible that we try 322 // to create the model security group a second time before the first time 323 // is complete causing failures. 324 // TODO (stickupkid): This can block forever (API timeouts). We should allow 325 // a mutex to timeout and fail with an error. 326 c.ensureGroupMutex.Lock() 327 defer c.ensureGroupMutex.Unlock() 328 329 neutronClient := c.environ.neutron() 330 var group neutron.SecurityGroupV2 331 332 // First attempt to look up an existing group by name. 333 groupsFound, err := neutronClient.SecurityGroupByNameV2(name) 334 // a list is returned, but there should be only one 335 if err == nil && len(groupsFound) == 1 { 336 group = groupsFound[0] 337 } else if err != nil && strings.Contains(err.Error(), "failed to find security group") { 338 // TODO(hml): We should use a typed error here. SecurityGroupByNameV2 339 // doesn't currently return one for this case. 340 g, err := neutronClient.CreateSecurityGroupV2(name, "juju group") 341 if err != nil { 342 return zeroGroup, err 343 } 344 group = *g 345 } else if err == nil && len(groupsFound) > 1 { 346 // TODO(hml): Add unit test for this case 347 return zeroGroup, errors.New(fmt.Sprintf("More than one security group named %s was found", name)) 348 } else { 349 return zeroGroup, err 350 } 351 352 if !isModelGroup { 353 return group, nil 354 } 355 356 if err := c.ensureInternalRules(neutronClient, group); err != nil { 357 return zeroGroup, errors.Annotate(err, "failed to enable internal model rules") 358 } 359 // Since we may have done a few add or delete rules, get a new 360 // copy of the security group to return containing the end 361 // list of rules. 362 groupsFound, err = neutronClient.SecurityGroupByNameV2(name) 363 if err != nil { 364 return zeroGroup, err 365 } else if len(groupsFound) > 1 { 366 // TODO(hml): Add unit test for this case 367 return zeroGroup, errors.New(fmt.Sprintf("More than one security group named %s was found after group was ensured", name)) 368 } 369 return groupsFound[0], nil 370 } 371 372 func (c *neutronFirewaller) ensureInternalRules(neutronClient *neutron.Client, group neutron.SecurityGroupV2) error { 373 rules := []neutron.RuleInfoV2{ 374 { 375 Direction: "ingress", 376 IPProtocol: "tcp", 377 PortRangeMin: 1, 378 PortRangeMax: 65535, 379 EthernetType: "IPv6", 380 ParentGroupId: group.Id, 381 RemoteGroupId: group.Id, 382 }, 383 { 384 Direction: "ingress", 385 IPProtocol: "tcp", 386 PortRangeMin: 1, 387 PortRangeMax: 65535, 388 ParentGroupId: group.Id, 389 RemoteGroupId: group.Id, 390 }, 391 { 392 Direction: "ingress", 393 IPProtocol: "udp", 394 PortRangeMin: 1, 395 PortRangeMax: 65535, 396 EthernetType: "IPv6", 397 ParentGroupId: group.Id, 398 RemoteGroupId: group.Id, 399 }, 400 { 401 Direction: "ingress", 402 IPProtocol: "udp", 403 PortRangeMin: 1, 404 PortRangeMax: 65535, 405 ParentGroupId: group.Id, 406 RemoteGroupId: group.Id, 407 }, 408 { 409 Direction: "ingress", 410 IPProtocol: "icmp", 411 EthernetType: "IPv6", 412 ParentGroupId: group.Id, 413 RemoteGroupId: group.Id, 414 }, 415 { 416 Direction: "ingress", 417 IPProtocol: "icmp", 418 ParentGroupId: group.Id, 419 RemoteGroupId: group.Id, 420 }, 421 } 422 for _, rule := range rules { 423 if _, err := neutronClient.CreateSecurityGroupRuleV2(rule); err != nil && !gooseerrors.IsDuplicateValue(err) { 424 return err 425 } 426 } 427 return nil 428 } 429 430 func (c *neutronFirewaller) deleteSecurityGroups(ctx context.ProviderCallContext, match func(name string) bool) error { 431 neutronClient := c.environ.neutron() 432 securityGroups, err := neutronClient.ListSecurityGroupsV2() 433 if err != nil { 434 handleCredentialError(err, ctx) 435 return errors.Annotate(err, "cannot list security groups") 436 } 437 for _, group := range securityGroups { 438 if match(group.Name) { 439 deleteSecurityGroup( 440 ctx, 441 neutronClient.DeleteSecurityGroupV2, 442 group.Name, 443 group.Id, 444 clock.WallClock, 445 ) 446 } 447 } 448 return nil 449 } 450 451 // DeleteGroups implements Firewaller interface. 452 func (c *neutronFirewaller) DeleteGroups(ctx context.ProviderCallContext, names ...string) error { 453 return deleteSecurityGroupsOneOfNames(ctx, c.deleteSecurityGroups, names...) 454 } 455 456 // DeleteAllControllerGroups implements Firewaller interface. 457 func (c *neutronFirewaller) DeleteAllControllerGroups(ctx context.ProviderCallContext, controllerUUID string) error { 458 return deleteSecurityGroupsMatchingName(ctx, c.deleteSecurityGroups, c.jujuControllerGroupPrefix(controllerUUID)) 459 } 460 461 // DeleteAllModelGroups implements Firewaller interface. 462 func (c *neutronFirewaller) DeleteAllModelGroups(ctx context.ProviderCallContext) error { 463 return deleteSecurityGroupsMatchingName(ctx, c.deleteSecurityGroups, c.jujuGroupPrefixRegexp()) 464 } 465 466 // UpdateGroupController implements Firewaller interface. 467 func (c *neutronFirewaller) UpdateGroupController(ctx context.ProviderCallContext, controllerUUID string) error { 468 neutronClient := c.environ.neutron() 469 groups, err := neutronClient.ListSecurityGroupsV2() 470 if err != nil { 471 handleCredentialError(err, ctx) 472 return errors.Trace(err) 473 } 474 re, err := regexp.Compile(c.jujuGroupPrefixRegexp()) 475 if err != nil { 476 handleCredentialError(err, ctx) 477 return errors.Trace(err) 478 } 479 480 var failed []string 481 for _, group := range groups { 482 if !re.MatchString(group.Name) { 483 continue 484 } 485 err := c.updateGroupControllerUUID(&group, controllerUUID) 486 if err != nil { 487 logger.Errorf("error updating controller for security group %s: %v", group.Id, err) 488 failed = append(failed, group.Id) 489 if common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx) { 490 // No need to continue here since we will 100% fail with an invalid credential. 491 break 492 } 493 494 } 495 } 496 if len(failed) != 0 { 497 return errors.Errorf("errors updating controller for security groups: %v", failed) 498 } 499 return nil 500 } 501 502 func (c *neutronFirewaller) updateGroupControllerUUID(group *neutron.SecurityGroupV2, controllerUUID string) error { 503 newName, err := replaceControllerUUID(group.Name, controllerUUID) 504 if err != nil { 505 return errors.Trace(err) 506 } 507 client := c.environ.neutron() 508 _, err = client.UpdateSecurityGroupV2(group.Id, newName, group.Description) 509 return errors.Trace(err) 510 } 511 512 // OpenPorts implements Firewaller interface. 513 func (c *neutronFirewaller) OpenPorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error { 514 if c.environ.Config().FirewallMode() != config.FwGlobal { 515 return errors.Errorf("invalid firewall mode %q for opening ports on model", 516 c.environ.Config().FirewallMode()) 517 } 518 if err := c.openPortsInGroup(ctx, c.globalGroupRegexp(), rules); err != nil { 519 handleCredentialError(err, ctx) 520 return errors.Trace(err) 521 } 522 logger.Infof("opened ports in global group: %v", rules) 523 return nil 524 } 525 526 // ClosePorts implements Firewaller interface. 527 func (c *neutronFirewaller) ClosePorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error { 528 if c.environ.Config().FirewallMode() != config.FwGlobal { 529 return errors.Errorf("invalid firewall mode %q for closing ports on model", 530 c.environ.Config().FirewallMode()) 531 } 532 if err := c.closePortsInGroup(ctx, c.globalGroupRegexp(), rules); err != nil { 533 handleCredentialError(err, ctx) 534 return errors.Trace(err) 535 } 536 logger.Infof("closed ports in global group: %v", rules) 537 return nil 538 } 539 540 // IngressRules implements Firewaller interface. 541 func (c *neutronFirewaller) IngressRules(ctx context.ProviderCallContext) (firewall.IngressRules, error) { 542 if c.environ.Config().FirewallMode() != config.FwGlobal { 543 return nil, errors.Errorf("invalid firewall mode %q for retrieving ingress rules from model", 544 c.environ.Config().FirewallMode()) 545 } 546 rules, err := c.ingressRulesInGroup(ctx, c.globalGroupRegexp()) 547 if err != nil { 548 handleCredentialError(err, ctx) 549 return rules, errors.Trace(err) 550 } 551 return rules, nil 552 } 553 554 // OpenModelPorts implements Firewaller interface 555 func (c *neutronFirewaller) OpenModelPorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error { 556 err := c.openPortsInGroup(ctx, c.jujuGroupRegexp(), rules) 557 if errors.IsNotFound(err) && !c.environ.usingSecurityGroups { 558 logger.Warningf("attempted to open %v but network port security is disabled. Already open", rules) 559 return nil 560 } 561 if err != nil { 562 handleCredentialError(err, ctx) 563 return errors.Trace(err) 564 } 565 logger.Infof("opened ports in model group: %v", rules) 566 return nil 567 } 568 569 // CloseModelPorts implements Firewaller interface 570 func (c *neutronFirewaller) CloseModelPorts(ctx context.ProviderCallContext, rules firewall.IngressRules) error { 571 if err := c.closePortsInGroup(ctx, c.jujuGroupRegexp(), rules); err != nil { 572 handleCredentialError(err, ctx) 573 return errors.Trace(err) 574 } 575 logger.Infof("closed ports in global group: %v", rules) 576 return nil 577 } 578 579 // ModelIngressRules implements Firewaller interface 580 func (c *neutronFirewaller) ModelIngressRules(ctx context.ProviderCallContext) (firewall.IngressRules, error) { 581 rules, err := c.ingressRulesInGroup(ctx, c.jujuGroupRegexp()) 582 if err != nil { 583 handleCredentialError(err, ctx) 584 return rules, errors.Trace(err) 585 } 586 return rules, nil 587 } 588 589 // OpenInstancePorts implements Firewaller interface. 590 func (c *neutronFirewaller) OpenInstancePorts(ctx context.ProviderCallContext, inst instances.Instance, machineID string, ports firewall.IngressRules) error { 591 if c.environ.Config().FirewallMode() != config.FwInstance { 592 return errors.Errorf("invalid firewall mode %q for opening ports on instance", 593 c.environ.Config().FirewallMode()) 594 } 595 // For bug 1680787 596 // No security groups exist if the network used to boot the instance has 597 // PortSecurityEnabled set to false. To avoid filling up the log files, 598 // skip trying to open ports in this cases. 599 if securityGroups := inst.(*openstackInstance).getServerDetail().Groups; securityGroups == nil { 600 return nil 601 } 602 nameRegexp := c.machineGroupRegexp(machineID) 603 if err := c.openPortsInGroup(ctx, nameRegexp, ports); err != nil { 604 handleCredentialError(err, ctx) 605 return errors.Trace(err) 606 } 607 logger.Infof("opened ports in security group %s-%s: %v", c.environ.Config().UUID(), machineID, ports) 608 return nil 609 } 610 611 // CloseInstancePorts implements Firewaller interface. 612 func (c *neutronFirewaller) CloseInstancePorts(ctx context.ProviderCallContext, inst instances.Instance, machineID string, ports firewall.IngressRules) error { 613 if c.environ.Config().FirewallMode() != config.FwInstance { 614 return errors.Errorf("invalid firewall mode %q for closing ports on instance", 615 c.environ.Config().FirewallMode()) 616 } 617 // For bug 1680787 618 // No security groups exist if the network used to boot the instance has 619 // PortSecurityEnabled set to false. To avoid filling up the log files, 620 // skip trying to open ports in this cases. 621 if securityGroups := inst.(*openstackInstance).getServerDetail().Groups; securityGroups == nil { 622 return nil 623 } 624 nameRegexp := c.machineGroupRegexp(machineID) 625 if err := c.closePortsInGroup(ctx, nameRegexp, ports); err != nil { 626 handleCredentialError(err, ctx) 627 return errors.Trace(err) 628 } 629 logger.Infof("closed ports in security group %s-%s: %v", c.environ.Config().UUID(), machineID, ports) 630 return nil 631 } 632 633 // InstanceIngressRules implements Firewaller interface. 634 func (c *neutronFirewaller) InstanceIngressRules(ctx context.ProviderCallContext, inst instances.Instance, machineID string) (firewall.IngressRules, error) { 635 if c.environ.Config().FirewallMode() != config.FwInstance { 636 return nil, errors.Errorf("invalid firewall mode %q for retrieving ingress rules from instance", 637 c.environ.Config().FirewallMode()) 638 } 639 // For bug 1680787 640 // No security groups exist if the network used to boot the instance has 641 // PortSecurityEnabled set to false. To avoid filling up the log files, 642 // skip trying to open ports in this cases. 643 if securityGroups := inst.(*openstackInstance).getServerDetail().Groups; securityGroups == nil { 644 return firewall.IngressRules{}, nil 645 } 646 nameRegexp := c.machineGroupRegexp(machineID) 647 rules, err := c.ingressRulesInGroup(ctx, nameRegexp) 648 if err != nil { 649 handleCredentialError(err, ctx) 650 return rules, errors.Trace(err) 651 } 652 return rules, err 653 } 654 655 // Matching a security group by name only works if each name is unqiue. Neutron 656 // security groups are not required to have unique names. Juju constructs unique 657 // names, but there are frequently multiple matches to 'default' 658 func (c *neutronFirewaller) matchingGroup(ctx context.ProviderCallContext, nameRegExp string) (neutron.SecurityGroupV2, error) { 659 re, err := regexp.Compile(nameRegExp) 660 if err != nil { 661 return neutron.SecurityGroupV2{}, err 662 } 663 neutronClient := c.environ.neutron() 664 665 // If the security group has just been created, it might not be available 666 // yet. If we get not matching groups, we will retry the request using 667 // shortRetryStrategy before giving up 668 var matchingGroup neutron.SecurityGroupV2 669 670 retryStrategy := shortRetryStrategy 671 retryStrategy.IsFatalError = func(err error) bool { 672 return !errors.IsNotFound(err) 673 } 674 retryStrategy.Func = func() error { 675 allGroups, err := neutronClient.ListSecurityGroupsV2() 676 if err != nil { 677 handleCredentialError(err, ctx) 678 return err 679 } 680 var matchingGroups []neutron.SecurityGroupV2 681 for _, group := range allGroups { 682 if re.MatchString(group.Name) { 683 matchingGroups = append(matchingGroups, group) 684 } 685 } 686 numMatching := len(matchingGroups) 687 if numMatching == 0 { 688 return errors.NotFoundf("security groups matching %q", nameRegExp) 689 } else if numMatching > 1 { 690 return errors.New(fmt.Sprintf("%d security groups found matching %q, expected 1", numMatching, nameRegExp)) 691 } 692 matchingGroup = matchingGroups[0] 693 return nil 694 } 695 err = retry.Call(retryStrategy) 696 if retry.IsAttemptsExceeded(err) || retry.IsDurationExceeded(err) { 697 err = retry.LastError(err) 698 } 699 return matchingGroup, err 700 } 701 702 func (c *neutronFirewaller) openPortsInGroup(ctx context.ProviderCallContext, nameRegExp string, rules firewall.IngressRules) error { 703 group, err := c.matchingGroup(ctx, nameRegExp) 704 if err != nil { 705 return errors.Trace(err) 706 } 707 neutronClient := c.environ.neutron() 708 ruleInfo := rulesToRuleInfo(group.Id, rules) 709 for _, rule := range ruleInfo { 710 _, err := neutronClient.CreateSecurityGroupRuleV2(rule) 711 if err != nil && !gooseerrors.IsDuplicateValue(err) { 712 handleCredentialError(err, ctx) 713 // TODO: if err is not rule already exists, raise? 714 return fmt.Errorf("creating security group rule %q for parent group id %q using proto %q: %w", rule.Direction, rule.ParentGroupId, rule.IPProtocol, err) 715 } 716 } 717 return nil 718 } 719 720 // secGroupMatchesIngressRule checks if supplied nova security group rule matches the ingress rule 721 func secGroupMatchesIngressRule(secGroupRule neutron.SecurityGroupRuleV2, rule firewall.IngressRule) bool { 722 if secGroupRule.IPProtocol == nil || 723 secGroupRule.PortRangeMax == nil || *secGroupRule.PortRangeMax == 0 || 724 secGroupRule.PortRangeMin == nil || *secGroupRule.PortRangeMin == 0 { 725 return false 726 } 727 portsMatch := *secGroupRule.IPProtocol == rule.PortRange.Protocol && 728 *secGroupRule.PortRangeMin == rule.PortRange.FromPort && 729 *secGroupRule.PortRangeMax == rule.PortRange.ToPort 730 if !portsMatch { 731 return false 732 } 733 // The ports match, so if the security group RemoteIPPrefix matches *any* of the 734 // rule's source ranges, then that's a match. 735 if len(rule.SourceCIDRs) == 0 { 736 return secGroupRule.RemoteIPPrefix == "" || 737 secGroupRule.RemoteIPPrefix == "0.0.0.0/0" || 738 secGroupRule.RemoteIPPrefix == "::/0" 739 } 740 return rule.SourceCIDRs.Contains(secGroupRule.RemoteIPPrefix) 741 } 742 743 func (c *neutronFirewaller) closePortsInGroup(ctx context.ProviderCallContext, nameRegExp string, rules firewall.IngressRules) error { 744 if len(rules) == 0 { 745 return nil 746 } 747 group, err := c.matchingGroup(ctx, nameRegExp) 748 if err != nil { 749 return errors.Trace(err) 750 } 751 752 neutronClient := c.environ.neutron() 753 // TODO: Hey look ma, it's quadratic 754 for _, rule := range rules { 755 for _, p := range group.Rules { 756 if !secGroupMatchesIngressRule(p, rule) { 757 continue 758 } 759 if err := neutronClient.DeleteSecurityGroupRuleV2(p.Id); err != nil { 760 if gooseerrors.IsNotFound(err) { 761 break 762 } 763 handleCredentialError(err, ctx) 764 return errors.Trace(err) 765 } 766 767 // The rule to be removed may contain multiple CIDRs; 768 // even though we matched it to one of the group rules 769 // we should keep searching other rules whose IPPrefix 770 // may match one of the other CIDRs. 771 } 772 } 773 return nil 774 } 775 776 func (c *neutronFirewaller) ingressRulesInGroup(ctx context.ProviderCallContext, nameRegexp string) (rules firewall.IngressRules, err error) { 777 group, err := c.matchingGroup(ctx, nameRegexp) 778 if err != nil { 779 return nil, errors.Trace(err) 780 } 781 // Keep track of all the RemoteIPPrefixes for each port range. 782 portSourceCIDRs := make(map[corenetwork.PortRange]*[]string) 783 for _, p := range group.Rules { 784 // Skip the default Security Group Rules created by Neutron 785 if p.Direction == "egress" { 786 continue 787 } 788 // Skip internal security group rules 789 if p.RemoteGroupID != "" { 790 continue 791 } 792 793 portRange := corenetwork.PortRange{ 794 Protocol: *p.IPProtocol, 795 } 796 if portRange.Protocol == "ipv6-icmp" { 797 portRange.Protocol = "icmp" 798 } 799 // NOTE: Juju firewall rule validation expects that icmp rules have port 800 // values set to -1 801 if p.PortRangeMin != nil { 802 portRange.FromPort = *p.PortRangeMin 803 } else if portRange.Protocol == "icmp" { 804 portRange.FromPort = -1 805 } 806 if p.PortRangeMax != nil { 807 portRange.ToPort = *p.PortRangeMax 808 } else if portRange.Protocol == "icmp" { 809 portRange.ToPort = -1 810 } 811 // Record the RemoteIPPrefix for the port range. 812 remotePrefix := p.RemoteIPPrefix 813 if remotePrefix == "" { 814 remotePrefix = "0.0.0.0/0" 815 } 816 sourceCIDRs, ok := portSourceCIDRs[portRange] 817 if !ok { 818 sourceCIDRs = &[]string{} 819 portSourceCIDRs[portRange] = sourceCIDRs 820 } 821 *sourceCIDRs = append(*sourceCIDRs, remotePrefix) 822 } 823 // Combine all the port ranges and remote prefixes. 824 for portRange, sourceCIDRs := range portSourceCIDRs { 825 rules = append(rules, firewall.NewIngressRule(portRange, *sourceCIDRs...)) 826 } 827 if err := rules.Validate(); err != nil { 828 return nil, errors.Trace(err) 829 } 830 rules.Sort() 831 return rules, nil 832 } 833 834 func replaceControllerUUID(oldName, controllerUUID string) (string, error) { 835 if !extractControllerRe.MatchString(oldName) { 836 return "", errors.Errorf("unexpected security group name format for %q", oldName) 837 } 838 newName := extractControllerRe.ReplaceAllString( 839 oldName, 840 "${prefix}"+controllerUUID+"${suffix}", 841 ) 842 return newName, nil 843 }