blob: a468b0260568c50f54d8752c73c4dcffdd6d4b11 [file] [log] [blame]
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +02001# Copyright 2016 Mirantis, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
Dmitry Tyzhnenkob8641832017-11-07 17:02:47 +020015import netaddr
16
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030017from collections import defaultdict
18
19from datetime import datetime
Dennis Dmitrievb8115f52017-12-15 13:09:56 +020020from pepper import libpepper
21from tcp_tests.helpers import utils
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030022from tcp_tests import logger
Dmitry Tyzhnenko35413c02018-03-05 14:12:37 +020023from tcp_tests import settings
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030024from tcp_tests.managers.execute_commands import ExecuteCommandsMixin
25
26LOG = logger.logger
27
28
29class SaltManager(ExecuteCommandsMixin):
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +020030 """docstring for SaltManager"""
31
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030032 __config = None
33 __underlay = None
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030034 _map = {
35 'enforceState': 'enforce_state',
36 'enforceStates': 'enforce_states',
37 'runState': 'run_state',
38 'runStates': 'run_states',
39 }
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +020040
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020041 def __init__(self, config, underlay, host=None, port='6969',
42 username=None, password=None):
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030043 self.__config = config
44 self.__underlay = underlay
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030045 self.__port = port
46 self.__host = host
47 self.__api = None
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020048 self.__user = username or settings.SALT_USER
49 self.__password = password or settings.SALT_PASSWORD
Dennis Dmitrieveac3aab2017-07-12 16:36:41 +030050 self._salt = self
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +020051
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +030052 super(SaltManager, self).__init__(config=config, underlay=underlay)
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +020053
54 def install(self, commands):
Dina Belovae6fdffb2017-09-19 13:58:34 -070055 # if self.__config.salt.salt_master_host == '0.0.0.0':
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030056 # # Temporary workaround. Underlay should be extended with roles
57 # salt_nodes = self.__underlay.node_names()
58 # self.__config.salt.salt_master_host = \
59 # self.__underlay.host_by_node_name(salt_nodes[0])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +020060
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030061 self.execute_commands(commands=commands,
62 label="Install and configure salt")
63
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +020064 def change_creds(self, username, password):
65 self.__user = username
66 self.__password = password
67
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030068 @property
69 def port(self):
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030070 return self.__port
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030071
72 @property
73 def host(self):
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030074 if self.__host:
75 return self.__host
76 else:
Dina Belovae6fdffb2017-09-19 13:58:34 -070077 # TODO(ddmitriev): consider to add a check and raise
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030078 # exception if 'salt_master_host' is not initialized.
79 return self.__config.salt.salt_master_host
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030080
81 @property
82 def api(self):
83 def login():
84 LOG.info("Authentication in Salt API")
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030085 self.__api.login(
86 username=self.__user,
87 password=self.__password,
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030088 eauth='pam')
89 return datetime.now()
90
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030091 if self.__api:
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030092 if (datetime.now() - self.__session_start).seconds < 5 * 60:
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030093 return self.__api
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030094 else:
95 # FIXXME: Change to debug
96 LOG.info("Session's expired")
97 self.__session_start = login()
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +030098 return self.__api
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +030099
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300100 url = "http://{host}:{port}".format(
101 host=self.host, port=self.port)
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +0300102 LOG.info("Connecting to Salt API {0}".format(url))
Dennis Dmitrievb8115f52017-12-15 13:09:56 +0200103 self.__api = libpepper.Pepper(url)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300104 self.__session_start = login()
Dennis Dmitriev2d60c8e2017-05-12 18:34:01 +0300105 return self.__api
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300106
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000107 def local(self, tgt, fun, args=None, kwargs=None, timeout=None):
108 return self.api.local(tgt, fun, args, kwargs, timeout=timeout,
109 expr_form='compound')
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300110
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000111 def local_async(self, tgt, fun, args=None, kwargs=None, timeout=None):
112 return self.api.local_async(tgt, fun, args, kwargs, timeout=timeout)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300113
114 def lookup_result(self, jid):
115 return self.api.lookup_jid(jid)
116
117 def check_result(self, r):
118 if len(r.get('return', [])) == 0:
119 raise LookupError("Result is empty or absent")
120
121 result = r['return'][0]
Dmitry Tyzhnenkobc0f8262017-04-28 15:39:26 +0300122 if len(result) == 0:
123 raise LookupError("Result is empty or absent")
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300124 LOG.info("Job has result for %s nodes", result.keys())
125 fails = defaultdict(list)
126 for h in result:
127 host_result = result[h]
128 LOG.info("On %s executed:", h)
129 if isinstance(host_result, list):
130 fails[h].append(host_result)
131 continue
132 for t in host_result:
133 task = host_result[t]
134 if task['result'] is False:
135 fails[h].append(task)
136 LOG.error("%s - %s", t, task['result'])
137 else:
138 LOG.info("%s - %s", t, task['result'])
139
140 return fails if fails else None
141
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000142 def enforce_state(self, tgt, state, args=None, kwargs=None, timeout=None):
143 r = self.local(tgt=tgt, fun='state.sls', args=state, timeout=timeout)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300144 f = self.check_result(r)
145 return r, f
146
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000147 def enforce_states(self, tgt, state, args=None, kwargs=None, timeout=None):
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300148 rets = []
149 for s in state:
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000150 r = self.enforce_state(tgt=tgt, state=s, timeout=timeout)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300151 rets.append(r)
152 return rets
153
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000154 def run_state(self, tgt, state, args=None, kwargs=None, timeout=None):
155 return self.local(tgt=tgt, fun=state, args=args, kwargs=kwargs,
156 timeout=timeout), None
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300157
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000158 def run_states(self, tgt, state, args=None, kwargs=None, timeout=None):
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300159 rets = []
160 for s in state:
Dennis Dmitrievb6bcc5c2018-09-26 11:07:53 +0000161 r = self.run_state(tgt=tgt, state=s, args=args, kwargs=kwargs,
162 timeout=timeout)
Dmitry Tyzhnenko2b730a02017-04-07 19:31:32 +0300163 rets.append(r)
164 return rets
Artem Panchenko0594cd72017-06-12 13:25:26 +0300165
166 def get_pillar(self, tgt, pillar):
167 result = self.local(tgt=tgt, fun='pillar.get', args=pillar)
168 return result['return']
Dmitry Tyzhnenkob8641832017-11-07 17:02:47 +0200169
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200170 def get_grains(self, tgt, grains):
171 result = self.local(tgt=tgt, fun='grains.get', args=grains)
172 return result['return']
173
Dmitry Tyzhnenkob8641832017-11-07 17:02:47 +0200174 def get_ssh_data(self):
175 """Generate ssh config for Underlay
176
177 :param roles: list of strings
178 """
179
180 pool_name = self.__config.underlay.net_mgmt
181 pool_net = netaddr.IPNetwork(self.__config.underlay.address_pools[
182 self.__config.underlay.net_mgmt])
183 hosts = self.local('*', 'grains.item', ['host', 'ipv4'])
184
185 if len(hosts.get('return', [])) == 0:
186 raise LookupError("Hosts is empty or absent")
187 hosts = hosts['return'][0]
188 if len(hosts) == 0:
189 raise LookupError("Hosts is empty or absent")
190
Dennis Dmitriev83cc1d52018-11-09 15:35:30 +0200191 def host(minion_id, ip):
Dmitry Tyzhnenkob8641832017-11-07 17:02:47 +0200192 return {
193 'roles': ['salt_minion'],
194 'keys': [
195 k['private'] for k in self.__config.underlay.ssh_keys
196 ],
Dennis Dmitriev83cc1d52018-11-09 15:35:30 +0200197 'node_name': minion_id,
198 'minion_id': minion_id,
Dmitry Tyzhnenkob8641832017-11-07 17:02:47 +0200199 'host': ip,
200 'address_pool': pool_name,
201 'login': settings.SSH_NODE_CREDENTIALS['login'],
202 'password': settings.SSH_NODE_CREDENTIALS['password']
203 }
204
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200205 try:
206 ret = [
207 host(k, next(i for i in v['ipv4'] if i in pool_net))
208 for k, v in hosts.items()
209 if next(i for i in v['ipv4'] if i in pool_net)]
210 LOG.debug("Fetched ssh data from salt grains - {}".format(ret))
211 return ret
212 except StopIteration:
213 msg = ("Can't match nodes ip address with network cidr\n"
214 "Managment network - {net}\n"
215 "Host with address - {host_list}".format(
216 net=pool_net,
217 host_list={k: v['ipv4'] for k, v in hosts.items()}))
218 raise StopIteration(msg)
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200219
Dennis Dmitriev83cc1d52018-11-09 15:35:30 +0200220 def update_ssh_data_from_minions(self):
221 """Combine existing underlay.ssh with VCP salt minions"""
222 salt_nodes = self.get_ssh_data()
223
224 for salt_node in salt_nodes:
225 nodes = [n for n in self.__config.underlay.ssh
226 if salt_node['host'] == n['host']
227 and salt_node['address_pool'] == n['address_pool']]
228 if nodes:
229 # Assume that there can be only one node with such IP address
230 # Just update minion_id for this node
231 nodes[0]['minion_id'] = salt_node['minion_id']
232 else:
233 # New node, add to config.underlay.ssh
234 self.__config.underlay.ssh.append(salt_node)
235
236 self.__underlay.config_ssh = []
237 self.__underlay.add_config_ssh(self.__config.underlay.ssh)
238
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200239 def service_status(self, tgt, service):
240 result = self.local(tgt=tgt, fun='service.status', args=service)
241 return result['return']
242
243 def service_restart(self, tgt, service):
244 result = self.local(tgt=tgt, fun='service.restart', args=service)
245 return result['return']
246
247 def service_stop(self, tgt, service):
248 result = self.local(tgt=tgt, fun='service.stop', args=service)
249 return result['return']
Dennis Dmitrievb8115f52017-12-15 13:09:56 +0200250
251 @utils.retry(3, exception=libpepper.PepperException)
252 def sync_time(self, tgt='*'):
253 LOG.info("NTP time sync on the salt minions '{0}'".format(tgt))
254 # Force authentication update on the next API access
255 # because previous authentication most probably is not valid
256 # before or after time sync.
257 self.__api = None
258 self.run_state(
259 tgt,
Dmitry Tyzhnenko35413c02018-03-05 14:12:37 +0200260 'cmd.run', 'service ntp stop; if [ -x /usr/sbin/ntpdate ]; then ntpdate -s ntp.ubuntu.com; else ntpd -gq ; fi; service ntp start') # noqa
Dennis Dmitrievb8115f52017-12-15 13:09:56 +0200261 new_time_res = self.run_state(tgt, 'cmd.run', 'date')
262 for node_name, time in sorted(new_time_res[0]['return'][0].items()):
263 LOG.info("{0}: {1}".format(node_name, time))
264 self.__api = None