File: submit.py

package info (click to toggle)
lqa 20230104~git9be8db8ab65c-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 340 kB
  • sloc: python: 2,237; makefile: 7
file content (298 lines) | stat: -rw-r--r-- 12,382 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
###################################################################################
# LAVA QA tool
# Copyright (C) 2015 Collabora Ltd.

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  US
###################################################################################

from __future__ import print_function

import re
import yaml
import json
import os
import os.path
try:
    from urllib.parse import urlparse
except ImportError:
    from urlparse import urlparse
import jinja2.exceptions
import jinja2
import requests

try:
    from xmlrpc.client import Fault
except ImportError:
    from xmlrpclib import Fault
from jinja2 import Environment, FileSystemLoader, \
    StrictUndefined, DebugUndefined
from lqa_api.exit_codes import APPLICATION_ERROR
from lqa_api.job import Job
from lqa_api.waitqueue import WaitQueue
from lqa_api.outputlog import OutputLog, OutputLogError
from lqa_tool.settings import lqa_logger
from lqa_tool.utils import merge_profiles
from lqa_tool.commands import Command
from lqa_tool.exceptions import ProfileNotFound
from lqa_tool.utils import print_add_msg, print_wait_msg, \
    print_remove_msg, print_timeout_msg


class Profiles(object):
    """Initialize profiles for command objects.

    :param cmd: The command object to setup
    :param default_config_file: The default configuration file path"""

    def __init__(self, cmd, default_config_file):
        # Set yaml configuration
        try:
            with open(default_config_file) as conf_data:
                env = jinja2.Environment(loader = jinja2.FileSystemLoader('.'))
                t = env.get_template(default_config_file)
                self.config = yaml.safe_load(t.render())
        except EnvironmentError as e:
            lqa_logger.error(e)
            exit(APPLICATION_ERROR)
        except yaml.scanner.ScannerError as e:
            lqa_logger.error(e)
            exit(APPLICATION_ERROR)

class SubmitCmd(Command):

    def __init__(self, args):
        Command.__init__(self, args)
        self.waitq = WaitQueue()

    def run(self):

        """Submit job id

        It follows the priority for profiles processing:
        1) Any profile specified by '-p'
        2) All profiles from 'all-profiles'
        3) Or run without any profile....
        """
        if self.args.profile or self.args.all_profiles:
            # A profile file is required for using profiles
            if not self.args.profile_file:
                raise ProfileNotFound

            # Get the profiles from the <profiles>.yaml file
            self.profiles = Profiles(self, self.args.profile_file)
            # Fetch the main profile and merge later with any sub profile
            main_profile = self.profiles.config.get('main-profile', {})

            if self.args.profile:
                # If -p/--profile specified.
                for profile_name in self.args.profile:
                    # Filter profiles from the profiles file matching -p.
                    profiles = [
                        profile
                        for profile in self.profiles.config.get('profiles', [])
                        if profile['name'] == profile_name ]
                    # Exit with failure if no profile in the filtered list.
                    if not profiles:
                        lqa_logger.error(
                            "error: profile {} not found in profile file {}"
                            .format(profile_name, self.args.profile_file))
                        exit(APPLICATION_ERROR)
                    # Run with found profiles. 
                    # There can exist profiles with the same name.
                    for profile in profiles:
                        self._run(merge_profiles(main_profile, profile))
            else:
                # Otherwise is --all-profiles.
                for profile in self.profiles.config.get('profiles', []):
                    self._run(merge_profiles(main_profile, profile))
        else:
            self._run()

        # Wait for jobs if --wait option enabled.
        if self.waitq.has_jobs():
            try:
                self.waitq.wait(self.args.wait_timeout, print_wait_msg,
                                print_remove_msg, print_timeout_msg)
                # Exit from here if the --wait option is passed so the
                # exit code from the queue is used instead.
                exit(self.waitq.exit_code)
            except ValueError as e:
                lqa_logger.error("wait timeout: {}".format(e))
                exit(APPLICATION_ERROR)

    def _mangle_json(self, data, variables, job_file):
        # Deserialize rendered template into a json object.
        # This is required to apply the 'key fields' replacement.
        try:
            json_obj = json.loads(data)
        except ValueError as e:
            lqa_logger.error("json file '{}': {}".format(job_file, e))
            return None

        # Replace key field variables.
        r_json_obj = _replaceKeyFields(json_obj, variables)

        # Set priority.
        # This option overrides the profile values to the 'priority' field.
        if self.args.priority:
            r_json_obj['priority'] = self.args.priority

        # After all variable substitutions, check if the image url exists if
        # --check-image-url is true.
        if self.args.check_image_url:
            self._check_image_url(r_json_obj)

        # Serialize json object to json string for submitting job.
        return json.dumps(r_json_obj, indent=2)


    def _run(self, profile={}):
        variables = profile.get('variables', {})

        if self.args.template_vars:
            # The variables translates to a hash that we can easily use
            # for mapping the fields -> values in the json template
            for item in self.args.template_vars:
                k, v = item.split(':', 1)
                variables[k] = v

        # Get the job files from either the command line if available,
        # or from the 'templates' profile variable otherwise.
        job_files = []
        template_dirs = []
        if self.args.submit_job:
            for f in self.args.submit_job:
                job_files.append(os.path.basename(f))
                template_dirs.append(os.path.dirname(f))
        else:
            job_files = profile.get('templates', [])
            template_dirs.append(profile.get('template-dir', os.getcwd()))

        # Choose the 'undefined' variable strategy
        uv = (self.args.debug_vars and DebugUndefined) or StrictUndefined
        # Create a template environment.
        # Use list/set to remove any duplicate.
        env = Environment(loader=FileSystemLoader(list(set(template_dirs))),
                          undefined=uv)

        # Process job files
        for job_file in job_files:
            try:
                # Get template from environment and render it.
                data = env.get_template(job_file).render(variables)
            except TypeError as e:
                lqa_logger.error("type error in {}: {}".format(job_file, e))
                continue
            except jinja2.exceptions.TemplateNotFound as e:
                lqa_logger.error("template not found: {}".format(e))
                continue
            except jinja2.exceptions.UndefinedError as e:
                lqa_logger.error("template variable not defined in {}: {}"
                                 .format(job_file, e))
                continue

            # V1 files we replace key-value pairs not jus templated values
            if job_file.endswith(".json"):
                data = self._mangle_json(data, variables, job_file)
                if data == None:
                    continue
            elif not job_file.endswith(".yaml"):
                lqa_logger.error("Job file not recognize as v1 or v2: {}"
                                 .format(job_file))

            if self.args.verbose:
                print(data)

            if not self.args.dry_run:
                try:
                    job_id = self.server.submit_job(data)
                    # Queue jobs ids to wait for them if 'wait' option enabled.
                    if self.args.wait_timeout:
                        self.waitq.addjob(Job(job_id, self.server), print_add_msg)
                except Fault as e:
                    lqa_logger.error("Submitting job {}: {}".format(job_file, e))
                    continue
                lqa_logger.info("Submitted job {} with id {}" \
                                    .format(job_file, job_id))

                # Show live output if '--live' option passed
                if self.args.live:
                    if len(job_files) > 1:
                        lqa_logger.info("lqa submit: --live option is not valid "
                                        "with more than 1 file (skipping)")
                    elif self.args.wait_timeout:
                        lqa_logger.info("lqa submit: --live option can't be used "
                                        "together with --wait option (skipping)")
                    else:
                        # Everything is valid to use live option, then go for it!
                        try:
                            OutputLog(job_id, self.server, is_live=True,
                                      logger=lqa_logger).run()
                        except OutputLogError as e:
                            lqa_logger.error("lqa submit: error: {}".format(e))
                            exit(APPLICATION_ERROR)

    def _check_image_url(self, json_job_obj):
        """This method checks for the existence of the image field url path
        for all the 'deploy_image' actions (if available)"""
        for action in json_job_obj['actions']:
            if action['command'] == 'deploy_image':
                image = action['parameters'].get('image', None)
                if image:
                    # Test for local url path
                    url = urlparse(image)
                    if url.scheme == 'file':
                        if os.path.exists(url.path):
                            return
                    else:
                        url_code = requests.head(image)
                        if url_code.status_code == requests.codes.ok:
                            return
                    lqa_logger.error(
                        "lqa submit error: image url does not exist: {}"
                        .format(image))
                    exit(APPLICATION_ERROR)

def _replaceKeyFields(json_obj, variables):
    """Replace key fields

    This method will iterate over a JSON object and will assign values to
    those keys (json fields) with the same name of a (profile) variable.

    :param json_obj: The json object to be processed
    :param variables: Dictionary containing variable and values.

    :returns: A new json object containing the replaced values for the
              json fields with the same name of the specified variables.
    """
    def _replaceKey(variable, value, json_obj):

        if type(json_obj) == list:
            for i, json_obj_v in enumerate(json_obj):
                json_obj[i] = _replaceKey(variable, value, json_obj_v)
        elif type(json_obj) == dict:
            for k in json_obj:
                # Replace key value if the 'key' == 'variable'
                if k == variable:
                    json_obj[k] = value
                # Otherwise continue processing the json fields.
                else:
                    json_obj[k] = _replaceKey(variable, value, json_obj[k])
        return json_obj

    j = json_obj
    for variable in variables:
        j = _replaceKey(variable, variables[variable], j)
    return j