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  }