aboutsummaryrefslogtreecommitdiff
path: root/scripts/resolve-profile.py.in
blob: 5aca4d787544eda3474652e4c9549c82efe9efe3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
@PYTHON@
# vim: set ts=4 et:

import json
import os
import shutil
import sys
import boto3
from botocore.exceptions import ClientError
from datetime import datetime, timedelta
from pyhocon import ConfigFactory

if len(sys.argv) != 2:
    sys.exit("Usage: " + os.path.basename(__file__) + " <profile>")

PROFILE = sys.argv[1]

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))

# path to the profile config file
PROFILE_CONF = os.path.join(SCRIPT_DIR, '..', 'profiles', PROFILE + '.conf')

# load the profile's build configuration
BUILDS = ConfigFactory.parse_file(PROFILE_CONF)['BUILDS']

# where we store the profile's builds' config/output
PROFILE_DIR = os.path.join(SCRIPT_DIR, 'profile', PROFILE)
if not os.path.exists(PROFILE_DIR):
    os.makedirs(PROFILE_DIR)

# fold these build config keys' dict to scalar
FOLD_DICTS = {
    'ami_access': ',{0}',
    'ami_regions': ',{0}',
    'repos': "\n@{1} {0}",
    'pkgs': ' {0}@{1}',
    'kernel_modules': ',{0}',
    'kernel_options': ' {0}'
}

NOW = datetime.utcnow()
ONE_DAY = timedelta(days=1)


# func to fold dict down to scalar
def fold(fdict, ffmt):
    folded = ''
    for fkey, fval in fdict.items():
        fkey = fkey.strip('"')    # complex keys may be in quotes
        if fval is True:
            folded += ffmt[0] + fkey
        elif fval not in [None, False]:
            folded += ffmt.format(fkey, fval)
    return folded[1:]


# list of AWS regions, and whether they're enabled
all_regions = {}
AWS = boto3.session.Session()
sys.stderr.write("\n>>> Determining region availability...")
sys.stderr.flush()
for region in AWS.get_available_regions('ec2'):
    ec2 = AWS.client('ec2', region_name=region)
    try:
        ec2.describe_regions()
    except ClientError as e:
        if e.response['Error']['Code'] == 'AuthFailure':
            sys.stderr.write('-')
            sys.stderr.flush()
            all_regions[region] = False
            continue
        elif e.response['Error']['Code'] == 'UnauthorizedOperation':
            # have access to the region, but not to ec2:DescribeRegions
            pass
        else:
            raise
    sys.stderr.write('+')
    sys.stderr.flush()
    all_regions[region] = True
sys.stderr.write("\n")

# parse/resolve HOCON profile's builds' config
for build, cfg in BUILDS.items():
    sys.stderr.write(f">>> Resolving configuration for '{build}'\n")
    build_dir = os.path.join(PROFILE_DIR, build)

    # make a fresh profile build directory
    if os.path.exists(build_dir):
        shutil.rmtree(build_dir)
    os.makedirs(build_dir)

    # populate profile build vars
    cfg['profile'] = PROFILE
    cfg['profile_build'] = build

    # mostly edge-related temporal substitutions
    if cfg['end_of_life'] == '@TOMORROW@':
        cfg['end_of_life'] = (NOW + ONE_DAY).isoformat(timespec='seconds')
    elif cfg['end_of_life'] is not None:
        # to explicitly UTC-ify end_of_life
        cfg['end_of_life'] = datetime.fromisoformat(
            cfg['end_of_life'] + '+00:00').isoformat(timespec='seconds')
    if cfg['revision'] == '@NOW@':
        cfg['revision'] = NOW.strftime('%Y%m%d%H%M%S')

    # 'ALL' region expansion (or retraction)
    if 'ALL' in cfg['ami_regions']:
        all_val = cfg['ami_regions']['ALL']
        if all_val not in [None, False]:
            cfg['ami_regions'] = all_regions
        else:
            cfg['ami_regions'] = {}
    else:
        # warn/remove disabled regions
        for region, enabled in all_regions.items():
            if enabled is not False or region not in cfg['ami_regions']:
                continue
            if cfg['ami_regions'][region] not in [None, False]:
                sys.stderr.write(f"*** WARNING: skipping disabled region {region}\n")
                cfg['ami_regions'][region] = False

    # fold dict vars to scalars
    for foldkey, foldfmt in FOLD_DICTS.items():
        cfg[foldkey] = fold(cfg[foldkey], foldfmt)

    # fold 'svcs' dict to scalar
    lvls = {}
    for svc, lvl in cfg['svcs'].items():
        if lvl is True:
            # service in default runlevel
            lvls['default'].append(svc)
        elif lvl not in [None, False]:
            # service in specified runlevel (skip svc when false/null)
            if lvl not in lvls.keys():
                lvls[lvl] = []
            lvls[lvl].append(svc)
    cfg['svcs'] = ' '.join(
        str(lvl) + '=' + ','.join(
            str(svc) for svc in svcs
        ) for lvl, svcs in lvls.items()
    )

    # resolve ami_name and ami_desc
    cfg['ami_name'] = cfg['ami_name'].format(var=cfg)
    cfg['ami_desc'] = cfg['ami_desc'].format(var=cfg)

    # write build vars file
    with open(os.path.join(build_dir, 'vars.json'), 'w') as out:
        json.dump(cfg, out, indent=4, separators=(',', ': '))