summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Van Dijk <mvandijk@tesora.com>2015-10-14 14:47:11 -0400
committerMatt Van Dijk <mvandijk@tesora.com>2016-10-12 11:51:57 -0400
commit1c819c3e37e6019464ed59d90bf3d8c83581202d (patch)
treed6bcd6fa89645c24f6dd6d0e35250e789ce7e5e4
parentd6fabc850493bd13d647fbf1084fd092836f74ee (diff)
downloadtrove-1c819c3e37e6019464ed59d90bf3d8c83581202d.tar.gz
Improve guestagent datastore models
All validation of requests is currently done against the MySQL datastore models. In order to switch to using datastore extensions for validation the models need to be refactored. The current approach to guestagent/db/models.py is getting unruly and is difficult to apply to other datastores. This change moves the models from the guestagent package to the common package, so that the extensions package can use it in the future. That is: guestagent/db/models.py -> common/db/models.py The generic models are separated from the datastore-specific models. For datastores with custom models the classes have been moved to their own sub-package. Example: common/db/mysql/models.py Using this new approach any datastores that want custom models just need to create packages similar to the common/db/mysql/ package. The code references to these models have all been updated. Non-MySQL based datastores using these references have been switched to using the generic models. The tests for these models have been improved. Change-Id: If321202a3ec4ab0f57ee8e516b885a8307d464b7 Partial-Bug: 1498573
-rw-r--r--trove/common/db/__init__.py (renamed from trove/guestagent/db/__init__.py)0
-rw-r--r--trove/common/db/cassandra/__init__.py0
-rw-r--r--trove/common/db/cassandra/models.py45
-rw-r--r--trove/common/db/couchdb/__init__.py0
-rw-r--r--trove/common/db/couchdb/models.py32
-rw-r--r--trove/common/db/models.py438
-rw-r--r--trove/common/db/mongodb/__init__.py0
-rw-r--r--trove/common/db/mongodb/models.py158
-rw-r--r--trove/common/db/mysql/__init__.py0
-rw-r--r--trove/common/db/mysql/data.py244
-rw-r--r--trove/common/db/mysql/models.py170
-rw-r--r--trove/common/db/postgresql/__init__.py0
-rw-r--r--trove/common/db/postgresql/models.py70
-rw-r--r--trove/extensions/cassandra/service.py4
-rw-r--r--trove/extensions/common/models.py7
-rw-r--r--trove/extensions/mysql/common.py12
-rw-r--r--trove/extensions/mysql/models.py28
-rw-r--r--trove/extensions/mysql/service.py63
-rw-r--r--trove/extensions/postgresql/service.py4
-rw-r--r--trove/guestagent/datastore/experimental/cassandra/service.py43
-rw-r--r--trove/guestagent/datastore/experimental/couchbase/service.py7
-rw-r--r--trove/guestagent/datastore/experimental/couchdb/service.py14
-rw-r--r--trove/guestagent/datastore/experimental/db2/service.py47
-rw-r--r--trove/guestagent/datastore/experimental/mongodb/service.py127
-rw-r--r--trove/guestagent/datastore/experimental/postgresql/manager.py2
-rw-r--r--trove/guestagent/datastore/experimental/postgresql/service.py18
-rw-r--r--trove/guestagent/datastore/experimental/vertica/service.py7
-rw-r--r--trove/guestagent/datastore/mysql_common/service.py62
-rw-r--r--trove/guestagent/db/models.py1069
-rw-r--r--trove/guestagent/strategies/backup/experimental/db2_impl.py4
-rw-r--r--trove/guestagent/strategies/replication/experimental/postgresql_impl.py2
-rw-r--r--trove/guestagent/strategies/replication/mysql_base.py14
-rw-r--r--trove/tests/unittests/common/test_dbmodels.py342
-rw-r--r--trove/tests/unittests/guestagent/test_cassandra_manager.py4
-rw-r--r--trove/tests/unittests/guestagent/test_dbaas.py11
-rw-r--r--trove/tests/unittests/guestagent/test_dbmodels.py112
-rw-r--r--trove/tests/unittests/guestagent/test_mongodb_manager.py39
37 files changed, 1749 insertions, 1450 deletions
diff --git a/trove/guestagent/db/__init__.py b/trove/common/db/__init__.py
index e69de29b..e69de29b 100644
--- a/trove/guestagent/db/__init__.py
+++ b/trove/common/db/__init__.py
diff --git a/trove/common/db/cassandra/__init__.py b/trove/common/db/cassandra/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/common/db/cassandra/__init__.py
diff --git a/trove/common/db/cassandra/models.py b/trove/common/db/cassandra/models.py
new file mode 100644
index 00000000..ad69aa17
--- /dev/null
+++ b/trove/common/db/cassandra/models.py
@@ -0,0 +1,45 @@
+# Copyright 2016 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.common.db import models
+
+
+class CassandraSchema(models.DatastoreSchema):
+ """Represents a Cassandra schema and its associated properties.
+
+ Keyspace names are 32 or fewer alpha-numeric characters and underscores,
+ the first of which is an alpha character.
+ """
+
+ @property
+ def _max_schema_name_length(self):
+ return 32
+
+ def _is_valid_schema_name(self, value):
+ return not any(c in value for c in '/\. "$')
+
+
+class CassandraUser(models.DatastoreUser):
+ """Represents a Cassandra user and its associated properties."""
+
+ root_username = 'cassandra'
+
+ @property
+ def _max_user_name_length(self):
+ return 65535
+
+ @property
+ def schema_model(self):
+ return CassandraSchema
diff --git a/trove/common/db/couchdb/__init__.py b/trove/common/db/couchdb/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/common/db/couchdb/__init__.py
diff --git a/trove/common/db/couchdb/models.py b/trove/common/db/couchdb/models.py
new file mode 100644
index 00000000..30e1b315
--- /dev/null
+++ b/trove/common/db/couchdb/models.py
@@ -0,0 +1,32 @@
+# Copyright 2016 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.common.db import models
+
+
+class CouchDBSchema(models.DatastoreSchema):
+ """Represents a CouchDB schema and its associated properties."""
+
+ @property
+ def _max_schema_name_length(self):
+ return 32
+
+
+class CouchDBUser(models.DatastoreUser):
+ """Represents a CouchDB user and its associated properties."""
+
+ @property
+ def schema_model(self):
+ return CouchDBSchema
diff --git a/trove/common/db/models.py b/trove/common/db/models.py
new file mode 100644
index 00000000..232a7b40
--- /dev/null
+++ b/trove/common/db/models.py
@@ -0,0 +1,438 @@
+# Copyright 2016 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.
+
+import abc
+
+from trove.common import cfg
+from trove.common.i18n import _
+from trove.common import utils
+
+CONF = cfg.CONF
+
+"""
+The classes below are generic and can be used for any datastore, but will not
+provide validation. To add a new datastore create a sub-package (see mysql for
+example) and create new child classes inheriting from these generic classes.
+
+As a guideline, for new datastores the following class methods/variables should
+be overridden if validation is desired (see their docstrings for additional
+info):
+
+DatastoreModelsBase:
+ __init__
+
+DatastoreSchema:
+ _max_schema_name_length
+ _is_valid_schema_name
+ verify_dict
+ _create_checks
+ _delete_checks
+
+DatastoreUser:
+ _is_valid_user_name
+ _is_valid_host_name
+ _is_valid_password
+ _is_valid_database
+ verify_dict
+ _create_checks
+ _delete_checks
+"""
+
+
+class DatastoreModelsBase(object):
+ """Base model for the datastore schema and user models."""
+
+ def serialize(self):
+ return self.__dict__
+
+ def _deserialize(self, obj):
+ self.__dict__ = obj
+
+ def __repr__(self):
+ return str(self.serialize())
+
+ @classmethod
+ def deserialize(cls, value, verify=True):
+ item = cls(deserializing=True)
+ item._deserialize(value)
+ if verify:
+ item.verify_dict()
+ return item
+
+ @abc.abstractmethod
+ def verify_dict(self):
+ """Validate the object's data dictionary.
+ :returns: True if dictionary is valid.
+ """
+
+ @staticmethod
+ def check_string(value, desc):
+ """Check if the value is a string/unicode.
+ :param value: Value to check.
+ :param desc: Description for exception message.
+ :raises: ValueError if not a string/unicode.
+ """
+ if not (isinstance(value, str) or
+ isinstance(value, unicode)):
+ raise ValueError(_("%(desc)s is not a string. Type = %(t)s.")
+ % {'desc': desc, 't': type(value)})
+
+
+class DatastoreSchema(DatastoreModelsBase):
+ """Represents a database schema."""
+
+ def __init__(self, name=None, deserializing=False):
+ self._name = None
+ self._collate = None
+ self._character_set = None
+ # If both or neither are passed in this is a bug.
+ if bool(deserializing) == bool(name):
+ raise RuntimeError("Bug in DatastoreSchema()")
+ if not deserializing:
+ self.name = name
+
+ def __str__(self):
+ return str(self.name)
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ self._validate_schema_name(value)
+ self._name = value
+
+ def _validate_schema_name(self, value):
+ """Perform checks on a given schema name.
+ :param value: Validated schema name.
+ :type value: string
+ :raises: ValueError On validation errors.
+ """
+ if not value:
+ raise ValueError(_("Schema name empty."))
+ self.check_string(value, 'Schema name')
+ if self._max_schema_name_length and (len(value) >
+ self._max_schema_name_length):
+ raise ValueError(_("Schema name '%(name)s' is too long. "
+ "Max length = %(max_length)d.")
+ % {'name': value,
+ 'max_length': self._max_schema_name_length})
+ elif not self._is_valid_schema_name(value):
+ raise ValueError(_("'%s' is not a valid schema name.") % value)
+
+ @property
+ def _max_schema_name_length(self):
+ """Return the maximum valid schema name length if any.
+ :returns: Maximum schema name length or None if unlimited.
+ """
+ return None
+
+ def _is_valid_schema_name(self, value):
+ """Validate a given schema name.
+ :param value: Validated schema name.
+ :type value: string
+ :returns: TRUE if valid, FALSE otherwise.
+ """
+ return True
+
+ def verify_dict(self):
+ """Check that the object's dictionary values are valid by reloading
+ them via the property setters. The checkers should raise the
+ ValueError exception if invalid. All mandatory fields should be
+ checked.
+ """
+ self.name = self._name
+
+ @property
+ def ignored_dbs(self):
+ return cfg.get_ignored_dbs()
+
+ def is_ignored(self):
+ return self.name in self.ignored_dbs
+
+ def check_reserved(self):
+ """Check if the name is on the ignore_dbs list, meaning it is
+ reserved.
+ :raises: ValueError if name is on the reserved list.
+ """
+ if self.is_ignored():
+ raise ValueError(_('Database name "%(name)s" is on the reserved'
+ 'list: %(reserved)s.')
+ % {'name': self.name,
+ 'reserved': self.ignored_dbs})
+
+ def _create_checks(self):
+ """Checks to be performed before database can be created."""
+ self.check_reserved()
+
+ def check_create(self):
+ """Check if the database can be created.
+ :raises: ValueError if the schema is not valid for create.
+ """
+ try:
+ self._create_checks()
+ except ValueError as e:
+ raise ValueError(_('Cannot create database: %(error)s')
+ % {'error': str(e)})
+
+ def _delete_checks(self):
+ """Checks to be performed before database can be deleted."""
+ self.check_reserved()
+
+ def check_delete(self):
+ """Check if the database can be deleted.
+ :raises: ValueError if the schema is not valid for delete.
+ """
+ try:
+ self._delete_checks()
+ except ValueError as e:
+ raise ValueError(_('Cannot delete database: %(error)s')
+ % {'error': str(e)})
+
+
+class DatastoreUser(DatastoreModelsBase):
+ """Represents a datastore user."""
+
+ _HOSTNAME_WILDCARD = '%'
+ root_username = 'root'
+
+ def __init__(self, name=None, password=None, host=None, databases=None,
+ deserializing=False):
+ self._name = None
+ self._password = None
+ self._host = self._HOSTNAME_WILDCARD
+ self._databases = []
+ self._is_root = False
+ if not deserializing:
+ self.name = name
+ if password:
+ self.password = password
+ if host:
+ self.host = host
+ if databases:
+ self.databases = databases
+
+ @classmethod
+ def root(cls, name=None, password=None, *args, **kwargs):
+ if not name:
+ name = cls.root_username
+ if not password:
+ password = utils.generate_random_password()
+ user = cls(name, password, *args, **kwargs)
+ user.make_root()
+ return user
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ self._validate_user_name(value)
+ self._name = value
+
+ @property
+ def password(self):
+ return self._password
+
+ @password.setter
+ def password(self, value):
+ self.check_string(value, "User password")
+ if self._is_valid_password(value):
+ self._password = value
+ else:
+ raise ValueError(_("'%s' is not a valid password.") % value)
+
+ def _add_database(self, value):
+ serial_db = self._build_database_schema(value).serialize()
+ if self._is_valid_database(serial_db):
+ self._databases.append(serial_db)
+
+ @property
+ def databases(self):
+ return self._databases
+
+ @databases.setter
+ def databases(self, value):
+ if isinstance(value, list):
+ for dbname in value:
+ self._add_database(dbname)
+ else:
+ self._add_database(value)
+
+ @property
+ def host(self):
+ if self._host is None:
+ return self._HOSTNAME_WILDCARD
+ return self._host
+
+ @host.setter
+ def host(self, value):
+ self.check_string(value, "User host name")
+ if self._is_valid_host_name(value):
+ self._host = value
+ else:
+ raise ValueError(_("'%s' is not a valid hostname.") % value)
+
+ def _build_database_schema(self, name):
+ """Build a schema for this user.
+ :type name: string
+ """
+ return self.schema_model(name)
+
+ def deserialize_schema(self, value):
+ """Deserialize a user's databases value.
+ :type value: dict
+ """
+ return self.schema_model.deserialize(value)
+
+ def _validate_user_name(self, value):
+ """Perform validations on a given user name.
+ :param value: Validated user name.
+ :type value: string
+ :raises: ValueError On validation errors.
+ """
+ if not value:
+ raise ValueError(_("User name empty."))
+ self.check_string(value, "User name")
+ if self._max_user_name_length and (len(value) >
+ self._max_user_name_length):
+ raise ValueError(_("User name '%(name)s' is too long. "
+ "Max length = %(max_length)d.")
+ % {'name': value,
+ 'max_length': self._max_user_name_length})
+ elif not self._is_valid_user_name(value):
+ raise ValueError(_("'%s' is not a valid user name.") % value)
+
+ @property
+ def _max_user_name_length(self):
+ """Return the maximum valid user name length if any.
+ :returns: Maximum user name length or None if unlimited.
+ """
+ return None
+
+ def _is_valid_user_name(self, value):
+ """Validate a given user name.
+ :param value: User name to be validated.
+ :type value: string
+ :returns: TRUE if valid, FALSE otherwise.
+ """
+ return True
+
+ def _is_valid_host_name(self, value):
+ """Validate a given host name.
+ :param value: Host name to be validated.
+ :type value: string
+ :returns: TRUE if valid, FALSE otherwise.
+ """
+ return True
+
+ def _is_valid_password(self, value):
+ """Validate a given password.
+ :param value: Password to be validated.
+ :type value: string
+ :returns: TRUE if valid, FALSE otherwise.
+ """
+ return True
+
+ def _is_valid_database(self, value):
+ """Validate a given database (serialized schema object).
+ :param value: The database to be validated.
+ :type value: dict
+ :returns: TRUE if valid, FALSE otherwise.
+ :raises: ValueError if operation not allowed.
+ """
+ return value not in self.databases
+
+ def verify_dict(self):
+ """Check that the object's dictionary values are valid by reloading
+ them via the property setters. The checkers should raise the
+ ValueError exception if invalid. All mandatory fields should be
+ checked.
+ """
+ self.name = self._name
+ if self.__dict__.get('_password'):
+ self.password = self._password
+ else:
+ self._password = None
+ if self.__dict__.get('_host'):
+ self.host = self._host
+ else:
+ self._host = self._HOSTNAME_WILDCARD
+ if self.__dict__.get('_databases'):
+ for database in self._databases:
+ # Create the schema for validation only
+ self.deserialize_schema(database)
+ else:
+ self._databases = []
+ if not self.__dict__.get('_is_root'):
+ self._is_root = False
+
+ @property
+ def schema_model(self):
+ return DatastoreSchema
+
+ @property
+ def ignored_users(self):
+ if self._is_root:
+ return []
+ return cfg.get_ignored_users()
+
+ @property
+ def is_ignored(self):
+ return self.name in self.ignored_users
+
+ def make_root(self):
+ self._is_root = True
+
+ def check_reserved(self):
+ """Check if the name is on the ignore_users list, meaning it is
+ reserved.
+ :raises: ValueError if name is on the reserved list.
+ """
+ if self.is_ignored:
+ raise ValueError(_('User name "%(name)s" is on the reserved '
+ 'list: %(reserved)s.')
+ % {'name': self.name,
+ 'reserved': self.ignored_users})
+
+ def _create_checks(self):
+ """Checks to be performed before user can be created."""
+ self.check_reserved()
+
+ def check_create(self):
+ """Check if the user can be created.
+ :raises: ValueError if the user is not valid for create.
+ """
+ try:
+ self._create_checks()
+ except ValueError as e:
+ raise ValueError(_('Cannot create user: %(error)s')
+ % {'error': str(e)})
+
+ def _delete_checks(self):
+ """Checks to be performed before user can be created."""
+ self.check_reserved()
+
+ def check_delete(self):
+ """Check if the user can be deleted.
+ :raises: ValueError if the user is not valid for delete.
+ """
+ try:
+ self._delete_checks()
+ except ValueError as e:
+ raise ValueError(_('Cannot delete user: %(error)s')
+ % {'error': str(e)})
diff --git a/trove/common/db/mongodb/__init__.py b/trove/common/db/mongodb/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/common/db/mongodb/__init__.py
diff --git a/trove/common/db/mongodb/models.py b/trove/common/db/mongodb/models.py
new file mode 100644
index 00000000..9185c73d
--- /dev/null
+++ b/trove/common/db/mongodb/models.py
@@ -0,0 +1,158 @@
+# Copyright 2016 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.common.db import models
+from trove.common.i18n import _
+
+
+class MongoDBSchema(models.DatastoreSchema):
+ """Represents a MongoDB database and its associated properties."""
+
+ @property
+ def _max_schema_name_length(self):
+ return 64
+
+ def _is_valid_schema_name(self, value):
+ # check against the invalid character set from
+ # http://docs.mongodb.org/manual/reference/limits
+ return not any(c in value for c in '/\. "$')
+
+
+class MongoDBUser(models.DatastoreUser):
+ """Represents a MongoDB user and its associated properties.
+ MongoDB users are identified using their name and database.
+ Trove stores this as <database>.<username>
+ """
+
+ root_username = 'admin.root'
+
+ def __init__(self, name=None, password=None, host=None, databases=None,
+ deserializing=False):
+ super(MongoDBUser, self).__init__(name=name, password=password,
+ host=host, databases=databases,
+ deserializing=deserializing)
+ if not deserializing:
+ self._init_roles()
+
+ @property
+ def username(self):
+ return self._username
+
+ @username.setter
+ def username(self, value):
+ self._update_name(username=value)
+
+ @property
+ def database(self):
+ return MongoDBSchema.deserialize(self._database)
+
+ @database.setter
+ def database(self, value):
+ self._update_name(database=value)
+
+ def _validate_user_name(self, value):
+ self._update_name(name=value)
+
+ def _update_name(self, name=None, username=None, database=None):
+ """Keep the name, username, and database values in sync."""
+ if name:
+ (database, username) = self._parse_name(name)
+ if not (database and username):
+ missing = 'username' if self.database else 'database'
+ raise ValueError(_("MongoDB user's name missing %s.")
+ % missing)
+ else:
+ if username:
+ if not self.database:
+ raise ValueError(_('MongoDB user missing database.'))
+ database = self.database.name
+ else: # database
+ if not self.username:
+ raise ValueError(_('MongoDB user missing username.'))
+ username = self.username
+ name = '%s.%s' % (database, username)
+ self._name = name
+ self._username = username
+ self._database = self._build_database_schema(database).serialize()
+
+ @property
+ def roles(self):
+ return self._roles
+
+ @roles.setter
+ def roles(self, value):
+ if isinstance(value, list):
+ for role in value:
+ self._add_role(role)
+ else:
+ self._add_role(value)
+
+ def revoke_role(self, role):
+ if role in self.roles:
+ self._roles.remove(role)
+
+ def _init_roles(self):
+ if '_roles' not in self.__dict__:
+ self._roles = []
+ for db in self._databases:
+ self._roles.append({'db': db['_name'], 'role': 'readWrite'})
+
+ def _build_database_schema(self, name):
+ return MongoDBSchema(name)
+
+ def deserialize_schema(self, value):
+ return MongoDBSchema.deserialize(value)
+
+ @staticmethod
+ def _parse_name(value):
+ """The name will be <database>.<username>, so split it."""
+ parts = value.split('.', 1)
+ if len(parts) != 2:
+ raise ValueError(_(
+ 'MongoDB user name "%s" not in <database>.<username> format.'
+ ) % value)
+ return parts[0], parts[1]
+
+ @property
+ def _max_user_name_length(self):
+ return 128
+
+ def _add_role(self, value):
+ if not self._is_valid_role(value):
+ raise ValueError(_('Role %s is invalid.') % value)
+ self._roles.append(value)
+ if value['role'] == 'readWrite':
+ self.databases = value['db']
+
+ def _is_valid_role(self, value):
+ if not isinstance(value, dict):
+ return False
+ if not {'db', 'role'} == set(value):
+ return False
+ return True
+
+ def verify_dict(self):
+ super(MongoDBUser, self).verify_dict()
+ self._init_roles()
+
+ @property
+ def schema_model(self):
+ return MongoDBSchema
+
+ def _create_checks(self):
+ super(MongoDBUser, self)._create_checks()
+ if not self.password:
+ raise ValueError(_("MongoDB user to create is missing a "
+ "password."))
diff --git a/trove/common/db/mysql/__init__.py b/trove/common/db/mysql/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/common/db/mysql/__init__.py
diff --git a/trove/common/db/mysql/data.py b/trove/common/db/mysql/data.py
new file mode 100644
index 00000000..10a04586
--- /dev/null
+++ b/trove/common/db/mysql/data.py
@@ -0,0 +1,244 @@
+# Copyright 2016 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.
+
+
+charset = {"big5": ["big5_chinese_ci", "big5_bin"],
+ "dec8": ["dec8_swedish_ci", "dec8_bin"],
+ "cp850": ["cp850_general_ci", "cp850_bin"],
+ "hp8": ["hp8_english_ci", "hp8_bin"],
+ "koi8r": ["koi8r_general_ci", "koi8r_bin"],
+ "latin1": ["latin1_swedish_ci",
+ "latin1_german1_ci",
+ "latin1_danish_ci",
+ "latin1_german2_ci",
+ "latin1_bin",
+ "latin1_general_ci",
+ "latin1_general_cs",
+ "latin1_spanish_ci"],
+ "latin2": ["latin2_general_ci",
+ "latin2_czech_cs",
+ "latin2_hungarian_ci",
+ "latin2_croatian_ci",
+ "latin2_bin"],
+ "swe7": ["swe7_swedish_ci", "swe7_bin"],
+ "ascii": ["ascii_general_ci", "ascii_bin"],
+ "ujis": ["ujis_japanese_ci", "ujis_bin"],
+ "sjis": ["sjis_japanese_ci", "sjis_bin"],
+ "hebrew": ["hebrew_general_ci", "hebrew_bin"],
+ "tis620": ["tis620_thai_ci", "tis620_bin"],
+ "euckr": ["euckr_korean_ci", "euckr_bin"],
+ "koi8u": ["koi8u_general_ci", "koi8u_bin"],
+ "gb2312": ["gb2312_chinese_ci", "gb2312_bin"],
+ "greek": ["greek_general_ci", "greek_bin"],
+ "cp1250": ["cp1250_general_ci",
+ "cp1250_czech_cs",
+ "cp1250_croatian_ci",
+ "cp1250_bin",
+ "cp1250_polish_ci"],
+ "gbk": ["gbk_chinese_ci", "gbk_bin"],
+ "latin5": ["latin5_turkish_ci", "latin5_bin"],
+ "armscii8": ["armscii8_general_ci", "armscii8_bin"],
+ "utf8": ["utf8_general_ci",
+ "utf8_bin",
+ "utf8_unicode_ci",
+ "utf8_icelandic_ci",
+ "utf8_latvian_ci",
+ "utf8_romanian_ci",
+ "utf8_slovenian_ci",
+ "utf8_polish_ci",
+ "utf8_estonian_ci",
+ "utf8_spanish_ci",
+ "utf8_swedish_ci",
+ "utf8_turkish_ci",
+ "utf8_czech_ci",
+ "utf8_danish_ci",
+ "utf8_lithuanian_ci",
+ "utf8_slovak_ci",
+ "utf8_spanish2_ci",
+ "utf8_roman_ci",
+ "utf8_persian_ci",
+ "utf8_esperanto_ci",
+ "utf8_hungarian_ci"],
+ "ucs2": ["ucs2_general_ci",
+ "ucs2_bin",
+ "ucs2_unicode_ci",
+ "ucs2_icelandic_ci",
+ "ucs2_latvian_ci",
+ "ucs2_romanian_ci",
+ "ucs2_slovenian_ci",
+ "ucs2_polish_ci",
+ "ucs2_estonian_ci",
+ "ucs2_spanish_ci",
+ "ucs2_swedish_ci",
+ "ucs2_turkish_ci",
+ "ucs2_czech_ci",
+ "ucs2_danish_ci",
+ "ucs2_lithuanian_ci",
+ "ucs2_slovak_ci",
+ "ucs2_spanish2_ci",
+ "ucs2_roman_ci",
+ "ucs2_persian_ci",
+ "ucs2_esperanto_ci",
+ "ucs2_hungarian_ci"],
+ "cp866": ["cp866_general_ci", "cp866_bin"],
+ "keybcs2": ["keybcs2_general_ci", "keybcs2_bin"],
+ "macce": ["macce_general_ci", "macce_bin"],
+ "macroman": ["macroman_general_ci", "macroman_bin"],
+ "cp852": ["cp852_general_ci", "cp852_bin"],
+ "latin7": ["latin7_general_ci",
+ "latin7_estonian_cs",
+ "latin7_general_cs",
+ "latin7_bin"],
+ "cp1251": ["cp1251_general_ci",
+ "cp1251_bulgarian_ci",
+ "cp1251_ukrainian_ci",
+ "cp1251_bin",
+ "cp1251_general_cs"],
+ "cp1256": ["cp1256_general_ci", "cp1256_bin"],
+ "cp1257": ["cp1257_general_ci",
+ "cp1257_lithuanian_ci",
+ "cp1257_bin"],
+ "binary": ["binary"],
+ "geostd8": ["geostd8_general_ci", "geostd8_bin"],
+ "cp932": ["cp932_japanese_ci", "cp932_bin"],
+ "eucjpms": ["eucjpms_japanese_ci", "eucjpms_bin"]}
+
+collation = {"big5_chinese_ci": "big5",
+ "big5_bin": "big5",
+ "dec8_swedish_ci": "dec8",
+ "dec8_bin": "dec8",
+ "cp850_general_ci": "cp850",
+ "cp850_bin": "cp850",
+ "hp8_english_ci": "hp8",
+ "hp8_bin": "hp8",
+ "koi8r_general_ci": "koi8r",
+ "koi8r_bin": "koi8r",
+ "latin1_german1_ci": "latin1",
+ "latin1_swedish_ci": "latin1",
+ "latin1_danish_ci": "latin1",
+ "latin1_german2_ci": "latin1",
+ "latin1_bin": "latin1",
+ "latin1_general_ci": "latin1",
+ "latin1_general_cs": "latin1",
+ "latin1_spanish_ci": "latin1",
+ "latin2_czech_cs": "latin2",
+ "latin2_general_ci": "latin2",
+ "latin2_hungarian_ci": "latin2",
+ "latin2_croatian_ci": "latin2",
+ "latin2_bin": "latin2",
+ "swe7_swedish_ci": "swe7",
+ "swe7_bin": "swe7",
+ "ascii_general_ci": "ascii",
+ "ascii_bin": "ascii",
+ "ujis_japanese_ci": "ujis",
+ "ujis_bin": "ujis",
+ "sjis_japanese_ci": "sjis",
+ "sjis_bin": "sjis",
+ "hebrew_general_ci": "hebrew",
+ "hebrew_bin": "hebrew",
+ "tis620_thai_ci": "tis620",
+ "tis620_bin": "tis620",
+ "euckr_korean_ci": "euckr",
+ "euckr_bin": "euckr",
+ "koi8u_general_ci": "koi8u",
+ "koi8u_bin": "koi8u",
+ "gb2312_chinese_ci": "gb2312",
+ "gb2312_bin": "gb2312",
+ "greek_general_ci": "greek",
+ "greek_bin": "greek",
+ "cp1250_general_ci": "cp1250",
+ "cp1250_czech_cs": "cp1250",
+ "cp1250_croatian_ci": "cp1250",
+ "cp1250_bin": "cp1250",
+ "cp1250_polish_ci": "cp1250",
+ "gbk_chinese_ci": "gbk",
+ "gbk_bin": "gbk",
+ "latin5_turkish_ci": "latin5",
+ "latin5_bin": "latin5",
+ "armscii8_general_ci": "armscii8",
+ "armscii8_bin": "armscii8",
+ "utf8_general_ci": "utf8",
+ "utf8_bin": "utf8",
+ "utf8_unicode_ci": "utf8",
+ "utf8_icelandic_ci": "utf8",
+ "utf8_latvian_ci": "utf8",
+ "utf8_romanian_ci": "utf8",
+ "utf8_slovenian_ci": "utf8",
+ "utf8_polish_ci": "utf8",
+ "utf8_estonian_ci": "utf8",
+ "utf8_spanish_ci": "utf8",
+ "utf8_swedish_ci": "utf8",
+ "utf8_turkish_ci": "utf8",
+ "utf8_czech_ci": "utf8",
+ "utf8_danish_ci": "utf8",
+ "utf8_lithuanian_ci": "utf8",
+ "utf8_slovak_ci": "utf8",
+ "utf8_spanish2_ci": "utf8",
+ "utf8_roman_ci": "utf8",
+ "utf8_persian_ci": "utf8",
+ "utf8_esperanto_ci": "utf8",
+ "utf8_hungarian_ci": "utf8",
+ "ucs2_general_ci": "ucs2",
+ "ucs2_bin": "ucs2",
+ "ucs2_unicode_ci": "ucs2",
+ "ucs2_icelandic_ci": "ucs2",
+ "ucs2_latvian_ci": "ucs2",
+ "ucs2_romanian_ci": "ucs2",
+ "ucs2_slovenian_ci": "ucs2",
+ "ucs2_polish_ci": "ucs2",
+ "ucs2_estonian_ci": "ucs2",
+ "ucs2_spanish_ci": "ucs2",
+ "ucs2_swedish_ci": "ucs2",
+ "ucs2_turkish_ci": "ucs2",
+ "ucs2_czech_ci": "ucs2",
+ "ucs2_danish_ci": "ucs2",
+ "ucs2_lithuanian_ci": "ucs2",
+ "ucs2_slovak_ci": "ucs2",
+ "ucs2_spanish2_ci": "ucs2",
+ "ucs2_roman_ci": "ucs2",
+ "ucs2_persian_ci": "ucs2",
+ "ucs2_esperanto_ci": "ucs2",
+ "ucs2_hungarian_ci": "ucs2",
+ "cp866_general_ci": "cp866",
+ "cp866_bin": "cp866",
+ "keybcs2_general_ci": "keybcs2",
+ "keybcs2_bin": "keybcs2",
+ "macce_general_ci": "macce",
+ "macce_bin": "macce",
+ "macroman_general_ci": "macroman",
+ "macroman_bin": "macroman",
+ "cp852_general_ci": "cp852",
+ "cp852_bin": "cp852",
+ "latin7_estonian_cs": "latin7",
+ "latin7_general_ci": "latin7",
+ "latin7_general_cs": "latin7",
+ "latin7_bin": "latin7",
+ "cp1251_bulgarian_ci": "cp1251",
+ "cp1251_ukrainian_ci": "cp1251",
+ "cp1251_bin": "cp1251",
+ "cp1251_general_ci": "cp1251",
+ "cp1251_general_cs": "cp1251",
+ "cp1256_general_ci": "cp1256",
+ "cp1256_bin": "cp1256",
+ "cp1257_lithuanian_ci": "cp1257",
+ "cp1257_bin": "cp1257",
+ "cp1257_general_ci": "cp1257",
+ "binary": "binary",
+ "geostd8_general_ci": "geostd8",
+ "geostd8_bin": "geostd8",
+ "cp932_japanese_ci": "cp932",
+ "cp932_bin": "cp932",
+ "eucjpms_japanese_ci": "eucjpms",
+ "eucjpms_bin": "eucjpms"}
diff --git a/trove/common/db/mysql/models.py b/trove/common/db/mysql/models.py
new file mode 100644
index 00000000..cf9d0fbf
--- /dev/null
+++ b/trove/common/db/mysql/models.py
@@ -0,0 +1,170 @@
+# Copyright 2016 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.
+
+import re
+
+import netaddr
+
+from trove.common import cfg
+from trove.common.db import models
+from trove.common.db.mysql import data as mysql_settings
+from trove.common.i18n import _
+
+CONF = cfg.CONF
+
+
+class MySQLSchema(models.DatastoreSchema):
+ """Represents a MySQL database and its properties."""
+
+ # Defaults
+ __charset__ = "utf8"
+ __collation__ = "utf8_general_ci"
+ dbname = re.compile("^[A-Za-z0-9_-]+[\s\?\#\@]*[A-Za-z0-9_-]+$")
+
+ # Complete list of acceptable values
+ collation = mysql_settings.collation
+ charset = mysql_settings.charset
+
+ def __init__(self, name=None, collate=None, character_set=None,
+ deserializing=False):
+ super(MySQLSchema, self).__init__(name=name,
+ deserializing=deserializing)
+ if not deserializing:
+ if collate:
+ self.collate = collate
+ if character_set:
+ self.character_set = character_set
+
+ @property
+ def _max_schema_name_length(self):
+ return 64
+
+ def _is_valid_schema_name(self, value):
+ # must match the dbname regex, and
+ # cannot contain a '\' character.
+ return not any([
+ not self.dbname.match(value),
+ ("%r" % value).find("\\") != -1
+ ])
+
+ @property
+ def collate(self):
+ """Get the appropriate collate value."""
+ if not self._collate and not self._character_set:
+ return self.__collation__
+ elif not self._collate:
+ return self.charset[self._character_set][0]
+ else:
+ return self._collate
+
+ @collate.setter
+ def collate(self, value):
+ """Validate the collation and set it."""
+ if not value:
+ pass
+ elif self._character_set:
+ if value not in self.charset[self._character_set]:
+ msg = (_("%(val)s not a valid collation for charset %(char)s.")
+ % {'val': value, 'char': self._character_set})
+ raise ValueError(msg)
+ self._collate = value
+ else:
+ if value not in self.collation:
+ raise ValueError(_("'%s' not a valid collation.") % value)
+ self._collate = value
+ self._character_set = self.collation[value]
+
+ @property
+ def character_set(self):
+ """Get the appropriate character set value."""
+ if not self._character_set:
+ return self.__charset__
+ else:
+ return self._character_set
+
+ @character_set.setter
+ def character_set(self, value):
+ """Validate the character set and set it."""
+ if not value:
+ pass
+ elif value not in self.charset:
+ raise ValueError(_("'%s' not a valid character set.") % value)
+ else:
+ self._character_set = value
+
+ def verify_dict(self):
+ # Also check the collate and character_set values if set, initialize
+ # them if not.
+ super(MySQLSchema, self).verify_dict()
+ if self.__dict__.get('_collate'):
+ self.collate = self._collate
+ else:
+ self._collate = None
+ if self.__dict__.get('_character_set'):
+ self.character_set = self._character_set
+ else:
+ self._character_set = None
+
+
+class MySQLUser(models.DatastoreUser):
+ """Represents a MySQL User and its associated properties."""
+
+ not_supported_chars = re.compile("^\s|\s$|'|\"|;|`|,|/|\\\\")
+
+ def _is_valid_string(self, value):
+ if (not value or
+ self.not_supported_chars.search(value) or
+ ("%r" % value).find("\\") != -1):
+ return False
+ else:
+ return True
+
+ def _is_valid_user_name(self, value):
+ return self._is_valid_string(value)
+
+ def _is_valid_password(self, value):
+ return self._is_valid_string(value)
+
+ def _is_valid_host_name(self, value):
+ if value in [None, "%"]:
+ # % is MySQL shorthand for "everywhere". Always permitted.
+ # Null host defaults to % anyway.
+ return True
+ if CONF.hostname_require_valid_ip:
+ try:
+ # '%' works as a MySQL wildcard, but it is not a valid
+ # part of an IPAddress
+ netaddr.IPAddress(value.replace('%', '1'))
+ except (ValueError, netaddr.AddrFormatError):
+ return False
+ else:
+ return True
+ else:
+ # If it wasn't required, anything else goes.
+ return True
+
+ def _build_database_schema(self, name):
+ return MySQLSchema(name)
+
+ def deserialize_schema(self, value):
+ return MySQLSchema.deserialize(value)
+
+ @property
+ def _max_user_name_length(self):
+ return 16
+
+ @property
+ def schema_model(self):
+ return MySQLSchema
diff --git a/trove/common/db/postgresql/__init__.py b/trove/common/db/postgresql/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/common/db/postgresql/__init__.py
diff --git a/trove/common/db/postgresql/models.py b/trove/common/db/postgresql/models.py
new file mode 100644
index 00000000..bc0ba73a
--- /dev/null
+++ b/trove/common/db/postgresql/models.py
@@ -0,0 +1,70 @@
+# Copyright 2016 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.
+
+import re
+
+from six import u
+
+from trove.common.db import models
+
+
+class PostgreSQLSchema(models.DatastoreSchema):
+ """Represents a PostgreSQL schema and its associated properties."""
+
+ name_regex = re.compile(u(r'^[\u0001-\u007F\u0080-\uFFFF]+[^\s]$'))
+
+ def __init__(self, name=None, collate=None, character_set=None,
+ deserializing=False):
+ super(PostgreSQLSchema, self).__init__(name=name,
+ deserializing=deserializing)
+ self.collate = collate
+ self.character_set = character_set
+
+ @property
+ def collate(self):
+ return self._collate
+
+ @collate.setter
+ def collate(self, value):
+ self._collate = value
+
+ @property
+ def character_set(self):
+ return self._character_set
+
+ @character_set.setter
+ def character_set(self, value):
+ self._character_set = value
+
+ @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
+
+
+class PostgreSQLUser(models.DatastoreUser):
+ """Represents a PostgreSQL user and its associated properties."""
+
+ root_username = 'postgres'
+
+ @property
+ def _max_user_name_length(self):
+ return 63
+
+ @property
+ def schema_model(self):
+ return PostgreSQLSchema
diff --git a/trove/extensions/cassandra/service.py b/trove/extensions/cassandra/service.py
index 4d4b289c..ffffab07 100644
--- a/trove/extensions/cassandra/service.py
+++ b/trove/extensions/cassandra/service.py
@@ -13,15 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
+from trove.common.db.cassandra import models as guest_models
from trove.extensions.common.service import DefaultRootController
from trove.extensions.mysql import models
-from trove.guestagent.db import models as guest_models
class CassandraRootController(DefaultRootController):
def _find_root_user(self, context, instance_id):
- user = guest_models.CassandraRootUser()
+ user = guest_models.CassandraUser.root()
# TODO(pmalik): Using MySQL model until we have datastore specific
# extensions (bug/1498573).
return models.User.load(
diff --git a/trove/extensions/common/models.py b/trove/extensions/common/models.py
index 1b1a97e2..71b96f26 100644
--- a/trove/extensions/common/models.py
+++ b/trove/extensions/common/models.py
@@ -15,11 +15,11 @@
from oslo_log import log as logging
+from trove.common.db import models as guest_models
from trove.common import exception
from trove.common.remote import create_guest_client
from trove.common import utils
from trove.db import get_db_api
-from trove.guestagent.db import models as guest_models
from trove.instance import models as base_models
@@ -64,8 +64,9 @@ class Root(object):
else:
root = create_guest_client(context, instance_id).enable_root()
- root_user = guest_models.RootUser()
- root_user.deserialize(root)
+ root_user = guest_models.DatastoreUser.deserialize(root,
+ verify=False)
+ root_user.make_root()
# if cluster_instances_list none, then root create is called for
# single instance, adding an RootHistory entry for the instance_id
diff --git a/trove/extensions/mysql/common.py b/trove/extensions/mysql/common.py
index 67740ce3..71a15606 100644
--- a/trove/extensions/mysql/common.py
+++ b/trove/extensions/mysql/common.py
@@ -14,8 +14,8 @@
from six.moves.urllib.parse import unquote
+from trove.common.db.mysql import models as guest_models
from trove.common import exception
-from trove.guestagent.db import models as guest_models
def populate_validated_databases(dbs):
@@ -27,8 +27,8 @@ def populate_validated_databases(dbs):
databases = []
unique_identities = set()
for database in dbs:
- mydb = guest_models.ValidatedMySQLDatabase()
- mydb.name = database.get('name', '')
+ mydb = guest_models.MySQLSchema(name=database.get('name', ''))
+ mydb.check_reserved()
if mydb.name in unique_identities:
raise exception.DatabaseInitialDatabaseDuplicateError()
unique_identities.add(mydb.name)
@@ -49,9 +49,9 @@ def populate_users(users, initial_databases=None):
users_data = []
unique_identities = set()
for user in users:
- u = guest_models.MySQLUser()
- u.name = user.get('name', '')
- u.host = user.get('host', '%')
+ u = guest_models.MySQLUser(name=user.get('name', ''),
+ host=user.get('host', '%'))
+ u.check_reserved()
user_identity = (u.name, u.host)
if user_identity in unique_identities:
raise exception.DatabaseInitialUserDuplicateError()
diff --git a/trove/extensions/mysql/models.py b/trove/extensions/mysql/models.py
index c413073d..71eb939d 100644
--- a/trove/extensions/mysql/models.py
+++ b/trove/extensions/mysql/models.py
@@ -18,13 +18,13 @@ Model classes that extend the instances functionality for MySQL instances.
"""
from trove.common import cfg
+from trove.common.db.mysql import models as guest_models
from trove.common import exception
from trove.common.notification import StartNotification
from trove.common.remote import create_guest_client
from trove.common import utils
from trove.extensions.common.models import load_and_verify
from trove.extensions.common.models import RootHistory
-from trove.guestagent.db import models as guest_models
CONF = cfg.CONF
@@ -46,12 +46,10 @@ class User(object):
@classmethod
def load(cls, context, instance_id, username, hostname, root_user=False):
load_and_verify(context, instance_id)
+ validate = guest_models.MySQLUser(name=username, host=hostname)
if root_user:
- validate = guest_models.RootUser()
- else:
- validate = guest_models.MySQLUser()
- validate.name = username
- validate.host = hostname
+ validate.make_root()
+ validate.check_reserved()
client = create_guest_client(context, instance_id)
found_user = client.get_user(username=username, hostname=hostname)
if not found_user:
@@ -138,14 +136,12 @@ class User(object):
user_changed = user_attrs.get('name')
host_changed = user_attrs.get('host')
- validate = guest_models.MySQLUser()
- if host_changed:
- validate.host = host_changed
- if user_changed:
- validate.name = user_changed
-
user = user_changed or username
host = host_changed or hostname
+
+ validate = guest_models.MySQLUser(name=user, host=host)
+ validate.check_reserved()
+
userhost = "%s@%s" % (user, host)
if user_changed or host_changed:
existing_users, _nadda = Users.load_with_client(
@@ -194,8 +190,8 @@ class Users(object):
include_marker=include_marker)
model_users = []
for user in user_list:
- mysql_user = guest_models.MySQLUser()
- mysql_user.deserialize(user)
+ mysql_user = guest_models.MySQLUser.deserialize(user,
+ verify=False)
if mysql_user.name in cfg.get_ignored_users():
continue
# TODO(hub-cap): databases are not being returned in the
@@ -257,8 +253,8 @@ class Schemas(object):
include_marker=include_marker)
model_schemas = []
for schema in schemas:
- mysql_schema = guest_models.MySQLDatabase()
- mysql_schema.deserialize(schema)
+ mysql_schema = guest_models.MySQLSchema.deserialize(schema,
+ verify=False)
if mysql_schema.name in cfg.get_ignored_dbs():
continue
model_schemas.append(Schema(mysql_schema.name,
diff --git a/trove/extensions/mysql/service.py b/trove/extensions/mysql/service.py
index f27feca2..15dc4e86 100644
--- a/trove/extensions/mysql/service.py
+++ b/trove/extensions/mysql/service.py
@@ -21,6 +21,7 @@ import webob.exc
import trove.common.apischema as apischema
from trove.common import cfg
+from trove.common.db.mysql import models as guest_models
from trove.common import exception
from trove.common.i18n import _
from trove.common import notification
@@ -34,7 +35,6 @@ from trove.extensions.mysql.common import populate_validated_databases
from trove.extensions.mysql.common import unquote_user_host
from trove.extensions.mysql import models
from trove.extensions.mysql import views
-from trove.guestagent.db import models as guest_models
LOG = logging.getLogger(__name__)
@@ -85,7 +85,8 @@ class UserController(wsgi.Controller):
model_users = populate_users(users)
models.User.create(context, instance_id, model_users)
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
+ raise exception.BadRequest(_("User create error: %(e)s")
+ % {'e': e})
return wsgi.Result(None, 202)
def delete(self, req, tenant_id, instance_id, id):
@@ -95,21 +96,21 @@ class UserController(wsgi.Controller):
context = req.environ[wsgi.CONTEXT_KEY]
id = correct_id_with_req(id, req)
username, host = unquote_user_host(id)
+ user = None
context.notification = notification.DBaaSUserDelete(context,
request=req)
with StartNotification(context, instance_id=instance_id,
username=username):
- user = None
try:
- user = guest_models.MySQLUser()
- user.name = username
- user.host = host
+ user = guest_models.MySQLUser(name=username,
+ host=host)
found_user = models.User.load(context, instance_id, username,
host)
if not found_user:
user = None
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
+ raise exception.BadRequest(_("User delete error: %(e)s")
+ % {'e': e})
if not user:
raise exception.UserNotFound(uuid=id)
models.User.delete(context, instance_id, user.serialize())
@@ -127,7 +128,8 @@ class UserController(wsgi.Controller):
try:
user = models.User.load(context, instance_id, username, host)
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
+ raise exception.BadRequest(_("User show error: %(e)s")
+ % {'e': e})
if not user:
raise exception.UserNotFound(uuid=id)
view = views.UserView(user)
@@ -151,14 +153,16 @@ class UserController(wsgi.Controller):
user = models.User.load(context, instance_id, username,
hostname)
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
+ raise exception.BadRequest(_("Error loading user: %(e)s")
+ % {'e': e})
if not user:
raise exception.UserNotFound(uuid=id)
try:
models.User.update_attributes(context, instance_id, username,
hostname, user_attrs)
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
+ raise exception.BadRequest(_("User update error: %(e)s")
+ % {'e': e})
return wsgi.Result(None, 202)
def update_all(self, req, body, tenant_id, instance_id):
@@ -170,16 +174,15 @@ class UserController(wsgi.Controller):
context.notification = notification.DBaaSUserChangePassword(
context, request=req)
users = body['users']
+ model_users = []
with StartNotification(context, instance_id=instance_id,
username=",".join([user['name']
for user in users])):
- model_users = []
for user in users:
try:
- mu = guest_models.MySQLUser()
- mu.name = user['name']
- mu.host = user.get('host')
- mu.password = user['password']
+ mu = guest_models.MySQLUser(name=user['name'],
+ host=user.get('host'),
+ password=user['password'])
found_user = models.User.load(context, instance_id,
mu.name, mu.host)
if not found_user:
@@ -189,8 +192,14 @@ class UserController(wsgi.Controller):
raise exception.UserNotFound(uuid=user_and_host)
model_users.append(mu)
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
- models.User.change_password(context, instance_id, model_users)
+ raise exception.BadRequest(_("Error loading user: %(e)s")
+ % {'e': e})
+ try:
+ models.User.change_password(context, instance_id, model_users)
+ except (ValueError, AttributeError) as e:
+ raise exception.BadRequest(_("User password update error: "
+ "%(e)s")
+ % {'e': e})
return wsgi.Result(None, 202)
@@ -210,7 +219,8 @@ class UserAccessController(wsgi.Controller):
try:
user = models.User.load(context, instance_id, username, hostname)
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
+ raise exception.BadRequest(_("Error loading user: %(e)s")
+ % {'e': e})
if not user:
raise exception.UserNotFound(uuid=user_id)
return user
@@ -311,8 +321,12 @@ class SchemaController(wsgi.Controller):
with StartNotification(context, instance_id=instance_id,
dbname=".".join([db['name']
for db in schemas])):
- model_schemas = populate_validated_databases(schemas)
- models.Schema.create(context, instance_id, model_schemas)
+ try:
+ model_schemas = populate_validated_databases(schemas)
+ models.Schema.create(context, instance_id, model_schemas)
+ except (ValueError, AttributeError) as e:
+ raise exception.BadRequest(_("Database create error: %(e)s")
+ % {'e': e})
return wsgi.Result(None, 202)
def delete(self, req, tenant_id, instance_id, id):
@@ -324,11 +338,12 @@ class SchemaController(wsgi.Controller):
context, request=req)
with StartNotification(context, instance_id=instance_id, dbname=id):
try:
- schema = guest_models.ValidatedMySQLDatabase()
- schema.name = id
+ schema = guest_models.MySQLSchema(name=id)
+ schema.check_delete()
models.Schema.delete(context, instance_id, schema.serialize())
except (ValueError, AttributeError) as e:
- raise exception.BadRequest(msg=str(e))
+ raise exception.BadRequest(_("Database delete error: %(e)s")
+ % {'e': e})
return wsgi.Result(None, 202)
def show(self, req, tenant_id, instance_id, id):
@@ -338,7 +353,7 @@ class SchemaController(wsgi.Controller):
class MySQLRootController(DefaultRootController):
def _find_root_user(self, context, instance_id):
- user = guest_models.MySQLRootUser()
+ user = guest_models.MySQLUser.root()
return models.User.load(context, instance_id,
user.name, user.host,
root_user=True)
diff --git a/trove/extensions/postgresql/service.py b/trove/extensions/postgresql/service.py
index dc85023c..316641e2 100644
--- a/trove/extensions/postgresql/service.py
+++ b/trove/extensions/postgresql/service.py
@@ -13,15 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
+from trove.common.db.postgresql import models as guest_models
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()
+ user = guest_models.PostgreSQLUser.root()
# This is currently using MySQL model.
# MySQL extension *should* work for now, but may lead to
# future bugs (incompatible input validation, unused field etc).
diff --git a/trove/guestagent/datastore/experimental/cassandra/service.py b/trove/guestagent/datastore/experimental/cassandra/service.py
index bf97cd9d..a961446a 100644
--- a/trove/guestagent/datastore/experimental/cassandra/service.py
+++ b/trove/guestagent/datastore/experimental/cassandra/service.py
@@ -26,6 +26,7 @@ from oslo_log import log as logging
from oslo_utils import netutils
from trove.common import cfg
+from trove.common.db.cassandra import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance as rd_instance
@@ -40,7 +41,6 @@ from trove.guestagent.common import guestagent_utils
from trove.guestagent.common import operating_system
from trove.guestagent.common.operating_system import FileMode
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
from trove.guestagent import pkg
@@ -142,10 +142,6 @@ class CassandraApp(object):
self.CASSANDRA_LOGBACK_FILE)
@property
- def default_superuser_name(self):
- return "cassandra"
-
- @property
def default_superuser_password(self):
return "cassandra"
@@ -311,7 +307,8 @@ class CassandraApp(object):
CassandraAdmin(update_user).alter_user_password(os_admin)
else:
cassandra = models.CassandraUser(
- self.default_superuser_name, self.default_superuser_password)
+ models.CassandraUser.root_username,
+ self.default_superuser_password)
CassandraAdmin(cassandra)._create_superuser(os_admin)
CassandraAdmin(os_admin).drop_user(cassandra)
@@ -328,7 +325,7 @@ class CassandraApp(object):
self.status = CassandraAppStatus(user)
def store_admin_credentials(self, admin_credentials):
- user = models.CassandraUser.deserialize_user(admin_credentials)
+ user = models.CassandraUser.deserialize(admin_credentials)
self._update_admin_credentials(user)
def get_admin_credentials(self):
@@ -448,8 +445,8 @@ class CassandraApp(object):
LOG.warning(
_("Trove administrative user has not been configured yet. "
"Using the built-in default: %s")
- % self.default_superuser_name)
- return models.CassandraUser(self.default_superuser_name,
+ % models.CassandraUser.root_username)
+ return models.CassandraUser(models.CassandraUser.root_username,
self.default_superuser_password)
def has_user_config(self):
@@ -731,7 +728,7 @@ class CassandraApp(object):
Create a new superuser if it does not exist and grant it full
superuser-level access to all keyspaces.
"""
- cassandra = models.CassandraRootUser(password=root_password)
+ cassandra = models.CassandraUser.root(password=root_password)
admin = self.build_admin()
if self.is_root_enabled():
admin.alter_user_password(cassandra)
@@ -897,7 +894,7 @@ class CassandraAdmin(object):
def _load_user(self, client, username, check_reserved=True):
if check_reserved:
- self._check_reserved_user_name(username)
+ models.CassandraUser(username).check_reserved()
acl = self._get_acl(client, username=username)
return self._build_user(username, acl)
@@ -1002,8 +999,8 @@ class CassandraAdmin(object):
Grant all non-superuser permissions on a keyspace to a given user.
"""
if check_reserved:
- self._check_reserved_user_name(user.name)
- self._check_reserved_keyspace_name(keyspace.name)
+ user.check_reserved()
+ keyspace.check_reserved()
for access in self.__NO_SUPERUSER_MODIFIERS:
self._grant_permission_on_keyspace(client, access, keyspace, user)
@@ -1026,8 +1023,8 @@ class CassandraAdmin(object):
def _revoke_all_access_on_keyspace(self, client, keyspace, user,
check_reserved=True):
if check_reserved:
- self._check_reserved_user_name(user.name)
- self._check_reserved_keyspace_name(keyspace.name)
+ user.check_reserved()
+ keyspace.check_reserved()
LOG.debug("Revoking all permissions on '%s' from user '%s'."
% (keyspace.name, user.name))
@@ -1149,32 +1146,24 @@ class CassandraAdmin(object):
def _deserialize_keyspace(self, keyspace_dict, check_reserved=True):
if keyspace_dict:
- db = models.CassandraSchema.deserialize_schema(keyspace_dict)
+ db = models.CassandraSchema.deserialize(keyspace_dict)
if check_reserved:
- self._check_reserved_keyspace_name(db.name)
+ db.check_reserved()
return db
return None
- def _check_reserved_keyspace_name(self, name):
- if name in self.ignore_dbs:
- raise ValueError(_("This keyspace-name is reserved: %s") % name)
-
def _deserialize_user(self, user_dict, check_reserved=True):
if user_dict:
- user = models.CassandraUser.deserialize_user(user_dict)
+ user = models.CassandraUser.deserialize(user_dict)
if check_reserved:
- self._check_reserved_user_name(user.name)
+ user.check_reserved()
return user
return None
- def _check_reserved_user_name(self, name):
- if name in self.ignore_users:
- raise ValueError(_("This user-name is reserved: %s") % name)
-
@property
def ignore_users(self):
return cfg.get_ignored_users()
diff --git a/trove/guestagent/datastore/experimental/couchbase/service.py b/trove/guestagent/datastore/experimental/couchbase/service.py
index 516d24b1..0ad3368c 100644
--- a/trove/guestagent/datastore/experimental/couchbase/service.py
+++ b/trove/guestagent/datastore/experimental/couchbase/service.py
@@ -25,6 +25,7 @@ import pexpect
import six
from trove.common import cfg
+from trove.common.db import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance as rd_instance
@@ -32,7 +33,6 @@ from trove.common import utils as utils
from trove.guestagent.common import operating_system
from trove.guestagent.datastore.experimental.couchbase import system
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
from trove.guestagent import pkg
@@ -210,10 +210,7 @@ class CouchbaseRootAccess(object):
@classmethod
def enable_root(cls, root_password=None):
- user = models.RootUser()
- user.name = "root"
- user.host = "%"
- user.password = root_password or utils.generate_random_password()
+ user = models.DatastoreUser.root(password=root_password)
if root_password:
CouchbaseRootAccess().write_password_to_file(root_password)
diff --git a/trove/guestagent/datastore/experimental/couchdb/service.py b/trove/guestagent/datastore/experimental/couchdb/service.py
index 91390a20..4ffe61ce 100644
--- a/trove/guestagent/datastore/experimental/couchdb/service.py
+++ b/trove/guestagent/datastore/experimental/couchdb/service.py
@@ -19,6 +19,7 @@ import json
from oslo_log import log as logging
from trove.common import cfg
+from trove.common.db.couchdb import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance as rd_instance
@@ -29,7 +30,6 @@ from trove.guestagent.common import operating_system
from trove.guestagent.common.operating_system import FileMode
from trove.guestagent.datastore.experimental.couchdb import system
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
from trove.guestagent import pkg
CONF = cfg.CONF
@@ -218,7 +218,7 @@ class CouchDBAdmin(object):
self._admin_user()
try:
for item in users:
- user = models.CouchDBUser.deserialize_user(item)
+ user = models.CouchDBUser.deserialize(item)
try:
LOG.debug("Creating user: %s." % user.name)
utils.execute_with_timeout(
@@ -234,7 +234,7 @@ class CouchDBAdmin(object):
pass
for database in user.databases:
- mydb = models.CouchDBSchema.deserialize_schema(database)
+ mydb = models.CouchDBSchema.deserialize(database)
try:
LOG.debug("Granting user: %s access to database: %s."
% (user.name, mydb.name))
@@ -257,7 +257,7 @@ class CouchDBAdmin(object):
def delete_user(self, user):
LOG.debug("Delete a given CouchDB user.")
- couchdb_user = models.CouchDBUser.deserialize_user(user)
+ couchdb_user = models.CouchDBUser.deserialize(user)
db_names = self.list_database_names()
for db in db_names:
@@ -457,7 +457,7 @@ class CouchDBAdmin(object):
def enable_root(self, root_pwd=None):
'''Create admin user root'''
- root_user = models.CouchDBRootUser(password=root_pwd)
+ root_user = models.CouchDBUser.root(password=root_pwd)
out, err = utils.execute_with_timeout(
system.ENABLE_ROOT %
{'admin_name': self._admin_user().name,
@@ -486,7 +486,7 @@ class CouchDBAdmin(object):
LOG.debug("Creating CouchDB databases.")
for database in databases:
- dbName = models.CouchDBSchema.deserialize_schema(database).name
+ dbName = models.CouchDBSchema.deserialize(database).name
if self._is_modifiable_database(dbName):
LOG.debug('Creating CouchDB database %s' % dbName)
try:
@@ -535,7 +535,7 @@ class CouchDBAdmin(object):
def delete_database(self, database):
'''Delete the specified database.'''
- dbName = models.CouchDBSchema.deserialize_schema(database).name
+ dbName = models.CouchDBSchema.deserialize(database).name
if self._is_modifiable_database(dbName):
try:
LOG.debug("Deleting CouchDB database: %s." % dbName)
diff --git a/trove/guestagent/datastore/experimental/db2/service.py b/trove/guestagent/datastore/experimental/db2/service.py
index 75922381..c14005e7 100644
--- a/trove/guestagent/datastore/experimental/db2/service.py
+++ b/trove/guestagent/datastore/experimental/db2/service.py
@@ -19,6 +19,7 @@ from oslo_log import log as logging
from oslo_utils import encodeutils
from trove.common import cfg
+from trove.common.db import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance as rd_instance
@@ -30,7 +31,6 @@ from trove.guestagent.common import guestagent_utils
from trove.guestagent.common import operating_system
from trove.guestagent.datastore.experimental.db2 import system
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@@ -346,8 +346,8 @@ class DB2Admin(object):
db_create_failed = []
LOG.debug("Creating DB2 databases.")
for item in databases:
- mydb = models.ValidatedMySQLDatabase()
- mydb.deserialize(item)
+ mydb = models.DatastoreSchema.deserialize(item)
+ mydb.check_create()
dbName = mydb.name
LOG.debug("Creating DB2 database: %s." % dbName)
try:
@@ -385,8 +385,8 @@ class DB2Admin(object):
"""Delete the specified database."""
dbName = None
try:
- mydb = models.ValidatedMySQLDatabase()
- mydb.deserialize(database)
+ mydb = models.DatastoreSchema.deserialize(database)
+ mydb.check_delete()
dbName = mydb.name
LOG.debug("Deleting DB2 database: %s." % dbName)
run_command(system.DELETE_DB_COMMAND % {'dbname': dbName})
@@ -423,11 +423,8 @@ class DB2Admin(object):
while item:
count = count + 1
if (limit and count <= limit) or limit is None:
- db2_db = models.MySQLDatabase()
- db2_db.name = item
+ db2_db = models.DatastoreSchema(name=item)
LOG.debug("database = %s ." % item)
- db2_db.character_set = None
- db2_db.collate = None
next_marker = db2_db.name
databases.append(db2_db.serialize())
item = next(result)
@@ -448,8 +445,8 @@ class DB2Admin(object):
LOG.debug("Creating user(s) for accessing DB2 database(s).")
try:
for item in users:
- user = models.MySQLUser()
- user.deserialize(item)
+ user = models.DatastoreUser.deserialize(item)
+ user.check_create()
try:
LOG.debug("Creating OS user: %s." % user.name)
utils.execute_with_timeout(
@@ -461,8 +458,7 @@ class DB2Admin(object):
continue
for database in user.databases:
- mydb = models.ValidatedMySQLDatabase()
- mydb.deserialize(database)
+ mydb = models.DatastoreSchema.deserialize(database)
try:
LOG.debug("Granting user: %s access to database: %s."
% (user.name, mydb.name))
@@ -481,8 +477,8 @@ class DB2Admin(object):
def delete_user(self, user):
LOG.debug("Delete a given user.")
- db2_user = models.MySQLUser()
- db2_user.deserialize(user)
+ db2_user = models.DatastoreUser.deserialize(user)
+ db2_user.check_delete()
userName = db2_user.name
user_dbs = db2_user.databases
LOG.debug("For user %s, databases to be deleted = %r." % (
@@ -495,8 +491,7 @@ class DB2Admin(object):
LOG.debug("databases for user = %r." % databases)
for database in databases:
- mydb = models.ValidatedMySQLDatabase()
- mydb.deserialize(database)
+ mydb = models.DatastoreSchema.deserialize(database)
try:
run_command(system.REVOKE_USER_ACCESS % {
'dbname': mydb.name,
@@ -526,8 +521,7 @@ class DB2Admin(object):
databases, marker = self.list_databases()
for database in databases:
- db2_db = models.MySQLDatabase()
- db2_db.deserialize(database)
+ db2_db = models.DatastoreSchema.deserialize(database)
out = None
try:
out, err = run_command(
@@ -562,8 +556,6 @@ class DB2Admin(object):
try:
item = next(result)
- db2db = models.MySQLDatabase()
- db2db.name = db2_db.name
while item:
'''
@@ -572,7 +564,7 @@ class DB2Admin(object):
'''
if item in user_map:
db2user = user_map.get(item)
- db2user.databases.append(db2db.serialize())
+ db2user.databases = db2_db.name
item = next(result)
continue
'''
@@ -581,9 +573,8 @@ class DB2Admin(object):
'''
count = count + 1
if (limit and count <= limit) or limit is None:
- db2_user = models.MySQLUser()
- db2_user.name = item
- db2_user.databases.append(db2db.serialize())
+ db2_user = models.DatastoreUser(name=item,
+ databases=db2_db.name)
users.append(db2_user.serialize())
user_map.update({item: db2_user})
item = next(result)
@@ -606,13 +597,11 @@ class DB2Admin(object):
def _get_user(self, username, hostname):
LOG.debug("Get details of a given database user %s." % username)
- user = models.MySQLUser()
- user.name = username
+ user = models.DatastoreUser(name=username)
databases, marker = self.list_databases()
out = None
for database in databases:
- db2_db = models.MySQLDatabase()
- db2_db.deserialize(database)
+ db2_db = models.DatastoreSchema.deserialize(database)
try:
out, err = run_command(
system.LIST_DB_USERS % {'dbname': db2_db.name})
diff --git a/trove/guestagent/datastore/experimental/mongodb/service.py b/trove/guestagent/datastore/experimental/mongodb/service.py
index 2b73a339..e40494c0 100644
--- a/trove/guestagent/datastore/experimental/mongodb/service.py
+++ b/trove/guestagent/datastore/experimental/mongodb/service.py
@@ -20,6 +20,7 @@ from oslo_utils import netutils
import pymongo
from trove.common import cfg
+from trove.common.db.mongodb import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance as ds_instance
@@ -31,7 +32,6 @@ from trove.guestagent.common import guestagent_utils
from trove.guestagent.common import operating_system
from trove.guestagent.datastore.experimental.mongodb import system
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
LOG = logging.getLogger(__name__)
@@ -480,12 +480,6 @@ class MongoDBAdmin(object):
type(self).admin_user = user
return type(self).admin_user
- def _is_modifiable_user(self, name):
- if ((name in cfg.get_ignored_users()) or
- name == system.MONGO_ADMIN_NAME):
- return False
- return True
-
@property
def cmd_admin_auth_params(self):
"""Returns a list of strings that constitute MongoDB command line
@@ -509,10 +503,6 @@ class MongoDBAdmin(object):
"""
LOG.debug('Creating user %s on database %s with roles %s.'
% (user.username, user.database.name, str(user.roles)))
-
- if not user.password:
- raise exception.BadRequest(_("User's password is empty."))
-
if client:
self._create_user_with_client(user, client)
else:
@@ -525,15 +515,19 @@ class MongoDBAdmin(object):
"""
with MongoDBClient(self._admin_user()) as client:
for item in users:
- user = models.MongoDBUser.deserialize_user(item)
- if not self._is_modifiable_user(user.name):
- LOG.warning('Skipping creation of user with reserved '
- 'name %(user)s' % {'user': user.name})
- elif self._get_user_record(user.name, client=client):
- LOG.warning('Skipping creation of user with pre-existing '
- 'name %(user)s' % {'user': user.name})
- else:
+ user = models.MongoDBUser.deserialize(item)
+ # this could be called to create multiple users at once;
+ # catch exceptions, log the message, and continue
+ try:
+ user.check_create()
+ if self._get_user_record(user.name, client=client):
+ raise ValueError('User with name %(user)s already '
+ 'exists.' % {'user': user.name})
self.create_validated_user(user, client=client)
+ except (ValueError, pymongo.errors.PyMongoError) as e:
+ LOG.error(e)
+ LOG.warning('Skipping creation of user with name %(user)s'
+ % {'user': user.name})
def delete_validated_user(self, user):
"""Deletes a user from their database. The caller should ensure that
@@ -549,18 +543,14 @@ class MongoDBAdmin(object):
"""Delete the given user.
:param user: a serialized user object
"""
- user = models.MongoDBUser.deserialize_user(user)
- if not self._is_modifiable_user(user.name):
- raise exception.BadRequest(_(
- 'Cannot delete user with reserved name %(user)s')
- % {'user': user.name})
- else:
- self.delete_validated_user(user)
+ user = models.MongoDBUser.deserialize(user)
+ user.check_delete()
+ self.delete_validated_user(user)
def _get_user_record(self, name, client=None):
"""Get the user's record."""
user = models.MongoDBUser(name)
- if not self._is_modifiable_user(user.name):
+ if user.is_ignored:
LOG.warning('Skipping retrieval of user with reserved '
'name %(user)s' % {'user': user.name})
return None
@@ -576,6 +566,14 @@ class MongoDBAdmin(object):
user.roles = user_info['roles']
return user
+ def get_existing_user(self, name):
+ """Check that a user exists."""
+ user = self._get_user_record(name)
+ if not user:
+ raise ValueError('User with name %(user)s does not'
+ 'exist.' % {'user': name})
+ return user
+
def get_user(self, name):
"""Get information for the given user."""
LOG.debug('Getting user %s.' % name)
@@ -591,7 +589,7 @@ class MongoDBAdmin(object):
for user_info in admin_client.admin.system.users.find():
user = models.MongoDBUser(name=user_info['_id'])
user.roles = user_info['roles']
- if self._is_modifiable_user(user.name):
+ if not user.is_ignored:
users.append(user)
LOG.debug('users = ' + str(users))
return guestagent_utils.serialize_list(
@@ -601,23 +599,24 @@ class MongoDBAdmin(object):
def change_passwords(self, users):
with MongoDBClient(self._admin_user()) as admin_client:
for item in users:
- user = models.MongoDBUser.deserialize_user(item)
- if not self._is_modifiable_user(user.name):
+ user = models.MongoDBUser.deserialize(item)
+ # this could be called to create multiple users at once;
+ # catch exceptions, log the message, and continue
+ try:
+ user.check_create()
+ self.get_existing_user(user.name)
+ self.create_validated_user(user, admin_client)
+ LOG.debug('Changing password for user %(user)s'
+ % {'user': user.name})
+ self._create_user_with_client(user, admin_client)
+ except (ValueError, pymongo.errors.PyMongoError) as e:
+ LOG.error(e)
LOG.warning('Skipping password change for user with '
- 'reserved name %(user)s.'
- % {'user': user.name})
- return None
- LOG.debug('Changing password for user %(user)s'
- % {'user': user.name})
- self._create_user_with_client(user, admin_client)
+ 'name %(user)s' % {'user': user.name})
def update_attributes(self, name, user_attrs):
"""Update user attributes."""
- user = self._get_user_record(name)
- if not user:
- raise exception.BadRequest(_(
- 'Cannot update attributes for user %(user)s as it either does '
- 'not exist or is a reserved user.') % {'user': name})
+ user = self.get_existing_user(name)
password = user_attrs.get('password')
if password:
user.password = password
@@ -632,8 +631,9 @@ class MongoDBAdmin(object):
if not password:
LOG.debug('Generating root user password.')
password = utils.generate_random_password()
- root_user = models.MongoDBUser(name='admin.root', password=password)
+ root_user = models.MongoDBUser.root(password=password)
root_user.roles = {'db': 'admin', 'role': 'root'}
+ root_user.check_create()
self.create_validated_user(root_user)
return root_user.serialize()
@@ -652,11 +652,7 @@ class MongoDBAdmin(object):
def grant_access(self, username, databases):
"""Adds the RW role to the user for each specified database."""
- user = self._get_user_record(username)
- if not user:
- raise exception.BadRequest(_(
- 'Cannot grant access for reserved or non-existant user '
- '%(user)s') % {'user': username})
+ user = self.get_existing_user(username)
for db_name in databases:
# verify the database name
models.MongoDBSchema(db_name)
@@ -673,11 +669,7 @@ class MongoDBAdmin(object):
def revoke_access(self, username, database):
"""Removes the RW role from the user for the specified database."""
- user = self._get_user_record(username)
- if not user:
- raise exception.BadRequest(_(
- 'Cannot revoke access for reserved or non-existant user '
- '%(user)s') % {'user': username})
+ user = self.get_existing_user(username)
# verify the database name
models.MongoDBSchema(database)
role = {'db': database, 'role': 'readWrite'}
@@ -690,11 +682,7 @@ class MongoDBAdmin(object):
def list_access(self, username):
"""Returns a list of all databases for which the user has the RW role.
"""
- user = self._get_user_record(username)
- if not user:
- raise exception.BadRequest(_(
- 'Cannot list access for reserved or non-existant user '
- '%(user)s') % {'user': username})
+ user = self.get_existing_user(username)
return user.databases
def create_database(self, databases):
@@ -705,17 +693,19 @@ class MongoDBAdmin(object):
tmp = 'dummy'
with MongoDBClient(self._admin_user()) as admin_client:
for item in databases:
- db_name = models.MongoDBSchema.deserialize_schema(item).name
- LOG.debug('Creating MongoDB database %s' % db_name)
- db = admin_client[db_name]
+ schema = models.MongoDBSchema.deserialize(item)
+ schema.check_create()
+ LOG.debug('Creating MongoDB database %s' % schema.name)
+ db = admin_client[schema.name]
db[tmp].insert({'dummy': True})
db.drop_collection(tmp)
def delete_database(self, database):
"""Deletes the database."""
with MongoDBClient(self._admin_user()) as admin_client:
- db_name = models.MongoDBSchema.deserialize_schema(database).name
- admin_client.drop_database(db_name)
+ schema = models.MongoDBSchema.deserialize(database)
+ schema.check_delete()
+ admin_client.drop_database(schema.name)
def list_database_names(self):
"""Get the list of database names."""
@@ -724,12 +714,11 @@ class MongoDBAdmin(object):
def list_databases(self, limit=None, marker=None, include_marker=False):
"""Lists the databases."""
- db_names = self.list_database_names()
- for hidden in cfg.get_ignored_dbs():
- if hidden in db_names:
- db_names.remove(hidden)
- databases = [models.MongoDBSchema(db_name)
- for db_name in db_names]
+ databases = []
+ for db_name in self.list_database_names():
+ schema = models.MongoDBSchema(name=db_name)
+ if not schema.is_ignored():
+ databases.append(schema)
LOG.debug('databases = ' + str(databases))
return guestagent_utils.serialize_list(
databases,
@@ -767,7 +756,7 @@ class MongoDBAdmin(object):
def db_stats(self, database, scale=1):
"""Gets the stats for the given database."""
with MongoDBClient(self._admin_user()) as admin_client:
- db_name = models.MongoDBSchema.deserialize_schema(database).name
+ db_name = models.MongoDBSchema.deserialize(database).name
return admin_client[db_name].command('dbStats', scale=scale)
def list_active_shards(self):
diff --git a/trove/guestagent/datastore/experimental/postgresql/manager.py b/trove/guestagent/datastore/experimental/postgresql/manager.py
index 7e7adf13..84051312 100644
--- a/trove/guestagent/datastore/experimental/postgresql/manager.py
+++ b/trove/guestagent/datastore/experimental/postgresql/manager.py
@@ -19,6 +19,7 @@ import os
from oslo_log import log as logging
from trove.common import cfg
+from trove.common.db.postgresql import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance as trove_instance
@@ -29,7 +30,6 @@ from trove.guestagent.datastore.experimental.postgresql.service import (
PgSqlAdmin)
from trove.guestagent.datastore.experimental.postgresql.service import PgSqlApp
from trove.guestagent.datastore import manager
-from trove.guestagent.db import models
from trove.guestagent import guest_log
from trove.guestagent import volume
diff --git a/trove/guestagent/datastore/experimental/postgresql/service.py b/trove/guestagent/datastore/experimental/postgresql/service.py
index 5c1cfef5..a59d7110 100644
--- a/trove/guestagent/datastore/experimental/postgresql/service.py
+++ b/trove/guestagent/datastore/experimental/postgresql/service.py
@@ -23,6 +23,7 @@ from oslo_log import log as logging
import psycopg2
from trove.common import cfg
+from trove.common.db.postgresql import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance
@@ -35,7 +36,6 @@ from trove.guestagent.common import operating_system
from trove.guestagent.common.operating_system import FileMode
from trove.guestagent.datastore.experimental.postgresql import pgsql_query
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
from trove.guestagent import pkg
LOG = logging.getLogger(__name__)
@@ -505,7 +505,7 @@ class PgSqlApp(object):
return user.serialize()
def build_root_user(self, password=None):
- return models.PostgreSQLRootUser(password=password)
+ return models.PostgreSQLUser.root(password=password)
def pg_start_backup(self, backup_label):
r = self.build_admin().query(
@@ -662,7 +662,7 @@ class PgSqlAdmin(object):
for database in databases:
self._create_database(
context,
- models.PostgreSQLSchema.deserialize_schema(database))
+ models.PostgreSQLSchema.deserialize(database))
def _create_database(self, context, database):
"""Create a database.
@@ -689,7 +689,7 @@ class PgSqlAdmin(object):
"""Delete the specified database.
"""
self._drop_database(
- models.PostgreSQLSchema.deserialize_schema(database))
+ models.PostgreSQLSchema.deserialize(database))
def _drop_database(self, database):
"""Drop a given Postgres database.
@@ -736,7 +736,7 @@ class PgSqlAdmin(object):
for user in users:
self._create_user(
context,
- models.PostgreSQLUser.deserialize_user(user), None)
+ models.PostgreSQLUser.deserialize(user), None)
def _create_user(self, context, user, encrypt_password=None, *options):
"""Create a user and grant privileges for the specified databases.
@@ -775,7 +775,7 @@ class PgSqlAdmin(object):
)
self._grant_access(
context, user.name,
- [models.PostgreSQLSchema.deserialize_schema(db)
+ [models.PostgreSQLSchema.deserialize(db)
for db in user.databases])
def _create_admin_user(self, context, user, encrypt_password=None):
@@ -827,7 +827,7 @@ class PgSqlAdmin(object):
"""Delete the specified user.
"""
self._drop_user(
- context, models.PostgreSQLUser.deserialize_user(user))
+ context, models.PostgreSQLUser.deserialize(user))
def _drop_user(self, context, user):
"""Drop a given Postgres user.
@@ -838,7 +838,7 @@ class PgSqlAdmin(object):
# Postgresql requires that you revoke grants before dropping the user
dbs = self.list_access(context, user.name, None)
for d in dbs:
- db = models.PostgreSQLSchema.deserialize_schema(d)
+ db = models.PostgreSQLSchema.deserialize(d)
self.revoke_access(context, user.name, None, db.name)
LOG.info(
@@ -888,7 +888,7 @@ class PgSqlAdmin(object):
for user in users:
self.alter_user(
context,
- models.PostgreSQLUser.deserialize_user(user), None)
+ models.PostgreSQLUser.deserialize(user), None)
def alter_user(self, context, user, encrypt_password=None, *options):
"""Change the password and options of an existing users.
diff --git a/trove/guestagent/datastore/experimental/vertica/service.py b/trove/guestagent/datastore/experimental/vertica/service.py
index ef84e531..2bf77068 100644
--- a/trove/guestagent/datastore/experimental/vertica/service.py
+++ b/trove/guestagent/datastore/experimental/vertica/service.py
@@ -20,6 +20,7 @@ from oslo_utils import netutils
from six.moves import configparser
from trove.common import cfg
+from trove.common.db import models
from trove.common import exception
from trove.common.i18n import _
from trove.common.i18n import _LI
@@ -33,7 +34,6 @@ from trove.guestagent.common import operating_system
from trove.guestagent.common.operating_system import FileMode
from trove.guestagent.datastore.experimental.vertica import system
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
from trove.guestagent import pkg
from trove.guestagent import volume
@@ -475,10 +475,7 @@ class VerticaApp(object):
def enable_root(self, root_password=None):
"""Resets the root password."""
LOG.info(_LI("Enabling root."))
- user = models.RootUser()
- user.name = "root"
- user.host = "%"
- user.password = root_password or utils.generate_random_password()
+ user = models.DatastoreUser.root(password=root_password)
if not self.is_root_enabled():
self._create_user(user.name, user.password, 'pseudosuperuser')
else:
diff --git a/trove/guestagent/datastore/mysql_common/service.py b/trove/guestagent/datastore/mysql_common/service.py
index 96c9330d..0e4d8e23 100644
--- a/trove/guestagent/datastore/mysql_common/service.py
+++ b/trove/guestagent/datastore/mysql_common/service.py
@@ -34,6 +34,7 @@ from sqlalchemy.sql.expression import text
from trove.common import cfg
from trove.common.configurations import MySQLConfParser
+from trove.common.db.mysql import models
from trove.common import exception
from trove.common.exception import PollTimeOut
from trove.common.i18n import _
@@ -46,7 +47,6 @@ from trove.guestagent.common import guestagent_utils
from trove.guestagent.common import operating_system
from trove.guestagent.common import sql_query
from trove.guestagent.datastore import service
-from trove.guestagent.db import models
from trove.guestagent import pkg
ADMIN_USER_NAME = "os_admin"
@@ -243,9 +243,7 @@ class BaseMySqlAdmin(object):
for db in db_result:
LOG.debug("\t db: %s." % db)
if db['grantee'] == "'%s'@'%s'" % (user.name, user.host):
- mysql_db = models.MySQLDatabase()
- mysql_db.name = db['table_schema']
- user.databases.append(mysql_db.serialize())
+ user.databases = db['table_schema']
def change_passwords(self, users):
"""Change the passwords of one or more existing users."""
@@ -256,8 +254,7 @@ class BaseMySqlAdmin(object):
user_dict = {'_name': item['name'],
'_host': item['host'],
'_password': item['password']}
- user = models.MySQLUser()
- user.deserialize(user_dict)
+ user = models.MySQLUser.deserialize(user_dict)
LOG.debug("\tDeserialized: %s." % user.__dict__)
uu = sql_query.SetPassword(user.name, host=user.host,
new_password=user.password)
@@ -295,8 +292,8 @@ class BaseMySqlAdmin(object):
"""Create the list of specified databases."""
with self.local_sql_client(self.mysql_app.get_engine()) as client:
for item in databases:
- mydb = models.ValidatedMySQLDatabase()
- mydb.deserialize(item)
+ mydb = models.MySQLSchema.deserialize(item)
+ mydb.check_create()
cd = sql_query.CreateDatabase(mydb.name,
mydb.character_set,
mydb.collate)
@@ -309,8 +306,8 @@ class BaseMySqlAdmin(object):
"""
with self.local_sql_client(self.mysql_app.get_engine()) as client:
for item in users:
- user = models.MySQLUser()
- user.deserialize(item)
+ user = models.MySQLUser.deserialize(item)
+ user.check_create()
# TODO(cp16net):Should users be allowed to create users
# 'os_admin' or 'debian-sys-maint'
g = sql_query.Grant(user=user.name, host=user.host,
@@ -318,8 +315,7 @@ class BaseMySqlAdmin(object):
t = text(str(g))
client.execute(t)
for database in user.databases:
- mydb = models.ValidatedMySQLDatabase()
- mydb.deserialize(database)
+ mydb = models.MySQLSchema.deserialize(database)
g = sql_query.Grant(permissions='ALL', database=mydb.name,
user=user.name, host=user.host,
clear=user.password)
@@ -329,16 +325,16 @@ class BaseMySqlAdmin(object):
def delete_database(self, database):
"""Delete the specified database."""
with self.local_sql_client(self.mysql_app.get_engine()) as client:
- mydb = models.ValidatedMySQLDatabase()
- mydb.deserialize(database)
+ mydb = models.MySQLSchema.deserialize(database)
+ mydb.check_delete()
dd = sql_query.DropDatabase(mydb.name)
t = text(str(dd))
client.execute(t)
def delete_user(self, user):
"""Delete the specified user."""
- mysql_user = models.MySQLUser()
- mysql_user.deserialize(user)
+ mysql_user = models.MySQLUser.deserialize(user)
+ mysql_user.check_delete()
self.delete_user_by_name(mysql_user.name, mysql_user.host)
def delete_user_by_name(self, name, host='%'):
@@ -356,9 +352,11 @@ class BaseMySqlAdmin(object):
def _get_user(self, username, hostname):
"""Return a single user matching the criteria."""
- user = models.MySQLUser()
+ user = None
try:
- user.name = username # Could possibly throw a BadRequest here.
+ # Could possibly throw a ValueError here.
+ user = models.MySQLUser(name=username)
+ user.check_reserved()
except ValueError as ve:
LOG.exception(_("Error Getting user information"))
err_msg = encodeutils.exception_to_unicode(ve)
@@ -387,11 +385,15 @@ class BaseMySqlAdmin(object):
def grant_access(self, username, hostname, databases):
"""Grant a user permission to use a given database."""
user = self._get_user(username, hostname)
- mydb = models.ValidatedMySQLDatabase()
+ mydb = None # cache the model as we just want name validation
with self.local_sql_client(self.mysql_app.get_engine()) as client:
for database in databases:
try:
- mydb.name = database
+ if mydb:
+ mydb.name = database
+ else:
+ mydb = models.MySQLSchema(name=database)
+ mydb.check_reserved()
except ValueError:
LOG.exception(_("Error granting access"))
raise exception.BadRequest(_(
@@ -455,11 +457,10 @@ class BaseMySqlAdmin(object):
if count >= limit:
break
LOG.debug("database = %s." % str(database))
- mysql_db = models.MySQLDatabase()
- mysql_db.name = database[0]
+ mysql_db = models.MySQLSchema(name=database[0],
+ character_set=database[1],
+ collate=database[2])
next_marker = mysql_db.name
- mysql_db.character_set = database[1]
- mysql_db.collate = database[2]
databases.append(mysql_db.serialize())
LOG.debug("databases = " + str(databases))
if limit is not None and database_names.rowcount <= limit:
@@ -492,7 +493,6 @@ class BaseMySqlAdmin(object):
"be omitted from the listing: %s" % ignored_user_names)
users = []
with self.local_sql_client(self.mysql_app.get_engine()) as client:
- mysql_user = models.MySQLUser()
iq = sql_query.Query() # Inner query.
iq.columns = ['User', 'Host', "CONCAT(User, '@', Host) as Marker"]
iq.tables = ['mysql.user']
@@ -520,9 +520,9 @@ class BaseMySqlAdmin(object):
if count >= limit:
break
LOG.debug("user = " + str(row))
- mysql_user = models.MySQLUser()
- mysql_user.name = row['User']
- mysql_user.host = row['Host']
+ mysql_user = models.MySQLUser(name=row['User'],
+ host=row['Host'])
+ mysql_user.check_reserved()
self._associate_dbs(mysql_user)
next_marker = row['Marker']
users.append(mysql_user.serialize())
@@ -669,7 +669,7 @@ class BaseMySqlApp(object):
"""Generate and set a random root password and forget about it."""
localhost = "localhost"
uu = sql_query.SetPassword(
- "root", host=localhost,
+ models.MySQLUser.root_username, host=localhost,
new_password=utils.generate_random_password())
t = text(str(uu))
client.execute(t)
@@ -1052,9 +1052,8 @@ class BaseMySqlRootAccess(object):
"""Enable the root user global access and/or
reset the root password.
"""
- user = models.MySQLRootUser(root_password)
+ user = models.MySQLUser.root(password=root_password)
with self.local_sql_client(self.mysql_app.get_engine()) as client:
- print(client)
try:
cu = sql_query.CreateUser(user.name, host=user.host)
t = text(str(cu))
@@ -1064,7 +1063,6 @@ class BaseMySqlRootAccess(object):
# TODO(rnirmal): More fine grained error checking later on
LOG.debug(err)
with self.local_sql_client(self.mysql_app.get_engine()) as client:
- print(client)
uu = sql_query.SetPassword(user.name, host=user.host,
new_password=user.password)
t = text(str(uu))
diff --git a/trove/guestagent/db/models.py b/trove/guestagent/db/models.py
deleted file mode 100644
index 4af8bd35..00000000
--- a/trove/guestagent/db/models.py
+++ /dev/null
@@ -1,1069 +0,0 @@
-# Copyright (c) 2011 OpenStack Foundation
-# 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.
-
-import abc
-import re
-
-import netaddr
-from six import u
-
-from trove.common import cfg
-from trove.common import exception
-from trove.common.i18n import _
-from trove.common import utils
-
-CONF = cfg.CONF
-
-
-class Base(object):
-
- def serialize(self):
- return self.__dict__
-
- def deserialize(self, o):
- self.__dict__ = o
-
- @classmethod
- def _validate_dict(cls, value):
- reqs = cls._dict_requirements()
- return (isinstance(value, dict) and
- all(key in value for key in reqs))
-
- @classmethod
- @abc.abstractmethod
- def _dict_requirements(cls):
- """Get the dictionary requirements for a user created via
- deserialization.
- :returns: List of required dictionary keys.
- """
-
-
-class DatastoreSchema(Base):
- """Represents a database schema."""
-
- def __init__(self, name, deserializing=False, *args, **kwargs):
- self._name = None
- self._collate = None
- self._character_set = None
-
- # If both or neither are passed in this is a bug.
- if not (bool(deserializing) != bool(name)):
- raise RuntimeError("Bug in DatastoreSchema()")
- if not deserializing:
- self.name = name
-
- def __str__(self):
- return str(self.name)
-
- def __repr__(self):
- return str(self.serialize())
-
- @classmethod
- def deserialize_schema(cls, value):
- if not cls._validate_dict(value):
- raise ValueError(_("Bad dictionary. Keys: %(keys)s. "
- "Required: %(reqs)s")
- % ({'keys': value.keys(),
- 'reqs': cls._dict_requirements()}))
- schema = cls(name=None, deserializing=True)
- schema.deserialize(value)
- return schema
-
- @property
- def name(self):
- return self._name
-
- @name.setter
- def name(self, value):
- self._validate_schema_name(value)
- self._name = value
-
- @property
- def collate(self):
- return self._collate
-
- @property
- def character_set(self):
- return self._character_set
-
- def _validate_schema_name(self, value):
- """Perform validations on a given schema name.
- :param value: Validated schema name.
- :type value: string
- :raises: ValueError On validation errors.
- """
- if self._max_schema_name_length and (len(value) >
- self._max_schema_name_length):
- raise ValueError(_("Schema name '%(name)s' is too long. "
- "Max length = %(max_length)d.")
- % {'name': value,
- 'max_length': self._max_schema_name_length})
- elif not self._is_valid_schema_name(value):
- raise ValueError(_("'%s' is not a valid schema name.") % value)
-
- @abc.abstractproperty
- def _max_schema_name_length(self):
- """Return the maximum valid schema name length if any.
- :returns: Maximum schema name length or None if unlimited.
- """
-
- @abc.abstractmethod
- def _is_valid_schema_name(self, value):
- """Validate a given schema name.
- :param value: Validated schema name.
- :type value: string
- :returns: TRUE if valid, FALSE otherwise.
- """
-
- @classmethod
- @abc.abstractmethod
- def _dict_requirements(cls):
- """Get the dictionary requirements for a user created via
- deserialization.
- :returns: List of required dictionary keys.
- """
-
-
-class MongoDBSchema(DatastoreSchema):
- """Represents the MongoDB schema and its associated properties.
-
- MongoDB database names are limited to 128 characters,
- alphanumeric and - and _ only.
- """
-
- name_regex = re.compile(r'^[a-zA-Z0-9_\-]+$')
-
- def __init__(self, name, *args, **kwargs):
- super(MongoDBSchema, self).__init__(name, *args, **kwargs)
-
- @property
- def _max_schema_name_length(self):
- return 64
-
- def _is_valid_schema_name(self, value):
- # check against the invalid character set from
- # http://docs.mongodb.org/manual/reference/limits
- return not any(c in value for c in '/\. "$')
-
- @classmethod
- def _dict_requirements(cls):
- return ['_name']
-
-
-class CassandraSchema(DatastoreSchema):
- """Represents a Cassandra schema and its associated properties.
-
- Keyspace names are 32 or fewer alpha-numeric characters and underscores,
- the first of which is an alpha character.
- """
-
- def __init__(self, name, *args, **kwargs):
- super(CassandraSchema, self).__init__(name, *args, **kwargs)
-
- @property
- def _max_schema_name_length(self):
- return 32
-
- def _is_valid_schema_name(self, value):
- return True
-
- @classmethod
- def _dict_requirements(cls):
- return ['_name']
-
-
-class CouchDBSchema(DatastoreSchema):
- '''Represents the CouchDB schema and its associated properties.
-
- The database name must consist of one or more of the following characters
- and the name must begin with a lowercase letter.
-
- - Lowercase characters (a-z)
- - Digits (0-9)
- - Any of the characters _, $, (, ), +, -, and /
- '''
-
- name_regex = re.compile(r'^[a-z][a-z0-9_$()+/-]*$')
-
- def __init__(self, name, *args, **kwargs):
- super(CouchDBSchema, self).__init__(name, *args, **kwargs)
-
- @property
- def _max_schema_name_length(self):
- return None
-
- def _is_valid_schema_name(self, value):
- # https://wiki.apache.org/couchdb/HTTP_database_API
- if re.match(r'^[a-z]*$', value[0]):
- return True
- else:
- return False
-
- @classmethod
- def _dict_requirements(cls):
- 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, character_set=None, collate=None,
- *args, **kwargs):
- super(PostgreSQLSchema, self).__init__(name, *args, **kwargs)
-
- self.character_set = character_set
- self.collate = collate
-
- @DatastoreSchema.collate.setter
- def collate(self, value):
- self._collate = value
-
- @DatastoreSchema.character_set.setter
- def character_set(self, value):
- self._character_set = value
-
- @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."""
-
- # Defaults
- __charset__ = "utf8"
- __collation__ = "utf8_general_ci"
- dbname = re.compile("^[A-Za-z0-9_-]+[\s\?\#\@]*[A-Za-z0-9_-]+$")
-
- # Complete list of acceptable values
- charset = {"big5": ["big5_chinese_ci", "big5_bin"],
- "dec8": ["dec8_swedish_ci", "dec8_bin"],
- "cp850": ["cp850_general_ci", "cp850_bin"],
- "hp8": ["hp8_english_ci", "hp8_bin"],
- "koi8r": ["koi8r_general_ci", "koi8r_bin"],
- "latin1": ["latin1_swedish_ci",
- "latin1_german1_ci",
- "latin1_danish_ci",
- "latin1_german2_ci",
- "latin1_bin",
- "latin1_general_ci",
- "latin1_general_cs",
- "latin1_spanish_ci"],
- "latin2": ["latin2_general_ci",
- "latin2_czech_cs",
- "latin2_hungarian_ci",
- "latin2_croatian_ci",
- "latin2_bin"],
- "swe7": ["swe7_swedish_ci", "swe7_bin"],
- "ascii": ["ascii_general_ci", "ascii_bin"],
- "ujis": ["ujis_japanese_ci", "ujis_bin"],
- "sjis": ["sjis_japanese_ci", "sjis_bin"],
- "hebrew": ["hebrew_general_ci", "hebrew_bin"],
- "tis620": ["tis620_thai_ci", "tis620_bin"],
- "euckr": ["euckr_korean_ci", "euckr_bin"],
- "koi8u": ["koi8u_general_ci", "koi8u_bin"],
- "gb2312": ["gb2312_chinese_ci", "gb2312_bin"],
- "greek": ["greek_general_ci", "greek_bin"],
- "cp1250": ["cp1250_general_ci",
- "cp1250_czech_cs",
- "cp1250_croatian_ci",
- "cp1250_bin",
- "cp1250_polish_ci"],
- "gbk": ["gbk_chinese_ci", "gbk_bin"],
- "latin5": ["latin5_turkish_ci", "latin5_bin"],
- "armscii8": ["armscii8_general_ci", "armscii8_bin"],
- "utf8mb4": ["utf8_unicode_ci", "utf8mb4_unicode_ci"],
- "utf8": ["utf8_general_ci",
- "utf8_bin",
- "utf8_unicode_ci",
- "utf8_icelandic_ci",
- "utf8_latvian_ci",
- "utf8_romanian_ci",
- "utf8_slovenian_ci",
- "utf8_polish_ci",
- "utf8_estonian_ci",
- "utf8_spanish_ci",
- "utf8_swedish_ci",
- "utf8_turkish_ci",
- "utf8_czech_ci",
- "utf8_danish_ci",
- "utf8_lithuanian_ci",
- "utf8_slovak_ci",
- "utf8_spanish2_ci",
- "utf8_roman_ci",
- "utf8_persian_ci",
- "utf8_esperanto_ci",
- "utf8_hungarian_ci"],
- "ucs2": ["ucs2_general_ci",
- "ucs2_bin",
- "ucs2_unicode_ci",
- "ucs2_icelandic_ci",
- "ucs2_latvian_ci",
- "ucs2_romanian_ci",
- "ucs2_slovenian_ci",
- "ucs2_polish_ci",
- "ucs2_estonian_ci",
- "ucs2_spanish_ci",
- "ucs2_swedish_ci",
- "ucs2_turkish_ci",
- "ucs2_czech_ci",
- "ucs2_danish_ci",
- "ucs2_lithuanian_ci",
- "ucs2_slovak_ci",
- "ucs2_spanish2_ci",
- "ucs2_roman_ci",
- "ucs2_persian_ci",
- "ucs2_esperanto_ci",
- "ucs2_hungarian_ci"],
- "cp866": ["cp866_general_ci", "cp866_bin"],
- "keybcs2": ["keybcs2_general_ci", "keybcs2_bin"],
- "macce": ["macce_general_ci", "macce_bin"],
- "macroman": ["macroman_general_ci", "macroman_bin"],
- "cp852": ["cp852_general_ci", "cp852_bin"],
- "latin7": ["latin7_general_ci",
- "latin7_estonian_cs",
- "latin7_general_cs",
- "latin7_bin"],
- "cp1251": ["cp1251_general_ci",
- "cp1251_bulgarian_ci",
- "cp1251_ukrainian_ci",
- "cp1251_bin",
- "cp1251_general_cs"],
- "cp1256": ["cp1256_general_ci", "cp1256_bin"],
- "cp1257": ["cp1257_general_ci",
- "cp1257_lithuanian_ci",
- "cp1257_bin"],
- "binary": ["binary"],
- "geostd8": ["geostd8_general_ci", "geostd8_bin"],
- "cp932": ["cp932_japanese_ci", "cp932_bin"],
- "eucjpms": ["eucjpms_japanese_ci", "eucjpms_bin"]}
-
- collation = {"big5_chinese_ci": "big5",
- "big5_bin": "big5",
- "dec8_swedish_ci": "dec8",
- "dec8_bin": "dec8",
- "cp850_general_ci": "cp850",
- "cp850_bin": "cp850",
- "hp8_english_ci": "hp8",
- "hp8_bin": "hp8",
- "koi8r_general_ci": "koi8r",
- "koi8r_bin": "koi8r",
- "latin1_german1_ci": "latin1",
- "latin1_swedish_ci": "latin1",
- "latin1_danish_ci": "latin1",
- "latin1_german2_ci": "latin1",
- "latin1_bin": "latin1",
- "latin1_general_ci": "latin1",
- "latin1_general_cs": "latin1",
- "latin1_spanish_ci": "latin1",
- "latin2_czech_cs": "latin2",
- "latin2_general_ci": "latin2",
- "latin2_hungarian_ci": "latin2",
- "latin2_croatian_ci": "latin2",
- "latin2_bin": "latin2",
- "swe7_swedish_ci": "swe7",
- "swe7_bin": "swe7",
- "ascii_general_ci": "ascii",
- "ascii_bin": "ascii",
- "ujis_japanese_ci": "ujis",
- "ujis_bin": "ujis",
- "sjis_japanese_ci": "sjis",
- "sjis_bin": "sjis",
- "hebrew_general_ci": "hebrew",
- "hebrew_bin": "hebrew",
- "tis620_thai_ci": "tis620",
- "tis620_bin": "tis620",
- "euckr_korean_ci": "euckr",
- "euckr_bin": "euckr",
- "koi8u_general_ci": "koi8u",
- "koi8u_bin": "koi8u",
- "gb2312_chinese_ci": "gb2312",
- "gb2312_bin": "gb2312",
- "greek_general_ci": "greek",
- "greek_bin": "greek",
- "cp1250_general_ci": "cp1250",
- "cp1250_czech_cs": "cp1250",
- "cp1250_croatian_ci": "cp1250",
- "cp1250_bin": "cp1250",
- "cp1250_polish_ci": "cp1250",
- "gbk_chinese_ci": "gbk",
- "gbk_bin": "gbk",
- "latin5_turkish_ci": "latin5",
- "latin5_bin": "latin5",
- "armscii8_general_ci": "armscii8",
- "armscii8_bin": "armscii8",
- "utf8mb4_unicode_ci": "utf8mb4",
- "utf8_general_ci": "utf8",
- "utf8_bin": "utf8",
- "utf8_unicode_ci": "utf8",
- "utf8_icelandic_ci": "utf8",
- "utf8_latvian_ci": "utf8",
- "utf8_romanian_ci": "utf8",
- "utf8_slovenian_ci": "utf8",
- "utf8_polish_ci": "utf8",
- "utf8_estonian_ci": "utf8",
- "utf8_spanish_ci": "utf8",
- "utf8_swedish_ci": "utf8",
- "utf8_turkish_ci": "utf8",
- "utf8_czech_ci": "utf8",
- "utf8_danish_ci": "utf8",
- "utf8_lithuanian_ci": "utf8",
- "utf8_slovak_ci": "utf8",
- "utf8_spanish2_ci": "utf8",
- "utf8_roman_ci": "utf8",
- "utf8_persian_ci": "utf8",
- "utf8_esperanto_ci": "utf8",
- "utf8_hungarian_ci": "utf8",
- "ucs2_general_ci": "ucs2",
- "ucs2_bin": "ucs2",
- "ucs2_unicode_ci": "ucs2",
- "ucs2_icelandic_ci": "ucs2",
- "ucs2_latvian_ci": "ucs2",
- "ucs2_romanian_ci": "ucs2",
- "ucs2_slovenian_ci": "ucs2",
- "ucs2_polish_ci": "ucs2",
- "ucs2_estonian_ci": "ucs2",
- "ucs2_spanish_ci": "ucs2",
- "ucs2_swedish_ci": "ucs2",
- "ucs2_turkish_ci": "ucs2",
- "ucs2_czech_ci": "ucs2",
- "ucs2_danish_ci": "ucs2",
- "ucs2_lithuanian_ci": "ucs2",
- "ucs2_slovak_ci": "ucs2",
- "ucs2_spanish2_ci": "ucs2",
- "ucs2_roman_ci": "ucs2",
- "ucs2_persian_ci": "ucs2",
- "ucs2_esperanto_ci": "ucs2",
- "ucs2_hungarian_ci": "ucs2",
- "cp866_general_ci": "cp866",
- "cp866_bin": "cp866",
- "keybcs2_general_ci": "keybcs2",
- "keybcs2_bin": "keybcs2",
- "macce_general_ci": "macce",
- "macce_bin": "macce",
- "macroman_general_ci": "macroman",
- "macroman_bin": "macroman",
- "cp852_general_ci": "cp852",
- "cp852_bin": "cp852",
- "latin7_estonian_cs": "latin7",
- "latin7_general_ci": "latin7",
- "latin7_general_cs": "latin7",
- "latin7_bin": "latin7",
- "cp1251_bulgarian_ci": "cp1251",
- "cp1251_ukrainian_ci": "cp1251",
- "cp1251_bin": "cp1251",
- "cp1251_general_ci": "cp1251",
- "cp1251_general_cs": "cp1251",
- "cp1256_general_ci": "cp1256",
- "cp1256_bin": "cp1256",
- "cp1257_lithuanian_ci": "cp1257",
- "cp1257_bin": "cp1257",
- "cp1257_general_ci": "cp1257",
- "binary": "binary",
- "geostd8_general_ci": "geostd8",
- "geostd8_bin": "geostd8",
- "cp932_japanese_ci": "cp932",
- "cp932_bin": "cp932",
- "eucjpms_japanese_ci": "eucjpms",
- "eucjpms_bin": "eucjpms"}
-
- def __init__(self):
- self._name = None
- self._collate = None
- self._character_set = None
- self._ignore_dbs = cfg.get_ignored_dbs()
-
- @property
- def name(self):
- return self._name
-
- def _is_valid(self, value):
- return value.lower() not in self._ignore_dbs
-
- @name.setter
- def name(self, value):
- self._name = value
-
- @property
- def collate(self):
- """Get the appropriate collate value."""
- if not self._collate and not self._character_set:
- return self.__collation__
- elif not self._collate:
- return self.charset[self._character_set][0]
- else:
- return self._collate
-
- @collate.setter
- def collate(self, value):
- """Validate the collation and set it."""
- if not value:
- pass
- elif self._character_set:
- if value not in self.charset[self._character_set]:
- msg = (_("%(val)s not a valid collation for charset %(char)s.")
- % {'val': value, 'char': self._character_set})
- raise ValueError(msg)
- self._collate = value
- else:
- if value not in self.collation:
- raise ValueError(_("'%s' not a valid collation.") % value)
- self._collate = value
- self._character_set = self.collation[value]
-
- @property
- def character_set(self):
- """Get the appropriate character set value."""
- if not self._character_set:
- return self.__charset__
- else:
- return self._character_set
-
- @character_set.setter
- def character_set(self, value):
- """Validate the character set and set it."""
- if not value:
- pass
- elif value not in self.charset:
- raise ValueError(_("'%s' not a valid character set.") % value)
- else:
- self._character_set = value
-
-
-class ValidatedMySQLDatabase(MySQLDatabase):
-
- @MySQLDatabase.name.setter
- def name(self, value):
- if any([not value,
- not self._is_valid(value),
- not self.dbname.match(value),
- ("%r" % value).find("\\") != -1]):
- raise ValueError(_("'%s' is not a valid database name.") % value)
- elif len(value) > 64:
- msg = _("Database name '%s' is too long. Max length = 64.")
- raise ValueError(msg % value)
- else:
- self._name = value
-
-
-class DatastoreUser(Base):
- """Represents a datastore user."""
-
- _HOSTNAME_WILDCARD = '%'
-
- def __init__(self, name, password, deserializing=False, *args, **kwargs):
- self._name = None
- self._password = None
- self._host = None
- self._databases = []
-
- # need only one of: deserializing, name, or (name and password)
- if ((not (bool(deserializing) != bool(name))) or
- (bool(deserializing) and bool(password))):
- raise RuntimeError("Bug in DatastoreUser()")
- if not deserializing:
- if name:
- self.name = name
- if password is not None:
- self.password = password
-
- def __str__(self):
- return str(self.name)
-
- def __repr__(self):
- return str(self.serialize())
-
- @classmethod
- def deserialize_user(cls, value):
- if not cls._validate_dict(value):
- raise ValueError(_("Bad dictionary. Keys: %(keys)s. "
- "Required: %(reqs)s")
- % ({'keys': value.keys(),
- 'reqs': cls._dict_requirements()}))
- user = cls(name=None, password=None, deserializing=True)
- user.deserialize(value)
- return user
-
- @property
- def name(self):
- return self._name
-
- @name.setter
- def name(self, value):
- self._validate_user_name(value)
- self._name = value
-
- @property
- def password(self):
- return self._password
-
- @password.setter
- def password(self, value):
- if self._is_valid_password(value):
- self._password = value
- else:
- raise ValueError(_("'%s' is not a valid password.") % value)
-
- @property
- def databases(self):
- return self._databases
-
- @databases.setter
- def databases(self, value):
- mydb = self._build_database_schema(value)
- self._databases.append(mydb.serialize())
-
- @property
- def host(self):
- if self._host is None:
- return self._HOSTNAME_WILDCARD
- return self._host
-
- @host.setter
- def host(self, value):
- if self._is_valid_host_name(value):
- self._host = value
- else:
- raise ValueError(_("'%s' is not a valid hostname.") % value)
-
- @abc.abstractmethod
- def _build_database_schema(self, name):
- """Build a schema for this user.
- :type name: string
- :type character_set: string
- :type collate: string
- """
-
- def _validate_user_name(self, value):
- """Perform validations on a given user name.
- :param value: Validated user name.
- :type value: string
- :raises: ValueError On validation errors.
- """
- if self._max_username_length and (len(value) >
- self._max_username_length):
- raise ValueError(_("User name '%(name)s' is too long. "
- "Max length = %(max_length)d.")
- % {'name': value,
- 'max_length': self._max_username_length})
- elif not self._is_valid_name(value):
- raise ValueError(_("'%s' is not a valid user name.") % value)
-
- @abc.abstractproperty
- def _max_username_length(self):
- """Return the maximum valid user name length if any.
- :returns: Maximum user name length or None if unlimited.
- """
-
- @abc.abstractmethod
- def _is_valid_name(self, value):
- """Validate a given user name.
- :param value: User name to be validated.
- :type value: string
- :returns: TRUE if valid, FALSE otherwise.
- """
-
- @abc.abstractmethod
- def _is_valid_host_name(self, value):
- """Validate a given host name.
- :param value: Host name to be validated.
- :type value: string
- :returns: TRUE if valid, FALSE otherwise.
- """
-
- @abc.abstractmethod
- def _is_valid_password(self, value):
- """Validate a given password.
- :param value: Password to be validated.
- :type value: string
- :returns: TRUE if valid, FALSE otherwise.
- """
-
- @classmethod
- @abc.abstractmethod
- def _dict_requirements(cls):
- """Get the dictionary requirements for a user created via
- deserialization.
- :returns: List of required dictionary keys.
- """
-
-
-class MongoDBUser(DatastoreUser):
- """Represents a MongoDB user and its associated properties.
- MongoDB users are identified using their namd and database.
- Trove stores this as <database>.<username>
- """
-
- def __init__(self, name=None, password=None, *args, **kwargs):
- self._username = None
- self._database = None
- self._roles = []
- super(MongoDBUser, self).__init__(name, password, *args, **kwargs)
-
- @property
- def username(self):
- return self._username
-
- @username.setter
- def username(self, value):
- self._update_name(username=value)
-
- @property
- def database(self):
- return MongoDBSchema.deserialize_schema(self._database)
-
- @database.setter
- def database(self, value):
- self._update_name(database=value)
-
- @property
- def name(self):
- return self._name
-
- @name.setter
- def name(self, value):
- self._update_name(name=value)
-
- def _update_name(self, name=None, username=None, database=None):
- """Keep the name, username, and database values in sync."""
- if name:
- (database, username) = self._parse_name(name)
- if not (database and username):
- missing = 'username' if self.database else 'database'
- raise ValueError(_("MongoDB user's name missing %s.")
- % missing)
- else:
- if username:
- if not self.database:
- raise ValueError(_('MongoDB user missing database.'))
- database = self.database.name
- else: # database
- if not self.username:
- raise ValueError(_('MongoDB user missing username.'))
- username = self.username
- name = '%s.%s' % (database, username)
- self._name = name
- self._username = username
- self._database = self._build_database_schema(database).serialize()
-
- @property
- def roles(self):
- return self._roles
-
- @roles.setter
- def roles(self, value):
- if isinstance(value, list):
- for role in value:
- self._add_role(role)
- else:
- self._add_role(value)
-
- def revoke_role(self, role):
- if role in self.roles:
- self._roles.remove(role)
-
- def _init_roles(self):
- if '_roles' not in self.__dict__:
- self._roles = []
- for db in self._databases:
- self._roles.append({'db': db['_name'], 'role': 'readWrite'})
-
- @classmethod
- def deserialize_user(cls, value):
- user = super(MongoDBUser, cls).deserialize_user(value)
- user.name = user._name
- user._init_roles()
- return user
-
- def _build_database_schema(self, name):
- return MongoDBSchema(name)
-
- @staticmethod
- def _parse_name(value):
- """The name will be <database>.<username>, so split it."""
- parts = value.split('.', 1)
- if len(parts) != 2:
- raise exception.BadRequest(_(
- 'MongoDB user name "%s" not in <database>.<username> format.'
- ) % value)
- return parts[0], parts[1]
-
- @property
- def _max_username_length(self):
- return None
-
- 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
-
- def _add_role(self, value):
- if not self._is_valid_role(value):
- raise ValueError(_('Role %s is invalid.') % value)
- self._roles.append(value)
- if value['role'] == 'readWrite':
- self.databases = value['db']
-
- def _is_valid_role(self, value):
- if not isinstance(value, dict):
- return False
- if not {'db', 'role'} == set(value):
- return False
- return True
-
- @classmethod
- def _dict_requirements(cls):
- return ['_name']
-
-
-class CassandraUser(DatastoreUser):
- """Represents a Cassandra user and its associated properties."""
-
- def __init__(self, name, password=None, *args, **kwargs):
- super(CassandraUser, self).__init__(name, password, *args, **kwargs)
-
- def _build_database_schema(self, name):
- return CassandraSchema(name)
-
- @property
- def _max_username_length(self):
- return 65535
-
- 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 CouchDBUser(DatastoreUser):
- """Represents a CouchDB user and its associated properties."""
-
- def __init__(self, name, password=None, *args, **kwargs):
- super(CouchDBUser, self).__init__(name, password, *args, **kwargs)
-
- def _build_database_schema(self, name):
- return CouchDBSchema(name)
-
- @property
- def _max_username_length(self):
- return None
-
- 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 MySQLUser(Base):
- """Represents a MySQL User and its associated properties."""
-
- not_supported_chars = re.compile("^\s|\s$|'|\"|;|`|,|/|\\\\")
-
- def __init__(self):
- self._name = None
- self._host = None
- self._password = None
- self._databases = []
- self._ignore_users = cfg.get_ignored_users()
-
- def _is_valid(self, value):
- if (not value or
- self.not_supported_chars.search(value) or
- ("%r" % value).find("\\") != -1):
- return False
- else:
- return True
-
- def _is_valid_user_name(self, value):
- if (self._is_valid(value) and
- value.lower() not in self._ignore_users):
- return True
- return False
-
- def _is_valid_host_name(self, value):
- if value in [None, "%"]:
- # % is MySQL shorthand for "everywhere". Always permitted.
- # Null host defaults to % anyway.
- return True
- if CONF.hostname_require_valid_ip:
- try:
- # '%' works as a MySQL wildcard, but it is not a valid
- # part of an IPAddress
- netaddr.IPAddress(value.replace('%', '1'))
- except (ValueError, netaddr.AddrFormatError):
- return False
- else:
- return True
- else:
- # If it wasn't required, anything else goes.
- return True
-
- @property
- def name(self):
- return self._name
-
- @name.setter
- def name(self, value):
- if not self._is_valid_user_name(value):
- raise ValueError(_("'%s' is not a valid user name.") % value)
- elif len(value) > 16:
- raise ValueError(_("User name '%s' is too long. Max length = 16.")
- % value)
- else:
- self._name = value
-
- @property
- def password(self):
- return self._password
-
- @password.setter
- def password(self, value):
- if not self._is_valid(value):
- raise ValueError(_("'%s' is not a valid password.") % value)
- else:
- self._password = value
-
- @property
- def databases(self):
- return self._databases
-
- @databases.setter
- def databases(self, value):
- mydb = ValidatedMySQLDatabase()
- mydb.name = value
- self._databases.append(mydb.serialize())
-
- @property
- def host(self):
- if self._host is None:
- return '%'
- return self._host
-
- @host.setter
- def host(self, value):
- if not self._is_valid_host_name(value):
- raise ValueError(_("'%s' is not a valid hostname.") % value)
- else:
- self._host = value
-
-
-class PostgreSQLUser(DatastoreUser):
- """Represents a PostgreSQL user and its associated properties."""
-
- def __init__(self, name, password=None, *args, **kwargs):
- super(PostgreSQLUser, self).__init__(name, password, *args, **kwargs)
-
- 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 = []
-
-
-class MySQLRootUser(RootUser):
- """Represents the MySQL root user."""
-
- def __init__(self, password=None):
- super(MySQLRootUser, self).__init__()
- self._name = "root"
- self._host = "%"
- if password is None:
- self._password = utils.generate_random_password()
- else:
- self._password = password
-
-
-class CassandraRootUser(CassandraUser):
- """Represents the Cassandra default superuser."""
-
- def __init__(self, password=None, *args, **kwargs):
- if password is None:
- 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, *args, **kwargs):
- if password is None:
- password = utils.generate_random_password()
- super(PostgreSQLRootUser, self).__init__("postgres", password=password,
- *args, **kwargs)
-
-
-class CouchDBRootUser(CouchDBUser):
- """Represents the CouchDB default superuser."""
-
- def __init__(self, password=None, *args, **kwargs):
- if password is None:
- password = utils.generate_random_password()
- super(CouchDBRootUser, self).__init__("root", password=password,
- *args, **kwargs)
diff --git a/trove/guestagent/strategies/backup/experimental/db2_impl.py b/trove/guestagent/strategies/backup/experimental/db2_impl.py
index 14790459..855fd5c4 100644
--- a/trove/guestagent/strategies/backup/experimental/db2_impl.py
+++ b/trove/guestagent/strategies/backup/experimental/db2_impl.py
@@ -14,13 +14,13 @@
# under the License.
from oslo_log import log as logging
+from trove.common.db import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import utils
from trove.guestagent.common import operating_system
from trove.guestagent.datastore.experimental.db2 import service
from trove.guestagent.datastore.experimental.db2 import system
-from trove.guestagent.db import models
from trove.guestagent.strategies.backup import base
LOG = logging.getLogger(__name__)
@@ -39,7 +39,7 @@ class DB2Backup(base.BackupRunner):
dbNames = []
databases, marker = self.admin.list_databases()
for database in databases:
- mydb = models.MySQLDatabase()
+ mydb = models.DatastoreSchema()
mydb.deserialize(database)
dbNames.append(mydb.name)
return dbNames
diff --git a/trove/guestagent/strategies/replication/experimental/postgresql_impl.py b/trove/guestagent/strategies/replication/experimental/postgresql_impl.py
index 40e4a0a8..e0b1e3a1 100644
--- a/trove/guestagent/strategies/replication/experimental/postgresql_impl.py
+++ b/trove/guestagent/strategies/replication/experimental/postgresql_impl.py
@@ -19,6 +19,7 @@ import os
from oslo_log import log as logging
from oslo_utils import netutils
from trove.common import cfg
+from trove.common.db.postgresql import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import stream_codecs
@@ -26,7 +27,6 @@ from trove.common import utils
from trove.guestagent.backup.backupagent import BackupAgent
from trove.guestagent.common import operating_system
from trove.guestagent.common.operating_system import FileMode
-from trove.guestagent.db import models
from trove.guestagent.strategies import backup
from trove.guestagent.strategies.replication import base
diff --git a/trove/guestagent/strategies/replication/mysql_base.py b/trove/guestagent/strategies/replication/mysql_base.py
index 6a28b3b0..f2cc618b 100644
--- a/trove/guestagent/strategies/replication/mysql_base.py
+++ b/trove/guestagent/strategies/replication/mysql_base.py
@@ -21,11 +21,11 @@ from oslo_log import log as logging
from oslo_utils import netutils
from trove.common import cfg
+from trove.common.db.mysql import models
from trove.common.i18n import _
from trove.common import utils
from trove.guestagent.backup.backupagent import BackupAgent
from trove.guestagent.datastore.mysql.service import MySqlAdmin
-from trove.guestagent.db import models
from trove.guestagent.strategies import backup
from trove.guestagent.strategies.replication import base
@@ -65,14 +65,20 @@ class MysqlReplicationBase(base.Replication):
replication_user = None
replication_password = utils.generate_random_password(16)
- mysql_user = models.MySQLUser()
- mysql_user.password = replication_password
+ mysql_user = None # cache the model as we just want name validation
retry_count = 0
while replication_user is None:
try:
- mysql_user.name = 'slave_' + str(uuid.uuid4())[:8]
+ name = 'slave_' + str(uuid.uuid4())[:8]
+ if mysql_user:
+ mysql_user.name = name
+ else:
+ mysql_user = models.MySQLUser(
+ name=name, password=replication_password
+ )
+ mysql_user.check_create()
MySqlAdmin().create_user([mysql_user.serialize()])
LOG.debug("Trying to create replication user " +
mysql_user.name)
diff --git a/trove/tests/unittests/common/test_dbmodels.py b/trove/tests/unittests/common/test_dbmodels.py
new file mode 100644
index 00000000..77bfdfeb
--- /dev/null
+++ b/trove/tests/unittests/common/test_dbmodels.py
@@ -0,0 +1,342 @@
+# Copyright 2016 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.
+
+import mock
+
+from trove.common.db import models
+from trove.tests.unittests import trove_testtools
+
+
+class DatastoreSchemaTest(trove_testtools.TestCase):
+
+ def setUp(self):
+ super(DatastoreSchemaTest, self).setUp()
+ self.dbname = 'testdb'
+ self.serial_db = {'_name': self.dbname,
+ '_character_set': None,
+ '_collate': None}
+
+ def tearDown(self):
+ super(DatastoreSchemaTest, self).tearDown()
+
+ def _empty_schema(self):
+ return models.DatastoreSchema(deserializing=True)
+
+ def test_init_name(self):
+ database = models.DatastoreSchema(self.dbname)
+ self.assertEqual(self.dbname, database.name)
+ database2 = models.DatastoreSchema(name=self.dbname)
+ self.assertEqual(self.dbname, database2.name)
+
+ def test_init_no_name(self):
+ self.assertRaises(RuntimeError, models.DatastoreSchema)
+
+ @mock.patch.object(models.DatastoreSchema, 'verify_dict')
+ def test_init_deserializing(self, mock_verify):
+ database = models.DatastoreSchema.deserialize(self.serial_db)
+ mock_verify.assert_any_call()
+ self.assertEqual(self.dbname, database.name)
+
+ def test_serialize(self):
+ database = models.DatastoreSchema(self.dbname)
+ self.assertEqual(self.serial_db, database.serialize())
+
+ def test_name_property(self):
+ test_name = "Anna"
+ database = self._empty_schema()
+ database.name = test_name
+ self.assertEqual(test_name, database.name)
+
+ def _do_validate_bad_schema_name(self, name):
+ database = self._empty_schema()
+ self.assertRaises(ValueError, database._validate_schema_name, name)
+
+ def test_validate_name_empty(self):
+ self._do_validate_bad_schema_name(None)
+
+ @mock.patch.object(models.DatastoreSchema, '_max_schema_name_length',
+ new_callable=mock.PropertyMock)
+ def test_validate_name_long(self, mock_max_len):
+ mock_max_len.return_value = 5
+ self._do_validate_bad_schema_name('toolong')
+
+ @mock.patch.object(models.DatastoreSchema, '_is_valid_schema_name')
+ def test_validate_name_invalid(self, mock_is_valid):
+ mock_is_valid.return_value = False
+ self._do_validate_bad_schema_name('notvalid')
+
+ def test_verify_dict(self):
+ database = models.DatastoreSchema(self.dbname)
+ # using context patch because the property setter needs to work
+ # properly during init for this test
+ with mock.patch.object(
+ models.DatastoreSchema, 'name',
+ new_callable=mock.PropertyMock) as mock_name_property:
+ database.verify_dict()
+ mock_name_property.assert_called_with(self.dbname)
+
+ def test_checks_pass(self):
+ database = models.DatastoreSchema(self.dbname)
+ database.check_reserved()
+ database.check_create()
+ database.check_delete()
+
+ @mock.patch.object(models.DatastoreSchema, 'ignored_dbs',
+ new_callable=mock.PropertyMock)
+ def test_checks_fail(self, mock_ignored_dbs):
+ mock_ignored_dbs.return_value = [self.dbname]
+ database = models.DatastoreSchema(self.dbname)
+ self.assertRaises(ValueError, database.check_reserved)
+ self.assertRaises(ValueError, database.check_create)
+ self.assertRaises(ValueError, database.check_delete)
+
+
+class DatastoreUserTest(trove_testtools.TestCase):
+
+ def setUp(self):
+ super(DatastoreUserTest, self).setUp()
+ self.username = 'testuser'
+ self.password = 'password'
+ self.host = '192.168.0.1'
+ self.dbname = 'testdb'
+ self.serial_db = {'_name': self.dbname,
+ '_character_set': None,
+ '_collate': None}
+ self.databases = [self.serial_db]
+ self.host_wildcard = '%'
+ self.serial_user_basic = {
+ '_name': self.username, '_password': None,
+ '_host': self.host_wildcard, '_databases': [],
+ '_is_root': False
+ }
+ self.serial_user_full = {
+ '_name': self.username, '_password': self.password,
+ '_host': self.host, '_databases': self.databases,
+ '_is_root': False
+ }
+
+ def tearDown(self):
+ super(DatastoreUserTest, self).tearDown()
+
+ def _empty_user(self):
+ return models.DatastoreUser(deserializing=True)
+
+ def _test_user_basic(self, user):
+ self.assertEqual(self.username, user.name)
+ self.assertEqual(None, user.password)
+ self.assertEqual(self.host_wildcard, user.host)
+ self.assertEqual([], user.databases)
+
+ def _test_user_full(self, user):
+ self.assertEqual(self.username, user.name)
+ self.assertEqual(self.password, user.password)
+ self.assertEqual(self.host, user.host)
+ self.assertEqual(self.databases, user.databases)
+
+ def test_init_name(self):
+ user1 = models.DatastoreUser(self.username)
+ self._test_user_basic(user1)
+ user2 = models.DatastoreUser(name=self.username)
+ self._test_user_basic(user2)
+
+ def test_init_no_name(self):
+ self.assertRaises(ValueError, models.DatastoreUser)
+
+ def test_init_options(self):
+ user1 = models.DatastoreUser(self.username)
+ self._test_user_basic(user1)
+ user2 = models.DatastoreUser(self.username, self.password,
+ self.host, self.dbname)
+ self._test_user_full(user2)
+ user3 = models.DatastoreUser(name=self.username,
+ password=self.password,
+ host=self.host,
+ databases=self.dbname)
+ self._test_user_full(user3)
+
+ @mock.patch.object(models.DatastoreUser, 'verify_dict')
+ def test_init_deserializing(self, mock_verify):
+ user1 = models.DatastoreUser.deserialize(self.serial_user_basic)
+ self._test_user_basic(user1)
+ user2 = models.DatastoreUser.deserialize(self.serial_user_full)
+ self._test_user_full(user2)
+ self.assertEqual(2, mock_verify.call_count)
+
+ def test_serialize(self):
+ user1 = models.DatastoreUser(self.username)
+ self.assertEqual(self.serial_user_basic, user1.serialize())
+ user2 = models.DatastoreUser(self.username, self.password,
+ self.host, self.dbname)
+ self.assertEqual(self.serial_user_full, user2.serialize())
+
+ @mock.patch.object(models.DatastoreUser, '_validate_user_name')
+ def test_name_property(self, mock_validate):
+ test_name = "Anna"
+ user = self._empty_user()
+ user.name = test_name
+ self.assertEqual(test_name, user.name)
+ mock_validate.assert_called_with(test_name)
+
+ def _do_validate_bad_user_name(self, name):
+ user = self._empty_user()
+ self.assertRaises(ValueError, user._validate_user_name, name)
+
+ def test_validate_name_empty(self):
+ self._do_validate_bad_user_name(None)
+
+ @mock.patch.object(models.DatastoreUser, '_max_user_name_length',
+ new_callable=mock.PropertyMock)
+ def test_validate_name_long(self, mock_max_len):
+ mock_max_len.return_value = 5
+ self._do_validate_bad_user_name('toolong')
+
+ @mock.patch.object(models.DatastoreUser, '_is_valid_user_name')
+ def test_validate_name_invalid(self, mock_is_valid):
+ mock_is_valid.return_value = False
+ self._do_validate_bad_user_name('notvalid')
+
+ @mock.patch.object(models.DatastoreUser, '_is_valid_password')
+ def test_password_property(self, mock_validate):
+ test_password = "NewPassword"
+ user = self._empty_user()
+ user.password = test_password
+ mock_validate.assert_called_with(test_password)
+ self.assertEqual(test_password, user.password)
+
+ @mock.patch.object(models.DatastoreUser, '_is_valid_password')
+ def test_password_property_error(self, mock_validate):
+ mock_validate.return_value = False
+ test_password = "NewPassword"
+ user = self._empty_user()
+
+ def test():
+ user.password = test_password
+
+ self.assertRaises(ValueError, test)
+
+ @mock.patch.object(models.DatastoreUser, '_is_valid_host_name')
+ def test_host_property(self, mock_validate):
+ test_host = "192.168.0.2"
+ user = self._empty_user()
+ user.host = test_host
+ mock_validate.assert_called_with(test_host)
+ self.assertEqual(test_host, user.host)
+
+ @mock.patch.object(models.DatastoreUser, '_is_valid_host_name')
+ def test_host_property_error(self, mock_validate):
+ mock_validate.return_value = False
+ test_host = "192.168.0.2"
+ user = self._empty_user()
+
+ def test():
+ user.host = test_host
+
+ self.assertRaises(ValueError, test)
+
+ @mock.patch.object(models.DatastoreUser, '_add_database')
+ def test_databases_property(self, mock_add_database):
+ test_dbname1 = 'otherdb'
+ test_dbname2 = 'lastdb'
+ user = self._empty_user()
+
+ def test(value):
+ user._databases.append({'_name': value,
+ '_character_set': None,
+ '_collate': None})
+
+ mock_add_database.side_effect = test
+ user.databases = self.dbname
+ user.databases = [test_dbname1, test_dbname2]
+ mock_add_database.assert_any_call(self.dbname)
+ mock_add_database.assert_any_call(test_dbname1)
+ mock_add_database.assert_any_call(test_dbname2)
+ self.assertIn(self.serial_db, user.databases)
+ self.assertIn({'_name': test_dbname1,
+ '_character_set': None,
+ '_collate': None}, user.databases)
+ self.assertIn({'_name': test_dbname2,
+ '_character_set': None,
+ '_collate': None}, user.databases)
+
+ def test_build_database_schema(self):
+ user = self._empty_user()
+ schema = user._build_database_schema(self.dbname)
+ self.assertEqual(self.serial_db, schema.serialize())
+
+ def test_add_database(self):
+ user = self._empty_user()
+ user._add_database(self.dbname)
+ self.assertEqual([self.serial_db], user.databases)
+ # check that adding an exsting db does nothing
+ user._add_database(self.dbname)
+ self.assertEqual([self.serial_db], user.databases)
+
+ @mock.patch.object(models, 'DatastoreSchema')
+ def test_deserialize_schema(self, mock_ds_schema):
+ mock_ds_schema.deserialize = mock.Mock()
+ user = self._empty_user()
+ user.deserialize_schema(self.serial_db)
+ mock_ds_schema.deserialize.assert_called_with(self.serial_db)
+
+ @mock.patch.object(models.DatastoreUser, 'deserialize_schema')
+ @mock.patch.object(models.DatastoreUser, 'host',
+ new_callable=mock.PropertyMock)
+ @mock.patch.object(models.DatastoreUser, 'password',
+ new_callable=mock.PropertyMock)
+ @mock.patch.object(models.DatastoreUser, 'name',
+ new_callable=mock.PropertyMock)
+ def _test_verify_dict_with_mocks(self, user,
+ mock_name_property,
+ mock_password_property,
+ mock_host_property,
+ mock_deserialize_schema):
+ user.verify_dict()
+ mock_name_property.assert_called_with(self.username)
+ mock_password_property.assert_called_with(self.password)
+ mock_host_property.assert_called_with(self.host)
+ mock_deserialize_schema.assert_called_with(self.serial_db)
+
+ def test_verify_dict(self):
+ user = models.DatastoreUser(self.username, self.password,
+ self.host, self.dbname)
+ self._test_verify_dict_with_mocks(user)
+
+ def test_validate_dict_defaults(self):
+ user = models.DatastoreUser(self.username)
+ user.verify_dict()
+ self.assertEqual(None, user.password)
+ self.assertEqual(self.host_wildcard, user.host)
+ self.assertEqual([], user.databases)
+
+ def test_is_root(self):
+ user = models.DatastoreUser(self.username)
+ self.assertFalse(user._is_root)
+ user.make_root()
+ self.assertTrue(user._is_root)
+
+ def test_checks_pass(self):
+ user = models.DatastoreUser(self.username)
+ user.check_reserved()
+ user.check_create()
+ user.check_delete()
+
+ @mock.patch.object(models.DatastoreUser, 'ignored_users',
+ new_callable=mock.PropertyMock)
+ def test_checks_fail(self, mock_ignored_users):
+ mock_ignored_users.return_value = [self.username]
+ user = models.DatastoreUser(self.username)
+ self.assertRaises(ValueError, user.check_reserved)
+ self.assertRaises(ValueError, user.check_create)
+ self.assertRaises(ValueError, user.check_delete)
diff --git a/trove/tests/unittests/guestagent/test_cassandra_manager.py b/trove/tests/unittests/guestagent/test_cassandra_manager.py
index 5cd020b1..36429b75 100644
--- a/trove/tests/unittests/guestagent/test_cassandra_manager.py
+++ b/trove/tests/unittests/guestagent/test_cassandra_manager.py
@@ -26,6 +26,7 @@ from mock import patch
from oslo_utils import netutils
from testtools import ExpectedException
+from trove.common.db.cassandra import models
from trove.common import exception
from trove.common.instance import ServiceStatuses
from trove.guestagent import backup
@@ -35,7 +36,6 @@ from trove.guestagent.datastore.experimental.cassandra import (
manager as cass_manager)
from trove.guestagent.datastore.experimental.cassandra import (
service as cass_service)
-from trove.guestagent.db import models
from trove.guestagent import pkg as pkg
from trove.guestagent import volume
from trove.tests.unittests.guestagent.test_datastore_manager import \
@@ -52,7 +52,7 @@ class GuestAgentCassandraDBManagerTest(DatastoreManagerTest):
__N_BU = '_build_user'
__N_RU = '_rename_user'
__N_AUP = '_alter_user_password'
- __N_CAU = 'trove.guestagent.db.models.CassandraUser'
+ __N_CAU = 'trove.common.db.cassandra.models.CassandraUser'
__N_CU = '_create_user'
__N_GFA = '_grant_full_access_on_keyspace'
__N_DU = '_drop_user'
diff --git a/trove/tests/unittests/guestagent/test_dbaas.py b/trove/tests/unittests/guestagent/test_dbaas.py
index 9cb89abb..8d4aaa0e 100644
--- a/trove/tests/unittests/guestagent/test_dbaas.py
+++ b/trove/tests/unittests/guestagent/test_dbaas.py
@@ -32,6 +32,7 @@ import sqlalchemy
from trove.common import cfg
from trove.common import context as trove_context
+from trove.common.db.mysql import models as mysql_models
from trove.common.exception import BadRequest
from trove.common.exception import GuestError
from trove.common.exception import PollTimeOut
@@ -77,7 +78,6 @@ from trove.guestagent.datastore.mysql.service import MySqlRootAccess
import trove.guestagent.datastore.mysql_common.service as mysql_common_service
import trove.guestagent.datastore.service as base_datastore_service
from trove.guestagent.datastore.service import BaseDbStatus
-from trove.guestagent.db import models
from trove.guestagent import dbaas as dbaas_sr
from trove.guestagent.dbaas import get_filesystem_volume_stats
from trove.guestagent import pkg
@@ -414,7 +414,7 @@ class MySqlAdminTest(trove_testtools.TestCase):
local_client_patcher.start()
self.orig_MySQLUser_is_valid_user_name = (
- models.MySQLUser._is_valid_user_name)
+ mysql_models.MySQLUser._is_valid_user_name)
dbaas.get_engine = MagicMock(name='get_engine')
# trove.guestagent.common.configuration import ConfigurationManager
@@ -430,7 +430,7 @@ class MySqlAdminTest(trove_testtools.TestCase):
def tearDown(self):
dbaas.get_engine = self.orig_get_engine
- models.MySQLUser._is_valid_user_name = (
+ mysql_models.MySQLUser._is_valid_user_name = (
self.orig_MySQLUser_is_valid_user_name)
dbaas.MySqlApp.configuration_manager = \
dbaas.orig_configuration_manager
@@ -1515,7 +1515,6 @@ class TextClauseMatcher(object):
return "TextClause(%s)" % self.text
def __eq__(self, arg):
- print("Matching %s" % arg.text)
return self.text in arg.text
@@ -3398,7 +3397,8 @@ class DB2AdminTest(trove_testtools.TestCase):
def test_delete_users_without_db(self):
FAKE_USER.append(
- {"_name": "random2", "_password": "guesswhat", "_databases": []})
+ {"_name": "random2", "_password": "guesswhat", "_host": '%',
+ "_databases": []})
with patch.object(db2service, 'run_command',
MagicMock(return_value=None)):
with patch.object(db2service.DB2Admin, 'list_access',
@@ -3418,6 +3418,7 @@ class DB2AdminTest(trove_testtools.TestCase):
expected, args[0],
"Revoke database access queries are not the same")
self.assertEqual(1, db2service.run_command.call_count)
+ FAKE_USER.pop()
def test_list_users(self):
databases = []
diff --git a/trove/tests/unittests/guestagent/test_dbmodels.py b/trove/tests/unittests/guestagent/test_dbmodels.py
deleted file mode 100644
index b9bea43d..00000000
--- a/trove/tests/unittests/guestagent/test_dbmodels.py
+++ /dev/null
@@ -1,112 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-#
-# 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 mock import MagicMock
-
-from trove.guestagent.db import models as dbmodels
-from trove.tests.unittests import trove_testtools
-
-
-class MySQLDatabaseTest(trove_testtools.TestCase):
-
- def setUp(self):
- super(MySQLDatabaseTest, self).setUp()
-
- self.mysqlDb = dbmodels.ValidatedMySQLDatabase()
- self.origin_ignore_db = self.mysqlDb._ignore_dbs
- self.mysqlDb._ignore_dbs = ['mysql']
-
- def tearDown(self):
- super(MySQLDatabaseTest, self).tearDown()
- self.mysqlDb._ignore_dbs = self.origin_ignore_db
-
- def test_name(self):
- self.assertIsNone(self.mysqlDb.name)
-
- def test_name_setter(self):
- test_name = "Anna"
- self.mysqlDb.name = test_name
- self.assertEqual(test_name, self.mysqlDb.name)
-
- def test_is_valid_positive(self):
- self.assertTrue(self.mysqlDb._is_valid('pymysql'))
-
- def test_is_valid_negative(self):
- self.assertFalse(self.mysqlDb._is_valid('mysql'))
-
-
-class MySQLUserTest(trove_testtools.TestCase):
- def setUp(self):
- super(MySQLUserTest, self).setUp()
- self.mysqlUser = dbmodels.MySQLUser()
-
- def tearDown(self):
- super(MySQLUserTest, self).tearDown()
-
- def test_is_valid_negative(self):
- self.assertFalse(self.mysqlUser._is_valid(None))
- self.assertFalse(self.mysqlUser._is_valid("|;"))
- self.assertFalse(self.mysqlUser._is_valid("\\"))
-
- def test_is_valid_positive(self):
- self.assertTrue(self.mysqlUser._is_valid("real_name"))
-
-
-class IsValidUsernameTest(trove_testtools.TestCase):
- def setUp(self):
- super(IsValidUsernameTest, self).setUp()
- self.mysqlUser = dbmodels.MySQLUser()
- self.origin_is_valid = self.mysqlUser._is_valid
- self.origin_ignore_users = self.mysqlUser._ignore_users
- self.mysqlUser._ignore_users = ["king"]
-
- def tearDown(self):
- super(IsValidUsernameTest, self).tearDown()
- self.mysqlUser._is_valid = self.origin_is_valid
- self.mysqlUser._ignore_users = self.origin_ignore_users
-
- def test_is_valid_user_name(self):
- value = "trove"
- self.assertTrue(self.mysqlUser._is_valid_user_name(value))
-
- def test_is_valid_user_name_negative(self):
- self.mysqlUser._is_valid = MagicMock(return_value=False)
- self.assertFalse(self.mysqlUser._is_valid_user_name("trove"))
-
- self.mysqlUser._is_valid = MagicMock(return_value=True)
- self.assertFalse(self.mysqlUser._is_valid_user_name("king"))
-
-
-class IsValidHostnameTest(trove_testtools.TestCase):
- def setUp(self):
- super(IsValidHostnameTest, self).setUp()
- self.mysqlUser = dbmodels.MySQLUser()
-
- def tearDown(self):
- super(IsValidHostnameTest, self).tearDown()
-
- def test_is_valid_octet(self):
- self.assertTrue(self.mysqlUser._is_valid_host_name('192.168.1.1'))
-
- def test_is_valid_bad_octet(self):
- self.assertFalse(self.mysqlUser._is_valid_host_name('999.168.1.1'))
-
- def test_is_valid_global_wildcard(self):
- self.assertTrue(self.mysqlUser._is_valid_host_name('%'))
-
- def test_is_valid_prefix_wildcard(self):
- self.assertTrue(self.mysqlUser._is_valid_host_name('%.168.1.1'))
-
- def test_is_valid_suffix_wildcard(self):
- self.assertTrue(self.mysqlUser._is_valid_host_name('192.168.1.%'))
diff --git a/trove/tests/unittests/guestagent/test_mongodb_manager.py b/trove/tests/unittests/guestagent/test_mongodb_manager.py
index 4fe48239..b6ae7f3b 100644
--- a/trove/tests/unittests/guestagent/test_mongodb_manager.py
+++ b/trove/tests/unittests/guestagent/test_mongodb_manager.py
@@ -15,12 +15,12 @@
import mock
import pymongo
+import trove.common.db.mongodb.models as models
import trove.common.utils as utils
import trove.guestagent.backup as backup
from trove.guestagent.common.configuration import ImportOverrideStrategy
import trove.guestagent.datastore.experimental.mongodb.manager as manager
import trove.guestagent.datastore.experimental.mongodb.service as service
-import trove.guestagent.db.models as models
import trove.guestagent.volume as volume
from trove.tests.unittests.guestagent.test_datastore_manager import \
DatastoreManagerTest
@@ -46,6 +46,17 @@ class GuestAgentMongoDBManagerTest(DatastoreManagerTest):
self.pymongo_patch.start()
self.mount_point = '/var/lib/mongodb'
+ self.host_wildcard = '%' # This is used in the test_*_user tests below
+ self.serialized_user = {
+ '_name': 'testdb.testuser', '_password': None,
+ '_roles': [{'db': 'testdb', 'role': 'testrole'}],
+ '_username': 'testuser', '_databases': [],
+ '_host': self.host_wildcard,
+ '_database': {'_name': 'testdb',
+ '_character_set': None,
+ '_collate': None},
+ '_is_root': False
+ }
def tearDown(self):
super(GuestAgentMongoDBManagerTest, self).tearDown()
@@ -152,21 +163,12 @@ class GuestAgentMongoDBManagerTest(DatastoreManagerTest):
mocked_enable_root.assert_called_with('test_password')
- # This is used in the test_*_user tests below
- _serialized_user = {'_name': 'testdb.testuser', '_password': None,
- '_roles': [{'db': 'testdb', 'role': 'testrole'}],
- '_username': 'testuser', '_databases': [],
- '_host': None,
- '_database': {'_name': 'testdb',
- '_character_set': None,
- '_collate': None}}
-
@mock.patch.object(service, 'MongoDBClient')
@mock.patch.object(service.MongoDBAdmin, '_admin_user')
@mock.patch.object(service.MongoDBAdmin, '_get_user_record')
def test_create_user(self, mocked_get_user, mocked_admin_user,
mocked_client):
- user = self._serialized_user.copy()
+ user = self.serialized_user.copy()
user['_password'] = 'testpassword'
users = [user]
@@ -184,7 +186,7 @@ class GuestAgentMongoDBManagerTest(DatastoreManagerTest):
def test_delete_user(self, mocked_admin_user, mocked_client):
client = mocked_client().__enter__()['testdb']
- self.manager.delete_user(self.context, self._serialized_user)
+ self.manager.delete_user(self.context, self.serialized_user)
client.remove_user.assert_called_with('testuser')
@@ -202,20 +204,20 @@ class GuestAgentMongoDBManagerTest(DatastoreManagerTest):
result = self.manager.get_user(self.context, 'testdb.testuser', None)
mocked_find.assert_called_with({'user': 'testuser', 'db': 'testdb'})
- self.assertEqual(self._serialized_user, result)
+ self.assertEqual(self.serialized_user, result)
@mock.patch.object(service, 'MongoDBClient')
@mock.patch.object(service.MongoDBAdmin, '_admin_user')
def test_list_users(self, mocked_admin_user, mocked_client):
# roles are NOT returned by list_users
- user1 = self._serialized_user.copy()
- user2 = self._serialized_user.copy()
+ user1 = self.serialized_user.copy()
+ user2 = self.serialized_user.copy()
user2['_name'] = 'testdb.otheruser'
user2['_username'] = 'otheruser'
user2['_roles'] = [{'db': 'testdb2', 'role': 'readWrite'}]
user2['_databases'] = [{'_name': 'testdb2',
- '_character_set': None,
- '_collate': None}]
+ '_character_set': None,
+ '_collate': None}]
mocked_find = mock.MagicMock(return_value=[
{
@@ -256,7 +258,8 @@ class GuestAgentMongoDBManagerTest(DatastoreManagerTest):
'_password': 'password',
'_roles': [{'db': 'admin', 'role': 'root'}],
'_databases': [],
- '_host': None}
+ '_host': self.host_wildcard,
+ '_is_root': True}
result = self.manager.enable_root(self.context)