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    version = ""
153    tar = tarfile.open(tar_file_path)
154    for member in tar.getmembers():
155        f = tar.extractfile(member)
156        content = f.read()
157        if content.find(b"version=") == -1:
158            # This tar member does not contain the version.
159            continue
160        content = content.decode("utf-8").split("\n")
161        content = [x for x in content if "version=" in x]
162        version = content[0].split("=")[-1]
163        break
164    tar.close()
165    return version
166
167
168def get_image_version(file_path):
169    r"""
170    Read the file for a version object.
171
172    Description of argument(s):
173    file_path                       The path to a file that holds the image
174                                    version.
175    """
176
177    stdout, stderr, rc = \
178        bsu.bmc_execute_command("cat " + file_path
179                                + " | grep \"version=\"", ignore_err=1)
180    return (stdout.split("\n")[0]).split("=")[-1]
181
182
183def get_image_purpose(file_path):
184    r"""
185    Read the file for a purpose object.
186
187    Description of argument(s):
188    file_path                       The path to a file that holds the image
189                                    purpose.
190    """
191
192    stdout, stderr, rc = \
193        bsu.bmc_execute_command("cat " + file_path
194                                + " | grep \"purpose=\"", ignore_err=1)
195    return stdout.split("=")[-1]
196
197
198def get_image_path(image_version):
199    r"""
200    Query the upload image dir for the presence of image matching
201    the version that was read from the MANIFEST before uploading
202    the image. Based on the purpose verify the activation object
203    exists and is either READY or INVALID.
204
205    Description of argument(s):
206    image_version                   The version of the image that should match
207                                    one of the images in the upload dir.
208    """
209
210    stdout, stderr, rc = \
211        bsu.bmc_execute_command("ls -d " + var.IMAGE_UPLOAD_DIR_PATH + "*/")
212
213    image_list = stdout.split("\n")
214    retry = 0
215    while (retry < 10):
216        for i in range(0, len(image_list)):
217            version = get_image_version(image_list[i] + "MANIFEST")
218            if (version == image_version):
219                return image_list[i]
220        time.sleep(10)
221        retry += 1
222
223
224def verify_image_upload(image_version,
225                        timeout=3):
226    r"""
227    Verify the image was uploaded correctly and that it created
228    a valid d-bus object. If the first check for the image
229    fails, try again until we reach the timeout.
230
231    Description of argument(s):
232    image_version                   The version from the image's manifest file
233                                    (e.g. "v2.2-253-g00050f1").
234    timeout                         How long, in minutes, to keep trying to
235                                    find the image on the BMC. Default is 3 minutes.
236    """
237
238    image_path = get_image_path(image_version)
239    image_version_id = image_path.split("/")[-2]
240
241    keyword.run_key_u("Open Connection And Log In")
242    image_purpose = get_image_purpose(image_path + "MANIFEST")
243    if (image_purpose == var.VERSION_PURPOSE_BMC
244            or image_purpose == var.VERSION_PURPOSE_HOST):
245        uri = var.SOFTWARE_VERSION_URI + image_version_id
246        ret_values = ""
247        for itr in range(timeout * 2):
248            status, ret_values = \
249                keyword.run_key("Read Attribute  " + uri + "  Activation")
250
251            if ((ret_values == var.READY) or (ret_values == var.INVALID)
252                    or (ret_values == var.ACTIVE)):
253                return True, image_version_id
254            else:
255                time.sleep(30)
256
257        # If we exit the for loop, the timeout has been reached
258        gp.print_var(ret_values)
259        return False, None
260    else:
261        gp.print_var(image_purpose)
262        return False, None
263
264
265def verify_image_not_in_bmc_uploads_dir(image_version, timeout=3):
266    r"""
267    Check that an image with the given version is not unpacked inside of the
268    BMCs image uploads directory. If no image is found, retry every 30 seconds
269    until the given timeout is hit, in case the BMC takes time
270    unpacking the image.
271
272    Description of argument(s):
273    image_version                   The version of the image to look for on
274                                    the BMC.
275    timeout                         How long, in minutes, to try to find an
276                                    image on the BMC. Default is 3 minutes.
277    """
278
279    for i in range(timeout * 2):
280        stdout, stderr, rc = \
281            bsu.bmc_execute_command('ls ' + var.IMAGE_UPLOAD_DIR_PATH
282                                    + '*/MANIFEST 2>/dev/null '
283                                    + '| xargs grep -rl "version='
284                                    + image_version + '"')
285        image_dir = os.path.dirname(stdout.split('\n')[0])
286        if '' != image_dir:
287            bsu.bmc_execute_command('rm -rf ' + image_dir)
288            BuiltIn().fail('Found invalid BMC Image: ' + image_dir)
289        time.sleep(30)
290