craton/craton/db/sqlalchemy/api.py

1339 lines
45 KiB
Python

"""SQLAlchemy backend implementation."""
import enum
import functools
from operator import attrgetter
import sys
import uuid
import jsonpath_rw as jspath
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_db import options as db_options
from oslo_db.sqlalchemy import session
from oslo_db.sqlalchemy import utils as db_utils
from oslo_log import log
from sqlalchemy import and_, sql
from sqlalchemy import func as sa_func
import sqlalchemy.orm.exc as sa_exc
from sqlalchemy.orm import with_polymorphic
from craton import exceptions
from craton.db.sqlalchemy import models
CONF = cfg.CONF
LOG = log.getLogger(__name__)
_FACADE = None
_DEFAULT_SQL_CONNECTION = 'sqlite://'
db_options.set_defaults(cfg.CONF,
connection=_DEFAULT_SQL_CONNECTION)
MYSQL_INVALID_JSONPATH_EXPRESSION = 3143
MYSQL_INVALID_JSON_TEXT = 3141
JSON_EXCEPTIONS = {
MYSQL_INVALID_JSONPATH_EXPRESSION: exceptions.InvalidJSONPath,
MYSQL_INVALID_JSON_TEXT: exceptions.InvalidJSONValue,
}
def _create_facade_lazily():
global _FACADE
if _FACADE is None:
_FACADE = session.EngineFacade.from_config(cfg.CONF)
return _FACADE
def get_engine():
facade = _create_facade_lazily()
return facade.get_engine()
def get_session(**kwargs):
facade = _create_facade_lazily()
return facade.get_session(**kwargs)
def get_backend():
"""The backend is this module itself."""
return sys.modules[__name__]
def is_admin_context(context):
"""Check if this request had admin project context."""
if (context.is_admin and context.is_admin_project):
return True
return False
def is_project_admin_context(context):
"""Check if this request has admin context with in the project."""
if context.is_admin:
return True
return False
def require_admin_context(f):
"""Decorator that ensures admin request context."""
def wrapper(*args, **kwargs):
if not is_admin_context(args[0]):
raise exceptions.AdminRequired()
return f(*args, **kwargs)
return wrapper
def require_project_admin_context(f):
"""Decorator that ensures admin or project_admin request context."""
def wrapper(*args, **kwargs):
context = args[0]
if is_project_admin_context(context):
return f(*args, **kwargs)
elif is_project_admin_context(args[0]):
return f(*args, **kwargs)
else:
raise exceptions.AdminRequired()
return wrapper
class CRUD(enum.Enum):
CREATE = 'create'
READ = 'read'
UPDATE = 'update'
DELETE = 'delete'
def permissions_for(action):
# NOTE(thomasem): Temporary shim applying existing project admin / root
# project checks to generic resource methods, since we don't have RBAC
# fully baked yet. This needs to be removed once we have solved this via
# craton-rbac-support blueprint. This affords us being able to use
# generic resource methods support for resources like Projects, where one
# must be Project admin to interact.
def get_permissions_for(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
# NOTE(thomasem): Standard order of arguments for generic resources
# methods is (context, resources, ...), so args[0] is always
# context and args[1] is always resources.
context = args[0]
resources = args[1]
permissions = {
CRUD.CREATE: {
'projects': is_project_admin_context(context),
},
CRUD.READ: {
'projects': is_project_admin_context(context),
},
CRUD.UPDATE: {
'projects': is_project_admin_context(context),
},
CRUD.DELETE: {
'projects': is_project_admin_context(context),
}
}
# NOTE(thomasem): Default to True unless otherwise specified in
# permissions dict.
if permissions[action].get(resources, True):
return fn(*args, **kwargs)
else:
raise exceptions.AdminRequired()
return wrapper
return get_permissions_for
def model_query(context, model, *args, **kwargs):
"""Query helper that accounts for context's `read_deleted` field.
:param context: context to query under
:param model: model to query. Must be a subclass of ModelBase.
:param session: if present, the session to use
:param project_only: if present and context is user-type, then restrict
query to match the context's project_id.
"""
session = kwargs.get('session') or get_session()
project_only = kwargs.get('project_only')
kwargs = dict()
if project_only and not context.is_admin:
kwargs['project_id'] = context.tenant
return db_utils.model_query(
model=model, session=session, args=args, **kwargs)
def _find_first_key_fragment(root):
"""Finds element where first key Field exists."""
desired = jspath.Fields
if isinstance(root, desired):
return root
while not isinstance(root.left, desired):
root = root.left
return root
def _split_key_path_prefix(root):
"""Extract first key and initial path from parsed JSON Path.
This is necessitated by us not actually including the top-most Key
component in the JSON column (Variables.value) in the database, so we only
need a parser to extract the first Key from the expression. The rest can be
handled in the database.
This essentially takes a parsed JSON Path, finds the first field and
optional Slice or Index. It then uses the value of the first field's value
(fields[0]) as the key and, if a Slice of Index exist on the right
side of the expression, it builds an Array specifier as the initial part of
the path and then returns them.
"""
# NOTE(thomasem): Because of how keys work and our data model, the first
# element should not be anything other than a Fields or a Child fragment.
if not isinstance(root, (jspath.Fields, jspath.Child)):
raise exceptions.InvalidJSONPath()
path = ''
fragment = _find_first_key_fragment(root)
right = getattr(fragment, 'right', None)
left = getattr(fragment, 'left', fragment)
if isinstance(right, jspath.Slice) and any([right.start, right.end]):
# NOTE(thomasem): MySQL 5.7 does not support arbitrary slices, only
# '[*]'.
raise exceptions.InvalidJSONPath()
elif isinstance(right, jspath.Slice):
path = '[*]'
elif isinstance(right, jspath.Index):
path = '[{}]'.format(right.index)
key = left.fields[0]
return key, path
def _parse_path_expr(path_expression):
"""Split into the first key and the path used for querying MySQL."""
try:
parsed = jspath.parse(path_expression)
except Exception as e:
# NOTE(thomasem): Unfortunately, this library raises an Exception
# instead of something more specific.
raise exceptions.InvalidJSONPath() from e
# NOTE(thomasem): Get the first key from the parsed JSON Path expression
# and initial path for the Variables.value JSON column, if there is any.
# There would only be an initial path if there's an Array specifier
# immediately adjacent to the first key, for example: 'foo[5]' or 'foo[*]'.
key, path = _split_key_path_prefix(parsed)
# NOTE(thomasem): Remove the key we found from the original expression
# since that's not included in the Variables.value JSON column.
key_removed = path_expression[len(key):]
# NOTE(thomasem): Because the first key could have been wrapped in quotes
# to denote it as a JSON string for handling special characters in the
# key, we will want to find the first delimiter ('.') if one exists to
# know where to slice off the remainder of the path and combine prefix
# and suffix.
path_suffix_start = key_removed.find('.')
if path_suffix_start > -1:
path = '{}{}'.format(path, key_removed[path_suffix_start:])
return key, '${}'.format(path)
def _json_path_clause(kv_pair):
path_expr, value = kv_pair
key, path = _parse_path_expr(path_expr)
json_match = sa_func.json_contains(
sa_func.json_extract(models.Variable.value, path), value)
# NOTE(thomasem): Order is important here. MySQL will short-circuit and
# not properly validate the JSON Path expression when the key doesn't exist
# if the key match is first int he and_(...) expression. So, let's put
# the json_match first.
return and_(json_match, models.Variable.key == key)
def _get_variables(query):
try:
return set(query)
except db_exc.DBError as e:
orig = e.inner_exception.orig
code = orig.args[0]
if orig and code in JSON_EXCEPTIONS:
raise JSON_EXCEPTIONS[code]() from e
raise
def _matching_resources(query, resource_cls, get_descendants, kv):
# NOTE(jimbaker) Build a query that determine all resources that
# match this key/value, taking in account variable
# resolution. Note that this value is generalized to be a JSON
# path; and the resolution is inverted by finding any possible
# descendants.
kv_pairs = list(kv.items())
matches = {}
for kv_pair in kv_pairs:
match = matches[kv_pair] = set()
kv_clause = _json_path_clause(kv_pair)
matching_variables = _get_variables(
query.session.query(models.Variable).filter(kv_clause))
for variable in matching_variables:
if isinstance(variable.parent, resource_cls):
match.add(variable.parent)
# NOTE(jimbaker) now check for descendent overrides; in
# particular, it's possible that a chain of overrides
# could occur such that the original top value was
# restored.
#
# Because all of the matching variables were returned by
# the query; and SQLAlchemy guarantees that within the
# scope of a transaction ("unit of work") that a given
# object will have the same identity, we can check with
# respect to matching_variables. Do note that arbitrary
# additional object graph queries may be done to check the
# resolution ordering.
for descendant in get_descendants(variable.parent):
for level in descendant.resolution_order:
desc_variable = level._variables.get(variable.key)
if desc_variable is not None:
if desc_variable in matching_variables:
match.add(descendant)
break
# NOTE(jimbaker) For now, we simply match for the conjunction
# ("and") of all the supplied kv pairs we are matching
# against. Generalize in the future as desired with other boolean
# logic.
_, first_match = matches.popitem()
if matches:
resources = first_match.intersection(*matches.values())
else:
resources = first_match
return resources
def _get_devices(parent):
if isinstance(parent, models.Device):
return parent.descendants
else:
return parent.devices
_resource_mapping = {
models.Project: ([], None),
models.Cloud: ([models.Project], attrgetter('clouds')),
models.Region: ([models.Project, models.Cloud], attrgetter('regions')),
models.Cell: (
[models.Project, models.Cloud, models.Region],
attrgetter('cells')),
models.Network: (
[models.Project, models.Cloud, models.Region, models.Cell],
attrgetter('networks')),
models.Device: (
[models.Project, models.Cloud, models.Region, models.Cell,
models.Device],
_get_devices),
}
def matching_resources(query, resource_cls, kv, resolved):
def get_desc(parent):
parent_classes, getter = _resource_mapping[resource_cls]
# NOTE(thomasem): If we're not resolving, there are no descendants
# to process, so return an empty list.
if resolved and any(isinstance(parent, cls) for cls in parent_classes):
return getter(parent)
else:
return []
return _matching_resources(query, resource_cls, get_desc, kv)
def _add_var_filters_to_query(query, model, var_filters, resolved=True):
# vars filters are of form ?vars=a:b[,c:d,...] - the filters in
# this case are intersecting ("and" queries)
kv = dict(pairing.split(':', 1) for pairing in var_filters)
resource_ids = set(
resource.id
for resource in matching_resources(query, model, kv, resolved)
)
if not resource_ids:
# short circuit; this also avoids SQLAlchemy reporting that it is
# working with an empty "in" clause
return query.filter(sql.false())
return query.filter(model.id.in_(resource_ids))
def add_var_filters_to_query(query, model, filters):
var_filters = filters['vars'].split(',')
resolved = bool(filters.get('resolved-values'))
return _add_var_filters_to_query(query, model, var_filters,
resolved=resolved)
def get_user_info(context, username):
"""Get user info."""
query = model_query(context, models.User, project_only=True)
query = query.filter_by(username=username)
try:
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
except Exception as err:
raise exceptions.UnknownException(message=err)
def _get_resource_model(resource):
resource_models = {
"cells": models.Cell,
"devices": with_polymorphic(models.Device, "*"),
"hosts": with_polymorphic(models.Device, models.Host),
"network-devices": with_polymorphic(
models.Device, models.NetworkDevice
),
"networks": models.Network,
"regions": models.Region,
"clouds": models.Cloud,
"projects": models.Project,
}
return resource_models[resource]
@permissions_for(CRUD.READ)
def resource_get_by_id(
context, resources, resource_id, session=None, for_update=False
):
"""Get resource for the unique resource id."""
model = _get_resource_model(resources)
query = model_query(context, model, project_only=True, session=session)
if resources in ("hosts", "network-devices"):
query = query.filter_by(type=resources.replace("-", "_"))
query = query.filter_by(id=resource_id)
if for_update:
query = query.with_for_update()
try:
resource = query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
else:
return resource
@permissions_for(CRUD.UPDATE)
def variables_update_by_resource_id(context, resources, resource_id, data):
"""Update/create existing resource's variables."""
session = get_session()
with session.begin():
resource = resource_get_by_id(
context, resources, resource_id, session, for_update=True
)
resource.variables.update(data)
return resource
@permissions_for(CRUD.DELETE)
def variables_delete_by_resource_id(context, resources, resource_id, data):
"""Delete the existing variables, if present, from resource's data."""
session = get_session()
with session.begin():
resource = resource_get_by_id(
context, resources, resource_id, session, for_update=True
)
for key in data:
try:
del resource.variables[key]
except KeyError:
pass
return resource
def devices_get_all(context, filters, pagination_params):
"""Get all available devices."""
session = get_session()
devices = with_polymorphic(models.Device, "*")
query = model_query(context, devices, project_only=True, session=session)
if "parent_id" in filters and filters.get("descendants"):
parent = query.filter_by(id=filters["parent_id"]).one()
query = query.filter(
models.Device.id.in_(device.id for device in parent.descendants)
)
elif "parent_id" in filters:
query = query.filter_by(parent_id=filters["parent_id"])
if "region_id" in filters:
query = query.filter_by(region_id=filters["region_id"])
if "cloud_id" in filters:
query = query.filter_by(cloud_id=filters["cloud_id"])
if "cell_id" in filters:
query = query.filter_by(cell_id=filters["cell_id"])
if "active" in filters:
query = query.filter_by(active=filters["active"])
return _paginate(context, query, models.Device, session, filters,
pagination_params)
def _device_labels_update(context, device_type, device_id, labels):
"""Update labels for the given device. Add the label if it is not present
in host labels list, otherwise do nothing."""
session = get_session()
with session.begin():
devices = with_polymorphic(models.Device, '*')
query = model_query(context, devices, session=session,
project_only=True)
query = query.filter_by(type=device_type)
query = query.filter_by(id=device_id)
try:
device = query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
device.labels.update(labels["labels"])
device.save(session)
return device
def _device_labels_delete(context, device_type, device_id, labels):
"""Delete labels from the device labels list if it matches
the given label in the query, otherwise do nothing."""
session = get_session()
with session.begin():
devices = with_polymorphic(models.Device, '*')
query = model_query(context, devices, session=session,
project_only=True)
query = query.filter_by(type=device_type)
query = query.filter_by(id=device_id)
try:
device = query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
for label in labels["labels"]:
device.labels.discard(label)
device.save(session)
return device
def cells_get_all(context, filters, pagination_params):
"""Get all cells."""
session = get_session()
query = model_query(context, models.Cell, project_only=True,
session=session)
if "id" in filters:
query = query.filter_by(id=filters["id"])
if "region_id" in filters:
query = query.filter_by(region_id=filters["region_id"])
if "cloud_id" in filters:
query = query.filter_by(cloud_id=filters["cloud_id"])
if "name" in filters:
query = query.filter_by(name=filters["name"])
if "vars" in filters:
query = add_var_filters_to_query(query, models.Cell, filters)
return _paginate(context, query, models.Cell, session, filters,
pagination_params)
def cells_get_by_id(context, cell_id):
"""Get cell details given for a given cell id."""
try:
query = model_query(context, models.Cell).\
filter_by(id=cell_id)
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
def cells_create(context, values):
"""Create a new cell."""
session = get_session()
cell = models.Cell()
with session.begin():
try:
cell.update(values)
cell.save(session)
except db_exc.DBDuplicateEntry:
raise exceptions.DuplicateCell()
return cell
def cells_update(context, cell_id, values):
"""Update an existing cell."""
session = get_session()
with session.begin():
query = model_query(context, models.Cell, session=session,
project_only=True)
query = query.filter_by(id=cell_id)
cell_ref = query.with_for_update().one()
cell_ref.update(values)
cell_ref.save(session)
return cell_ref
def cells_delete(context, cell_id):
"""Delete an existing cell."""
session = get_session()
with session.begin():
query = model_query(context, models.Cell, session=session,
project_only=True)
query = query.filter_by(id=cell_id)
query.delete()
def regions_get_all(context, filters, pagination_params):
"""Get all available regions."""
session = get_session()
query = model_query(context, models.Region, project_only=True,
session=session)
if "vars" in filters:
query = add_var_filters_to_query(query, models.Region, filters)
if "cloud_id" in filters:
query = query.filter_by(cloud_id=filters["cloud_id"])
return _paginate(context, query, models.Region, session, filters,
pagination_params)
def regions_get_by_name(context, name):
"""Get cell detail for the region with given name."""
query = model_query(context, models.Region, project_only=True)
query = query.filter_by(name=name)
try:
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
def regions_get_by_id(context, region_id):
"""Get cell detail for the region with given id."""
query = model_query(context, models.Region, project_only=True)
query = query.filter_by(id=region_id)
try:
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
def regions_create(context, values):
"""Create a new region."""
session = get_session()
region = models.Region()
with session.begin():
try:
region.update(values)
region.save(session)
except db_exc.DBDuplicateEntry:
raise exceptions.DuplicateRegion()
return region
def regions_update(context, region_id, values):
"""Update an existing region."""
session = get_session()
with session.begin():
query = model_query(context, models.Region, session=session,
project_only=True)
query = query.filter_by(id=region_id)
region_ref = query.with_for_update().one()
region_ref.update(values)
region_ref.save(session)
return region_ref
def regions_delete(context, region_id):
"""Delete an existing region."""
session = get_session()
with session.begin():
query = model_query(context, models.Region, session=session,
project_only=True)
query = query.filter_by(id=region_id)
query.delete()
return
def clouds_get_all(context, filters, pagination_params):
"""Get all available clouds."""
session = get_session()
query = model_query(context, models.Cloud, project_only=True,
session=session)
if "vars" in filters:
query = add_var_filters_to_query(query, models.Cloud, filters)
return _paginate(context, query, models.Cloud, session, filters,
pagination_params)
def clouds_get_by_name(context, name):
"""Get cell detail for the cloud with given name."""
query = model_query(context, models.Cloud, project_only=True)
query = query.filter_by(name=name)
try:
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
def clouds_get_by_id(context, cloud_id):
"""Get cell detail for the cloud with given id."""
query = model_query(context, models.Cloud, project_only=True)
query = query.filter_by(id=cloud_id)
try:
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
def clouds_create(context, values):
"""Create a new cloud."""
session = get_session()
cloud = models.Cloud()
with session.begin():
try:
cloud.update(values)
cloud.save(session)
except db_exc.DBDuplicateEntry:
raise exceptions.DuplicateCloud()
return cloud
def clouds_update(context, cloud_id, values):
"""Update an existing cloud."""
session = get_session()
with session.begin():
query = model_query(context, models.Cloud, session=session,
project_only=True)
query = query.filter_by(id=cloud_id)
cloud_ref = query.with_for_update().one()
cloud_ref.update(values)
cloud_ref.save(session)
return cloud_ref
def clouds_delete(context, cloud_id):
"""Delete an existing cloud."""
session = get_session()
with session.begin():
query = model_query(context, models.Cloud, session=session,
project_only=True)
query = query.filter_by(id=cloud_id)
query.delete()
return
def hosts_get_all(context, filters, pagination_params):
"""Get all hosts matching filters.
:param filters: filters which contains different keys/values to match.
Supported filters are region_id, name, ip_address, id, cell, device_type,
label and vars.
"""
session = get_session()
host_devices = with_polymorphic(models.Device, [models.Host])
query = model_query(context, host_devices, project_only=True,
session=session)
query = query.filter_by(type='hosts')
if "region_id" in filters:
query = query.filter_by(region_id=filters["region_id"])
if "cloud_id" in filters:
query = query.filter_by(cloud_id=filters["cloud_id"])
if "name" in filters:
query = query.filter_by(name=filters["name"])
if "ip_address" in filters:
query = query.filter_by(ip_address=filters["ip_address"])
if "id" in filters:
query = query.filter_by(id=filters["id"])
if "cell_id" in filters:
query = query.filter_by(cell_id=filters["cell_id"])
if "device_type" in filters:
query = query.filter_by(device_type=filters["device_type"])
if "label" in filters:
query = query.filter(models.Device.related_labels.any(
models.Label.label == filters["label"]))
if "vars" in filters:
query = add_var_filters_to_query(query, models.Device, filters)
return _paginate(context, query, models.Host, session, filters,
pagination_params)
def hosts_get_by_id(context, host_id):
"""Get details for the host with given id."""
host_devices = with_polymorphic(models.Device, '*')
query = model_query(context, host_devices, project_only=True).\
filter_by(id=host_id)
query = query.filter_by(type='hosts')
try:
result = query.one()
LOG.info("Result by host id %s" % result)
except sa_exc.NoResultFound:
LOG.error("No result found for host with id %s" % host_id)
raise exceptions.NotFound()
except Exception as err:
raise exceptions.UnknownException(message=err)
return result
def hosts_create(context, values):
"""Create a new host."""
session = get_session()
host = models.Host()
with session.begin():
try:
host.update(values)
host.save(session)
except db_exc.DBDuplicateEntry:
raise exceptions.DuplicateDevice()
return host
def hosts_update(context, host_id, values):
"""Update an existing host."""
session = get_session()
with session.begin():
host_devices = with_polymorphic(models.Device, '*')
query = model_query(context, host_devices, session=session,
project_only=True)
query = query.filter_by(id=host_id)
host_ref = query.with_for_update().one()
try:
host_ref.update(values)
except exceptions.ParentIDError as e:
raise exceptions.BadRequest(message=str(e))
host_ref.save(session)
return host_ref
def hosts_delete(context, host_id):
"""Delete an existing host."""
session = get_session()
with session.begin():
host_devices = with_polymorphic(models.Device, '*')
query = model_query(context, host_devices, session=session,
project_only=True)
query = query.filter_by(type='hosts')
query = query.filter_by(id=host_id)
query.delete()
return
def hosts_labels_update(context, host_id, labels):
"""Update labels for host. Add the label if it is not present
in host labels list, otherwise do nothing."""
return _device_labels_update(context, 'hosts', host_id, labels)
def hosts_labels_delete(context, host_id, labels):
"""Delete labels from the host labels list if it matches
the given label in the query, otherwise do nothing."""
return _device_labels_delete(context, 'hosts', host_id, labels)
@require_admin_context
def projects_get_all(context, filters, pagination_params):
"""Get all the projects."""
session = get_session()
query = model_query(context, models.Project, session=session)
if "vars" in filters:
query = add_var_filters_to_query(query, models.Project, filters)
return _paginate(context, query, models.Project, session, filters,
pagination_params)
@require_admin_context
def projects_get_by_name(context, project_name, filters, pagination_params):
"""Get all projects that match the given name."""
query = model_query(context, models.Project)
query = query.filter(models.Project.name.like(project_name))
if "vars" in filters:
query = add_var_filters_to_query(query, models.Project, filters)
try:
return _paginate(context, query, models.Project, session, filters,
pagination_params)
except sa_exc.NoResultFound:
raise exceptions.NotFound()
except Exception as err:
raise exceptions.UnknownException(message=err)
@require_project_admin_context
def projects_get_by_id(context, project_id):
"""Get project by its id."""
query = model_query(context, models.Project)
query = query.filter_by(id=project_id)
try:
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
except Exception as err:
raise exceptions.UnknownException(message=err)
@require_admin_context
def projects_create(context, values):
"""Create a new project with given values."""
session = get_session()
project = models.Project()
if not values.get('id'):
values['id'] = uuid.uuid4()
with session.begin():
project.update(values)
project.save(session)
return project
@require_admin_context
def projects_delete(context, project_id):
"""Delete an existing project given by its id."""
session = get_session()
with session.begin():
query = model_query(context, models.Project, session=session)
query = query.filter_by(id=project_id)
query.delete()
@require_project_admin_context
def users_get_all(context, filters, pagination_params):
"""Get all the users."""
session = get_session()
if is_admin_context(context):
LOG.info("Getting all users as root user")
query = model_query(context, models.User, session=session)
else:
LOG.info("Getting all users as project admin user")
query = model_query(context, models.User, project_only=True,
session=session)
query = query.filter_by(project_id=context.tenant)
return _paginate(context, query, models.User, session, filters,
pagination_params)
@require_project_admin_context
def users_get_by_name(context, user_name, filters, pagination_params):
"""Get all users that match the given username."""
session = get_session()
if is_admin_context(context):
query = model_query(context, models.User, session=session)
else:
query = model_query(context, models.User, project_only=True,
session=session)
query = query.filter_by(username=user_name)
return _paginate(context, query, models.User, session, filters,
pagination_params)
@require_project_admin_context
def users_get_by_id(context, user_id):
"""Get user by its id."""
if is_admin_context(context):
query = model_query(context, models.User)
else:
query = model_query(context, models.User, project_only=True)
query = query.filter_by(id=user_id)
try:
return query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
@require_project_admin_context
def users_create(context, values):
"""Create a new user with given values."""
session = get_session()
user = models.User()
with session.begin():
user.update(values)
user.save(session)
return user
@require_project_admin_context
def users_delete(context, user_id):
"""Delete an existing user given by its id."""
LOG.info("Deleting user with id %s" % user_id)
session = get_session()
with session.begin():
query = model_query(context, models.User, session=session)
query = query.filter_by(id=user_id)
query.delete()
return
def networks_get_all(context, filters, pagination_params):
"""Get all networks."""
session = get_session()
query = model_query(context, models.Network, project_only=True,
session=session)
if "region_id" in filters:
query = query.filter_by(region_id=filters["region_id"])
if "id" in filters:
query = query.filter_by(id=filters["id"])
if "network_type" in filters:
query = query.filter_by(network_type=filters["network_type"])
if "cell_id" in filters:
query = query.filter_by(cell_id=filters["cell_id"])
if "name" in filters:
query = query.filter_by(name=filters["name"])
if "vars" in filters:
query = add_var_filters_to_query(query, models.Network, filters)
return _paginate(context, query, models.Network, session, filters,
pagination_params)
def networks_get_by_id(context, network_id):
"""Get a given network by its id."""
query = model_query(context, models.Network, project_only=True)
query = query.filter_by(id=network_id)
try:
result = query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
return result
def networks_create(context, values):
"""Create a new network."""
session = get_session()
network = models.Network()
with session.begin():
try:
network.update(values)
network.save(session)
except db_exc.DBDuplicateEntry:
raise exceptions.DuplicateNetwork()
return network
def networks_update(context, network_id, values):
"""Update an existing network."""
session = get_session()
with session.begin():
query = model_query(context, models.Network, session=session,
project_only=True)
query = query.filter_by(id=network_id)
network_ref = query.with_for_update().one()
network_ref.update(values)
network_ref.save(session)
return network_ref
def networks_delete(context, network_id):
"""Delete existing network."""
session = get_session()
with session.begin():
query = model_query(context, models.Network, session=session,
project_only=True)
query = query.filter_by(id=network_id)
query.delete()
return
def network_devices_get_all(context, filters, pagination_params):
"""Get all network devices."""
session = get_session()
devices = with_polymorphic(models.Device, [models.NetworkDevice])
query = model_query(context, devices, project_only=True, session=session)
query = query.filter_by(type='network_devices')
if "region_id" in filters:
query = query.filter_by(region_id=filters["region_id"])
if "cloud_id" in filters:
query = query.filter_by(cloud_id=filters["cloud_id"])
if "name" in filters:
query = query.filter_by(name=filters["name"])
if "ip_address" in filters:
query = query.filter_by(ip_address=filters["ip_address"])
if "id" in filters:
query = query.filter_by(id=filters["id"])
if "cell_id" in filters:
query = query.filter_by(cell_id=filters["cell_id"])
if "device_type" in filters:
query = query.filter_by(device_type=filters["device_type"])
if "vars" in filters:
query = add_var_filters_to_query(query, models.Device, filters)
return _paginate(context, query, models.Device, session, filters,
pagination_params)
def network_devices_get_by_id(context, network_device_id):
"""Get a given network device by its id."""
devices = with_polymorphic(models.Device, [models.NetworkDevice])
query = model_query(context, devices, project_only=True)
query = query.filter_by(type='network_devices')
query = query.filter_by(id=network_device_id)
try:
result = query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
return result
def network_devices_create(context, values):
"""Create a new network device."""
session = get_session()
device = models.NetworkDevice()
with session.begin():
device.update(values)
device.save(session)
return device
def network_devices_update(context, network_device_id, values):
"""Update existing network device"""
session = get_session()
with session.begin():
device = with_polymorphic(models.Device, '*')
query = model_query(context, device, session=session,
project_only=True)
query = query.filter_by(type='network_devices')
query = query.filter_by(id=network_device_id)
network_device_ref = query.with_for_update().one()
try:
network_device_ref.update(values)
except exceptions.ParentIDError as e:
raise exceptions.BadRequest(message=str(e))
network_device_ref.save(session)
return network_device_ref
def network_devices_delete(context, network_device_id):
"""Delete existing network device."""
session = get_session()
with session.begin():
device = with_polymorphic(models.Device, '*')
query = model_query(context, device, session=session,
project_only=True)
query = query.filter_by(type='network_devices')
query = query.filter_by(id=network_device_id)
query.delete()
def network_devices_labels_update(context, device_id, labels):
"""Update labels for a network device. Add the label if it is not present
in host labels list, otherwise do nothing."""
return _device_labels_update(context, 'network_devices', device_id, labels)
def network_devices_labels_delete(context, device_id, labels):
"""Delete labels from the network device labels list if it matches
the given label in the query, otherwise do nothing."""
return _device_labels_delete(context, 'network_devices', device_id, labels)
def network_interfaces_get_all(context, filters, pagination_params):
"""Get all network interfaces."""
session = get_session()
query = model_query(context, models.NetworkInterface, project_only=True,
session=session)
if "device_id" in filters:
query = query.filter_by(device_id=filters["device_id"])
if "id" in filters:
query = query.filter_by(id=filters["id"])
if "ip_address" in filters:
query = query.filter_by(ip_address=filters["ip_address"])
if "interface_type" in filters:
query = query.filter_by(interface_type=filters["interface_type"])
return _paginate(context, query, models.NetworkInterface, session,
filters, pagination_params)
def network_interfaces_get_by_id(context, interface_id):
"""Get a given network interface by its id."""
query = model_query(context, models.NetworkInterface, project_only=True)
query = query.filter_by(id=interface_id)
try:
result = query.one()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
return result
def network_interfaces_create(context, values):
"""Create a new network interface."""
session = get_session()
interface = models.NetworkInterface()
with session.begin():
interface.update(values)
interface.save(session)
return interface
def network_interfaces_update(context, interface_id, values):
"""Update an existing network interface."""
session = get_session()
with session.begin():
query = model_query(context, models.NetworkInterface, session=session,
project_only=True)
query = query.filter_by(id=interface_id)
network_interface_ref = query.with_for_update().one()
network_interface_ref.update(values)
network_interface_ref.save(session)
return network_interface_ref
def network_interfaces_delete(context, interface_id):
"""Delete existing network interface."""
session = get_session()
with session.begin():
query = model_query(context, models.NetworkInterface, session=session,
project_only=True)
query = query.filter_by(id=interface_id)
query.delete()
def _marker_from(context, session, model, marker, project_only):
if marker is None:
return None
query = model_query(context, model, session=session,
project_only=project_only)
return query.filter_by(id=marker).one()
def _get_previous(query, model, current_marker, page_size, filters):
# NOTE(sigmavirus24): To get the previous items based on the existing
# filters, we need only reverse the direction that the user requested.
original_sort_dir = filters['sort_dir']
sort_dir = 'desc'
if original_sort_dir == 'desc':
sort_dir = 'asc'
results = db_utils.paginate_query(
query, model,
limit=page_size,
sort_keys=filters['sort_keys'],
sort_dir=sort_dir,
marker=current_marker,
).all()
if not results:
return None
return results[-1].id
def _link_params_for(query, model, filters, pagination_params,
current_marker, current_results):
links = {}
# We can discern our base parameters for our links
base_parameters = {}
for (key, value) in filters.items():
# NOTE(thomasem): Sometimes the filters that are passed in will include
# a None value from the schema. This causes it to not get included
# in the link, since a None value indicates you did not include a value
# for that parameter in the original call.
if value is None:
continue
# This takes care of things like sort_keys which may have multiple
# values
if isinstance(value, list):
value = ','.join(value)
base_parameters[key] = value
base_parameters['limit'] = pagination_params['limit']
generate_links = ('first', 'self')
if current_results:
next_marker = current_results[-1]
# If there are results to return, there may be a next link to follow
generate_links += ('next',)
# We start our links dictionary with some basics
for relation in generate_links:
params = base_parameters.copy()
if relation == 'self':
if pagination_params['marker'] is not None:
params['marker'] = pagination_params['marker']
elif relation == 'next':
params['marker'] = next_marker.id
links[relation] = params
params = base_parameters.copy()
previous_marker = None
if current_marker is not None:
previous_marker = _get_previous(
query, model, current_marker, pagination_params['limit'], filters,
)
if previous_marker is not None:
params['marker'] = previous_marker
links['prev'] = params
return links
def _paginate(context, query, model, session, filters, pagination_params,
project_only=False):
# NOTE(sigmavirus24) Retrieve the instance of the model represented by the
# marker.
try:
marker = _marker_from(context, session, model,
pagination_params['marker'],
project_only)
except sa_exc.NoResultFound:
raise exceptions.BadRequest(
message='Marker "{}" does not exist'.format(
pagination_params['marker']
)
)
except Exception as err:
raise exceptions.UnknownException(message=err)
filters.setdefault('sort_keys', ['created_at', 'id'])
filters.setdefault('sort_dir', 'asc')
# Retrieve the results based on the marker and the limit
try:
results = db_utils.paginate_query(
query, model,
limit=pagination_params['limit'],
sort_keys=filters['sort_keys'],
sort_dir=filters['sort_dir'],
marker=marker,
).all()
except sa_exc.NoResultFound:
raise exceptions.NotFound()
except db_exc.InvalidSortKey as invalid_key:
raise exceptions.BadRequest(
message='"{}" is an invalid sort key'.format(invalid_key.key)
)
except Exception as err:
raise exceptions.UnknownException(message=err)
try:
links = _link_params_for(
query, model, filters, pagination_params, marker, results,
)
except Exception as err:
raise exceptions.UnknownException(message=err)
return results, links