diff --git a/releasenotes/notes/postgresql-use-proper-guestagent-models-7ba601c7b4c001d6.yaml b/releasenotes/notes/postgresql-use-proper-guestagent-models-7ba601c7b4c001d6.yaml new file mode 100644 index 0000000000..d68ff52302 --- /dev/null +++ b/releasenotes/notes/postgresql-use-proper-guestagent-models-7ba601c7b4c001d6.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - Implement Postgres guestagent models for + databases and users. + - Implement RootController extension for the Postgres + datastore. diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 71500d4634..51f04101ae 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -1043,7 +1043,8 @@ postgresql_opts = [ cfg.ListOpt('ignore_users', default=['os_admin', 'postgres', 'root']), cfg.ListOpt('ignore_dbs', default=['postgres']), cfg.StrOpt('root_controller', - default='trove.extensions.common.service.DefaultRootController', + default='trove.extensions.postgresql.service' + '.PostgreSQLRootController', help='Root controller implementation for postgresql.'), cfg.StrOpt('guest_log_exposed_logs', default='general', help='List of Guest Logs to expose for publishing.'), diff --git a/trove/extensions/postgresql/__init__.py b/trove/extensions/postgresql/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/extensions/postgresql/service.py b/trove/extensions/postgresql/service.py new file mode 100644 index 0000000000..dc85023c72 --- /dev/null +++ b/trove/extensions/postgresql/service.py @@ -0,0 +1,29 @@ +# Copyright 2015 Tesora Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from trove.extensions.common.service import DefaultRootController +from trove.extensions.mysql import models +from trove.guestagent.db import models as guest_models + + +class PostgreSQLRootController(DefaultRootController): + + def _find_root_user(self, context, instance_id): + user = guest_models.PostgreSQLRootUser() + # This is currently using MySQL model. + # MySQL extension *should* work for now, but may lead to + # future bugs (incompatible input validation, unused field etc). + return models.User.load( + context, instance_id, user.name, user.host, root_user=True) diff --git a/trove/guestagent/db/models.py b/trove/guestagent/db/models.py index 53a89bab47..191e674647 100644 --- a/trove/guestagent/db/models.py +++ b/trove/guestagent/db/models.py @@ -18,6 +18,7 @@ import re import string import netaddr +from six import u from trove.common import cfg from trove.common import exception @@ -231,6 +232,37 @@ class CouchDBSchema(DatastoreSchema): return ['_name'] +class PostgreSQLSchema(DatastoreSchema): + """Represents a PostgreSQL schema and its associated properties. + + Permitted characters in quoted identifiers include the full + Unicode Basic Multilingual Plane (BMP), except U+0000. + Database, table, and column names cannot end with space characters. + """ + name_regex = re.compile(u(r'^[\u0001-\u007F\u0080-\uFFFF]+[^\s]$')) + + def __init__(self, name=None, deserializing=False): + super(PostgreSQLSchema, self).__init__() + if not (bool(deserializing) != bool(name)): + raise ValueError(_("Bad args. name: %(name)s, " + "deserializing %(deser)s.") + % ({'name': bool(name), + 'deser': bool(deserializing)})) + if not deserializing: + self.name = name + + @property + def _max_schema_name_length(self): + return 63 + + def _is_valid_schema_name(self, value): + return self.name_regex.match(value) is not None + + @classmethod + def _dict_requirements(cls): + return ['_name'] + + class MySQLDatabase(Base): """Represents a Database and its properties.""" @@ -1010,8 +1042,49 @@ class MySQLUser(Base): self._host = value +class PostgreSQLUser(DatastoreUser): + """Represents a PostgreSQL user and its associated properties.""" + + def __init__(self, name=None, password=None, deserializing=False): + super(PostgreSQLUser, self).__init__() + + if ((not (bool(deserializing) != bool(name))) or + (bool(deserializing) and bool(password))): + raise ValueError(_("Bad args. name: %(name)s, " + "password %(pass)s, " + "deserializing %(deser)s.") + % ({'name': bool(name), + 'pass': bool(password), + 'deser': bool(deserializing)})) + + if not deserializing: + self.name = name + self.password = password + + def _build_database_schema(self, name): + return PostgreSQLSchema(name) + + @property + def _max_username_length(self): + return 63 + + def _is_valid_name(self, value): + return True + + def _is_valid_host_name(self, value): + return True + + def _is_valid_password(self, value): + return True + + @classmethod + def _dict_requirements(cls): + return ['name'] + + class RootUser(MySQLUser): """Overrides _ignore_users from the MySQLUser class.""" + def __init__(self): self._ignore_users = [] @@ -1037,3 +1110,11 @@ class CassandraRootUser(CassandraUser): password = utils.generate_random_password() super(CassandraRootUser, self).__init__("cassandra", password=password, *args, **kwargs) + + +class PostgreSQLRootUser(PostgreSQLUser): + """Represents the PostgreSQL default superuser.""" + + def __init__(self, password=None): + password = password if not None else utils.generate_random_password() + super(PostgreSQLRootUser, self).__init__("postgres", password=password)