1#!/usr/bin/env python
2
3r"""
4This module provides utilities for code updates.
5"""
6
7import os
8import re
9import sys
10import tarfile
11import time
12
13robot_pgm_dir_path = os.path.dirname(__file__) + os.sep
14repo_data_path = re.sub('/lib', '/data', robot_pgm_dir_path)
15sys.path.append(repo_data_path)
16
17import bmc_ssh_utils as bsu
18import gen_robot_keyword as keyword
19import gen_print as gp
20import variables as var
21from robot.libraries.BuiltIn import BuiltIn
22
23
24def verify_no_duplicate_image_priorities(image_purpose):
25    r"""
26    Check that there are no active images with the same purpose and priority.
27
28    Description of argument(s):
29    image_purpose                   The purpose that images must have to be
30                                    checked for priority duplicates.
31    """
32
33    taken_priorities = {}
34    _, image_names = keyword.run_key("Get Software Objects  "
35                                     + "version_type=" + image_purpose)
36
37    for image_name in image_names:
38        _, image = keyword.run_key("Get Host Software Property  " + image_name)
39        if image["Activation"] != var.ACTIVE:
40            continue
41        image_priority = image["Priority"]
42        if image_priority in taken_priorities:
43            BuiltIn().fail("Found active images with the same priority.\n"
44                           + gp.sprint_vars(image,
45                                            taken_priorities[image_priority]))
46        taken_priorities[image_priority] = image
47
48
49def get_non_running_bmc_software_object():
50    r"""
51    Get the URI to a BMC image from software that is not running on the BMC.
52    """
53
54    # Get the version of the image currently running on the BMC.
55    _, cur_img_version = keyword.run_key("Get BMC Version")
56    # Remove the surrounding double quotes from the version.
57    cur_img_version = cur_img_version.replace('"', '')
58
59    _, images = keyword.run_key("Read Properties  "
60                                + var.SOFTWARE_VERSION_URI + "enumerate")
61
62    for image_name in images:
63        _, image_properties = keyword.run_key(
64            "Get Host Software Property  " + image_name)
65        if 'Purpose' in image_properties and 'Version' in image_properties \
66                and image_properties['Purpose'] != var.VERSION_PURPOSE_HOST \
67                and image_properties['Version'] != cur_img_version:
68            return image_name
69    BuiltIn().fail("Did not find any non-running BMC images.")
70
71
72def delete_all_pnor_images():
73    r"""
74    Delete all PNOR images from the BMC.
75    """
76
77    keyword.run_key("Initiate Host PowerOff")
78
79    status, images = keyword.run_key("Get Software Objects  "
80                                     + var.VERSION_PURPOSE_HOST)
81    for image_name in images:
82        keyword.run_key("Delete Image And Verify  " + image_name + "  "
83                        + var.VERSION_PURPOSE_HOST)
84
85
86def wait_for_activation_state_change(version_id, initial_state):
87    r"""
88    Wait for the current activation state of ${version_id} to
89    change from the state provided by the calling function.
90
91    Description of argument(s):
92    version_id                      The version ID whose state change we are
93                                    waiting for.
94    initial_state                   The activation state we want to wait for.
95    """
96
97    keyword.run_key_u("Open Connection And Log In")
98    retry = 0
99    num_read_errors = 0
100    read_fail_threshold = 1
101    while (retry < 60):
102        # TODO: Use retry option in run_key when available.
103        status, software_state = keyword.run_key("Read Properties  "
104                                                 + var.SOFTWARE_VERSION_URI
105                                                 + str(version_id),
106                                                 ignore=1)
107        if status == 'FAIL':
108            num_read_errors += 1
109            if num_read_errors > read_fail_threshold:
110                message = "Read errors exceeds threshold:\n " \
111                    + gp.sprint_vars(num_read_errors, read_fail_threshold)
112                BuiltIn().fail(message)
113            time.sleep(10)
114            continue
115
116        current_state = (software_state)["Activation"]
117        if (initial_state == current_state):
118            time.sleep(10)
119            retry += 1
120            num_read_errors = 0
121        else:
122            return
123    return
124
125
126def get_latest_file(dir_path):
127    r"""
128    Get the path to the latest uploaded file.
129
130    Description of argument(s):
131    dir_path                        Path to the dir from which the name of the
132                                    last updated file or folder will be
133                                    returned to the calling function.
134    """
135
136    stdout, stderr, rc = \
137        bsu.bmc_execute_command("cd " + dir_path
138                                + "; stat -c '%Y %n' * |"
139                                + " sort -k1,1nr | head -n 1")
140    return stdout.split(" ")[-1]
141
142
143def get_version_tar(tar_file_path):
144    r"""
145    Read the image version from the MANIFEST inside the tarball.
146
147    Description of argument(s):
148    tar_file_path                   The path to a tar file that holds the image
149                                    version inside the MANIFEST.
150    """
151
152    tar = tarfile.open(tar_file_path)
153    for member in tar.getmembers():
154        f = tar.extractfile(member)
155        content = f.read()
156        if content.find(b"version=") == -1:
157            # This tar member does not contain the version.
158            continue
159        content = content.decode("utf-8").split("\n")
160        content = [x for x in content if "version=" in x]
161        version = content[0].split("=")[-1]
162        break
163    tar.close()
164    return version
165
166
167def get_image_version(file_path):
168    r"""
169    Read the file for a version object.
170
171    Description of argument(s):
172    file_path                       The path to a file that holds the image
173                                    version.
174    """
175
176    stdout, stderr, rc = \
177        bsu.bmc_execute_command("cat " + file_path
178                                + " | grep \"version=\"", ignore_err=1)
179    return (stdout.split("\n")[0]).split("=")[-1]
180
181
182def get_image_purpose(file_path):
183    r"""
184    Read the file for a purpose object.
185
186    Description of argument(s):
187    file_path                       The path to a file that holds the image
188                                    purpose.
189    """
190
191    stdout, stderr, rc = \
192        bsu.bmc_execute_command("cat " + file_path
193                                + " | grep \"purpose=\"", ignore_err=1)
194    return stdout.split("=")[-1]
195
196
197def get_image_path(image_version):
198    r"""
199    Query the upload image dir for the presence of image matching
200    the version that was read from the MANIFEST before uploading
201    the image. Based on the purpose verify the activation object
202    exists and is either READY or INVALID.
203
204    Description of argument(s):
205    image_version                   The version of the image that should match
206                                    one of the images in the upload dir.
207    """
208
209    stdout, stderr, rc = \
210        bsu.bmc_execute_command("ls -d " + var.IMAGE_UPLOAD_DIR_PATH + "*/")
211
212    image_list = stdout.split("\n")
213    retry = 0
214    while (retry < 10):
215        for i in range(0, len(image_list)):
216            version = get_image_version(image_list[i] + "MANIFEST")
217            if (version == image_version):
218                return image_list[i]
219        time.sleep(10)
220        retry += 1
221
222
223def verify_image_upload(image_version,
224                        timeout=3):
225    r"""
226    Verify the image was uploaded correctly and that it created
227    a valid d-bus object. If the first check for the image
228    fails, try again until we reach the timeout.
229
230    Description of argument(s):
231    image_version                   The version from the image's manifest file
232                                    (e.g. "v2.2-253-g00050f1").
233    timeout                         How long, in minutes, to keep trying to
234                                    find the image on the BMC. Default is 3 minutes.
235    """
236
237    image_path = get_image_path(image_version)
238    image_version_id = image_path.split("/")[-2]
239
240    keyword.run_key_u("Open Connection And Log In")
241    image_purpose = get_image_purpose(image_path + "MANIFEST")
242    if (image_purpose == var.VERSION_PURPOSE_BMC
243            or image_purpose == var.VERSION_PURPOSE_HOST):
244        uri = var.SOFTWARE_VERSION_URI + image_version_id
245        ret_values = ""
246        for itr in range(timeout * 2):
247            status, ret_values = \
248                keyword.run_key("Read Attribute  " + uri + "  Activation")
249
250            if ((ret_values == var.READY) or (ret_values == var.INVALID)
251                    or (ret_values == var.ACTIVE)):
252                return True, image_version_id
253            else:
254                time.sleep(30)
255
256        # If we exit the for loop, the timeout has been reached
257        gp.print_var(ret_values)
258        return False, None
259    else:
260        gp.print_var(image_purpose)
261        return False, None
262
263
264def verify_image_not_in_bmc_uploads_dir(image_version, timeout=3):
265    r"""
266    Check that an image with the given version is not unpacked inside of the
267    BMCs image uploads directory. If no image is found, retry every 30 seconds
268    until the given timeout is hit, in case the BMC takes time
269    unpacking the image.
270
271    Description of argument(s):
272    image_version                   The version of the image to look for on
273                                    the BMC.
274    timeout                         How long, in minutes, to try to find an
275                                    image on the BMC. Default is 3 minutes.
276    """
277
278    for i in range(timeout * 2):
279        stdout, stderr, rc = \
280            bsu.bmc_execute_command('ls ' + var.IMAGE_UPLOAD_DIR_PATH
281                                    + '*/MANIFEST 2>/dev/null '
282                                    + '| xargs grep -rl "version='
283                                    + image_version + '"')
284        image_dir = os.path.dirname(stdout.split('\n')[0])
285        if '' != image_dir:
286            bsu.bmc_execute_command('rm -rf ' + image_dir)
287            BuiltIn().fail('Found invalid BMC Image: ' + image_dir)
288        time.sleep(30)
289