aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-05-21 10:45:05 -0700
committerMike Crute <mike@crute.us>2020-05-21 16:45:17 -0700
commit1fd42af98dac79011496a7956a8cf592b596e2cf (patch)
tree6865f6c46f78b8b2757d2b1e52b2d4a8bdc7d2c6
parentfe362af91ffbe5874bce764c3230ae857693e359 (diff)
downloadalpine-ec2-ami-1fd42af98dac79011496a7956a8cf592b596e2cf.tar.bz2
alpine-ec2-ami-1fd42af98dac79011496a7956a8cf592b596e2cf.tar.xz
alpine-ec2-ami-1fd42af98dac79011496a7956a8cf592b596e2cf.zip
Refactor resolve-profile script
This is paving the way for identity broker improvements for opt-in regions. The output is functionally identical between the two scripts modulo the svcs change. Hopefully this makes the transformation process a little more clear.
-rw-r--r--profiles/base/146
-rw-r--r--scripts/resolve-profile.py.in273
2 files changed, 152 insertions, 167 deletions
diff --git a/profiles/base/1 b/profiles/base/1
index fdda3f0..18decd1 100644
--- a/profiles/base/1
+++ b/profiles/base/1
@@ -56,25 +56,33 @@ pkgs {
56 tzdata = true 56 tzdata = true
57} 57}
58svcs { 58svcs {
59 devfs = "sysinit" 59 sysinit {
60 dmesg = "sysinit" 60 devfs = true
61 hwdrivers = "sysinit" 61 dmesg = true
62 mdev = "sysinit" 62 hwdrivers = true
63 acpid = "boot" 63 mdev = true
64 bootmisc = "boot" 64 }
65 hostname = "boot" 65 boot {
66 hwclock = "boot" 66 acpid = true
67 modules = "boot" 67 bootmisc = true
68 swap = "boot" 68 hostname = true
69 sysctl = "boot" 69 hwclock = true
70 syslog = "boot" 70 modules = true
71 chronyd = "default" 71 swap = true
72 networking = "default" 72 sysctl = true
73 sshd = "default" 73 syslog = true
74 tiny-ec2-bootstrap = "default" 74 }
75 killprocs = "shutdown" 75 default {
76 mount-ro = "shutdown" 76 chronyd = true
77 savecache = "shutdown" 77 networking = true
78 sshd = true
79 tiny-ec2-bootstrap = true
80 }
81 shutdown {
82 killprocs = true
83 mount-ro = true
84 savecache = true
85 }
78} 86}
79kernel_modules { 87kernel_modules {
80 sd-mod = true 88 sd-mod = true
diff --git a/scripts/resolve-profile.py.in b/scripts/resolve-profile.py.in
index 884cd2d..2905423 100644
--- a/scripts/resolve-profile.py.in
+++ b/scripts/resolve-profile.py.in
@@ -1,156 +1,133 @@
1@PYTHON@ 1@PYTHON@
2# vim: set ts=4 et: 2# vim: set ts=4 et:
3 3
4import json
5import os 4import os
6import shutil
7import sys 5import sys
8import boto3 6import json
9from botocore.exceptions import ClientError 7import shutil
8import argparse
10from datetime import datetime, timedelta 9from datetime import datetime, timedelta
10
11from pyhocon import ConfigFactory 11from pyhocon import ConfigFactory
12 12
13if len(sys.argv) != 2: 13
14 sys.exit("Usage: " + os.path.basename(__file__) + " <profile>") 14# Just group together our transforms
15 15class Transforms:
16PROFILE = sys.argv[1] 16
17 17 NOW = datetime.utcnow()
18SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) 18 TOMORROW = NOW + timedelta(days=1)
19 19
20# path to the profile config file 20 unquote = lambda x: x.strip('"')
21PROFILE_CONF = os.path.join(SCRIPT_DIR, '..', 'profiles', PROFILE + '.conf') 21
22 22 @staticmethod
23# load the profile's build configuration 23 def force_iso_date(input):
24BUILDS = ConfigFactory.parse_file(PROFILE_CONF)['BUILDS'] 24 return datetime.fromisoformat(input).isoformat(timespec="seconds")
25 25
26# where we store the profile's builds' config/output 26 @classmethod
27PROFILE_DIR = os.path.join(SCRIPT_DIR, 'profile', PROFILE) 27 def resolve_tomorrow(cls, input):
28if not os.path.exists(PROFILE_DIR): 28 return cls.TOMORROW.isoformat(timespec="seconds")
29 os.makedirs(PROFILE_DIR) 29
30 30 @classmethod
31# fold these build config keys' dict to scalar 31 def resolve_now(cls, input):
32FOLD_DICTS = { 32 return cls.NOW.strftime("%Y%m%d%H%M%S")
33 'ami_access': ',{0}', 33
34 'ami_regions': ',{0}', 34 @classmethod
35 'repos': "\n@{1} {0}", 35 def fold_comma(cls, input):
36 'pkgs': ' {0}@{1}', 36 return ",".join([cls.unquote(k) for k in input.keys()])
37 'kernel_modules': ',{0}', 37
38 'kernel_options': ' {0}' 38 @classmethod
39} 39 def fold_space(cls, input):
40 40 return " ".join([cls.unquote(k) for k in input.keys()])
41NOW = datetime.utcnow() 41
42ONE_DAY = timedelta(days=1) 42 @classmethod
43 43 def fold_repos(cls, input):
44 44 return "\n".join(
45# func to fold dict down to scalar 45 f"@{v} {cls.unquote(k)}" if isinstance(v, str) else cls.unquote(k)
46def fold(fdict, ffmt): 46 for k, v in input.items())
47 folded = '' 47
48 for fkey, fval in fdict.items(): 48 @staticmethod
49 fkey = fkey.strip('"') # complex keys may be in quotes 49 def fold_packages(input):
50 if fval is True: 50 return " ".join(
51 folded += ffmt[0] + fkey 51 f"{k}@{v}" if isinstance(v, str) else k
52 elif fval not in [None, False]: 52 for k, v in input.items())
53 folded += ffmt.format(fkey, fval) 53
54 return folded[1:] 54 @staticmethod
55 55 def fold_services(input):
56 56 return " ".join(
57# list of AWS regions, and whether they're enabled 57 "{}={}".format(k, ",".join(v.keys()))
58all_regions = {} 58 for k, v in input.items())
59AWS = boto3.session.Session() 59
60sys.stderr.write("\n>>> Determining region availability...") 60
61sys.stderr.flush() 61class ConfigBuilder:
62for region in AWS.get_available_regions('ec2'): 62
63 ec2 = AWS.client('ec2', region_name=region) 63 _CFG_TRANSFORMS = {
64 try: 64 "ami_access" : Transforms.fold_comma,
65 ec2.describe_regions() 65 "ami_regions" : Transforms.fold_comma,
66 except ClientError as e: 66 "kernel_modules" : Transforms.fold_comma,
67 if e.response['Error']['Code'] == 'AuthFailure': 67 "kernel_options" : Transforms.fold_space,
68 sys.stderr.write('-') 68 "repos" : Transforms.fold_repos,
69 sys.stderr.flush() 69 "pkgs" : Transforms.fold_packages,
70 all_regions[region] = False 70 "svcs" : Transforms.fold_services,
71 continue 71 "revision" : Transforms.resolve_now,
72 elif e.response['Error']['Code'] == 'UnauthorizedOperation': 72 "end_of_life" : lambda x: \
73 # have access to the region, but not to ec2:DescribeRegions 73 Transforms.force_iso_date(Transforms.resolve_tomorrow(x)),
74 pass 74 }
75 else: 75
76 raise 76 def __init__(self, config_path, out_dir):
77 sys.stderr.write('+') 77 self.config_path = config_path
78 sys.stderr.flush() 78 self.out_dir = out_dir
79 all_regions[region] = True 79
80sys.stderr.write("\n") 80 def build(self, profile):
81 81 build_config = ConfigFactory.parse_file(self.config_path)
82for region, available in all_regions.items(): 82
83 if available is False: 83 for build, cfg in build_config["BUILDS"].items():
84 sys.stderr.write(f"*** WARNING: skipping disabled region {region}\n") 84 build_dir = os.path.join(self.out_dir, build)
85 85
86print() 86 # Always start fresh
87 87 shutil.rmtree(build_dir, ignore_errors=True)
88# parse/resolve HOCON profile's builds' config 88 os.makedirs(build_dir)
89for build, cfg in BUILDS.items(): 89
90 print(f">>> Resolving configuration for '{build}'") 90 cfg["profile"] = profile
91 build_dir = os.path.join(PROFILE_DIR, build) 91 cfg["profile_build"] = build
92 92
93 # make a fresh profile build directory 93 # Order of operations is important here
94 if os.path.exists(build_dir): 94 for k, v in cfg.items():
95 shutil.rmtree(build_dir) 95 transform = self._CFG_TRANSFORMS.get(k)
96 os.makedirs(build_dir) 96 if transform:
97 97 cfg[k] = transform(v)
98 # populate profile build vars 98
99 cfg['profile'] = PROFILE 99 if isinstance(v, str) and "{var." in v:
100 cfg['profile_build'] = build 100 cfg[k] = v.format(var=cfg)
101 101
102 # mostly edge-related temporal substitutions 102 with open(os.path.join(build_dir, "vars.json"), "w") as out:
103 if cfg['end_of_life'] == '@TOMORROW@': 103 json.dump(cfg, out, indent=4, separators=(",", ": "))
104 cfg['end_of_life'] = (NOW + ONE_DAY).isoformat(timespec='seconds') 104
105 elif cfg['end_of_life'] is not None: 105
106 # to explicitly UTC-ify end_of_life 106def find_repo_root():
107 cfg['end_of_life'] = datetime.fromisoformat( 107 path = os.getcwd()
108 cfg['end_of_life'] + '+00:00').isoformat(timespec='seconds') 108
109 if cfg['revision'] == '@NOW@': 109 while ".git" not in set(os.listdir(path)) and path != "/":
110 cfg['revision'] = NOW.strftime('%Y%m%d%H%M%S') 110 path = os.path.dirname(path)
111 111
112 # 'ALL' region expansion (or retraction) 112 if path == "/":
113 if 'ALL' in cfg['ami_regions']: 113 raise Exception("No repo found, stopping at /")
114 all_val = cfg['ami_regions']['ALL'] 114
115 if all_val not in [None, False]: 115 return path
116 cfg['ami_regions'] = all_regions 116
117 else: 117
118 cfg['ami_regions'] = {} 118def main(args):
119 else: 119 parser = argparse.ArgumentParser(description="Build Packer JSON variable "
120 # warn/remove disabled regions 120 "files from HOCON build profiles")
121 for region, enabled in all_regions.items(): 121 parser.add_argument("profile", help="name of profile to build")
122 if enabled is not False or region not in cfg['ami_regions']: 122 args = parser.parse_args()
123 continue 123
124 if cfg['ami_regions'][region] not in [None, False]: 124 root = find_repo_root()
125 cfg['ami_regions'][region] = False 125
126 126 ConfigBuilder(
127 # fold dict vars to scalars 127 os.path.join(root, "profiles", f"{args.profile}.conf"),
128 for foldkey, foldfmt in FOLD_DICTS.items(): 128 os.path.join(root, "build", "profile", args.profile)
129 cfg[foldkey] = fold(cfg[foldkey], foldfmt) 129 ).build(args.profile)
130 130
131 # fold 'svcs' dict to scalar 131
132 lvls = {} 132if __name__ == "__main__":
133 for svc, lvl in cfg['svcs'].items(): 133 main(sys.argv)
134 if lvl is True:
135 # service in default runlevel
136 lvls['default'].append(svc)
137 elif lvl not in [None, False]:
138 # service in specified runlevel (skip svc when false/null)
139 if lvl not in lvls.keys():
140 lvls[lvl] = []
141 lvls[lvl].append(svc)
142 cfg['svcs'] = ' '.join(
143 str(lvl) + '=' + ','.join(
144 str(svc) for svc in svcs
145 ) for lvl, svcs in lvls.items()
146 )
147
148 # resolve ami_name and ami_desc
149 cfg['ami_name'] = cfg['ami_name'].format(var=cfg)
150 cfg['ami_desc'] = cfg['ami_desc'].format(var=cfg)
151
152 # write build vars file
153 with open(os.path.join(build_dir, 'vars.json'), 'w') as out:
154 json.dump(cfg, out, indent=4, separators=(',', ': '))
155
156print()