Example #1
0
    def __init__(self, config):

        self._config = config
        self._eth_client = EthereumWrapper(config)
        tcf_home = os.environ.get("TCF_HOME", "../../")

        worker_reg_contract_file = tcf_home + "/" + \
            config["ethereum"]["worker_registry_contract_file"]
        worker_reg_contract_address = \
            config["ethereum"]["worker_registry_contract_address"]
        self._worker_reg_contract_instance,\
            self._worker_reg_contract_instance_evt = self._eth_client\
            .get_contract_instance(
                worker_reg_contract_file, worker_reg_contract_address)

        self._worker_registry = EthereumWorkerRegistryImpl(config)

        work_order_contract_file = tcf_home + "/" + \
            config["ethereum"]["work_order_contract_file"]
        work_order_contract_address = \
            config["ethereum"]["work_order_contract_address"]
        self._work_order_contract_instance,\
            self._work_order_contract_instance_evt = self._eth_client\
            .get_contract_instance(
                work_order_contract_file, work_order_contract_address)

        self._work_order_proxy = EthereumWorkOrderProxyImpl(config)
 def __init__(self, config_file):
     super(TestEthereumWorkerRegistryImpl, self).__init__()
     if not path.isfile(config_file):
         raise FileNotFoundError("File not found at path: {0}".format(
             path.realpath(config_file)))
     try:
         with open(config_file) as fd:
             self.__config = toml.load(fd)
     except IOError as e:
         if e.errno != errno.ENOENT:
             raise Exception("Could not open config file: %s", e)
     self.__eth_conn = EthereumWorkerRegistryImpl(self.__config)
Example #3
0
def _create_worker_registry_instance(blockchain_type, config):
    # create worker registry instance for direct/proxy model
    if blockchain_type == 'fabric':
        return FabricWorkerRegistryImpl(config)
    elif blockchain_type == 'ethereum':
        return EthereumWorkerRegistryImpl(config)
    else:
        return JRPCWorkerRegistryImpl(config)
Example #4
0
class EthereumConnector:
    """
    This class is the bridge between the Ethereum blockchain and the Avalon
    core. It listens for events generated by the Ethereum blockchain.
    It handles event data corresponding to the event (eg: workOrderSubmitted
    and submits requests to Avalon on behalf of the client. The service also
    invokes smart contract APIs (eg: workOrderComplete).
    """
    def __init__(self, config):

        self._config = config
        self._eth_client = EthereumWrapper(config)
        tcf_home = os.environ.get("TCF_HOME", "../../")

        worker_reg_contract_file = tcf_home + "/" + \
            config["ethereum"]["worker_registry_contract_file"]
        worker_reg_contract_address = \
            config["ethereum"]["worker_registry_contract_address"]
        self._worker_reg_contract_instance,\
            self._worker_reg_contract_instance_evt = self._eth_client\
            .get_contract_instance(
                worker_reg_contract_file, worker_reg_contract_address)

        self._worker_registry = EthereumWorkerRegistryImpl(config)

        work_order_contract_file = tcf_home + "/" + \
            config["ethereum"]["work_order_contract_file"]
        work_order_contract_address = \
            config["ethereum"]["work_order_contract_address"]
        self._work_order_contract_instance,\
            self._work_order_contract_instance_evt = self._eth_client\
            .get_contract_instance(
                work_order_contract_file, work_order_contract_address)

        self._work_order_proxy = EthereumWorkOrderProxyImpl(config)

    def _lookup_workers_in_kv_storage(self, worker_registry):
        """
        This function retrieves the worker ids from shared kv using
        worker_lookup direct API.
        Returns list of worker id
        """
        jrpc_req_id = random.randint(0, 100000)

        worker_lookup_result = worker_registry.worker_lookup(
            worker_type=WorkerType.TEE_SGX, id=jrpc_req_id)
        logging.info("\nWorker lookup response from kv storage : {}\n".format(
            json.dumps(worker_lookup_result, indent=4)))
        if "result" in worker_lookup_result and \
                "ids" in worker_lookup_result["result"].keys():
            if worker_lookup_result["result"]["totalCount"] != 0:
                return worker_lookup_result["result"]["ids"]
            else:
                logging.error("No workers found in kv storage")
        else:
            logging.error("Failed to lookup worker in kv storage")
        return []

    def _retrieve_worker_details_from_kv_storage(self, worker_registry,
                                                 worker_id):
        # Retrieve worker details
        jrpc_req_id = random.randint(0, 100000)
        worker_info = worker_registry.worker_retrieve(worker_id, jrpc_req_id)
        logging.info("Worker retrieve response from kv storage: {}".format(
            json.dumps(worker_info, indent=4)))

        if "error" in worker_info:
            logging.error("Unable to retrieve worker details from kv storage")
        return worker_info["result"]

    def _add_update_worker_to_chain(self, wids_onchain, wids_kv,
                                    jrpc_worker_registry):
        """
        This function adds/updates a worker in the Ethereum blockchain
        """

        for wid in wids_kv:
            worker_info = self._retrieve_worker_details_from_kv_storage(
                jrpc_worker_registry, wid)
            worker_id = wid
            worker_type = WorkerType(worker_info["workerType"])
            org_id = worker_info["organizationId"]
            app_type_id = worker_info["applicationTypeId"]
            details = json.dumps(worker_info["details"])

            result = None
            if wid in wids_onchain:
                logging.info(
                    "Updating worker {} on ethereum blockchain".format(wid))
                result = self._worker_registry.worker_update(
                    worker_id, details)
            else:
                logging.info(
                    "Adding new worker {} to ethereum blockchain".format(wid))
                result = self._worker_registry.worker_register(
                    worker_id, worker_type, org_id, [app_type_id], details)
            if result is None:
                logging.error(
                    "Error while adding/updating worker to ethereum" +
                    " blockchain")

        for wid in wids_onchain:
            # Mark all stale workers on blockchain as decommissioned
            if wid not in wids_kv:
                worker_id = wid
                worker = self._worker_registry\
                    .worker_retrieve(wid, random.randint(0, 100000))
                worker_status_onchain = worker["result"]["status"]
                # If worker is not already decommissoined, mark it decommission
                # as it is no longer available in the kv storage
                if worker_status_onchain != WorkerStatus.DECOMMISSIONED.value:
                    self._worker_registry.worker_set_status(
                        worker_id, WorkerStatus.DECOMMISSIONED)
                    logging.info("Marked worker " + wid +
                                 " as decommissioned on" +
                                 " ethereum blockchain")

    def _lookup_workers_onchain(self):
        """
        Lookup all workers on chain to sync up with kv storage
        """
        jrpc_req_id = random.randint(0, 100000)
        # TODO: Remove hardcoding and pass wild characters instead
        worker_lookup_result = self._worker_registry.worker_lookup(
            WorkerType.TEE_SGX, self._config["WorkerConfig"]["OrganizationId"],
            self._config["WorkerConfig"]["ApplicationTypeId"], jrpc_req_id)
        logging.info("Worker lookup response from blockchain: {}\n".format(
            json.dumps(worker_lookup_result, indent=4)))
        if "result" in worker_lookup_result and \
                "ids" in worker_lookup_result["result"].keys():
            if worker_lookup_result["result"]["totalCount"] != 0:
                return worker_lookup_result["result"]["ids"]
            else:
                logging.error("No workers found in ethereum blockchain")
        else:
            logging.error("Failed to lookup worker in ethereum blockchain")
        return []

    def _submit_work_order_and_get_result(self, work_order_id, worker_id,
                                          requester_id, work_order_params):
        """
        This function submits work order using work_order_submit direct API
        """
        work_order_impl = JRPCWorkOrderImpl(self._config)
        logging.info("About to submit work order to kv storage")
        response = work_order_impl\
            .work_order_submit(work_order_id, worker_id, requester_id,
                               work_order_params, id=random.randint(0, 100000))
        logging.info("Work order submit response : {}".format(
            json.dumps(response, indent=4)))

        work_order_result = work_order_impl\
            .work_order_get_result(work_order_id,
                                   id=random.randint(0, 100000))

        logging.info("Work order get result : {} ".format(
            json.dumps(work_order_result, indent=4)))

        return work_order_result

    def _add_work_order_result_to_chain(self, work_order_id, response):
        """
        This function adds a work order result to the Ethereum blockchain
        """
        result = self._work_order_proxy.work_order_complete(
            work_order_id, json.dumps(response))
        if result == SUCCESS:
            logging.info("Successfully added work order result to blockchain")
        else:
            logging.error("Error adding work order result to blockchain")
        return result

    def handleEvent(self, event, account, contract):
        """
        The function retrieves pertinent information from the event received
        and makes request to listener using Direct API and writes back result
        to the blockchain
        """

        work_order_request = json.loads(event["args"]["workOrderRequest"])

        work_order_id = work_order_request["workOrderId"]
        worker_id = work_order_request["workerId"]
        requester_id = work_order_request["requesterId"]
        work_order_params = event["args"]["workOrderRequest"]
        logging.info("Received event from blockchain")
        response = self\
            ._submit_work_order_and_get_result(work_order_id, worker_id,
                                               requester_id,
                                               work_order_params)
        self._add_work_order_result_to_chain(work_order_id, response)

    def start(self):
        logging.info("Ethereum Connector service started")

        # Fetch first worker details from shared KV (via direct API)
        # and add the worker to block chain.
        # TODO: Fetch all workers from shared KV and block chain
        # and do 2-way sync.
        jrpc_worker_registry = JRPCWorkerRegistryImpl(self._config)
        worker_ids_onchain = self._lookup_workers_onchain()
        worker_ids_kv = self._lookup_workers_in_kv_storage(
            jrpc_worker_registry)

        self._add_update_worker_to_chain(worker_ids_onchain, worker_ids_kv,
                                         jrpc_worker_registry)

        # Start an event listener that listens for events from the proxy
        # blockchain, extracts request payload from there and make a request
        # to avalon-listener

        w3 = BlockchainInterface(self._config)

        contract = self._work_order_contract_instance_evt
        # Listening only for workOrderSubmitted event now
        listener = w3.newListener(contract, "workOrderSubmitted")

        try:
            daemon = EventProcessor(self._config)
            asyncio.get_event_loop().run_until_complete(
                daemon.start(
                    listener,
                    self.handleEvent,
                    account=None,
                    contract=contract,
                ))
        except KeyboardInterrupt:
            asyncio.get_event_loop().run_until_complete(daemon.stop())
class TestEthereumWorkerRegistryImpl(unittest.TestCase):
    def __init__(self, config_file):
        super(TestEthereumWorkerRegistryImpl, self).__init__()
        if not path.isfile(config_file):
            raise FileNotFoundError("File not found at path: {0}".format(
                path.realpath(config_file)))
        try:
            with open(config_file) as fd:
                self.__config = toml.load(fd)
        except IOError as e:
            if e.errno != errno.ENOENT:
                raise Exception("Could not open config file: %s", e)
        self.__eth_conn = EthereumWorkerRegistryImpl(self.__config)

    def test_worker_register(self):
        self.__worker_id = urandom(32).hex()
        self.__worker_type = WorkerType.TEE_SGX
        self.__details = json.dumps(
            {"workOrderSyncUri": "http://worker-order:8008"})
        self.__org_id = urandom(32).hex()
        self.__application_ids = [
            urandom(32).hex(),
            urandom(32).hex(),
            urandom(32).hex()
        ]
        logging.info(
            "Calling worker_register contract..\n worker_id: %s\n " +
            "worker_type: %d\n " +
            "orgId: %s\n applicationIds %s\n details %s", self.__worker_id,
            self.__worker_type.value, self.__org_id, self.__application_ids,
            self.__details)
        result = self.__eth_conn.worker_register(self.__worker_id,
                                                 self.__worker_type,
                                                 self.__org_id,
                                                 self.__application_ids,
                                                 self.__details)
        logging.info("worker_register status \n %s", result)
        self.assertIsNotNone(result, "transaction execution failed")

    def test_worker_set_status(self):
        self.__status = WorkerStatus.DECOMMISSIONED
        logging.info(
            "Calling worker_set_status..\n worker_id: %s\n status: %d",
            self.__worker_id, self.__status.value)
        result = self.__eth_conn.worker_set_status(self.__worker_id,
                                                   self.__status)
        logging.info("worker_set_status status \n%s", result)
        self.assertIsNotNone(result, "worker set status response not matched")

    def test_worker_update(self):
        self.__new_details = json.dumps({
            "torkOrderSyncUri":
            "http://worker-order:8008",
            "workOrderNotifyUri":
            "http://worker-order-notify:9909"
        })
        logging.info("Calling worker_update..\n worker_id: %s\n details: %s",
                     self.__worker_id, self.__new_details)
        result = self.__eth_conn.worker_update(self.__worker_id,
                                               self.__new_details)
        logging.info("worker_update status \n %s", result)
        self.assertIsNotNone(result, "worker update response not matched")

    def test_worker_lookup(self):
        logging.info(
            "Calling worker_lookup..\n worker_type: %d\n orgId: %s\n " +
            "applicationId: %s", self.__worker_type.value, self.__org_id,
            self.__application_ids[0])
        result = self.__eth_conn.worker_lookup(
            self.__worker_type, self.__org_id,
            self.__application_ids[0])["result"]
        logging.info("worker_lookup status [%d, %s, %s]", result["totalCount"],
                     result["lookupTag"], result["ids"])
        match = self.__worker_id in result["ids"]
        self.assertEqual(result["totalCount"], 1,
                         "Worker lookup response should match")
        self.assertTrue(match,
                        "Worker lookup response worker id doesn't match")

    def test_worker_retrieve(self):
        logging.info("Calling worker_retrieve..\n worker_id: %s",
                     self.__worker_id)
        result = self.__eth_conn.worker_retrieve(self.__worker_id)["result"]
        logging.info("worker_retrieve status [%d, %s, %s, %s, %s]",
                     result["status"], result["workerType"],
                     result["organizationId"], result["applicationTypeId"],
                     result["details"])
        self.assertEqual(result["workerType"], self.__worker_type.value,
                         "Worker retrieve response worker type doesn't match")
        self.assertEqual(
            result["organizationId"], self.__org_id,
            "Worker retrieve response organization id doesn't match")
        self.assertEqual(
            result["applicationTypeId"][0], self.__application_ids[0],
            "Worker retrieve response application id[0] doesn't match")
        self.assertEqual(
            result["applicationTypeId"][1], self.__application_ids[1],
            "Worker retrieve response application id[1] doesn't match")
        self.assertEqual(
            result["details"], json.loads(self.__new_details),
            "Worker retrieve response worker details doesn't match")
        self.assertEqual(
            result["status"], self.__status.value,
            "Worker retrieve response worker status doesn't match")

    def test_worker_lookup_next(self):
        lookUpTag = ""
        logging.info(
            "Calling worker_lookup_next..\n worker_type: %d\n" +
            "orgId: %s\n applicationId:%s\n lookUpTag: %s",
            self.__worker_type.value, self.__org_id, self.__application_ids[0],
            lookUpTag)
        result = self.__eth_conn.worker_lookup_next(self.__worker_type,
                                                    self.__org_id,
                                                    self.__application_ids[0],
                                                    lookUpTag)
        logging.info(result)
        logging.info("worker_lookup_next status [%d, %s, %s]", result[0],
                     result[1], result[2])
        self.assertEqual(result[0], 0,
                         "worker_lookup_next response count doesn't match")