Nashwan Azhari 5 лет назад
Родитель
Сommit
9b4bd8e612

+ 104 - 0
coriolis/api/v1/minion_pools.py

@@ -0,0 +1,104 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from oslo_log import log as logging
+from webob import exc
+
+from coriolis import exception
+from coriolis.api.v1.views import minion_pool_view
+from coriolis.api import wsgi as api_wsgi
+from coriolis.policies import minion_pools as pools_policies
+from coriolis.minion_pools import api
+
+LOG = logging.getLogger(__name__)
+
+
+class MinionPoolController(api_wsgi.Controller):
+    def __init__(self):
+        self._minion_pool_api = api.API()
+        super(MinionPoolController, self).__init__()
+
+    def show(self, req, id):
+        context = req.environ["coriolis.context"]
+        context.can(pools_policies.get_minion_pools_policy_label("show"))
+        minion_pool = self._minion_pool_api.get_minion_pool(context, id)
+        if not minion_pool:
+            raise exc.HTTPNotFound()
+
+        return minion_pool_view.single(req, minion_pool)
+
+    def index(self, req):
+        context = req.environ["coriolis.context"]
+        context.can(pools_policies.get_minion_pools_policy_label("list"))
+        return minion_pool_view.collection(
+            req, self._minion_pool_api.get_minion_pools(context))
+
+    def _validate_create_body(self, body):
+        try:
+            minion_pool = body["minion_pool"]
+            name = minion_pool["name"]
+            endpoint_id = minion_pool["endpoint_id"]
+            # TODO(aznashwan): validate pool schema:
+            environment_options = minion_pool["environment_options"]
+            minimum_minions = minion_pool.get("minimum_minions", 0)
+            maximum_minions = minion_pool.get("maximum_minions", 1)
+            minion_max_idle_time = minion_pool.get(
+                "minion_max_idle_time", 1)
+            minion_retention_strategy = minion_pool.get(
+                "minion_retention_strategy")
+            return (
+                name, endpoint_id, environment_options, minimum_minions,
+                maximum_minions, minion_max_idle_time,
+                minion_retention_strategy)
+        except Exception as ex:
+            LOG.exception(ex)
+            if hasattr(ex, "message"):
+                msg = ex.message
+            else:
+                msg = str(ex)
+            raise exception.InvalidInput(msg)
+
+    def create(self, req, body):
+        context = req.environ["coriolis.context"]
+        context.can(pools_policies.get_minion_pools_policy_label("create"))
+        (name, endpoint_id, environment_options, minimum_minions,
+         maximum_minions, minion_max_idle_time, minion_retention_strategy) = (
+            self._validate_create_body(body))
+        return minion_pool_view.single(req, self._minion_pool_api.create(
+            context, name, endpoint_id, environment_options, minimum_minions,
+            maximum_minions, minion_max_idle_time, minion_retention_strategy))
+
+    def _validate_update_body(self, body):
+        try:
+            minion_pool = body["minion_pool"]
+            return {k: minion_pool[k] for k in minion_pool.keys() &
+                    {"name", "environment_options", "minimum_minions",
+                     "maximum_minions", "minion_max_idle_time",
+                     "minion_retention_strategy"}}
+        except Exception as ex:
+            LOG.exception(ex)
+            if hasattr(ex, "message"):
+                msg = ex.message
+            else:
+                msg = str(ex)
+            raise exception.InvalidInput(msg)
+
+    def update(self, req, id, body):
+        context = req.environ["coriolis.context"]
+        context.can(pools_policies.get_minion_pools_policy_label("update"))
+        updated_values = self._validate_update_body(body)
+        return minion_pool_view.single(req, self._minion_pool_api.update(
+            req.environ['coriolis.context'], id, updated_values))
+
+    def delete(self, req, id):
+        context = req.environ["coriolis.context"]
+        context.can(pools_policies.get_minion_pools_policy_label("delete"))
+        try:
+            self._minion_pool_api.delete(req.environ['coriolis.context'], id)
+            raise exc.HTTPNoContent()
+        except exception.NotFound as ex:
+            raise exc.HTTPNotFound(explanation=ex.msg)
+
+
+def create_resource():
+    return api_wsgi.Resource(MinionPoolController())

+ 6 - 0
coriolis/api/v1/router.py

@@ -14,6 +14,7 @@ from coriolis.api.v1 import endpoint_storage
 from coriolis.api.v1 import endpoints
 from coriolis.api.v1 import migration_actions
 from coriolis.api.v1 import migrations
+from coriolis.api.v1 import minion_pools
 from coriolis.api.v1 import provider_schemas
 from coriolis.api.v1 import providers
 from coriolis.api.v1 import regions
@@ -61,6 +62,11 @@ class APIRouter(api.APIRouter):
                         controller=self.resources['services'],
                         collection={'detail': 'GET'})
 
+        self.resources['minion_pools'] = minion_pools.create_resource()
+        mapper.resource('minion_pool', 'minion_pools',
+                        controller=self.resources['minion_pools'],
+                        collection={'detail': 'GET'})
+
         endpoint_actions_resource = endpoint_actions.create_resource()
         self.resources['endpoint_actions'] = endpoint_actions_resource
         endpoint_path = '/{project_id}/endpoints/{id}'

+ 26 - 0
coriolis/api/v1/views/minion_pool_view.py

@@ -0,0 +1,26 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import itertools
+
+
+def _format_minion_pool(req, minion_pool, keys=None):
+    def transform(key, value):
+        if keys and key not in keys:
+            return
+        yield (key, value)
+
+    minion_pool_dict = dict(itertools.chain.from_iterable(
+        transform(k, v) for k, v in minion_pool.items()))
+
+    return minion_pool_dict
+
+
+def single(req, minion_pool):
+    return {"minion_pool": _format_minion_pool(req, minion_pool)}
+
+
+def collection(req, minion_pools):
+    formatted_minion_pools = [
+        _format_minion_pool(req, r) for r in minion_pools]
+    return {'minion_pools': formatted_minion_pools}

+ 27 - 0
coriolis/conductor/rpc/client.py

@@ -372,3 +372,30 @@ class ConductorClient(object):
     def delete_service(self, ctxt, service_id):
         return self._client.call(
             ctxt, 'delete_service', service_id=service_id)
+
+    def create_minion_pool(
+            self, ctxt, name, endpoint_id, environment_options,
+            minimum_minions, maximum_minions, minion_max_idle_time,
+            minion_retention_strategy):
+        return self._client.call(
+            ctxt, 'create_minion_pool', name=name, endpoint_id=endpoint_id,
+            environment_options=environment_options,
+            minimum_minions=minimum_minions, maximum_minions=maximum_minions,
+            minion_max_idle_time=minion_max_idle_time,
+            minion_retention_strategy=minion_retention_strategy)
+
+    def get_minion_pools(self, ctxt):
+        return self._client.call(ctxt, 'get_minion_pools')
+
+    def get_minion_pool(self, ctxt, minion_pool_id):
+        return self._client.call(
+            ctxt, 'get_minion_pool', minion_pool_id=minion_pool_id)
+
+    def update_minion_pool(self, ctxt, minion_pool_id, updated_values):
+        return self._client.call(
+            ctxt, 'update_minion_pool',
+            minion_pool_id=minion_pool_id, updated_values=updated_values)
+
+    def delete_minion_pool(self, ctxt, minion_pool_id):
+        return self._client.call(
+            ctxt, 'delete_minion_pool', minion_pool_id=minion_pool_id)

+ 97 - 0
coriolis/conductor/rpc/server.py

@@ -167,6 +167,18 @@ def service_synchronized(func):
     return wrapper
 
 
+def minion_pool_synchronized(func):
+    @functools.wraps(func)
+    def wrapper(self, ctxt, pool_id, *args, **kwargs):
+        @lockutils.synchronized(
+            constants.MINION_POOL_LOCK_NAME_FORMAT % pool_id,
+            external=True)
+        def inner():
+            return func(self, ctxt, pool_id, *args, **kwargs)
+        return inner()
+    return wrapper
+
+
 class ConductorServerEndpoint(object):
     def __init__(self):
         self._licensing_client = licensing_client.LicensingClient.from_env()
@@ -2897,3 +2909,88 @@ class ConductorServerEndpoint(object):
     @service_synchronized
     def delete_service(self, ctxt, service_id):
         db_api.delete_service(ctxt, service_id)
+
+    def create_minion_pool(
+            self, ctxt, name, endpoint_id, environment_options,
+            minimum_minions, maximum_minions, minion_max_idle_time,
+            minion_retention_strategy):
+        endpoint = db_api.get_endpoint(ctxt, endpoint_id)
+
+        minion_pool = models.MinionPool()
+        minion_pool.id = str(uuid.uuid4())
+        minion_pool.name = name
+        minion_pool.endpoint_id = endpoint_id
+        minion_pool.environment_options = environment_options
+        minion_pool.minimum_minions = minimum_minions
+        minion_pool.maximum_minions = maximum_minions
+        minion_pool.minion_max_idle_time = minion_max_idle_time
+        minion_pool.minion_retention_strategy = minion_retention_strategy
+
+        db_api.add_minion_pool(ctxt, minion_pool)
+        return self.get_minion_pool(ctxt, minion_pool.id)
+
+    def get_minion_pools(self, ctxt):
+        return db_api.get_minion_pools(ctxt)
+
+    @minion_pool_synchronized
+    def get_minion_pool(self, ctxt, minion_pool_id):
+        minion_pool = db_api.get_minion_pool(ctxt, minion_pool_id)
+        if not minion_pool:
+            raise exception.NotFound(
+                "minion_pool with ID '%s' not found." % minion_pool_id)
+        return minion_pool
+
+    @minion_pool_synchronized
+    def update_minion_pool(self, ctxt, minion_pool_id, updated_values):
+        LOG.info(
+            "Attempting to update minion_pool '%s' with payload: %s",
+            minion_pool_id, updated_values)
+        db_api.update_minion_pool(ctxt, minion_pool_id, updated_values)
+        LOG.info("Minion Pool '%s' successfully updated", minion_pool_id)
+        return db_api.get_minion_pool(ctxt, minion_pool_id)
+
+    @minion_pool_synchronized
+    def delete_minion_pool(self, ctxt, minion_pool_id):
+        # TODO(aznashwan): add checks for endpoints/services
+        # associated to the minion_pool before deletion:
+        db_api.delete_minion_pool(ctxt, minion_pool_id)
+
+    @replica_synchronized
+    def get_minion_pool_lifecycle_executions(
+            self, ctxt, minion_pool_id, include_tasks=False):
+        return db_api.get_replica_tasks_executions(
+            ctxt, replica_id, include_tasks)
+
+    @tasks_execution_synchronized
+    def get_replica_tasks_execution(self, ctxt, replica_id, execution_id):
+        return self._get_replica_tasks_execution(
+            ctxt, replica_id, execution_id)
+
+    @tasks_execution_synchronized
+    def delete_replica_tasks_execution(self, ctxt, replica_id, execution_id):
+        execution = self._get_replica_tasks_execution(
+            ctxt, replica_id, execution_id)
+        if execution.status in constants.ACTIVE_EXECUTION_STATUSES:
+            raise exception.InvalidMigrationState(
+                "Cannot delete execution '%s' for Replica '%s' as it is "
+                "currently in '%s' state." % (
+                    execution_id, replica_id, execution.status))
+        db_api.delete_replica_tasks_execution(ctxt, execution_id)
+
+    @tasks_execution_synchronized
+    def cancel_replica_tasks_execution(self, ctxt, replica_id, execution_id,
+                                       force):
+        execution = self._get_replica_tasks_execution(
+            ctxt, replica_id, execution_id)
+        if execution.status not in constants.ACTIVE_EXECUTION_STATUSES:
+            raise exception.InvalidReplicaState(
+                "Replica '%s' has no running execution to cancel." % (
+                    replica_id))
+        if execution.status == constants.EXECUTION_STATUS_CANCELLING and (
+                not force):
+            raise exception.InvalidReplicaState(
+                "Replica '%s' is already being cancelled. Please use the "
+                "force option if you'd like to force-cancel it." % (
+                    replica_id))
+        self._cancel_tasks_execution(ctxt, execution, force=force)
+

+ 1 - 0
coriolis/constants.py

@@ -214,6 +214,7 @@ REPLICA_LOCK_NAME_FORMAT = "replica-%s"
 SCHEDULE_LOCK_NAME_FORMAT = "schedule-%s"
 REGION_LOCK_NAME_FORMAT = "region-%s"
 SERVICE_LOCK_NAME_FORMAT = "service-%s"
+MINION_POOL_LOCK_NAME_FORMAT = "minion-pool-%s"
 
 EXECUTION_TYPE_TO_ACTION_LOCK_NAME_FORMAT_MAP = {
     EXECUTION_TYPE_MIGRATION: MIGRATION_LOCK_NAME_FORMAT,

+ 129 - 0
coriolis/db/api.py

@@ -1087,3 +1087,132 @@ def get_mapped_services_for_region(context, region_id):
     q = q.filter(
         models.ServiceRegionMapping.service_id == region_id)
     return q.all()
+
+
+@enginefacade.writer
+def add_minion_pool(context, minion_pool):
+    minion_pool.user_id = context.user
+    minion_pool.project_id = context.tenant
+    _session(context).add(minion_pool)
+
+
+@enginefacade.reader
+def get_minion_pools(context):
+    q = _soft_delete_aware_query(context, models.MinionPool)
+    q = q.options(orm.joinedload('endpoint'))
+    return q.all()
+
+
+@enginefacade.reader
+def get_minion_pool(context, minion_pool_id):
+    q = _soft_delete_aware_query(context, models.MinionPool)
+    q = q.options(orm.joinedload('endpoint'))
+    return q.filter(
+        models.MinionPool.id == minion_pool_id).first()
+
+
+@enginefacade.writer
+def update_minion_pool(context, minion_pool_id, updated_values):
+    if not minion_pool_id:
+        raise exception.InvalidInput(
+            "No minion_pool ID specified for updating.")
+    minion_pool = get_minion_pool(context, minion_pool_id)
+    if not minion_pool:
+        raise exception.NotFound(
+            "MinionPool with ID '%s' does not exist." % minion_pool_id)
+
+    updateable_fields = [
+        "name", "environment_options", "minimum_minions", "maximum_minions",
+        "minion_max_idle_time", "minion_retention_strategy"]
+    _update_sqlalchemy_object_fields(
+        minion_pool, updateable_fields, updated_values)
+
+
+@enginefacade.writer
+def delete_minion_pool(context, minion_pool_id):
+    minion_pool = get_minion_pool(context, minion_pool_id)
+    count = _soft_delete_aware_query(context, models.MinionPool).filter_by(
+        id=minion_pool_id).soft_delete()
+    if count == 0:
+        raise exception.NotFound("0 minion_pool entries were soft deleted")
+
+
+@enginefacade.writer
+def add_minion_machine(context, minion_machine):
+    _session(context).add(minion_machine)
+
+
+@enginefacade.reader
+def get_minion_machines(context):
+    q = _soft_delete_aware_query(context, models.MinionMachine)
+    q = q.options(orm.joinedload('mapped_services'))
+    return q.all()
+
+
+@enginefacade.reader
+def get_minion_machine(context, minion_machine_id):
+    q = _soft_delete_aware_query(context, models.MinionMachine)
+    q = q.options(orm.joinedload('mapped_endpoints'))
+    q = q.options(orm.joinedload('mapped_services'))
+    return q.filter(
+        models.MinionMachine.id == minion_machine_id).first()
+
+
+@enginefacade.writer
+def update_minion_machine(context, minion_machine_id, updated_values):
+    if not minion_machine_id:
+        raise exception.InvalidInput(
+            "No minion_machine ID specified for updating.")
+    minion_machine = get_minion_machine(context, minion_machine_id)
+    if not minion_machine:
+        raise exception.NotFound(
+            "MinionMachine with ID '%s' does not exist." % minion_machine_id)
+
+    updateable_fields = ["connection_info"]
+    _update_sqlalchemy_object_fields(
+        minion_machine, updateable_fields, updated_values)
+
+
+@enginefacade.writer
+def delete_minion_machine(context, minion_machine_id):
+    minion_machine = get_minion_machine(context, minion_machine_id)
+    count = _soft_delete_aware_query(context, models.MinionMachine).filter_by(
+        id=minion_machine_id).soft_delete()
+    if count == 0:
+        raise exception.NotFound("0 MinionMachine entries were soft deleted")
+
+
+@enginefacade.writer
+def add_minion_pool_lifecycle(context, lifecycle):
+    lifecycle.user_id = context.user
+    lifecycle.project_id = context.tenant
+    _session(context).add(lifecycle)
+
+
+@enginefacade.writer
+def add_minion_pool_lifecycle_execution(context, execution):
+    if is_user_context(context):
+        if execution.action.project_id != context.tenant:
+            raise exception.NotAuthorized()
+
+    # include deleted records
+    max_number = _model_query(
+        context, func.max(models.TasksExecution.number)).filter_by(
+            action_id=execution.action.id).first()[0] or 0
+    execution.number = max_number + 1
+
+    _session(context).add(execution)
+
+
+@enginefacade.reader
+def get_lifecycle_executions_for_minion_pool(
+        context, minion_pool_id, include_tasks=True):
+    minion_pool = get_minion_pool(context, minion_pool_id)
+    q = _soft_delete_aware_query(context, models.TasksExecution)
+    q = q.join(models.MinionPoolLifecycle)
+    if include_tasks:
+        q = _get_tasks_with_details_options(q)
+    if is_user_context(context):
+        q = q.filter(models.MinionPoolLifecycle.project_id == context.tenant)
+    return q.filter(
+        models.MinionPoolLifecycle.id == minion_pool.lifecycle_action.id).all()

+ 102 - 0
coriolis/db/sqlalchemy/migrate_repo/versions/015_adds_minion_vm_pools.py

@@ -0,0 +1,102 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import uuid
+
+import sqlalchemy
+
+
+def upgrade(migrate_engine):
+    meta = sqlalchemy.MetaData()
+    meta.bind = migrate_engine
+
+    endpoint = sqlalchemy.Table(
+        'endpoint', meta, autoload=True)
+    base_transfer_action = sqlalchemy.Table(
+        'base_transfer_action', meta, autoload=True)
+
+    tables = []
+
+    # declare minion pool table:
+    tables.append(
+        sqlalchemy.Table(
+            'minion_pool',
+            meta,
+            sqlalchemy.Column('id', sqlalchemy.String(36), primary_key=True,
+                              default=lambda: str(uuid.uuid4())),
+            sqlalchemy.Column(
+                "user_id", sqlalchemy.String(255), nullable=False),
+            sqlalchemy.Column(
+                "project_id", sqlalchemy.String(255), nullable=False),
+
+            sqlalchemy.Column('name', sqlalchemy.String(255), nullable=False),
+            sqlalchemy.Column('created_at', sqlalchemy.DateTime),
+            sqlalchemy.Column('updated_at', sqlalchemy.DateTime),
+            sqlalchemy.Column('deleted_at', sqlalchemy.DateTime),
+            sqlalchemy.Column('deleted', sqlalchemy.String(36)),
+            sqlalchemy.Column(
+                'endpoint_id', sqlalchemy.String(36),
+                sqlalchemy.ForeignKey('endpoint.id'), nullable=False),
+            sqlalchemy.Column(
+                'environment_options', sqlalchemy.Text, nullable=False),
+            sqlalchemy.Column(
+                'minimum_minions', sqlalchemy.Integer, nullable=False),
+            sqlalchemy.Column(
+                'maximum_minions', sqlalchemy.Integer, nullable=False),
+            sqlalchemy.Column(
+                'minion_max_idle_time', sqlalchemy.Integer, nullable=False),
+            sqlalchemy.Column(
+                'minion_retention_strategy', sqlalchemy.String(255),
+                nullable=False)))
+
+    # declare minion machine table:
+    tables.append(
+        sqlalchemy.Table(
+            'minion_machine',
+            meta,
+            sqlalchemy.Column('id', sqlalchemy.String(36), primary_key=True,
+                              default=lambda: str(uuid.uuid4())),
+            sqlalchemy.Column('created_at', sqlalchemy.DateTime),
+            sqlalchemy.Column('updated_at', sqlalchemy.DateTime),
+            sqlalchemy.Column('deleted_at', sqlalchemy.DateTime),
+            sqlalchemy.Column('deleted', sqlalchemy.String(36)),
+            sqlalchemy.Column(
+                'pool_id', sqlalchemy.String(36),
+                sqlalchemy.ForeignKey('minion_pool.id'), nullable=False),
+            sqlalchemy.Column(
+                'status', sqlalchemy.String(255), nullable=False),
+            sqlalchemy.Column('connection_info', sqlalchemy.Text),
+            sqlalchemy.Column('provider_properties', sqlalchemy.Text)))
+
+    # add table for pool lifecycles:
+    tables.append(
+        sqlalchemy.Table(
+            'minion_pool_lifecycle',
+            meta,
+            sqlalchemy.Column(
+                "id", sqlalchemy.String(36),
+                sqlalchemy.ForeignKey('base_transfer_action.base_id'),
+                primary_key=True),
+            sqlalchemy.Column(
+                "minion_pool_id", sqlalchemy.String(36),
+                sqlalchemy.ForeignKey('minion_pool.id'), nullable=False)))
+
+    for index, table in enumerate(tables):
+        try:
+            table.create()
+        except Exception:
+            # If an error occurs, drop all tables created so far to return
+            # to the previously existing state.
+            meta.drop_all(tables=tables[:index])
+            raise
+
+    # update base_transfer_action:
+    columns = [
+        sqlalchemy.Column(
+            "source_minion_pool_id", sqlalchemy.String(36),
+            sqlalchemy.ForeignKey('minion_pool.id'), nullable=True),
+        sqlalchemy.Column(
+            "destination_minion_pool_id", sqlalchemy.String(36),
+            sqlalchemy.ForeignKey('minion_pool.id'), nullable=True)]
+    for col in columns:
+        base_transfer_action.create_column(col)

+ 93 - 0
coriolis/db/sqlalchemy/models.py

@@ -196,6 +196,12 @@ class BaseTransferAction(BASE, models.TimestampMixin, models.ModelBase,
     network_map = sqlalchemy.Column(types.Json, nullable=True)
     storage_mappings = sqlalchemy.Column(types.Json, nullable=True)
     source_environment = sqlalchemy.Column(types.Json, nullable=True)
+    source_minion_pool_id = sqlalchemy.Column(
+        sqlalchemy.String(36), sqlalchemy.ForeignKey('minion_pool.id'),
+        nullable=True, default=lambda: None)
+    destination_minion_pool_id = sqlalchemy.Column(
+        sqlalchemy.String(36), sqlalchemy.ForeignKey('minion_pool.id'),
+        nullable=True, default=lambda: None)
 
     __mapper_args__ = {
         'polymorphic_identity': 'base_transfer_action',
@@ -233,6 +239,32 @@ class BaseTransferAction(BASE, models.TimestampMixin, models.ModelBase,
         return result
 
 
+class MinionPoolLifecycle(BaseTransferAction):
+    # TODO(aznashwan): this class inherits numerous redundant fields from
+    # BaseTransferAction. Ideally, the upper hirearchy should be split into a
+    # BaseAction, and a separate inheriting BaseTransferAction.
+    __tablename__ = 'minion_pool_lifecycle'
+
+    id = sqlalchemy.Column(
+        sqlalchemy.String(36),
+        sqlalchemy.ForeignKey(
+            'base_transfer_action.base_id'), primary_key=True)
+
+    minion_pool_id = sqlalchemy.Column(
+        sqlalchemy.String(36),
+        sqlalchemy.ForeignKey('minion_pool.id'), nullable=False)
+
+    __mapper_args__ = {
+        'polymorphic_identity': 'minion_pool_lifecycle',
+    }
+
+    def to_dict(self, include_info=True):
+        base = super(MinionPoolLifecycle, self).to_dict(
+            include_info=include_info)
+        base.update({"id": self.id})
+        return base
+
+
 class Replica(BaseTransferAction):
     __tablename__ = 'replica'
 
@@ -436,3 +468,64 @@ class ReplicaSchedule(BASE, models.TimestampMixin, models.ModelBase,
     shutdown_instance = sqlalchemy.Column(
         sqlalchemy.Boolean, nullable=False, default=False)
     trust_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
+
+
+class MinionPool(BASE, models.TimestampMixin, models.ModelBase,
+                 models.SoftDeleteMixin):
+    __tablename__ = "minion_pool"
+
+    id = sqlalchemy.Column(sqlalchemy.String(36),
+                           default=lambda: str(uuid.uuid4()),
+                           primary_key=True)
+    user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
+    project_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
+
+    name = sqlalchemy.Column(
+        sqlalchemy.String(255),
+        nullable=False)
+
+    endpoint_id = sqlalchemy.Column(
+        sqlalchemy.String(36),
+        sqlalchemy.ForeignKey('endpoint.id'),
+        nullable=False)
+    endpoint = orm.relationship(
+        Endpoint, backref=orm.backref("minion_pools"),
+        foreign_keys=[endpoint_id])
+
+    environment_options = sqlalchemy.Column(
+        types.Json, nullable=False)
+
+    minimum_minions = sqlalchemy.Column(
+        sqlalchemy.Integer, nullable=False)
+    maximum_minions = sqlalchemy.Column(
+        sqlalchemy.Integer, nullable=False)
+    minion_max_idle_time = sqlalchemy.Column(
+        sqlalchemy.Integer, nullable=False)
+    minion_retention_strategy = sqlalchemy.Column(
+        sqlalchemy.String(255), nullable=False)
+
+
+class MinionMachine(BASE, models.TimestampMixin, models.ModelBase,
+                    models.SoftDeleteMixin):
+    __tablename__ = "minion_machine"
+
+    id = sqlalchemy.Column(sqlalchemy.String(36),
+                           default=lambda: str(uuid.uuid4()),
+                           primary_key=True)
+    user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
+    project_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
+
+    pool_id = sqlalchemy.Column(
+        sqlalchemy.String(36),
+        sqlalchemy.ForeignKey('minion_pool.id'),
+        nullable=False)
+    pool = orm.relationship(
+        MinionPool, backref=orm.backref("minion_machines"),
+        foreign_keys=[pool_id])
+
+    status = sqlalchemy.Column(
+        sqlalchemy.String(255), nullable=False)
+
+    connection_info = sqlalchemy.Column(types.Json)
+
+    provider_properties = sqlalchemy.Column(types.Json)

+ 0 - 0
coriolis/minion_pools/__init__.py


+ 31 - 0
coriolis/minion_pools/api.py

@@ -0,0 +1,31 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from coriolis import utils
+from coriolis.conductor.rpc import client as rpc_client
+
+
+class API(object):
+    def __init__(self):
+        self._rpc_client = rpc_client.ConductorClient()
+
+    def create(
+            self, ctxt, name, endpoint_id, environment_options,
+            minimum_minions, maximum_minions, minion_max_idle_time,
+            minion_retention_strategy):
+        return self._rpc_client.create_minion_pool(
+            ctxt, name, endpoint_id, environment_options, minimum_minions,
+            maximum_minions, minion_max_idle_time, minion_retention_strategy)
+
+    def update(self, ctxt, minion_pool_id, updated_values):
+        return self._rpc_client.update_minion_pool(
+            ctxt, minion_pool_id, updated_values=updated_values)
+
+    def delete(self, ctxt, minion_pool_id):
+        self._rpc_client.delete_minion_pool(ctxt, minion_pool_id)
+
+    def get_minion_pools(self, ctxt):
+        return self._rpc_client.get_minion_pools(ctxt)
+
+    def get_minion_pool(self, ctxt, minion_pool_id):
+        return self._rpc_client.get_minion_pool(ctxt, minion_pool_id)

+ 79 - 0
coriolis/policies/minion_pools.py

@@ -0,0 +1,79 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+
+from oslo_policy import policy
+
+from coriolis.policies import base
+
+
+MINION_POOLS_POLICY_PREFIX = "%s:minion_pools" % base.CORIOLIS_POLICIES_PREFIX
+MINION_POOLS_DEFAULT_RULE = "rule:admin_or_owner"
+
+
+def get_minion_pools_policy_label(rule_label):
+    return "%s:%s" % (
+        MINION_POOLS_POLICY_PREFIX, rule_label)
+
+
+MINION_POOLS_DEFAULT_RULES = [
+    policy.DocumentedRuleDefault(
+        get_minion_pools_policy_label('create'),
+        MINION_POOLS_DEFAULT_RULE,
+        "Create a minion_pool",
+        [
+            {
+                "path": "/minion_pools",
+                "method": "POST"
+            }
+        ]
+    ),
+    policy.DocumentedRuleDefault(
+        get_minion_pools_policy_label('list'),
+        MINION_POOLS_DEFAULT_RULE,
+        "List minion_pools",
+        [
+            {
+                "path": "/minion_pools",
+                "method": "GET"
+            }
+        ]
+    ),
+    policy.DocumentedRuleDefault(
+        get_minion_pools_policy_label('show'),
+        MINION_POOLS_DEFAULT_RULE,
+        "Show details for minion_pool",
+        [
+            {
+                "path": "/minion_pools/{minion_pool_id}",
+                "method": "GET"
+            }
+        ]
+    ),
+    policy.DocumentedRuleDefault(
+        get_minion_pools_policy_label('update'),
+        MINION_POOLS_DEFAULT_RULE,
+        "Update details for minion_pool",
+        [
+            {
+                "path": "/minion_pools/{minion_pool_id}",
+                "method": "PUT"
+            }
+        ]
+    ),
+    policy.DocumentedRuleDefault(
+        get_minion_pools_policy_label('delete'),
+        MINION_POOLS_DEFAULT_RULE,
+        "Delete minion_pool",
+        [
+            {
+                "path": "/minion_pools/{minion_pool_id}",
+                "method": "DELETE"
+            }
+        ]
+    )
+]
+
+
+def list_rules():
+    return MINION_POOLS_DEFAULT_RULES

+ 2 - 1
coriolis/policy.py

@@ -14,6 +14,7 @@ from coriolis.policies import diagnostics
 from coriolis.policies import endpoints
 from coriolis.policies import general
 from coriolis.policies import migrations
+from coriolis.policies import minion_pools
 from coriolis.policies import regions
 from coriolis.policies import replicas
 from coriolis.policies import replica_schedules
@@ -28,7 +29,7 @@ _ENFORCER = None
 
 DEFAULT_POLICIES_MODULES = [
     base, endpoints, general, migrations, replicas, replica_schedules,
-    replica_tasks_executions, diagnostics, regions, services]
+    replica_tasks_executions, diagnostics, regions, services, minion_pools]
 
 
 def reset():