def getProductConfiguration(self, product_id): """ Get the product configuration --- WITHOUT THE DB PASSWORD --- of the given product. """ try: session = self.__session() product = session.query(Product).get(product_id) if product is None: msg = "Product with ID {0} does not exist!".format(product_id) LOG.error(msg) raise shared.ttypes.RequestFailed( shared.ttypes.ErrorCode.DATABASE, msg) # Put together the database connection's descriptor. args = SQLServer.connection_string_to_args(product.connection) if args['postgresql']: db_engine = 'postgresql' db_host = args['dbaddress'] db_port = args['dbport'] db_user = args['dbusername'] db_name = args['dbname'] else: db_engine = 'sqlite' db_host = "" db_port = 0 db_user = "" db_name = args['sqlite'] dbc = ttypes.DatabaseConnection( engine=db_engine, host=db_host, port=db_port, username_b64=base64.b64encode(db_user), # DO NOT TRANSPORT PASSWORD BACK TO THE USER! database=db_name) # Put together the product configuration. name = base64.b64encode(product.display_name.encode('utf-8')) descr = base64.b64encode(product.description.encode('utf-8')) \ if product.description else None prod = ttypes.ProductConfiguration( id=product.id, endpoint=product.endpoint, displayedName_b64=name, description_b64=descr, connection=dbc, runLimit=product.run_limit) return prod except sqlalchemy.exc.SQLAlchemyError as alchemy_ex: msg = str(alchemy_ex) LOG.error(msg) raise shared.ttypes.RequestFailed(shared.ttypes.ErrorCode.DATABASE, msg) finally: session.close()
def editProduct(self, product_id, new_config): """ Edit the given product's properties to the one specified by new_configuration. """ try: session = self.__session() product = session.query(Product).get(product_id) if product is None: msg = "Product with ID {0} does not exist!".format(product_id) LOG.error(msg) raise shared.ttypes.RequestFailed( shared.ttypes.ErrorCode.DATABASE, msg) # Editing the metadata of the product, such as display name and # description is available for product admins. # Because this query doesn't come through a product endpoint, # __init__ sets the value in the extra_args to None. self.__permission_args['productID'] = product.id self.__require_permission([permissions.PRODUCT_ADMIN]) LOG.info("User requested edit product '{0}'".format( product.endpoint)) dbc = new_config.connection if not dbc: msg = "Product's database configuration cannot be removed!" LOG.error(msg) raise shared.ttypes.RequestFailed( shared.ttypes.ErrorCode.GENERAL, msg) if new_config.endpoint != product.endpoint: if not is_valid_product_endpoint(new_config.endpoint): msg = "The endpoint to move the product to is invalid." LOG.error(msg) raise shared.ttypes.RequestFailed( shared.ttypes.ErrorCode.GENERAL, msg) if self.__server.get_product(new_config.endpoint): msg = "A product endpoint '/{0}' is already configured!" \ .format(product.endpoint) LOG.error(msg) raise shared.ttypes.RequestFailed( shared.ttypes.ErrorCode.GENERAL, msg) LOG.info("User renamed product '{0}' to '{1}'".format( product.endpoint, new_config.endpoint)) # Some values come encoded as Base64, decode these. displayed_name = base64.b64decode(new_config.displayedName_b64) \ .decode('utf-8') if new_config.displayedName_b64 \ else new_config.endpoint description = base64.b64decode(new_config.description_b64) \ .decode('utf-8') if new_config.description_b64 else None if dbc.engine == 'sqlite' and not os.path.isabs(dbc.database): # Transform the database relative path to be under the # server's config directory. dbc.database = os.path.join(self.__server.config_directory, dbc.database) # Transform arguments into a database connection string. if dbc.engine == 'postgresql': dbuser = "******" if dbc.username_b64 and dbc.username_b64 != '': dbuser = base64.b64decode(dbc.username_b64) old_connection_args = SQLServer.connection_string_to_args( product.connection) if dbc.password_b64 and dbc.password_b64 != '': dbpass = base64.b64decode(dbc.password_b64) elif 'dbpassword' in old_connection_args: # The password was not changed. Retrieving from old # configuration -- if the old configuration contained such! dbpass = old_connection_args['dbpassword'] else: dbpass = None conn_str_args = { 'postgresql': True, 'sqlite': False, 'dbaddress': dbc.host, 'dbport': dbc.port, 'dbusername': dbuser, 'dbpassword': dbpass, 'dbname': dbc.database } elif dbc.engine == 'sqlite': conn_str_args = {'postgresql': False, 'sqlite': dbc.database} else: msg = "Database engine '{0}' unknown!".format(dbc.engine) LOG.error(msg) raise shared.ttypes.RequestFailed( shared.ttypes.ErrorCode.GENERAL, msg) conn_str = SQLServer \ .from_cmdline_args(conn_str_args, IDENTIFIER, None, False, None).get_connection_string() # If endpoint or database arguments change, the product # configuration has changed so severely, that it needs # to be reconnected. product_needs_reconnect = \ product.endpoint != new_config.endpoint or \ product.connection != conn_str old_endpoint = product.endpoint if product_needs_reconnect: # Changing values that alter connection-specific data # should only be available for superusers! self.__require_permission([permissions.SUPERUSER]) # Test if the new database settings are correct or not. dummy_endpoint = new_config.endpoint + '_' + ''.join( random.sample(new_config.endpoint, min(len(new_config.endpoint), 5))) dummy_prod = Product(endpoint=dummy_endpoint, conn_str=conn_str, name=displayed_name, description=description) LOG.debug("Attempting database connection with new " "settings...") # Connect and create the database schema. self.__server.add_product(dummy_prod) LOG.debug("Product database successfully connected to.") connection_wrapper = self.__server.get_product(dummy_endpoint) if connection_wrapper.last_connection_failure: msg = "The configured connection for '/{0}' failed: {1}" \ .format(new_config.endpoint, connection_wrapper.last_connection_failure) LOG.error(msg) self.__server.remove_product(dummy_endpoint) raise shared.ttypes.RequestFailed( shared.ttypes.ErrorCode.IOERROR, msg) # The orm_prod object above is not bound to the database as it # was just created. We use the actual database-backed # configuration entry to handle connections, so a "reconnect" # is issued later. self.__server.remove_product(dummy_endpoint) # Update the settings in the database. product.endpoint = new_config.endpoint product.run_limit = new_config.runLimit product.is_review_status_change_disabled = \ new_config.isReviewStatusChangeDisabled product.connection = conn_str product.display_name = displayed_name product.description = description session.commit() LOG.info("Product configuration edited and saved successfully.") if product_needs_reconnect: product = session.query(Product).get(product_id) LOG.info("Product change requires database reconnection...") LOG.debug("Disconnecting...") self.__server.remove_product(old_endpoint) LOG.debug("Connecting new settings...") self.__server.add_product(product) LOG.info("Product reconnected successfully.") return True except sqlalchemy.exc.SQLAlchemyError as alchemy_ex: msg = str(alchemy_ex) LOG.error(msg) raise shared.ttypes.RequestFailed(shared.ttypes.ErrorCode.DATABASE, msg) finally: session.close()