From 0b0939b87c9bc598f72ca702372762edd6323763 Mon Sep 17 00:00:00 2001 From: Vineet Pruthi <48789821+vineet-pruthi@users.noreply.github.com> Date: Fri, 11 Oct 2019 15:34:47 +0200 Subject: [PATCH] Rds v3 (#50) * Adding RDS v3 implementation --- doc/source/enforcer.py | 1 + .../user/proxies/{rds.rst => rds_v1.rst} | 0 doc/source/user/proxies/rds_v3.rst | 55 + doc/source/user/resources/rds/index.rst | 3 + .../user/resources/rds/v3/configuration.rst | 13 + doc/source/user/resources/rds/v3/flavor.rst | 13 + doc/source/user/resources/rds/v3/instance.rst | 13 + otcextensions/osclient/rds/client.py | 14 +- otcextensions/osclient/rds/v3/__init__.py | 0 otcextensions/osclient/rds/v3/backup.py | 211 +++ .../osclient/rds/v3/configuration.py | 300 ++++ otcextensions/osclient/rds/v3/datastore.py | 69 + otcextensions/osclient/rds/v3/flavor.py | 63 + otcextensions/osclient/rds/v3/instance.py | 663 +++++++++ otcextensions/sdk/__init__.py | 1 + otcextensions/sdk/job.py | 99 ++ otcextensions/sdk/rds/rds_service.py | 6 +- otcextensions/sdk/rds/v3/__init__.py | 0 otcextensions/sdk/rds/v3/_base.py | 146 ++ otcextensions/sdk/rds/v3/_proxy.py | 402 ++++++ otcextensions/sdk/rds/v3/backup.py | 97 ++ otcextensions/sdk/rds/v3/configuration.py | 104 ++ otcextensions/sdk/rds/v3/datastore.py | 30 + otcextensions/sdk/rds/v3/flavor.py | 39 + otcextensions/sdk/rds/v3/instance.py | 314 +++++ .../tests/functional/osclient/rds/__init__.py | 0 .../functional/osclient/rds/v3/__init__.py | 0 .../osclient/rds/v3/test_configuration.py | 148 ++ .../osclient/rds/v3/test_datastore.py | 35 + .../functional/osclient/rds/v3/test_flavor.py | 36 + .../osclient/rds/v3/test_instance.py | 39 + .../tests/unit/osclient/dns/v2/fakes.py | 21 +- .../tests/unit/osclient/rds/v3/__init__.py | 0 .../tests/unit/osclient/rds/v3/fakes.py | 204 +++ .../tests/unit/osclient/rds/v3/test_backup.py | 280 ++++ .../osclient/rds/v3/test_configuration.py | 412 ++++++ .../unit/osclient/rds/v3/test_datastore.py | 96 ++ .../tests/unit/osclient/rds/v3/test_flavor.py | 61 + .../unit/osclient/rds/v3/test_instance.py | 1212 +++++++++++++++++ otcextensions/tests/unit/sdk/rds/__init__.py | 0 .../tests/unit/sdk/rds/v3/__init__.py | 0 .../tests/unit/sdk/rds/v3/test_backup.py | 83 ++ .../tests/unit/sdk/rds/v3/test_config.py | 131 ++ .../tests/unit/sdk/rds/v3/test_datastore.py | 43 + .../tests/unit/sdk/rds/v3/test_flavor.py | 50 + .../tests/unit/sdk/rds/v3/test_instance.py | 251 ++++ .../tests/unit/sdk/rds/v3/test_proxy.py | 204 +++ setup.cfg | 38 +- 48 files changed, 5970 insertions(+), 30 deletions(-) rename doc/source/user/proxies/{rds.rst => rds_v1.rst} (100%) create mode 100644 doc/source/user/proxies/rds_v3.rst create mode 100644 doc/source/user/resources/rds/v3/configuration.rst create mode 100644 doc/source/user/resources/rds/v3/flavor.rst create mode 100644 doc/source/user/resources/rds/v3/instance.rst create mode 100644 otcextensions/osclient/rds/v3/__init__.py create mode 100644 otcextensions/osclient/rds/v3/backup.py create mode 100644 otcextensions/osclient/rds/v3/configuration.py create mode 100644 otcextensions/osclient/rds/v3/datastore.py create mode 100644 otcextensions/osclient/rds/v3/flavor.py create mode 100644 otcextensions/osclient/rds/v3/instance.py create mode 100644 otcextensions/sdk/job.py create mode 100644 otcextensions/sdk/rds/v3/__init__.py create mode 100644 otcextensions/sdk/rds/v3/_base.py create mode 100644 otcextensions/sdk/rds/v3/_proxy.py create mode 100644 otcextensions/sdk/rds/v3/backup.py create mode 100644 otcextensions/sdk/rds/v3/configuration.py create mode 100644 otcextensions/sdk/rds/v3/datastore.py create mode 100644 otcextensions/sdk/rds/v3/flavor.py create mode 100644 otcextensions/sdk/rds/v3/instance.py create mode 100644 otcextensions/tests/functional/osclient/rds/__init__.py create mode 100644 otcextensions/tests/functional/osclient/rds/v3/__init__.py create mode 100644 otcextensions/tests/functional/osclient/rds/v3/test_configuration.py create mode 100644 otcextensions/tests/functional/osclient/rds/v3/test_datastore.py create mode 100644 otcextensions/tests/functional/osclient/rds/v3/test_flavor.py create mode 100644 otcextensions/tests/functional/osclient/rds/v3/test_instance.py create mode 100644 otcextensions/tests/unit/osclient/rds/v3/__init__.py create mode 100644 otcextensions/tests/unit/osclient/rds/v3/fakes.py create mode 100644 otcextensions/tests/unit/osclient/rds/v3/test_backup.py create mode 100644 otcextensions/tests/unit/osclient/rds/v3/test_configuration.py create mode 100644 otcextensions/tests/unit/osclient/rds/v3/test_datastore.py create mode 100644 otcextensions/tests/unit/osclient/rds/v3/test_flavor.py create mode 100644 otcextensions/tests/unit/osclient/rds/v3/test_instance.py create mode 100644 otcextensions/tests/unit/sdk/rds/__init__.py create mode 100644 otcextensions/tests/unit/sdk/rds/v3/__init__.py create mode 100644 otcextensions/tests/unit/sdk/rds/v3/test_backup.py create mode 100644 otcextensions/tests/unit/sdk/rds/v3/test_config.py create mode 100644 otcextensions/tests/unit/sdk/rds/v3/test_datastore.py create mode 100644 otcextensions/tests/unit/sdk/rds/v3/test_flavor.py create mode 100644 otcextensions/tests/unit/sdk/rds/v3/test_instance.py create mode 100644 otcextensions/tests/unit/sdk/rds/v3/test_proxy.py diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py index 80f9db381..ca79b0311 100644 --- a/doc/source/enforcer.py +++ b/doc/source/enforcer.py @@ -43,6 +43,7 @@ def get_proxy_methods(): "otcextensions.sdk.kms.v1._proxy", "otcextensions.sdk.obs.v1._proxy", "otcextensions.sdk.rds.v1._proxy", + "otcextensions.sdk.rds.v3._proxy", "otcextensions.sdk.volume_backup.v2._proxy" ] diff --git a/doc/source/user/proxies/rds.rst b/doc/source/user/proxies/rds_v1.rst similarity index 100% rename from doc/source/user/proxies/rds.rst rename to doc/source/user/proxies/rds_v1.rst diff --git a/doc/source/user/proxies/rds_v3.rst b/doc/source/user/proxies/rds_v3.rst new file mode 100644 index 000000000..2efeddecc --- /dev/null +++ b/doc/source/user/proxies/rds_v3.rst @@ -0,0 +1,55 @@ +Database RDS API +================ + +For details on how to use database, see :doc:`/user/guides/rds` + +.. automodule:: otcextensions.sdk.rds.v3._proxy + +The Database Class +------------------ + +The database high-level interface is available through the ``rds`` member of a +:class:`~openstack.connection.Connection` object. The ``rds`` member will only +be added if the ``otcextensions.sdk.register_otc_extensions(conn)`` method is +called. + +Datastore Operations +^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.rds.v3._proxy.Proxy + + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.datastore_versions + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.get_datastore_version + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.datastore_types + +Flavor Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.rds.v3._proxy.Proxy + + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.get_flavor + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.flavors + +Instance Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.rds.v3._proxy.Proxy + + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.create_instance + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.update_instance + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.delete_instance + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.get_instance + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.find_instance + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.instances + + +Backup Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.rds.v3._proxy.Proxy + + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.backups + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.create_backup + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.delete_backup + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.get_backup_policy + .. automethod:: otcextensions.sdk.rds.v3._proxy.Proxy.set_backup_policy diff --git a/doc/source/user/resources/rds/index.rst b/doc/source/user/resources/rds/index.rst index c27641abc..935c168e6 100644 --- a/doc/source/user/resources/rds/index.rst +++ b/doc/source/user/resources/rds/index.rst @@ -7,3 +7,6 @@ RDS Resources v1/configuration v1/flavor v1/instance + v3/configuration + v3/flavor + v3/instance diff --git a/doc/source/user/resources/rds/v3/configuration.rst b/doc/source/user/resources/rds/v3/configuration.rst new file mode 100644 index 000000000..2952778eb --- /dev/null +++ b/doc/source/user/resources/rds/v3/configuration.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.rds.v3.configuration +====================================== + +.. automodule:: otcextensions.sdk.rds.v3.configuration + +The Configuration Class +----------------------- + +The ``Configuration`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.rds.v3.configuration.ConfigurationGroup + :members: diff --git a/doc/source/user/resources/rds/v3/flavor.rst b/doc/source/user/resources/rds/v3/flavor.rst new file mode 100644 index 000000000..a8806f213 --- /dev/null +++ b/doc/source/user/resources/rds/v3/flavor.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.rds.v3.flavor +=============================== + +.. automodule:: otcextensions.sdk.rds.v3.flavor + +The Flavor Class +---------------- + +The ``Flavor`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.rds.v3.flavor.Flavor + :members: diff --git a/doc/source/user/resources/rds/v3/instance.rst b/doc/source/user/resources/rds/v3/instance.rst new file mode 100644 index 000000000..d72e7cd4f --- /dev/null +++ b/doc/source/user/resources/rds/v3/instance.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.rds.v3.instance +================================= + +.. automodule:: otcextensions.sdk.rds.v3.instance + +The Instance Class +------------------ + +The ``Instance`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.rds.v3.instance.Instance + :members: diff --git a/otcextensions/osclient/rds/client.py b/otcextensions/osclient/rds/client.py index db8479d9b..eecdfebff 100644 --- a/otcextensions/osclient/rds/client.py +++ b/otcextensions/osclient/rds/client.py @@ -12,17 +12,20 @@ # import logging -from otcextensions import sdk +from osc_lib import utils +from otcextensions import sdk +from otcextensions.i18n import _ LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '1.0' +DEFAULT_API_VERSION = '3' API_VERSION_OPTION = 'os_rds_api_version' API_NAME = "rds" API_VERSIONS = { "1.0": "openstack.connection.Connection", "1": "openstack.connection.Connection", + "3": "openstack.connection.Connection" } @@ -42,4 +45,11 @@ def make_client(instance): def build_option_parser(parser): """Hook to add global options""" + parser.add_argument( + '--os-rds-api-version', + metavar='', + default=utils.env('OS_RDS_API_VERSION'), + help=_("RDS API version, default=%s " + "(Env: OS_RDS_API_VERSION)") % DEFAULT_API_VERSION + ) return parser diff --git a/otcextensions/osclient/rds/v3/__init__.py b/otcextensions/osclient/rds/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/osclient/rds/v3/backup.py b/otcextensions/osclient/rds/v3/backup.py new file mode 100644 index 000000000..77165ad78 --- /dev/null +++ b/otcextensions/osclient/rds/v3/backup.py @@ -0,0 +1,211 @@ +# 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. +# +"""Backup v3 action implementations""" + +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + + +BACKUP_TYPE_CHOICES = ['auto', 'manual', 'fragment', 'incremental'] + + +_formatters = { +} + + +def _get_columns(item): + column_map = {} + hidden = ['location'] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden) + + +class ListBackup(command.Lister): + _description = _("List database backups") + column_headers = ( + 'ID', 'Name', 'Type', 'Instance Id', 'Size (KB)' + ) + + columns = ( + 'id', 'name', 'type', 'instance_id', 'size' + ) + + def get_parser(self, prog_name): + parser = super(ListBackup, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + help=_('Specify instance ID or Name to get backup list'), + ) + parser.add_argument( + '--backup-id', + metavar='', + help=_('Specify the backup ID.'), + ) + parser.add_argument( + '--backup-type', + metavar='{' + ','.join(BACKUP_TYPE_CHOICES) + '}', + choices=BACKUP_TYPE_CHOICES, + type=lambda s: s.lower(), + help=_('Specify the backup type.'), + ) + parser.add_argument( + '--offset', + metavar='', + type=int, + help=_('Specify the index position.'), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Specify the limit of resources to be queried.'), + ) +# parser.add_argument( +# '--begin-time', +# metavar='', +# help=_('Specify the start time for obtaining the backup list.'), +# ) +# parser.add_argument( +# '--end-time', +# metavar='', +# help=_('Specify the end time for obtaining the backup list.'), +# ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + attrs = {} + args_list = [ + 'backup_id', 'backup_type', 'offset', 'limit' + # , 'begin_time', 'end_time' + ] + for arg in args_list: + if getattr(parsed_args, arg): + attrs[arg] = getattr(parsed_args, arg) + if parsed_args.limit: + attrs['paginated'] = False + + instance = client.find_instance(parsed_args.instance, + ignore_missing=False) + + data = client.backups(instance=instance, **attrs) + + return (self.column_headers, (utils.get_item_properties( + s, + self.columns, + ) for s in data)) + + +class CreateBackup(command.ShowOne): + _description = _('Create Database backup') + + def get_parser(self, prog_name): + parser = super(CreateBackup, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name for the backup')) + parser.add_argument( + 'instance', + metavar='', + help=_('ID or Name of the instance to create backup from')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the backup')) + parser.add_argument( + '--databases', + metavar='', + help=_( + 'Specifies a CSV list of self-built SQL Server' + 'databases that are partially backed up' + '(Only SQL Server support partial backups.)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + attrs = {'name': parsed_args.name} + if parsed_args.description: + attrs['description'] = parsed_args.description + + instance = client.find_instance(parsed_args.instance, + ignore_missing=False) + + if (parsed_args.databases + and instance.datastore['type'].lower() == 'sqlserver'): + attrs['databases'] = [] + databases = parsed_args.databases + databases = databases.split(",") + for db_name in databases: + attrs['databases'].append({'name': db_name}) + else: + # Do we want to raise error otherwise? + pass + + obj = client.create_backup(instance=instance, + **attrs) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, + formatters=_formatters) + + return (display_columns, data) + + +class DeleteBackup(command.Command): + _description = _('Delete Backup') + + def get_parser(self, prog_name): + parser = super(DeleteBackup, self).get_parser(prog_name) + parser.add_argument('backup', + metavar='', + nargs='+', + help=_('ID of the backup')) + return parser + + def take_action(self, parsed_args): + + if parsed_args.backup: + client = self.app.client_manager.rds + for bck in parsed_args.backup: + client.delete_backup(backup=bck, ignore_missing=False) + + +class ListBackupDownloadLinks(command.Lister): + _description = _('List Backup Download Links') + + column_headers = ('Size (KB)', 'URL', 'Expires at') + columns = ('size', 'download_link', 'expires_at') + + def get_parser(self, prog_name): + parser = super(ListBackupDownloadLinks, self).get_parser(prog_name) + parser.add_argument('backup_id', + metavar='', + help=_('ID of the backup')) + return parser + + def take_action(self, parsed_args): + + client = self.app.client_manager.rds + data = client.backup_download_links(backup_id=parsed_args.backup_id) + return (self.column_headers, (utils.get_item_properties( + s, + self.columns, + formatters={} + ) for s in data)) diff --git a/otcextensions/osclient/rds/v3/configuration.py b/otcextensions/osclient/rds/v3/configuration.py new file mode 100644 index 000000000..149dcaa9e --- /dev/null +++ b/otcextensions/osclient/rds/v3/configuration.py @@ -0,0 +1,300 @@ +# 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. +# +"""Configuration v3 action implementations""" + +from osc_lib import utils +from osc_lib.cli import parseractions +from osc_lib.command import command + +from otcextensions.i18n import _ +from otcextensions.common import sdk_utils + + +DATASTORE_TYPE_CHOICES = ['mysql', 'postgresql', 'sqlserver'] + + +_formatters = { +} + + +def _get_columns(item, skip_values=True): + column_map = {} + hidden = ['location', 'links'] + if skip_values: + if 'configuration_parameters' in item: + hidden.append('configuration_parameters') + if 'values' in item: + hidden.append('values') + + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden) + + +class ListConfigurations(command.Lister): + _description = _("List Configurations") + column_headers = [ + 'ID', 'Name', 'Description', 'Datastore Name', + 'Datastore Version Name', 'User Defined' + ] + + columns = [ + 'id', 'name', 'description', 'datastore_name', + 'datastore_version_name', 'is_user_defined' + ] + + def get_parser(self, prog_name): + parser = super(ListConfigurations, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + data = client.configurations() + + return (self.column_headers, (utils.get_item_properties( + s, + self.columns, + ) for s in data)) + + +class ShowConfiguration(command.ShowOne): + _description = _("Show details of a database configuration") + + def get_parser(self, prog_name): + parser = super(ShowConfiguration, self).get_parser(prog_name) + parser.add_argument('configuration', + metavar="", + help=_("ID or name of the configuration")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + obj = client.find_configuration(parsed_args.configuration, + ignore_missing=False) + + display_columns, columns = _get_columns(obj, skip_values=True) + data = utils.get_item_properties(obj, columns, + formatters=_formatters) + + return (display_columns, data) + + +class ListConfigurationParameters(command.Lister): + _description = _("List Configuration parameters") + column_headers = ( + 'Name', 'Value', 'Type', 'Description', + 'Restart Required', 'Readonly', 'Value Range' + ) + columns = ( + 'name', 'value', 'type', 'description', + 'restart_required', 'readonly', 'value_range' + ) + + def get_parser(self, prog_name): + parser = super(ListConfigurationParameters, self).get_parser(prog_name) + parser.add_argument('configuration', + metavar="", + help=_("ID or name of the configuration")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + data = client.find_configuration( + parsed_args.configuration, + ignore_missing=False).configuration_parameters + + return (self.column_headers, (utils.get_dict_properties( + s, + self.columns, + ) for s in data)) + + +class CreateConfiguration(command.ShowOne): + _description = _("Create new Configuration") + + def get_parser(self, prog_name): + parser = super(CreateConfiguration, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar="", + help=_("Parameter group name")) + parser.add_argument( + '--description', + metavar="", + help=_("Parameter group description")) + parser.add_argument( + '--datastore-type', + metavar='{' + ','.join(DATASTORE_TYPE_CHOICES) + '}', + choices=DATASTORE_TYPE_CHOICES, + type=lambda s: s.lower(), + required=True, + help=_("Datastore type")) + parser.add_argument( + '--datastore-version', + metavar="", + required=True, + help=_("Datastore version")) + parser.add_argument( + '--value', + dest='values', + metavar="", + action=parseractions.KeyValueAction, + help=_("Configuration value" + "(repeat option to set multiple values)") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + config_attrs = {} + config_attrs['datastore'] = {} + if parsed_args.name: + config_attrs['name'] = parsed_args.name + if parsed_args.description: + config_attrs['description'] = parsed_args.description + if parsed_args.datastore_type and parsed_args.datastore_version: + config_attrs['datastore'] = { + 'type': parsed_args.datastore_type, + 'version': parsed_args.datastore_version + } + + if parsed_args.values: + config_attrs['values'] = parsed_args.values + obj = client.create_configuration(**config_attrs) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + + return (display_columns, data) + + +class ApplyConfiguration(command.Lister): + _description = _("Apply Configuration to the instance(s)") + + columns = ( + 'instance_id', 'instance_name', + 'restart_required', 'success' + ) + + column_headers = ( + 'ID', 'Name', 'Restart required', 'success' + ) + + def get_parser(self, prog_name): + parser = super(ApplyConfiguration, self).get_parser(prog_name) + parser.add_argument( + 'configuration', + metavar="", + help=_("Configuration name or id") + ) + parser.add_argument( + '--instance', + metavar="", + dest='instances', + action='append', + required=True, + help=_('ID of the instance the configuration ' + 'should be applied to. ' + '(repeat option to apply to multiple instances).') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + config = client.find_configuration(parsed_args.configuration, + ignore_missing=False) + + inst_ids = [] + + # Ensure instance_ids are right + for inst in parsed_args.instances: + inst_ids.append(client.get_instance(inst).id) + + obj = client.apply_configuration(config.id, instances=inst_ids) + + return (self.column_headers, (utils.get_dict_properties( + s, + self.columns, + ) for s in obj.apply_results)) + + +class SetConfiguration(command.Command): + _description = _("Set values of the Configuration") + + columns = ('id', 'name', 'description', 'datastore_version_id', + 'datastore_version_name', 'datastore_name', 'created', + 'updated', 'allowed_updated', 'instance_count', 'values') + + def get_parser(self, prog_name): + parser = super(SetConfiguration, self).get_parser(prog_name) + parser.add_argument('configuration', + metavar="", + help=_("Configuration id")) + parser.add_argument('--name', + metavar="", + help=_("New Configuration name")) + parser.add_argument('--description', + metavar="", + help=_("New Configuration description")) + parser.add_argument( + '--value', + dest="values", + metavar="", + required=True, + action=parseractions.KeyValueAction, + help=_("Configuration value" + "(repeat option to set multiple values).")) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + config_attrs = {} + + if parsed_args.name: + config_attrs['name'] = parsed_args.name + if parsed_args.description: + config_attrs['description'] = parsed_args.description + if parsed_args.values: + config_attrs['values'] = parsed_args.values + + config = client.find_configuration(parsed_args.configuration, + ignore_missing=False) + + client.update_configuration(config, **config_attrs) + + +class DeleteConfiguration(command.Command): + _description = _("Delete a configuration") + + def get_parser(self, prog_name): + parser = super(DeleteConfiguration, self).get_parser(prog_name) + parser.add_argument('configuration', + metavar='', + nargs='+', + help=_('ID or name of the configuration group')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + if parsed_args.configuration: + for cnf in parsed_args.configuration: + resource = client.find_configuration(cnf, ignore_missing=False) + client.delete_configuration(resource) diff --git a/otcextensions/osclient/rds/v3/datastore.py b/otcextensions/osclient/rds/v3/datastore.py new file mode 100644 index 000000000..42645af2f --- /dev/null +++ b/otcextensions/osclient/rds/v3/datastore.py @@ -0,0 +1,69 @@ +# 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. +# +"""Datastore v3 action implementations""" +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.i18n import _ +from otcextensions.common import sdk_utils + + +DB_TYPE_CHOICES = ['mysql', 'postgresql', 'sqlserver'] + + +def _get_columns(item): + column_map = {} + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +class ListDatastoreTypes(command.Lister): + + _description = _("List available datastores.") + columns = ('Name', ) + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + data = client.datastore_types() + + return (self.columns, (utils.get_item_properties( + s, + self.columns, + ) for s in data)) + + +class ListDatastoreVersions(command.Lister): + _description = _("Lists available versions for a datastore.") + columns = ('ID', 'Name') + + def get_parser(self, prog_name): + parser = super(ListDatastoreVersions, self).get_parser(prog_name) + + parser.add_argument( + 'database', + metavar='{' + ','.join(DB_TYPE_CHOICES) + '}', + type=lambda s: s.lower(), + choices=DB_TYPE_CHOICES, + help=_('Name of the datastore Engine.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + data = client.datastores(database_name=parsed_args.database) + + return (self.columns, (utils.get_item_properties( + s, + self.columns, + ) for s in data)) diff --git a/otcextensions/osclient/rds/v3/flavor.py b/otcextensions/osclient/rds/v3/flavor.py new file mode 100644 index 000000000..a9aeaf6c9 --- /dev/null +++ b/otcextensions/osclient/rds/v3/flavor.py @@ -0,0 +1,63 @@ +# 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. +# +"""Flavor v3 action implementations""" +import logging + +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.i18n import _ + +LOG = logging.getLogger(__name__) + +DB_TYPE_CHOICES = ['mysql', 'postgresql', 'sqlserver'] + + +class ListDatabaseFlavors(command.Lister): + + _description = _("List database flavors.") + columns = ('name', 'instance_mode', 'vcpus', 'ram') + + def get_parser(self, prog_name): + parser = super(ListDatabaseFlavors, self).get_parser(prog_name) + + parser.add_argument( + 'database', + metavar='{' + ','.join(DB_TYPE_CHOICES) + '}', + type=lambda s: s.lower(), + choices=DB_TYPE_CHOICES, + help=_('Name of the datastore Engine.'), + ) + + parser.add_argument( + 'version', + metavar='', + help=_('Specifies the database version.'), + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + + data = client.flavors( + datastore_name=parsed_args.database, + version_name=parsed_args.version) + + return ( + self.columns, + (utils.get_item_properties( + s, + self.columns, + ) for s in data) + ) diff --git a/otcextensions/osclient/rds/v3/instance.py b/otcextensions/osclient/rds/v3/instance.py new file mode 100644 index 000000000..71bfe6a85 --- /dev/null +++ b/otcextensions/osclient/rds/v3/instance.py @@ -0,0 +1,663 @@ +# 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. +# +'''Instance v1 action implementations''' +import argparse + +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + + +def _get_columns(item): + column_map = {} + hidden = ['job_id'] + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map, + hidden) + + +def set_attributes_for_print(instances): + for instance in instances: + if getattr(instance, 'volume', None): + setattr(instance, 'size', instance.volume['size']) + else: + setattr(instance, 'size', '-') + datastore = instance.datastore + if datastore.get('version'): + setattr(instance, 'datastore_version', datastore['version']) + if datastore.get('type'): + setattr(instance, 'datastore_type', datastore['type']) + yield instance + + +HA_MODE_CHOICES = ['sync', 'semisync', 'async'] +DISK_TYPE_CHOICES = ['common', 'ultrahigh'] +HA_TYPE_CHOICES = ['ha', 'replica', 'single'] +DATASTORE_TYPE_CHOICES = ['mysql', 'postgresql', 'sqlserver'] + + +class ListDatabaseInstances(command.Lister): + _description = _('List database instances') + columns = ( + 'ID', 'Name', 'Datastore Type', 'Datastore Version', 'Status', + 'Flavor_ref', 'Type', 'Size', 'Region' + ) + + def get_parser(self, prog_name): + parser = super(ListDatabaseInstances, self).get_parser(prog_name) + parser.add_argument( + '--limit', + dest='limit', + metavar='', + type=int, + help=_('Limit the number of results displayed')) + parser.add_argument( + '--id', + dest='id', + metavar='', + type=str, + help=_('Specifies the DB instance ID.')) + parser.add_argument( + '--name', + dest='name', + metavar='', + type=str, + help=_('Specifies the DB instance Name.')) + parser.add_argument( + '--type', + dest='type', + metavar='{' + ','.join(HA_TYPE_CHOICES) + '}', + type=lambda s: s.lower(), + choices=HA_TYPE_CHOICES, + help=_( + 'Specifies the instance type. Values cane be single/ha/replica' + )) + parser.add_argument( + '--datastore-type', + metavar='{' + ','.join(DATASTORE_TYPE_CHOICES) + '}', + type=lambda s: s.lower(), + choices=DATASTORE_TYPE_CHOICES, + help=_( + 'Specifies the database type. ' + 'value is MySQL, PostgreSQL, or SQLServer.')) + parser.add_argument( + '--router-id', + dest='router_id', + metavar='', + type=str, + help=_('Specifies the ID of Router to which ' + 'instance should be connected to.')) + parser.add_argument( + '--subnet-id', + dest='subnet_id', + metavar='', + type=str, + help=_('Indicates the Subnet ID of DB Instance.')) + parser.add_argument( + '--offset', + dest='offset', + metavar='', + type=int, + default=None, + help=_('Specifies the index position.')) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + args_list = [ + 'name', 'id', 'router_id', 'subnet_id', 'offset', 'limit' + ] + attrs = {} + for arg in args_list: + if getattr(parsed_args, arg): + attrs[arg] = getattr(parsed_args, arg) + if parsed_args.type: + attrs['type'] = parsed_args.type.title() + if parsed_args.datastore_type == 'mysql': + attrs['datastore_type'] = 'MySQL' + elif parsed_args.datastore_type == 'postgresql': + attrs['datastore_type'] = 'PostgreSQL' + elif parsed_args.datastore_type == 'sqlserver': + attrs['datastore_type'] = 'SQLServer' + if parsed_args.limit: + attrs['paginated'] = False + + data = client.instances(**attrs) + if data: + data = set_attributes_for_print(data) + + return (self.columns, (utils.get_item_properties( + s, + self.columns, + ) for s in data)) + + +class ShowDatabaseInstance(command.ShowOne): + _description = _("Show instance details") + + def get_parser(self, prog_name): + parser = super(ShowDatabaseInstance, self).get_parser(prog_name) + parser.add_argument( + 'instance', + metavar='', + help=_('Instance (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.rds + obj = client.find_instance(parsed_args.instance, ignore_missing=False) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + + return (display_columns, data) + + +class CreateDatabaseInstance(command.ShowOne): + + _description = _("Create a new database instance.") + + def get_parser(self, prog_name): + parser = super(CreateDatabaseInstance, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_("Name of the instance.") + ) + parser.add_argument( + 'flavor_ref', + metavar='', + help=_("Flavor spec_code") + ) + disk_group = parser.add_argument_group('Disk data') + disk_group.add_argument( + '--size', + metavar='', + type=int, + required=True, + help=_("Size of the instance disk volume in GB. ") + ) + disk_group.add_argument( + '--volume-type', + metavar='{' + ','.join(DISK_TYPE_CHOICES) + '}', + type=lambda s: s.upper(), + required=True, + choices=[s.upper() for s in DISK_TYPE_CHOICES], + help=_("Volume type. (COMMON, ULTRAHIGH).") + ) + parser.add_argument( + '--availability-zone', + metavar='', + required=True, + help=_("The Zone hint to give to Nova.") + ) + parser.add_argument( + '--region', + metavar='', + required=True, + help=argparse.SUPPRESS, + ) + ds_group = parser.add_argument_group('Datasoure parameters') + ds_group.add_argument( + '--datastore-type', + metavar='', + required=True, + help=_("Name of the datastore (type).") + ) + ds_group.add_argument( + '--datastore-version', + required=True, + metavar='', + help=_("Datastore version.") + ) + parser.add_argument( + '--configuration', + dest='configuration_id', + metavar='', + default=None, + help=_("ID of the configuration group to attach to the instance.") + ) + parser.add_argument( + '--disk-encryption-id', + metavar='', + default=None, + help=_("key ID for disk encryption.") + ) + new_instance_group = parser.add_argument_group( + 'New instance parameters', + 'Parameters to be used for the new instance creation (not when ' + 'created as replica or from backup') + new_instance_group.add_argument( + '--port', + metavar='', + type=int, + help=_("Database Port") + ) + new_instance_group.add_argument( + '--password', + metavar='', + help=_("ID of the configuration group to attach to the instance.") + ) + new_instance_group.add_argument( + '--router-id', + metavar='', + help=_('ID of a Router the DB should be connected to') + ) + new_instance_group.add_argument( + '--subnet-id', + metavar='', + help=_('ID of a subnet the DB should be connected to.') + ) + new_instance_group.add_argument( + '--security-group-id', + dest='security_group_id', + metavar='', + help=_('Security group ID') + ) + parser.add_argument( + '--ha-mode', + metavar='{' + ','.join(HA_MODE_CHOICES) + '}', + type=lambda s: s.lower(), + choices=HA_MODE_CHOICES, + help=_('replication mode for the standby DB instance. ' + 'This parameter is required when using `ha`flavors') + ) + parser.add_argument( + '--charge-mode', + metavar='', + default='postPaid', + help=_('Specifies the billing mode') + ) + create_from_group = parser.add_argument_group( + 'Create FROM group', + 'Parameters to be used when creating new instance as a ' + 'replica or from backup') + create_from_group.add_argument( + '--replica-of', + metavar='', + default=None, + help=_("ID or name of an existing instance to replicate from.") + ) + create_from_group.add_argument( + '--from-instance', + metavar='', + help=_('Source instance ID or Name to create from. ' + 'It is required when recovering from backup or PITR.') + ) + create_from_group.add_argument( + '--backup', + metavar='', + help=_('Backup ID or Name to create new instance from.') + ) + create_from_group.add_argument( + '--restore-time', + metavar='