Beispiel #1
0
    def test_read_write_configuration(self, read_file, write_file, chown,
                                      chmod):
        sample_path = Mock()
        sample_owner = Mock()
        sample_group = Mock()
        sample_codec = MagicMock()
        sample_requires_root = Mock()
        sample_strategy = MagicMock()
        sample_strategy.configure = Mock()
        sample_strategy.parse_updates = Mock(return_value={})

        manager = ConfigurationManager(sample_path,
                                       sample_owner,
                                       sample_group,
                                       sample_codec,
                                       requires_root=sample_requires_root,
                                       override_strategy=sample_strategy)

        manager.parse_configuration()
        read_file.assert_called_with(sample_path,
                                     codec=sample_codec,
                                     as_root=sample_requires_root)

        with patch.object(manager,
                          'parse_configuration',
                          return_value={
                              'key1': 'v1',
                              'key2': 'v2'
                          }):
            self.assertEqual('v1', manager.get_value('key1'))
            self.assertIsNone(manager.get_value('key3'))

        sample_contents = Mock()
        manager.save_configuration(sample_contents)
        write_file.assert_called_with(sample_path,
                                      sample_contents,
                                      as_root=sample_requires_root)

        chown.assert_called_with(sample_path,
                                 sample_owner,
                                 sample_group,
                                 as_root=sample_requires_root)
        chmod.assert_called_with(sample_path,
                                 FileMode.ADD_READ_ALL,
                                 as_root=sample_requires_root)

        sample_data = {}
        manager.apply_system_override(sample_data)
        manager.apply_user_override(sample_data)
        manager.apply_system_override(sample_data, change_id='sys1')
        manager.apply_user_override(sample_data, change_id='usr1')
        sample_strategy.apply.has_calls([
            call(manager.SYSTEM_GROUP, manager.DEFAULT_CHANGE_ID, sample_data),
            call(manager.USER_GROUP, manager.DEFAULT_CHANGE_ID, sample_data),
            call(manager.SYSTEM_GROUP, 'sys1', sample_data),
            call(manager.USER_GROUP, 'usr1', sample_data)
        ])
    def test_read_write_configuration(self, read_file, write_file,
                                      chown, chmod):
        sample_path = Mock()
        sample_owner = Mock()
        sample_group = Mock()
        sample_codec = MagicMock()
        sample_requires_root = Mock()
        sample_strategy = MagicMock()
        sample_strategy.configure = Mock()
        sample_strategy.parse_updates = Mock(return_value={})

        manager = ConfigurationManager(
            sample_path, sample_owner, sample_group, sample_codec,
            requires_root=sample_requires_root,
            override_strategy=sample_strategy)

        manager.parse_configuration()
        read_file.assert_called_with(sample_path, codec=sample_codec,
                                     as_root=sample_requires_root)

        with patch.object(manager, 'parse_configuration',
                          return_value={'key1': 'v1', 'key2': 'v2'}):
            self.assertEqual('v1', manager.get_value('key1'))
            self.assertIsNone(manager.get_value('key3'))

        sample_contents = Mock()
        manager.save_configuration(sample_contents)
        write_file.assert_called_with(
            sample_path, sample_contents, as_root=sample_requires_root)

        chown.assert_called_with(sample_path, sample_owner, sample_group,
                                 as_root=sample_requires_root)
        chmod.assert_called_with(
            sample_path, FileMode.ADD_READ_ALL, as_root=sample_requires_root)

        sample_data = {}
        manager.apply_system_override(sample_data)
        manager.apply_user_override(sample_data)
        manager.apply_system_override(sample_data, change_id='sys1')
        manager.apply_user_override(sample_data, change_id='usr1')
        manager.apply_system_override(sample_data, change_id='sys2',
                                      pre_user=True)
        sample_strategy.apply.has_calls([
            call(manager.SYSTEM_POST_USER_GROUP,
                 manager.DEFAULT_CHANGE_ID, sample_data),
            call(manager.USER_GROUP, manager.DEFAULT_CHANGE_ID, sample_data),
            call(manager.SYSTEM_POST_USER_GROUP,
                 'sys1', sample_data),
            call(manager.USER_GROUP, 'usr1', sample_data),
            call(manager.SYSTEM_PRE_USER_GROUP,
                 'sys2', sample_data),
        ])
Beispiel #3
0
    def test_read_write_configuration(self, read_file, write_file, chown, chmod):
        sample_path = Mock()
        sample_owner = Mock()
        sample_group = Mock()
        sample_codec = MagicMock()
        sample_requires_root = Mock()
        sample_strategy = MagicMock()
        sample_strategy.configure = Mock()
        sample_strategy.parse_updates = Mock(return_value={})

        manager = ConfigurationManager(
            sample_path,
            sample_owner,
            sample_group,
            sample_codec,
            requires_root=sample_requires_root,
            override_strategy=sample_strategy,
        )

        manager.parse_configuration()
        read_file.assert_called_with(sample_path, codec=sample_codec)

        with patch.object(manager, "parse_configuration", return_value={"key1": "v1", "key2": "v2"}):
            self.assertEqual("v1", manager.get_value("key1"))
            self.assertIsNone(manager.get_value("key3"))

        sample_contents = Mock()
        manager.save_configuration(sample_contents)
        write_file.assert_called_with(sample_path, sample_contents, as_root=sample_requires_root)

        chown.assert_called_with(sample_path, sample_owner, sample_group, as_root=sample_requires_root)
        chmod.assert_called_with(sample_path, FileMode.ADD_READ_ALL, as_root=sample_requires_root)

        sample_data = {}
        manager.apply_system_override(sample_data)
        manager.apply_user_override(sample_data)
        manager.apply_system_override(sample_data, change_id="sys1")
        manager.apply_user_override(sample_data, change_id="usr1")
        sample_strategy.apply.has_calls(
            [
                call(manager.SYSTEM_GROUP, manager.DEFAULT_CHANGE_ID, sample_data),
                call(manager.USER_GROUP, manager.DEFAULT_CHANGE_ID, sample_data),
                call(manager.SYSTEM_GROUP, "sys1", sample_data),
                call(manager.USER_GROUP, "usr1", sample_data),
            ]
        )
Beispiel #4
0
    def test_read_write_configuration(self, read_file, write_file, chown,
                                      chmod):
        sample_path = Mock()
        sample_owner = Mock()
        sample_group = Mock()
        sample_codec = MagicMock()
        sample_requires_root = Mock()

        manager = ConfigurationManager(sample_path,
                                       sample_owner,
                                       sample_group,
                                       sample_codec,
                                       requires_root=sample_requires_root)

        manager.parse_configuration()
        read_file.assert_called_with(sample_path, codec=sample_codec)

        with patch.object(manager,
                          'parse_configuration',
                          return_value={
                              'key1': 'v1',
                              'key2': 'v2'
                          }):
            self.assertEqual('v1', manager.get_value('key1'))
            self.assertEqual(None, manager.get_value('key3'))

        sample_contents = Mock()
        manager.save_configuration(sample_contents)
        write_file.assert_called_with(sample_path,
                                      sample_contents,
                                      as_root=sample_requires_root)

        chown.assert_called_with(sample_path,
                                 sample_owner,
                                 sample_group,
                                 as_root=sample_requires_root)
        chmod.assert_called_with(sample_path,
                                 FileMode.ADD_READ_ALL,
                                 as_root=sample_requires_root)

        sample_options = Mock()
        with patch.object(manager, 'save_configuration') as save_config:
            manager.render_configuration(sample_options)
            save_config.assert_called_once_with(
                sample_codec.serialize.return_value)
            sample_codec.serialize.assert_called_once_with(sample_options)

        with patch('trove.guestagent.common.configuration.'
                   'ConfigurationOverrideStrategy') as mock_strategy:
            manager.set_override_strategy(mock_strategy)
            manager._current_revision = 3
            manager.save_configuration(sample_contents)
            mock_strategy.remove_last.assert_called_once_with(
                manager._current_revision + 1)
            write_file.assert_called_with(sample_path,
                                          sample_contents,
                                          as_root=sample_requires_root)
Beispiel #5
0
    def test_read_write_configuration(self, read_file, write_file,
                                      chown, chmod):
        sample_path = Mock()
        sample_owner = Mock()
        sample_group = Mock()
        sample_codec = MagicMock()
        sample_requires_root = Mock()

        manager = ConfigurationManager(
            sample_path, sample_owner, sample_group, sample_codec,
            requires_root=sample_requires_root)

        manager.parse_configuration()
        read_file.assert_called_with(sample_path, codec=sample_codec)

        with patch.object(manager, 'parse_configuration',
                          return_value={'key1': 'v1', 'key2': 'v2'}):
            self.assertEqual('v1', manager.get_value('key1'))
            self.assertEqual(None, manager.get_value('key3'))

        sample_contents = Mock()
        manager.save_configuration(sample_contents)
        write_file.assert_called_with(
            sample_path, sample_contents, as_root=sample_requires_root)

        chown.assert_called_with(sample_path, sample_owner, sample_group,
                                 as_root=sample_requires_root)
        chmod.assert_called_with(
            sample_path, FileMode.ADD_READ_ALL, as_root=sample_requires_root)

        sample_options = Mock()
        with patch.object(manager, 'save_configuration') as save_config:
            manager.render_configuration(sample_options)
            save_config.assert_called_once_with(
                sample_codec.serialize.return_value)
            sample_codec.serialize.assert_called_once_with(sample_options)

        with patch('trove.guestagent.common.configuration.'
                   'ConfigurationOverrideStrategy') as mock_strategy:
            manager.set_override_strategy(mock_strategy)
            manager._current_revision = 3
            manager.save_configuration(sample_contents)
            mock_strategy.remove_last.assert_called_once_with(
                manager._current_revision + 1)
            write_file.assert_called_with(
                sample_path, sample_contents, as_root=sample_requires_root)
Beispiel #6
0
class RedisApp(object):
    """
    Handles installation and configuration of redis
    on a trove instance.
    """

    def __init__(self, state_change_wait_time=None):
        """
        Sets default status and state_change_wait_time
        """
        if state_change_wait_time:
            self.state_change_wait_time = state_change_wait_time
        else:
            self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(system.REDIS_CONFIG),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)
        config_value_mappings = {'yes': True, 'no': False, "''": None}
        self._value_converter = StringConverter(config_value_mappings)
        self.configuration_manager = ConfigurationManager(
            system.REDIS_CONFIG,
            system.REDIS_OWNER, system.REDIS_OWNER,
            PropertiesCodec(
                unpack_singletons=False,
                string_mappings=config_value_mappings
            ), requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

        self.admin = self._build_admin_client()
        self.status = RedisAppStatus(self.admin)

    def _build_admin_client(self):
        password = self.get_configuration_property('requirepass')
        socket = self.get_configuration_property('unixsocket')

        return RedisAdmin(password=password, unix_socket_path=socket)

    def install_if_needed(self, packages):
        """
        Install redis if needed do nothing if it is already installed.
        """
        LOG.info(_('Preparing Guest as Redis Server.'))
        if not packager.pkg_is_installed(packages):
            LOG.info(_('Installing Redis.'))
            self._install_redis(packages)
        LOG.info(_('Redis installed completely.'))

    def _install_redis(self, packages):
        """
        Install the redis server.
        """
        LOG.debug('Installing redis server.')
        msg = "Creating %s." % system.REDIS_CONF_DIR
        LOG.debug(msg)
        operating_system.create_directory(system.REDIS_CONF_DIR, as_root=True)
        pkg_opts = {}
        packager.pkg_install(packages, pkg_opts, TIME_OUT)
        self.start_db()
        LOG.debug('Finished installing redis server.')

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        self.status.stop_db_service(
            system.SERVICE_CANDIDATES, self.state_change_wait_time,
            disable_on_boot=do_not_start_on_reboot, update_db=update_db)

    def restart(self):
        self.status.restart_db_service(
            system.SERVICE_CANDIDATES, self.state_change_wait_time)

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def apply_overrides(self, client, overrides):
        """Use the 'CONFIG SET' command to apply configuration at runtime.

        Commands that appear multiple times have values separated by a
        white space. For instance, the following two 'save' directives from the
        configuration file...

            save 900 1
            save 300 10

        ... would be applied in a single command as:

            CONFIG SET save "900 1 300 10"

        Note that the 'CONFIG' command has been renamed to prevent
        users from using it to bypass configuration groups.
        """
        for prop_name, prop_args in overrides.items():
            args_string = self._join_lists(
                self._value_converter.to_strings(prop_args), ' ')
            client.config_set(prop_name, args_string)

    def _join_lists(self, items, sep):
        """Join list items (including items from sub-lists) into a string.
        Non-list inputs are returned unchanged.

        _join_lists('1234', ' ') = "1234"
        _join_lists(['1','2','3','4'], ' ') = "1 2 3 4"
        _join_lists([['1','2'], ['3','4']], ' ') = "1 2 3 4"
        """
        if isinstance(items, list):
            return sep.join([sep.join(e) if isinstance(e, list) else e
                             for e in items])
        return items

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def make_read_only(self, read_only):
        # Redis has no mechanism to make an instance read-only at present
        pass

    def start_db_with_conf_changes(self, config_contents):
        LOG.info(_('Starting redis with conf changes.'))
        if self.status.is_running:
            format = 'Cannot start_db_with_conf_changes because status is %s.'
            LOG.debug(format, self.status)
            raise RuntimeError(format % self.status)
        LOG.info(_("Initiating config."))
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration()
        self.start_db(True)

    def start_db(self, update_db=False):
        self.status.start_db_service(
            system.SERVICE_CANDIDATES, self.state_change_wait_time,
            enable_on_boot=True, update_db=update_db)

    def apply_initial_guestagent_configuration(self):
        """Update guestagent-controlled configuration properties.
        """

        # Hide the 'CONFIG' command from end users by mangling its name.
        self.admin.set_config_command_name(self._mangle_config_command_name())

        self.configuration_manager.apply_system_override(
            {'daemonize': 'yes',
             'pidfile': system.REDIS_PID_FILE,
             'logfile': system.REDIS_LOG_FILE,
             'dir': system.REDIS_DATA_DIR})

    def get_config_command_name(self):
        """Get current name of the 'CONFIG' command.
        """
        renamed_cmds = self.configuration_manager.get_value('rename-command')
        for name_pair in renamed_cmds:
            if name_pair[0] == 'CONFIG':
                return name_pair[1]

        return None

    def _mangle_config_command_name(self):
        """Hide the 'CONFIG' command from the clients by renaming it to a
        random string known only to the guestagent.
        Return the mangled name.
        """
        mangled = utils.generate_random_password()
        self._rename_command('CONFIG', mangled)
        return mangled

    def _rename_command(self, old_name, new_name):
        """It is possible to completely disable a command by renaming it
        to an empty string.
        """
        self.configuration_manager.apply_system_override(
            {'rename-command': [old_name, new_name]})

    def get_logfile(self):
        """Specify the log file name. Also the empty string can be used to
        force Redis to log on the standard output.
        Note that if you use standard output for logging but daemonize,
        logs will be sent to /dev/null
        """
        return self.get_configuration_property('logfile')

    def get_db_filename(self):
        """The filename where to dump the DB.
        """
        return self.get_configuration_property('dbfilename')

    def get_working_dir(self):
        """The DB will be written inside this directory,
        with the filename specified the 'dbfilename' configuration directive.
        The Append Only File will also be created inside this directory.
        """
        return self.get_configuration_property('dir')

    def get_persistence_filepath(self):
        """Returns the full path to the persistence file."""
        return guestagent_utils.build_file_path(
            self.get_working_dir(), self.get_db_filename())

    def get_port(self):
        """Port for this instance or default if not set."""
        return self.get_configuration_property('port', system.REDIS_PORT)

    def get_auth_password(self):
        """Client authentication password for this instance or None if not set.
        """
        return self.get_configuration_property('requirepass')

    def is_appendonly_enabled(self):
        """True if the Append Only File (AOF) persistence mode is enabled.
        """
        return self.get_configuration_property('appendonly', False)

    def get_append_file_name(self):
        """The name of the append only file (AOF).
        """
        return self.get_configuration_property('appendfilename')

    def is_cluster_enabled(self):
        """Only nodes that are started as cluster nodes can be part of a
        Redis Cluster.
        """
        return self.get_configuration_property('cluster-enabled', False)

    def enable_cluster(self):
        """In order to start a Redis instance as a cluster node enable the
        cluster support
        """
        self.configuration_manager.apply_system_override(
            {'cluster-enabled': 'yes'}, CLUSTER_CFG)

    def get_cluster_config_filename(self):
        """Cluster node configuration file.
        """
        return self.get_configuration_property('cluster-config-file')

    def set_cluster_config_filename(self, name):
        """Make sure that instances running in the same system do not have
        overlapping cluster configuration file names.
        """
        self.configuration_manager.apply_system_override(
            {'cluster-config-file': name}, CLUSTER_CFG)

    def get_cluster_node_timeout(self):
        """Cluster node timeout is the amount of milliseconds a node must be
        unreachable for it to be considered in failure state.
        """
        return self.get_configuration_property('cluster-node-timeout')

    def get_configuration_property(self, name, default=None):
        """Return the value of a Redis configuration property.
        Returns a single value for single-argument properties or
        a list otherwise.
        """
        return utils.unpack_singleton(
            self.configuration_manager.get_value(name, default))

    def cluster_meet(self, ip, port):
        try:
            utils.execute_with_timeout('redis-cli', 'cluster', 'meet',
                                       ip, port)
        except exception.ProcessExecutionError:
            LOG.exception(_('Error joining node to cluster at %s.'), ip)
            raise

    def cluster_addslots(self, first_slot, last_slot):
        try:
            slots = map(str, range(first_slot, last_slot + 1))
            group_size = 200
            while slots:
                cmd = ([system.REDIS_CLI, 'cluster', 'addslots']
                       + slots[0:group_size])
                out, err = utils.execute_with_timeout(*cmd, run_as_root=True,
                                                      root_helper='sudo')
                if 'OK' not in out:
                    raise RuntimeError(_('Error executing addslots: %s')
                                       % out)
                del slots[0:group_size]
        except exception.ProcessExecutionError:
            LOG.exception(_('Error adding slots %(first_slot)s-%(last_slot)s'
                            ' to cluster.'),
                          {'first_slot': first_slot, 'last_slot': last_slot})
            raise

    def _get_node_info(self):
        try:
            out, _ = utils.execute_with_timeout('redis-cli', '--csv',
                                                'cluster', 'nodes')
            return [line.split(' ') for line in out.splitlines()]
        except exception.ProcessExecutionError:
            LOG.exception(_('Error getting node info.'))
            raise

    def _get_node_details(self):
        for node_details in self._get_node_info():
            if 'myself' in node_details[2]:
                return node_details
        raise exception.TroveError(_("Unable to determine node details"))

    def get_node_ip(self):
        """Returns [ip, port] where both values are strings"""
        return self._get_node_details()[1].split(':')

    def get_node_id_for_removal(self):
        node_details = self._get_node_details()
        node_id = node_details[0]
        my_ip = node_details[1].split(':')[0]
        try:
            slots, _ = utils.execute_with_timeout('redis-cli', '--csv',
                                                  'cluster', 'slots')
            return node_id if my_ip not in slots else None
        except exception.ProcessExecutionError:
            LOG.exception(_('Error validating node to for removal.'))
            raise

    def remove_nodes(self, node_ids):
        try:
            for node_id in node_ids:
                utils.execute_with_timeout('redis-cli', 'cluster',
                                           'forget', node_id)
        except exception.ProcessExecutionError:
            LOG.exception(_('Error removing node from cluster.'))
            raise
Beispiel #7
0
class MongoDBApp(object):
    """Prepares DBaaS on a Guest container."""
    def __init__(self):
        self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(CONFIG_FILE),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)
        self.configuration_manager = ConfigurationManager(
            CONFIG_FILE,
            system.MONGO_USER,
            system.MONGO_USER,
            SafeYamlCodec(default_flow_style=False),
            requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

        self.is_query_router = False
        self.is_cluster_member = False
        self.status = MongoDBAppStatus()

    def install_if_needed(self, packages):
        """Prepare the guest machine with a MongoDB installation."""
        LOG.info(_("Preparing Guest as MongoDB."))
        if not system.PACKAGER.pkg_is_installed(packages):
            LOG.debug("Installing packages: %s." % str(packages))
            system.PACKAGER.pkg_install(packages, {}, system.TIME_OUT)
        LOG.info(_("Finished installing MongoDB server."))

    def _get_service_candidates(self):
        if self.is_query_router:
            return system.MONGOS_SERVICE_CANDIDATES
        return system.MONGOD_SERVICE_CANDIDATES

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        self.status.stop_db_service(self._get_service_candidates(),
                                    self.state_change_wait_time,
                                    disable_on_boot=do_not_start_on_reboot,
                                    update_db=update_db)

    def restart(self):
        self.status.restart_db_service(self._get_service_candidates(),
                                       self.state_change_wait_time)

    def start_db(self, update_db=False):
        self.status.start_db_service(self._get_service_candidates(),
                                     self.state_change_wait_time,
                                     enable_on_boot=True,
                                     update_db=update_db)

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def start_db_with_conf_changes(self, config_contents):
        LOG.info(_('Starting MongoDB with configuration changes.'))
        if self.status.is_running:
            format = 'Cannot start_db_with_conf_changes because status is %s.'
            LOG.debug(format, self.status)
            raise RuntimeError(format % self.status)
        LOG.info(_("Initiating config."))
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration(
            None, mount_point=system.MONGODB_MOUNT_POINT)
        self.start_db(True)

    def apply_initial_guestagent_configuration(self,
                                               cluster_config,
                                               mount_point=None):
        LOG.debug("Applying initial configuration.")

        # Mongodb init scripts assume the PID-file path is writable by the
        # database service.
        # See: https://jira.mongodb.org/browse/SERVER-20075
        self._initialize_writable_run_dir()

        self.configuration_manager.apply_system_override({
            'processManagement.fork':
            False,
            'processManagement.pidFilePath':
            system.MONGO_PID_FILE,
            'systemLog.destination':
            'file',
            'systemLog.path':
            system.MONGO_LOG_FILE,
            'systemLog.logAppend':
            True
        })

        if mount_point:
            self.configuration_manager.apply_system_override(
                {'storage.dbPath': mount_point})

        if cluster_config is not None:
            self._configure_as_cluster_instance(cluster_config)
        else:
            self._configure_network(MONGODB_PORT)

    def _initialize_writable_run_dir(self):
        """Create a writable directory for Mongodb's runtime data
        (e.g. PID-file).
        """
        mongodb_run_dir = os.path.dirname(system.MONGO_PID_FILE)
        LOG.debug("Initializing a runtime directory: %s" % mongodb_run_dir)
        operating_system.create_directory(mongodb_run_dir,
                                          user=system.MONGO_USER,
                                          group=system.MONGO_USER,
                                          force=True,
                                          as_root=True)

    def _configure_as_cluster_instance(self, cluster_config):
        """Configure this guest as a cluster instance and return its
        new status.
        """
        if cluster_config['instance_type'] == "query_router":
            self._configure_as_query_router()
        elif cluster_config["instance_type"] == "config_server":
            self._configure_as_config_server()
        elif cluster_config["instance_type"] == "member":
            self._configure_as_cluster_member(
                cluster_config['replica_set_name'])
        else:
            LOG.error(
                _("Bad cluster configuration; instance type "
                  "given as %s.") % cluster_config['instance_type'])
            return ds_instance.ServiceStatuses.FAILED

        if 'key' in cluster_config:
            self._configure_cluster_security(cluster_config['key'])

    def _configure_as_query_router(self):
        LOG.info(_("Configuring instance as a cluster query router."))
        self.is_query_router = True

        # FIXME(pmalik): We should really have a separate configuration
        # template for the 'mongos' process.
        # Remove all storage configurations from the template.
        # They apply only to 'mongod' processes.
        # Already applied overrides will be integrated into the base file and
        # their current groups removed.
        config = guestagent_utils.expand_dict(
            self.configuration_manager.parse_configuration())
        if 'storage' in config:
            LOG.debug("Removing 'storage' directives from the configuration "
                      "template.")
            del config['storage']
            self.configuration_manager.save_configuration(
                guestagent_utils.flatten_dict(config))

        # Apply 'mongos' configuration.
        self._configure_network(MONGODB_PORT)
        self.configuration_manager.apply_system_override(
            {'sharding.configDB': ''}, CNF_CLUSTER)

    def _configure_as_config_server(self):
        LOG.info(_("Configuring instance as a cluster config server."))
        self._configure_network(CONFIGSVR_PORT)
        self.configuration_manager.apply_system_override(
            {'sharding.clusterRole': 'configsvr'}, CNF_CLUSTER)

    def _configure_as_cluster_member(self, replica_set_name):
        LOG.info(_("Configuring instance as a cluster member."))
        self.is_cluster_member = True
        self._configure_network(MONGODB_PORT)
        # we don't want these thinking they are in a replica set yet
        # as that would prevent us from creating the admin user,
        # so start mongo before updating the config.
        # mongo will be started by the cluster taskmanager
        self.start_db()
        self.configuration_manager.apply_system_override(
            {'replication.replSetName': replica_set_name}, CNF_CLUSTER)

    def _configure_cluster_security(self, key_value):
        """Force cluster key-file-based authentication.

        This will enabled RBAC.
        """
        # Store the cluster member authentication key.
        self.store_key(key_value)

        self.configuration_manager.apply_system_override(
            {
                'security.clusterAuthMode': 'keyFile',
                'security.keyFile': self.get_key_file()
            }, CNF_CLUSTER)

    def _configure_network(self, port=None):
        """Make the service accessible at a given (or default if not) port.
        """
        instance_ip = netutils.get_my_ipv4()
        bind_interfaces_string = ','.join([instance_ip, '127.0.0.1'])
        options = {'net.bindIp': bind_interfaces_string}
        if port is not None:
            guestagent_utils.update_dict({'net.port': port}, options)

        self.configuration_manager.apply_system_override(options)
        self.status.set_host(instance_ip, port=port)

    def clear_storage(self):
        mount_point = "/var/lib/mongodb/*"
        LOG.debug("Clearing storage at %s." % mount_point)
        try:
            operating_system.remove(mount_point, force=True, as_root=True)
        except exception.ProcessExecutionError:
            LOG.exception(_("Error clearing storage."))

    def _has_config_db(self):
        value_string = self.configuration_manager.get_value('sharding',
                                                            {}).get('configDB')

        return value_string is not None

    # FIXME(pmalik): This method should really be called 'set_config_servers'.
    # The current name suggests it adds more config servers, but it
    # rather replaces the existing ones.
    def add_config_servers(self, config_server_hosts):
        """Set config servers on a query router (mongos) instance.
        """
        config_servers_string = ','.join(
            ['%s:%s' % (host, CONFIGSVR_PORT) for host in config_server_hosts])
        LOG.info(_("Setting config servers: %s") % config_servers_string)
        self.configuration_manager.apply_system_override(
            {'sharding.configDB': config_servers_string}, CNF_CLUSTER)
        self.start_db(True)

    def add_shard(self, replica_set_name, replica_set_member):
        """
        This method is used by query router (mongos) instances.
        """
        url = "%(rs)s/%(host)s:%(port)s"\
              % {'rs': replica_set_name,
                 'host': replica_set_member,
                 'port': MONGODB_PORT}
        MongoDBAdmin().add_shard(url)

    def add_members(self, members):
        """
        This method is used by a replica-set member instance.
        """
        def check_initiate_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()

            if ((status["ok"] == 1)
                    and (status["members"][0]["stateStr"] == "PRIMARY")
                    and (status["myState"] == 1)):
                return True
            else:
                return False

        def check_rs_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()
            primary_count = 0

            if status["ok"] != 1:
                return False
            if len(status["members"]) != (len(members) + 1):
                return False
            for rs_member in status["members"]:
                if rs_member["state"] not in [1, 2, 7]:
                    return False
                if rs_member["health"] != 1:
                    return False
                if rs_member["state"] == 1:
                    primary_count += 1

            return primary_count == 1

        MongoDBAdmin().rs_initiate()
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_initiate_status,
                         sleep_time=30,
                         time_out=CONF.mongodb.add_members_timeout)

        # add replica-set members
        MongoDBAdmin().rs_add_members(members)
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_rs_status,
                         sleep_time=10,
                         time_out=CONF.mongodb.add_members_timeout)

    def _set_localhost_auth_bypass(self, enabled):
        """When active, the localhost exception allows connections from the
        localhost interface to create the first user on the admin database.
        The exception applies only when there are no users created in the
        MongoDB instance.
        """
        self.configuration_manager.apply_system_override(
            {'setParameter': {
                'enableLocalhostAuthBypass': enabled
            }})

    def list_all_dbs(self):
        return MongoDBAdmin().list_database_names()

    def db_data_size(self, db_name):
        schema = models.MongoDBSchema(db_name)
        return MongoDBAdmin().db_stats(schema.serialize())['dataSize']

    def admin_cmd_auth_params(self):
        return MongoDBAdmin().cmd_admin_auth_params

    def get_key_file(self):
        return system.MONGO_KEY_FILE

    def get_key(self):
        return operating_system.read_file(system.MONGO_KEY_FILE,
                                          as_root=True).rstrip()

    def store_key(self, key):
        """Store the cluster key."""
        LOG.debug('Storing key for MongoDB cluster.')
        operating_system.write_file(system.MONGO_KEY_FILE, key, as_root=True)
        operating_system.chmod(system.MONGO_KEY_FILE,
                               operating_system.FileMode.SET_USR_RO,
                               as_root=True)
        operating_system.chown(system.MONGO_KEY_FILE,
                               system.MONGO_USER,
                               system.MONGO_USER,
                               as_root=True)

    def store_admin_password(self, password):
        LOG.debug('Storing admin password.')
        creds = MongoDBCredentials(username=system.MONGO_ADMIN_NAME,
                                   password=password)
        creds.write(system.MONGO_ADMIN_CREDS_FILE)
        return creds

    def create_admin_user(self, password):
        """Create the admin user while the localhost exception is active."""
        LOG.debug('Creating the admin user.')
        creds = self.store_admin_password(password)
        user = models.MongoDBUser(name='admin.%s' % creds.username,
                                  password=creds.password)
        user.roles = system.MONGO_ADMIN_ROLES
        # the driver engine is already cached, but we need to change it it
        with MongoDBClient(None, host='localhost',
                           port=MONGODB_PORT) as client:
            MongoDBAdmin().create_validated_user(user, client=client)
        # now revert to the normal engine
        self.status.set_host(host=netutils.get_my_ipv4(), port=MONGODB_PORT)
        LOG.debug('Created admin user.')

    def secure(self):
        """Create the Trove admin user.

        The service should not be running at this point.
        This will enable role-based access control (RBAC) by default.
        """
        if self.status.is_running:
            raise RuntimeError(
                _("Cannot secure the instance. "
                  "The service is still running."))

        try:
            self.configuration_manager.apply_system_override(
                {'security.authorization': 'enabled'})
            self._set_localhost_auth_bypass(True)
            self.start_db(update_db=False)
            password = utils.generate_random_password()
            self.create_admin_user(password)
            LOG.debug("MongoDB secure complete.")
        finally:
            self._set_localhost_auth_bypass(False)
            self.stop_db()

    def get_configuration_property(self, name, default=None):
        """Return the value of a MongoDB configuration property.
        """
        return self.configuration_manager.get_value(name, default)

    def prep_primary(self):
        # Prepare the primary member of a replica set.
        password = utils.generate_random_password()
        self.create_admin_user(password)
        self.restart()

    @property
    def replica_set_name(self):
        return MongoDBAdmin().get_repl_status()['set']

    @property
    def admin_password(self):
        creds = MongoDBCredentials()
        creds.read(system.MONGO_ADMIN_CREDS_FILE)
        return creds.password

    def is_shard_active(self, replica_set_name):
        shards = MongoDBAdmin().list_active_shards()
        if replica_set_name in [shard['_id'] for shard in shards]:
            LOG.debug('Replica set %s is active.' % replica_set_name)
            return True
        else:
            LOG.debug('Replica set %s is not active.' % replica_set_name)
            return False
Beispiel #8
0
    def _assert_get_value(self, override_strategy):
        base_config_contents = {"Section_1": {"name": "pi", "is_number": "True", "value": "3.1415"}}

        config_overrides_v1a = {"Section_1": {"name": "sqrt(2)", "value": "1.4142"}}

        config_overrides_v2 = {"Section_1": {"name": "e", "value": "2.7183"}, "Section_2": {"foo": "bar"}}

        config_overrides_v1b = {"Section_1": {"name": "sqrt(4)", "value": "2.0"}}

        codec = IniCodec()
        current_user = getpass.getuser()

        with tempfile.NamedTemporaryFile() as base_config:

            # Write initial config contents.
            operating_system.write_file(base_config.name, base_config_contents, codec)

            manager = ConfigurationManager(
                base_config.name,
                current_user,
                current_user,
                codec,
                requires_root=False,
                override_strategy=override_strategy,
            )

            # Test default value.
            self.assertIsNone(manager.get_value("Section_2"))
            self.assertEqual("foo", manager.get_value("Section_2", "foo"))

            # Test value before applying overrides.
            self.assertEqual("pi", manager.get_value("Section_1")["name"])
            self.assertEqual("3.1415", manager.get_value("Section_1")["value"])

            # Test value after applying overrides.
            manager.apply_user_override(config_overrides_v1a, change_id="id1")
            self.assertEqual("sqrt(2)", manager.get_value("Section_1")["name"])
            self.assertEqual("1.4142", manager.get_value("Section_1")["value"])
            manager.apply_user_override(config_overrides_v2, change_id="id2")
            self.assertEqual("e", manager.get_value("Section_1")["name"])
            self.assertEqual("2.7183", manager.get_value("Section_1")["value"])
            self.assertEqual("bar", manager.get_value("Section_2")["foo"])

            # Editing change 'id1' become visible only after removing
            # change 'id2', which overrides 'id1'.
            manager.apply_user_override(config_overrides_v1b, change_id="id1")
            self.assertEqual("e", manager.get_value("Section_1")["name"])
            self.assertEqual("2.7183", manager.get_value("Section_1")["value"])

            # Test value after removing overrides.

            # The edited values from change 'id1' should be visible after
            # removing 'id2'.
            manager.remove_user_override(change_id="id2")
            self.assertEqual("sqrt(4)", manager.get_value("Section_1")["name"])
            self.assertEqual("2.0", manager.get_value("Section_1")["value"])

            # Back to the base.
            manager.remove_user_override(change_id="id1")
            self.assertEqual("pi", manager.get_value("Section_1")["name"])
            self.assertEqual("3.1415", manager.get_value("Section_1")["value"])
            self.assertIsNone(manager.get_value("Section_2"))

            # Test system overrides.
            manager.apply_system_override(config_overrides_v1b, change_id="id1")
            self.assertEqual("sqrt(4)", manager.get_value("Section_1")["name"])
            self.assertEqual("2.0", manager.get_value("Section_1")["value"])

            # The system values should take precedence over the user
            # override.
            manager.apply_user_override(config_overrides_v1a, change_id="id1")
            self.assertEqual("sqrt(4)", manager.get_value("Section_1")["name"])
            self.assertEqual("2.0", manager.get_value("Section_1")["value"])

            # The user values should become visible only after removing the
            # system change.
            manager.remove_system_override(change_id="id1")
            self.assertEqual("sqrt(2)", manager.get_value("Section_1")["name"])
            self.assertEqual("1.4142", manager.get_value("Section_1")["value"])

            # Back to the base.
            manager.remove_user_override(change_id="id1")
            self.assertEqual("pi", manager.get_value("Section_1")["name"])
            self.assertEqual("3.1415", manager.get_value("Section_1")["value"])
            self.assertIsNone(manager.get_value("Section_2"))
Beispiel #9
0
class MongoDBApp(object):
    """Prepares DBaaS on a Guest container."""

    @classmethod
    def _init_overrides_dir(cls):
        """Initialize a directory for configuration overrides.
        """
        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(CONFIG_FILE), ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR
        )

        if not os.path.exists(revision_dir):
            operating_system.create_directory(
                revision_dir, user=system.MONGO_USER, group=system.MONGO_USER, force=True, as_root=True
            )

        return revision_dir

    def __init__(self):
        self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = self._init_overrides_dir()
        self.configuration_manager = ConfigurationManager(
            CONFIG_FILE,
            system.MONGO_USER,
            system.MONGO_USER,
            SafeYamlCodec(default_flow_style=False),
            requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir),
        )

        self.is_query_router = False
        self.is_cluster_member = False
        self.status = MongoDBAppStatus()

    def install_if_needed(self, packages):
        """Prepare the guest machine with a MongoDB installation."""
        LOG.info(_("Preparing Guest as MongoDB."))
        if not system.PACKAGER.pkg_is_installed(packages):
            LOG.debug("Installing packages: %s." % str(packages))
            system.PACKAGER.pkg_install(packages, {}, system.TIME_OUT)
        LOG.info(_("Finished installing MongoDB server."))

    def _get_service(self):
        if self.is_query_router:
            return operating_system.service_discovery(system.MONGOS_SERVICE_CANDIDATES)
        else:
            return operating_system.service_discovery(system.MONGOD_SERVICE_CANDIDATES)

    def _enable_db_on_boot(self):
        LOG.info(_("Enabling MongoDB on boot."))
        try:
            mongo_service = self._get_service()
            utils.execute_with_timeout(mongo_service["cmd_enable"], shell=True)
        except KeyError:
            raise RuntimeError(_("MongoDB service is not discovered."))

    def _disable_db_on_boot(self):
        LOG.info(_("Disabling MongoDB on boot."))
        try:
            mongo_service = self._get_service()
            utils.execute_with_timeout(mongo_service["cmd_disable"], shell=True)
        except KeyError:
            raise RuntimeError("MongoDB service is not discovered.")

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        LOG.info(_("Stopping MongoDB."))
        if do_not_start_on_reboot:
            self._disable_db_on_boot()

        try:
            mongo_service = self._get_service()
            # TODO(ramashri) see if hardcoded values can be removed
            utils.execute_with_timeout(mongo_service["cmd_stop"], shell=True, timeout=100)
        except KeyError:
            raise RuntimeError(_("MongoDB service is not discovered."))

        if not self.status.wait_for_real_status_to_change_to(
            ds_instance.ServiceStatuses.SHUTDOWN, self.state_change_wait_time, update_db
        ):
            LOG.error(_("Could not stop MongoDB."))
            self.status.end_install_or_restart()
            raise RuntimeError(_("Could not stop MongoDB"))

    def restart(self):
        LOG.info(_("Restarting MongoDB."))
        try:
            self.status.begin_restart()
            self.stop_db()
            self.start_db()
        finally:
            self.status.end_install_or_restart()

    def start_db(self, update_db=False):
        LOG.info(_("Starting MongoDB."))

        self._enable_db_on_boot()

        try:
            mongo_service = self._get_service()
            utils.execute_with_timeout(mongo_service["cmd_start"], shell=True)
        except ProcessExecutionError:
            pass
        except KeyError:
            raise RuntimeError("MongoDB service is not discovered.")
        self.wait_for_start(update_db=update_db)

    def wait_for_start(self, update_db=False):
        LOG.debug("Waiting for MongoDB to start.")
        if not self.status.wait_for_real_status_to_change_to(
            ds_instance.ServiceStatuses.RUNNING, self.state_change_wait_time, update_db
        ):
            LOG.error(_("Start up of MongoDB failed."))
            # If it won't start, but won't die either, kill it by hand so we
            # don't let a rouge process wander around.
            try:
                out, err = utils.execute_with_timeout(system.FIND_PID, shell=True)
                pid = "".join(out.split(" ")[1:2])
                utils.execute_with_timeout(system.MONGODB_KILL % pid, shell=True)
            except exception.ProcessExecutionError:
                LOG.exception(_("Error killing MongoDB start command."))
                # There's nothing more we can do...
            self.status.end_install_or_restart()
            raise RuntimeError("Could not start MongoDB.")
        LOG.debug("MongoDB started successfully.")

    def complete_install_or_restart(self):
        self.status.end_install_or_restart()

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def start_db_with_conf_changes(self, config_contents):
        LOG.info(_("Starting MongoDB with configuration changes."))
        if self.status.is_running:
            format = "Cannot start_db_with_conf_changes because status is %s."
            LOG.debug(format, self.status)
            raise RuntimeError(format % self.status)
        LOG.info(_("Initiating config."))
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration(None, mount_point=system.MONGODB_MOUNT_POINT)
        self.start_db(True)

    def reset_configuration(self, configuration):
        LOG.info(_("Resetting configuration."))
        config_contents = configuration["config_contents"]
        self.configuration_manager.save_configuration(config_contents)

    def apply_initial_guestagent_configuration(self, cluster_config, mount_point=None):
        LOG.debug("Applying initial configuration.")

        # Mongodb init scripts assume the PID-file path is writable by the
        # database service.
        # See: https://jira.mongodb.org/browse/SERVER-20075
        self._initialize_writable_run_dir()

        # todo mvandijk: enable authorization.
        # 'security.authorization': True
        self.configuration_manager.apply_system_override(
            {
                "processManagement.fork": False,
                "processManagement.pidFilePath": system.MONGO_PID_FILE,
                "systemLog.destination": "file",
                "systemLog.path": system.MONGO_LOG_FILE,
                "systemLog.logAppend": True,
            }
        )

        if mount_point:
            self.configuration_manager.apply_system_override({"storage.dbPath": mount_point})

        if cluster_config is not None:
            self._configure_as_cluster_instance(cluster_config)
        else:
            self._configure_network(MONGODB_PORT)

    def _initialize_writable_run_dir(self):
        """Create a writable directory for Mongodb's runtime data
        (e.g. PID-file).
        """
        mongodb_run_dir = os.path.dirname(system.MONGO_PID_FILE)
        LOG.debug("Initializing a runtime directory: %s" % mongodb_run_dir)
        operating_system.create_directory(
            mongodb_run_dir, user=system.MONGO_USER, group=system.MONGO_USER, force=True, as_root=True
        )

    def _configure_as_cluster_instance(self, cluster_config):
        """Configure this guest as a cluster instance and return its
        new status.
        """
        if cluster_config["instance_type"] == "query_router":
            self._configure_as_query_router()
        elif cluster_config["instance_type"] == "config_server":
            self._configure_as_config_server()
        elif cluster_config["instance_type"] == "member":
            self._configure_as_cluster_member(cluster_config["replica_set_name"])
        else:
            LOG.error(_("Bad cluster configuration; instance type " "given as %s.") % cluster_config["instance_type"])
            return ds_instance.ServiceStatuses.FAILED

        if "key" in cluster_config:
            self._configure_cluster_security(cluster_config["key"])

    def _configure_as_query_router(self):
        LOG.info(_("Configuring instance as a cluster query router."))
        self.is_query_router = True

        # Write the 'mongos' upstart script.
        # FIXME(pmalik): The control script should really be written in the
        # elements.
        # The guestagent will choose the right daemon ('mongod' or 'mongos')
        # based on the 'cluster_config' values.
        upstart_contents = system.MONGOS_UPSTART_CONTENTS.format(config_file_placeholder=CONFIG_FILE)
        operating_system.write_file(system.MONGOS_UPSTART, upstart_contents, as_root=True)

        # FIXME(pmalik): We should really have a separate configuration
        # template for the 'mongos' process.
        # Remove all storage configurations from the template.
        # They apply only to 'mongod' processes.
        # Already applied overrides will be integrated into the base file and
        # their current groups removed.
        config = guestagent_utils.expand_dict(self.configuration_manager.parse_configuration())
        if "storage" in config:
            LOG.debug("Removing 'storage' directives from the configuration " "template.")
            del config["storage"]
            self.configuration_manager.save_configuration(guestagent_utils.flatten_dict(config))

        # Apply 'mongos' configuration.
        self._configure_network(MONGODB_PORT)
        self.configuration_manager.apply_system_override({"sharding.configDB": ""}, CNF_CLUSTER)

    def _configure_as_config_server(self):
        LOG.info(_("Configuring instance as a cluster config server."))
        self._configure_network(CONFIGSVR_PORT)
        self.configuration_manager.apply_system_override({"sharding.clusterRole": "configsvr"}, CNF_CLUSTER)

    def _configure_as_cluster_member(self, replica_set_name):
        LOG.info(_("Configuring instance as a cluster member."))
        self.is_cluster_member = True
        self._configure_network(MONGODB_PORT)
        # we don't want these thinking they are in a replica set yet
        # as that would prevent us from creating the admin user,
        # so start mongo before updating the config.
        # mongo will be started by the cluster taskmanager
        self.start_db()
        self.configuration_manager.apply_system_override({"replication.replSetName": replica_set_name}, CNF_CLUSTER)

    def _configure_cluster_security(self, key_value):
        """Force cluster key-file-based authentication.
        """
        # Store the cluster member authentication key.
        self.store_key(key_value)

        # TODO(mvandijk): enable cluster security once Trove features are in
        # self.configuration_manager.apply_system_override(
        #     {'security.clusterAuthMode': 'keyFile',
        #      'security.keyFile': self.get_key_file()}, CNF_CLUSTER)

    def _configure_network(self, port=None):
        """Make the service accessible at a given (or default if not) port.
        """
        instance_ip = netutils.get_my_ipv4()
        bind_interfaces_string = ",".join([instance_ip, "127.0.0.1"])
        options = {"net.bindIp": bind_interfaces_string}
        if port is not None:
            guestagent_utils.update_dict({"net.port": port}, options)

        self.configuration_manager.apply_system_override(options)
        self.status.set_host(instance_ip, port=port)

    def clear_storage(self):
        mount_point = "/var/lib/mongodb/*"
        LOG.debug("Clearing storage at %s." % mount_point)
        try:
            operating_system.remove(mount_point, force=True, as_root=True)
        except exception.ProcessExecutionError:
            LOG.exception(_("Error clearing storage."))

    def _has_config_db(self):
        value_string = self.configuration_manager.get_value("sharding", {}).get("configDB")

        return value_string is not None

    # FIXME(pmalik): This method should really be called 'set_config_servers'.
    # The current name suggests it adds more config servers, but it
    # rather replaces the existing ones.
    def add_config_servers(self, config_server_hosts):
        """Set config servers on a query router (mongos) instance.
        """
        config_servers_string = ",".join(["%s:27019" % host for host in config_server_hosts])
        LOG.info(_("Setting config servers: %s") % config_servers_string)
        self.configuration_manager.apply_system_override({"sharding.configDB": config_servers_string}, CNF_CLUSTER)
        self.start_db(True)

    def add_shard(self, replica_set_name, replica_set_member):
        """
        This method is used by query router (mongos) instances.
        """
        url = "%(rs)s/%(host)s:%(port)s" % {"rs": replica_set_name, "host": replica_set_member, "port": MONGODB_PORT}
        MongoDBAdmin().add_shard(url)

    def add_members(self, members):
        """
        This method is used by a replica-set member instance.
        """

        def check_initiate_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()

            if (status["ok"] == 1) and (status["members"][0]["stateStr"] == "PRIMARY") and (status["myState"] == 1):
                return True
            else:
                return False

        def check_rs_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()
            primary_count = 0

            if status["ok"] != 1:
                return False
            if len(status["members"]) != (len(members) + 1):
                return False
            for rs_member in status["members"]:
                if rs_member["state"] not in [1, 2, 7]:
                    return False
                if rs_member["health"] != 1:
                    return False
                if rs_member["state"] == 1:
                    primary_count += 1

            return primary_count == 1

        MongoDBAdmin().rs_initiate()
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_initiate_status, sleep_time=30, time_out=100)

        # add replica-set members
        MongoDBAdmin().rs_add_members(members)
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_rs_status, sleep_time=10, time_out=100)

    def _set_localhost_auth_bypass(self, enabled):
        """When active, the localhost exception allows connections from the
        localhost interface to create the first user on the admin database.
        The exception applies only when there are no users created in the
        MongoDB instance.
        """
        self.configuration_manager.apply_system_override({"setParameter": {"enableLocalhostAuthBypass": enabled}})

    def list_all_dbs(self):
        return MongoDBAdmin().list_database_names()

    def db_data_size(self, db_name):
        schema = models.MongoDBSchema(db_name)
        return MongoDBAdmin().db_stats(schema.serialize())["dataSize"]

    def admin_cmd_auth_params(self):
        return MongoDBAdmin().cmd_admin_auth_params

    def get_key_file(self):
        return system.MONGO_KEY_FILE

    def get_key(self):
        return operating_system.read_file(system.MONGO_KEY_FILE, as_root=True).rstrip()

    def store_key(self, key):
        """Store the cluster key."""
        LOG.debug("Storing key for MongoDB cluster.")
        operating_system.write_file(system.MONGO_KEY_FILE, key, as_root=True)
        operating_system.chmod(system.MONGO_KEY_FILE, operating_system.FileMode.SET_USR_RO, as_root=True)
        operating_system.chown(system.MONGO_KEY_FILE, system.MONGO_USER, system.MONGO_USER, as_root=True)

    def store_admin_password(self, password):
        LOG.debug("Storing admin password.")
        creds = MongoDBCredentials(username=system.MONGO_ADMIN_NAME, password=password)
        creds.write(system.MONGO_ADMIN_CREDS_FILE)
        return creds

    def create_admin_user(self, password):
        """Create the admin user while the localhost exception is active."""
        LOG.debug("Creating the admin user.")
        creds = self.store_admin_password(password)
        user = models.MongoDBUser(name="admin.%s" % creds.username, password=creds.password)
        user.roles = system.MONGO_ADMIN_ROLES
        # the driver engine is already cached, but we need to change it it
        with MongoDBClient(None, host="localhost", port=MONGODB_PORT) as client:
            MongoDBAdmin().create_user(user, client=client)
        # now revert to the normal engine
        self.status.set_host(host=netutils.get_my_ipv4(), port=MONGODB_PORT)
        LOG.debug("Created admin user.")

    def secure(self):
        """Create the Trove admin user.

        The service should not be running at this point.
        """
        if self.status.is_running:
            raise RuntimeError(_("Cannot secure the instance. " "The service is still running."))

        try:
            self._set_localhost_auth_bypass(True)
            self.start_db(update_db=False)
            password = utils.generate_random_password()
            self.create_admin_user(password)
            LOG.debug("MongoDB secure complete.")
        finally:
            self._set_localhost_auth_bypass(False)
            self.stop_db()

    def get_configuration_property(self, name, default=None):
        """Return the value of a MongoDB configuration property.
        """
        return self.configuration_manager.get_value(name, default)

    def prep_primary(self):
        # Prepare the primary member of a replica set.
        password = utils.generate_random_password()
        self.create_admin_user(password)
        self.restart()

    @property
    def replica_set_name(self):
        return MongoDBAdmin().get_repl_status()["set"]

    @property
    def admin_password(self):
        creds = MongoDBCredentials()
        creds.read(system.MONGO_ADMIN_CREDS_FILE)
        return creds.password

    def is_shard_active(self, replica_set_name):
        shards = MongoDBAdmin().list_active_shards()
        if replica_set_name in [shard["_id"] for shard in shards]:
            LOG.debug("Replica set %s is active." % replica_set_name)
            return True
        else:
            LOG.debug("Replica set %s is not active." % replica_set_name)
            return False
Beispiel #10
0
    def _assert_get_value(self, override_strategy):
        base_config_contents = {
            'Section_1': {
                'name': 'pi',
                'is_number': 'True',
                'value': '3.1415'
            }
        }

        config_overrides_v1a = {
            'Section_1': {
                'name': 'sqrt(2)',
                'value': '1.4142'
            }
        }

        config_overrides_v2 = {
            'Section_1': {
                'name': 'e',
                'value': '2.7183'
            },
            'Section_2': {
                'foo': 'bar'
            }
        }

        config_overrides_v1b = {
            'Section_1': {
                'name': 'sqrt(4)',
                'value': '2.0'
            }
        }

        codec = IniCodec()
        current_user = getpass.getuser()

        with tempfile.NamedTemporaryFile() as base_config:

            # Write initial config contents.
            operating_system.write_file(base_config.name, base_config_contents,
                                        codec)

            manager = ConfigurationManager(base_config.name,
                                           current_user,
                                           current_user,
                                           codec,
                                           requires_root=False,
                                           override_strategy=override_strategy)

            # Test default value.
            self.assertIsNone(manager.get_value('Section_2'))
            self.assertEqual('foo', manager.get_value('Section_2', 'foo'))

            # Test value before applying overrides.
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])

            # Test value after applying overrides.
            manager.apply_user_override(config_overrides_v1a, change_id='id1')
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])
            manager.apply_user_override(config_overrides_v2, change_id='id2')
            self.assertEqual('e', manager.get_value('Section_1')['name'])
            self.assertEqual('2.7183', manager.get_value('Section_1')['value'])
            self.assertEqual('bar', manager.get_value('Section_2')['foo'])

            # Editing change 'id1' become visible only after removing
            # change 'id2', which overrides 'id1'.
            manager.apply_user_override(config_overrides_v1b, change_id='id1')
            self.assertEqual('e', manager.get_value('Section_1')['name'])
            self.assertEqual('2.7183', manager.get_value('Section_1')['value'])

            # Test value after removing overrides.

            # The edited values from change 'id1' should be visible after
            # removing 'id2'.
            manager.remove_user_override(change_id='id2')
            self.assertEqual('sqrt(4)', manager.get_value('Section_1')['name'])
            self.assertEqual('2.0', manager.get_value('Section_1')['value'])

            # Back to the base.
            manager.remove_user_override(change_id='id1')
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertIsNone(manager.get_value('Section_2'))

            # Test system overrides.
            manager.apply_system_override(config_overrides_v1b,
                                          change_id='id1')
            self.assertEqual('sqrt(4)', manager.get_value('Section_1')['name'])
            self.assertEqual('2.0', manager.get_value('Section_1')['value'])

            # The system values should take precedence over the user
            # override.
            manager.apply_user_override(config_overrides_v1a, change_id='id1')
            self.assertEqual('sqrt(4)', manager.get_value('Section_1')['name'])
            self.assertEqual('2.0', manager.get_value('Section_1')['value'])

            # The user values should become visible only after removing the
            # system change.
            manager.remove_system_override(change_id='id1')
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])

            # Back to the base.
            manager.remove_user_override(change_id='id1')
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertIsNone(manager.get_value('Section_2'))
Beispiel #11
0
class CassandraApp(object):
    """Prepares DBaaS on a Guest container."""

    _ADMIN_USER = '******'

    _CONF_AUTH_SEC = 'authentication'
    _CONF_USR_KEY = 'username'
    _CONF_PWD_KEY = 'password'
    _CONF_DIR_MODS = stat.S_IRWXU
    _CONF_FILE_MODS = stat.S_IRUSR

    CASSANDRA_CONF_FILE = "cassandra.yaml"
    CASSANDRA_TOPOLOGY_FILE = 'cassandra-rackdc.properties'

    _TOPOLOGY_CODEC = PropertiesCodec(
        delimiter='=', unpack_singletons=True, string_mappings={
            'true': True, 'false': False})

    CASSANDRA_KILL_CMD = "sudo killall java  || true"

    def __init__(self):
        self.state_change_wait_time = CONF.state_change_wait_time
        self.status = CassandraAppStatus(self.get_current_superuser())

        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(self.cassandra_conf),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)
        self.configuration_manager = ConfigurationManager(
            self.cassandra_conf,
            self.cassandra_owner, self.cassandra_owner,
            SafeYamlCodec(default_flow_style=False), requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

    @property
    def service_candidates(self):
        return ['cassandra']

    @property
    def cassandra_conf_dir(self):
        return {
            operating_system.REDHAT: "/etc/cassandra/default.conf/",
            operating_system.DEBIAN: "/etc/cassandra/",
            operating_system.SUSE: "/etc/cassandra/default.conf/"
        }[operating_system.get_os()]

    @property
    def cassandra_conf(self):
        return guestagent_utils.build_file_path(self.cassandra_conf_dir,
                                                self.CASSANDRA_CONF_FILE)

    @property
    def cassandra_topology(self):
        return guestagent_utils.build_file_path(self.cassandra_conf_dir,
                                                self.CASSANDRA_TOPOLOGY_FILE)

    @property
    def cassandra_owner(self):
        return 'cassandra'

    @property
    def cassandra_data_dir(self):
        return guestagent_utils.build_file_path(
            self.cassandra_working_dir, 'data')

    @property
    def cassandra_working_dir(self):
        return "/var/lib/cassandra"

    @property
    def default_superuser_name(self):
        return "cassandra"

    @property
    def default_superuser_password(self):
        return "cassandra"

    @property
    def default_superuser_pwd_hash(self):
        # Default 'salted_hash' value for 'cassandra' user on Cassandra 2.1.
        return "$2a$10$wPEVuXBU7WE2Uwzqq3t19ObRJyoKztzC/Doyfr0VtDmVXC4GDAV3e"

    @property
    def cqlsh_conf_path(self):
        return "~/.cassandra/cqlshrc"

    def build_admin(self):
        return CassandraAdmin(self.get_current_superuser())

    def install_if_needed(self, packages):
        """Prepare the guest machine with a Cassandra server installation."""
        LOG.info(_("Preparing Guest as a Cassandra Server"))
        if not packager.pkg_is_installed(packages):
            self._install_db(packages)
        LOG.debug("Cassandra install_if_needed complete")

    def init_storage_structure(self, mount_point):
        try:
            operating_system.create_directory(mount_point, as_root=True)
        except exception.ProcessExecutionError:
            LOG.exception(_("Error while initiating storage structure."))

    def start_db(self, update_db=False, enable_on_boot=True):
        self.status.start_db_service(
            self.service_candidates, self.state_change_wait_time,
            enable_on_boot=enable_on_boot, update_db=update_db)

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        self.status.stop_db_service(
            self.service_candidates, self.state_change_wait_time,
            disable_on_boot=do_not_start_on_reboot, update_db=update_db)

    def restart(self):
        self.status.restart_db_service(
            self.service_candidates, self.state_change_wait_time)

    def _install_db(self, packages):
        """Install Cassandra server"""
        LOG.debug("Installing Cassandra server.")
        packager.pkg_install(packages, None, 10000)
        LOG.debug("Finished installing Cassandra server")

    def _remove_system_tables(self):
        """
        Clean up the system keyspace.

        System tables are initialized on the first boot.
        They store certain properties, such as 'cluster_name',
        that cannot be easily changed once afterwards.
        The system keyspace needs to be cleaned up first. The
        tables will be regenerated on the next startup.
        Make sure to also cleanup the commitlog and caches to avoid
        startup errors due to inconsistencies.

        The service should not be running at this point.
        """
        if self.status.is_running:
            raise RuntimeError(_("Cannot remove system tables. "
                                 "The service is still running."))

        LOG.info(_('Removing existing system tables.'))
        system_keyspace_dir = guestagent_utils.build_file_path(
            self.cassandra_data_dir, 'system')
        commitlog_file = guestagent_utils.build_file_path(
            self.cassandra_working_dir, 'commitlog')
        chaches_dir = guestagent_utils.build_file_path(
            self.cassandra_working_dir, 'saved_caches')

        operating_system.remove(system_keyspace_dir,
                                force=True, recursive=True, as_root=True)
        operating_system.remove(commitlog_file,
                                force=True, recursive=True, as_root=True)
        operating_system.remove(chaches_dir,
                                force=True, recursive=True, as_root=True)

        operating_system.create_directory(
            system_keyspace_dir,
            user=self.cassandra_owner, group=self.cassandra_owner,
            force=True, as_root=True)
        operating_system.create_directory(
            commitlog_file,
            user=self.cassandra_owner, group=self.cassandra_owner,
            force=True, as_root=True)
        operating_system.create_directory(
            chaches_dir,
            user=self.cassandra_owner, group=self.cassandra_owner,
            force=True, as_root=True)

    def _apply_post_restore_updates(self, backup_info):
        """The service should not be running at this point.

        The restored database files carry some properties over from the
        original instance that need to be updated with appropriate
        values for the new instance.
        These include:

            - Reset the 'cluster_name' property to match the new unique
              ID of this instance.
              This is to ensure that the restored instance is a part of a new
              single-node cluster rather than forming a one with the
              original node.
            - Reset the administrator's password.
              The original password from the parent instance may be
              compromised or long lost.

        A general procedure is:
            - update the configuration property with the current value
              so that the service can start up
            - reset the superuser password
            - restart the service
            - change the cluster name
            - restart the service

        :seealso: _reset_admin_password
        :seealso: change_cluster_name
        """

        if self.status.is_running:
            raise RuntimeError(_("Cannot reset the cluster name. "
                                 "The service is still running."))

        LOG.debug("Applying post-restore updates to the database.")

        try:
            # Change the 'cluster_name' property to the current in-database
            # value so that the database can start up.
            self._update_cluster_name_property(backup_info['instance_id'])

            # Reset the superuser password so that we can log-in.
            self._reset_admin_password()

            # Start the database and update the 'cluster_name' to the
            # new value.
            self.start_db(update_db=False)
            self.change_cluster_name(CONF.guest_id)
        finally:
            self.stop_db()  # Always restore the initial state of the service.

    def cluster_secure(self, password):
        return self.secure(password=password).serialize()

    def secure(self, update_user=None, password=None):
        """Configure the Trove administrative user.
        Update an existing user if given.
        Create a new one using the default database credentials
        otherwise and drop the built-in user when finished.
        """
        LOG.info(_('Configuring Trove superuser.'))

        if password is None:
            password = utils.generate_random_password()

        admin_username = update_user.name if update_user else self._ADMIN_USER
        os_admin = models.CassandraUser(admin_username, password)

        if update_user:
            CassandraAdmin(update_user).alter_user_password(os_admin)
        else:
            cassandra = models.CassandraUser(
                self.default_superuser_name, self.default_superuser_password)
            CassandraAdmin(cassandra)._create_superuser(os_admin)
            CassandraAdmin(os_admin).drop_user(cassandra)

        self._update_admin_credentials(os_admin)

        return os_admin

    def _update_admin_credentials(self, user):
        self.__create_cqlsh_config({self._CONF_AUTH_SEC:
                                    {self._CONF_USR_KEY: user.name,
                                     self._CONF_PWD_KEY: user.password}})

        # Update the internal status with the new user.
        self.status = CassandraAppStatus(user)

    def store_admin_credentials(self, admin_credentials):
        user = models.CassandraUser.deserialize_user(admin_credentials)
        self._update_admin_credentials(user)

    def get_admin_credentials(self):
        return self.get_current_superuser().serialize()

    def _reset_admin_password(self):
        """
        Reset the password of the Trove's administrative superuser.

        The service should not be running at this point.

        A general password reset procedure is:
            - disable user authentication and remote access
            - restart the service
            - update the password in the 'system_auth.credentials' table
            - re-enable authentication and make the host reachable
            - restart the service
        """
        if self.status.is_running:
            raise RuntimeError(_("Cannot reset the administrative password. "
                                 "The service is still running."))

        try:
            # Disable automatic startup in case the node goes down before
            # we have the superuser secured.
            operating_system.disable_service_on_boot(self.service_candidates)

            self.__disable_remote_access()
            self.__disable_authentication()

            # We now start up the service and immediately re-enable
            # authentication in the configuration file (takes effect after
            # restart).
            # Then we reset the superuser password to its default value
            # and restart the service to get user functions back.
            self.start_db(update_db=False, enable_on_boot=False)
            self.__enable_authentication()
            os_admin = self.__reset_user_password_to_default(self._ADMIN_USER)
            self.status = CassandraAppStatus(os_admin)
            self.restart()

            # Now change the administrative password to a new secret value.
            self.secure(update_user=os_admin)
        finally:
            self.stop_db()  # Always restore the initial state of the service.

        # At this point, we should have a secured database with new Trove-only
        # superuser password.
        # Proceed to re-enable remote access and automatic startup.
        self.__enable_remote_access()
        operating_system.enable_service_on_boot(self.service_candidates)

    def __reset_user_password_to_default(self, username):
        LOG.debug("Resetting the password of user '%s' to '%s'."
                  % (username, self.default_superuser_password))

        user = models.CassandraUser(username, self.default_superuser_password)
        with CassandraLocalhostConnection(user) as client:
            client.execute(
                "UPDATE system_auth.credentials SET salted_hash=%s "
                "WHERE username='******';", (user.name,),
                (self.default_superuser_pwd_hash,))

            return user

    def change_cluster_name(self, cluster_name):
        """Change the 'cluster_name' property of an exesting running instance.
        Cluster name is stored in the database and is required to match the
        configuration value. Cassandra fails to start otherwise.
        """

        if not self.status.is_running:
            raise RuntimeError(_("Cannot change the cluster name. "
                                 "The service is not running."))

        LOG.debug("Changing the cluster name to '%s'." % cluster_name)

        # Update the in-database value.
        self.__reset_cluster_name(cluster_name)

        # Update the configuration property.
        self._update_cluster_name_property(cluster_name)

        self.restart()

    def __reset_cluster_name(self, cluster_name):
        # Reset the in-database value stored locally on this node.
        current_superuser = self.get_current_superuser()
        with CassandraLocalhostConnection(current_superuser) as client:
            client.execute(
                "UPDATE system.local SET cluster_name = '{}' "
                "WHERE key='local';", (cluster_name,))

        # Newer version of Cassandra require a flush to ensure the changes
        # to the local system keyspace persist.
        self.flush_tables('system', 'local')

    def __create_cqlsh_config(self, sections):
        config_path = self._get_cqlsh_conf_path()
        config_dir = os.path.dirname(config_path)
        if not os.path.exists(config_dir):
            os.mkdir(config_dir, self._CONF_DIR_MODS)
        else:
            os.chmod(config_dir, self._CONF_DIR_MODS)
        operating_system.write_file(config_path, sections, codec=IniCodec())
        os.chmod(config_path, self._CONF_FILE_MODS)

    def get_current_superuser(self):
        """
        Build the Trove superuser.
        Use the stored credentials.
        If not available fall back to the defaults.
        """
        if self.has_user_config():
            return self._load_current_superuser()

        LOG.warn(_("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,
                                    self.default_superuser_password)

    def has_user_config(self):
        """
        Return TRUE if there is a client configuration file available
        on the guest.
        """
        return os.path.exists(self._get_cqlsh_conf_path())

    def _load_current_superuser(self):
        config = operating_system.read_file(self._get_cqlsh_conf_path(),
                                            codec=IniCodec())
        return models.CassandraUser(
            config[self._CONF_AUTH_SEC][self._CONF_USR_KEY],
            config[self._CONF_AUTH_SEC][self._CONF_PWD_KEY]
        )

    def apply_initial_guestagent_configuration(self, cluster_name=None):
        """Update guestagent-controlled configuration properties.
        These changes to the default template are necessary in order to make
        the database service bootable and accessible in the guestagent context.

        :param cluster_name:  The 'cluster_name' configuration property.
                              Use the unique guest id by default.
        :type cluster_name:   string
        """
        self.configuration_manager.apply_system_override(
            {'data_file_directories': [self.cassandra_data_dir]})
        self._make_host_reachable()
        self._update_cluster_name_property(cluster_name or CONF.guest_id)
        # A single-node instance may use the SimpleSnitch
        # (keyspaces use SimpleStrategy).
        # A network-aware snitch has to be used otherwise.
        if cluster_name is None:
            updates = {'endpoint_snitch': 'SimpleSnitch'}
        else:
            updates = {'endpoint_snitch': 'GossipingPropertyFileSnitch'}
        self.configuration_manager.apply_system_override(updates)

    def _make_host_reachable(self):
        """
        Some of these settings may be overriden by user defined
        configuration groups.

        authenticator and authorizer
            - Necessary to enable users and permissions.
        rpc_address - Enable remote connections on all interfaces.
        broadcast_rpc_address - RPC address to broadcast to drivers and
                                other clients. Must be set if
                                rpc_address = 0.0.0.0 and can never be
                                0.0.0.0 itself.
        listen_address - The address on which the node communicates with
                         other nodes. Can never be 0.0.0.0.
        seed_provider - A list of discovery contact points.
        """
        self.__enable_authentication()
        self.__enable_remote_access()

    def __enable_remote_access(self):
        updates = {
            'rpc_address': "0.0.0.0",
            'broadcast_rpc_address': netutils.get_my_ipv4(),
            'listen_address': netutils.get_my_ipv4(),
            'seed_provider': {'parameters':
                              [{'seeds': netutils.get_my_ipv4()}]
                              }
        }

        self.configuration_manager.apply_system_override(updates)

    def __disable_remote_access(self):
        updates = {
            'rpc_address': "127.0.0.1",
            'listen_address': '127.0.0.1',
            'seed_provider': {'parameters':
                              [{'seeds': '127.0.0.1'}]
                              }
        }

        self.configuration_manager.apply_system_override(updates)

    def __enable_authentication(self):
        updates = {
            'authenticator': 'org.apache.cassandra.auth.PasswordAuthenticator',
            'authorizer': 'org.apache.cassandra.auth.CassandraAuthorizer'
        }

        self.configuration_manager.apply_system_override(updates)

    def __disable_authentication(self):
        updates = {
            'authenticator': 'org.apache.cassandra.auth.AllowAllAuthenticator',
            'authorizer': 'org.apache.cassandra.auth.AllowAllAuthorizer'
        }

        self.configuration_manager.apply_system_override(updates)

    def _update_cluster_name_property(self, name):
        """This 'cluster_name' property prevents nodes from one
        logical cluster from talking to another.
        All nodes in a cluster must have the same value.
        """
        self.configuration_manager.apply_system_override({'cluster_name':
                                                          name})

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def write_cluster_topology(self, data_center, rack, prefer_local=True):
        LOG.info(_('Saving Cassandra cluster topology configuration.'))

        config = {'dc': data_center,
                  'rack': rack,
                  'prefer_local': prefer_local}

        operating_system.write_file(self.cassandra_topology, config,
                                    codec=self._TOPOLOGY_CODEC, as_root=True)
        operating_system.chown(
            self.cassandra_topology,
            self.cassandra_owner, self.cassandra_owner, as_root=True)
        operating_system.chmod(
            self.cassandra_topology, FileMode.ADD_READ_ALL, as_root=True)

    def start_db_with_conf_changes(self, config_contents):
        LOG.debug("Starting database with configuration changes.")
        if self.status.is_running:
            raise RuntimeError(_("The service is still running."))

        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration()
        self.start_db(True)

    def reset_configuration(self, configuration):
        LOG.debug("Resetting configuration.")
        config_contents = configuration['config_contents']
        self.configuration_manager.save_configuration(config_contents)

    def _get_cqlsh_conf_path(self):
        return os.path.expanduser(self.cqlsh_conf_path)

    def get_data_center(self):
        config = operating_system.read_file(self.cassandra_topology,
                                            codec=self._TOPOLOGY_CODEC)
        return config['dc']

    def get_rack(self):
        config = operating_system.read_file(self.cassandra_topology,
                                            codec=self._TOPOLOGY_CODEC)
        return config['rack']

    def set_seeds(self, seeds):
        LOG.debug("Setting seed nodes: %s" % seeds)
        updates = {
            'seed_provider': {'parameters':
                              [{'seeds': ','.join(seeds)}]
                              }
        }

        self.configuration_manager.apply_system_override(updates)

    def get_seeds(self):
        """Return a list of seed node IPs if any.

        The seed IPs are stored as a comma-separated string in the
        seed-provider parameters:
        [{'class_name': '<name>', 'parameters': [{'seeds': '<ip>,<ip>'}, ...]}]
        """

        def find_first(key, dict_list):
            for item in dict_list:
                if key in item:
                    return item[key]
            return []

        sp_property = self.configuration_manager.get_value('seed_provider', [])
        seeds_str = find_first('seeds', find_first('parameters', sp_property))
        return seeds_str.split(',') if seeds_str else []

    def set_auto_bootstrap(self, enabled):
        """Auto-bootstrap makes new (non-seed) nodes automatically migrate the
        right data to themselves.
        The feature has to be turned OFF when initializing a fresh cluster
        without data.
        It must be turned back ON once the cluster is initialized.
        """
        LOG.debug("Setting auto-bootstrapping: %s" % enabled)
        updates = {'auto_bootstrap': enabled}
        self.configuration_manager.apply_system_override(updates)

    def node_cleanup_begin(self):
        """Suspend periodic status updates and mark the instance busy
        throughout the operation.
        """
        self.status.begin_restart()
        self.status.set_status(rd_instance.ServiceStatuses.BLOCKED)

    def node_cleanup(self):
        """Cassandra does not automatically remove data from nodes that
        lose part of their partition range to a newly added node.
        Cleans up keyspaces and partition keys no longer belonging to the node.

        Do not treat cleanup failures as fatal. Resume the heartbeat after
        finishing and let it signal the true state of the instance to the
        caller.
        """
        LOG.debug("Running node cleanup.")
        # nodetool -h <HOST> -p <PORT> -u <USER> -pw <PASSWORD> cleanup
        try:
            self._run_nodetool_command('cleanup')
            self.status.set_status(rd_instance.ServiceStatuses.RUNNING)
        except Exception:
            LOG.exception(_("The node failed to complete its cleanup."))
        finally:
            self.status.end_restart()

    def node_decommission(self):
        """Causes a live node to decommission itself,
        streaming its data to the next node on the ring.

        Shutdown the database after successfully finishing the operation,
        or leave the node in a failed state otherwise.

        Suspend periodic status updates, so that the caller can poll for the
        database shutdown.
        """
        LOG.debug("Decommissioning the node.")
        # nodetool -h <HOST> -p <PORT> -u <USER> -pw <PASSWORD> decommission
        self.status.begin_restart()
        try:
            self._run_nodetool_command('decommission')
        except Exception:
            LOG.exception(_("The node failed to decommission itself."))
            self.status.set_status(rd_instance.ServiceStatuses.FAILED)
            return
        finally:
            # Cassandra connections have ability to automatically discover and
            # fallback to other cluster nodes whenever a node goes down.
            # Reset the status after decomissioning to ensure the heartbeat
            # connection talks to this node only.
            self.status = CassandraAppStatus(self.get_current_superuser())

        try:
            self.stop_db(update_db=True, do_not_start_on_reboot=True)
        finally:
            self.status.end_restart()

    def flush_tables(self, keyspace, *tables):
        """Flushes one or more tables from the memtable.
        """
        LOG.debug("Flushing tables.")
        # nodetool -h <HOST> -p <PORT> -u <USER> -pw <PASSWORD> flush --
        # <keyspace> ( <table> ... )
        self._run_nodetool_command('flush', keyspace, *tables)

    def _run_nodetool_command(self, cmd, *args, **kwargs):
        """Execute a nodetool command on this node.
        """
        return utils.execute('nodetool', '-h', 'localhost',
                             cmd, *args, **kwargs)

    def enable_root(self, root_password=None):
        """Cassandra's 'root' user is called 'cassandra'.
        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)
        admin = self.build_admin()
        if self.is_root_enabled():
            admin.alter_user_password(cassandra)
        else:
            admin._create_superuser(cassandra)

        return cassandra.serialize()

    def is_root_enabled(self):
        """The Trove administrative user ('os_admin') should normally be the
        only superuser in the system.
        """
        found = self.build_admin().list_superusers()
        return len([user for user in found
                    if user.name != self._ADMIN_USER]) > 0
Beispiel #12
0
    def _assert_update_configuration(self, override_strategy):
        base_config_contents = {
            'Section_1': {
                'name': 'pi',
                'is_number': 'True',
                'value': '3.1415'
            }
        }

        config_overrides_v1 = {
            'Section_1': {
                'name': 'sqrt(2)',
                'value': '1.4142'
            }
        }

        config_overrides_v2 = {
            'Section_1': {
                'name': 'e',
                'value': '2.7183'
            },
            'Section_2': {
                'foo': 'bar'
            }
        }

        codec = IniCodec()
        current_user = getpass.getuser()

        with tempfile.NamedTemporaryFile() as base_config:

            # Write initial config contents.
            operating_system.write_file(base_config.name, base_config_contents,
                                        codec)

            manager = ConfigurationManager(base_config.name,
                                           current_user,
                                           current_user,
                                           codec,
                                           requires_root=False)

            manager.update_configuration(
                {'System': {
                    'name': 'c',
                    'is_number': 'True',
                    'value': 'N/A'
                }})

            manager.set_override_strategy(override_strategy, 2)

            # Test value before applying overrides.
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertEqual('N/A', manager.get_value('System')['value'])
            self.assertEqual(0, manager.current_revision)

            manager.update_configuration({'System': {'value': '300000000'}})
            self.assertEqual('300000000', manager.get_value('System')['value'])
            self.assertEqual(0, manager.current_revision)

            # Test value after applying overrides.
            manager.apply_override(config_overrides_v1)
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])
            self.assertEqual('300000000', manager.get_value('System')['value'])
            self.assertEqual(1, manager.current_revision)

            manager.update_configuration({'System': {'value': '299792458'}})

            manager.apply_override(config_overrides_v2)
            self.assertEqual('e', manager.get_value('Section_1')['name'])
            self.assertEqual('2.7183', manager.get_value('Section_1')['value'])
            self.assertEqual('bar', manager.get_value('Section_2')['foo'])
            self.assertEqual('299792458', manager.get_value('System')['value'])
            self.assertEqual(2, manager.current_revision)

            # Test value after removing overrides.
            manager.remove_override()
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])
            self.assertEqual(1, manager.current_revision)

            manager.update_configuration({'System': {'value': '299792458'}})

            manager.remove_override()
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertEqual(None, manager.get_value('Section_2'))
            self.assertEqual(0, manager.current_revision)

            manager.update_configuration({'System': {'value': 'N/A'}})
            self.assertEqual('N/A', manager.get_value('System')['value'])
            self.assertEqual(0, manager.current_revision)
Beispiel #13
0
class RabbitmqApp(object):
    """
    Handles installation and configuration of the rabbitmq
    in trove.
    """
    def __init__(self, state_change_wait_time=None):
        """
        Set default status and state_change_wait_time.
        """
        if state_change_wait_time:
            self.state_change_wait_time = state_change_wait_time
        else:
            self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(system.RABBITMQ_CONFIG),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)
        config_value_mappings = {'yes': True, 'no': False, "''": None}
        self._value_converter = StringConverter(config_value_mappings)
        self.configuration_manager = ConfigurationManager(
            system.RABBITMQ_CONFIG,
            system.RABBITMQ_OWNER,
            system.RABBITMQ_OWNER,
            PropertiesCodec(unpack_singletons=False,
                            string_mappings=config_value_mappings),
            requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

        self.admin = self._build_admin_client()
        self.status = RabbitmqStatus(self.admin)

    def _build_admin_client(self):
        api_url = self.get_configuration_property('api_url')
        username = self.get_configuration_property('username')
        password = self.get_configuration_property('requirepass')

        return RabbitmqAdmin(api_url=api_url,
                             username=username,
                             password=password)

    def install_if_needed(self, packages):
        """
        Install rabbitmq if needed to  nothing if it is already installed.
        """
        pass

    def start_db_with_conf_changes(self, config_contents):
        LOG.info(_('Starting rabbitmq with conf changes.'))
        if self.status.is_running:
            msg = 'Cannot start_db_with_conf_changes because status is %s.'
            LOG.debug(msg, self.status)
            raise RuntimeError(msg % self.status)
        LOG.info(_("Initiating config."))
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration()
        self.start_db(True)

    def start_db(self, enable_on_boot=True, update_db=False):
        self.status.start_db_service(system.SERVICE_CANDIDATES,
                                     CONF.state_change_wait_time,
                                     enable_on_boot=enable_on_boot,
                                     update_db=update_db)

    def apply_initial_guestagent_configuration(self):
        """Update guestagent-controlled configuration properties.
        """

        # Hide the 'CONFIG' command from end users by mangling its name.
        self.admin.set_config_command_name(self._mangle_config_command_name())

        self.configuration_manager.apply_system_override({
            'daemonize':
            'yes',
            'protected-mode':
            'no',
            'supervised':
            'systemd',
            'pidfile':
            system.RABBITMQ_PID_FILE,
            'logfile':
            system.RABBITMQ_LOG_FILE,
            'dir':
            system.RABBITMQ_DATA_DIR
        })

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        self.status.stop_db_service(system.SERVICE_CANDIDATES,
                                    self.state_change_wait_time,
                                    disable_on_boot=do_not_start_on_reboot,
                                    update_db=update_db)

    def restart(self):
        self.status.restart_db_service(system.SERVICE_CANDIDATES,
                                       self.state_change_wait_time)

    def get_config_command_name(self):
        """Get current name of the 'CONFIG' command.
        """
        renamed_cmds = self.configuration_manager.get_value('rename-command')
        for name_pair in renamed_cmds:
            if name_pair[0] == 'CONFIG':
                return name_pair[1]

        return None

    def _mangle_config_command_name(self):
        """Hide the 'CONFIG' command from the clients by renaming it to a
        random string known only to the guestagent.
        Return the mangled name.
        """
        mangled = utils.generate_random_password()
        self._rename_command('CONFIG', mangled)
        return mangled

    def _rename_command(self, old_name, new_name):
        """It is possible to completely disable a command by renaming it
        to an empty string.
        """
        self.configuration_manager.apply_system_override(
            {'rename-command': [old_name, new_name]})

    def update_overrides(self, overrides):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def get_configuration_property(self, name, default=None):
        """Return the value of a Rabbitmq configuration property.
        Returns a single value for single-argument properties or
        a list otherwise.
        """
        return utils.unpack_singleton(
            self.configuration_manager.get_value(name, default))

    def is_cluster_enabled(self):
        pass

    def enable_cluster(self):
        pass

    def get_cluster_config_filename(self):
        pass

    def cluster_addslots(self):
        pass

    def get_node_ip(self):
        pass

    def get_node_id_for_removal(self):
        pass

    def remove_node(self, node_ids):
        pass
Beispiel #14
0
class RedisApp(object):
    """
    Handles installation and configuration of redis
    on a trove instance.
    """

    @classmethod
    def _init_overrides_dir(cls):
        """Initialize a directory for configuration overrides.
        """
        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(system.REDIS_CONFIG),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)

        if not os.path.exists(revision_dir):
            operating_system.create_directory(
                revision_dir,
                user=system.REDIS_OWNER, group=system.REDIS_OWNER,
                force=True, as_root=True)

        return revision_dir

    def __init__(self, state_change_wait_time=None):
        """
        Sets default status and state_change_wait_time
        """
        if state_change_wait_time:
            self.state_change_wait_time = state_change_wait_time
        else:
            self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = self._init_overrides_dir()
        config_value_mappings = {'yes': True, 'no': False, "''": None}
        self._value_converter = StringConverter(config_value_mappings)
        self.configuration_manager = ConfigurationManager(
            system.REDIS_CONFIG,
            system.REDIS_OWNER, system.REDIS_OWNER,
            PropertiesCodec(
                unpack_singletons=False,
                string_mappings=config_value_mappings
            ), requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

        self.admin = self._build_admin_client()
        self.status = RedisAppStatus(self.admin)

    def _build_admin_client(self):
        password = self.get_configuration_property('requirepass')
        socket = self.get_configuration_property('unixsocket')

        return RedisAdmin(password=password, unix_socket_path=socket)

    def install_if_needed(self, packages):
        """
        Install redis if needed do nothing if it is already installed.
        """
        LOG.info(_('Preparing Guest as Redis Server.'))
        if not packager.pkg_is_installed(packages):
            LOG.info(_('Installing Redis.'))
            self._install_redis(packages)
        LOG.info(_('Redis installed completely.'))

    def complete_install_or_restart(self):
        """
        finalize status updates for install or restart.
        """
        LOG.debug("Complete install or restart called.")
        self.status.end_install_or_restart()

    def _install_redis(self, packages):
        """
        Install the redis server.
        """
        LOG.debug('Installing redis server.')
        msg = "Creating %s." % system.REDIS_CONF_DIR
        LOG.debug(msg)
        operating_system.create_directory(system.REDIS_CONF_DIR, as_root=True)
        pkg_opts = {}
        packager.pkg_install(packages, pkg_opts, TIME_OUT)
        self.start_redis()
        LOG.debug('Finished installing redis server.')

    def _enable_redis_on_boot(self):
        """
        Enables redis on boot.
        """
        LOG.info(_('Enabling Redis on boot.'))
        operating_system.enable_service_on_boot(system.SERVICE_CANDIDATES)

    def _disable_redis_on_boot(self):
        """
        Disables redis on boot.
        """
        LOG.info(_("Disabling Redis on boot."))
        operating_system.disable_service_on_boot(system.SERVICE_CANDIDATES)

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        """
        Stops the redis application on the trove instance.
        """
        LOG.info(_('Stopping redis.'))
        if do_not_start_on_reboot:
            self._disable_redis_on_boot()

        operating_system.stop_service(system.SERVICE_CANDIDATES)
        if not self.status.wait_for_real_status_to_change_to(
                rd_instance.ServiceStatuses.SHUTDOWN,
                self.state_change_wait_time, update_db):
            LOG.error(_('Could not stop Redis.'))
            self.status.end_install_or_restart()

    def restart(self):
        """
        Restarts the redis daemon.
        """
        LOG.debug("Restarting Redis daemon.")
        try:
            self.status.begin_restart()
            self.stop_db()
            self.start_redis()
        finally:
            self.status.end_install_or_restart()

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def apply_overrides(self, client, overrides):
        """Use the 'CONFIG SET' command to apply configuration at runtime.

        Commands that appear multiple times have values separated by a
        white space. For instance, the following two 'save' directives from the
        configuration file...

            save 900 1
            save 300 10

        ... would be applied in a single command as:

            CONFIG SET save "900 1 300 10"

        Note that the 'CONFIG' command has been renamed to prevent
        users from using it to bypass configuration groups.
        """
        for prop_name, prop_args in overrides.items():
            args_string = self._join_lists(
                self._value_converter.to_strings(prop_args), ' ')
            client.config_set(prop_name, args_string)

    def _join_lists(self, items, sep):
        """Join list items (including items from sub-lists) into a string.
        Non-list inputs are returned unchanged.

        _join_lists('1234', ' ') = "1234"
        _join_lists(['1','2','3','4'], ' ') = "1 2 3 4"
        _join_lists([['1','2'], ['3','4']], ' ') = "1 2 3 4"
        """
        if isinstance(items, list):
            return sep.join([sep.join(e) if isinstance(e, list) else e
                             for e in items])
        return items

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def start_db_with_conf_changes(self, config_contents):
        LOG.info(_('Starting redis with conf changes.'))
        if self.status.is_running:
            format = 'Cannot start_db_with_conf_changes because status is %s.'
            LOG.debug(format, self.status)
            raise RuntimeError(format % self.status)
        LOG.info(_("Initiating config."))
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration()
        self.start_redis(True)

    def reset_configuration(self, configuration):
        LOG.info(_("Resetting configuration."))
        config_contents = configuration['config_contents']
        self.configuration_manager.save_configuration(config_contents)

    def start_redis(self, update_db=False):
        """
        Start the redis daemon.
        """
        LOG.info(_("Starting redis."))
        self._enable_redis_on_boot()
        operating_system.start_service(system.SERVICE_CANDIDATES)
        if not self.status.wait_for_real_status_to_change_to(
                rd_instance.ServiceStatuses.RUNNING,
                self.state_change_wait_time, update_db):
            LOG.error(_("Start up of redis failed."))
            try:
                utils.execute_with_timeout('pkill', '-9',
                                           'redis-server',
                                           run_as_root=True,
                                           root_helper='sudo')
            except exception.ProcessExecutionError:
                LOG.exception(_('Error killing stalled redis start command.'))
            self.status.end_install_or_restart()

    def apply_initial_guestagent_configuration(self):
        """Update guestagent-controlled configuration properties.
        """

        # Hide the 'CONFIG' command from end users by mangling its name.
        self.admin.set_config_command_name(self._mangle_config_command_name())

        self.configuration_manager.apply_system_override(
            {'daemonize': 'yes',
             'pidfile': system.REDIS_PID_FILE,
             'logfile': system.REDIS_LOG_FILE,
             'dir': system.REDIS_DATA_DIR})

    def get_config_command_name(self):
        """Get current name of the 'CONFIG' command.
        """
        renamed_cmds = self.configuration_manager.get_value('rename-command')
        for name_pair in renamed_cmds:
            if name_pair[0] == 'CONFIG':
                return name_pair[1]

        return None

    def _mangle_config_command_name(self):
        """Hide the 'CONFIG' command from the clients by renaming it to a
        random string known only to the guestagent.
        Return the mangled name.
        """
        mangled = utils.generate_random_password()
        self._rename_command('CONFIG', mangled)
        return mangled

    def _rename_command(self, old_name, new_name):
        """It is possible to completely disable a command by renaming it
        to an empty string.
        """
        self.configuration_manager.apply_system_override(
            {'rename-command': [old_name, new_name]})

    def get_logfile(self):
        """Specify the log file name. Also the empty string can be used to
        force Redis to log on the standard output.
        Note that if you use standard output for logging but daemonize,
        logs will be sent to /dev/null
        """
        return self.get_configuration_property('logfile')

    def get_db_filename(self):
        """The filename where to dump the DB.
        """
        return self.get_configuration_property('dbfilename')

    def get_working_dir(self):
        """The DB will be written inside this directory,
        with the filename specified the 'dbfilename' configuration directive.
        The Append Only File will also be created inside this directory.
        """
        return self.get_configuration_property('dir')

    def get_auth_password(self):
        """Client authentication password for this instance or None if not set.
        """
        return self.get_configuration_property('requirepass')

    def is_appendonly_enabled(self):
        """True if the Append Only File (AOF) persistence mode is enabled.
        """
        return self.get_configuration_property('appendonly', False)

    def get_append_file_name(self):
        """The name of the append only file (AOF).
        """
        return self.get_configuration_property('appendfilename')

    def is_cluster_enabled(self):
        """Only nodes that are started as cluster nodes can be part of a
        Redis Cluster.
        """
        return self.get_configuration_property('cluster-enabled', False)

    def enable_cluster(self):
        """In order to start a Redis instance as a cluster node enable the
        cluster support
        """
        self.configuration_manager.apply_system_override(
            {'cluster-enabled': 'yes'}, CLUSTER_CFG)

    def get_cluster_config_filename(self):
        """Cluster node configuration file.
        """
        return self.get_configuration_property('cluster-config-file')

    def set_cluster_config_filename(self, name):
        """Make sure that instances running in the same system do not have
        overlapping cluster configuration file names.
        """
        self.configuration_manager.apply_system_override(
            {'cluster-config-file': name}, CLUSTER_CFG)

    def get_cluster_node_timeout(self):
        """Cluster node timeout is the amount of milliseconds a node must be
        unreachable for it to be considered in failure state.
        """
        return self.get_configuration_property('cluster-node-timeout')

    def get_configuration_property(self, name, default=None):
        """Return the value of a Redis configuration property.
        Returns a single value for single-argument properties or
        a list otherwise.
        """
        return utils.unpack_singleton(
            self.configuration_manager.get_value(name, default))
    def _assert_get_value(self, override_strategy):
        base_config_contents = {'Section_1': {'name': 'pi',
                                              'is_number': 'True',
                                              'value': '3.1415'}
                                }

        config_overrides_v1a = {'Section_1': {'name': 'sqrt(2)',
                                              'value': '1.4142'}
                                }

        config_overrides_v2 = {'Section_1': {'name': 'e',
                                             'value': '2.7183'},
                               'Section_2': {'foo': 'bar'}
                               }

        config_overrides_v1b = {'Section_1': {'name': 'sqrt(4)',
                                              'value': '2.0'}
                                }

        codec = IniCodec()
        current_user = getpass.getuser()

        with tempfile.NamedTemporaryFile() as base_config:

            # Write initial config contents.
            operating_system.write_file(
                base_config.name, base_config_contents, codec)

            manager = ConfigurationManager(
                base_config.name, current_user, current_user, codec,
                requires_root=False, override_strategy=override_strategy)

            # Test default value.
            self.assertIsNone(manager.get_value('Section_2'))
            self.assertEqual('foo', manager.get_value('Section_2', 'foo'))

            # Test value before applying overrides.
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])

            # Test value after applying overrides.
            manager.apply_user_override(config_overrides_v1a, change_id='id1')
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])
            manager.apply_user_override(config_overrides_v2, change_id='id2')
            self.assertEqual('e', manager.get_value('Section_1')['name'])
            self.assertEqual('2.7183', manager.get_value('Section_1')['value'])
            self.assertEqual('bar', manager.get_value('Section_2')['foo'])

            # Editing change 'id1' become visible only after removing
            # change 'id2', which overrides 'id1'.
            manager.apply_user_override(config_overrides_v1b, change_id='id1')
            self.assertEqual('e', manager.get_value('Section_1')['name'])
            self.assertEqual('2.7183', manager.get_value('Section_1')['value'])

            # Test value after removing overrides.

            # The edited values from change 'id1' should be visible after
            # removing 'id2'.
            manager.remove_user_override(change_id='id2')
            self.assertEqual('sqrt(4)', manager.get_value('Section_1')['name'])
            self.assertEqual('2.0', manager.get_value('Section_1')['value'])

            # Back to the base.
            manager.remove_user_override(change_id='id1')
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertIsNone(manager.get_value('Section_2'))

            # Test system overrides.
            manager.apply_system_override(
                config_overrides_v1b, change_id='id1')
            self.assertEqual('sqrt(4)', manager.get_value('Section_1')['name'])
            self.assertEqual('2.0', manager.get_value('Section_1')['value'])

            # The system values should take precedence over the user
            # override.
            manager.apply_user_override(
                config_overrides_v1a, change_id='id1')
            self.assertEqual('sqrt(4)', manager.get_value('Section_1')['name'])
            self.assertEqual('2.0', manager.get_value('Section_1')['value'])

            # The user values should become visible only after removing the
            # system change.
            manager.remove_system_override(change_id='id1')
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])

            # Back to the base.
            manager.remove_user_override(change_id='id1')
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertIsNone(manager.get_value('Section_2'))
Beispiel #16
0
class RedisApp(object):
    """
    Handles installation and configuration of redis
    on a trove instance.
    """
    def __init__(self, state_change_wait_time=None):
        """
        Sets default status and state_change_wait_time
        """
        if state_change_wait_time:
            self.state_change_wait_time = state_change_wait_time
        else:
            self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(system.REDIS_CONFIG),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)
        config_value_mappings = {'yes': True, 'no': False, "''": None}
        self._value_converter = StringConverter(config_value_mappings)
        self.configuration_manager = ConfigurationManager(
            system.REDIS_CONFIG,
            system.REDIS_OWNER,
            system.REDIS_OWNER,
            PropertiesCodec(unpack_singletons=False,
                            string_mappings=config_value_mappings),
            requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

        self.admin = self._build_admin_client()
        self.status = RedisAppStatus(self.admin)

    def _build_admin_client(self):
        password = self.get_configuration_property('requirepass')
        socket = self.get_configuration_property('unixsocket')
        cmd = self.get_config_command_name()

        return RedisAdmin(password=password,
                          unix_socket_path=socket,
                          config_cmd=cmd)

    def _refresh_admin_client(self):
        self.admin = self._build_admin_client()
        self.status.set_client(self.admin)
        return self.admin

    def install_if_needed(self, packages):
        """
        Install redis if needed do nothing if it is already installed.
        """
        LOG.info('Preparing Guest as Redis Server.')
        if not packager.pkg_is_installed(packages):
            LOG.info('Installing Redis.')
            self._install_redis(packages)
        LOG.info('Redis installed completely.')

    def _install_redis(self, packages):
        """
        Install the redis server.
        """
        LOG.debug('Installing redis server.')
        LOG.debug("Creating %s.", system.REDIS_CONF_DIR)
        operating_system.create_directory(system.REDIS_CONF_DIR, as_root=True)
        pkg_opts = {}
        packager.pkg_install(packages, pkg_opts, TIME_OUT)
        self.start_db()
        LOG.debug('Finished installing redis server.')

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        self.status.stop_db_service(system.SERVICE_CANDIDATES,
                                    self.state_change_wait_time,
                                    disable_on_boot=do_not_start_on_reboot,
                                    update_db=update_db)

    def restart(self):
        self.status.restart_db_service(system.SERVICE_CANDIDATES,
                                       self.state_change_wait_time)

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)
            # apply requirepass at runtime
            # TODO(zhaochao): updating 'requirepass' here will be removed
            # in the future releases, Redis only use enable_root/disable_root
            # to set this parameter.
            if 'requirepass' in overrides:
                self.admin.config_set('requirepass', overrides['requirepass'])
                self._refresh_admin_client()

    def apply_overrides(self, client, overrides):
        """Use the 'CONFIG SET' command to apply configuration at runtime.

        Commands that appear multiple times have values separated by a
        white space. For instance, the following two 'save' directives from the
        configuration file...

            save 900 1
            save 300 10

        ... would be applied in a single command as:

            CONFIG SET save "900 1 300 10"

        Note that the 'CONFIG' command has been renamed to prevent
        users from using it to bypass configuration groups.
        """
        for prop_name, prop_args in overrides.items():
            args_string = self._join_lists(
                self._value_converter.to_strings(prop_args), ' ')
            client.config_set(prop_name, args_string)
            # NOTE(zhaochao): requirepass applied in update_overrides is
            # only kept for back compatibility. Now requirepass is set
            # via enable_root/disable_root, Redis admin client should be
            # refreshed here.
            if prop_name == "requirepass":
                client = self._refresh_admin_client()

    def _join_lists(self, items, sep):
        """Join list items (including items from sub-lists) into a string.
        Non-list inputs are returned unchanged.

        _join_lists('1234', ' ') = "1234"
        _join_lists(['1','2','3','4'], ' ') = "1 2 3 4"
        _join_lists([['1','2'], ['3','4']], ' ') = "1 2 3 4"
        """
        if isinstance(items, list):
            return sep.join(
                [sep.join(e) if isinstance(e, list) else e for e in items])
        return items

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def make_read_only(self, read_only):
        # Redis has no mechanism to make an instance read-only at present
        pass

    def start_db_with_conf_changes(self, config_contents):
        LOG.info('Starting redis with conf changes.')
        if self.status.is_running:
            format = 'Cannot start_db_with_conf_changes because status is %s.'
            LOG.debug(format, self.status)
            raise RuntimeError(format % self.status)
        LOG.info("Initiating config.")
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration()
        self.start_db(True)

    def start_db(self, update_db=False):
        self.status.start_db_service(system.SERVICE_CANDIDATES,
                                     self.state_change_wait_time,
                                     enable_on_boot=True,
                                     update_db=update_db)

    def apply_initial_guestagent_configuration(self):
        """Update guestagent-controlled configuration properties.
        """

        # Hide the 'CONFIG' command from end users by mangling its name.
        self.admin.set_config_command_name(self._mangle_config_command_name())

        self.configuration_manager.apply_system_override({
            'daemonize':
            'yes',
            'protected-mode':
            'no',
            'supervised':
            'systemd',
            'pidfile':
            system.REDIS_PID_FILE,
            'logfile':
            system.REDIS_LOG_FILE,
            'dir':
            system.REDIS_DATA_DIR
        })

    def get_config_command_name(self):
        """Get current name of the 'CONFIG' command.
        """
        renamed_cmds = self.configuration_manager.get_value('rename-command')
        if renamed_cmds:
            for name_pair in renamed_cmds:
                if name_pair[0] == 'CONFIG':
                    return name_pair[1]

        return None

    def _mangle_config_command_name(self):
        """Hide the 'CONFIG' command from the clients by renaming it to a
        random string known only to the guestagent.
        Return the mangled name.
        """
        mangled = utils.generate_random_password()
        self._rename_command('CONFIG', mangled)
        return mangled

    def _rename_command(self, old_name, new_name):
        """It is possible to completely disable a command by renaming it
        to an empty string.
        """
        self.configuration_manager.apply_system_override(
            {'rename-command': [old_name, new_name]})

    def get_logfile(self):
        """Specify the log file name. Also the empty string can be used to
        force Redis to log on the standard output.
        Note that if you use standard output for logging but daemonize,
        logs will be sent to /dev/null
        """
        return self.get_configuration_property('logfile')

    def get_db_filename(self):
        """The filename where to dump the DB.
        """
        return self.get_configuration_property('dbfilename')

    def get_working_dir(self):
        """The DB will be written inside this directory,
        with the filename specified the 'dbfilename' configuration directive.
        The Append Only File will also be created inside this directory.
        """
        return self.get_configuration_property('dir')

    def get_persistence_filepath(self):
        """Returns the full path to the persistence file."""
        return guestagent_utils.build_file_path(self.get_working_dir(),
                                                self.get_db_filename())

    def get_port(self):
        """Port for this instance or default if not set."""
        return self.get_configuration_property('port', system.REDIS_PORT)

    def get_auth_password(self):
        """Client authentication password for this instance or None if not set.
        """
        return self.get_configuration_property('requirepass')

    def is_appendonly_enabled(self):
        """True if the Append Only File (AOF) persistence mode is enabled.
        """
        return self.get_configuration_property('appendonly', False)

    def get_append_file_name(self):
        """The name of the append only file (AOF).
        """
        return self.get_configuration_property('appendfilename')

    def is_cluster_enabled(self):
        """Only nodes that are started as cluster nodes can be part of a
        Redis Cluster.
        """
        return self.get_configuration_property('cluster-enabled', False)

    def enable_cluster(self):
        """In order to start a Redis instance as a cluster node enable the
        cluster support
        """
        self.configuration_manager.apply_system_override(
            {'cluster-enabled': 'yes'}, CLUSTER_CFG)

    def get_cluster_config_filename(self):
        """Cluster node configuration file.
        """
        return self.get_configuration_property('cluster-config-file')

    def set_cluster_config_filename(self, name):
        """Make sure that instances running in the same system do not have
        overlapping cluster configuration file names.
        """
        self.configuration_manager.apply_system_override(
            {'cluster-config-file': name}, CLUSTER_CFG)

    def get_cluster_node_timeout(self):
        """Cluster node timeout is the amount of milliseconds a node must be
        unreachable for it to be considered in failure state.
        """
        return self.get_configuration_property('cluster-node-timeout')

    def get_configuration_property(self, name, default=None):
        """Return the value of a Redis configuration property.
        Returns a single value for single-argument properties or
        a list otherwise.
        """
        return utils.unpack_singleton(
            self.configuration_manager.get_value(name, default))

    def cluster_meet(self, ip, port):
        try:
            utils.execute_with_timeout('redis-cli', 'cluster', 'meet', ip,
                                       port)
        except exception.ProcessExecutionError:
            LOG.exception('Error joining node to cluster at %s.', ip)
            raise

    def cluster_addslots(self, first_slot, last_slot):
        try:
            group_size = 200
            # Create list of slots represented in strings
            # eg. ['10', '11', '12', '13']
            slots = list(map(str, range(first_slot, last_slot + 1)))
            while slots:
                cmd = (['redis-cli', 'cluster', 'addslots'] +
                       slots[0:group_size])
                out, err = utils.execute_with_timeout(*cmd,
                                                      run_as_root=True,
                                                      root_helper='sudo')
                if 'OK' not in out:
                    raise RuntimeError(_('Error executing addslots: %s') % out)
                del slots[0:group_size]
        except exception.ProcessExecutionError:
            LOG.exception(
                'Error adding slots %(first_slot)s-%(last_slot)s'
                ' to cluster.', {
                    'first_slot': first_slot,
                    'last_slot': last_slot
                })
            raise

    def _get_node_info(self):
        try:
            out, _ = utils.execute_with_timeout('redis-cli', '--csv',
                                                'cluster', 'nodes')
            return [line.split(' ') for line in out.splitlines()]
        except exception.ProcessExecutionError:
            LOG.exception('Error getting node info.')
            raise

    def _get_node_details(self):
        for node_details in self._get_node_info():
            if 'myself' in node_details[2]:
                return node_details
        raise exception.TroveError(_("Unable to determine node details"))

    def get_node_ip(self):
        """Returns [ip, port] where both values are strings"""
        return self._get_node_details()[1].split(':')

    def get_node_id_for_removal(self):
        node_details = self._get_node_details()
        node_id = node_details[0]
        my_ip = node_details[1].split(':')[0]
        try:
            slots, _ = utils.execute_with_timeout('redis-cli', '--csv',
                                                  'cluster', 'slots')
            return node_id if my_ip not in slots else None
        except exception.ProcessExecutionError:
            LOG.exception('Error validating node to for removal.')
            raise

    def remove_nodes(self, node_ids):
        try:
            for node_id in node_ids:
                utils.execute_with_timeout('redis-cli', 'cluster', 'forget',
                                           node_id)
        except exception.ProcessExecutionError:
            LOG.exception('Error removing node from cluster.')
            raise

    def enable_root(self, password=None):
        if not password:
            password = utils.generate_random_password()
        redis_password = RedisRootUser(password=password)
        try:
            self.configuration_manager.apply_system_override(
                {
                    'requirepass': password,
                    'masterauth': password
                },
                change_id=SYS_OVERRIDES_AUTH)
            self.apply_overrides(self.admin, {
                'requirepass': password,
                'masterauth': password
            })
        except exception.TroveError:
            LOG.exception('Error enabling authentication for instance.')
            raise
        return redis_password.serialize()

    def disable_root(self):
        try:
            self.configuration_manager.remove_system_override(
                change_id=SYS_OVERRIDES_AUTH)
            self.apply_overrides(self.admin, {
                'requirepass': '',
                'masterauth': ''
            })
        except exception.TroveError:
            LOG.exception('Error disabling authentication for instance.')
            raise
Beispiel #17
0
    def _assert_update_configuration(self, override_strategy):
        base_config_contents = {'Section_1': {'name': 'pi',
                                              'is_number': 'True',
                                              'value': '3.1415'}
                                }

        config_overrides_v1 = {'Section_1': {'name': 'sqrt(2)',
                                             'value': '1.4142'}
                               }

        config_overrides_v2 = {'Section_1': {'name': 'e',
                                             'value': '2.7183'},
                               'Section_2': {'foo': 'bar'}
                               }

        codec = IniCodec()
        current_user = getpass.getuser()

        with tempfile.NamedTemporaryFile() as base_config:

            # Write initial config contents.
            operating_system.write_file(
                base_config.name, base_config_contents, codec)

            manager = ConfigurationManager(
                base_config.name, current_user, current_user, codec,
                requires_root=False)

            manager.update_configuration({'System': {'name': 'c',
                                                     'is_number': 'True',
                                                     'value': 'N/A'}})

            manager.set_override_strategy(override_strategy, 2)

            # Test value before applying overrides.
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertEqual('N/A', manager.get_value('System')['value'])
            self.assertEqual(0, manager.current_revision)

            manager.update_configuration({'System': {'value': '300000000'}})
            self.assertEqual('300000000', manager.get_value('System')['value'])
            self.assertEqual(0, manager.current_revision)

            # Test value after applying overrides.
            manager.apply_override(config_overrides_v1)
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])
            self.assertEqual('300000000', manager.get_value('System')['value'])
            self.assertEqual(1, manager.current_revision)

            manager.update_configuration({'System': {'value': '299792458'}})

            manager.apply_override(config_overrides_v2)
            self.assertEqual('e', manager.get_value('Section_1')['name'])
            self.assertEqual('2.7183', manager.get_value('Section_1')['value'])
            self.assertEqual('bar', manager.get_value('Section_2')['foo'])
            self.assertEqual('299792458', manager.get_value('System')['value'])
            self.assertEqual(2, manager.current_revision)

            # Test value after removing overrides.
            manager.remove_override()
            self.assertEqual('sqrt(2)', manager.get_value('Section_1')['name'])
            self.assertEqual('1.4142', manager.get_value('Section_1')['value'])
            self.assertEqual(1, manager.current_revision)

            manager.update_configuration({'System': {'value': '299792458'}})

            manager.remove_override()
            self.assertEqual('pi', manager.get_value('Section_1')['name'])
            self.assertEqual('3.1415', manager.get_value('Section_1')['value'])
            self.assertEqual(None, manager.get_value('Section_2'))
            self.assertEqual(0, manager.current_revision)

            manager.update_configuration({'System': {'value': 'N/A'}})
            self.assertEqual('N/A', manager.get_value('System')['value'])
            self.assertEqual(0, manager.current_revision)
Beispiel #18
0
class MongoDBApp(object):
    """Prepares DBaaS on a Guest container."""

    def __init__(self):
        self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(CONFIG_FILE),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)
        self.configuration_manager = ConfigurationManager(
            CONFIG_FILE, system.MONGO_USER, system.MONGO_USER,
            SafeYamlCodec(default_flow_style=False),
            requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

        self.is_query_router = False
        self.is_cluster_member = False
        self.status = MongoDBAppStatus()

    def install_if_needed(self, packages):
        """Prepare the guest machine with a MongoDB installation."""
        LOG.info(_("Preparing Guest as MongoDB."))
        if not system.PACKAGER.pkg_is_installed(packages):
            LOG.debug("Installing packages: %s." % str(packages))
            system.PACKAGER.pkg_install(packages, {}, system.TIME_OUT)
        LOG.info(_("Finished installing MongoDB server."))

    def _get_service_candidates(self):
        if self.is_query_router:
            return system.MONGOS_SERVICE_CANDIDATES
        return system.MONGOD_SERVICE_CANDIDATES

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        self.status.stop_db_service(
            self._get_service_candidates(), self.state_change_wait_time,
            disable_on_boot=do_not_start_on_reboot, update_db=update_db)

    def restart(self):
        self.status.restart_db_service(
            self._get_service_candidates(), self.state_change_wait_time)

    def start_db(self, update_db=False):
        self.status.start_db_service(
            self._get_service_candidates(), self.state_change_wait_time,
            enable_on_boot=True, update_db=update_db)

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def start_db_with_conf_changes(self, config_contents):
        LOG.info(_('Starting MongoDB with configuration changes.'))
        if self.status.is_running:
            format = 'Cannot start_db_with_conf_changes because status is %s.'
            LOG.debug(format, self.status)
            raise RuntimeError(format % self.status)
        LOG.info(_("Initiating config."))
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration(
            None, mount_point=system.MONGODB_MOUNT_POINT)
        self.start_db(True)

    def apply_initial_guestagent_configuration(
            self, cluster_config, mount_point=None):
        LOG.debug("Applying initial configuration.")

        # Mongodb init scripts assume the PID-file path is writable by the
        # database service.
        # See: https://jira.mongodb.org/browse/SERVER-20075
        self._initialize_writable_run_dir()

        self.configuration_manager.apply_system_override(
            {'processManagement.fork': False,
             'processManagement.pidFilePath': system.MONGO_PID_FILE,
             'systemLog.destination': 'file',
             'systemLog.path': system.MONGO_LOG_FILE,
             'systemLog.logAppend': True
             })

        if mount_point:
            self.configuration_manager.apply_system_override(
                {'storage.dbPath': mount_point})

        if cluster_config is not None:
            self._configure_as_cluster_instance(cluster_config)
        else:
            self._configure_network(MONGODB_PORT)

    def _initialize_writable_run_dir(self):
        """Create a writable directory for Mongodb's runtime data
        (e.g. PID-file).
        """
        mongodb_run_dir = os.path.dirname(system.MONGO_PID_FILE)
        LOG.debug("Initializing a runtime directory: %s" % mongodb_run_dir)
        operating_system.create_directory(
            mongodb_run_dir, user=system.MONGO_USER, group=system.MONGO_USER,
            force=True, as_root=True)

    def _configure_as_cluster_instance(self, cluster_config):
        """Configure this guest as a cluster instance and return its
        new status.
        """
        if cluster_config['instance_type'] == "query_router":
            self._configure_as_query_router()
        elif cluster_config["instance_type"] == "config_server":
            self._configure_as_config_server()
        elif cluster_config["instance_type"] == "member":
            self._configure_as_cluster_member(
                cluster_config['replica_set_name'])
        else:
            LOG.error(_("Bad cluster configuration; instance type "
                        "given as %s.") % cluster_config['instance_type'])
            return ds_instance.ServiceStatuses.FAILED

        if 'key' in cluster_config:
            self._configure_cluster_security(cluster_config['key'])

    def _configure_as_query_router(self):
        LOG.info(_("Configuring instance as a cluster query router."))
        self.is_query_router = True

        # FIXME(pmalik): We should really have a separate configuration
        # template for the 'mongos' process.
        # Remove all storage configurations from the template.
        # They apply only to 'mongod' processes.
        # Already applied overrides will be integrated into the base file and
        # their current groups removed.
        config = guestagent_utils.expand_dict(
            self.configuration_manager.parse_configuration())
        if 'storage' in config:
            LOG.debug("Removing 'storage' directives from the configuration "
                      "template.")
            del config['storage']
            self.configuration_manager.save_configuration(
                guestagent_utils.flatten_dict(config))

        # Apply 'mongos' configuration.
        self._configure_network(MONGODB_PORT)
        self.configuration_manager.apply_system_override(
            {'sharding.configDB': ''}, CNF_CLUSTER)

    def _configure_as_config_server(self):
        LOG.info(_("Configuring instance as a cluster config server."))
        self._configure_network(CONFIGSVR_PORT)
        self.configuration_manager.apply_system_override(
            {'sharding.clusterRole': 'configsvr'}, CNF_CLUSTER)

    def _configure_as_cluster_member(self, replica_set_name):
        LOG.info(_("Configuring instance as a cluster member."))
        self.is_cluster_member = True
        self._configure_network(MONGODB_PORT)
        # we don't want these thinking they are in a replica set yet
        # as that would prevent us from creating the admin user,
        # so start mongo before updating the config.
        # mongo will be started by the cluster taskmanager
        self.start_db()
        self.configuration_manager.apply_system_override(
            {'replication.replSetName': replica_set_name}, CNF_CLUSTER)

    def _configure_cluster_security(self, key_value):
        """Force cluster key-file-based authentication.

        This will enabled RBAC.
        """
        # Store the cluster member authentication key.
        self.store_key(key_value)

        self.configuration_manager.apply_system_override(
            {'security.clusterAuthMode': 'keyFile',
             'security.keyFile': self.get_key_file()}, CNF_CLUSTER)

    def _configure_network(self, port=None):
        """Make the service accessible at a given (or default if not) port.
        """
        instance_ip = netutils.get_my_ipv4()
        bind_interfaces_string = ','.join([instance_ip, '127.0.0.1'])
        options = {'net.bindIp': bind_interfaces_string}
        if port is not None:
            guestagent_utils.update_dict({'net.port': port}, options)

        self.configuration_manager.apply_system_override(options)
        self.status.set_host(instance_ip, port=port)

    def clear_storage(self):
        mount_point = "/var/lib/mongodb/*"
        LOG.debug("Clearing storage at %s." % mount_point)
        try:
            operating_system.remove(mount_point, force=True, as_root=True)
        except exception.ProcessExecutionError:
            LOG.exception(_("Error clearing storage."))

    def _has_config_db(self):
        value_string = self.configuration_manager.get_value(
            'sharding', {}).get('configDB')

        return value_string is not None

    # FIXME(pmalik): This method should really be called 'set_config_servers'.
    # The current name suggests it adds more config servers, but it
    # rather replaces the existing ones.
    def add_config_servers(self, config_server_hosts):
        """Set config servers on a query router (mongos) instance.
        """
        config_servers_string = ','.join(['%s:%s' % (host, CONFIGSVR_PORT)
                                          for host in config_server_hosts])
        LOG.info(_("Setting config servers: %s") % config_servers_string)
        self.configuration_manager.apply_system_override(
            {'sharding.configDB': config_servers_string}, CNF_CLUSTER)
        self.start_db(True)

    def add_shard(self, replica_set_name, replica_set_member):
        """
        This method is used by query router (mongos) instances.
        """
        url = "%(rs)s/%(host)s:%(port)s"\
              % {'rs': replica_set_name,
                 'host': replica_set_member,
                 'port': MONGODB_PORT}
        MongoDBAdmin().add_shard(url)

    def add_members(self, members):
        """
        This method is used by a replica-set member instance.
        """
        def check_initiate_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()

            if((status["ok"] == 1) and
               (status["members"][0]["stateStr"] == "PRIMARY") and
               (status["myState"] == 1)):
                return True
            else:
                return False

        def check_rs_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()
            primary_count = 0

            if status["ok"] != 1:
                return False
            if len(status["members"]) != (len(members) + 1):
                return False
            for rs_member in status["members"]:
                if rs_member["state"] not in [1, 2, 7]:
                    return False
                if rs_member["health"] != 1:
                    return False
                if rs_member["state"] == 1:
                    primary_count += 1

            return primary_count == 1

        MongoDBAdmin().rs_initiate()
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_initiate_status, sleep_time=30, time_out=100)

        # add replica-set members
        MongoDBAdmin().rs_add_members(members)
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_rs_status, sleep_time=10, time_out=100)

    def _set_localhost_auth_bypass(self, enabled):
        """When active, the localhost exception allows connections from the
        localhost interface to create the first user on the admin database.
        The exception applies only when there are no users created in the
        MongoDB instance.
        """
        self.configuration_manager.apply_system_override(
            {'setParameter': {'enableLocalhostAuthBypass': enabled}})

    def list_all_dbs(self):
        return MongoDBAdmin().list_database_names()

    def db_data_size(self, db_name):
        schema = models.MongoDBSchema(db_name)
        return MongoDBAdmin().db_stats(schema.serialize())['dataSize']

    def admin_cmd_auth_params(self):
        return MongoDBAdmin().cmd_admin_auth_params

    def get_key_file(self):
        return system.MONGO_KEY_FILE

    def get_key(self):
        return operating_system.read_file(
            system.MONGO_KEY_FILE, as_root=True).rstrip()

    def store_key(self, key):
        """Store the cluster key."""
        LOG.debug('Storing key for MongoDB cluster.')
        operating_system.write_file(system.MONGO_KEY_FILE, key, as_root=True)
        operating_system.chmod(system.MONGO_KEY_FILE,
                               operating_system.FileMode.SET_USR_RO,
                               as_root=True)
        operating_system.chown(system.MONGO_KEY_FILE,
                               system.MONGO_USER, system.MONGO_USER,
                               as_root=True)

    def store_admin_password(self, password):
        LOG.debug('Storing admin password.')
        creds = MongoDBCredentials(username=system.MONGO_ADMIN_NAME,
                                   password=password)
        creds.write(system.MONGO_ADMIN_CREDS_FILE)
        return creds

    def create_admin_user(self, password):
        """Create the admin user while the localhost exception is active."""
        LOG.debug('Creating the admin user.')
        creds = self.store_admin_password(password)
        user = models.MongoDBUser(name='admin.%s' % creds.username,
                                  password=creds.password)
        user.roles = system.MONGO_ADMIN_ROLES
        # the driver engine is already cached, but we need to change it it
        with MongoDBClient(None, host='localhost',
                           port=MONGODB_PORT) as client:
            MongoDBAdmin().create_validated_user(user, client=client)
        # now revert to the normal engine
        self.status.set_host(host=netutils.get_my_ipv4(),
                             port=MONGODB_PORT)
        LOG.debug('Created admin user.')

    def secure(self):
        """Create the Trove admin user.

        The service should not be running at this point.
        This will enable role-based access control (RBAC) by default.
        """
        if self.status.is_running:
            raise RuntimeError(_("Cannot secure the instance. "
                                 "The service is still running."))

        try:
            self.configuration_manager.apply_system_override(
                {'security.authorization': 'enabled'})
            self._set_localhost_auth_bypass(True)
            self.start_db(update_db=False)
            password = utils.generate_random_password()
            self.create_admin_user(password)
            LOG.debug("MongoDB secure complete.")
        finally:
            self._set_localhost_auth_bypass(False)
            self.stop_db()

    def get_configuration_property(self, name, default=None):
        """Return the value of a MongoDB configuration property.
        """
        return self.configuration_manager.get_value(name, default)

    def prep_primary(self):
        # Prepare the primary member of a replica set.
        password = utils.generate_random_password()
        self.create_admin_user(password)
        self.restart()

    @property
    def replica_set_name(self):
        return MongoDBAdmin().get_repl_status()['set']

    @property
    def admin_password(self):
        creds = MongoDBCredentials()
        creds.read(system.MONGO_ADMIN_CREDS_FILE)
        return creds.password

    def is_shard_active(self, replica_set_name):
        shards = MongoDBAdmin().list_active_shards()
        if replica_set_name in [shard['_id'] for shard in shards]:
            LOG.debug('Replica set %s is active.' % replica_set_name)
            return True
        else:
            LOG.debug('Replica set %s is not active.' % replica_set_name)
            return False
Beispiel #19
0
class MongoDBApp(object):
    """Prepares DBaaS on a Guest container."""
    @classmethod
    def _init_overrides_dir(cls):
        """Initialize a directory for configuration overrides.
        """
        revision_dir = guestagent_utils.build_file_path(
            os.path.dirname(system.MONGO_USER),
            ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)

        if not os.path.exists(revision_dir):
            operating_system.create_directory(revision_dir,
                                              user=system.MONGO_USER,
                                              group=system.MONGO_USER,
                                              force=True,
                                              as_root=True)

        return revision_dir

    def __init__(self):
        self.state_change_wait_time = CONF.state_change_wait_time

        revision_dir = self._init_overrides_dir()
        self.configuration_manager = ConfigurationManager(
            CONFIG_FILE,
            system.MONGO_USER,
            system.MONGO_USER,
            SafeYamlCodec(default_flow_style=False),
            requires_root=True,
            override_strategy=OneFileOverrideStrategy(revision_dir))

        self.is_query_router = False
        self.status = MongoDBAppStatus()

    def install_if_needed(self, packages):
        """Prepare the guest machine with a MongoDB installation."""
        LOG.info(_("Preparing Guest as MongoDB."))
        if not system.PACKAGER.pkg_is_installed(packages):
            LOG.debug("Installing packages: %s." % str(packages))
            system.PACKAGER.pkg_install(packages, {}, system.TIME_OUT)
        LOG.info(_("Finished installing MongoDB server."))

    def _get_service(self):
        if self.is_query_router:
            return (operating_system.service_discovery(
                system.MONGOS_SERVICE_CANDIDATES))
        else:
            return (operating_system.service_discovery(
                system.MONGOD_SERVICE_CANDIDATES))

    def _enable_db_on_boot(self):
        LOG.info(_("Enabling MongoDB on boot."))
        try:
            mongo_service = self._get_service()
            utils.execute_with_timeout(mongo_service['cmd_enable'], shell=True)
        except KeyError:
            raise RuntimeError(_("MongoDB service is not discovered."))

    def _disable_db_on_boot(self):
        LOG.info(_("Disabling MongoDB on boot."))
        try:
            mongo_service = self._get_service()
            utils.execute_with_timeout(mongo_service['cmd_disable'],
                                       shell=True)
        except KeyError:
            raise RuntimeError("MongoDB service is not discovered.")

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        LOG.info(_("Stopping MongoDB."))
        if do_not_start_on_reboot:
            self._disable_db_on_boot()

        try:
            mongo_service = self._get_service()
            # TODO(ramashri) see if hardcoded values can be removed
            utils.execute_with_timeout(mongo_service['cmd_stop'],
                                       shell=True,
                                       timeout=100)
        except KeyError:
            raise RuntimeError(_("MongoDB service is not discovered."))

        if not self.status.wait_for_real_status_to_change_to(
                ds_instance.ServiceStatuses.SHUTDOWN,
                self.state_change_wait_time, update_db):
            LOG.error(_("Could not stop MongoDB."))
            self.status.end_install_or_restart()
            raise RuntimeError(_("Could not stop MongoDB"))

    def restart(self):
        LOG.info(_("Restarting MongoDB."))
        try:
            self.status.begin_restart()
            self.stop_db()
            self.start_db()
        finally:
            self.status.end_install_or_restart()

    def start_db(self, update_db=False):
        LOG.info(_("Starting MongoDB."))

        self._enable_db_on_boot()

        try:
            mongo_service = self._get_service()
            utils.execute_with_timeout(mongo_service['cmd_start'], shell=True)
        except ProcessExecutionError:
            pass
        except KeyError:
            raise RuntimeError("MongoDB service is not discovered.")
        self.wait_for_start(update_db=update_db)

    def wait_for_start(self, update_db=False):
        LOG.debug('Waiting for MongoDB to start.')
        if not self.status.wait_for_real_status_to_change_to(
                ds_instance.ServiceStatuses.RUNNING,
                self.state_change_wait_time, update_db):
            LOG.error(_("Start up of MongoDB failed."))
            # If it won't start, but won't die either, kill it by hand so we
            # don't let a rouge process wander around.
            try:
                out, err = utils.execute_with_timeout(system.FIND_PID,
                                                      shell=True)
                pid = "".join(out.split(" ")[1:2])
                utils.execute_with_timeout(system.MONGODB_KILL % pid,
                                           shell=True)
            except exception.ProcessExecutionError:
                LOG.exception(_("Error killing MongoDB start command."))
                # There's nothing more we can do...
            self.status.end_install_or_restart()
            raise RuntimeError("Could not start MongoDB.")
        LOG.debug('MongoDB started successfully.')

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.configuration_manager.apply_user_override(overrides)

    def remove_overrides(self):
        self.configuration_manager.remove_user_override()

    def start_db_with_conf_changes(self, config_contents):
        LOG.info(_('Starting MongoDB with configuration changes.'))
        if self.status.is_running:
            format = 'Cannot start_db_with_conf_changes because status is %s.'
            LOG.debug(format, self.status)
            raise RuntimeError(format % self.status)
        LOG.info(_("Initiating config."))
        self.configuration_manager.save_configuration(config_contents)
        # The configuration template has to be updated with
        # guestagent-controlled settings.
        self.apply_initial_guestagent_configuration(
            None, mount_point=system.MONGODB_MOUNT_POINT)
        self.start_db(True)

    def reset_configuration(self, configuration):
        LOG.info(_("Resetting configuration."))
        config_contents = configuration['config_contents']
        self.configuration_manager.save_configuration(config_contents)

    def apply_initial_guestagent_configuration(self,
                                               cluster_config,
                                               mount_point=None):
        LOG.debug("Applying initial configuration.")

        # todo mvandijk: enable authorization.
        # 'security.authorization': True
        self.configuration_manager.apply_system_override({
            'processManagement.fork':
            False,
            'processManagement.pidFilePath':
            system.MONGO_PID_FILE,
            'systemLog.destination':
            'file',
            'systemLog.path':
            system.MONGO_LOG_FILE,
            'systemLog.logAppend':
            True
        })

        if mount_point:
            self.configuration_manager.apply_system_override(
                {'storage.dbPath': mount_point})

        if cluster_config is not None:
            self._configure_as_cluster_instance(cluster_config)
        else:
            self._configure_network(MONGODB_PORT)

    def _configure_as_cluster_instance(self, cluster_config):
        """Configure this guest as a cluster instance and return its
        new status.
        """
        if cluster_config['instance_type'] == "query_router":
            self._configure_as_query_router()
        elif cluster_config["instance_type"] == "config_server":
            self._configure_as_config_server()
        elif cluster_config["instance_type"] == "member":
            self._configure_as_cluster_member(
                cluster_config['replica_set_name'])
        else:
            LOG.error(
                _("Bad cluster configuration; instance type "
                  "given as %s.") % cluster_config['instance_type'])
            return ds_instance.ServiceStatuses.FAILED

        if 'key' in cluster_config:
            self._configure_cluster_security(cluster_config['key'])

    def _configure_as_query_router(self):
        LOG.info(_("Configuring instance as a cluster query router."))
        self.is_query_router = True

        # Write the 'mongos' upstart script.
        # FIXME(pmalik): The control script should really be written in the
        # elements.
        # The guestagent will choose the right daemon ('mongod' or 'mongos')
        # based on the 'cluster_config' values.
        upstart_contents = (system.MONGOS_UPSTART_CONTENTS.format(
            config_file_placeholder=CONFIG_FILE))
        operating_system.write_file(system.MONGOS_UPSTART,
                                    upstart_contents,
                                    as_root=True)

        # FIXME(pmalik): We should really have a separate configuration
        # template for the 'mongos' process.
        # Remove all storage configurations from the template.
        # They apply only to 'mongod' processes.
        # Already applied overrides will be integrated into the base file and
        # their current groups removed.
        config = guestagent_utils.expand_dict(
            self.configuration_manager.parse_configuration())
        if 'storage' in config:
            LOG.debug("Removing 'storage' directives from the configuration "
                      "template.")
            del config['storage']
            self.configuration_manager.save_configuration(
                guestagent_utils.flatten_dict(config))

        # Apply 'mongos' configuration.
        self._configure_network(MONGODB_PORT)
        self.configuration_manager.apply_system_override(
            {'sharding.configDB': ''}, CNF_CLUSTER)

    def _configure_as_config_server(self):
        LOG.info(_("Configuring instance as a cluster config server."))
        self._configure_network(CONFIGSVR_PORT)
        self.configuration_manager.apply_system_override(
            {'sharding.clusterRole': 'configsvr'}, CNF_CLUSTER)

    def _configure_as_cluster_member(self, replica_set_name):
        LOG.info(_("Configuring instance as a cluster member."))
        self._configure_network(MONGODB_PORT)
        self.configuration_manager.apply_system_override(
            {'replication.replSetName': replica_set_name}, CNF_CLUSTER)

    def _configure_cluster_security(self, key_value):
        """Force cluster key-file-based authentication.
        """
        # Store the cluster member authentication key.
        self.store_key(key_value)

        self.configuration_manager.apply_system_override(
            {
                'security.clusterAuthMode': 'keyFile',
                'security.keyFile': self.get_key_file()
            }, CNF_CLUSTER)

    def _configure_network(self, port=None):
        """Make the service accessible at a given (or default if not) port.
        """
        instance_ip = netutils.get_my_ipv4()
        bind_interfaces_string = ','.join([instance_ip, '127.0.0.1'])
        options = {'net.bindIp': bind_interfaces_string}
        if port is not None:
            guestagent_utils.update_dict({'net.port': port}, options)

        self.configuration_manager.apply_system_override(options)
        self.status.set_host(instance_ip, port=port)

    def clear_storage(self):
        mount_point = "/var/lib/mongodb/*"
        LOG.debug("Clearing storage at %s." % mount_point)
        try:
            operating_system.remove(mount_point, force=True, as_root=True)
        except exception.ProcessExecutionError:
            LOG.exception(_("Error clearing storage."))

    def _has_config_db(self):
        value_string = self.configuration_manager.get_value('sharding',
                                                            {}).get('configDB')

        return value_string is not None

    # FIXME(pmalik): This method should really be called 'set_config_servers'.
    # The current name suggests it adds more config servers, but it
    # rather replaces the existing ones.
    def add_config_servers(self, config_server_hosts):
        """Set config servers on a query router (mongos) instance.
        """
        config_servers_string = ','.join(
            ['%s:27019' % host for host in config_server_hosts])
        LOG.info(_("Setting config servers: %s") % config_servers_string)
        self.configuration_manager.apply_system_override(
            {'sharding.configDB': config_servers_string}, CNF_CLUSTER)
        self.start_db(True)

    def add_shard(self, replica_set_name, replica_set_member):
        """
        This method is used by query router (mongos) instances.
        """
        url = "%(rs)s/%(host)s:%(port)s"\
              % {'rs': replica_set_name,
                 'host': replica_set_member,
                 'port': MONGODB_PORT}
        MongoDBAdmin().add_shard(url)

    def add_members(self, members):
        """
        This method is used by a replica-set member instance.
        """
        def check_initiate_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()

            if ((status["ok"] == 1)
                    and (status["members"][0]["stateStr"] == "PRIMARY")
                    and (status["myState"] == 1)):
                return True
            else:
                return False

        def check_rs_status():
            """
            This method is used to verify replica-set status.
            """
            status = MongoDBAdmin().get_repl_status()
            primary_count = 0

            if status["ok"] != 1:
                return False
            if len(status["members"]) != (len(members) + 1):
                return False
            for rs_member in status["members"]:
                if rs_member["state"] not in [1, 2, 7]:
                    return False
                if rs_member["health"] != 1:
                    return False
                if rs_member["state"] == 1:
                    primary_count += 1

            return primary_count == 1

        # Create the admin user on this member.
        # This is only necessary for setting up the replica set.
        # The query router will handle requests once this set
        # is added as a shard.
        password = utils.generate_random_password()
        self.create_admin_user(password)

        # initiate replica-set
        MongoDBAdmin().rs_initiate()
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_initiate_status, sleep_time=60, time_out=100)

        # add replica-set members
        MongoDBAdmin().rs_add_members(members)
        # TODO(ramashri) see if hardcoded values can be removed
        utils.poll_until(check_rs_status, sleep_time=60, time_out=100)

    def _set_localhost_auth_bypass(self, enabled):
        """When active, the localhost exception allows connections from the
        localhost interface to create the first user on the admin database.
        The exception applies only when there are no users created in the
        MongoDB instance.
        """
        self.configuration_manager.apply_system_override(
            {'setParameter': {
                'enableLocalhostAuthBypass': enabled
            }})

    def list_all_dbs(self):
        return MongoDBAdmin().list_database_names()

    def db_data_size(self, db_name):
        schema = models.MongoDBSchema(db_name)
        return MongoDBAdmin().db_stats(schema.serialize())['dataSize']

    def admin_cmd_auth_params(self):
        return MongoDBAdmin().cmd_admin_auth_params

    def get_key_file(self):
        return system.MONGO_KEY_FILE

    def get_key(self):
        return open(system.MONGO_KEY_FILE).read().rstrip()

    def store_key(self, key):
        """Store the cluster key."""
        LOG.debug('Storing key for MongoDB cluster.')
        operating_system.write_file(system.MONGO_KEY_FILE, key, as_root=True)
        operating_system.chmod(system.MONGO_KEY_FILE,
                               operating_system.FileMode.SET_USR_RO,
                               as_root=True)
        operating_system.chown(system.MONGO_KEY_FILE,
                               system.MONGO_USER,
                               system.MONGO_USER,
                               as_root=True)

    def store_admin_password(self, password):
        LOG.debug('Storing admin password.')
        creds = MongoDBCredentials(username=system.MONGO_ADMIN_NAME,
                                   password=password)
        creds.write(system.MONGO_ADMIN_CREDS_FILE)
        return creds

    def create_admin_user(self, password):
        """Create the admin user while the localhost exception is active."""
        LOG.debug('Creating the admin user.')
        creds = self.store_admin_password(password)
        user = models.MongoDBUser(name='admin.%s' % creds.username,
                                  password=creds.password)
        user.roles = system.MONGO_ADMIN_ROLES
        with MongoDBClient(None) as client:
            MongoDBAdmin().create_user(user, client=client)
        LOG.debug('Created admin user.')

    def secure(self):
        """Create the Trove admin user.

        The service should not be running at this point.
        """
        if self.status.is_running:
            raise RuntimeError(
                _("Cannot secure the instance. "
                  "The service is still running."))

        try:
            self._set_localhost_auth_bypass(True)
            self.start_db(update_db=False)
            password = utils.generate_random_password()
            self.create_admin_user(password)
            LOG.debug("MongoDB secure complete.")
        finally:
            self._set_localhost_auth_bypass(False)
            self.stop_db()

    def get_configuration_property(self, name, default=None):
        """Return the value of a MongoDB configuration property.
        """
        return self.configuration_manager.get_value(name, default)