def handle_mapping_post(req, name): try: rc = getIncomingJSON(req, 'prefix', 'service') logging.debug("handle_mapping_post %s: got args %s" % (name, rc.toDict())) if not rc: return rc prefix = rc.prefix service = rc.service rewrite = '/' if 'rewrite' in rc: rewrite = rc.rewrite logging.debug("handle_mapping_post %s: pfx %s => svc %s (rewrite %s)" % (name, prefix, service, rewrite)) conn = get_db("ambassador") cursor = conn.cursor() cursor.execute( 'INSERT INTO mappings VALUES(:name, :prefix, :service, :rewrite)', locals()) conn.commit() app.reconfigurator.trigger() return RichStatus.OK(name=name) except pg8000.Error as e: return RichStatus.fromError("%s: could not save info: %s" % (name, e))
def delete_mapping(self, name): if not self: return self.status try: # Have to delete modules first since it holds a foreign key rc = self._delete_mapping_modules(name) if not rc: self.conn.rollback() return rc modules_deleted = rc.modules_deleted rc = self._delete_mapping_basics(name) if not rc: self.conn.rollback() return rc self.conn.commit() return RichStatus.OK(name=name, deleted=rc.deleted, modules_deleted=modules_deleted) except pg8000.Error as e: return RichStatus.fromError( "delete_mapping %s: could not delete mapping: %s" % (name, e))
def setup(): try: conn = get_db("postgres") conn.autocommit = True cursor = conn.cursor() cursor.execute( "SELECT 1 FROM pg_database WHERE datname = 'ambassador'") results = cursor.fetchall() if not results: cursor.execute("CREATE DATABASE ambassador") conn.close() except pg8000.Error as e: return RichStatus.fromError("no ambassador database in setup: %s" % e) try: conn = get_db("ambassador") cursor = conn.cursor() cursor.execute(AMBASSADOR_TABLE_SQL) cursor.execute(PRINCIPAL_TABLE_SQL) conn.commit() conn.close() except pg8000.Error as e: return RichStatus.fromError("no data tables in setup: %s" % e) return RichStatus.OK()
def delete_consumer(self, consumer_id): if not self: return self.status try: # Have to delete modules first since it holds a foreign key rc = self._delete_consumer_modules(consumer_id) if not rc: self.conn.rollback() return rc modules_deleted = rc.modules_deleted rc = self._delete_consumer_basics(consumer_id) if not rc: self.conn.rollback() return rc self.conn.commit() return RichStatus.OK(consumer_id=consumer_id, deleted=rc.deleted, modules_deleted=modules_deleted) except pg8000.Error as e: return RichStatus.fromError( "delete_consumer %s: could not delete consumer: %s" % (consumer_id, e))
def getIncomingJSON(req, *needed): """ Pull incoming arguments from JSON into a RichStatus. 'needed' specifies keys that are mandatory, but _all keys are converted_, so for any optional things, just check if the key is present in the returned RichStatus. If any 'needed' keys are missing, returns a False RichStatus including error text. If all 'needed' keys are present, returns a True RichStatus with elements copied from the input. """ try: incoming = req.get_json() except Exception as e: return RichStatus.fromError("invalid JSON: %s" % e) logging.debug("getIncomingJSON: %s" % incoming) if not incoming: incoming = {} missing = [] for key in needed: if key not in incoming: missing.append(key) if missing: return RichStatus.fromError("Required fields missing: %s" % " ".join(missing)) else: return RichStatus.OK(**incoming)
def handle_principal_post(req, name): try: rc = getIncomingJSON(req, 'fingerprint') logging.debug("handle_principal_post %s: got args %s" % (name, rc.toDict())) if not rc: return rc fingerprint = rc.fingerprint logging.debug("handle_principal_post %s: fingerprint %s" % (name, fingerprint)) conn = get_db("ambassador") cursor = conn.cursor() cursor.execute('INSERT INTO principals VALUES(:name, :fingerprint)', locals()) conn.commit() return RichStatus.OK(name=name) except pg8000.Error as e: return RichStatus.fromError("%s: could not save info: %s" % (name, e))
def _fetch_consumer_basics(self, where): if not self: return self.status try: sql = "SELECT consumer_id, username, fullname, shortname FROM consumers WHERE %s" % where.sql self.cursor.execute(sql, where.keys) if self.cursor.rowcount == 0: return RichStatus.fromError("consumer %s not found" % where.hr) if self.cursor.rowcount > 1: return RichStatus.fromError( "consumer %s matched more than one entry?" % where.hr) # We know there's exactly one consumer match. Good. consumer_id, username, fullname, shortname = self.cursor.fetchone() return RichStatus.OK(consumer_id=consumer_id, username=username, fullname=fullname, shortname=shortname) except pg8000.Error as e: return RichStatus.fromError( "fetch_consumer_basics %s: could not fetch info: %s" % (where.hr, e))
def store_consumer(self, consumer_id, username, fullname, shortname, modules): if not self: return self.status try: rc = self._store_consumer_basics(consumer_id, username, fullname, shortname) if not rc: self.conn.rollback() return rc for module_name in modules.keys(): rc = self._store_consumer_module(consumer_id, module_name, modules[module_name]) if not rc: self.conn.rollback() return rc self.conn.commit() return RichStatus.OK(consumer_id=consumer_id) except pg8000.Error as e: return RichStatus.fromError( "store_consumer %s: could not store consumer: %s" % (consumer_id, e))
def _fetch_mapping_basics(self, name): if not self: return self.status try: self.cursor.execute( "SELECT prefix, service, rewrite FROM mappings WHERE name = :name", locals()) if self.cursor.rowcount == 0: return RichStatus.fromError("mapping %s not found" % name) if self.cursor.rowcount > 1: return RichStatus.fromError( "mapping %s matched more than one entry?" % name) # We know there's exactly one mapping match. Good. prefix, service, rewrite = self.cursor.fetchone() return RichStatus.OK(name=name, prefix=prefix, service=service, rewrite=rewrite) except pg8000.Error as e: return RichStatus.fromError( "fetch_mapping_basics %s: could not fetch info: %s" % (name, e))
def handle_service_get(req, name): try: conn = get_db("ambassador") cursor = conn.cursor() cursor.execute("SELECT prefix FROM services WHERE name = :name", locals()) [ prefix ] = cursor.fetchone() return RichStatus.OK(name=name, prefix=prefix) except pg8000.Error as e: return RichStatus.fromError("%s: could not fetch info: %s" % (name, e))
def wrapper(*args, **kwds): rc = RichStatus.fromError("impossible error") logging.debug("%s: method %s" % (func_name, request.method)) try: rc = f(*args, **kwds) except Exception as e: logging.exception(e) rc = RichStatus.fromError("%s: %s failed: %s" % (func_name, request.method, e)) return jsonify(rc.toDict())
def handle_principal_get(req, name): try: conn = get_db("ambassador") cursor = conn.cursor() cursor.execute("SELECT fingerprint FROM principals WHERE name = :name", locals()) [fingerprint] = cursor.fetchone() return RichStatus.OK(name=name, fingerprint=fingerprint) except pg8000.Error as e: return RichStatus.fromError("%s: could not fetch info: %s" % (name, e))
def store_principal(self, name, fingerprint): if not self: return self.status try: self.cursor.execute( 'INSERT INTO principals VALUES(:name, :fingerprint)', locals()) self.conn.commit() return RichStatus.OK(name=name) except pg8000.Error as e: return RichStatus.fromError( "store_principal %s: could not save info: %s" % (name, e))
def handle_service_del(req, name): try: conn = get_db("ambassador") cursor = conn.cursor() cursor.execute("DELETE FROM services WHERE name = :name", locals()) conn.commit() app.reconfigurator.trigger() return RichStatus.OK(name=name) except pg8000.Error as e: return RichStatus.fromError("%s: could not delete service: %s" % (name, e))
def _delete_mapping_basics(self, name): if not self: return self.status try: self.cursor.execute("DELETE FROM mappings WHERE name = :name", locals()) deleted = self.cursor.rowcount return RichStatus.OK(name=name, deleted=deleted) except pg8000.Error as e: return RichStatus.fromError( "delete_mapping %s: could not delete mapping info: %s" % (name, e))
def delete_module(self, name): if not self: return self.status try: self.cursor.execute("DELETE FROM modules WHERE name = :name", locals()) deleted = self.cursor.rowcount self.conn.commit() return RichStatus.OK(name=name, deleted=deleted) except pg8000.Error as e: return RichStatus.fromError( "delete_module %s: could not delete module: %s" % (name, e))
def fetch_all_modules(self): if not self: return self.status try: self.cursor.execute( "SELECT name, config FROM modules ORDER BY name") modules = {name: config for name, config in self.cursor} return RichStatus.OK(modules=modules, count=len(modules.keys())) except pg8000.Error as e: return RichStatus.fromError( "fetch_all_modules: could not fetch info: %s" % e)
def _delete_consumer_basics(self, consumer_id): if not self: return self.status try: self.cursor.execute( "DELETE FROM consumers WHERE consumer_id = :consumer_id", locals()) deleted = self.cursor.rowcount return RichStatus.OK(consumer_id=consumer_id, deleted=deleted) except pg8000.Error as e: return RichStatus.fromError( "delete_consumer %s: could not delete consumer info: %s" % (consumer_id, e))
def fetch_all_services(): try: conn = get_db("ambassador") cursor = conn.cursor() cursor.execute("SELECT name, prefix FROM services ORDER BY name, prefix") services = [] for name, prefix in cursor: services.append({ 'name': name, 'prefix': prefix }) return RichStatus.OK(services=services, count=len(services)) except pg8000.Error as e: return RichStatus.fromError("services: could not fetch info: %s" % e)
def generate_envoy_config(self, template=None, template_dir=None): # Finally! Render the template to JSON... envoy_json = self.to_json(template=template, template_dir=template_dir) rc = RichStatus.fromError("impossible") # ...and use the JSON parser as a final sanity check. try: obj = json.loads(envoy_json) rc = RichStatus.OK(msg="Envoy configuration OK", envoy_config=obj) except json.decoder.JSONDecodeError as e: rc = RichStatus.fromError("Invalid Envoy configuration: %s" % str(e), raw=envoy_json, exception=e) return rc
def new_config(envoy_base_config=None, envoy_tls_config=None, envoy_config_path=None, envoy_restarter_pid=None): if not envoy_base_config: envoy_base_config = app.envoy_base_config if not envoy_tls_config: envoy_tls_config = app.envoy_tls_config if not envoy_config_path: envoy_config_path = app.envoy_config_path if not envoy_restarter_pid: envoy_restarter_pid = app.envoy_restarter_pid config = EnvoyConfig(envoy_base_config, envoy_tls_config) rc = fetch_all_services() num_services = 0 if rc and rc.services: num_services = len(rc.services) for service in rc.services: config.add_service(service['name'], service['prefix']) config.write_config(envoy_config_path) if envoy_restarter_pid > 0: os.kill(envoy_restarter_pid, signal.SIGHUP) return RichStatus.OK(count=num_services)
def __init__(self): pg8000.paramstyle = 'named' self.status = RichStatus.OK() # Make sure we have tables and such. # # All of these functions update self.status if something goes wrong, and they're # no-ops if not self.status. self.conn = self._get_connection() logging.info("storage_postgres: conn %sset, status %s" % ("NOT " if not self.conn else "", self.status)) # Get a cursor and verify our database. self.cursor = self._get_cursor() self._verify_database() # Switch autocommit off... self._autocommit(False) # ...grab a new cursor... self.cursor = self._get_cursor() # ...and make sure our tables are OK. self._verify_tables()
def generate_envoy_config(self, template=None, template_dir=None, **kwargs): # Finally! Render the template to JSON... envoy_json = self.to_json(template=template, template_dir=template_dir) # We used to use the JSON parser as a final sanity check here. That caused # Forge some issues, so it's turned off for now. # rc = RichStatus.fromError("impossible") # # ...and use the JSON parser as a final sanity check. # try: # obj = json.loads(envoy_json) # rc = RichStatus.OK(msg="Envoy configuration OK", envoy_config=obj) # except json.decoder.JSONDecodeError as e: # rc = RichStatus.fromError("Invalid Envoy configuration: %s" % str(e), # raw=envoy_json, exception=e) # Go ahead and report that we generated an Envoy config, if we can. scout_result = AmbassadorConfig.scout_report(action="config", result=True, generated=True, **kwargs) rc = RichStatus.OK(envoy_config=envoy_json, scout_result=scout_result) self.logger.debug("Scout reports %s" % json.dumps(rc.scout_result)) return rc
def _get_connection(self, autocommit=False): # Figure out where the DB lives... self.db_name = "postgres" self.db_host = "ambassador-store" self.db_port = 5432 if "AMBASSADOR_DB_NAME" in os.environ: self.db_name = os.environ["AMBASSADOR_DB_NAME"] if "AMBASSADOR_DB_HOST" in os.environ: self.db_host = os.environ["AMBASSADOR_DB_HOST"] if "AMBASSADOR_DB_PORT" in os.environ: self.db_port = int(os.environ["AMBASSADOR_DB_PORT"]) conn = None try: conn = pg8000.connect(user="******", password="******", database=self.db_name, host=self.db_host, port=self.db_port) # Start with autocommit on. conn.autocommit = True except pg8000.Error as e: self.status = RichStatus.fromError( "could not connect to db %s:%d - %s" % (self.db_host, self.db_port, e)) return conn
def process_object(self, obj): obj_version = obj['apiVersion'] obj_kind = obj['kind'] obj_name = obj['name'] # ...save the source info... source_key = "%s.%d" % (self.filename, self.ocount) self.sources[source_key] = { 'kind': obj_kind, 'version': obj_version, 'name': obj_name, 'filename': self.filename, 'index': self.ocount, 'yaml': yaml.safe_dump(obj, default_flow_style=False) } source_map = self.source_map.setdefault(self.filename, {}) source_map[source_key] = True # OK. What is this thing? rc = self.validate_object(obj) if not rc: # Well that's no good. return rc # OK, so far so good. Grab the handler for this object type. handler_name = "handle_%s" % obj_kind.lower() handler = getattr(self, handler_name, None) if not handler: handler = self.save_object self.logger.warning("%s[%d]: no handler for %s, just saving" % (self.filename, self.ocount, obj_kind)) else: self.logger.debug("%s[%d]: handling %s..." % (self.filename, self.ocount, obj_kind)) try: handler(source_key, obj, obj_name, obj_kind, obj_version) except Exception as e: # Bzzzt. return RichStatus.fromError("could not process %s object: %s" % (obj_kind, e)) # OK, all's well. return RichStatus.OK(msg="%s object processed successfully" % obj_kind)
def _store_mapping_basics(self, name, prefix, service, rewrite): if not self: return self.status try: self.cursor.execute( ''' INSERT INTO mappings VALUES(:name, :prefix, :service, :rewrite) ON CONFLICT (name) DO UPDATE SET name=EXCLUDED.name, prefix=EXCLUDED.prefix, service=EXCLUDED.service, rewrite=EXCLUDED.rewrite ''', locals()) return RichStatus.OK(name=name) except pg8000.Error as e: return RichStatus.fromError( "store_mapping %s: could not save info: %s" % (name, e))
def validate_object(self, obj): # Each object must be a dict, and must include "apiVersion" # and "type" at toplevel. if not isinstance(obj, collections.Mapping): return RichStatus.fromError("not a dictionary") if not (("apiVersion" in obj) and ("kind" in obj) and ("name" in obj)): return RichStatus.fromError("must have apiVersion, kind, and name") obj_version = obj['apiVersion'] obj_kind = obj['kind'] obj_name = obj['name'] if obj_version.startswith("ambassador/"): obj_version = obj_version.split('/')[1] else: return RichStatus.fromError("apiVersion %s unsupported" % obj_version) schema_key = "%s-%s" % (obj_version, obj_kind) schema = self.schemas.get(schema_key, None) if not schema: schema_path = os.path.join(self.schema_dir_path, obj_version, "%s.schema" % obj_kind) try: schema = json.load(open(schema_path, "r")) except OSError: self.logger.debug("no schema at %s, skipping" % schema_path) except json.decoder.JSONDecodeError as e: self.logger.warning("corrupt schema at %s, skipping (%s)" % (schema_path, e)) if schema: self.schemas[schema_key] = schema try: jsonschema.validate(obj, schema) except jsonschema.exceptions.ValidationError as e: return RichStatus.fromError("not a valid %s: %s" % (obj_kind, e)) return RichStatus.OK(msg="valid %s" % obj_kind, details=(obj_kind, obj_version, obj_name))
def fetch_all_principals(): try: conn = get_db("ambassador") cursor = conn.cursor() cursor.execute( "SELECT name, fingerprint FROM principals ORDER BY name, fingerprint" ) principals = [] for name, fingerprint in cursor: principals.append({'name': name, 'fingerprint': fingerprint}) return RichStatus.OK(principals=principals, count=len(principals)) except pg8000.Error as e: return RichStatus.fromError("principals: could not fetch info: %s" % e)
def fetch_mapping_module(self, name, module_name=None): if not self: return self.status if not module_name: return RichStatus.fromError( "fetch_mapping_module: module_name is required") return self._fetch_mapping_modules(name, module_name=module_name)
def fetch_all_principals(self): if not self: return self.status try: self.cursor.execute( "SELECT name, fingerprint FROM principals ORDER BY name, fingerprint" ) principals = [] for name, fingerprint in self.cursor: principals.append({'name': name, 'fingerprint': fingerprint}) return RichStatus.OK(principals=principals, count=len(principals)) except pg8000.Error as e: return RichStatus.fromError( "fetch_all_principals: could not fetch info: %s" % e)