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