def on_put(self, req, resp, name): """ Handles the creation of a new Cluster. :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 name: The name of the Cluster being created. :type name: str """ # PUT is idempotent, and since there's no body to this request, # there's nothing to conflict with. The request should always # succeed, even if we didn't actually do anything. if util.etcd_cluster_exists(name): self.logger.info( 'Creation of already exisiting cluster {0} requested.'.format( name)) else: key = util.etcd_cluster_key(name) cluster = Cluster(status='ok', hostset=[]) etcd_resp, _ = cherrypy.engine.publish( 'store-save', key, cluster.to_json(secure=True))[0] self.logger.info('Created cluster {0} per request.'.format(name)) self.logger.debug('Etcd Response: {0}'.format(etcd_resp)) resp.status = falcon.HTTP_201
def on_put(self, req, resp, name): """ Handles the creation of a new Cluster. :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 name: The name of the Cluster being created. :type name: str """ # PUT is idempotent, and since there's no body to this request, # there's nothing to conflict with. The request should always # succeed, even if we didn't actually do anything. if util.etcd_cluster_exists(self.store, name): self.logger.info( 'Creation of already exisiting cluster {0} requested.'.format( name)) else: key = util.etcd_cluster_key(name) cluster = Cluster(status='ok', hostset=[]) etcd_resp = self.store.set(key, cluster.to_json(secure=True)) self.logger.info( 'Created cluster {0} per request.'.format(name)) self.logger.debug('Etcd Response: {0}'.format(etcd_resp)) resp.status = falcon.HTTP_201
def on_get(self, req, resp, name): """ Handles retrieval of an existing Cluster. :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 name: The name of the Cluster being requested. :type name: str """ key = util.etcd_cluster_key(name) etcd_resp, error = cherrypy.engine.publish('store-get', key)[0] if error: resp.status = falcon.HTTP_404 return cluster = Cluster(**json.loads(etcd_resp.value)) if not cluster: resp.status = falcon.HTTP_404 return self._calculate_hosts(cluster) # Have to set resp.body explicitly to include Hosts. resp.body = cluster.to_json_with_hosts() resp.status = falcon.HTTP_200
def on_get(self, req, resp, name): """ Handles retrieval of an existing Cluster. :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 name: The name of the Cluster being requested. :type name: str """ key = '/commissaire/clusters/{0}'.format(name) try: etcd_resp = self.store.get(key) self.logger.info( 'Request for cluster {0}.'.format(name)) self.logger.debug('{0}'.format(etcd_resp)) except etcd.EtcdKeyNotFound: self.logger.info( 'Request for non-existent cluster {0}.'.format(name)) resp.status = falcon.HTTP_404 return cluster = Cluster(**json.loads(etcd_resp.value)) self._calculate_hosts(cluster) # Have to set resp.body explicitly to include Hosts. resp.body = cluster.to_json_with_hosts() resp.status = falcon.HTTP_200
def on_put(self, req, resp, name): """ Handles the creation of a new Cluster. :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 name: The name of the Cluster being created. :type name: str """ # PUT is idempotent, and since there's no body to this request, # there's nothing to conflict with. The request should always # succeed, even if we didn't actually do anything. key = '/commissaire/clusters/{0}'.format(name) try: etcd_resp = self.store.get(key) self.logger.info( 'Creation of already exisiting cluster {0} requested.'.format( name)) except etcd.EtcdKeyNotFound: cluster = Cluster(status='ok', hostset=[]) etcd_resp = self.store.set(key, cluster.to_json(secure=True)) self.logger.info( 'Created cluster {0} per request.'.format(name)) cluster = Cluster(**json.loads(etcd_resp.value)) resp.status = falcon.HTTP_201
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 = self.store.delete("/commissaire/hosts/{0}".format(address)) resp.status = falcon.HTTP_410 except etcd.EtcdKeyNotFound: 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_dir = self.store.get("/commissaire/clusters") except etcd.EtcdKeyNotFound: self.logger.warn("Etcd does not have any clusters") return if len(clusters_dir._children): for etcd_resp in clusters_dir.leaves: cluster = Cluster(**json.loads(etcd_resp.value)) if address in cluster.hostset: cluster.hostset.remove(address) self.store.set(etcd_resp.key, cluster.to_json(secure=True))
def on_put(self, req, resp, name): """ Handles the creation of a new Cluster. :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 name: The name of the Cluster being created. :type name: str """ # PUT is idempotent, and since there's no body to this request, # there's nothing to conflict with. The request should always # succeed, even if we didn't actually do anything. try: Cluster.retrieve(name) self.logger.info( 'Creation of already exisiting cluster {0} requested.'.format( name)) except: pass cluster = Cluster(status='ok', hostset=[]) cluster.save(name) self.logger.info( 'Created cluster {0} per request.'.format(name)) resp.status = falcon.HTTP_201
def on_put(self, req, resp, name): """ Handles the creation of a new Cluster. :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 name: The name of the Cluster being created. :type name: str """ # PUT is idempotent, and since there's no body to this request, # there's nothing to conflict with. The request should always # succeed, even if we didn't actually do anything. try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) self.logger.info( 'Creation of already exisiting cluster {0} requested.'.format( name)) except: pass args = {} data = req.stream.read() if data: try: args = json.loads(data.decode()) self.logger.debug('Cluster args received: "{0}"'.format(args)) except ValueError as error: self.logger.error( 'Unable to parse cluster arguments: {0}'.format(error)) try: self.logger.debug('Looking for network {0}'.format( args['network'])) network = store_manager.get( Network.new(name=args['network'])) except KeyError: network = Network.new(**C.DEFAULT_CLUSTER_NETWORK_JSON) cluster = Cluster.new( name=name, type=args.get('type', C.CLUSTER_TYPE_DEFAULT), network=network.name, status='ok', hostset=[]) self.logger.debug('Cluster to create: {0}'.format( cluster.to_json_with_hosts())) store_manager.save(cluster) self.logger.info( 'Created cluster {0} per request.'.format(name)) self.logger.debug('New Cluster: {0}'.format( cluster.to_json_with_hosts())) resp.status = falcon.HTTP_201
def etcd_cluster_add_host(name, address): """ Adds a host address to a cluster with the given name. If no such cluster exists, the function raises KeyError. Note the function is idempotent: if the host address is already in the cluster, no change occurs. :param name: Name of a cluster :type name: str :param address: Host address to add :type address: str """ try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) except: raise KeyError # FIXME: Need input validation. # - Does the host exist at /commissaire/hosts/{IP}? # - Does the host already belong to another cluster? # 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. if address not in cluster.hostset: cluster.hostset.append(address) cluster = store_manager.save(cluster)
def on_get(self, req, resp, name): """ Handles retrieval of an existing Cluster. :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 name: The name of the Cluster being requested. :type name: str """ try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) except: resp.status = falcon.HTTP_404 return if not cluster: resp.status = falcon.HTTP_404 return self._calculate_hosts(cluster) # Have to set resp.body explicitly to include Hosts. resp.body = cluster.to_json_with_hosts() resp.status = falcon.HTTP_200
def on_get(self, req, resp, name, address): """ Handles GET requests for individual hosts in a Cluster. This is a membership test, returning 200 OK if the host address is part of the cluster, or else 404 Not Found. :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 name: The name of the Cluster being requested. :type name: str :param address: The address of the Host being requested. :type address: str """ try: cluster = Cluster.retrieve(name) except: resp.status = falcon.HTTP_404 return if address in cluster.hostset: resp.status = falcon.HTTP_200 else: resp.status = falcon.HTTP_404
def on_get(self, req, resp, name, address): """ Handles GET requests for individual hosts in a Cluster. This is a membership test, returning 200 OK if the host address is part of the cluster, or else 404 Not Found. :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 name: The name of the Cluster being requested. :type name: str :param address: The address of the Host being requested. :type address: str """ try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) except: resp.status = falcon.HTTP_404 return if address in cluster.hostset: resp.status = falcon.HTTP_200 else: resp.status = falcon.HTTP_404
def on_get(self, req, resp, name): """ Handles retrieval of an existing Cluster. :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 name: The name of the Cluster being requested. :type name: str """ try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) except Exception as error: self.logger.error("{0}: {1}".format(type(error), error)) resp.status = falcon.HTTP_404 return if not cluster: resp.status = falcon.HTTP_404 return self._calculate_hosts(cluster) # Have to set resp.body explicitly to include Hosts. resp.body = cluster.to_json_with_hosts() resp.status = falcon.HTTP_200 self.logger.debug('Cluster retrieval: {0}'.format(resp.body))
def etcd_cluster_add_host(name, address): """ Adds a host address to a cluster with the given name. If no such cluster exists, the function raises KeyError. Note the function is idempotent: if the host address is already in the cluster, no change occurs. :param name: Name of a cluster :type name: str :param address: Host address to add :type address: str """ try: cluster = Cluster.retrieve(name) except: raise KeyError # FIXME: Need input validation. # - Does the host exist at /commissaire/hosts/{IP}? # - Does the host already belong to another cluster? # 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. if address not in cluster.hostset: cluster.hostset.append(address) cluster.save(name)
def test_implicit_host_create(self): """ Verify creation of a Host with an implied address. """ with mock.patch('cherrypy.engine.publish') as _publish: manager = mock.MagicMock(StoreHandlerManager) _publish.return_value = [manager] manager.save.return_value = make_new(HOST) manager.get.side_effect = ( Exception, make_new(CLUSTER), make_new(HOST), make_new(CLUSTER), make_new(HOST)) data = ('{"ssh_priv_key": "dGVzdAo=", "remote_user": "******",' ' "cluster": "cluster"}') body = self.simulate_request( '/api/v0/host', method='PUT', body=data) self.assertEqual(self.srmock.status, falcon.HTTP_201) self.assertEqual( json.loads(INITIAL_IMPLICIT_HOST_JSON), json.loads(body[0])) # Make sure creation fails if the cluster doesn't exist manager.get.side_effect = ( make_new(HOST), Exception) body = self.simulate_request( '/api/v0/host', method='PUT', body=data) self.assertEqual(self.srmock.status, falcon.HTTP_409) self.assertEqual({}, json.loads(body[0])) # Make sure creation is idempotent if the request parameters # agree with an existing host. manager.get.side_effect = ( make_new(HOST), Cluster.new( name='cluster', status='ok', hostset=["127.0.0.1"])) body = self.simulate_request( '/api/v0/host', method='PUT', body=data) self.assertEqual(self.srmock.status, falcon.HTTP_200) self.assertEqual(json.loads(HOST_JSON), json.loads(body[0])) # Make sure creation fails if the request parameters conflict # with an existing host. manager.get.side_effect = ( make_new(HOST), make_new(HOST)) bad_data = '{"ssh_priv_key": "boguskey"}' body = self.simulate_request( '/api/v0/host', method='PUT', body=bad_data) self.assertEqual(self.srmock.status, falcon.HTTP_409) self.assertEqual({}, json.loads(body[0]))
def on_put(self, req, resp, name): """ Handles the creation of a new Cluster. :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 name: The name of the Cluster being created. :type name: str """ # PUT is idempotent, and since there's no body to this request, # there's nothing to conflict with. The request should always # succeed, even if we didn't actually do anything. try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) self.logger.info( 'Creation of already exisiting cluster {0} requested.'.format( name)) except: pass # Honor cluster type if it is passed in cluster_type = C.CLUSTER_TYPE_DEFAULT try: data = req.stream.read().decode() args = json.loads(data) cluster_type = args['type'] except KeyError: # Data was provided but no type was listed. Use default. pass except ValueError: # Cluster type was not provided. Use default. pass cluster = Cluster.new( name=name, cluster=cluster_type, status='ok', hostset=[]) store_manager.save(cluster) self.logger.info( 'Created cluster {0} per request.'.format(name)) resp.status = falcon.HTTP_201
def test_implicit_host_create(self): """ Verify creation of a Host with an implied address. """ with mock.patch('cherrypy.engine.publish') as _publish: manager = mock.MagicMock(StoreHandlerManager) _publish.return_value = [manager] manager.save.return_value = make_new(HOST) manager.get.side_effect = ( Exception, make_new(HOST), make_new(CLUSTER), make_new(HOST)) data = ('{"ssh_priv_key": "dGVzdAo=", "remote_user": "******",' ' "cluster": "cluster"}') body = self.simulate_request( '/api/v0/host', method='PUT', body=data) self.assertEqual(self.srmock.status, falcon.HTTP_201) self.assertEqual(json.loads(HOST_JSON), json.loads(body[0])) # Make sure creation fails if the cluster doesn't exist manager.get.side_effect = ( make_new(HOST), Exception) body = self.simulate_request( '/api/v0/host', method='PUT', body=data) self.assertEqual(self.srmock.status, falcon.HTTP_409) self.assertEqual({}, json.loads(body[0])) # Make sure creation is idempotent if the request parameters # agree with an existing host. manager.get.side_effect = ( make_new(HOST), Cluster.new( name='cluster', status='ok', hostset=["127.0.0.1"])) body = self.simulate_request( '/api/v0/host', method='PUT', body=data) self.assertEqual(self.srmock.status, falcon.HTTP_200) self.assertEqual(json.loads(HOST_JSON), json.loads(body[0])) # Make sure creation fails if the request parameters conflict # with an existing host. manager.get.side_effect = ( make_new(HOST), make_new(HOST)) bad_data = '{"ssh_priv_key": "boguskey"}' body = self.simulate_request( '/api/v0/host', method='PUT', body=bad_data) self.assertEqual(self.srmock.status, falcon.HTTP_409) self.assertEqual({}, json.loads(body[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: self.store.delete(util.etcd_host_key(address)) resp.status = falcon.HTTP_200 except etcd.EtcdKeyNotFound: 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_dir = self.store.get('/commissaire/clusters') self.logger.debug('Etcd Response: {0}'.format(clusters_dir)) except etcd.EtcdKeyNotFound: self.logger.warn('Etcd does not have any clusters') return if len(clusters_dir._children): self.logger.info( 'There are clusters associated with {0}...'.format(address)) for etcd_resp in clusters_dir.leaves: cluster = Cluster(**json.loads(etcd_resp.value)) if address in cluster.hostset: cluster_name = etcd_resp.key.split('/')[-1] self.logger.info('Removing {0} from cluster {1}'.format( address, cluster_name)) cluster.hostset.remove(address) self.store.set(etcd_resp.key, cluster.to_json(secure=True)) self.logger.info( '{0} has been removed from cluster {1}'.format( address, cluster_name))
def get_cluster_model(name): """ Returns a Cluster instance from the etcd record for the given cluster name, if it exists, or else None. For convenience, the EtcdResult is embedded in the Cluster instance as an 'etcd' property. :param name: Name of a cluster :type name: str """ key = etcd_cluster_key(name) etcd_resp, error = cherrypy.engine.publish('store-get', key)[0] if error: return None cluster = Cluster(**json.loads(etcd_resp.value)) cluster.etcd = etcd_resp return cluster
def test__format_kwargs(self): """ Verify keyword arguments get formatted properly. """ model_instance = Cluster.new(name='test') annotations = { 'commissaire-cluster-test-name': 'test', 'commissaire-cluster-test-status': 'test', } kwargs = self.instance._format_kwargs(model_instance, annotations) self.assertEquals({'name': 'test', 'status': 'test'}, kwargs)
def test__get_on_namespace(self): """ Verify getting data from namespaces works. """ model_instance = Cluster.new(name='test') self.instance._store.get = mock.MagicMock() self.instance._store.get().json().get().get.return_value = { 'commissaire-cluster-test-name': 'test', 'commissaire-cluster-test-status': 'ok', } self.instance._get_on_namespace(model_instance)
def get_cluster_model(store, name): """ Returns a Cluster instance from the etcd record for the given cluster name, if it exists, or else None. For convenience, the EtcdResult is embedded in the Cluster instance as an 'etcd' property. :param store: Data store. :type store: etcd.Client :param name: Name of a cluster :type name: str """ key = etcd_cluster_key(name) try: etcd_resp = store.get(key) except etcd.EtcdKeyNotFound: return None cluster = Cluster(**json.loads(etcd_resp.value)) cluster.etcd = etcd_resp return cluster
def test__format_model(self): """ Verify responses from Kubernetes can be turned into models. """ model_instance = Cluster.new(name='test') resp_data = {'metadata': {'annotations': { 'commissaire-cluster-test-name': 'test', 'commissaire-cluster-test-status': 'test', }}} result = self.instance._format_model(resp_data, model_instance) self.assertEquals('test', result.name) self.assertEquals('test', result.status)
def etcd_cluster_exists(name): """ Returns whether a cluster with the given name exists. :param name: Name of a cluster :type name: str """ store_manager = cherrypy.engine.publish('get-store-manager')[0] try: store_manager.get(Cluster.new(name=name)) except: return False return True
def on_put(self, req, resp, name): """ Handles PUT requests for Cluster hosts. This replaces the entire host list for a Cluster. :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 name: The name of the Cluster being requested. :type name: str """ try: req_body = json.loads(req.stream.read().decode()) old_hosts = set(req_body['old']) # Ensures no duplicates new_hosts = set(req_body['new']) # Ensures no duplicates except (KeyError, TypeError): self.logger.info( 'Bad client PUT request for cluster "{0}": {1}'. format(name, req_body)) resp.status = falcon.HTTP_400 return try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) except: resp.status = falcon.HTTP_404 return # old_hosts must match current hosts to accept new_hosts. if old_hosts != set(cluster.hostset): self.logger.info( 'Conflict setting hosts for cluster {0}'.format(name)) self.logger.debug('{0} != {1}'.format(old_hosts, cluster.hostset)) resp.status = falcon.HTTP_409 return # FIXME: Need input validation. For each new host, # - Does the host exist at /commissaire/hosts/{IP}? # - Does the host already belong to another cluster? # 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. cluster.hostset = list(new_hosts) store_manager.save(cluster) resp.status = falcon.HTTP_200
def on_delete(self, req, resp, name): """ Handles the deletion of a Cluster. :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 name: The name of the Cluster being deleted. :type name: str """ resp.body = '{}' try: Cluster.delete(name) resp.status = falcon.HTTP_200 self.logger.info( 'Deleted cluster {0} per request.'.format(name)) except: self.logger.info( 'Deleting for non-existent cluster {0} requested.'.format( name)) resp.status = falcon.HTTP_404
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 etcd_cluster_has_host(name, address): """ Checks if a host address belongs to a cluster with the given name. If no such cluster exists, the function raises KeyError. :param name: Name of a cluster :type name: str :param address: Host address :type address: str """ try: cluster = Cluster.retrieve(name) except: raise KeyError return address in cluster.hostset
def test__format_model(self): """ Verify responses from Kubernetes can be turned into models. """ model_instance = Cluster.new(name='test') resp_data = { 'metadata': { 'annotations': { 'commissaire-cluster-test-name': 'test', 'commissaire-cluster-test-status': 'test', } } } result = self.instance._format_model(resp_data, model_instance) self.assertEquals('test', result.name) self.assertEquals('test', result.status)
def etcd_cluster_has_host(name, address): """ Checks if a host address belongs to a cluster with the given name. If no such cluster exists, the function raises KeyError. :param name: Name of a cluster :type name: str :param address: Host address :type address: str """ try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) except: raise KeyError return address in cluster.hostset
def get_cluster_model(name): """ Returns a Cluster instance from the etcd record for the given cluster name, if it exists, or else None. For convenience, the EtcdResult is embedded in the Cluster instance as an 'etcd' property. :param name: Name of a cluster :type name: str """ store_manager = cherrypy.engine.publish('get-store-manager')[0] try: cluster = store_manager.get(Cluster.new(name=name)) except: cluster = None return cluster
def on_get(self, req, resp, name): """ Handles GET requests for Cluster 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 :param name: The name of the Cluster being requested. :type name: str """ try: cluster = Cluster.retrieve(name) except: resp.status = falcon.HTTP_404 return resp.body = json.dumps(cluster.hostset) resp.status = falcon.HTTP_200
def on_get(self, req, resp, name): """ Handles GET requests for Cluster 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 :param name: The name of the Cluster being requested. :type name: str """ try: store_manager = cherrypy.engine.publish('get-store-manager')[0] cluster = store_manager.get(Cluster.new(name=name)) except: resp.status = falcon.HTTP_404 return resp.body = json.dumps(cluster.hostset) resp.status = falcon.HTTP_200
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_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 on_delete(self, req, resp, name): """ Handles the deletion of a Cluster. :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 name: The name of the Cluster being deleted. :type name: str """ resp.body = '{}' try: store_manager = cherrypy.engine.publish('get-store-manager')[0] store_manager.delete(Cluster.new(name=name)) resp.status = falcon.HTTP_200 self.logger.info( 'Deleted cluster {0} per request.'.format(name)) except: self.logger.info( 'Deleting for non-existent cluster {0} requested.'.format( name)) resp.status = falcon.HTTP_404
def bootstrap(self, ip, cluster_data, key_file, store_manager, oscmd): """ Bootstraps a host via ansible. :param ip: IP address to bootstrap. :type ip: str :param cluster_data: The data required to create a Cluster instance. :type cluster_data: dict or None :param key_file: Full path to the file holding the private SSH key. :type key_file: str :param store_manager: Remote object for remote stores :type store_manager: commissaire.store.storehandlermanager. StoreHandlerManager :param oscmd: OSCmd class to use :type oscmd: commissaire.oscmd.OSCmdBase :returns: tuple -- (exitcode(int), facts(dict)). """ self.logger.debug('Using {0} as the oscmd class for {1}'.format( oscmd.os_type, ip)) # cluster_data can be None. If it is change it to an empty dict if cluster_data is None: cluster_data = {} cluster_type = C.CLUSTER_TYPE_HOST network = Network.new(**C.DEFAULT_CLUSTER_NETWORK_JSON) try: self.logger.debug( 'Grabbing cluster type from {0}'.format(cluster_data)) cluster = Cluster.new(**cluster_data) cluster_type = cluster.type self.logger.debug('Found network {0}'.format(cluster.network)) network = store_manager.get(Network.new(name=cluster.network)) except KeyError: # Not part of a cluster pass etcd_config = self._get_etcd_config(store_manager) kube_config = self._get_kube_config(store_manager) play_vars = { 'commissaire_cluster_type': cluster_type, 'commissaire_bootstrap_ip': ip, 'commissaire_kubernetes_api_server_url': kube_config['server_url'], 'commissaire_kubernetes_bearer_token': kube_config['token'], # TODO: Where do we get this? 'commissaire_docker_registry_host': '127.0.0.1', # TODO: Where do we get this? 'commissaire_docker_registry_port': 8080, # TODO: Where do we get this? 'commissaire_flannel_key': '/atomic01/network', 'commissaire_docker_config_local': resource_filename('commissaire', 'data/templates/docker'), 'commissaire_flanneld_config_local': resource_filename('commissaire', 'data/templates/flanneld'), 'commissaire_kubelet_config_local': resource_filename('commissaire', 'data/templates/kubelet'), 'commissaire_kubernetes_config_local': resource_filename('commissaire', 'data/templates/kube_config'), 'commissaire_kubeconfig_config_local': resource_filename('commissaire', 'data/templates/kubeconfig'), 'commissaire_install_libselinux_python': " ".join(oscmd.install_libselinux_python()), 'commissaire_docker_config': oscmd.docker_config, 'commissaire_flanneld_config': oscmd.flanneld_config, 'commissaire_kubelet_config': oscmd.kubelet_config, 'commissaire_kubernetes_config': oscmd.kubernetes_config, 'commissaire_kubeconfig_config': oscmd.kubernetes_kubeconfig, 'commissaire_install_flannel': " ".join(oscmd.install_flannel()), 'commissaire_install_docker': " ".join(oscmd.install_docker()), 'commissaire_install_kube': " ".join(oscmd.install_kube()), 'commissaire_flannel_service': oscmd.flannel_service, 'commissaire_docker_service': oscmd.flannel_service, 'commissaire_kubelet_service': oscmd.kubelet_service, 'commissaire_kubeproxy_service': oscmd.kubelet_proxy_service, } # If we are a flannel_server network then set the var if network.type == 'flannel_server': play_vars['commissaire_flanneld_server'] = network.options.get( 'address') elif network.type == 'flannel_etcd': play_vars['commissaire_etcd_server_url'] = etcd_config[ 'server_url'] # Provide the CA if etcd is being used over https if (etcd_config['server_url'].startswith('https:') and 'certificate_ca_path' in etcd_config): play_vars['commissaire_etcd_ca_path'] = oscmd.etcd_ca play_vars['commissaire_etcd_ca_path_local'] = ( etcd_config['certificate_ca_path']) # Client Certificate additions if 'certificate_path' in etcd_config: self.logger.info('Using etcd client certs') play_vars['commissaire_etcd_client_cert_path'] = ( oscmd.etcd_client_cert) play_vars['commissaire_etcd_client_cert_path_local'] = ( etcd_config['certificate_path']) play_vars['commissaire_etcd_client_key_path'] = ( oscmd.etcd_client_key) play_vars['commissaire_etcd_client_key_path_local'] = ( etcd_config['certificate_key_path']) if 'certificate_path' in kube_config: self.logger.info('Using kubernetes client certs') play_vars['commissaire_kubernetes_client_cert_path'] = ( oscmd.kube_client_cert) play_vars['commissaire_kubernetes_client_cert_path_local'] = ( kube_config['certificate_path']) play_vars['commissaire_kubernetes_client_key_path'] = ( oscmd.kube_client_key) play_vars['commissaire_kubernetes_client_key_path_local'] = ( kube_config['certificate_key_path']) # XXX: Need to enable some package repositories for OS 'rhel' # (or 'redhat'). This is a hack for a single corner case. # We discussed how to generalize future cases where we need # extra commands for a specific OS but decided to defer until # more crop up. # # See https://github.com/projectatomic/commissaire/pull/56 # if oscmd.os_type in ('rhel', 'redhat'): play_vars['commissaire_enable_pkg_repos'] = ( 'subscription-manager repos ' '--enable=rhel-7-server-extras-rpms ' '--enable=rhel-7-server-optional-rpms') else: play_vars['commissaire_enable_pkg_repos'] = 'true' self.logger.debug('Variables for bootstrap: {0}'.format(play_vars)) play_file = resource_filename('commissaire', 'data/ansible/playbooks/bootstrap.yaml') results = self._run(ip, key_file, play_file, [0], play_vars) return results
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))
#: 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', hostset=[HOST], ) #: Cluster model with flattened HOST for tests CLUSTER_WITH_FLAT_HOST = Cluster.new( name='cluster', status='ok', hostset=[HOST.address], ) #: ClusterRestart model for most tests
def test_bootstrap(self): """ Verify Transport().bootstrap works as expected. """ with patch( 'commissaire.transport.ansibleapi.TaskQueueManager') as _tqm: _tqm().run.return_value = 0 transport = ansibleapi.Transport() transport.variable_manager._fact_cache = {} oscmd = MagicMock(OSCmdBase) result, facts = transport.bootstrap('10.2.0.2', Cluster.new().__dict__, 'test/fake_key', MagicMock(), oscmd) # We should have a successful response self.assertEquals(0, result) # We should see expected calls self.assertEquals(1, oscmd.install_docker.call_count) self.assertEquals(1, oscmd.install_kube.call_count) # Check user-config to playbook-variable translation. etcd_config = { 'server_url': 'https://192.168.1.1:1234', 'certificate_ca_path': '/path/to/etcd/ca/cert', 'certificate_path': '/path/to/etcd/client/cert', 'certificate_key_path': '/path/to/etcd/client/key' } kube_config = { 'server_url': 'https://192.168.2.2:4567', 'certificate_path': '/path/to/kube/client/cert', 'certificate_key_path': '/path/to/kube/client/key' } store_manager = MagicMock(StoreHandlerManager) store_manager.list_store_handlers.return_value = [ (EtcdStoreHandler, etcd_config, ()), (KubernetesStoreHandler, kube_config, ()) ] store_manager.get.return_value = Network.new(name='default', type='flannel_etcd') cluster_data = Cluster.new(name='default', network='default').__dict__ transport = ansibleapi.Transport() transport._run = MagicMock() transport._run.return_value = (0, {}) result, facts = transport.bootstrap('10.2.0.2', cluster_data, 'test/fake_key', store_manager, oscmd) play_vars = transport._run.call_args[0][4] self.assertEqual(play_vars['commissaire_etcd_server_url'], 'https://192.168.1.1:1234') self.assertEqual(play_vars['commissaire_etcd_ca_path_local'], '/path/to/etcd/ca/cert') self.assertEqual( play_vars['commissaire_etcd_client_cert_path_local'], '/path/to/etcd/client/cert') self.assertEqual( play_vars['commissaire_etcd_client_key_path_local'], '/path/to/etcd/client/key') # Check 'commissaire_enable_pkg_repos' playbook variable # for various operating systems. transport = ansibleapi.Transport() transport._run = MagicMock() transport._run.return_value = (0, {}) needs_enable_repos = ('redhat', 'rhel') for os_type in available_os_types: oscmd = get_oscmd(os_type) result, facts = transport.bootstrap('10.2.0.2.', cluster_data, 'test/fake_key', MagicMock(), oscmd) play_vars = transport._run.call_args[0][4] command = play_vars['commissaire_enable_pkg_repos'] if os_type in needs_enable_repos: self.assertIn('subscription-manager repos', command) else: self.assertEqual('true', command) # no-op command
def clusterexec(store_manager, cluster_name, command, kwargs={}): """ Remote executes a shell commands across a cluster. :param store_manager: Proxy object for remtote stores :type store_manager: commissaire.store.StoreHandlerManager :param cluster_name: Name of the cluster to act on :type cluster_name: str :param command: Top-level command to execute :type command: str :param kwargs: Keyword arguments for the command :type kwargs: dict """ logger = logging.getLogger('clusterexec') # TODO: This is a hack and should really be done elsewhere command_args = () if command == 'upgrade': finished_hosts_key = 'upgraded' model_instance = ClusterUpgrade.new( name=cluster_name, status='in_process', started_at=datetime.datetime.utcnow().isoformat(), upgraded=[], in_process=[], ) elif command == 'restart': finished_hosts_key = 'restarted' model_instance = ClusterRestart.new( name=cluster_name, status='in_process', started_at=datetime.datetime.utcnow().isoformat(), restarted=[], in_process=[], ) elif command == 'deploy': finished_hosts_key = 'deployed' version = kwargs.get('version', '') command_args = (version,) model_instance = ClusterDeploy.new( name=cluster_name, status='in_process', started_at=datetime.datetime.utcnow().isoformat(), version=version, deployed=[], in_process=[], ) end_status = 'finished' try: # Set the initial status in the store logger.info('Setting initial status.') logger.debug('Status={0}'.format(model_instance.to_json())) store_manager.save(model_instance) except Exception as error: logger.error( 'Unable to save initial state for "{0}" clusterexec due to ' '{1}: {2}'.format(cluster_name, type(error), error)) return # Collect all host addresses in the cluster try: cluster = store_manager.get(Cluster.new( name=cluster_name, status='', hostset=[])) except Exception as error: logger.warn( 'Unable to continue for cluster "{0}" due to ' '{1}: {2}. Returning...'.format(cluster_name, type(error), error)) return if cluster.hostset: logger.debug( '{0} hosts in cluster "{1}"'.format( len(cluster.hostset), cluster_name)) else: logger.warn('No hosts in cluster "{0}"'.format(cluster_name)) # TODO: Find better way to do this try: hosts = store_manager.list(Hosts(hosts=[])) except Exception as error: logger.warn( 'No hosts in the cluster. Error: {0}. Exiting clusterexec'.format( error)) return for host in hosts.hosts: if host.address not in cluster.hostset: logger.debug( 'Skipping {0} as it is not in this cluster.'.format( host.address)) continue # Move on to the next one oscmd = get_oscmd(host.os) # command_list is only used for logging command_list = getattr(oscmd, command)(*command_args) logger.info('Executing {0} on {1}...'.format( command_list, host.address)) model_instance.in_process.append(host.address) try: store_manager.save(model_instance) except Exception as error: logger.error( 'Unable to save in_process state for "{0}" clusterexec due to ' '{1}: {2}'.format(cluster_name, type(error), error)) return key = TemporarySSHKey(host, logger) key.create() try: transport = ansibleapi.Transport(host.remote_user) exe = getattr(transport, command) result, facts = exe( host.address, key.path, oscmd, kwargs) # XXX: ansibleapi explicitly raises Exception() except Exception as ex: # If there was a failure set the end_status and break out end_status = 'failed' logger.error('Clusterexec {0} for {1} failed: {2}: {3}'.format( command, host.address, type(ex), ex)) break finally: try: key.remove() logger.debug('Removed temporary key file {0}'.format(key.path)) except: logger.warn( 'Unable to remove the temporary key file: {0}'.format( key.path)) # Set the finished hosts new_finished_hosts = getattr( model_instance, finished_hosts_key) + [host.address] setattr( model_instance, finished_hosts_key, new_finished_hosts) try: idx = model_instance.in_process.index(host.address) model_instance.in_process.pop(idx) except ValueError: logger.warn('Host {0} was not in_process for {1} {2}'.format( host['address'], command, cluster_name)) try: store_manager.save(model_instance) logger.info('Finished executing {0} for {1} in {2}'.format( command, host.address, cluster_name)) except Exception as error: logger.error( 'Unable to save cluster state for "{0}" clusterexec due to ' '{1}: {2}'.format(cluster_name, type(error), error)) return # Final set of command result model_instance.finished_at = datetime.datetime.utcnow().isoformat() model_instance.status = end_status logger.info('Cluster {0} final {1} status: {2}'.format( cluster_name, command, model_instance.to_json())) try: store_manager.save(model_instance) except Exception as error: logger.error( 'Unable to save final state for "{0}" clusterexec due to ' '{1}: {2}'.format(cluster_name, type(error), error)) logger.info('Clusterexec stopping')
# 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 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ Cluster(s) handlers. """ import datetime import json import cherrypy import falcon from multiprocessing import Process from commissaire.resource import Resource from commissaire.jobs.clusterexec import clusterexec from commissaire.handlers.models import (Cluster, Clusters, ClusterRestart, ClusterUpgrade, Host)