github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"time"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/retry"
    14  	"github.com/juju/utils/clock"
    15  	gooseerrors "gopkg.in/goose.v1/errors"
    16  	"gopkg.in/goose.v1/nova"
    17  
    18  	"github.com/juju/juju/environs"
    19  	"github.com/juju/juju/environs/config"
    20  	"github.com/juju/juju/instance"
    21  	"github.com/juju/juju/network"
    22  )
    23  
    24  //factory for obtaining firawaller object.
    25  type FirewallerFactory interface {
    26  	GetFirewaller(env environs.Environ) Firewaller
    27  }
    28  
    29  // Firewaller allows custom openstack provider behaviour.
    30  // This is used in other providers that embed the openstack provider.
    31  type Firewaller interface {
    32  	// OpenPorts opens the given port ranges for the whole environment.
    33  	OpenPorts(ports []network.PortRange) error
    34  
    35  	// ClosePorts closes the given port ranges for the whole environment.
    36  	ClosePorts(ports []network.PortRange) error
    37  
    38  	// Ports returns the port ranges opened for the whole environment.
    39  	Ports() ([]network.PortRange, error)
    40  
    41  	// Implementations are expected to delete all security groups for the
    42  	// environment.
    43  	DeleteAllModelGroups() error
    44  
    45  	// Implementations are expected to delete all security groups for the
    46  	// controller, ie those for all hosted models.
    47  	DeleteAllControllerGroups(controllerUUID string) error
    48  
    49  	// Implementations should return list of security groups, that belong to given instances.
    50  	GetSecurityGroups(ids ...instance.Id) ([]string, error)
    51  
    52  	// Implementations should set up initial security groups, if any.
    53  	SetUpGroups(controllerUUID, machineId string, apiPort int) ([]nova.SecurityGroup, error)
    54  
    55  	// Set of initial networks, that should be added by default to all new instances.
    56  	InitialNetworks() []nova.ServerNetworks
    57  
    58  	// OpenInstancePorts opens the given port ranges for the specified  instance.
    59  	OpenInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error
    60  
    61  	// CloseInstancePorts closes the given port ranges for the specified  instance.
    62  	CloseInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error
    63  
    64  	// InstancePorts returns the port ranges opened for the specified  instance.
    65  	InstancePorts(inst instance.Instance, machineId string) ([]network.PortRange, error)
    66  }
    67  
    68  type firewallerFactory struct {
    69  }
    70  
    71  // GetFirewaller implements FirewallerFactory
    72  func (f *firewallerFactory) GetFirewaller(env environs.Environ) Firewaller {
    73  	return &defaultFirewaller{environ: env.(*Environ)}
    74  }
    75  
    76  type defaultFirewaller struct {
    77  	environ *Environ
    78  }
    79  
    80  // InitialNetworks implements Firewaller interface.
    81  func (c *defaultFirewaller) InitialNetworks() []nova.ServerNetworks {
    82  	return []nova.ServerNetworks{}
    83  }
    84  
    85  // SetUpGroups creates the security groups for the new machine, and
    86  // returns them.
    87  //
    88  // Instances are tagged with a group so they can be distinguished from
    89  // other instances that might be running on the same OpenStack account.
    90  // In addition, a specific machine security group is created for each
    91  // machine, so that its firewall rules can be configured per machine.
    92  //
    93  // Note: ideally we'd have a better way to determine group membership so that 2
    94  // people that happen to share an openstack account and name their environment
    95  // "openstack" don't end up destroying each other's machines.
    96  func (c *defaultFirewaller) SetUpGroups(controllerUUID, machineId string, apiPort int) ([]nova.SecurityGroup, error) {
    97  	jujuGroup, err := c.setUpGlobalGroup(c.jujuGroupName(controllerUUID), apiPort)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	var machineGroup nova.SecurityGroup
   102  	switch c.environ.Config().FirewallMode() {
   103  	case config.FwInstance:
   104  		machineGroup, err = c.ensureGroup(c.machineGroupName(controllerUUID, machineId), nil)
   105  	case config.FwGlobal:
   106  		machineGroup, err = c.ensureGroup(c.globalGroupName(controllerUUID), nil)
   107  	}
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	groups := []nova.SecurityGroup{jujuGroup, machineGroup}
   112  	if c.environ.ecfg().useDefaultSecurityGroup() {
   113  		defaultGroup, err := c.environ.nova().SecurityGroupByName("default")
   114  		if err != nil {
   115  			return nil, fmt.Errorf("loading default security group: %v", err)
   116  		}
   117  		groups = append(groups, *defaultGroup)
   118  	}
   119  	return groups, nil
   120  }
   121  
   122  func (c *defaultFirewaller) setUpGlobalGroup(groupName string, apiPort int) (nova.SecurityGroup, error) {
   123  	return c.ensureGroup(groupName,
   124  		[]nova.RuleInfo{
   125  			{
   126  				IPProtocol: "tcp",
   127  				FromPort:   22,
   128  				ToPort:     22,
   129  				Cidr:       "0.0.0.0/0",
   130  			},
   131  			{
   132  				IPProtocol: "tcp",
   133  				FromPort:   apiPort,
   134  				ToPort:     apiPort,
   135  				Cidr:       "0.0.0.0/0",
   136  			},
   137  			{
   138  				IPProtocol: "tcp",
   139  				FromPort:   1,
   140  				ToPort:     65535,
   141  			},
   142  			{
   143  				IPProtocol: "udp",
   144  				FromPort:   1,
   145  				ToPort:     65535,
   146  			},
   147  			{
   148  				IPProtocol: "icmp",
   149  				FromPort:   -1,
   150  				ToPort:     -1,
   151  			},
   152  		})
   153  }
   154  
   155  // zeroGroup holds the zero security group.
   156  var zeroGroup nova.SecurityGroup
   157  
   158  // ensureGroup returns the security group with name and perms.
   159  // If a group with name does not exist, one will be created.
   160  // If it exists, its permissions are set to perms.
   161  func (c *defaultFirewaller) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) {
   162  	novaClient := c.environ.nova()
   163  	// First attempt to look up an existing group by name.
   164  	group, err := novaClient.SecurityGroupByName(name)
   165  	if err == nil {
   166  		// Group exists, so assume it is correctly set up and return it.
   167  		// TODO(jam): 2013-09-18 http://pad.lv/121795
   168  		// We really should verify the group is set up correctly,
   169  		// because deleting and re-creating environments can get us bad
   170  		// groups (especially if they were set up under Python)
   171  		return *group, nil
   172  	}
   173  	// Doesn't exist, so try and create it.
   174  	group, err = novaClient.CreateSecurityGroup(name, "juju group")
   175  	if err != nil {
   176  		if !gooseerrors.IsDuplicateValue(err) {
   177  			return zeroGroup, err
   178  		} else {
   179  			// We just tried to create a duplicate group, so load the existing group.
   180  			group, err = novaClient.SecurityGroupByName(name)
   181  			if err != nil {
   182  				return zeroGroup, err
   183  			}
   184  			return *group, nil
   185  		}
   186  	}
   187  	// The new group is created so now add the rules.
   188  	group.Rules = make([]nova.SecurityGroupRule, len(rules))
   189  	for i, rule := range rules {
   190  		rule.ParentGroupId = group.Id
   191  		if rule.Cidr == "" {
   192  			// http://pad.lv/1226996 Rules that don't have a CIDR
   193  			// are meant to apply only to this group. If you don't
   194  			// supply CIDR or GroupId then openstack assumes you
   195  			// mean CIDR=0.0.0.0/0
   196  			rule.GroupId = &group.Id
   197  		}
   198  		groupRule, err := novaClient.CreateSecurityGroupRule(rule)
   199  		if err != nil && !gooseerrors.IsDuplicateValue(err) {
   200  			return zeroGroup, err
   201  		}
   202  		group.Rules[i] = *groupRule
   203  	}
   204  	return *group, nil
   205  }
   206  
   207  // GetSecurityGroups implements Firewaller interface.
   208  func (c *defaultFirewaller) GetSecurityGroups(ids ...instance.Id) ([]string, error) {
   209  	var securityGroupNames []string
   210  	if c.environ.Config().FirewallMode() == config.FwInstance {
   211  		instances, err := c.environ.Instances(ids)
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  		novaClient := c.environ.nova()
   216  		securityGroupNames = make([]string, 0, len(ids))
   217  		for _, inst := range instances {
   218  			if inst == nil {
   219  				continue
   220  			}
   221  			openstackName := inst.(*openstackInstance).getServerDetail().Name
   222  			lastDashPos := strings.LastIndex(openstackName, "-")
   223  			if lastDashPos == -1 {
   224  				return nil, fmt.Errorf("cannot identify machine ID in openstack server name %q", openstackName)
   225  			}
   226  			serverId := openstackName[lastDashPos+1:]
   227  			groups, err := novaClient.GetServerSecurityGroups(string(inst.Id()))
   228  			if err != nil {
   229  				return nil, err
   230  			}
   231  			for _, group := range groups {
   232  				// We only include the group specifically tied to the instance, not
   233  				// any group global to the model itself.
   234  				if strings.HasSuffix(group.Name, fmt.Sprintf("%s-%s", c.environ.Config().UUID(), serverId)) {
   235  					securityGroupNames = append(securityGroupNames, group.Name)
   236  				}
   237  			}
   238  		}
   239  	}
   240  	return securityGroupNames, nil
   241  }
   242  
   243  func (c *defaultFirewaller) deleteSecurityGroups(prefix string) error {
   244  	novaClient := c.environ.nova()
   245  	securityGroups, err := novaClient.ListSecurityGroups()
   246  	if err != nil {
   247  		return errors.Annotate(err, "cannot list security groups")
   248  	}
   249  
   250  	re, err := regexp.Compile("^" + prefix)
   251  	if err != nil {
   252  		return errors.Trace(err)
   253  	}
   254  	for _, group := range securityGroups {
   255  		if re.MatchString(group.Name) {
   256  			deleteSecurityGroup(novaClient, group.Name, group.Id)
   257  		}
   258  	}
   259  	return nil
   260  }
   261  
   262  // DeleteAllControllerGroups implements Firewaller interface.
   263  func (c *defaultFirewaller) DeleteAllControllerGroups(controllerUUID string) error {
   264  	return c.deleteSecurityGroups(c.jujuControllerGroupPrefix(controllerUUID))
   265  }
   266  
   267  // DeleteAllModelGroups implements Firewaller interface.
   268  func (c *defaultFirewaller) DeleteAllModelGroups() error {
   269  	return c.deleteSecurityGroups(c.jujuGroupRegexp())
   270  }
   271  
   272  // deleteSecurityGroup attempts to delete the security group. Should it fail,
   273  // the deletion is retried due to timing issues in openstack. A security group
   274  // cannot be deleted while it is in use. Theoretically we terminate all the
   275  // instances before we attempt to delete the associated security groups, but
   276  // in practice nova hasn't always finished with the instance before it
   277  // returns, so there is a race condition where we think the instance is
   278  // terminated and hence attempt to delete the security groups but nova still
   279  // has it around internally. To attempt to catch this timing issue, deletion
   280  // of the groups is tried multiple times.
   281  func deleteSecurityGroup(novaclient *nova.Client, name, id string) {
   282  	logger.Debugf("deleting security group %q", name)
   283  	err := retry.Call(retry.CallArgs{
   284  		Func: func() error {
   285  			return novaclient.DeleteSecurityGroup(id)
   286  		},
   287  		NotifyFunc: func(err error, attempt int) {
   288  			if attempt%4 == 0 {
   289  				message := fmt.Sprintf("waiting to delete security group %q", name)
   290  				if attempt != 4 {
   291  					message = "still " + message
   292  				}
   293  				logger.Debugf(message)
   294  			}
   295  		},
   296  		Attempts: 30,
   297  		Delay:    time.Second,
   298  		// TODO(dimitern): This should be fixed to take a clock.Clock arg, not
   299  		// hard-coded WallClock, like in provider/ec2/securitygroups_test.go!
   300  		// See PR juju:#5197, especially the code around autoAdvancingClock.
   301  		// LP Bug: http://pad.lv/1580626.
   302  		Clock: clock.WallClock,
   303  	})
   304  	if err != nil {
   305  		logger.Warningf("cannot delete security group %q. Used by another model?", name)
   306  	}
   307  }
   308  
   309  // OpenPorts implements Firewaller interface.
   310  func (c *defaultFirewaller) OpenPorts(ports []network.PortRange) error {
   311  	if c.environ.Config().FirewallMode() != config.FwGlobal {
   312  		return fmt.Errorf("invalid firewall mode %q for opening ports on model",
   313  			c.environ.Config().FirewallMode())
   314  	}
   315  	if err := c.openPortsInGroup(c.globalGroupRegexp(), ports); err != nil {
   316  		return err
   317  	}
   318  	logger.Infof("opened ports in global group: %v", ports)
   319  	return nil
   320  }
   321  
   322  // ClosePorts implements Firewaller interface.
   323  func (c *defaultFirewaller) ClosePorts(ports []network.PortRange) error {
   324  	if c.environ.Config().FirewallMode() != config.FwGlobal {
   325  		return fmt.Errorf("invalid firewall mode %q for closing ports on model",
   326  			c.environ.Config().FirewallMode())
   327  	}
   328  	if err := c.closePortsInGroup(c.globalGroupRegexp(), ports); err != nil {
   329  		return err
   330  	}
   331  	logger.Infof("closed ports in global group: %v", ports)
   332  	return nil
   333  }
   334  
   335  // Ports implements Firewaller interface.
   336  func (c *defaultFirewaller) Ports() ([]network.PortRange, error) {
   337  	if c.environ.Config().FirewallMode() != config.FwGlobal {
   338  		return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from model",
   339  			c.environ.Config().FirewallMode())
   340  	}
   341  	return c.portsInGroup(c.globalGroupRegexp())
   342  }
   343  
   344  // OpenInstancePorts implements Firewaller interface.
   345  func (c *defaultFirewaller) OpenInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error {
   346  	if c.environ.Config().FirewallMode() != config.FwInstance {
   347  		return fmt.Errorf("invalid firewall mode %q for opening ports on instance",
   348  			c.environ.Config().FirewallMode())
   349  	}
   350  	nameRegexp := c.machineGroupRegexp(machineId)
   351  	if err := c.openPortsInGroup(nameRegexp, ports); err != nil {
   352  		return err
   353  	}
   354  	logger.Infof("opened ports in security group %s-%s: %v", c.environ.Config().UUID(), machineId, ports)
   355  	return nil
   356  }
   357  
   358  // CloseInstancePorts implements Firewaller interface.
   359  func (c *defaultFirewaller) CloseInstancePorts(inst instance.Instance, machineId string, ports []network.PortRange) error {
   360  	if c.environ.Config().FirewallMode() != config.FwInstance {
   361  		return fmt.Errorf("invalid firewall mode %q for closing ports on instance",
   362  			c.environ.Config().FirewallMode())
   363  	}
   364  	nameRegexp := c.machineGroupRegexp(machineId)
   365  	if err := c.closePortsInGroup(nameRegexp, ports); err != nil {
   366  		return err
   367  	}
   368  	logger.Infof("closed ports in security group %s-%s: %v", c.environ.Config().UUID(), machineId, ports)
   369  	return nil
   370  }
   371  
   372  // InstancePorts implements Firewaller interface.
   373  func (c *defaultFirewaller) InstancePorts(inst instance.Instance, machineId string) ([]network.PortRange, error) {
   374  	if c.environ.Config().FirewallMode() != config.FwInstance {
   375  		return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance",
   376  			c.environ.Config().FirewallMode())
   377  	}
   378  	nameRegexp := c.machineGroupRegexp(machineId)
   379  	portRanges, err := c.portsInGroup(nameRegexp)
   380  	if err != nil {
   381  		return nil, err
   382  	}
   383  	return portRanges, nil
   384  }
   385  
   386  func (c *defaultFirewaller) matchingGroup(nameRegExp string) (nova.SecurityGroup, error) {
   387  	re, err := regexp.Compile(nameRegExp)
   388  	if err != nil {
   389  		return nova.SecurityGroup{}, err
   390  	}
   391  	novaclient := c.environ.nova()
   392  	allGroups, err := novaclient.ListSecurityGroups()
   393  	if err != nil {
   394  		return nova.SecurityGroup{}, err
   395  	}
   396  	var matchingGroups []nova.SecurityGroup
   397  	for _, group := range allGroups {
   398  		if re.MatchString(group.Name) {
   399  			matchingGroups = append(matchingGroups, group)
   400  		}
   401  	}
   402  	if len(matchingGroups) != 1 {
   403  		return nova.SecurityGroup{}, errors.NotFoundf("security groups matching %q", nameRegExp)
   404  	}
   405  	return matchingGroups[0], nil
   406  }
   407  
   408  func (c *defaultFirewaller) openPortsInGroup(nameRegExp string, portRanges []network.PortRange) error {
   409  	group, err := c.matchingGroup(nameRegExp)
   410  	if err != nil {
   411  		return err
   412  	}
   413  	novaclient := c.environ.nova()
   414  	rules := portsToRuleInfo(group.Id, portRanges)
   415  	for _, rule := range rules {
   416  		_, err := novaclient.CreateSecurityGroupRule(rule)
   417  		if err != nil {
   418  			// TODO: if err is not rule already exists, raise?
   419  			logger.Debugf("error creating security group rule: %v", err.Error())
   420  		}
   421  	}
   422  	return nil
   423  }
   424  
   425  // ruleMatchesPortRange checks if supplied nova security group rule matches the port range
   426  func ruleMatchesPortRange(rule nova.SecurityGroupRule, portRange network.PortRange) bool {
   427  	if rule.IPProtocol == nil || rule.FromPort == nil || rule.ToPort == nil {
   428  		return false
   429  	}
   430  	return *rule.IPProtocol == portRange.Protocol &&
   431  		*rule.FromPort == portRange.FromPort &&
   432  		*rule.ToPort == portRange.ToPort
   433  }
   434  
   435  func (c *defaultFirewaller) closePortsInGroup(nameRegExp string, portRanges []network.PortRange) error {
   436  	if len(portRanges) == 0 {
   437  		return nil
   438  	}
   439  	group, err := c.matchingGroup(nameRegExp)
   440  	if err != nil {
   441  		return err
   442  	}
   443  	novaclient := c.environ.nova()
   444  	// TODO: Hey look ma, it's quadratic
   445  	for _, portRange := range portRanges {
   446  		for _, p := range group.Rules {
   447  			if !ruleMatchesPortRange(p, portRange) {
   448  				continue
   449  			}
   450  			err := novaclient.DeleteSecurityGroupRule(p.Id)
   451  			if err != nil {
   452  				return err
   453  			}
   454  			break
   455  		}
   456  	}
   457  	return nil
   458  }
   459  
   460  func (c *defaultFirewaller) portsInGroup(nameRegexp string) (portRanges []network.PortRange, err error) {
   461  	group, err := c.matchingGroup(nameRegexp)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  	for _, p := range group.Rules {
   466  		portRanges = append(portRanges, network.PortRange{
   467  			Protocol: *p.IPProtocol,
   468  			FromPort: *p.FromPort,
   469  			ToPort:   *p.ToPort,
   470  		})
   471  	}
   472  	network.SortPortRanges(portRanges)
   473  	return portRanges, nil
   474  }
   475  
   476  func (c *defaultFirewaller) globalGroupName(controllerUUID string) string {
   477  	return fmt.Sprintf("%s-global", c.jujuGroupName(controllerUUID))
   478  }
   479  
   480  func (c *defaultFirewaller) machineGroupName(controllerUUID, machineId string) string {
   481  	return fmt.Sprintf("%s-%s", c.jujuGroupName(controllerUUID), machineId)
   482  }
   483  
   484  func (c *defaultFirewaller) jujuGroupName(controllerUUID string) string {
   485  	cfg := c.environ.Config()
   486  	return fmt.Sprintf("juju-%v-%v", controllerUUID, cfg.UUID())
   487  }
   488  
   489  func (c *defaultFirewaller) jujuControllerGroupPrefix(controllerUUID string) string {
   490  	return fmt.Sprintf("juju-%v-", controllerUUID)
   491  }
   492  
   493  func (c *defaultFirewaller) jujuGroupRegexp() string {
   494  	cfg := c.environ.Config()
   495  	return fmt.Sprintf("juju-.*-%v", cfg.UUID())
   496  }
   497  
   498  func (c *defaultFirewaller) globalGroupRegexp() string {
   499  	return fmt.Sprintf("%s-global", c.jujuGroupRegexp())
   500  }
   501  
   502  func (c *defaultFirewaller) machineGroupRegexp(machineId string) string {
   503  	return fmt.Sprintf("%s-%s", c.jujuGroupRegexp(), machineId)
   504  }