Move unitfiles/ to packaging/packages/
[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(r'>\d\.{}\.(\d)-SNAPSHOT\/'.format(version_major),
202                                    parent_dir_html))
203
204     # Dir that contains snapshot builds for the given major version
205     snapshot_dir_url = parent_dir_url + "0.{}.{}-SNAPSHOT/".format(
206         version_major,
207         version_minor)
208
209     # Get HTML of dir that contains snapshot builds for given major version
210     snapshot_dir_html = urlopen(snapshot_dir_url).read().decode('utf-8')
211
212     # Find most recent URL to tarball, ie most recent snapshot build
213     return re.findall(r'href="(.*\.tar\.gz)"', snapshot_dir_html)[-1]
214
215
216 def get_sysd_commit():
217     """Get latest Int/Pack repo commit hash"""
218
219     int_pack_repo = "https://github.com/opendaylight/integration-packaging.git"
220     # Get the commit hash at the tip of the master branch
221     args_git = ['git', 'ls-remote', int_pack_repo, "HEAD"]
222     args_awk = ['awk', '{print $1}']
223     references = subprocess.Popen(args_git, stdout=subprocess.PIPE,
224                                   shell=False)
225     sysd_commit = subprocess.check_output(args_awk, stdin=references.stdout,
226                                           shell=False).strip()
227
228     return sysd_commit
229
230
231 def get_java_version(version_major):
232     """Get the java_version dependency for ODL builds
233
234        :arg str version_major: OpenDaylight major version number
235        :return int java_version: Java version required by given ODL version
236     """
237     if int(version_major) < 5:
238         java_version = 7
239     else:
240         java_version = 8
241     return java_version
242
243
244 def get_changelog_date(pkg_type):
245     """Get the changelog datetime formatted for the given package type
246
247     :arg str pkg_type: Type of datetime formatting (rpm, deb)
248     :return str changelog_date: Date or datetime formatted for given pkg_type
249     """
250     if pkg_type == "rpm":
251         # RPMs require a date of the format "Day Month Date Year". For example:
252         # Mon Jun 21 2017
253         return datetime.date.today().strftime("%a %b %d %Y")
254     elif pkg_type == "deb":
255         # Debs require both a date and time.
256         # Date must be of the format "Day, Date Month Year". For example:
257         # Mon, 21 Jun 2017
258         date = datetime.date.today().strftime("%a, %d %b %Y")
259         # Time must be of the format "HH:MM:SS +HHMM". For example:
260         # 15:01:16 +0530
261         time = datetime.datetime.now(tzlocal.get_localzone()).\
262             strftime("%H:%M:%S %z")
263         return "{} {}".format(date, time)
264     else:
265         raise ValueError("Unknown package type: {}".format(pkg_type))
266
267
268 def get_distro_name_prefix(version_major):
269     """Return Karaf 3 or 4-style distro name prefix based on ODL major version
270
271     :arg str major_version: OpenDaylight major version umber
272     :return str distro_name_style: Karaf 3 or 4-style distro name prefix
273
274     """
275     if int(version_major) < 7:
276         # ODL versions before Nitrogen use Karaf 3, distribution-karaf- names
277         return "distribution-karaf"
278     else:
279         # ODL versions Nitrogen and after use Karaf 4, karaf- names
280         return "karaf"
281
282
283 def cache_distro(build):
284     """Cache the OpenDaylight distribution to package as RPM/Deb.
285
286     :param build: Description of an RPM build
287     :type build: dict
288     :return str distro_tar_path: Path to cached distribution tarball
289
290     """
291     # Specialize templates for the given build
292     distro = distro_template.substitute(build)
293
294     # Append file extensions to get ODL distro zip/tarball templates
295     distro_tar = distro + ".tar.gz"
296     distro_zip = distro + ".zip"
297
298     # Prepend cache dir path to get template of full path to cached zip/tarball
299     distro_tar_path = os.path.join(cache_dir, distro_tar)
300     distro_zip_path = os.path.join(cache_dir, distro_zip)
301
302     # Cache OpenDaylight tarball to be packaged
303     if not os.path.isfile(distro_tar_path):
304         if build["download_url"].endswith(".tar.gz"):
305             print("Downloading: {}".format(build["download_url"]))
306             urllib.urlretrieve(build["download_url"], distro_tar_path)
307             print("Cached: {}".format(distro_tar))
308         # If download_url points at a zip, repackage as a tarball
309         elif build["download_url"].endswith(".zip"):
310             if not os.path.isfile(distro_zip):
311                 print("URL is to a zip, will download and convert to tar.gz")
312                 print("Downloading: {}".format(build["download_url"]))
313                 urllib.urlretrieve(build["download_url"], distro_zip_path)
314                 print("Downloaded {}".format(distro_zip_path))
315             else:
316                 print("Already cached: {}".format(distro_zip_path))
317             # Extract zip archive
318             # NB: zipfile.ZipFile.extractall doesn't preserve permissions
319             # https://bugs.python.org/issue15795
320             subprocess.call(["unzip", "-oq", distro_zip_path, "-d", cache_dir])
321             # Get files in cache dir
322             cache_dir_ls_all = glob.glob(os.path.join(cache_dir, "*"))
323             # Remove pyc files that may be newer than just-extracted zip
324             cache_dir_ls = filter(lambda f: '.pyc' not in f, cache_dir_ls_all)
325             # Get the most recent file in cache dir, hopefully unzipped archive
326             unzipped_distro_path = max(cache_dir_ls, key=os.path.getctime)
327             print("Extracted: {}".format(unzipped_distro_path))
328             # Remove path from unzipped distro filename, as will cd to dir below
329             unzipped_distro = os.path.basename(unzipped_distro_path)
330             # Using the full paths here creates those paths in the tarball, which
331             # breaks the build. There's a way to change the working dir during a
332             # single tar command using the system tar binary, but I don't see a
333             # way to do that with Python.
334             # TODO: Is there a good way to do this without changing directories?
335             # TODO: Try https://goo.gl/XMx5gb
336             cwd = os.getcwd()
337             os.chdir(cache_dir)
338             with tarfile.open(distro_tar, "w:gz") as tb:
339                 tb.add(unzipped_distro)
340                 print("Taring {} into {}".format(unzipped_distro, distro_tar))
341             os.chdir(cwd)
342             print("Cached: {}".format(distro_tar))
343     else:
344         print("Already cached: {}".format(distro_tar))
345
346     return distro_tar_path
347
348
349 def cache_sysd(build):
350     """Cache the artifacts required for the given RPM build.
351
352     :param build: Description of an RPM build
353     :type build: dict
354     :return dict unitfile_path: Paths to cached unit file and unit file tarball
355
356     """
357     # Specialize templates for the given build
358     unitfile = unitfile_template.substitute(build)
359     unitfile_url = unitfile_url_template.substitute(build)
360
361     # Append file extensions to get ODL distro zip/tarball templates
362     unitfile_tar = unitfile + ".tar.gz"
363
364     # Prepend cache dir path to get template of full path to cached zip/tarball
365     unitfile_path = os.path.join(cache_dir, unitfile)
366     unitfile_tar_path = os.path.join(cache_dir, unitfile_tar)
367
368     # Cache appropriate version of ODL's systemd unit file as a tarball
369     if not os.path.isfile(unitfile_tar_path):
370         # Download ODL's systemd unit file
371         urllib.urlretrieve(unitfile_url, unitfile_path)
372
373         # Using the full paths here creates those paths in the tarball, which
374         # breaks the build. There's a way to change the working dir during a
375         # single tar command using the system tar binary, but I don't see a
376         # way to do that with Python.
377         # TODO: Is there a good way to do this without changing directories?
378         # TODO: Try https://goo.gl/XMx5gb
379         cwd = os.getcwd()
380         os.chdir(cache_dir)
381         # Create a .tar.gz archive containing ODL's systemd unitfile
382         with tarfile.open(unitfile_tar, "w:gz") as tb:
383             tb.add(unitfile)
384         os.chdir(cwd)
385
386         # Remove the now-archived unitfile
387         os.remove(unitfile_path)
388         print("Cached: {}".format(unitfile_tar))
389     else:
390         print("Already cached: {}".format(unitfile_tar))
391
392     return {"unitfile_tar_path": unitfile_tar_path,
393             "unitfile_path": unitfile_path}