blob: 0b4b698426ff806c85e45f32373afdf00cd63cb5 [file] [log] [blame]
# Copyright 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
import pkg_resources
from collections import defaultdict
from polling import poll
from datetime import datetime
from pepper import libpepper
from tcp_tests.helpers import utils
from tcp_tests import logger
from tcp_tests import settings
from tcp_tests.managers.execute_commands import ExecuteCommandsMixin
LOG = logger.logger
class SaltManager(ExecuteCommandsMixin):
"""docstring for SaltManager"""
__config = None
__underlay = None
_map = {
'enforceState': 'enforce_state',
'enforceStates': 'enforce_states',
'runState': 'run_state',
'runStates': 'run_states',
}
def __init__(self, config, underlay, host=None, port='6969',
username=None, password=None):
self.__config = config
self.__underlay = underlay
self.__port = port
self.__host = host
self.__api = None
self.__user = username or settings.SALT_USER
self.__password = password or settings.SALT_PASSWORD
self._salt = self
super(SaltManager, self).__init__(config=config, underlay=underlay)
def install(self, commands):
# if self.__config.salt.salt_master_host == '0.0.0.0':
# # Temporary workaround. Underlay should be extended with roles
# salt_nodes = self.__underlay.node_names()
# self.__config.salt.salt_master_host = \
# self.__underlay.host_by_node_name(salt_nodes[0])
self.execute_commands(commands=commands,
label="Install and configure salt")
self.create_env_salt()
self.create_env_jenkins_day01()
# self.create_env_jenkins_cicd()
# self.create_env_k8s()
def change_creds(self, username, password):
self.__user = username
self.__password = password
@property
def port(self):
return self.__port
@property
def host(self):
if self.__host:
return self.__host
else:
# TODO(ddmitriev): consider to add a check and raise
# exception if 'salt_master_host' is not initialized.
return self.__config.salt.salt_master_host
@property
def api(self):
def login():
LOG.info("Authentication in Salt API")
self.__api.login(
username=self.__user,
password=self.__password,
eauth='pam')
return datetime.now()
if self.__api:
if (datetime.now() - self.__session_start).seconds < 5 * 60:
return self.__api
else:
# FIXXME: Change to debug
LOG.info("Session's expired")
self.__session_start = login()
return self.__api
url = "http://{host}:{port}".format(
host=self.host, port=self.port)
LOG.info("Connecting to Salt API {0}".format(url))
self.__api = libpepper.Pepper(url)
self.__session_start = login()
return self.__api
def local(self, tgt, fun, args=None, kwargs=None, timeout=None):
return self.api.local(tgt, fun, args, kwargs, timeout=timeout,
expr_form='compound')
def local_async(self, tgt, fun, args=None, kwargs=None, timeout=None):
return self.api.local_async(tgt, fun, args, kwargs, timeout=timeout)
def runner(self, fun, args=None, **kwargs):
response = self.api.runner(fun, arg=args, **kwargs)
return response.get("return")
def lookup_result(self, jid):
return self.api.lookup_jid(jid)
def check_result(self, r):
if len(r.get('return', [])) == 0:
raise LookupError("Result is empty or absent")
result = r['return'][0]
if len(result) == 0:
raise LookupError("Result is empty or absent")
LOG.info("Job has result for %s nodes", result.keys())
fails = defaultdict(list)
for h in result:
host_result = result[h]
LOG.info("On %s executed:", h)
if isinstance(host_result, list):
fails[h].append(host_result)
continue
for t in host_result:
task = host_result[t]
if task['result'] is False:
fails[h].append(task)
LOG.error("%s - %s", t, task['result'])
else:
LOG.info("%s - %s", t, task['result'])
return fails if fails else None
def enforce_state(self, tgt, state, args=None, kwargs=None, timeout=None):
r = self.local(tgt=tgt, fun='state.sls', args=state, timeout=timeout)
f = self.check_result(r)
return r, f
def enforce_states(self, tgt, state, args=None, kwargs=None, timeout=None):
rets = []
for s in state:
r = self.enforce_state(tgt=tgt, state=s, timeout=timeout)
rets.append(r)
return rets
def run_state(self, tgt, state, args=None, kwargs=None, timeout=None):
return self.local(tgt=tgt, fun=state, args=args, kwargs=kwargs,
timeout=timeout), None
def run_states(self, tgt, state, args=None, kwargs=None, timeout=None):
rets = []
for s in state:
r = self.run_state(tgt=tgt, state=s, args=args, kwargs=kwargs,
timeout=timeout)
rets.append(r)
return rets
def get_pillar(self, tgt, pillar):
result = self.local(tgt=tgt, fun='pillar.get', args=pillar)
return result['return']
def get_single_pillar(self, tgt, pillar):
"""Get a scalar value from a single node
:return: pillar value
"""
result = self.get_pillar(tgt, pillar)
nodes = result[0].keys()
if not nodes:
raise LookupError("No minions selected "
"for the target '{0}'".format(tgt))
if len(nodes) > 1:
raise LookupError("Too many minions selected "
"for the target '{0}' , expected one: {1}"
.format(tgt, nodes))
return result[0][nodes[0]]
def get_grains(self, tgt, grains):
result = self.local(tgt=tgt, fun='grains.get', args=grains)
return result['return']
def wait_jobs_completed(self, timeout=60, interval=5):
"""
:param timeout: int, time seconds to wait
:param interval: int, time in second between attempts
:return: None
"""
# TODO(harhipova) PROD-36434 : need to check that last job completed
# with successful result
poll(
target=lambda: self.runner('jobs.active')[0] == {},
timeout=timeout,
step=interval
)
def get_ssh_data(self):
"""Generate ssh config for Underlay
:param roles: list of strings
"""
pool_name = self.__config.underlay.net_mgmt
pool_net = netaddr.IPNetwork(self.__config.underlay.address_pools[
self.__config.underlay.net_mgmt])
hosts = self.local('*', 'grains.item', ['host', 'ipv4'])
if len(hosts.get('return', [])) == 0:
raise LookupError("Hosts is empty or absent")
hosts = hosts['return'][0]
if len(hosts) == 0:
raise LookupError("Hosts is empty or absent")
def host(minion_id, ip):
return {
'roles': ['salt_minion'],
'keys': [
k['private'] for k in self.__config.underlay.ssh_keys
],
'node_name': minion_id,
'minion_id': minion_id,
'host': ip,
'address_pool': pool_name,
'login': settings.SSH_NODE_CREDENTIALS['login'],
'password': settings.SSH_NODE_CREDENTIALS['password']
}
try:
ret = [
host(k, next(i for i in v['ipv4'] if i in pool_net))
for k, v in hosts.items()
if next(i for i in v['ipv4'] if i in pool_net)]
LOG.debug("Fetched ssh data from salt grains - {}".format(ret))
return ret
except StopIteration:
msg = ("Can't match nodes ip address with network cidr\n"
"Managment network - {net}\n"
"Host with address - {host_list}".format(
net=pool_net,
host_list={k: v['ipv4'] for k, v in hosts.items()}))
raise StopIteration(msg)
def update_ssh_data_from_minions(self):
"""Combine existing underlay.ssh with VCP salt minions"""
salt_nodes = self.get_ssh_data()
for salt_node in salt_nodes:
nodes = [n for n in self.__config.underlay.ssh
if salt_node['host'] == n['host'] and
salt_node['address_pool'] == n['address_pool']]
if nodes:
# Assume that there can be only one node with such IP address
# Just update minion_id for this node
nodes[0]['minion_id'] = salt_node['minion_id']
else:
# New node, add to config.underlay.ssh
self.__config.underlay.ssh.append(salt_node)
self.__underlay.config_ssh = []
self.__underlay.add_config_ssh(self.__config.underlay.ssh)
def service_status(self, tgt, service):
result = self.local(tgt=tgt, fun='service.status', args=service)
return result['return']
def service_restart(self, tgt, service):
result = self.local(tgt=tgt, fun='service.restart', args=service)
return result['return']
def service_stop(self, tgt, service):
result = self.local(tgt=tgt, fun='service.stop', args=service)
return result['return']
def pkg_install(self, tgt, pkg):
result = self.local(tgt=tgt, fun='pkg.install', args=pkg)
return result['return']
def pkg_info_installed(self, tgt, pkg):
result = self.local(tgt=tgt, fun='pkg.info_installed', args=pkg)
return result['return']
def cmd_run(self, tgt, cmd):
result = self.local(tgt=tgt, fun='cmd.run', args=cmd)
return result['return']
def file_write(self, tgt, filename, content):
result = self.local(tgt=tgt,
fun='file.write',
args=[filename, content])
return result['return']
def file_makedirs(self, tgt, path):
if path[-1] != "/":
path += "/"
result = self.local(tgt=tgt, fun='file.makedirs', args=path)
return result['return']
@utils.retry(10, exception=libpepper.PepperException)
def sync_time(self, tgt='*'):
LOG.info("NTP time sync on the salt minions '{0}'".format(tgt))
# Force authentication update on the next API access
# because previous authentication most probably is not valid
# before or after time sync.
self.__api = None
if not settings.SKIP_SYNC_TIME:
cmd = ('chmod -x /usr/sbin/ntpd'
'service ntp stop;'
# note: maas-rackd will return 'pool' after start
'sed -i \'s/^pool ntp.ubuntu.com/server ntp.cesnet.cz/g\' '
'/etc/ntp/maas.conf;'
'sed -i \'s/^pool ntp.ubuntu.com/server ntp.cesnet.cz/g\' '
'/etc/ntp.conf;'
'if [ -x /usr/sbin/ntpdate ]; then'
' ntpdate -s ntp.cesnet.cz;'
'else'
' ntpd -gq;'
'fi;'
'chmod +x /usr/sbin/ntpd'
'service ntp start;'
'sleep 3; ntpq -pn;')
self.run_state(
tgt,
'cmd.run', cmd, timeout=360) # noqa
new_time_res = self.run_state(tgt, 'cmd.run', 'date')
for node_name, time in sorted(new_time_res[0]['return'][0].items()):
LOG.info("{0}: {1}".format(node_name, time))
self.__api = None
def create_env_salt(self):
"""Creates static utils/env_salt file"""
env_salt_filename = pkg_resources.resource_filename(
settings.__name__, 'utils/env_salt')
with open(env_salt_filename, 'w') as f:
f.write(
'export SALT_MASTER_IP={host}\n'
'export SALTAPI_URL=http://{host}:{port}/\n'
'export SALTAPI_USER="{user}"\n'
'export SALTAPI_PASS="{password}"\n'
'export SALTAPI_EAUTH="pam"\n'
'echo "export SALT_MASTER_IP=${{SALT_MASTER_IP}}"\n'
'echo "export SALTAPI_URL=${{SALTAPI_URL}}"\n'
'echo "export SALTAPI_USER=${{SALTAPI_USER}}"\n'
'echo "export SALTAPI_PASS=${{SALTAPI_PASS}}"\n'
'echo "export SALTAPI_EAUTH=${{SALTAPI_EAUTH}}"\n'
.format(host=self.host, port=self.port,
user=self.__user, password=self.__password)
)
def create_env_jenkins_day01(self):
"""Creates static utils/env_jenkins_day01 file"""
env_jenkins_day01_filename = pkg_resources.resource_filename(
settings.__name__, 'utils/env_jenkins_day01')
tgt = 'I@docker:client:stack:jenkins and cfg01*'
jenkins_params = self.get_single_pillar(
tgt=tgt, pillar="jenkins:client:master")
jenkins_host = jenkins_params.get('host')
jenkins_port = jenkins_params.get('port')
jenkins_protocol = jenkins_params.get('proto', 'http')
jenkins_user = jenkins_params.get('username', 'admin')
jenkins_pass = jenkins_params.get('password')
if not all([jenkins_host,
jenkins_port,
jenkins_protocol,
jenkins_user,
jenkins_pass]):
raise LookupError(
"Some of the required parameters for Jenkins not set in the "
"pillar jenkins:client:master on {0}: {1}"
.format(tgt, jenkins_params))
with open(env_jenkins_day01_filename, 'w') as f:
f.write(
'export JENKINS_URL={protocol}://{host}:{port}\n'
'export JENKINS_USER={user}\n'
'export JENKINS_PASS={password}\n'
'export JENKINS_START_TIMEOUT=60\n'
'export JENKINS_BUILD_TIMEOUT=1800\n'
'echo "export JENKINS_URL=${{JENKINS_URL}}'
' # Jenkins API URL"\n'
'echo "export JENKINS_USER=${{JENKINS_USER}}'
' # Jenkins API username"\n'
'echo "export JENKINS_PASS=${{JENKINS_PASS}}'
' # Jenkins API password or token"n\n'
'echo "export JENKINS_START_TIMEOUT=${{JENKINS_START_TIMEOUT}}'
' # Timeout waiting for job in queue to start building"\n'
'echo "export JENKINS_BUILD_TIMEOUT=${{JENKINS_BUILD_TIMEOUT}}'
' # Timeout waiting for building job to complete"\n'
.format(host=jenkins_host, port=jenkins_port,
protocol=jenkins_protocol, user=jenkins_user,
password=jenkins_pass)
)
def create_env_jenkins_cicd(self):
"""Creates static utils/env_jenkins_cicd file"""
env_jenkins_cicd_filename = pkg_resources.resource_filename(
settings.__name__, 'utils/env_jenkins_cicd')
tgt = 'I@docker:client:stack:jenkins and cid01*'
try:
jenkins_params = self.get_single_pillar(
tgt=tgt, pillar="jenkins:client:master")
except LookupError as e:
LOG.error("Skipping creation {0} because cannot get Jenkins CICD "
"parameters from '{1}': {2}"
.format(env_jenkins_cicd_filename, tgt, e.message))
return
jenkins_host = jenkins_params.get('host')
jenkins_port = jenkins_params.get('port')
jenkins_protocol = jenkins_params.get('proto', 'http')
jenkins_user = jenkins_params.get('username', 'admin')
jenkins_pass = jenkins_params.get('password')
if not all([jenkins_host,
jenkins_port,
jenkins_protocol,
jenkins_user,
jenkins_pass]):
raise LookupError(
"Some of the required parameters for Jenkins not set in the "
"pillar jenkins:client:master on {0}: {1}"
.format(tgt, jenkins_params))
with open(env_jenkins_cicd_filename, 'w') as f:
f.write(
'export JENKINS_URL={protocol}://{host}:{port}\n'
'export JENKINS_USER={user}\n'
'export JENKINS_PASS={password}\n'
'export JENKINS_START_TIMEOUT=60\n'
'export JENKINS_BUILD_TIMEOUT=1800\n'
'echo "export JENKINS_URL=${{JENKINS_URL}}'
' # Jenkins API URL"\n'
'echo "export JENKINS_USER=${{JENKINS_USER}}'
' # Jenkins API username"\n'
'echo "export JENKINS_PASS=${{JENKINS_PASS}}'
' # Jenkins API password or token"n\n'
'echo "export JENKINS_START_TIMEOUT=${{JENKINS_START_TIMEOUT}}'
' # Timeout waiting for job in queue to start building"\n'
'echo "export JENKINS_BUILD_TIMEOUT=${{JENKINS_BUILD_TIMEOUT}}'
' # Timeout waiting for building job to complete"\n'
.format(host=jenkins_host, port=jenkins_port,
protocol=jenkins_protocol, user=jenkins_user,
password=jenkins_pass)
)
def create_env_k8s(self):
"""Creates static utils/env_k8s file"""
env_k8s_filename = pkg_resources.resource_filename(
settings.__name__, 'utils/env_k8s')
tgt = 'I@haproxy:proxy:enabled:true and I@kubernetes:master and *01*'
try:
haproxy_params = self.get_single_pillar(
tgt=tgt, pillar="haproxy:proxy:listen:k8s_secure:binds")[0]
k8s_params = self.get_single_pillar(
tgt=tgt, pillar="kubernetes:master:admin")
except LookupError as e:
LOG.error("Skipping creation {0} because cannot get Kubernetes "
"parameters from '{1}': {2}"
.format(env_k8s_filename, tgt, e.message))
return
kube_host = haproxy_params['address']
kube_apiserver_port = haproxy_params['port']
kubernetes_admin_user = k8s_params['username']
kubernetes_admin_password = k8s_params['password']
with open(env_k8s_filename, 'w') as f:
f.write(
'export kube_host={host}\n'
'export kube_apiserver_port={port}\n'
'export kubernetes_admin_user={user}\n'
'export kubernetes_admin_password={password}\n'
'echo "export kube_host=${{kube_host}}'
' # Kube API host"\n'
'echo "export kube_apiserver_port=${{kube_apiserver_port}}'
' # Kube API port"\n'
'echo "export kubernetes_admin_user=${{kubernetes_admin_user}}'
' # Kube API username"\n'
'echo "export kubernetes_admin_password='
'${{kubernetes_admin_password}} # Kube API password"n\n'
.format(host=kube_host, port=kube_apiserver_port,
user=kubernetes_admin_user,
password=kubernetes_admin_password)
)
def get_minions_by_target(self, tgt):
result = self.local(tgt=tgt, fun="test.ping")
return result['return'][0].keys()