[kvm-plugin] Update the kernel version
[fuel.git] / build / f_isoroot / f_repobuild / opnfv_mirror_ubuntu.py
1 #!/usr/bin/env python
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 ##############################################################################
11
12 """Build multiarch partial local Ubuntu mirror using packetary"""
13
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
25 #    "main".
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 ##############################################################################
45
46 from copy import deepcopy
47 import os
48 import shutil
49 import sys
50 import yaml
51 from contextlib import contextmanager
52 from cStringIO import StringIO
53 from packetary.cli.app import main
54
55 @contextmanager
56 def capture_stdout(output):
57     """Context manager for capturing stdout"""
58     stdout = sys.stdout
59     sys.stdout = output
60     yield
61     sys.stdout = stdout
62
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'):
68             try:
69                 reload(sys.modules[mod])
70             except:
71                 pass
72
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('#'):
82             continue
83         dep = dep_pkg.split(';')
84         unresolved_pkgs += [{'name': dep[0], 'version': dep[1]}]
85     force_logger_reload()
86     return unresolved_pkgs
87
88 def from_legacy_pkglist(legacy_pkglist):
89     """Package list conversion from `old fuel-mirror` to `packetary` style"""
90     pkglist = list()
91     for pkg in legacy_pkglist:
92         pkglist += [{'name': pkg}]
93     return pkglist
94
95 def to_legacy_pkglist(pkglist):
96     """Package list conversion from `packetary` style to `old fuel-mirror`"""
97     legacy_pkglist = list()
98     for pkg in pkglist:
99         legacy_pkglist.append(pkg['name'])
100     return legacy_pkglist
101
102 def legacy_diff(base_pkglist, new_pkglist, requester, architecture):
103     """Package list diff (old format)"""
104     diff_set = set(new_pkglist)
105     if base_pkglist:
106         diff_set -= set(base_pkglist)
107     if diff_set:
108         print(' * {0} requires new packages for architecture [{1}]: {2}'
109               .format(requester, architecture, ', '.join(diff_set)))
110     return list(diff_set)
111
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()
119
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()
127
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))
132
133 def get_env(env_var, architecture=None):
134     """Evaluate architecture-specific overrides of env vars"""
135     if architecture:
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]
141     return None
142
143 # Architecture name mapping (dpkg:packetary) for packetary CLI invocation
144 _ARCH = {
145     "i386": "i386",
146     "amd64": "x86_64",
147     "arm64": "aarch64",
148 }
149
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)
165
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)
173     if plugins is None:
174         plugins = get_env('PLUGINS', arch)
175
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)
182
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']
190
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']
204
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)
210     else:
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])
216
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')],
222         'Bootstrap', arch)
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)
231     unresolved = dict()
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']
238
239     # OPNFV plugins dependency resolution
240     if plugins:
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)
250
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)
254
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)
262
263 if MIRROR_UBUNTU_MERGE is None:
264     shutil.move(MIRROR_UBUNTU_TMP_PATH, MIRROR_UBUNTU_PATH)
265 else:
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)
269         # OPNFV blacklist
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)