Skip to content

Commit 5699e75

Browse files
committed
terraform/aws: Add a script to generate the Kconfig.locations menu
Enable kdevops to quickly pick up new regions and availability zones whenever they are introduced by AWS. The regions that are discovered and output by this script are those that the user has opted-in or are "no opt-in required". boto3 reuses connections to the API endpoint, making the generation of the Kconfig menu quick. Jinja2 lets us manage a large amount of bespoke Kconfig help text painlessly. The new script reports some basic authentication issues and has a "raw" output to enable troubleshooting. This implementation is still missing geographic location information that is retrieved via the 'pricing' API. IMO that can be introduced later. To use: $ terraform/aws/scripts/gen_kconfig_location > terraform/aws/kconfigs/Kconfig.location Signed-off-by: Chuck Lever <[email protected]>
1 parent 7231134 commit 5699e75

File tree

3 files changed

+402
-0
lines changed

3 files changed

+402
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
#!/usr/bin/env python3
2+
# ex: set filetype=python:
3+
4+
"""
5+
Retrieve region and availability zone information from AWS. Use
6+
it to construct the "locations" Kconfig menu.
7+
"""
8+
9+
import sys
10+
import argparse
11+
import os
12+
13+
from configparser import ConfigParser
14+
from jinja2 import Environment, FileSystemLoader
15+
16+
import boto3
17+
from botocore.exceptions import ClientError, NoCredentialsError
18+
19+
20+
def get_default_region():
21+
"""
22+
Get the default AWS region from the ~/.aws/config file.
23+
24+
Returns:
25+
str: Default region, or 'us-east-1' if no default
26+
is found.
27+
"""
28+
config_path = os.path.expanduser('~/.aws/config')
29+
if os.path.exists(config_path):
30+
try:
31+
config = ConfigParser()
32+
config.read(config_path)
33+
if 'default' in config:
34+
return config['default'].get('region', 'us-east-1')
35+
if 'profile default' in config:
36+
return config['profile default'].get('region', 'us-east-1')
37+
except Exception as e:
38+
print(f"Warning: Error reading AWS config file: {e}", file=sys.stderr)
39+
return 'us-east-1'
40+
41+
42+
def get_all_regions():
43+
"""
44+
Retrieve the list of all AWS regions. Convert it to a dictionary
45+
keyed on region name
46+
47+
Returns:
48+
list: sort list of dictionaries each containing a region
49+
"""
50+
try:
51+
ec2 = boto3.client('ec2')
52+
response = ec2.describe_regions(AllRegions=True)
53+
54+
regions = {}
55+
for region in response['Regions']:
56+
region_name = region['RegionName']
57+
regions[region_name] = {
58+
'region_name': region_name,
59+
'opt_in_status': region.get('OptInStatus', 'Unknown'),
60+
'end_point': region.get('Endpoint')
61+
}
62+
return sorted(regions.values(), key=lambda x: x['region_name'])
63+
except Exception as e:
64+
print(f"Error retrieving AWS regions: {e}", file=sys.stderr)
65+
return {}
66+
67+
68+
def get_region_info(regions, region_name, quiet=False):
69+
"""
70+
Get detailed information about a specific region including availability zones.
71+
72+
Args:
73+
region_name (str): AWS region name (e.g., 'us-east-1', 'eu-west-1')
74+
quiet (bool): Suppress debug messages
75+
76+
Returns:
77+
dict: Dictionary containing region information and availability zones
78+
"""
79+
try:
80+
if not quiet:
81+
print(f"Querying information for region {region_name}...", file=sys.stderr)
82+
83+
region_info = next(filter(lambda x: x['region_name'] == region_name, regions), None)
84+
if not region_info:
85+
if not quiet:
86+
print(f"Region {region_name} was not found", file=sys.stderr)
87+
return None
88+
if region_info['opt_in_status'] == "not-opted-in":
89+
if not quiet:
90+
print(f"Region {region_name} is not accessible.", file=sys.stderr)
91+
return None
92+
93+
ec2 = boto3.client('ec2', region_name=region_name)
94+
response = ec2.describe_availability_zones(
95+
AllAvailabilityZones=True,
96+
Filters=[
97+
{'Name': 'region-name', 'Values': [region_name]},
98+
{'Name': 'zone-type', 'Values': ['availability-zone']},
99+
]
100+
)
101+
availability_zones = []
102+
for zone in response['AvailabilityZones']:
103+
zone_info = {
104+
'zone_id': zone['ZoneId'],
105+
'zone_name': zone['ZoneName'],
106+
'zone_type': zone.get('ZoneType', 'availability-zone'),
107+
'state': zone['State'],
108+
}
109+
availability_zones.append(zone_info)
110+
111+
result = {
112+
'region_name': region_info['region_name'],
113+
'endpoint': region_info.get('end_point', f'ec2.{region_name}.amazonaws.com'),
114+
'opt_in_status': region_info.get('opt_in_status', 'opt-in-not-required'),
115+
'availability_zone_count': len(availability_zones),
116+
'availability_zones': sorted(availability_zones, key=lambda x: x['zone_name'])
117+
}
118+
119+
if not quiet:
120+
print(f"Found {len(availability_zones)} availability zones in {region_name}",
121+
file=sys.stderr)
122+
123+
return result
124+
125+
except NoCredentialsError:
126+
print("Error: AWS credentials not found. Please configure your credentials.",
127+
file=sys.stderr)
128+
return None
129+
except ClientError as e:
130+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
131+
if error_code in ['UnauthorizedOperation', 'InvalidRegion']:
132+
print(f"Error: Cannot access region {region_name}. Check region name and permissions.",
133+
file=sys.stderr)
134+
else:
135+
print(f"AWS API Error: {e}", file=sys.stderr)
136+
return None
137+
except Exception as e:
138+
print(f"Unexpected error: {e}", file=sys.stderr)
139+
return None
140+
141+
142+
def output_region_kconfig(region_info):
143+
"""Output region information in Kconfig format."""
144+
environment = Environment(
145+
loader=FileSystemLoader(sys.path[0]),
146+
trim_blocks=True,
147+
lstrip_blocks=True,
148+
)
149+
template = environment.get_template("zone.j2")
150+
print(
151+
template.render(
152+
region_name=region_info['region_name'].upper().replace('-', '_'),
153+
zones=region_info['availability_zones'],
154+
)
155+
)
156+
157+
158+
def output_region_raw(region_info, quiet=False):
159+
"""Output region information in table format."""
160+
if not quiet:
161+
print(f"Region: {region_info['region_name']}")
162+
print(f"Endpoint: {region_info['endpoint']}")
163+
print(f"Opt-in status: {region_info['opt_in_status']}")
164+
print(f"Availability Zones: {region_info['availability_zone_count']}")
165+
166+
print(f"{'Zone Name':<15} {'Zone ID':<15} {'Zone Type':<18} {'State':<12} {'Parent Zone':<15}")
167+
print("-" * 80)
168+
169+
for zone in region_info['availability_zones']:
170+
parent_zone = zone.get('parent_zone_name', '') or zone.get('parent_zone_id', '')
171+
zone_type = zone.get('zone_type', 'availability_zone')
172+
print(f"{zone['zone_name']:<15} "
173+
f"{zone['zone_id']:<15} "
174+
f"{zone_type:<18} "
175+
f"{zone['state']:<12} "
176+
f"{parent_zone:<15}")
177+
178+
179+
def output_regions_kconfig(regions):
180+
"""Output available regions in kconfig format."""
181+
environment = Environment(
182+
loader=FileSystemLoader(sys.path[0]),
183+
trim_blocks=True,
184+
lstrip_blocks=True,
185+
)
186+
template = environment.get_template("regions.j2")
187+
print(
188+
template.render(
189+
default_region=get_default_region().upper().replace('-', '_'),
190+
regions=regions,
191+
)
192+
)
193+
194+
195+
def output_regions_raw(regions, quiet=False):
196+
"""Output available regions in table format."""
197+
if not quiet:
198+
print(f"Available AWS regions ({len(regions)}):\n")
199+
print(f"{'Region Name':<20} {'Opt-in Status':<20}")
200+
print("-" * 42)
201+
202+
for region in regions:
203+
opt_in_status = region.get('opt_in_status', 'Unknown')
204+
print(f"{region['region_name']:<20} {opt_in_status:<20}")
205+
206+
207+
def output_locations_kconfig(regions):
208+
"""Output the locations menu in Kconfg format."""
209+
environment = Environment(
210+
loader=FileSystemLoader(sys.path[0]),
211+
trim_blocks=True,
212+
lstrip_blocks=True,
213+
)
214+
template = environment.get_template("regions.j2")
215+
print(
216+
template.render(
217+
default_region=get_default_region().upper().replace('-', '_'),
218+
regions=regions,
219+
)
220+
)
221+
222+
template = environment.get_template("zone.j2")
223+
for region in regions:
224+
region_info = get_region_info(regions, region['region_name'], True)
225+
if region_info:
226+
print()
227+
print(
228+
template.render(
229+
region_name=region_info['region_name'].upper().replace('-', '_'),
230+
zones=region_info['availability_zones'],
231+
)
232+
)
233+
234+
235+
def parse_arguments():
236+
"""Parse command line arguments."""
237+
parser = argparse.ArgumentParser(
238+
description='Get AWS region and availability zone information',
239+
formatter_class=argparse.RawDescriptionHelpFormatter,
240+
epilog="""
241+
Examples:
242+
python %(prog)s --regions
243+
python %(prog)s us-east-1
244+
python %(prog)s ap-southeast-1 --quiet
245+
"""
246+
)
247+
parser.add_argument(
248+
'region_name',
249+
nargs='?',
250+
help='AWS region name (e.g., us-east-1, eu-west-1, ap-southeast-1)'
251+
)
252+
253+
parser.add_argument(
254+
'--format', '-f',
255+
choices=['raw', 'kconfig'],
256+
default='kconfig',
257+
help='Output format (default: kconfig)'
258+
)
259+
parser.add_argument(
260+
'--quiet', '-q',
261+
action='store_true',
262+
help='Suppress informational messages'
263+
)
264+
parser.add_argument(
265+
'--regions',
266+
action='store_true',
267+
help='List all available AWS regions'
268+
)
269+
return parser.parse_args()
270+
271+
272+
def main():
273+
"""Main function to run the program."""
274+
args = parse_arguments()
275+
276+
if not args.quiet:
277+
print("Fetching list of all AWS regions...", file=sys.stderr)
278+
regions = get_all_regions()
279+
if not regions:
280+
sys.exit(1)
281+
282+
if args.regions:
283+
if args.format == 'kconfig':
284+
output_regions_kconfig(regions)
285+
else:
286+
output_regions_raw(regions, args.quiet)
287+
return
288+
289+
if args.region_name:
290+
if not args.quiet:
291+
print(f"Fetching information for region {args.region_name}...", file=sys.stderr)
292+
293+
region_info = get_region_info(regions, args.region_name, args.quiet)
294+
if region_info:
295+
if args.format == 'kconfig':
296+
output_region_kconfig(region_info)
297+
else:
298+
output_region_raw(region_info, args.quiet)
299+
else:
300+
print(f"Could not retrieve information for region '{args.region_name}'.", file=sys.stderr)
301+
print("Try running with --regions to see available regions.", file=sys.stderr)
302+
sys.exit(1)
303+
return
304+
305+
output_locations_kconfig(regions)
306+
307+
308+
if __name__ == "__main__":
309+
main()

terraform/aws/scripts/regions.j2

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
choice
2+
prompt "AWS region"
3+
default TERRAFORM_AWS_REGION_{{ default_region }}
4+
help
5+
Use this option to select the AWS region that hosts your
6+
compute and storage resources. If you do not explicitly
7+
specify a region, the US West (Oregon) region is the
8+
default.
9+
10+
Once selected, you can stick with the default AV zone
11+
chosen by kdevops, or use:
12+
13+
aws ec2 describe-availability-zones --region <region-name>
14+
15+
to list the Availability Zones that are enabled for your
16+
AWS account. Enter your selection from this list using the
17+
TERRAFORM_AWS_AV_ZONE menu.
18+
19+
If you wish to expand on the region list, send a patch after
20+
reading this list:
21+
22+
https://docs.aws.amazon.com/general/latest/gr/rande.html
23+
24+
Note that if you change region the AMI may change as well even
25+
for the same distribution. At least that applies to Amazon EC2
26+
optimized images. Use the AWS console, to set that up it will
27+
ask you for your credentials and then a region. Before adding
28+
an entry for ami image be sure you are on the region and then
29+
query with something like:
30+
31+
aws ec2 describe-images --image-ids ami-0efa651876de2a5ce
32+
33+
For instance, this AMI belongs to us-west-2 only. us-east* have
34+
other AMIs for the same Amazon 2023 EC2 image. The output from
35+
here tells me:
36+
37+
"OwnerId": "137112412989"
38+
39+
And that is what value to use for ami-0efa651876de2a5ce
40+
for the TERRAFORM_AWS_AMI_OWNER. To get the ami-* for your regions
41+
just go to your EC2 console, that console will be associated with
42+
a region already. You have to change regions if you want to look
43+
for AMIs in other regions. There are for example two different
44+
ami-* values for Amazon Linux 2023 for different regions. However
45+
they values can be same. For example below are the us-west-2 queries
46+
for Amazon Linux 2023 for x86_64 and then for ARM64.
47+
48+
aws ec2 describe-images --image-ids ami-0efa651876de2a5ce | grep OwnerId
49+
"OwnerId": "137112412989",
50+
aws ec2 describe-images --image-ids ami-0699f753302dd8b00 | grep OwnerId
51+
"OwnerId": "137112412989",
52+
53+
{% for region in regions %}
54+
{% if region['opt_in_status'] != 'not-opted-in' %}
55+
config TERRAFORM_AWS_REGION_{{ region['region_name'].upper().replace('-', '_') }}
56+
bool "{{ region['region_name'] }}"
57+
help
58+
Use the {{ region['region_name'] }} region.
59+
60+
{% endif %}
61+
{% endfor %}
62+
endchoice
63+
64+
config TERRAFORM_AWS_REGION
65+
string
66+
output yaml
67+
{% for region in regions %}
68+
{% if region['opt_in_status'] != 'not-opted-in' %}
69+
default "{{ region['region_name'] }}" if TERRAFORM_AWS_REGION_{{ region['region_name'].upper().replace('-', '_') }}
70+
{% endif %}
71+
{% endfor %}

0 commit comments

Comments
 (0)