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
        """
        # If the host is still bootstrapping, the store handler
        # won't find it yet.  So respond with a fake host status.
        if cherrypy.engine.publish('investigator-is-pending', address)[0]:
            host = Host.new(address=address, status='investigating')
            resp.status = falcon.HTTP_200
            req.context['model'] = host
            return

        # TODO: Verify input
        try:
            store_manager = cherrypy.engine.publish('get-store-manager')[0]
            # TODO: use some kind of global default for Hosts
            host = store_manager.get(Host.new(address=address))
            resp.status = falcon.HTTP_200
            req.context['model'] = host
        except:
            resp.status = falcon.HTTP_404
            return
Exemple #2
0
    def on_get(self, req, resp, address):
        """
        Handles retrieval of existing Host credentials.

        :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
        # TODO: Decide if this should be a model or if it makes sense to
        #       stay a subset off of Host and bypass the req.context
        #       middleware system.
        try:
            store_manager = cherrypy.engine.publish('get-store-manager')[0]
            host = store_manager.get(Host.new(address=address))
            resp.status = falcon.HTTP_200
            body = {
                'ssh_priv_key': host.ssh_priv_key,
                'remote_user': host.remote_user or 'root',
            }
            resp.body = json.dumps(body)
        except:
            resp.status = falcon.HTTP_404
            return
Exemple #3
0
    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 on_get(self, req, resp, address):
        """
        Handles retrieval of existing Host credentials.

        :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
        # TODO: Decide if this should be a model or if it makes sense to
        #       stay a subset off of Host and bypass the req.context
        #       middleware system.
        try:
            store_manager = cherrypy.engine.publish('get-store-manager')[0]
            host = store_manager.get(Host.new(address=address))
            resp.status = falcon.HTTP_200
            body = {
                'ssh_priv_key': host.ssh_priv_key,
                'remote_user': host.remote_user or 'root',
            }
            resp.body = json.dumps(body)
        except:
            resp.status = falcon.HTTP_404
            return
Exemple #5
0
    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 test__format_key_with_primary_key(self):
     """
     Verify etcd keys are generated correctly whith a primary key.
     """
     self.assertEquals(
         '/commissaire/hosts/10.0.0.1',
         self.instance._format_key(
             Host.new(
                 address='10.0.0.1', status='', os='', cpus=2,
                 memory=1024, space=1000, last_check='',
                 ssh_priv_key='', remote_user='')))
Exemple #7
0
    def on_delete(self, req, resp, address):
        """
        Handles the Deletion of a 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
        """
        resp.body = "{}"
        try:
            Host.delete(address)
            resp.status = falcon.HTTP_200
        except:
            resp.status = falcon.HTTP_404

        # Also remove the host from all clusters.
        # Note: We've done all we need to for the host deletion,
        #       so if an error occurs from here just log it and
        #       return.
        try:
            clusters = Clusters.retrieve()
        except:
            self.logger.warn("Etcd does not have any clusters")
            return
        try:
            for cluster_name in clusters.clusters:
                self.logger.debug("Checking cluster {0}".format(cluster_name))
                cluster = Cluster.retrieve(cluster_name)
                if address in cluster.hostset:
                    self.logger.info("Removing {0} from cluster {1}".format(address, cluster_name))
                    cluster.hostset.remove(address)
                    cluster.save(cluster_name)
                    self.logger.info("{0} has been removed from cluster {1}".format(address, cluster_name))
        except:
            self.logger.warn("Failed to remove {0} from cluster {1}".format(address, cluster_name))
    def test__dispatch(self):
        """
        Verify dispatching of operations works properly.
        """
        # Test namespace
        self.instance._save_on_namespace = mock.MagicMock()
        self.instance._dispatch('save', Cluster.new(name='test'))
        self.instance._save_on_namespace.assert_called_once()

        self.instance._get_on_namespace = mock.MagicMock()
        self.instance._dispatch('get', Cluster.new(name='test'))
        self.instance._get_on_namespace.assert_called_once()

        self.instance._delete_on_namespace = mock.MagicMock()
        self.instance._dispatch('delete', Cluster.new(name='test'))
        self.instance._delete_on_namespace.assert_called_once()

        self.instance._list_on_namespace = mock.MagicMock()
        self.instance._dispatch('list', Cluster.new(name='test'))
        self.instance._list_on_namespace.assert_called_once()

        # Test host
        self.instance._save_host = mock.MagicMock()
        self.instance._dispatch('save', Host.new(name='test'))
        self.instance._save_host.assert_called_once()

        self.instance._get_host = mock.MagicMock()
        self.instance._dispatch('get', Host.new(name='test'))
        self.instance._get_host.assert_called_once()

        self.instance._delete_host = mock.MagicMock()
        self.instance._dispatch('delete', Host.new(name='test'))
        self.instance._delete_host.assert_called_once()

        self.instance._list_host = mock.MagicMock()
        self.instance._dispatch('list', Host.new(name='test'))
        self.instance._list_host.assert_called_once()
Exemple #9
0
    def test__dispatch(self):
        """
        Verify dispatching of operations works properly.
        """
        # Test namespace
        self.instance._save_on_namespace = mock.MagicMock()
        self.instance._dispatch('save', Cluster.new(name='test'))
        self.instance._save_on_namespace.assert_called_once()

        self.instance._get_on_namespace = mock.MagicMock()
        self.instance._dispatch('get', Cluster.new(name='test'))
        self.instance._get_on_namespace.assert_called_once()

        self.instance._delete_on_namespace = mock.MagicMock()
        self.instance._dispatch('delete', Cluster.new(name='test'))
        self.instance._delete_on_namespace.assert_called_once()

        self.instance._list_on_namespace = mock.MagicMock()
        self.instance._dispatch('list', Cluster.new(name='test'))
        self.instance._list_on_namespace.assert_called_once()

        # Test host
        self.instance._save_host = mock.MagicMock()
        self.instance._dispatch('save', Host.new(name='test'))
        self.instance._save_host.assert_called_once()

        self.instance._get_host = mock.MagicMock()
        self.instance._dispatch('get', Host.new(name='test'))
        self.instance._get_host.assert_called_once()

        self.instance._delete_host = mock.MagicMock()
        self.instance._dispatch('delete', Host.new(name='test'))
        self.instance._delete_host.assert_called_once()

        self.instance._list_host = mock.MagicMock()
        self.instance._dispatch('list', Host.new(name='test'))
        self.instance._list_host.assert_called_once()
    def on_delete(self, req, resp, address):
        """
        Handles the Deletion of a 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
        """
        resp.body = '{}'
        store_manager = cherrypy.engine.publish('get-store-manager')[0]
        try:
            host = Host.new(address=address)
            WATCHER_QUEUE.dequeue(host)
            store_manager.delete(host)
            self.logger.debug(
                'Deleted host {0} and dequeued it from the watcher.'.format(
                    host.address))
            resp.status = falcon.HTTP_200
        except:
            resp.status = falcon.HTTP_404

        # Also remove the host from all clusters.
        # Note: We've done all we need to for the host deletion,
        #       so if an error occurs from here just log it and
        #       return.
        try:
            clusters = store_manager.list(Clusters(clusters=[]))
        except:
            self.logger.warn('Store does not have any clusters')
            return
        for cluster in clusters.clusters:
            try:
                self.logger.debug('Checking cluster {0}'.format(cluster.name))
                if address in cluster.hostset:
                    self.logger.info('Removing {0} from cluster {1}'.format(
                        address, cluster.name))
                    cluster.hostset.remove(address)
                    store_manager.save(cluster)
                    self.logger.info(
                        '{0} has been removed from cluster {1}'.format(
                            address, cluster.name))
            except:
                self.logger.warn(
                    'Failed to remove {0} from cluster {1}'.format(
                        address, cluster.name))
 def test__format_key_with_primary_key(self):
     """
     Verify etcd keys are generated correctly whith a primary key.
     """
     self.assertEquals(
         '/commissaire/hosts/10.0.0.1',
         self.instance._format_key(
             Host.new(address='10.0.0.1',
                      status='',
                      os='',
                      cpus=2,
                      memory=1024,
                      space=1000,
                      last_check='',
                      ssh_priv_key='',
                      remote_user='')))
Exemple #12
0
    def on_delete(self, req, resp, address):
        """
        Handles the Deletion of a 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
        """
        resp.body = '{}'
        store_manager = cherrypy.engine.publish('get-store-manager')[0]
        try:
            # TODO: use some kind of global default for Hosts
            store_manager.delete(Host.new(address=address))
            resp.status = falcon.HTTP_200
        except:
            resp.status = falcon.HTTP_404

        # Also remove the host from all clusters.
        # Note: We've done all we need to for the host deletion,
        #       so if an error occurs from here just log it and
        #       return.
        try:
            clusters = store_manager.list(Clusters(clusters=[]))
        except:
            self.logger.warn('Store does not have any clusters')
            return
        for cluster in clusters.clusters:
            try:
                self.logger.debug(
                    'Checking cluster {0}'.format(cluster.name))
                if address in cluster.hostset:
                    self.logger.info(
                        'Removing {0} from cluster {1}'.format(
                            address, cluster.name))
                    cluster.hostset.remove(address)
                    store_manager.save(cluster)
                    self.logger.info(
                        '{0} has been removed from cluster {1}'.format(
                            address, cluster.name))
            except:
                self.logger.warn(
                    'Failed to remove {0} from cluster {1}'.format(
                        address, cluster.name))
Exemple #13
0
    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:
            host = Host.retrieve(address)
            resp.status = falcon.HTTP_200
            req.context["model"] = host
        except:
            resp.status = falcon.HTTP_404
            return
    def _list_host(self, model_instance):
        """
        Lists data at a location in a store and returns back model instances.

        :param model_instance: Model instance to search for and list
        :type model_instance: commissaire.model.Model
        :returns: A list of models
        :rtype: list
        """
        hosts = []
        path = _model_mapper[model_instance.__class__.__name__]
        items = self._store.get(self._endpoint + path).json()
        for item in items.get('items'):
            try:
                hosts.append(self._format_model(item, Host.new(), True))
            except (TypeError, KeyError):
                # TODO: Add logging
                pass

        return Hosts.new(hosts=hosts)
    def _list_host(self, model_instance):
        """
        Lists data at a location in a store and returns back model instances.

        :param model_instance: Model instance to search for and list
        :type model_instance: commissaire.model.Model
        :returns: A list of models
        :rtype: list
        """
        hosts = []
        path = _model_mapper[model_instance.__class__.__name__]
        items = self._store.get(self._endpoint + path).json()
        for item in items.get('items'):
            try:
                hosts.append(self._format_model(item, Host.new(), True))
            except (TypeError, KeyError):
                # TODO: Add logging
                pass

        return Hosts.new(hosts=hosts)
Exemple #16
0
    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)
Exemple #17
0
    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))
Exemple #18
0
    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:
            store_manager = cherrypy.engine.publish('get-store-manager')[0]
            # TODO: use some kind of global default for Hosts
            host = store_manager.get(Host.new(address=address))
            resp.status = falcon.HTTP_200
            req.context['model'] = host
        except:
            resp.status = falcon.HTTP_404
            return
Exemple #19
0
    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))
Exemple #20
0
    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))
Exemple #21
0
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)))
Exemple #22
0
# 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.
Exemple #23
0
def etcd_host_create(address, ssh_priv_key, remote_user, 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 remote_user: The user to use with SSH.
    :type remote_user: 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,
        'remote_user': remote_user,
    }

    # 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, remote_user))

    return (falcon.HTTP_201, Host(**json.loads(new_host.value)))
def investigator(queue, config, run_once=False):
    """
    Investigates new hosts to retrieve and store facts.

    :param queue: Queue to pull work from.
    :type queue: Queue.Queue
    :param config: Configuration information.
    :type config: commissaire.config.Config
    """
    logger = logging.getLogger('investigator')
    logger.info('Investigator started')

    while True:
        # Statuses follow:
        # http://commissaire.readthedocs.org/en/latest/enums.html#host-statuses
        store_manager, to_investigate, ssh_priv_key, remote_user = queue.get()
        address = to_investigate['address']
        logger.info('{0} is now in investigating.'.format(address))
        logger.debug(
            'Investigation details: key={0}, data={1}, remote_user={2}'.format(
                to_investigate, ssh_priv_key, remote_user))

        transport = ansibleapi.Transport(remote_user)

        try:
            host = store_manager.get(
                Host(
                    address=address,
                    status='',
                    os='',
                    cpus=0,
                    memory=0,
                    space=0,
                    last_check='',
                    ssh_priv_key='',
                    remote_user=''))
            key = TemporarySSHKey(host, logger)
            key.create()
        except Exception as error:
            logger.warn(
                'Unable to continue for {0} due to '
                '{1}: {2}. Returning...'.format(address, type(error), error))
            key.remove()
            continue

        try:
            result, facts = transport.get_info(address, key.path)
            # recreate the host instance with new data
            data = json.loads(host.to_json(secure=True))
            data.update(facts)
            host = Host(**data)
            host.last_check = datetime.datetime.utcnow().isoformat()
            host.status = 'bootstrapping'
            logger.info('Facts for {0} retrieved'.format(address))
            logger.debug('Data: {0}'.format(host.to_json()))
        except:
            exc_type, exc_msg, tb = sys.exc_info()
            logger.warn('Getting info failed for {0}: {1}'.format(
                address, exc_msg))
            host.status = 'failed'
            store_manager.save(host)
            key.remove()
            if run_once:
                break
            continue

        store_manager.save(host)
        logger.info(
            'Finished and stored investigation data for {0}'.format(address))
        logger.debug('Finished investigation update for {0}: {1}'.format(
            address, host.to_json()))

        logger.info('{0} is now in bootstrapping'.format(address))
        oscmd = get_oscmd(host.os)
        try:
            result, facts = transport.bootstrap(
                address, key.path, config, oscmd, store_manager)
            host.status = 'inactive'
            store_manager.save(host)
        except:
            exc_type, exc_msg, tb = sys.exc_info()
            logger.warn('Unable to start bootstraping for {0}: {1}'.format(
                address, exc_msg))
            host.status = 'disassociated'
            store_manager.save(host)
            key.remove()
            if run_once:
                break
            continue

        host.status = cluster_type = C.CLUSTER_TYPE_HOST
        try:
            cluster = util.cluster_for_host(address, store_manager)
            cluster_type = cluster.type
        except KeyError:
            # Not part of a cluster
            pass

        # Verify association with the container manager
        if cluster_type == C.CLUSTER_TYPE_KUBERNETES:
            try:
                container_mgr = KubeContainerManager(config)
                # Try 3 times waiting 5 seconds each time before giving up
                for cnt in range(0, 3):
                    if container_mgr.node_registered(address):
                        logger.info(
                            '{0} has been registered with the '
                            'container manager.'.format(address))
                        host.status = 'active'
                        break
                    if cnt == 3:
                        msg = 'Could not register with the container manager'
                        logger.warn(msg)
                        raise Exception(msg)
                    logger.debug(
                        '{0} has not been registered with the container '
                        ' manager. Checking again in 5 seconds...'.format(
                            address))
                    sleep(5)
            except:
                _, exc_msg, _ = sys.exc_info()
                logger.warn(
                    'Unable to finish bootstrap for {0} while associating '
                    'with the container manager: {1}'.format(
                        address, exc_msg))
                host.status = 'inactive'

        store_manager.save(host)
        logger.info(
            'Finished bootstrapping for {0}'.format(address))
        logging.debug('Finished bootstrapping for {0}: {1}'.format(
            address, host.to_json()))

        key.remove()
        if run_once:
            logger.info('Exiting due to run_once request.')
            break

    logger.info('Investigator stopping')
    def on_get(self, req, resp, address):
        """
        Handles retrieval of existing Host status.

        :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:
            store_manager = cherrypy.engine.publish('get-store-manager')[0]
            host = store_manager.get(Host.new(address=address))
            self.logger.debug('StatusHost found host {0}'.format(host.address))
            status = HostStatus.new(host={
                'last_check': host.last_check,
                'status': host.status,
            })

            try:
                resp.status = falcon.HTTP_200
                cluster = util.cluster_for_host(host.address, store_manager)
                status.type = cluster.type
                self.logger.debug('Cluster type for {0} is {1}'.format(
                    host.address, status.type))

                if status.type != C.CLUSTER_TYPE_HOST:
                    try:
                        container_mgr = store_manager.list_container_managers(
                            cluster.type)[0]
                    except Exception as error:
                        self.logger.error(
                            'StatusHost for host {0} did not find a '
                            'container_mgr: {1}: {2}'.format(
                                host.address, type(error), error))
                        raise error
                    self.logger.debug(
                        'StatusHost for host {0} got container_mgr '
                        'instance {1}'.format(host.address,
                                              type(container_mgr)))

                    is_raw = req.get_param_as_bool('raw') or False
                    self.logger.debug(
                        'StatusHost raw={0} found host {0} will'.format(
                            is_raw, host.address))

                    status_code, result = container_mgr.get_host_status(
                        host.address, is_raw)

                    # If we have a raw request ..
                    if is_raw:
                        # .. forward the http status as well or fall back to
                        # service unavailable
                        resp.status = getattr(
                            falcon.status_codes,
                            'HTTP_{0}'.format(status_code),
                            falcon.status_codes.HTTP_SERVICE_UNAVAILABLE)
                    status.container_manager = result
                else:
                    # Raise to be caught in host only type
                    raise KeyError
            except KeyError:
                # The host is not in a cluster.
                self.logger.info(
                    'Host {0} is not in a cluster. Defaulting to {1}'.format(
                        host.address, C.CLUSTER_TYPE_HOST))
                status.type = C.CLUSTER_TYPE_HOST

            self.logger.debug(
                'StatusHost end status code: {0} json={1}'.format(
                    resp.status, status.to_json()))

        except Exception as ex:
            self.logger.debug(
                'Host Status exception caught for {0}: {1}:{2}'.format(
                    host.address, type(ex), ex))
            resp.status = falcon.HTTP_404
            return

        self.logger.debug('Status for {0}: {1}'.format(host.address,
                                                       status.to_json()))

        req.context['model'] = status
Exemple #26
0
def etcd_host_create(address, ssh_priv_key, remote_user, 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 remote_user: The user to use with SSH.
    :type remote_user: str
    :param cluster_name: Name of the cluster to join, or None
    :type cluster_name: str or None
    :return: (status, host)
    :rtype: tuple
    """
    store_manager = cherrypy.engine.publish('get-store-manager')[0]

    try:
        # Check if the request conflicts with the existing host.
        existing_host = store_manager.get(Host.new(address=address))
        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)
    except:
        pass

    host_creation = Host.new(
        address=address,
        ssh_priv_key=ssh_priv_key,
        status='investigating',
        remote_user=remote_user
    ).__dict__

    # 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 = store_manager.save(host)

    # Add host to the requested cluster.
    if cluster_name:
        etcd_cluster_add_host(cluster_name, address)

    manager_clone = store_manager.clone()
    job_request = (manager_clone, host_creation, ssh_priv_key, remote_user)
    INVESTIGATE_QUEUE.put(job_request)

    return (falcon.HTTP_201, new_host)
Exemple #27
0
def etcd_host_create(address, ssh_priv_key, remote_user, 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 remote_user: The user to use with SSH.
    :type remote_user: str
    :param cluster_name: Name of the cluster to join, or None
    :type cluster_name: str or None
    :return: (status, host)
    :rtype: tuple
    """
    store_manager = cherrypy.engine.publish('get-store-manager')[0]

    try:
        # Check if the request conflicts with the existing host.
        existing_host = store_manager.get(Host.new(address=address))
        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)
    except:
        pass

    # Verify the cluster exists, if given.  Do it now
    # so we can fail before writing anything to etcd.
    if cluster_name:
        cluster = get_cluster_model(cluster_name)
        if cluster is None:
            return (falcon.HTTP_409, None)
    else:
        cluster = None

    host = Host.new(
        address=address,
        ssh_priv_key=ssh_priv_key,
        status='investigating',
        remote_user=remote_user)

    def callback(store_manager, host, exception):
        if exception is None:
            store_manager.save(host)

            # Add host to the requested cluster.
            if cluster_name:
                etcd_cluster_add_host(cluster_name, host.address)

    cherrypy.engine.publish(
        'investigator-submit', store_manager, host, cluster, callback)

    return (falcon.HTTP_201, host)
Exemple #28
0
    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
        """
        # TODO: Verify input
        try:
            host = self.store.get("/commissaire/hosts/{0}".format(address))
            resp.status = falcon.HTTP_409
            return
        except etcd.EtcdKeyNotFound:
            pass

        data = req.stream.read().decode()
        host_creation = json.loads(data)
        ssh_priv_key = host_creation["ssh_priv_key"]
        host_creation["address"] = address
        host_creation["os"] = ""
        host_creation["status"] = "investigating"
        host_creation["cpus"] = -1
        host_creation["memory"] = -1
        host_creation["space"] = -1
        host_creation["last_check"] = None

        # Don't store the cluster name in etcd.
        cluster_name = host_creation.pop("cluster", None)

        # Verify the cluster exists, if given.  Do it now
        # so we can fail before writing anything to etcd.
        if cluster_name:
            # XXX: Based on ClusterSingleHostResource.on_put().
            #      Add a util module to share common operations.
            cluster_key = "/commissaire/clusters/{0}".format(cluster_name)
            try:
                etcd_resp = self.store.get(cluster_key)
                self.logger.info("Request for cluster {0}".format(cluster_name))
                self.logger.debug("{0}".format(etcd_resp))
            except etcd.EtcdKeyNotFound:
                self.logger.info("Request for non-existent cluster {0}.".format(cluster_name))
                resp.status = falcon.HTTP_409
                return
            cluster = Cluster(**json.loads(etcd_resp.value))
            hostset = set(cluster.hostset)
            hostset.add(address)  # Ensures no duplicates
            cluster.hostset = list(hostset)

        host = Host(**host_creation)
        new_host = self.store.set("/commissaire/hosts/{0}".format(address), host.to_json(secure=True))
        INVESTIGATE_QUEUE.put((host_creation, ssh_priv_key))

        # Add host to the requested cluster.
        if cluster_name:
            # FIXME: Should guard against races here, since we're fetching
            #        the cluster record and writing it back with some parts
            #        unmodified.  Use either locking or a conditional write
            #        with the etcd 'modifiedIndex'.  Deferring for now.
            self.store.set(cluster_key, cluster.to_json(secure=True))

        resp.status = falcon.HTTP_201
        req.context["model"] = Host(**json.loads(new_host.value))
                     ' "cpus": 0, "memory": 0, "space": 0,'
                     ' "last_check": ""}')
#: Response JSON for a newly created implicit host (no address given)
INITIAL_IMPLICIT_HOST_JSON = ('{"address": "127.0.0.1",'
                              ' "status": "investigating", "os": "",'
                              ' "cpus": 0, "memory": 0, "space": 0,'
                              ' "last_check": ""}')
#: Credential JSON for tests
HOST_CREDS_JSON = '{"remote_user": "******", "ssh_priv_key": "dGVzdAo="}'
#: HostStatus JSON for tests
HOST_STATUS_JSON = (
    '{"type": "host_only", "container_manager": {}, "commissaire": '
    '{"status": "available", "last_check": "2016-07-29T20:39:50.529454"}}')
#: Host model for most tests
HOST = Host.new(ssh_priv_key='dGVzdAo=',
                remote_user='******',
                **json.loads(HOST_JSON))
#: HostStatus model for most tests
HOST_STATUS = HostStatus.new(**json.loads(HOST_STATUS_JSON))
#: Hosts model for most tests
HOSTS = Hosts.new(hosts=[HOST])
#: Cluster model for most tests
CLUSTER = Cluster.new(
    name='cluster',
    status='ok',
    hostset=[],
)
#: Cluster model with HOST for most tests
CLUSTER_WITH_HOST = Cluster.new(
    name='cluster',
    status='ok',
    Returns a new deepcopy of an instance.
    """
    return copy.deepcopy(instance)


#: Response JSON for a single host
HOST_JSON = (
    '{"address": "10.2.0.2",'
    ' "status": "available", "os": "atomic",'
    ' "cpus": 2, "memory": 11989228, "space": 487652,'
    ' "last_check": "2015-12-17T15:48:18.710454"}')
#: Credential JSON for tests
HOST_CREDS_JSON = '{"remote_user": "******", "ssh_priv_key": "dGVzdAo="}'
#: Host model for most tests
HOST = Host.new(
    ssh_priv_key='dGVzdAo=',
    remote_user='******',
    **json.loads(HOST_JSON))
#: Hosts model for most tests
HOSTS = Hosts.new(
    hosts=[HOST]
)
#: Cluster model for most tests
CLUSTER = Cluster.new(
    name='cluster',
    status='ok',
    hostset=[],
)
#: Cluster model with HOST for most tests
CLUSTER_WITH_HOST = Cluster.new(
    name='cluster',
    status='ok',