Skip to content

Commit

Permalink
Merge pull request #6 from bpineau/multi_interfaces
Browse files Browse the repository at this point in the history
Suport multi-homed/multi-network/multi-interfaces instances
  • Loading branch information
bpineau committed Mar 18, 2018
2 parents 88ff179 + 93ee46f commit f92ceaa
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 54 deletions.
42 changes: 33 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ Implement a floating/virtual IP by configuring cloud provider's routes.
Choose an arbitrary private IP address, and `cloud-floating-ip` will
route traffic for that IP to the AWS or GCP instance of your choice.

## Usage
## Instances preparation

Choosing the virtual IP: this address must not be already used in the
VPC; it doesn't have to be part of an existing subnet range.
To choose a virtual IP: this address must be available, and not used
elsewhere in the VPC; it doesn't have to be part of an existing subnet range.

All EC2/GCE instances that may become "primary" (carry the floating IP)
at some point should be allowed by the cloud provider to route traffic
(`SourceDestCheck` (EC2) or `canIpForward` (GCE) must be enabled).

Those instances should be able to accept traffic to the floating IP.
To that effect, we can assign the address to a loopback or a dummy
interface on all instances:
To that effect, we can assign the virtual IP address to a loopback or
a dummy interface on all instances:

```bash
# we can do that on all instances
Expand All @@ -25,6 +25,11 @@ ip address add 10.200.0.50/32 dev dummy0
ip link set dev dummy0 up
```

This can be persisted in network configurations (eg. in /etc/network/interfaces
or /etc/sysconfig/network-scripts/).

## Usage

To route the floating IP through the current instance:
```bash
# see what would change
Expand All @@ -44,21 +49,38 @@ cloud-floating-ip -i 10.200.0.50 status

When `cloud-floating-ip` runs on the target instance, most settings (region,
instance id, cloud provider, ...) can be guessed from the instance metadata.
To act on a remote instance, we must be more explicit :
To act on a remote instance, we must be more explicit (or use a configuration file). Eg:

```bash
cloud-floating-ip -o aws -i 10.200.0.50 -t i-0e3f4ac17545ce580 -r eu-west-1 status
cloud-floating-ip -o aws -i 10.200.0.50 -t i-0e3f4ac17545ce580 -r eu-west-1 preempt

cloud-floating-ip -o gce -i 10.200.0.50 -p my-gcp-project \
-t my-gce-instance -z europe-west1-b status

````

To store the configuration (and get rid of repetitive `-i ...` arguments):
To store the configuration (and save repetitive `-i ...` arguments):
```bash
cat<<EOF > /etc/cloud-floating-ip.yaml
ip: 10.200.0.50
quiet: true
EOF
```

## Multihomed instances

When the instance has only one interface attached to the VPC, `cloud-floating-ip`
will find and use this interface automatically.

If the instance has more than one external interfaces (and/or networks), we need
one of the following options to choose the target interface we'll route traffic to:
Provide either:
* --interface : the target interface name (ie. eni-xxxx on AWS, nicX on GCE)
* --subnet : the target network interface's subnet name
* --target-ip : the target network interface's private IP
## Options
The `ip` argument is mandatory. Other settings can be collected from
Expand Down Expand Up @@ -89,6 +111,9 @@ Flags:
-h, --help help for cloud-floating-ip
-o, --hoster string hosting provider (aws or gce)
-t, --instance string instance name
-f, --interface string Network interface ID
-s, --subnet string Subnet ID
-g, --target-ip string Target private IP
-m, --ignore-main-table (AWS) ignore routes in main table
-a, --aws-access-key-id string (AWS) access key Id
-k, --aws-secret-key string (AWS) secret key
Expand Down Expand Up @@ -120,7 +145,6 @@ container.operations.list
## Limitations
* `cloud-floating-ip` does not support instances with multiple interfaces in the VPC yet.
* On GCE, `cloud-floating-ip` won't remove already created, pre-existing routes with a custom name
* On GCE, `cloud-floating-ip` won't delete already created, pre-existing routes with a distinct custom name
* IPv4 only, for now
15 changes: 15 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ var (
nomain bool
accessk string
secretk string
iface string
subnet string
targetip string
)

func newCfiConfig() *config.CfiConfig {
Expand All @@ -40,6 +43,9 @@ func newCfiConfig() *config.CfiConfig {
Region: viper.GetString("region"),
Zone: viper.GetString("zone"),
NoMain: viper.GetBool("ignore-main-table"),
Iface: viper.GetString("interface"),
Subnet: viper.GetString("subnet"),
TargetIP: viper.GetString("target-ip"),
AwsAccesKeyID: viper.GetString("aws-access-key-id"),
AwsSecretKey: viper.GetString("aws-secret-key"),
}
Expand Down Expand Up @@ -116,6 +122,15 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&nomain, "ignore-main-table", "m", false, "(AWS) ignore routes in main table")
bindPFlag("ignore-main-table", "ignore-main-table")

rootCmd.PersistentFlags().StringVarP(&iface, "interface", "f", "", "Network interface ID")
bindPFlag("interface", "interface")

rootCmd.PersistentFlags().StringVarP(&subnet, "subnet", "s", "", "Subnet ID")
bindPFlag("subnet", "subnet")

rootCmd.PersistentFlags().StringVarP(&targetip, "target-ip", "g", "", "Target private IP")
bindPFlag("target-ip", "target-ip")

rootCmd.PersistentFlags().StringVarP(&accessk, "aws-access-key-id", "a", "", "(AWS) access key Id")
bindPFlag("aws-access-key-id", "aws-access-key-id")

Expand Down
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ type CfiConfig struct {
// Ignore tables associated with the main route table
NoMain bool

// Interface ID
Iface string

// Subnet ID
Subnet string

// Target private IP
TargetIP string

// AwsAccesKeyID (AWS only) is the acccess key to use (if we don't use an instance profile's role)
AwsAccesKeyID string

Expand Down
118 changes: 101 additions & 17 deletions pkg/hoster/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const (
rsAbsent routeStatus = iota
rsWrongTarget
rsCorrectTarget

inuse = "in-use"
)

// Init prepare an aws hoster for usage
Expand All @@ -39,20 +41,20 @@ func (h *Hoster) Init(conf *config.CfiConfig, logger log.Logger) {
h.log = logger
err := h.checkMissingParam()
if err != nil {
h.log.Fatalf("Missing param: %v", err)
h.log.Fatalf("Missing param: %v\n", err)
}

h.sess, err = session.NewSession(aws.NewConfig().WithMaxRetries(3))
if err != nil {
h.log.Fatalf("Failed to initialize an AWS session: %v", err)
h.log.Fatalf("Failed to initialize an AWS session: %v\n", err)
}

metadata := ec2metadata.New(h.sess)

if h.conf.Region == "" {
h.conf.Region, err = metadata.Region()
if err != nil {
h.log.Fatalf("Failed to collect region from instance metadata: %v", err)
h.log.Fatalf("Failed to collect region from instance metadata: %v\n", err)
}
}

Expand All @@ -61,32 +63,25 @@ func (h *Hoster) Init(conf *config.CfiConfig, logger log.Logger) {
if h.conf.Instance == "" {
h.conf.Instance, err = metadata.GetMetadata("instance-id")
if err != nil {
h.log.Fatalf("Failed to collect instanceid from instance metadata: %v", err)
h.log.Fatalf("Failed to collect instanceid from instance metadata: %v\n", err)
}
}

h.ec2s = ec2.New(h.sess)

err = h.getNetworkInfo()
if err != nil {
h.log.Fatalf("Failed to collect network infos: %v", err)
h.log.Fatalf("Failed to collect network infos: %v\n", err)
}
}

func (h *Hoster) getNetworkInfo() error {
instance, err := h.ec2s.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIds: []*string{aws.String(h.conf.Instance)}},
)
if err != nil {
return fmt.Errorf("Failed to DescribeInstances: %v", err)
}

// TODO: support instances with several interfaces
if len(instance.Reservations[0].Instances[0].NetworkInterfaces) != 1 {
return fmt.Errorf("For now, we don't support more than one interface")
eni, err := h.getNetworkInterface()
if err != nil {
return fmt.Errorf("failed to find the target interface: %v", err)
}

eni := instance.Reservations[0].Instances[0].NetworkInterfaces[0]
h.enid = eni.NetworkInterfaceId
h.vpc = *eni.VpcId
h.myip = *eni.PrivateIpAddress
Expand All @@ -113,6 +108,96 @@ func (h *Hoster) getNetworkInfo() error {
return nil
}

// find the target ENI/interface ; if we're multihomed (have several external
// interfaces), we'll filter using the user-provided interface or subnet name.
func (h *Hoster) getNetworkInterface() (*ec2.InstanceNetworkInterface, error) {
instance, err := h.ec2s.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIds: []*string{aws.String(h.conf.Instance)}},
)
if err != nil {
return nil, fmt.Errorf("failed to DescribeInstances: %v", err)
}

ifaces := instance.Reservations[0].Instances[0].NetworkInterfaces
if len(ifaces) < 1 {
return nil, fmt.Errorf("instance %s doesn't have a network interface",
h.conf.Instance)
}

if len(ifaces) != 1 && h.conf.Iface == "" && h.conf.Subnet == "" && h.conf.TargetIP == "" {
return nil, fmt.Errorf("the instance %s has more than one interface, %s",
h.conf.Instance, "please specify an interface, target IP, or subnet ID.")
}

if h.conf.Iface != "" {
return h.getNetworkInterfaceByName(h.conf.Iface, ifaces)
}

if h.conf.Subnet != "" {
return h.getNetworkInterfaceBySubnet(h.conf.Subnet, ifaces)
}

if h.conf.TargetIP != "" {
return h.getNetworkInterfaceByTargetIP(h.conf.TargetIP, ifaces)
}

return ifaces[0], nil
}

func (h *Hoster) getNetworkInterfaceByName(name string, ifaces []*ec2.InstanceNetworkInterface) (*ec2.InstanceNetworkInterface, error) {
for _, iface := range ifaces {
if iface.NetworkInterfaceId == nil || iface.SubnetId == nil || iface.PrivateIpAddress == nil {
continue
}

if iface.Status == nil || *iface.Status != inuse {
continue
}

if *iface.NetworkInterfaceId == name {
return iface, nil
}
}

return nil, fmt.Errorf("can't find the interface %s on instance %s", name, h.conf.Instance)
}

func (h *Hoster) getNetworkInterfaceBySubnet(name string, ifaces []*ec2.InstanceNetworkInterface) (*ec2.InstanceNetworkInterface, error) {
for _, iface := range ifaces {
if iface.NetworkInterfaceId == nil || iface.SubnetId == nil || iface.PrivateIpAddress == nil {
continue
}

if iface.Status == nil || *iface.Status != inuse {
continue
}

if *iface.SubnetId == name {
return iface, nil
}
}

return nil, fmt.Errorf("can't find an interface on subnet %s for instance %s", name, h.conf.Instance)
}

func (h *Hoster) getNetworkInterfaceByTargetIP(name string, ifaces []*ec2.InstanceNetworkInterface) (*ec2.InstanceNetworkInterface, error) {
for _, iface := range ifaces {
if iface.NetworkInterfaceId == nil || iface.SubnetId == nil || iface.PrivateIpAddress == nil {
continue
}

if iface.Status == nil || *iface.Status != inuse {
continue
}

if *iface.PrivateIpAddress == name {
return iface, nil
}
}

return nil, fmt.Errorf("can't find an interface with IP %s for instance %s", name, h.conf.Instance)
}

// OnThisHoster returns true when we run on an gce instance
func (h *Hoster) OnThisHoster() bool {
sess, err := session.NewSession(aws.NewConfig().WithMaxRetries(3))
Expand All @@ -135,7 +220,6 @@ func (h *Hoster) Preempt() error {

h.log.Infof("Preempting %s route(s)\n", h.conf.IP)

// contrary to GCE, an EC2 VPC can have several routes tables
for _, table := range h.routes {
var err error

Expand All @@ -155,7 +239,7 @@ func (h *Hoster) Preempt() error {
}

if err != nil {
h.log.Fatalf("Failed to create a route: %v", err)
h.log.Fatalf("Failed to create a route: %v\n", err)
}
}

Expand Down
Loading

0 comments on commit f92ceaa

Please sign in to comment.