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