Add tools to run jenkins jobs and remote commands

- ./tcp_tests/utils/create_devops_env.py
  Creates a fuel-devops enviromnet with VMs in disabled
  state, to generate networks and addresses for inventory.
  Required parameters:
    export ENV_NAME=test
    export LAB_CONFIG_NAME=<template directory with underlay.yml>
    export MANAGER=devops
  Other parameters may be required for the underlay.yml

  CLI example:
    export PYTHONPATH=$(pwd)
    python ./tcp_tests/utils/create_devops_env.py

- ./tcp_tests/utils/run_jenkins_job.py
  Run a jenkins job with parameters, wait for completion,
  print the console output to stdout while waiting.
  Required parameters:
    export JENKINS_URL=http://host:port/
    export JENKINS_USER=admin
    export JENKINS_PASS=admin
  CLI example:

    JOB_PARAMETERS="{
      \"SALT_MASTER_URL\": \"${SALTAPI_URL}\",
      \"STACK_INSTALL\": \"core,cicd\"
    }"
    JOB_PREFIX="[ {job_name} #{build_number}:cicd {time} ] "

    python ./tcp_tests/utils/run_jenkins_job.py \
        --verbose \
        --job-name=deploy_openstack \
        --job-parameters="$JOB_PARAMETERS" \
        --job-output-prefix="$JOB_PREFIX"

- ./tcp_tests/utils/get_param.py
  Get a single parameter from the salt pillar.
  Useful to get addresses and other scalar values.
  Required parameters are the same as for 'pepper' CLI:
    export SALTAPI_URL=http://${SALT_MASTER_IP}:6969/
    export SALTAPI_USER='salt'
    export SALTAPI_PASS='icecream12345!'
    export SALTAPI_EAUTH='pam'
  CLI example:
  export JENKINS_HOST=$(./tcp_tests/utils/get_param.py \
    -C 'I@docker:client:stack:jenkins' \
    pillar.get jenkins:client:master:host)

- ./tcp_tests/utils/run_template_commands.py
  Run remote commands from the ./tcp_tests/templates/
  No environment varialbes are required, but may be
  useful to provide the INI config from some completed
  deployment.
  CLI example:

    export TESTS_CONFIGS=$(pwd)/test_salt_deployed.ini
    ./tcp_tests/utils/run_template_commands.py \
        ./tcp_tests/templates/<lab_name>/common_services.yaml

- some env files for sourcing to get access to different APIs.
  This will simplify using the scripts above.

    . ./tcp_tests/utils/env_salt          # salt-api access
    . ./tcp_tests/utils/env_jenkins_day01 # jenkins on salt-master
    . ./tcp_tests/utils/env_jenkins_cicd  # jenkins on cicd
    . ./tcp_tests/utils/env_k8s           # k8s api access

- fixed UnderlayManager.sudo_check_call() to remove
  deprecation warning.

Improvements to JenkisClient:
- Add JenkinsWrapper class to workaround the bug
  https://bugs.launchpad.net/python-jenkins/+bug/1775047
  which is happened to CICD Jenkins behind the haproxy
- improved waiting for start of the job in run_build()
- new argument 'interval' in wait_end_of_build(), to set
  the polling interval while waiting the job
- new argument 'job_output_prefix' in wait_end_of_build(),
  which allows to set the prefix to each line of the console
  output of the job; with some pre-defined template keys.
- improved printing the job output in case of non-unicode characters

Change-Id: Ie7d1324d8247e55ba9c0f0492ca39fc176ff4935
diff --git a/tcp_tests/utils/run_jenkins_job.py b/tcp_tests/utils/run_jenkins_job.py
new file mode 100755
index 0000000..00ebec6
--- /dev/null
+++ b/tcp_tests/utils/run_jenkins_job.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+
+import argparse
+import os
+import sys
+
+import json
+
+sys.path.append(os.getcwd())
+try:
+    from tcp_tests.managers.jenkins.client import JenkinsClient
+except ImportError:
+    print("ImportError: Run the application from the tcp-qa directory or "
+          "set the PYTHONPATH environment variable to directory which contains"
+          " ./tcp_tests")
+    sys.exit(1)
+
+
+EXIT_CODES = {
+  "SUCCESS": 0,
+  # 1 - python runtime execution error
+  # 2 - job unknown status
+  "FAILURE": 3,
+  "UNSTABLE": 4,
+  "ABORTED": 5,
+  "DISABLED": 6
+  # 10 - invalid cli options
+}
+
+
+def load_params():
+    """
+    Parse CLI arguments and environment variables
+
+    Returns: ArgumentParser instance
+    """
+    env_host = os.environ.get('JENKINS_URL', None)
+    env_username = os.environ.get('JENKINS_USER', None)
+    env_password = os.environ.get('JENKINS_PASS', None)
+    env_start_timeout = os.environ.get('JENKINS_START_TIMEOUT', 1800)
+    env_build_timeout = os.environ.get('JENKINS_BUILD_TIMEOUT', 3600 * 4)
+
+    parser = argparse.ArgumentParser(description=(
+        'Host, username and password may be specified either by the command '
+        'line arguments or using environment variables: JENKINS_URL, '
+        'JENKINS_USER, JENKINS_PASS, JENKINS_START_TIMEOUT, '
+        'JENKINS_BUILD_TIMEOUT. \nCommand line arguments have the highest '
+        'priority, after that the environment variables are used as defaults.'
+    ))
+    parser.add_argument('--host',
+                        metavar='JENKINS_URL',
+                        help='Jenkins Host',
+                        default=env_host)
+    parser.add_argument('--username',
+                        metavar='JENKINS_USER',
+                        help='Jenkins Username', default=env_username)
+    parser.add_argument('--password',
+                        metavar='JENKINS_PASS',
+                        help='Jenkins Password or API token',
+                        default=env_password)
+    parser.add_argument('--start-timeout',
+                        metavar='JENKINS_START_TIMEOUT',
+                        help='Timeout waiting until build is started',
+                        default=env_start_timeout,
+                        type=int)
+    parser.add_argument('--build-timeout',
+                        metavar='JENKINS_BUILD_TIMEOUT',
+                        help='Timeout waiting until build is finished',
+                        default=env_build_timeout,
+                        type=int)
+    parser.add_argument('--job-name',
+                        help='Jenkins job name to run',
+                        default=None)
+    parser.add_argument('--job-parameters',
+                        metavar='json-dict',
+                        help=('Job parameters to use instead of default '
+                              'values, as a json string, for example: '
+                              '--job-parameters=\'{"SALT_MASTER_URL": '
+                              '"http://localhost:6969"}\''),
+                        default={}, type=json.loads)
+    parser.add_argument('--job-output-prefix',
+                        help=('Jenkins job output prefix for each line in the '
+                              'output, if --verbose is enabled. Useful for the'
+                              ' pipelines that use multiple different runs of '
+                              'jobs. The string is a template for python '
+                              'format() function where the following arguments'
+                              ' are allowed: job_name, build_number. '
+                              'Example: --job-output-prefix=\"[ {job_name} '
+                              '#{build_number}, core ]\"'),
+                        default='',
+                        type=str)
+    parser.add_argument('--verbose',
+                        action='store_const',
+                        const=True,
+                        help='Show build console output',
+                        default=False)
+    return parser
+
+
+def print_build_header(build, job_params, opts):
+    print('\n#############################################################')
+    print('##### Building job [{0}] #{1} (timeout={2}) with the following '
+          'parameters:'.format(build[0], build[1], opts.build_timeout))
+    print('##### ' + '\n##### '.join(
+        [str(key) + ": " + str(val) for key, val in job_params.iteritems()]
+    ))
+    print('#############################################################')
+
+
+def print_build_footer(build, result, url):
+    print('\n\n#############################################################')
+    print('##### Completed job [{0}] #{1} at {2}: {3}'
+          .format(build[0], build[1], url, result))
+    print('#############################################################\n')
+
+
+def run_job(opts):
+
+    jenkins = JenkinsClient(
+        host=opts.host,
+        username=opts.username,
+        password=opts.password)
+
+    job_params = jenkins.make_defults_params(opts.job_name)
+    job_params.update(opts.job_parameters)
+
+    build = jenkins.run_build(opts.job_name,
+                              job_params,
+                              verbose=opts.verbose,
+                              timeout=opts.start_timeout)
+    if opts.verbose:
+        print_build_header(build, job_params, opts)
+
+    jenkins.wait_end_of_build(
+        name=build[0],
+        build_id=build[1],
+        timeout=opts.build_timeout,
+        interval=1,
+        verbose=opts.verbose,
+        job_output_prefix=opts.job_output_prefix)
+    result = jenkins.build_info(name=build[0],
+                                build_id=build[1])['result']
+    if opts.verbose:
+        print_build_footer(build, result, opts.host)
+
+    return EXIT_CODES.get(result, 2)
+
+
+def main(args=None):
+    parser = load_params()
+    opts = parser.parse_args()
+
+    if opts.host is None or opts.job_name is None:
+        print("JENKINS_URL and a job name are required!")
+        parser.print_help()
+        return 10
+    else:
+        exit_code = run_job(opts)
+        return exit_code
+
+
+if __name__ == "__main__":
+    sys.exit(main())