dccda7cfa928193dfbf63a823d8446b0800058dd
[integration/packaging.git] / packages / lib.py
1 #!/usr/bin/env python
2
3 ##############################################################################
4 # Copyright (c) 2016 Daniel Farrell and Others.  All rights reserved.
5 #
6 # This program and the accompanying materials are made available under the
7 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
8 # and is available at http://www.eclipse.org/legal/epl-v10.html
9 ##############################################################################
10
11 import datetime
12 import glob
13 import os
14 import re
15 from string import Template
16 import subprocess
17 import sys
18 import tarfile
19 import urllib
20 from urllib2 import urlopen
21
22 try:
23     from bs4 import BeautifulSoup
24     import requests
25     from requests.exceptions import HTTPError
26     import tzlocal
27 except ImportError:
28     sys.stderr.write("We recommend using our included Vagrant env.\n")
29     sys.stderr.write("Else, do `pip install -r requirements.txt` in a venv.\n")
30     raise
31
32 # Path to directory for cache artifacts
33 cache_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache")
34
35 # Templates that can be specialized into common artifact names per-build
36 # NB: Templates can't be concatenated with other Templates or strings, or
37 # cast to strings for concatenation. If they could, we would do elegant
38 # refactoring like concatenating paths to templates here and only calling
39 # Template.substitute in the build_rpm function.
40 distro_template = Template("opendaylight-$version_major.$version_minor."
41                            "$version_patch-$pkg_version")
42 unitfile_template = Template("opendaylight-$sysd_commit.service")
43 unitfile_url_template = Template("https://git.opendaylight.org/gerrit/"
44                                  "gitweb?p=integration/packaging.git;a="
45                                  "blob_plain;f=packages/unitfiles/"
46                                  "opendaylight.service;hb=$sysd_commit")
47
48
49 def extract_version(url):
50     """Determine ODL version information from the ODL tarball build URL
51
52     :arg str url: URL of the ODL tarball build for building RPMs
53
54     """
55     # Version components will be added below. Patch version is always 0.
56     version = {"version_patch": "0"}
57
58     # Parse URL to find major and minor versions. Eg:
59     # https://nexus.opendaylight.org/content/repositories/public/org/
60     #  opendaylight/integration/distribution-karaf/0.3.4-Lithium-SR4/
61     #  distribution-karaf-0.3.4-Lithium-SR4.tar.gz
62     # major_version = 3
63     # minor_version = 4
64     re_out = re.search(r'\d\.(\d+)\.(\d)', url)
65     version["version_major"] = re_out.group(1)
66     version["version_minor"] = re_out.group(2)
67
68     # Add version components that need to be extracted based on type of build
69     if "autorelease" in url:
70         version = extract_autorelease_version(url, version)
71     elif "snapshot" in url:
72         version = extract_snapshot_version(url, version)
73     elif "public" or "opendaylight.release" in url:
74         version = extract_release_version(url, version)
75     else:
76         raise ValueError("Unrecognized URL {}".format(url))
77
78     return version
79
80
81 def extract_release_version(url, version):
82     """Extract package version components from release build URL
83
84     :arg str url: URL to release tarball
85     :arg dict version: Package version components for given distro
86     :return dict version: Version components, with additions
87     """
88     # If this version of ODL has a codename, parse it from URL. Eg:
89     # https://nexus.opendaylight.org/content/repositories/public/org/
90     #  opendaylight/integration/distribution-karaf/0.3.4-Lithium-SR4/
91     #  distribution-karaf-0.3.4-Lithium-SR4.tar.gz
92     # codename = Lithium-SR4
93     if int(version["version_major"]) < 7:
94         # ODL versions before Nitrogen use Karaf 3, have a codename
95         # Include "-" in codename to avoid hanging "-" when no codename
96         version["codename"] = "-" + re.search(r'0\.[0-9]+\.[0-9]+-(.*)\/',
97                                               url).group(1)
98     else:
99         # ODL versions Nitrogen and after use Karaf 4, don't have a codename
100         version["codename"] = ""
101
102     # Package version is assumed to be 1 for release builds
103     # TODO: Should be able to manually set this in case this is a rebuild
104     version["pkg_version"] = "1"
105
106     return version
107
108
109 def extract_autorelease_version(url, version):
110     """Extract package version components from an autorelease build URL
111
112     :arg str url: URL to autorelease tarball
113     :arg dict version: Package version components for given distro
114     :return dict version: Version components, with additions
115     """
116     # If this version of ODL has a codename, parse it from URL. Eg:
117     # https://nexus.opendaylight.org/content/repositories/autorelease-1533/
118     #     org/opendaylight/integration/distribution-karaf/0.4.4-Beryllium-SR4/
119     # codename = Beryllium-SR4
120     if int(version["version_major"]) < 7:
121         # ODL versions before Nitrogen use Karaf 3, have a codename
122         # Include "-" in codename to avoid hanging "-" when no codename
123         version["codename"] = "-" + re.search(r'0\.[0-9]+\.[0-9]+-(.*)\/',
124                                               url).group(1)
125     else:
126         # ODL versions Nitrogen and after use Karaf 4, don't have a codename
127         version["codename"] = ""
128
129     # Autorelease URLs don't include a date, parse HTML to find build date
130     # Strip distro zip/tarball archive part of URL, resulting in base URL
131     base_url = url.rpartition("/")[0]+url.rpartition("/")[1]
132     # Using bash subprocess to parse HTML and find date of build
133     # TODO: Do all of this with Python, don't spawn a bash process
134     # Set base_url as an environment var to pass it to subprocess
135     os.environ["base_url"] = base_url
136     raw_date = subprocess.Popen(
137         "curl -s $base_url | grep tar.gz -A1 | tail -n1 |"
138         "sed \"s/<td>//g\" | sed \"s/\\n//g\" | awk '{print $3,$2,$6}' ",
139         shell=True, stdout=subprocess.PIPE,
140         stdin=subprocess.PIPE).stdout.read().rstrip().strip("</td>")
141     build_date = datetime.datetime.strptime(raw_date, "%d %b %Y").strftime(
142                                             '%Y%m%d')
143
144     # Parse URL to find unique build ID. Eg:
145     # https://nexus.opendaylight.org/content/repositories/autorelease-1533/
146     #     org/opendaylight/integration/distribution-karaf/0.4.4-Beryllium-SR4/
147     # build_id = 1533
148     build_id = re.search(r'\/autorelease-([0-9]+)\/', url).group(1)
149
150     # Combine build date and build ID into pkg_version
151     version["pkg_version"] = "0.1." + build_date + "rel" + build_id
152
153     return version
154
155
156 def extract_snapshot_version(url, version):
157     """Extract package version components from a snapshot build URL
158
159     :arg str url: URL to snapshot tarball
160     :arg dict version: Package version components for given distro
161     :return dict version: Version components, with additions
162     """
163
164     # All snapshot builds use SNAPSHOT codename
165     # Include "-" in codename to avoid hanging "-" when no codename
166     version["codename"] = "-SNAPSHOT"
167
168     # Parse URL to find build date and build ID. Eg:
169     # https://nexus.opendaylight.org/content/repositories/
170     #     opendaylight.snapshot/org/opendaylight/integration/
171     #     distribution-karaf/0.6.0-SNAPSHOT/
172     #     distribution-karaf-0.6.0-20161201.031047-2242.tar.gz
173     # build_date = 20161201
174     # build_id = 2242
175     re_out = re.search(r'0.[0-9]+\.[0-9]+-([0-9]+)\.[0-9]+-([0-9]+)\.', url)
176     build_date = re_out.group(1)
177     build_id = re_out.group(2)
178
179     # Combine build date and build ID into pkg_version
180     version["pkg_version"] = "0.1." + build_date + "snap" + build_id
181
182     return version
183
184
185 def get_snap_url(version_major):
186     """Get the most recent snapshot build of the given ODL major version
187
188     :arg str version_major: ODL major version to get latest snapshot of
189     :return str snapshot_url: URL to latest snapshot tarball of ODL version
190
191     """
192     # Dir that contains all shapshot build dirs, varies based on Karaf 3/4
193     parent_dir_url = "https://nexus.opendaylight.org/content/repositories/" \
194                      "opendaylight.snapshot/org/opendaylight/integration/{}/" \
195                      .format(get_distro_name_prefix(version_major))
196
197     # Get HTML of dir that contains all shapshot dirs
198     parent_dir_html = urlopen(parent_dir_url).read().decode('utf-8')
199
200     # Get most recent minor version of the given major version
201     version_minor = max(re.findall(
202                         r'>\d\.{}\.(\d)-SNAPSHOT\/'.format(version_major),
203                         parent_dir_html))
204
205     # Dir that contains snapshot builds for the given major version
206     snapshot_dir_url = parent_dir_url + "0.{}.{}-SNAPSHOT/".format(
207         version_major,
208         version_minor)
209
210     # Get HTML of dir that contains snapshot builds for given major version
211     snapshot_dir_html = urlopen(snapshot_dir_url).read().decode('utf-8')
212
213     # Find most recent URL to tarball, ie most recent snapshot build
214     return re.findall(r'href="(.*\.tar\.gz)"', snapshot_dir_html)[-1]
215
216
217 def get_sysd_commit():
218     """Get latest Int/Pack repo commit hash"""
219
220     int_pack_repo = "https://github.com/opendaylight/integration-packaging.git"
221     # Get the commit hash at the tip of the master branch
222     args_git = ['git', 'ls-remote', int_pack_repo, "HEAD"]
223     args_awk = ['awk', '{print $1}']
224     references = subprocess.Popen(args_git, stdout=subprocess.PIPE,
225                                   shell=False)
226     sysd_commit = subprocess.check_output(args_awk, stdin=references.stdout,
227                                           shell=False).strip()
228
229     return sysd_commit
230
231
232 def get_java_version(version_major):
233     """Get the java_version dependency for ODL builds
234
235        :arg str version_major: OpenDaylight major version number
236        :return int java_version: Java version required by given ODL version
237     """
238     if int(version_major) < 5:
239         java_version = 7
240     else:
241         java_version = 8
242     return java_version
243
244
245 def get_changelog_date(pkg_type):
246     """Get the changelog datetime formatted for the given package type
247
248     :arg str pkg_type: Type of datetime formatting (rpm, deb)
249     :return str changelog_date: Date or datetime formatted for given pkg_type
250     """
251     if pkg_type == "rpm":
252         # RPMs require a date of the format "Day Month Date Year". For example:
253         # Mon Jun 21 2017
254         return datetime.date.today().strftime("%a %b %d %Y")
255     elif pkg_type == "deb":
256         # Debs require both a date and time.
257         # Date must be of the format "Day, Date Month Year". For example:
258         # Mon, 21 Jun 2017
259         date = datetime.date.today().strftime("%a, %d %b %Y")
260         # Time must be of the format "HH:MM:SS +HHMM". For example:
261         # 15:01:16 +0530
262         time = datetime.datetime.now(tzlocal.get_localzone()).\
263             strftime("%H:%M:%S %z")
264         return "{} {}".format(date, time)
265     else:
266         raise ValueError("Unknown package type: {}".format(pkg_type))
267
268
269 def get_distro_name_prefix(version_major):
270     """Return Karaf 3 or 4-style distro name prefix based on ODL major version
271
272     :arg str major_version: OpenDaylight major version umber
273     :return str distro_name_style: Karaf 3 or 4-style distro name prefix
274
275     """
276     if int(version_major) < 7:
277         # ODL versions before Nitrogen use Karaf 3, distribution-karaf- names
278         return "distribution-karaf"
279     else:
280         # ODL versions Nitrogen and after use Karaf 4, karaf- names
281         return "karaf"
282
283
284 def cache_distro(build):
285     """Cache the OpenDaylight distribution to package as RPM/Deb.
286
287     :param build: Description of an RPM build
288     :type build: dict
289     :return str distro_tar_path: Path to cached distribution tarball
290
291     """
292     # Specialize templates for the given build
293     distro = distro_template.substitute(build)
294
295     # Append file extensions to get ODL distro zip/tarball templates
296     distro_tar = distro + ".tar.gz"
297     distro_zip = distro + ".zip"
298
299     # Prepend cache dir path to get template of full path to cached zip/tarball
300     distro_tar_path = os.path.join(cache_dir, distro_tar)
301     distro_zip_path = os.path.join(cache_dir, distro_zip)
302
303     # Cache OpenDaylight tarball to be packaged
304     if not os.path.isfile(distro_tar_path):
305         if build["download_url"].endswith(".tar.gz"):
306             print("Downloading: {}".format(build["download_url"]))
307             urllib.urlretrieve(build["download_url"], distro_tar_path)
308             print("Cached: {}".format(distro_tar))
309         # If download_url points at a zip, repackage as a tarball
310         elif build["download_url"].endswith(".zip"):
311             if not os.path.isfile(distro_zip):
312                 print("URL is to a zip, will download and convert to tar.gz")
313                 print("Downloading: {}".format(build["download_url"]))
314                 urllib.urlretrieve(build["download_url"], distro_zip_path)
315                 print("Downloaded {}".format(distro_zip_path))
316             else:
317                 print("Already cached: {}".format(distro_zip_path))
318             # Extract zip archive
319             # NB: zipfile.ZipFile.extractall doesn't preserve permissions
320             # https://bugs.python.org/issue15795
321             subprocess.call(["unzip", "-oq", distro_zip_path, "-d", cache_dir])
322             # Get files in cache dir
323             cache_dir_ls_all = glob.glob(os.path.join(cache_dir, "*"))
324             # Remove pyc files that may be newer than just-extracted zip
325             cache_dir_ls = filter(lambda f: '.pyc' not in f, cache_dir_ls_all)
326             # Get the most recent file in cache dir, hopefully unzipped archive
327             unzipped_distro_path = max(cache_dir_ls, key=os.path.getctime)
328             print("Extracted: {}".format(unzipped_distro_path))
329             # Remove path from 'unzipped_distro_path', as will cd to dir below
330             unzipped_distro = os.path.basename(unzipped_distro_path)
331             # Using the full paths here creates those paths in the tarball,
332             # which breaks the build. There's a way to change the working dir
333             # during a single tar command using the system tar binary, but I
334             # don't see a way to do that with Python.
335             # TODO: Can this be done without changing directories?
336             # TODO: Try https://goo.gl/XMx5gb
337             cwd = os.getcwd()
338             os.chdir(cache_dir)
339             with tarfile.open(distro_tar, "w:gz") as tb:
340                 tb.add(unzipped_distro)
341                 print("Taring {} into {}".format(unzipped_distro, distro_tar))
342             os.chdir(cwd)
343             print("Cached: {}".format(distro_tar))
344     else:
345         print("Already cached: {}".format(distro_tar))
346
347     return distro_tar_path
348
349
350 def cache_sysd(build):
351     """Cache the artifacts required for the given RPM build.
352
353     :param build: Description of an RPM build
354     :type build: dict
355     :return dict unitfile_path: Paths to cached unit file and unit file tarball
356
357     """
358     # Specialize templates for the given build
359     unitfile = unitfile_template.substitute(build)
360     unitfile_url = unitfile_url_template.substitute(build)
361
362     # Append file extensions to get ODL distro zip/tarball templates
363     unitfile_tar = unitfile + ".tar.gz"
364
365     # Prepend cache dir path to get template of full path to cached zip/tarball
366     unitfile_path = os.path.join(cache_dir, unitfile)
367     unitfile_tar_path = os.path.join(cache_dir, unitfile_tar)
368
369     # Download ODL's systemd unit file
370     if not os.path.isfile(unitfile_path):
371         urllib.urlretrieve(unitfile_url, unitfile_path)
372         print("Cached: {}".format(unitfile))
373     else:
374         print("Already cached: {}".format(unitfile_path))
375
376     # Cache ODL's systemd unit file as a tarball
377     if not os.path.isfile(unitfile_tar_path):
378         # Using the full paths here creates those paths in the tarball, which
379         # breaks the build. There's a way to change the working dir during a
380         # single tar command using the system tar binary, but I don't see a
381         # way to do that with Python.
382         # TODO: Is there a good way to do this without changing directories?
383         # TODO: Try https://goo.gl/XMx5gb
384         cwd = os.getcwd()
385         os.chdir(cache_dir)
386         # Create a .tar.gz archive containing ODL's systemd unitfile
387         with tarfile.open(unitfile_tar, "w:gz") as tb:
388             tb.add(unitfile)
389         os.chdir(cwd)
390
391         print("Cached: {}".format(unitfile_tar))
392     else:
393         print("Already cached: {}".format(unitfile_tar_path))
394
395     return {"unitfile_tar_path": unitfile_tar_path,
396             "unitfile_path": unitfile_path}