def on_get(self, req, resp): """ Handles GET requests for Hosts. :param req: Request instance that will be passed through. :type req: falcon.Request :param resp: Response instance that will be passed through. :type resp: falcon.Response """ try: hosts_dir = self.store.get('/commissaire/hosts/') self.logger.debug('Etcd Response: {0}'.format(hosts_dir)) except etcd.EtcdKeyNotFound: self.logger.warn( 'Etcd does not have any hosts. Returning [] and 404.') resp.status = falcon.HTTP_404 req.context['model'] = None return results = [] # Don't let an empty host directory through if len(hosts_dir._children): for host in hosts_dir.leaves: results.append(Host(**json.loads(host.value))) resp.status = falcon.HTTP_200 req.context['model'] = Hosts(hosts=results) else: self.logger.debug( 'Etcd has a hosts directory but no content.') resp.status = falcon.HTTP_200 req.context['model'] = None
def _calculate_hosts(self, cluster): """ Calculates the hosts metadata for the cluster. :param cluster: The name of the cluster. :type cluster: str """ # XXX: Not sure which wil be more efficient: fetch all # the host data in one etcd call and sort through # them, or fetch the ones we need individually. # For the MVP phase, fetch all is better. etcd_resp, error = cherrypy.engine.publish('store-get', '/commissaire/hosts')[0] if error: self.logger.warn('Etcd does not have any hosts. ' 'Cannot determine cluster stats.') return available = unavailable = total = 0 for child in etcd_resp._children: host = Host(**json.loads(child['value'])) if host.address in cluster.hostset: total += 1 if host.status == 'active': available += 1 else: unavailable += 1 cluster.hosts['total'] = total cluster.hosts['available'] = available cluster.hosts['unavailable'] = unavailable
def test_investigator(self): """ Verify the investigator. """ with mock.patch('commissaire.transport.ansibleapi.Transport') as _tp: _tp().get_info.return_value = ( 0, { 'os': 'fedora', 'cpus': 2, 'memory': 11989228, 'space': 487652, } ) _tp().bootstrap.return_value = (0, {}) request_queue = Queue() response_queue = MagicMock(Queue) to_investigate = { 'address': '10.0.0.2', 'ssh_priv_key': 'dGVzdAo=', 'remote_user': '******' } manager = MagicMock(StoreHandlerManager) manager.get.return_value = Host(**json.loads(self.etcd_host)) request_queue.put_nowait(( manager, to_investigate, Cluster.new().__dict__)) investigator(request_queue, response_queue, run_once=True) # Investigator saves *after* bootstrapping. self.assertEquals(0, manager.save.call_count) self.assertEquals(1, response_queue.put.call_count) host, error = response_queue.put.call_args[0][0] self.assertEquals(host.status, 'inactive') self.assertIsNone(error)
def on_get(self, req, resp, address): """ Handles retrieval of an existing Host. :param req: Request instance that will be passed through. :type req: falcon.Request :param resp: Response instance that will be passed through. :type resp: falcon.Response :param address: The address of the Host being requested. :type address: str """ # TODO: Verify input try: etcd_resp = self.store.get(util.etcd_host_key(address)) self.logger.debug('Etcd Response: {0}'.format(etcd_resp)) except etcd.EtcdKeyNotFound: resp.status = falcon.HTTP_404 return resp.status = falcon.HTTP_200 req.context['model'] = Host(**json.loads(etcd_resp.value))
def on_get(self, req, resp, address): """ Handles retrieval of an existing Host. :param req: Request instance that will be passed through. :type req: falcon.Request :param resp: Response instance that will be passed through. :type resp: falcon.Response :param address: The address of the Host being requested. :type address: str """ # TODO: Verify input etcd_resp, error = cherrypy.engine.publish( 'store-get', util.etcd_host_key(address))[0] self.logger.debug('Etcd Response: {0}'.format(etcd_resp)) if error: resp.status = falcon.HTTP_404 return resp.status = falcon.HTTP_200 req.context['model'] = Host(**json.loads(etcd_resp.value))
def on_put(self, req, resp, address): """ Handles the creation of a new Host. :param req: Request instance that will be passed through. :type req: falcon.Request :param resp: Response instance that will be passed through. :type resp: falcon.Response :param address: The address of the Host being requested. :type address: str """ try: # Extract what we need from the input data. # Don't treat it as a skeletal host record. req_data = req.stream.read() req_body = json.loads(req_data.decode()) ssh_priv_key = req_body['ssh_priv_key'] # Cluster member is optional. cluster_name = req_body.get('cluster', None) except (KeyError, ValueError): self.logger.info( 'Bad client PUT request for host {0}: {1}'. format(address, req_data)) resp.status = falcon.HTTP_400 return key = util.etcd_host_key(address) try: etcd_resp = self.store.get(key) self.logger.debug('Etcd Response: {0}'.format(etcd_resp)) # Check if the request conflicts with the existing host. existing_host = Host(**json.loads(etcd_resp.value)) if existing_host.ssh_priv_key != ssh_priv_key: resp.status = falcon.HTTP_409 return if cluster_name: try: assert util.etcd_cluster_has_host( self.store, cluster_name, address) except (AssertionError, KeyError): resp.status = falcon.HTTP_409 return # Request is compatible with the existing host, so # we're done. (Not using HTTP_201 since we didn't # actually create anything.) resp.status = falcon.HTTP_200 req.context['model'] = existing_host return except etcd.EtcdKeyNotFound: pass host_creation = { 'address': address, 'ssh_priv_key': ssh_priv_key, 'os': '', 'status': 'investigating', 'cpus': -1, 'memory': -1, 'space': -1, 'last_check': None } # Verify the cluster exists, if given. Do it now # so we can fail before writing anything to etcd. if cluster_name: if not util.etcd_cluster_exists(self.store, cluster_name): resp.status = falcon.HTTP_409 return host = Host(**host_creation) new_host = self.store.set(key, host.to_json(secure=True)) INVESTIGATE_QUEUE.put((host_creation, ssh_priv_key)) # Add host to the requested cluster. if cluster_name: util.etcd_cluster_add_host(self.store, cluster_name, address) resp.status = falcon.HTTP_201 req.context['model'] = Host(**json.loads(new_host.value))
# Copyright (C) 2016 Red Hat, Inc # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ Host(s) handlers. """ import falcon import etcd import json from commissaire.queues import INVESTIGATE_QUEUE from commissaire.resource import Resource from commissaire.handlers.models import Cluster, Host, Hosts import commissaire.handlers.util as util class HostsResource(Resource): """ Resource for working with Hosts.
def etcd_host_create(address, ssh_priv_key, cluster_name=None): """ Creates a new host record in etcd and optionally adds the host to the specified cluster. Returns a (status, host) tuple where status is the Falcon HTTP status and host is a Host model instance, which may be None if an error occurred. This function is idempotent so long as the host parameters agree with an existing host record and cluster membership. :param address: Host address. :type address: str :param ssh_priv_key: Host's SSH key, base64-encoded. :type ssh_priv_key: str :param cluster_name: Name of the cluster to join, or None :type cluster_name: str or None :return: (status, host) :rtype: tuple """ key = etcd_host_key(address) etcd_resp, error = cherrypy.engine.publish('store-get', key)[0] if not error: # Check if the request conflicts with the existing host. existing_host = Host(**json.loads(etcd_resp.value)) if existing_host.ssh_priv_key != ssh_priv_key: return (falcon.HTTP_409, None) if cluster_name: try: assert etcd_cluster_has_host(cluster_name, address) except (AssertionError, KeyError): return (falcon.HTTP_409, None) # Request is compatible with the existing host, so # we're done. (Not using HTTP_201 since we didn't # actually create anything.) return (falcon.HTTP_200, existing_host) host_creation = { 'address': address, 'ssh_priv_key': ssh_priv_key, 'os': '', 'status': 'investigating', 'cpus': -1, 'memory': -1, 'space': -1, 'last_check': None } # Verify the cluster exists, if given. Do it now # so we can fail before writing anything to etcd. if cluster_name: if not etcd_cluster_exists(cluster_name): return (falcon.HTTP_409, None) host = Host(**host_creation) new_host, _ = cherrypy.engine.publish( 'store-save', key, host.to_json(secure=True))[0] # Add host to the requested cluster. if cluster_name: etcd_cluster_add_host(cluster_name, address) INVESTIGATE_QUEUE.put((host_creation, ssh_priv_key)) return (falcon.HTTP_201, Host(**json.loads(new_host.value)))