2 ##############################################################################
3 # Copyright (c) 2015,2016 Ericsson AB, Mirantis Inc., Enea AB and others.
4 # mskalski@mirantis.com
5 # Alexandru.Avadanii@enea.com
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 ##############################################################################
12 """Build multiarch partial local Ubuntu mirror using packetary"""
14 ##############################################################################
15 # Design quirks / workarounds:
16 # 1. Fuel-agent uses `debootstrap` to build bootstrap and target chroots from
17 # the local mirror; which only uses the "main" component from the first
18 # repository, i.e. does not include "updates"/"security".
19 # In order to fullfill all debootstrap dependencies in "main" repo, we will
20 # do an extra packetary run using a reduced scope:
21 # - only "main" component of the first mirror;
22 # - reduced package dependency list (without MOS/OPNFV plugin deps).
23 # 2. If repo structure is not mandatory to be in sync with official mirrors,
24 # we can mitigate the issue by "merging" all repo-components into a single
26 ##############################################################################
27 # Mirror build steps (for EACH architecture in UBUNTU_ARCH):
28 # 1. Collect bootstrap package deps from <fuel_bootstrap_cli.yaml>;
29 # 2. Collect all fixture release packages from fuel-web's <openstack.yaml>;
30 # 3. Parse new "opnfv_config.yaml" list of packages (from old fuel-mirror);
31 # 4. Inherit enviroment variable(s) for mirror URLs, paths etc.
32 # - Allow arch-specific overrides for each env var;
33 # 5. Mirror config is defined based on common config + OPNFV overrides;
34 # - Convert old configuration format to packetary style where needed;
35 # 6. Package lists are defined based on common config + OPNFV deps;
36 # - Keep track of "main" packages separately, required by debootstrap;
37 # 7. Clone/update all mirror components;
38 # 8. IF mirror merging is disabled:
39 # - Clone/update "main" mirror component (fix missing debootstrap deps);
40 # 9. IF mirror merging is enabled:
41 # - Use `dpkg-scanpackages` to filter out old versions of duplicate pkgs;
42 # - Run `packetary create` on the set of downloaded packages, merging
43 # them on the fly into a single-component mirror;
44 ##############################################################################
46 from copy import deepcopy
51 from contextlib import contextmanager
52 from cStringIO import StringIO
53 from packetary.cli.app import main
56 def capture_stdout(output):
57 """Context manager for capturing stdout"""
63 # FIXME: Find a better approach for eliminating duplicate logs than this
64 def force_logger_reload():
65 """Force logger reload (ugly hack to prevent log duplication)"""
66 for mod in sys.modules.keys():
67 if mod.startswith('logging'):
69 reload(sys.modules[mod])
73 def get_unres_pkgs(architecture, cfg_mirror):
74 """Determine missing package dependecies for a mirror defition"""
75 unresolved_pkgs = list()
76 packetary_output = StringIO()
77 with capture_stdout(packetary_output):
78 main('unresolved -a {0} -r {1} -c name version --sep ;'
79 .format(_ARCH[architecture], cfg_mirror).split(' '))
80 for dep_pkg in packetary_output.getvalue().splitlines():
81 if dep_pkg.startswith('#'):
83 dep = dep_pkg.split(';')
84 unresolved_pkgs += [{'name': dep[0], 'version': dep[1]}]
86 return unresolved_pkgs
88 def from_legacy_pkglist(legacy_pkglist):
89 """Package list conversion from `old fuel-mirror` to `packetary` style"""
91 for pkg in legacy_pkglist:
92 pkglist += [{'name': pkg}]
95 def to_legacy_pkglist(pkglist):
96 """Package list conversion from `packetary` style to `old fuel-mirror`"""
97 legacy_pkglist = list()
99 legacy_pkglist.append(pkg['name'])
100 return legacy_pkglist
102 def legacy_diff(base_pkglist, new_pkglist, requester, architecture):
103 """Package list diff (old format)"""
104 diff_set = set(new_pkglist)
106 diff_set -= set(base_pkglist)
108 print(' * {0} requires new packages for architecture [{1}]: {2}'
109 .format(requester, architecture, ', '.join(diff_set)))
110 return list(diff_set)
112 def do_local_repo(architecture, cfg_repo, cfg_packages_paths):
113 """Create single-component local repo (one architecture per call)"""
114 # Packetary does not use a global config file, so pass old settings here.
115 main('create -t deb -a {0} --repository {1} --package-files {2}'
116 ' --ignore-errors-num 2 --retries-num 3 --threads-num 10'
117 .format(_ARCH[architecture], cfg_repo, cfg_packages_paths).split(' '))
118 force_logger_reload()
120 def do_partial_mirror(architecture, cfg_mirror, cfg_packages):
121 """Clone partial local mirror (one architecture per call)"""
122 # Note: '-d .' is ignored, as each mirror defines its own path.
123 main('clone -t deb -a {0} -r {1} -R {2} -d .'
124 ' --ignore-errors-num 2 --retries-num 3 --threads-num 10'
125 .format(_ARCH[architecture], cfg_mirror, cfg_packages).split(' '))
126 force_logger_reload()
128 def write_cfg_file(cfg_mirror, data):
129 """Write configuration (yaml) file (package list / mirror defition)"""
130 with open(cfg_mirror, 'w') as outfile:
131 outfile.write(yaml.safe_dump(data, default_flow_style=False))
133 def get_env(env_var, architecture=None):
134 """Evaluate architecture-specific overrides of env vars"""
136 env_var_arch = '{0}_{1}'.format(env_var, architecture)
137 if os.environ.get(env_var_arch):
138 return os.environ[env_var_arch]
139 if os.environ.get(env_var):
140 return os.environ[env_var]
143 # Architecture name mapping (dpkg:packetary) for packetary CLI invocation
150 # Arch-indepedent configuration (old fuel-mirror + OPNFV extra packages)
151 CFG_D = 'opnfv_config'
152 CFG_OPNFV = 'opnfv_config.yaml'
153 MOS_VERSION = get_env('MOS_VERSION')
154 UBUNTU_ARCH = get_env('UBUNTU_ARCH')
155 MIRROR_UBUNTU_PATH = get_env('MIRROR_UBUNTU_OPNFV_PATH')
156 MIRROR_UBUNTU_TMP_PATH = '{0}.tmp'.format(MIRROR_UBUNTU_PATH)
157 MIRROR_UBUNTU_MERGE = get_env('MIRROR_UBUNTU_MERGE')
158 CFG_MM_UBUNTU = '{0}/ubuntu_mirror_local.yaml'.format(CFG_D)
159 FUEL_BOOTSTRAP_CLI_FILE = open('fuel_bootstrap_cli.yaml').read()
160 FUEL_BOOTSTRAP_CLI = yaml.load(FUEL_BOOTSTRAP_CLI_FILE)
161 FIXTURE_FILE = open('fuel-web/nailgun/nailgun/fixtures/openstack.yaml').read()
162 FIXTURE = yaml.load(FIXTURE_FILE)
163 OPNFV_CFG_YAML = open(CFG_OPNFV).read()
164 OPNFV_CFG = yaml.load(OPNFV_CFG_YAML)
166 # Create local partial mirror using packetary, one arch at a time
167 for arch in UBUNTU_ARCH.split(' '):
168 # Mirror / Package env vars, arch-overrideable
169 mos_ubuntu = get_env('MIRROR_MOS_UBUNTU', arch)
170 mos_ubuntu_root = get_env('MIRROR_MOS_UBUNTU_ROOT', arch)
171 mirror_ubuntu = get_env('MIRROR_UBUNTU_URL', arch)
172 plugins = get_env('BUILD_FUEL_PLUGINS', arch)
174 plugins = get_env('PLUGINS', arch)
176 # Mirror / Package list configuration files (arch-specific)
177 cfg_m_mos = '{0}/mos_{1}_mirror.yaml'.format(CFG_D, arch)
178 cfg_m_ubuntu = '{0}/ubuntu_{1}_mirror.yaml'.format(CFG_D, arch)
179 cfg_p_ubuntu = '{0}/ubuntu_{1}_packages.yaml'.format(CFG_D, arch)
180 cfg_m_ubuntu_main = '{0}/ubuntu_{1}_mirror_main.yaml'.format(CFG_D, arch)
181 cfg_p_ubuntu_main = '{0}/ubuntu_{1}_packages_main.yaml'.format(CFG_D, arch)
183 # Mirror config fork before customizing (arch-specific)
184 arch_mos = 'mos_{0}'.format(arch)
185 arch_ubuntu = 'ubuntu_{0}'.format(arch)
186 arch_packages = 'packages_{0}'.format(arch)
187 OPNFV_CFG['groups'][arch_mos] = deepcopy(OPNFV_CFG['groups']['mos'])
188 OPNFV_CFG['groups'][arch_ubuntu] = deepcopy(OPNFV_CFG['groups']['ubuntu'])
189 OPNFV_CFG[arch_packages] = OPNFV_CFG['packages']
191 # Mirror config update & conversion to packetary input
192 group_main_ubuntu = dict()
193 for group in OPNFV_CFG['groups'][arch_mos]:
194 group['uri'] = "http://{}{}".format(mos_ubuntu, mos_ubuntu_root)
195 group['suite'] = group['suite'].replace('$mos_version', MOS_VERSION)
196 for group in OPNFV_CFG['groups'][arch_ubuntu]:
197 group['uri'] = mirror_ubuntu
198 # FIXME: At `create`, packetary insists on copying all pkgs to dest dir,
199 # so configure it for another dir, which will replace the orig.
200 group['path'] = MIRROR_UBUNTU_TMP_PATH
201 if not group_main_ubuntu and 'main' in group:
202 group_main_ubuntu = [deepcopy(group)]
203 group_main_ubuntu[0]['section'] = ['main']
205 # Mirror config dump: MOS (for dep resolution), Ubuntu, Ubuntu[main]
206 write_cfg_file(cfg_m_mos, OPNFV_CFG['groups'][arch_mos])
207 write_cfg_file(cfg_m_ubuntu, OPNFV_CFG['groups'][arch_ubuntu])
208 if MIRROR_UBUNTU_MERGE is None:
209 write_cfg_file(cfg_m_ubuntu_main, group_main_ubuntu)
211 # FIXME: For multiarch, only one dump would be enough
212 group_main_ubuntu[0]['origin'] = 'Ubuntu'
213 group_main_ubuntu[0]['path'] = MIRROR_UBUNTU_PATH
214 group_main_ubuntu[0]['uri'] = MIRROR_UBUNTU_PATH
215 write_cfg_file(CFG_MM_UBUNTU, group_main_ubuntu[0])
217 # Collect package dependencies from:
218 ## 1. fuel_bootstrap_cli.yaml (bootstrap image additional packages)
219 legacy_unresolved = legacy_diff(None, FUEL_BOOTSTRAP_CLI['packages'] + [
220 FUEL_BOOTSTRAP_CLI['kernel_flavor'],
221 FUEL_BOOTSTRAP_CLI['kernel_flavor'].replace('image', 'headers')],
223 ## 2. openstack.yaml FIXTURE definition (default target image packages)
224 for release in FIXTURE:
225 editable = release['fields']['attributes_metadata']['editable']
226 if 'provision' in editable and 'packages' in editable['provision']:
227 release_pkgs = editable['provision']['packages']['value'].split()
228 legacy_unresolved += legacy_diff(legacy_unresolved, release_pkgs,
229 'Release {0}'.format(release['fields']['name']), arch)
230 ## 3. OPNFV additional packages (includes old fuel-mirror ubuntu.yaml pkgs)
232 unresolved['mandatory'] = 'exact'
233 unresolved['packages'] = from_legacy_pkglist(legacy_unresolved)
234 if 'packages' in OPNFV_CFG:
235 legacy_diff(legacy_unresolved, to_legacy_pkglist(OPNFV_CFG['packages']),
236 'OPNFV config', arch)
237 unresolved['packages'] += OPNFV_CFG['packages']
239 # OPNFV plugins dependency resolution
241 for plugin in plugins.split():
242 path = "../{}/packages.yaml".format(plugin)
243 if os.path.isfile(path):
244 f = open(path).read()
245 plugin_yaml = yaml.load(f)
246 new_pkgs = legacy_diff(
247 to_legacy_pkglist(unresolved['packages']),
248 plugin_yaml['packages'], 'Plugin {0}'.format(plugin), arch)
249 unresolved['packages'] += from_legacy_pkglist(new_pkgs)
251 # Package list (reduced, i.e. no MOS deps, but with OPNFV plugin deps)
252 if MIRROR_UBUNTU_MERGE is None:
253 write_cfg_file(cfg_p_ubuntu_main, unresolved)
255 # Mirror package list (full, including MOS/OPNFV plugin deps)
256 unresolved['packages'] += get_unres_pkgs(arch, cfg_m_mos)
257 write_cfg_file(cfg_p_ubuntu, unresolved)
258 do_partial_mirror(arch, cfg_m_ubuntu, cfg_p_ubuntu)
259 if MIRROR_UBUNTU_MERGE is None:
260 # Ubuntu[main] must be evaluated after Ubuntu
261 do_partial_mirror(arch, cfg_m_ubuntu_main, cfg_p_ubuntu_main)
263 if MIRROR_UBUNTU_MERGE is None:
264 shutil.move(MIRROR_UBUNTU_TMP_PATH, MIRROR_UBUNTU_PATH)
266 # Construct single-component mirror from all components
267 for arch in UBUNTU_ARCH.split(' '):
268 cfg_pp_ubuntu = '{0}/ubuntu_{1}_packages_paths.yaml'.format(CFG_D, arch)
270 opnfv_blacklist = to_legacy_pkglist(OPNFV_CFG['opnfv_blacklist'])
271 # FIXME: We need scanpackages to omit older DEBs
272 # Inspired from http://askubuntu.com/questions/198474/
273 os.system('dpkg-scanpackages -a {0} {1} 2>/dev/null | '
274 'grep -e "^Filename:" | sed "s|Filename: |- file://|g" | '
275 'grep -v -E "\/({2})_" > {3}'
276 .format(arch, MIRROR_UBUNTU_TMP_PATH,
277 '|'.join(opnfv_blacklist), cfg_pp_ubuntu))
278 do_local_repo(arch, CFG_MM_UBUNTU, cfg_pp_ubuntu)
279 shutil.rmtree(MIRROR_UBUNTU_TMP_PATH)