Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ipv6 support #1204

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
40 changes: 33 additions & 7 deletions projects/elife.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,40 @@ defaults:
# optional: specify `ports` to be opened
security-group: {}
region: us-east-1
vpc-id: vpc-78a2071d # vpc-id + subnet-id are peculiar to AWS account + region
vpc-id: vpc-78a2071d

subnet-id: subnet-1d4eb46a # elife-public-subnet, us-east-1d
subnet-cidr: '10.0.2.0/24'
# project supports IPv6.
use-ipv6: false # create DNS AAAA records, add IPv6 security group CIDRs

subnet-id: subnet-1d4eb46a # elife-public-subnet
subnet-cidr: '10.0.2.0/24' # *internal* subnet-cidr, always ipv4, used with RDS
availability-zone: us-east-1d

redundant-subnet-id: subnet-7a31dd46 # elife-public-subnet-2, us-east-1e
# ipv6 alternate
#subnet-id: subnet-0866e00eb5b9e27d4 # ipv6-elife-public-subnet
#subnet-cidr: '10.0.15.0/24' # contradiction? no! subnet is not *pure* ipv6. internally it uses ipv4.
#availability-zone: us-east-1d

redundant-subnet-id: subnet-7a31dd46 # elife-public-subnet-2
redundant-subnet-cidr: '10.0.3.0/24'
redundant-availability-zone: us-east-1e

# ipv6 alternate
#redundant-subnet-id: subnet-02000374730149bf6 # ipv6-elife-public-subnet-2
#redundant-subnet-cidr: '10.0.16.0/24'
#redundant-availability-zone: us-east-1e

# lsh@2022-06-27: t3.* instance types not available in us-east-1e
# - https://aws.amazon.com/premiumsupport/knowledge-center/ec2-instance-type-not-supported-az-error/
redundant-subnet-id-2: subnet-2116727b # elife-public-subnet-3, us-east-1a
redundant-subnet-id-2: subnet-2116727b # elife-public-subnet-3
redundant-subnet-cidr-2: '10.0.10.0/24'
redundant-availability-zone-2: us-east-1a

# ipv6 alternate
#redundant-subnet-id-2: subnet-03a5217409cbe6633 # ipv6-elife-public-subnet-3
#redundant-subnet-cidr-2: 10.0.17.0/24
#redundant-availability-zone-2: us-east-1a

rds:
multi-az: false
engine: postgres # or 'MySQL'
Expand Down Expand Up @@ -445,6 +463,14 @@ basebox:
ami: ami-027a754129abb5386 # focal, build 20231208, hvm:ebs-ssd
ports:
- 22
aws-alt:
ipv6test:
use-ipv6: true
subnet-id: subnet-0866e00eb5b9e27d4 # ipv6-elife-public-subnet
subnet-cidr: '10.0.15.0/24'
ec2:
ami: ami-08d0e948516bdfee8 # GENERATED created from basebox--2004

vagrant: {}

heavybox:
Expand Down Expand Up @@ -1490,8 +1516,8 @@ search:
vagrant:
ram: 4096
ports:
1244: 80
1245: 8080
1244: 80 # webserver
1245: 8080 # api-dummy
9921: 9201 # opensearch

recommendations:
Expand Down
4 changes: 3 additions & 1 deletion src/buildercore/cfngen.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ def build_context_aws(pdata, context):
'account-id',
'vpc-id',

'use-ipv6',

'subnet-id',
'subnet-cidr',
'availability-zone',
Expand Down Expand Up @@ -1094,7 +1096,7 @@ def _current_cloudformation_template(stackname):
raise

def download_cloudformation_template(stackname):
cloudformation.write_template(stackname, json.dumps(_current_cloudformation_template(stackname)))
cloudformation.write_template(stackname, json.dumps(_current_cloudformation_template(stackname), indent=4))

def regenerate_stack(stackname, **more_context):
current_context = context_handler.load_context(stackname)
Expand Down
38 changes: 25 additions & 13 deletions src/buildercore/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,28 @@ def find_ec2_instances(stackname, state='running', node_ids=None, allow_empty=Fa
raise NoRunningInstancesError("found no running ec2 instances for %r. The stack nodes may have been stopped, but here we were requiring them to be running" % stackname)
return ec2_instances

# prefer this over `find_ec2_instances`
def ec2_data(stackname, state=None):
"""returns a list of raw boto3 EC2.Instance data for ec2 instances attached to given `stackname`.
does not filter by state by default.
does not enforce single instance checking."""
try:
ec2_instances = find_ec2_instances(stackname, state=state, allow_empty=True)
return [ec2.meta.data for ec2 in ec2_instances]
except Exception:
LOG.exception('unhandled exception attempting to discover more information about this instance. Instance may not exist yet.')
raise

def pick_ip_address(ec2_data):
"""returns a public IP address, preferring ipv4 when available over ipv6.
for use with the ec2 instance data returned by `ec2_data`."""
return ec2_data.get('PublicIpAddress') or ec2_data.get('Ipv6Address')

def pick_ip_address_obj(ec2_obj):
"""returns a public IP address, preferring ipv4 when available over ipv6.
for use with the ec2 instance objects returned by `find_ec2_instances`."""
return ec2_obj.public_ip_address or ec2_obj.ipv6_address

#
#
#
Expand Down Expand Up @@ -344,7 +366,7 @@ def stack_conn(stackname, username=config.DEPLOY_USER, node=None, **kwargs):
node and ensure(utils.isint(node) and int(node) > 0, "given node must be an integer and greater than zero")
didx = int(node) - 1 if node else 0 # decrement to a zero-based value
data = data[didx] # data is ordered by node
public_ip = data['PublicIpAddress']
public_ip = pick_ip_address(data)
params = _ec2_connection_params(stackname, username, host_string=public_ip)

with settings(**params):
Expand All @@ -356,7 +378,7 @@ class NoPublicIpsError(Exception):
def all_node_params(stackname):
"returns a map of node data"
data = ec2_data(stackname, state='running')
public_ips = {ec2['InstanceId']: ec2.get('PublicIpAddress') for ec2 in data}
public_ips = {ec2['InstanceId']: pick_ip_address(ec2) for ec2 in data}
nodes = {
ec2['InstanceId']: int(tags2dict(ec2['Tags'])['Node'])
if 'Node' in tags2dict(ec2['Tags'])
Expand Down Expand Up @@ -385,7 +407,7 @@ def stack_all_ec2_nodes(stackname, workfn, username=config.DEPLOY_USER, concurre

data = ec2_data(stackname, state='running')
# TODO: reuse all_node_params?
public_ips = {ec2['InstanceId']: ec2.get('PublicIpAddress') for ec2 in data}
public_ips = {ec2['InstanceId']: pick_ip_address(ec2) for ec2 in data}
nodes = {ec2['InstanceId']: int(tags2dict(ec2['Tags'])['Node']) if 'Node' in tags2dict(ec2['Tags']) else 1 for ec2 in data}
if node:
nodes = {k: v for k, v in nodes.items() if v == int(node)}
Expand Down Expand Up @@ -586,16 +608,6 @@ def describe_stack(stackname, allow_missing=False):
class NoRunningInstancesError(Exception):
pass

def ec2_data(stackname, state=None):
"""returns a list of raw boto3 EC2.Instance data for ec2 instances attached to given `stackname`.
does not filter by state by default.
does not enforce single instance checking."""
try:
ec2_instances = find_ec2_instances(stackname, state=state, allow_empty=True)
return [ec2.meta.data for ec2 in ec2_instances]
except Exception:
LOG.exception('unhandled exception attempting to discover more information about this instance. Instance may not exist yet.')
raise


# DO NOT CACHE: function is used in polling
Expand Down
4 changes: 2 additions & 2 deletions src/buildercore/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ def _wait_for_running_nodes(stackname):
if context['ec2'].get('dns-external-primary'):
primary = 1
primary_hostname = context['ext_node_hostname'] % primary
primary_ip_address = nodes[0].public_ip_address
primary_ip_address = core.pick_ip_address_obj(nodes[0])
LOG.info("External primary full hostname: %s", primary_hostname)
_update_dns_a_record(context['domain'], primary_hostname, primary_ip_address)

Expand All @@ -384,7 +384,7 @@ def _wait_for_running_nodes(stackname):
LOG.info("External full hostname: %s", context['full_hostname'])
if context['full_hostname']:
for node in nodes:
_update_dns_a_record(context['domain'], context['full_hostname'], node.public_ip_address)
_update_dns_a_record(context['domain'], context['full_hostname'], core.pick_ip_address_obj(node))

def _delete_dns_a_record(zone_name, name):
"""deletes a Route53 DNS 'A' record `name` in hosted zone `zone_name`.
Expand Down
Loading