def read(self, key, recursive=False, wait=False, timeout=None, waitIndex=None): try: if waitIndex: result = self.client.read( key, recursive=recursive, wait=wait, timeout=timeout, waitIndex=waitIndex, ) else: result = self.client.read(key, recursive=recursive, wait=wait, timeout=timeout) except (etcd.EtcdConnectionFailed, etcd.EtcdKeyNotFound) as err: log.error("etcd: %s", err) raise except ReadTimeoutError: # For some reason, we have to catch this directly. It falls through # from python-etcd because it's trying to catch # urllib3.exceptions.ReadTimeoutError and strangely, doesn't catch. # This can occur from a watch timeout that expires, so it may be 'expected' # behavior. See issue #28553 if wait: # Wait timeouts will throw ReadTimeoutError, which isn't bad log.debug("etcd: Timed out while executing a wait") raise EtcdUtilWatchTimeout( "Watch on {0} timed out".format(key)) log.error("etcd: Timed out") raise etcd.EtcdConnectionFailed("Connection failed") except MaxRetryError as err: # Same issue as ReadTimeoutError. When it 'works', python-etcd # throws EtcdConnectionFailed, so we'll do that for it. log.error("etcd: Could not connect") raise etcd.EtcdConnectionFailed("Could not connect to etcd server") except etcd.EtcdException as err: # EtcdValueError inherits from ValueError, so we don't want to accidentally # catch this below on ValueError and give a bogus error message log.error("etcd: %s", err) raise except ValueError: # python-etcd doesn't fully support python 2.6 and ends up throwing this for *any* exception because # it uses the newer {} format syntax log.error( "etcd: error. python-etcd does not fully support python 2.6, no error information available" ) raise except Exception as err: # pylint: disable=broad-except log.error("etcd: uncaught exception %s", err) raise return result
def test_get(self): ''' Test if it get a value from etcd, by direct path ''' with patch('etcd.Client') as mock: client = etcd_util.EtcdClient({}) with patch.object(client, 'read', autospec=True) as mock: mock.return_value = MagicMock(value='stack') self.assertEqual(client.get('salt'), 'stack') mock.assert_called_with('salt', recursive=False) self.assertEqual(client.get('salt', recurse=True), 'stack') mock.assert_called_with('salt', recursive=True) # iter(list(Exception)) works correctly with both mock<1.1 and mock>=1.1 mock.side_effect = iter([etcd.EtcdKeyNotFound()]) self.assertEqual(client.get('not-found'), None) mock.side_effect = iter([etcd.EtcdConnectionFailed()]) self.assertEqual(client.get('watching'), None) # python 2.6 test mock.side_effect = ValueError self.assertEqual(client.get('not-found'), None) mock.side_effect = Exception self.assertRaises(Exception, client.get, 'some-error')
def _do_http_request(self, retry, machines_cache, request_executor, method, path, fields=None, **kwargs): some_request_failed = False for i, base_uri in enumerate(machines_cache): if i > 0: logger.info("Retrying on %s", base_uri) try: response = request_executor(method, base_uri + path, fields=fields, **kwargs) response.data.decode('utf-8') self._check_cluster_id(response) if some_request_failed: self.set_base_uri(base_uri) self._refresh_machines_cache() return response except (HTTPError, HTTPException, socket.error, socket.timeout) as e: self.http.clear() # switch to the next etcd node because we don't know exactly what happened, # whether the key didn't received an update or there is a network problem. if not retry and i + 1 < len(machines_cache): self.set_base_uri(machines_cache[i + 1]) if (isinstance(fields, dict) and fields.get("wait") == "true" and isinstance(e, (ReadTimeoutError, ProtocolError))): logger.debug("Watch timed out.") raise etcd.EtcdWatchTimedOut("Watch timed out: {0}".format(e), cause=e) logger.error("Request to server %s failed: %r", base_uri, e) logger.info("Reconnection allowed, looking for another server.") if not retry: raise etcd.EtcdException('{0} {1} request failed'.format(method, path)) some_request_failed = True raise etcd.EtcdConnectionFailed('No more machines in the cluster')
def machines(self): """Original `machines` method(property) of `etcd.Client` class raise exception when it failed to get list of etcd cluster members. This method is being called only when request failed on one of the etcd members during `api_execute` call. For us it's more important to execute original request rather then get new topology of etcd cluster. So we will catch this exception and return empty list of machines. Later, during next `api_execute` call we will forcefully update machines_cache. Also this method implements the same timeout-retry logic as `api_execute`, because the original method was retrying 2 times with the `read_timeout` on each node.""" machines_cache = self.machines_cache kwargs = self._build_request_parameters(len(machines_cache)) for base_uri in machines_cache: try: response = self.http.request(self._MGET, base_uri + self.version_prefix + '/machines', **kwargs) data = self._handle_server_response(response).data.decode('utf-8') machines = [m.strip() for m in data.split(',') if m.strip()] logger.debug("Retrieved list of machines: %s", machines) if machines: random.shuffle(machines) self._update_dns_cache(self._dns_resolver.resolve_async, machines) return machines except Exception as e: self.http.clear() logger.error("Failed to get list of machines from %s%s: %r", base_uri, self.version_prefix, e) raise etcd.EtcdConnectionFailed('No more machines in the cluster')
def test_get(self): """ Test if it get a value from etcd, by direct path """ with patch("etcd.Client") as mock: client = etcd_util.EtcdClient({}) with patch.object(client, "read", autospec=True) as mock: mock.return_value = MagicMock(value="stack") self.assertEqual(client.get("salt"), "stack") mock.assert_called_with("salt", recursive=False) self.assertEqual(client.get("salt", recurse=True), "stack") mock.assert_called_with("salt", recursive=True) # iter(list(Exception)) works correctly with both mock<1.1 and mock>=1.1 mock.side_effect = iter([etcd.EtcdKeyNotFound()]) self.assertEqual(client.get("not-found"), None) mock.side_effect = iter([etcd.EtcdConnectionFailed()]) self.assertEqual(client.get("watching"), None) # python 2.6 test mock.side_effect = ValueError self.assertEqual(client.get("not-found"), None) mock.side_effect = Exception self.assertRaises(Exception, client.get, "some-error")
def test_get(): """ Test if it get a value from etcd, by direct path """ with patch("etcd.Client") as mock: client = etcd_util.EtcdClient({}) with patch.object(client, "read", autospec=True) as mock: mock.return_value = MagicMock(value="stack") assert client.get("salt") == "stack" mock.assert_called_with("salt", recursive=False) # iter(list(Exception)) works correctly with both mock<1.1 and mock>=1.1 mock.side_effect = iter([etcd.EtcdKeyNotFound()]) assert client.get("not-found") is None mock.side_effect = iter([etcd.EtcdConnectionFailed()]) assert client.get("watching") is None # python 2.6 test mock.side_effect = ValueError assert client.get("not-found") is None mock.side_effect = Exception with pytest.raises(Exception): client.get("some-error") # Get with recurse now delegates to client.tree with patch.object(client, "tree", autospec=True) as tree_mock: tree_mock.return_value = {"salt": "stack"} assert client.get("salt", recurse=True) == {"salt": "stack"} tree_mock.assert_called_with("salt")
def wait_for_cluster(cluster_name, client, client_port=DEFAULT_CLIENT_PORT): """ Validates the health of the etcd cluster is healthy :param cluster_name: Name of the cluster :type cluster_name: str :param client: The client on which to validate the cluster :type client: SSHClient :param client_port: Port to be used by client :type client_port: int :return: None """ EtcdInstaller._logger.debug( 'Waiting for cluster "{0}"'.format(cluster_name)) tries = 10 healthy = EtcdInstaller._is_healty(cluster_name, client, client_port=client_port) while healthy is False and tries > 0: tries -= 1 time.sleep(10 - tries) healthy = EtcdInstaller._is_healty(cluster_name, client, client_port=client_port) if healthy is False: raise etcd.EtcdConnectionFailed( 'Etcd cluster "{0}" could not be started correctly'.format( cluster_name)) EtcdInstaller._logger.debug( 'Cluster "{0}" running'.format(cluster_name))
def _next_server(self): """ Selects the next server in the list, refreshes the server list. """ _log.debug("Selection next machine in cache. Available machines: %s", self._machines_cache) try: mach = self._machines_cache.pop() except IndexError: _log.error("Machines cache is empty, no machines to try.") raise etcd.EtcdConnectionFailed('No more machines in the cluster') else: _log.info("Selected new etcd server %s", mach) return mach
def test_watch(self): with patch('etcd.Client', autospec=True) as client_mock: client = etcd_util.EtcdClient({}) with patch.object(client, 'read', autospec=True) as mock: mock.return_value = MagicMock(value='stack', key='/some-key', modifiedIndex=1, dir=False) self.assertDictEqual(client.watch('/some-key'), {'value': 'stack', 'key': '/some-key', 'mIndex': 1, 'changed': True, 'dir': False}) mock.assert_called_with('/some-key', wait=True, recursive=False, timeout=0, waitIndex=None) mock.side_effect = iter([etcd_util.EtcdUtilWatchTimeout, mock.return_value]) self.assertDictEqual(client.watch('/some-key'), {'value': 'stack', 'changed': False, 'mIndex': 1, 'key': '/some-key', 'dir': False}) mock.side_effect = iter([etcd_util.EtcdUtilWatchTimeout, etcd.EtcdKeyNotFound]) self.assertEqual(client.watch('/some-key'), {'value': None, 'changed': False, 'mIndex': 0, 'key': '/some-key', 'dir': False}) mock.side_effect = iter([etcd_util.EtcdUtilWatchTimeout, ValueError]) self.assertEqual(client.watch('/some-key'), {}) mock.side_effect = None mock.return_value = MagicMock(value='stack', key='/some-key', modifiedIndex=1, dir=True) self.assertDictEqual(client.watch('/some-dir', recurse=True, timeout=5, index=10), {'value': 'stack', 'key': '/some-key', 'mIndex': 1, 'changed': True, 'dir': True}) mock.assert_called_with('/some-dir', wait=True, recursive=True, timeout=5, waitIndex=10) # iter(list(Exception)) works correctly with both mock<1.1 and mock>=1.1 mock.side_effect = iter([MaxRetryError(None, None)]) self.assertEqual(client.watch('/some-key'), {}) mock.side_effect = iter([etcd.EtcdConnectionFailed()]) self.assertEqual(client.watch('/some-key'), {}) mock.side_effect = None mock.return_value = None self.assertEqual(client.watch('/some-key'), {})
def api_execute(self, path, method, params=None, timeout=None): """ Executes the query. """ some_request_failed = False response = False if timeout is None: timeout = self.read_timeout if timeout == 0: timeout = None if not path.startswith('/'): raise ValueError('Path does not start with /') while not response: try: url = self._base_uri + path if (method == self._MGET) or (method == self._MDELETE): response = self.http.request(method, url, timeout=timeout, fields=params, redirect=self.allow_redirect, preload_content=False) elif (method == self._MPUT) or (method == self._MPOST): response = self.http.request_encode_body( method, url, fields=params, timeout=timeout, encode_multipart=False, redirect=self.allow_redirect, preload_content=False) else: raise etcd.EtcdException( 'HTTP method {} not supported'.format(method)) # urllib3 doesn't wrap all httplib exceptions and earlier versions # don't wrap socket errors either. except (urllib3.exceptions.HTTPError, HTTPException, socket.error) as e: _log.error("Request to server %s failed: %r", self._base_uri, e) if self._allow_reconnect: _log.info("Reconnection allowed, looking for another " "server.") # _next_server() raises EtcdException if there are no # machines left to try, breaking out of the loop. self._base_uri = self._next_server() some_request_failed = True else: _log.debug("Reconnection disabled, giving up.") raise etcd.EtcdConnectionFailed( "Connection to etcd failed due to %r" % e) except: _log.exception("Unexpected request failure, re-raising.") raise else: # Check the cluster ID hasn't changed under us. We use # preload_content=False above so we can read the headers # before we wait for the content of a long poll. cluster_id = response.getheader("x-etcd-cluster-id") id_changed = (self.expected_cluster_id and cluster_id is not None and cluster_id != self.expected_cluster_id) # Update the ID so we only raise the exception once. old_expected_cluster_id = self.expected_cluster_id self.expected_cluster_id = cluster_id if id_changed: # Defensive: clear the pool so that we connect afresh next # time. self.http.clear() raise etcd.EtcdClusterIdChanged( 'The UUID of the cluster changed from {} to ' '{}.'.format(old_expected_cluster_id, cluster_id)) if some_request_failed: if not self._use_proxies: # The cluster may have changed since last invocation self._machines_cache = self.machines self._machines_cache.remove(self._base_uri) return self._handle_server_response(response)
def wrapper(self, path, method, params=None, timeout=None): response = False if timeout is None: timeout = self.read_timeout if timeout == 0: timeout = None if not path.startswith('/'): raise ValueError('Path does not start with /') while not response: some_request_failed = False try: response = payload(self, path, method, params=params, timeout=timeout) # Check the cluster ID hasn't changed under us. We use # preload_content=False above so we can read the headers # before we wait for the content of a watch. self._check_cluster_id(response, path) # Now force the data to be preloaded in order to trigger any # IO-related errors in this method rather than when we try to # access it later. _ = response.data # urllib3 doesn't wrap all httplib exceptions and earlier versions # don't wrap socket errors either. except (HTTPError, HTTPException, socket.error) as e: if (isinstance(params, dict) and params.get("wait") == "true" and isinstance(e, ReadTimeoutError)): _log.debug("Watch timed out.") raise etcd.EtcdWatchTimedOut( "Watch timed out: %r" % e, cause=e ) _log.error("Request to server %s failed: %r", self._base_uri, e) if self._allow_reconnect: _log.info("Reconnection allowed, looking for another " "server.") # _next_server() raises EtcdException if there are no # machines left to try, breaking out of the loop. self._base_uri = self._next_server(cause=e) some_request_failed = True # if exception is raised on _ = response.data # the condition for while loop will be False # but we should retry response = False else: _log.debug("Reconnection disabled, giving up.") raise etcd.EtcdConnectionFailed( "Connection to etcd failed due to %r" % e, cause=e ) except etcd.EtcdClusterIdChanged as e: _log.warning(e) raise except: _log.debug("Unexpected request failure, re-raising.") raise if some_request_failed: if not self._use_proxies: # The cluster may have changed since last invocation self._machines_cache = self.machines self._machines_cache.remove(self._base_uri) return self._handle_server_response(response)
def api_execute(self, path, method, params=None, timeout=None): """ Executes the query. """ some_request_failed = False response = False if timeout is None: timeout = self.read_timeout if timeout == 0: timeout = None if not path.startswith('/'): raise ValueError('Path does not start with /') while not response: try: url = self._base_uri + path if (method == self._MGET) or (method == self._MDELETE): response = self.http.request(method, url, timeout=timeout, fields=params, redirect=self.allow_redirect, preload_content=False) elif (method == self._MPUT) or (method == self._MPOST): response = self.http.request_encode_body( method, url, fields=params, timeout=timeout, encode_multipart=False, redirect=self.allow_redirect, preload_content=False) else: raise etcd.EtcdException( 'HTTP method {} not supported'.format(method)) # Check the cluster ID hasn't changed under us. We use # preload_content=False above so we can read the headers # before we wait for the content of a watch. self._check_cluster_id(response) # Now force the data to be preloaded in order to trigger any # IO-related errors in this method rather than when we try to # access it later. _ = response.data # urllib3 doesn't wrap all httplib exceptions and earlier versions # don't wrap socket errors either. except (urllib3.exceptions.HTTPError, HTTPException, socket.error) as e: if (params.get("wait") == "true" and isinstance( e, urllib3.exceptions.ReadTimeoutError)): _log.debug("Watch timed out.") raise etcd.EtcdWatchTimedOut("Watch timed out: %r" % e, cause=e) _log.error("Request to server %s failed: %r", self._base_uri, e) if self._allow_reconnect: _log.info("Reconnection allowed, looking for another " "server.") # _next_server() raises EtcdException if there are no # machines left to try, breaking out of the loop. self._base_uri = self._next_server(cause=e) some_request_failed = True else: _log.debug("Reconnection disabled, giving up.") raise etcd.EtcdConnectionFailed( "Connection to etcd failed due to %r" % e, cause=e) except: _log.exception("Unexpected request failure, re-raising.") raise if some_request_failed: if not self._use_proxies: # The cluster may have changed since last invocation self._machines_cache = self.machines self._machines_cache.remove(self._base_uri) return self._handle_server_response(response)
def test_watch(self): with patch("etcd.Client", autospec=True) as client_mock: client = etcd_util.EtcdClient({}) with patch.object(client, "read", autospec=True) as mock: mock.return_value = MagicMock( value="stack", key="/some-key", modifiedIndex=1, dir=False ) self.assertDictEqual( client.watch("/some-key"), { "value": "stack", "key": "/some-key", "mIndex": 1, "changed": True, "dir": False, }, ) mock.assert_called_with( "/some-key", wait=True, recursive=False, timeout=0, waitIndex=None ) mock.side_effect = iter( [etcd_util.EtcdUtilWatchTimeout, mock.return_value] ) self.assertDictEqual( client.watch("/some-key"), { "value": "stack", "changed": False, "mIndex": 1, "key": "/some-key", "dir": False, }, ) mock.side_effect = iter( [etcd_util.EtcdUtilWatchTimeout, etcd.EtcdKeyNotFound] ) self.assertEqual( client.watch("/some-key"), { "value": None, "changed": False, "mIndex": 0, "key": "/some-key", "dir": False, }, ) mock.side_effect = iter([etcd_util.EtcdUtilWatchTimeout, ValueError]) self.assertEqual(client.watch("/some-key"), {}) mock.side_effect = None mock.return_value = MagicMock( value="stack", key="/some-key", modifiedIndex=1, dir=True ) self.assertDictEqual( client.watch("/some-dir", recurse=True, timeout=5, index=10), { "value": "stack", "key": "/some-key", "mIndex": 1, "changed": True, "dir": True, }, ) mock.assert_called_with( "/some-dir", wait=True, recursive=True, timeout=5, waitIndex=10 ) # iter(list(Exception)) works correctly with both mock<1.1 and mock>=1.1 mock.side_effect = iter([MaxRetryError(None, None)]) self.assertEqual(client.watch("/some-key"), {}) mock.side_effect = iter([etcd.EtcdConnectionFailed()]) self.assertEqual(client.watch("/some-key"), {}) mock.side_effect = None mock.return_value = None self.assertEqual(client.watch("/some-key"), {})
def test_watch(use_v2, client_name): with patch(client_name, autospec=True): client = etcd_util.get_conn( {"etcd.require_v2": use_v2, "etcd.encode_values": False} ) if use_v2: with patch.object(client, "read", autospec=True) as mock: mock.return_value = MagicMock( value="stack", key="/some-key", modifiedIndex=1, dir=False ) assert client.watch("/some-key") == { "value": "stack", "key": "/some-key", "mIndex": 1, "changed": True, "dir": False, } mock.assert_called_with( "/some-key", wait=True, recurse=False, timeout=0, start_revision=None, ) mock.side_effect = iter( [etcd_util.EtcdUtilWatchTimeout, mock.return_value] ) assert client.watch("/some-key") == { "value": "stack", "changed": False, "mIndex": 1, "key": "/some-key", "dir": False, } mock.side_effect = iter( [etcd_util.EtcdUtilWatchTimeout, etcd.EtcdKeyNotFound] ) assert client.watch("/some-key") == { "value": None, "changed": False, "mIndex": 0, "key": "/some-key", "dir": False, } mock.side_effect = iter([etcd_util.EtcdUtilWatchTimeout, ValueError]) assert client.watch("/some-key") == {} mock.side_effect = None mock.return_value = MagicMock( value="stack", key="/some-key", modifiedIndex=1, dir=True ) assert client.watch( "/some-dir", recurse=True, timeout=5, start_revision=10 ) == { "value": "stack", "key": "/some-key", "mIndex": 1, "changed": True, "dir": True, } mock.assert_called_with( "/some-dir", wait=True, recurse=True, timeout=5, start_revision=10 ) # iter(list(Exception)) works correctly with both mock<1.1 and mock>=1.1 mock.side_effect = iter([MaxRetryError(None, None)]) assert client.watch("/some-key") == {} mock.side_effect = iter([etcd.EtcdConnectionFailed()]) assert client.watch("/some-key") is None mock.side_effect = None mock.return_value = None assert client.watch("/some-key") == {} else: with patch.object(client, "read", autospec=True) as mock: mock.return_value = MagicMock( value="stack", key="/some-key", mod_revision=1 ) assert client.watch("/some-key") == { "value": "stack", "key": "/some-key", "mIndex": 1, "changed": True, "dir": False, } mock.assert_called_with( "/some-key", wait=True, recurse=False, timeout=0, start_revision=None, ) mock.return_value = MagicMock( value="stack", key="/some-key", mod_revision=1 ) assert client.watch( "/some-key", recurse=True, timeout=5, start_revision=10 ) == { "value": "stack", "key": "/some-key", "mIndex": 1, "changed": True, "dir": False, } mock.assert_called_with( "/some-key", wait=True, recurse=True, timeout=5, start_revision=10 ) mock.side_effect = None mock.return_value = None assert client.watch("/some-key") is None
def read(self, key, recurse=False, wait=False, timeout=None, start_revision=None, **kwargs): recursive = kwargs.pop("recursive", None) wait_index = kwargs.pop("waitIndex", None) if recursive is not None: salt.utils.versions.warn_until( "Argon", "The recursive kwarg has been deprecated, and will be removed " "in the Argon release. Please use recurse instead.", ) recurse = recursive if wait_index is not None: salt.utils.versions.warn_until( "Argon", "The waitIndex kwarg has been deprecated, and will be removed " "in the Argon release. Please use start_revision instead.", ) start_revision = wait_index if kwargs: log.warning("Invalid kwargs passed in will not be used: %s", kwargs) try: if start_revision: result = self.client.read( key, recursive=recurse, wait=wait, timeout=timeout, waitIndex=start_revision, ) else: result = self.client.read(key, recursive=recurse, wait=wait, timeout=timeout) except (etcd.EtcdConnectionFailed, etcd.EtcdKeyNotFound) as err: log.error("etcd: %s", err) raise except ReadTimeoutError: # For some reason, we have to catch this directly. It falls through # from python-etcd because it's trying to catch # urllib3.exceptions.ReadTimeoutError and strangely, doesn't catch. # This can occur from a watch timeout that expires, so it may be 'expected' # behavior. See issue #28553 if wait: # Wait timeouts will throw ReadTimeoutError, which isn't bad log.debug("etcd: Timed out while executing a wait") raise EtcdUtilWatchTimeout("Watch on {} timed out".format(key)) log.error("etcd: Timed out") raise etcd.EtcdConnectionFailed("Connection failed") except MaxRetryError as err: # Same issue as ReadTimeoutError. When it 'works', python-etcd # throws EtcdConnectionFailed, so we'll do that for it. log.error("etcd: Could not connect") raise etcd.EtcdConnectionFailed("Could not connect to etcd server") except etcd.EtcdException as err: # EtcdValueError inherits from ValueError, so we don't want to accidentally # catch this below on ValueError and give a bogus error message log.error("etcd: %s", err) raise except ValueError: # python-etcd doesn't fully support python 2.6 and ends up throwing this for *any* exception because # it uses the newer {} format syntax log.error( "etcd: error. python-etcd does not fully support python 2.6, no error" " information available") raise except Exception as err: # pylint: disable=broad-except log.error("etcd: uncaught exception %s", err) raise return result