0328a3b5cc494d8f6b94083c52c9187df232ebec
[apex.git] / apex / common / utils.py
1 ##############################################################################
2 # Copyright (c) 2016 Tim Rozet (trozet@redhat.com) and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10 import datetime
11 import distro
12 import json
13 import logging
14 import os
15 import pprint
16 import subprocess
17 import tarfile
18 import time
19 import urllib.error
20 import urllib.request
21 import urllib.parse
22 import yaml
23
24
25 def str2bool(var):
26     if isinstance(var, bool):
27         return var
28     else:
29         return var.lower() in ("true", "yes")
30
31
32 def parse_yaml(yaml_file):
33     with open(yaml_file) as f:
34         parsed_dict = yaml.safe_load(f)
35         return parsed_dict
36
37
38 def dump_yaml(data, file):
39     """
40     Dumps data to a file as yaml
41     :param data: yaml to be written to file
42     :param file: filename to write to
43     :return:
44     """
45     logging.debug("Writing file {} with "
46                   "yaml data:\n{}".format(file, yaml.safe_dump(data)))
47     with open(file, "w") as fh:
48         yaml.safe_dump(data, fh, default_flow_style=False)
49
50
51 def dict_objects_to_str(dictionary):
52         if isinstance(dictionary, list):
53             tmp_list = []
54             for element in dictionary:
55                 if isinstance(element, dict):
56                     tmp_list.append(dict_objects_to_str(element))
57                 else:
58                     tmp_list.append(str(element))
59             return tmp_list
60         elif not isinstance(dictionary, dict):
61             if not isinstance(dictionary, bool):
62                 return str(dictionary)
63             else:
64                 return dictionary
65         return dict((k, dict_objects_to_str(v)) for
66                     k, v in dictionary.items())
67
68
69 def run_ansible(ansible_vars, playbook, host='localhost', user='root',
70                 tmp_dir=None, dry_run=False):
71     """
72     Executes ansible playbook and checks for errors
73     :param ansible_vars: dictionary of variables to inject into ansible run
74     :param playbook: playbook to execute
75     :param tmp_dir: temp directory to store ansible command
76     :param dry_run: Do not actually apply changes
77     :return: None
78     """
79     logging.info("Executing ansible playbook: {}".format(playbook))
80     inv_host = "{},".format(host)
81     if host == 'localhost':
82         conn_type = 'local'
83     else:
84         conn_type = 'smart'
85     ansible_command = ['ansible-playbook', '--become', '-i', inv_host,
86                        '-u', user, '-c', conn_type, '-T', '30',
87                        playbook, '-vv']
88     if dry_run:
89         ansible_command.append('--check')
90
91     if isinstance(ansible_vars, dict) and ansible_vars:
92         logging.debug("Ansible variables to be set:\n{}".format(
93             pprint.pformat(ansible_vars)))
94         ansible_command.append('--extra-vars')
95         ansible_command.append(json.dumps(ansible_vars))
96         if tmp_dir:
97             ansible_tmp = os.path.join(tmp_dir,
98                                        os.path.basename(playbook) + '.rerun')
99             # FIXME(trozet): extra vars are printed without single quotes
100             # so a dev has to add them manually to the command to rerun
101             # the playbook.  Need to test if we can just add the single quotes
102             # to the json dumps to the ansible command and see if that works
103             with open(ansible_tmp, 'w') as fh:
104                 fh.write("ANSIBLE_HOST_KEY_CHECKING=FALSE {}".format(
105                     ' '.join(ansible_command)))
106
107     my_env = os.environ.copy()
108     my_env['ANSIBLE_HOST_KEY_CHECKING'] = 'False'
109     logging.info("Executing playbook...this may take some time")
110     p = subprocess.Popen(ansible_command,
111                          stdin=subprocess.PIPE,
112                          stdout=subprocess.PIPE,
113                          bufsize=1,
114                          env=my_env,
115                          universal_newlines=True)
116     # read first line
117     x = p.stdout.readline()
118     # initialize task
119     task = ''
120     while x:
121         # append lines to task
122         task += x
123         # log the line and read another
124         x = p.stdout.readline()
125         # deliver the task to info when we get a blank line
126         if not x.strip():
127             task += x
128             logging.info(task.replace('\\n', '\n'))
129             task = ''
130             x = p.stdout.readline()
131     # clean up and get return code
132     p.stdout.close()
133     rc = p.wait()
134     if rc:
135         # raise errors
136         e = "Ansible playbook failed. See Ansible logs for details."
137         logging.error(e)
138         raise Exception(e)
139
140
141 def fetch_upstream_and_unpack(dest, url, targets):
142     """
143     Fetches targets from a url destination and downloads them if they are
144     newer.  Also unpacks tar files in dest dir.
145     :param dest: Directory to download and unpack files to
146     :param url: URL where target files are located
147     :param targets: List of target files to download
148     :return: None
149     """
150     os.makedirs(dest, exist_ok=True)
151     assert isinstance(targets, list)
152     for target in targets:
153         download_target = True
154         target_url = urllib.parse.urljoin(url, target)
155         target_dest = os.path.join(dest, target)
156         logging.debug("Fetching and comparing upstream target: \n{}".format(
157             target_url))
158         try:
159             u = urllib.request.urlopen(target_url)
160         except urllib.error.URLError as e:
161             logging.error("Failed to fetch target url. Error: {}".format(
162                 e.reason))
163             raise
164         if os.path.isfile(target_dest):
165             logging.debug("Previous file found: {}".format(target_dest))
166             metadata = u.info()
167             headers = metadata.items()
168             target_url_date = None
169             for header in headers:
170                 if isinstance(header, tuple) and len(header) == 2:
171                     if header[0] == 'Last-Modified':
172                         target_url_date = header[1]
173                         break
174             if target_url_date is not None:
175                 target_dest_mtime = os.path.getmtime(target_dest)
176                 target_url_mtime = time.mktime(
177                     datetime.datetime.strptime(target_url_date,
178                                                "%a, %d %b %Y %X "
179                                                "GMT").timetuple())
180                 if target_url_mtime > target_dest_mtime:
181                     logging.debug('URL target is newer than disk...will '
182                                   'download')
183                 else:
184                     logging.info('URL target does not need to be downloaded')
185                     download_target = False
186             else:
187                 logging.debug('Unable to find last modified url date')
188         if download_target:
189             urllib.request.urlretrieve(target_url, filename=target_dest)
190             logging.info("Target downloaded: {}".format(target))
191         if target.endswith('.tar'):
192             logging.info('Unpacking tar file')
193             tar = tarfile.open(target_dest)
194             tar.extractall(path=dest)
195             tar.close()
196
197
198 def install_ansible():
199     # we only install for CentOS/Fedora for now
200     dist = distro.id()
201     if 'centos' in dist:
202         pkg_mgr = 'yum'
203     elif 'fedora' in dist:
204         pkg_mgr = 'dnf'
205     else:
206         return
207
208     # yum python module only exists for 2.x, so use subprocess
209     try:
210         subprocess.check_call([pkg_mgr, '-y', 'install', 'ansible'])
211     except subprocess.CalledProcessError:
212         logging.warning('Unable to install Ansible')