From 6cbb3b5ccc4982ec3afad6b3c1a554d61f2f5e22 Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Fri, 3 May 2013 14:01:05 -0700 Subject: [PATCH] Rename files and fix things. --- .gitignore | 5 + etc/ironic/policy.json | 6 + ironic/__init__.py | 16 + ironic/cmd/__init__.py | 27 ++ ironic/db/__init__.py | 16 + ironic/db/api.py | 178 +++++++ ironic/db/migration.py | 38 ++ ironic/db/sqlalchemy/__init__.py | 14 + ironic/db/sqlalchemy/api.py | 433 ++++++++++++++++++ ironic/db/sqlalchemy/migrate_repo/__init__.py | 14 + ironic/db/sqlalchemy/migrate_repo/migrate.cfg | 20 + .../migrate_repo/versions/001_init.py | 119 +++++ .../versions/002_drop_bm_deployments.py | 68 +++ .../versions/003_add_uuid_to_bm_nodes.py | 39 ++ .../004_add_instance_name_to_bm_nodes.py | 35 ++ .../005_drop_unused_columns_from_nodes.py | 35 ++ .../versions/006_move_prov_mac_address.py | 84 ++++ .../migrate_repo/versions/__init__.py | 14 + ironic/db/sqlalchemy/migration.py | 111 +++++ ironic/db/sqlalchemy/models.py | 75 +++ ironic/db/sqlalchemy/session.py | 65 +++ ironic/manager/__init__.py | 17 + ironic/openstack/common/rpc/__init__.py | 8 +- ironic/tests/__init__.py | 38 ++ ironic/tests/conf_fixture.py | 36 +- ironic/tests/db/__init__.py | 16 + ironic/tests/db/base.py | 50 ++ ironic/tests/fake_policy.py | 2 + ironic/tests/manager/__init__.py | 15 + ironic/tests/policy_fixture.py | 12 +- tools/__init__.py | 0 tools/install_venv_common.py | 22 +- tools/patch_tox_venv.py | 6 +- tools/with_venv.sh | 7 + 34 files changed, 1584 insertions(+), 57 deletions(-) create mode 100644 etc/ironic/policy.json create mode 100644 ironic/__init__.py create mode 100644 ironic/cmd/__init__.py create mode 100644 ironic/db/__init__.py create mode 100644 ironic/db/api.py create mode 100644 ironic/db/migration.py create mode 100644 ironic/db/sqlalchemy/__init__.py create mode 100644 ironic/db/sqlalchemy/api.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/__init__.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/migrate.cfg create mode 100644 ironic/db/sqlalchemy/migrate_repo/versions/001_init.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/versions/002_drop_bm_deployments.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/versions/003_add_uuid_to_bm_nodes.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/versions/004_add_instance_name_to_bm_nodes.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/versions/005_drop_unused_columns_from_nodes.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/versions/006_move_prov_mac_address.py create mode 100644 ironic/db/sqlalchemy/migrate_repo/versions/__init__.py create mode 100644 ironic/db/sqlalchemy/migration.py create mode 100644 ironic/db/sqlalchemy/models.py create mode 100644 ironic/db/sqlalchemy/session.py create mode 100644 ironic/manager/__init__.py create mode 100644 ironic/tests/__init__.py create mode 100644 ironic/tests/db/__init__.py create mode 100644 ironic/tests/db/base.py create mode 100644 ironic/tests/manager/__init__.py delete mode 100644 tools/__init__.py create mode 100755 tools/with_venv.sh diff --git a/.gitignore b/.gitignore index 1f39df9b..1fe4a33f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ develop-eggs .installed.cfg # Other +*.DS_Store .testrepository .tox .*.swp @@ -28,3 +29,7 @@ develop-eggs cover AUTHORS ChangeLog + +.testrepository/ +.tox +.venv diff --git a/etc/ironic/policy.json b/etc/ironic/policy.json new file mode 100644 index 00000000..b9458e8f --- /dev/null +++ b/etc/ironic/policy.json @@ -0,0 +1,6 @@ +{ + "admin_api": "is_admin:True", + "admin_or_owner": "is_admin:True or project_id:%(project_id)s", + "context_is_admin": "role:admin", + "default": "rule:admin_or_owner", +} diff --git a/ironic/__init__.py b/ironic/__init__.py new file mode 100644 index 00000000..56425d0f --- /dev/null +++ b/ironic/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/ironic/cmd/__init__.py b/ironic/cmd/__init__.py new file mode 100644 index 00000000..52a31541 --- /dev/null +++ b/ironic/cmd/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# TODO(mikal): move eventlet imports to ironic.__init__ once we move to PBR +import os +import sys + +os.environ['EVENTLET_NO_GREENDNS'] = 'yes' + +import eventlet + +eventlet.monkey_patch(os=False) + +from ironic.openstack.common import gettextutils +gettextutils.install('ironic') diff --git a/ironic/db/__init__.py b/ironic/db/__init__.py new file mode 100644 index 00000000..93994b1a --- /dev/null +++ b/ironic/db/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# flake8: noqa +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironic.db.api import * diff --git a/ironic/db/api.py b/ironic/db/api.py new file mode 100644 index 00000000..b3c2e619 --- /dev/null +++ b/ironic/db/api.py @@ -0,0 +1,178 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 NTT DOCOMO, INC. +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Defines interface for DB access. + +The underlying driver is loaded as a :class:`LazyPluggable`. + +Functions in this module are imported into the ironic.db +namespace. Call these functions from ironic.db namespace, not +the ironic.db.api namespace. + +All functions in this module return objects that implement a dictionary-like +interface. Currently, many of these objects are sqlalchemy objects that +implement a dictionary interface. However, a future goal is to have all of +these objects be simple dictionaries. + + +**Related Flags** + +:db_backend: string to lookup in the list of LazyPluggable backends. + `sqlalchemy` is the only supported backend right now. + +:sql_connection: string specifying the sqlalchemy connection to + use, like: `sqlite:///var/lib/ironic/ironic.sqlite`. + +""" + +from oslo.config import cfg + +from ironic import utils + +db_opts = [ + cfg.StrOpt('db_backend', + default='sqlalchemy', + help='The backend to use for the ironic database'), + ] + +CONF = cfg.CONF +CONF.register_opts(db_opts) + +IMPL = utils.LazyPluggable( + 'db_backend', + sqlalchemy='ironic.db.sqlalchemy.api') + + +def bm_node_get_all(context, service_host=None): + return IMPL.bm_node_get_all(context, + service_host=service_host) + + +def bm_node_get_associated(context, service_host=None): + return IMPL.bm_node_get_associated(context, + service_host=service_host) + + +def bm_node_get_unassociated(context, service_host=None): + return IMPL.bm_node_get_unassociated(context, + service_host=service_host) + + +def bm_node_find_free(context, service_host=None, + memory_mb=None, cpus=None, local_gb=None): + return IMPL.bm_node_find_free(context, + service_host=service_host, + memory_mb=memory_mb, + cpus=cpus, + local_gb=local_gb) + + +def bm_node_get(context, bm_node_id): + return IMPL.bm_node_get(context, bm_node_id) + + +def bm_node_get_by_instance_uuid(context, instance_uuid): + return IMPL.bm_node_get_by_instance_uuid(context, + instance_uuid) + + +def bm_node_get_by_node_uuid(context, node_uuid): + return IMPL.bm_node_get_by_node_uuid(context, node_uuid) + + +def bm_node_create(context, values): + return IMPL.bm_node_create(context, values) + + +def bm_node_destroy(context, bm_node_id): + return IMPL.bm_node_destroy(context, bm_node_id) + + +def bm_node_update(context, bm_node_id, values): + return IMPL.bm_node_update(context, bm_node_id, values) + + +def bm_node_associate_and_update(context, node_uuid, values): + return IMPL.bm_node_associate_and_update(context, node_uuid, values) + + +def bm_pxe_ip_create(context, address, server_address): + return IMPL.bm_pxe_ip_create(context, address, server_address) + + +def bm_pxe_ip_create_direct(context, bm_pxe_ip): + return IMPL.bm_pxe_ip_create_direct(context, bm_pxe_ip) + + +def bm_pxe_ip_destroy(context, ip_id): + return IMPL.bm_pxe_ip_destroy(context, ip_id) + + +def bm_pxe_ip_destroy_by_address(context, address): + return IMPL.bm_pxe_ip_destroy_by_address(context, address) + + +def bm_pxe_ip_get_all(context): + return IMPL.bm_pxe_ip_get_all(context) + + +def bm_pxe_ip_get(context, ip_id): + return IMPL.bm_pxe_ip_get(context, ip_id) + + +def bm_pxe_ip_get_by_bm_node_id(context, bm_node_id): + return IMPL.bm_pxe_ip_get_by_bm_node_id(context, bm_node_id) + + +def bm_pxe_ip_associate(context, bm_node_id): + return IMPL.bm_pxe_ip_associate(context, bm_node_id) + + +def bm_pxe_ip_disassociate(context, bm_node_id): + return IMPL.bm_pxe_ip_disassociate(context, bm_node_id) + + +def bm_interface_get(context, if_id): + return IMPL.bm_interface_get(context, if_id) + + +def bm_interface_get_all(context): + return IMPL.bm_interface_get_all(context) + + +def bm_interface_destroy(context, if_id): + return IMPL.bm_interface_destroy(context, if_id) + + +def bm_interface_create(context, bm_node_id, address, datapath_id, port_no): + return IMPL.bm_interface_create(context, bm_node_id, address, + datapath_id, port_no) + + +def bm_interface_set_vif_uuid(context, if_id, vif_uuid): + return IMPL.bm_interface_set_vif_uuid(context, if_id, vif_uuid) + + +def bm_interface_get_by_vif_uuid(context, vif_uuid): + return IMPL.bm_interface_get_by_vif_uuid(context, vif_uuid) + + +def bm_interface_get_all_by_bm_node_id(context, bm_node_id): + return IMPL.bm_interface_get_all_by_bm_node_id(context, bm_node_id) diff --git a/ironic/db/migration.py b/ironic/db/migration.py new file mode 100644 index 00000000..19f3ee85 --- /dev/null +++ b/ironic/db/migration.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Database setup and migration commands.""" + +from ironic import utils + + +IMPL = utils.LazyPluggable( + 'db_backend', + sqlalchemy='ironic.db.sqlalchemy.migration') + +INIT_VERSION = 0 + + +def db_sync(version=None): + """Migrate the database to `version` or the most recent version.""" + return IMPL.db_sync(version=version) + + +def db_version(): + """Display the current database version.""" + return IMPL.db_version() diff --git a/ironic/db/sqlalchemy/__init__.py b/ironic/db/sqlalchemy/__init__.py new file mode 100644 index 00000000..19071662 --- /dev/null +++ b/ironic/db/sqlalchemy/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py new file mode 100644 index 00000000..ff529ae1 --- /dev/null +++ b/ironic/db/sqlalchemy/api.py @@ -0,0 +1,433 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 NTT DOCOMO, INC. +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Implementation of SQLAlchemy backend.""" + +import uuid + +from sqlalchemy.sql.expression import asc +from sqlalchemy.sql.expression import literal_column + +import nova.context +from nova.db.sqlalchemy import api as sqlalchemy_api +from nova import exception +from ironic.openstack.common.db import exception as db_exc +from ironic.openstack.common import timeutils +from ironic.openstack.common import uuidutils +from nova.virt.baremetal.db.sqlalchemy import models +from nova.virt.baremetal.db.sqlalchemy import session as db_session + + +def model_query(context, *args, **kwargs): + """Query helper that accounts for context's `read_deleted` field. + + :param context: context to query under + :param session: if present, the session to use + :param read_deleted: if present, overrides context's read_deleted field. + :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 db_session.get_session() + read_deleted = kwargs.get('read_deleted') or context.read_deleted + project_only = kwargs.get('project_only') + + query = session.query(*args) + + if read_deleted == 'no': + query = query.filter_by(deleted=False) + elif read_deleted == 'yes': + pass # omit the filter to include deleted and active + elif read_deleted == 'only': + query = query.filter_by(deleted=True) + else: + raise Exception( + _("Unrecognized read_deleted value '%s'") % read_deleted) + + if project_only and nova.context.is_user_context(context): + query = query.filter_by(project_id=context.project_id) + + return query + + +def _save(ref, session=None): + if not session: + session = db_session.get_session() + # We must not call ref.save() with session=None, otherwise NovaBase + # uses nova-db's session, which cannot access bm-db. + ref.save(session=session) + + +def _build_node_order_by(query): + query = query.order_by(asc(models.BareMetalNode.memory_mb)) + query = query.order_by(asc(models.BareMetalNode.cpus)) + query = query.order_by(asc(models.BareMetalNode.local_gb)) + return query + + +@sqlalchemy_api.require_admin_context +def bm_node_get_all(context, service_host=None): + query = model_query(context, models.BareMetalNode, read_deleted="no") + if service_host: + query = query.filter_by(service_host=service_host) + return query.all() + + +@sqlalchemy_api.require_admin_context +def bm_node_get_associated(context, service_host=None): + query = model_query(context, models.BareMetalNode, read_deleted="no").\ + filter(models.BareMetalNode.instance_uuid is not None) + if service_host: + query = query.filter_by(service_host=service_host) + return query.all() + + +@sqlalchemy_api.require_admin_context +def bm_node_get_unassociated(context, service_host=None): + query = model_query(context, models.BareMetalNode, read_deleted="no").\ + filter(models.BareMetalNode.instance_uuid is None) + if service_host: + query = query.filter_by(service_host=service_host) + return query.all() + + +@sqlalchemy_api.require_admin_context +def bm_node_find_free(context, service_host=None, + cpus=None, memory_mb=None, local_gb=None): + query = model_query(context, models.BareMetalNode, read_deleted="no") + query = query.filter(models.BareMetalNode.instance_uuid is None) + if service_host: + query = query.filter_by(service_host=service_host) + if cpus is not None: + query = query.filter(models.BareMetalNode.cpus >= cpus) + if memory_mb is not None: + query = query.filter(models.BareMetalNode.memory_mb >= memory_mb) + if local_gb is not None: + query = query.filter(models.BareMetalNode.local_gb >= local_gb) + query = _build_node_order_by(query) + return query.first() + + +@sqlalchemy_api.require_admin_context +def bm_node_get(context, bm_node_id): + # bm_node_id may be passed as a string. Convert to INT to improve DB perf. + bm_node_id = int(bm_node_id) + result = model_query(context, models.BareMetalNode, read_deleted="no").\ + filter_by(id=bm_node_id).\ + first() + + if not result: + raise exception.NodeNotFound(node_id=bm_node_id) + + return result + + +@sqlalchemy_api.require_admin_context +def bm_node_get_by_instance_uuid(context, instance_uuid): + if not uuidutils.is_uuid_like(instance_uuid): + raise exception.InstanceNotFound(instance_id=instance_uuid) + + result = model_query(context, models.BareMetalNode, read_deleted="no").\ + filter_by(instance_uuid=instance_uuid).\ + first() + + if not result: + raise exception.InstanceNotFound(instance_id=instance_uuid) + + return result + + +@sqlalchemy_api.require_admin_context +def bm_node_get_by_node_uuid(context, bm_node_uuid): + result = model_query(context, models.BareMetalNode, read_deleted="no").\ + filter_by(uuid=bm_node_uuid).\ + first() + + if not result: + raise exception.NodeNotFoundByUUID(node_uuid=bm_node_uuid) + + return result + + +@sqlalchemy_api.require_admin_context +def bm_node_create(context, values): + if not values.get('uuid'): + values['uuid'] = str(uuid.uuid4()) + bm_node_ref = models.BareMetalNode() + bm_node_ref.update(values) + _save(bm_node_ref) + return bm_node_ref + + +@sqlalchemy_api.require_admin_context +def bm_node_update(context, bm_node_id, values): + rows = model_query(context, models.BareMetalNode, read_deleted="no").\ + filter_by(id=bm_node_id).\ + update(values) + + if not rows: + raise exception.NodeNotFound(node_id=bm_node_id) + + +@sqlalchemy_api.require_admin_context +def bm_node_associate_and_update(context, node_uuid, values): + """Associate an instance to a node safely + + Associate an instance to a node only if that node is not yet assocated. + Allow the caller to set any other fields they require in the same + operation. For example, this is used to set the node's task_state to + BUILDING at the beginning of driver.spawn(). + + """ + if 'instance_uuid' not in values: + raise exception.NovaException(_( + "instance_uuid must be supplied to bm_node_associate_and_update")) + + session = db_session.get_session() + with session.begin(): + query = model_query(context, models.BareMetalNode, + session=session, read_deleted="no").\ + filter_by(uuid=node_uuid) + + count = query.filter_by(instance_uuid=None).\ + update(values, synchronize_session=False) + if count != 1: + raise exception.NovaException(_( + "Failed to associate instance %(i_uuid)s to baremetal node " + "%(n_uuid)s.") % {'i_uuid': values['instance_uuid'], + 'n_uuid': node_uuid}) + ref = query.first() + return ref + + +@sqlalchemy_api.require_admin_context +def bm_node_destroy(context, bm_node_id): + # First, delete all interfaces belonging to the node. + # Delete physically since these have unique columns. + session = db_session.get_session() + with session.begin(): + model_query(context, models.BareMetalInterface, read_deleted="no").\ + filter_by(bm_node_id=bm_node_id).\ + delete() + rows = model_query(context, models.BareMetalNode, read_deleted="no").\ + filter_by(id=bm_node_id).\ + update({'deleted': True, + 'deleted_at': timeutils.utcnow(), + 'updated_at': literal_column('updated_at')}) + + if not rows: + raise exception.NodeNotFound(node_id=bm_node_id) + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_get_all(context): + query = model_query(context, models.BareMetalPxeIp, read_deleted="no") + return query.all() + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_create(context, address, server_address): + ref = models.BareMetalPxeIp() + ref.address = address + ref.server_address = server_address + _save(ref) + return ref + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_create_direct(context, bm_pxe_ip): + ref = bm_pxe_ip_create(context, + address=bm_pxe_ip['address'], + server_address=bm_pxe_ip['server_address']) + return ref + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_destroy(context, ip_id): + # Delete physically since it has unique columns + model_query(context, models.BareMetalPxeIp, read_deleted="no").\ + filter_by(id=ip_id).\ + delete() + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_destroy_by_address(context, address): + # Delete physically since it has unique columns + model_query(context, models.BareMetalPxeIp, read_deleted="no").\ + filter_by(address=address).\ + delete() + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_get(context, ip_id): + result = model_query(context, models.BareMetalPxeIp, read_deleted="no").\ + filter_by(id=ip_id).\ + first() + + return result + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_get_by_bm_node_id(context, bm_node_id): + result = model_query(context, models.BareMetalPxeIp, read_deleted="no").\ + filter_by(bm_node_id=bm_node_id).\ + first() + + if not result: + raise exception.NodeNotFound(node_id=bm_node_id) + + return result + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_associate(context, bm_node_id): + session = db_session.get_session() + with session.begin(): + # Check if the node really exists + node_ref = model_query(context, models.BareMetalNode, + read_deleted="no", session=session).\ + filter_by(id=bm_node_id).\ + first() + if not node_ref: + raise exception.NodeNotFound(node_id=bm_node_id) + + # Check if the node already has a pxe_ip + ip_ref = model_query(context, models.BareMetalPxeIp, + read_deleted="no", session=session).\ + filter_by(bm_node_id=bm_node_id).\ + first() + if ip_ref: + return ip_ref.id + + # with_lockmode('update') and filter_by(bm_node_id=None) will lock all + # records. It may cause a performance problem in high-concurrency + # environment. + ip_ref = model_query(context, models.BareMetalPxeIp, + read_deleted="no", session=session).\ + filter_by(bm_node_id=None).\ + with_lockmode('update').\ + first() + + # this exception is not caught in nova/compute/manager + if not ip_ref: + raise exception.NovaException(_("No more PXE IPs available")) + + ip_ref.bm_node_id = bm_node_id + session.add(ip_ref) + return ip_ref.id + + +@sqlalchemy_api.require_admin_context +def bm_pxe_ip_disassociate(context, bm_node_id): + model_query(context, models.BareMetalPxeIp, read_deleted="no").\ + filter_by(bm_node_id=bm_node_id).\ + update({'bm_node_id': None}) + + +@sqlalchemy_api.require_admin_context +def bm_interface_get(context, if_id): + result = model_query(context, models.BareMetalInterface, + read_deleted="no").\ + filter_by(id=if_id).\ + first() + + if not result: + raise exception.NovaException(_("Baremetal interface %s " + "not found") % if_id) + + return result + + +@sqlalchemy_api.require_admin_context +def bm_interface_get_all(context): + query = model_query(context, models.BareMetalInterface, + read_deleted="no") + return query.all() + + +@sqlalchemy_api.require_admin_context +def bm_interface_destroy(context, if_id): + # Delete physically since it has unique columns + model_query(context, models.BareMetalInterface, read_deleted="no").\ + filter_by(id=if_id).\ + delete() + + +@sqlalchemy_api.require_admin_context +def bm_interface_create(context, bm_node_id, address, datapath_id, port_no): + ref = models.BareMetalInterface() + ref.bm_node_id = bm_node_id + ref.address = address + ref.datapath_id = datapath_id + ref.port_no = port_no + _save(ref) + return ref.id + + +@sqlalchemy_api.require_admin_context +def bm_interface_set_vif_uuid(context, if_id, vif_uuid): + session = db_session.get_session() + with session.begin(): + bm_interface = model_query(context, models.BareMetalInterface, + read_deleted="no", session=session).\ + filter_by(id=if_id).\ + with_lockmode('update').\ + first() + if not bm_interface: + raise exception.NovaException(_("Baremetal interface %s " + "not found") % if_id) + + bm_interface.vif_uuid = vif_uuid + try: + session.add(bm_interface) + session.flush() + except db_exc.DBError as e: + # TODO(deva): clean up when db layer raises DuplicateKeyError + if str(e).find('IntegrityError') != -1: + raise exception.NovaException(_("Baremetal interface %s " + "already in use") % vif_uuid) + else: + raise e + + +@sqlalchemy_api.require_admin_context +def bm_interface_get_by_vif_uuid(context, vif_uuid): + result = model_query(context, models.BareMetalInterface, + read_deleted="no").\ + filter_by(vif_uuid=vif_uuid).\ + first() + + if not result: + raise exception.NovaException(_("Baremetal virtual interface %s " + "not found") % vif_uuid) + + return result + + +@sqlalchemy_api.require_admin_context +def bm_interface_get_all_by_bm_node_id(context, bm_node_id): + result = model_query(context, models.BareMetalInterface, + read_deleted="no").\ + filter_by(bm_node_id=bm_node_id).\ + all() + + if not result: + raise exception.NodeNotFound(node_id=bm_node_id) + + return result diff --git a/ironic/db/sqlalchemy/migrate_repo/__init__.py b/ironic/db/sqlalchemy/migrate_repo/__init__.py new file mode 100644 index 00000000..19071662 --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/ironic/db/sqlalchemy/migrate_repo/migrate.cfg b/ironic/db/sqlalchemy/migrate_repo/migrate.cfg new file mode 100644 index 00000000..368e93a5 --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/migrate.cfg @@ -0,0 +1,20 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=nova_bm + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/001_init.py b/ironic/db/sqlalchemy/migrate_repo/versions/001_init.py new file mode 100644 index 00000000..e4fc8e5c --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/versions/001_init.py @@ -0,0 +1,119 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Boolean, Column, DateTime +from sqlalchemy import Index, Integer, MetaData, String, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + bm_nodes = Table('bm_nodes', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('deleted', Boolean), + Column('id', Integer, primary_key=True, nullable=False), + Column('cpus', Integer), + Column('memory_mb', Integer), + Column('local_gb', Integer), + Column('pm_address', String(length=255)), + Column('pm_user', String(length=255)), + Column('pm_password', String(length=255)), + Column('service_host', String(length=255)), + Column('prov_mac_address', String(length=255)), + Column('instance_uuid', String(length=36)), + Column('registration_status', String(length=16)), + Column('task_state', String(length=255)), + Column('prov_vlan_id', Integer), + Column('terminal_port', Integer), + mysql_engine='InnoDB', + #mysql_charset='utf8' + ) + + bm_interfaces = Table('bm_interfaces', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('deleted', Boolean), + Column('id', Integer, primary_key=True, nullable=False), + Column('bm_node_id', Integer), + Column('address', String(length=255), unique=True), + Column('datapath_id', String(length=255)), + Column('port_no', Integer), + Column('vif_uuid', String(length=36), unique=True), + mysql_engine='InnoDB', + #mysql_charset='utf8' + ) + + bm_pxe_ips = Table('bm_pxe_ips', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('deleted', Boolean), + Column('id', Integer, primary_key=True, nullable=False), + Column('address', String(length=255), unique=True), + Column('bm_node_id', Integer), + Column('server_address', String(length=255), unique=True), + mysql_engine='InnoDB', + #mysql_charset='utf8' + ) + + bm_deployments = Table('bm_deployments', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('deleted', Boolean), + Column('id', Integer, primary_key=True, nullable=False), + Column('bm_node_id', Integer), + Column('key', String(length=255)), + Column('image_path', String(length=255)), + Column('pxe_config_path', String(length=255)), + Column('root_mb', Integer), + Column('swap_mb', Integer), + mysql_engine='InnoDB', + #mysql_charset='utf8' + ) + + bm_nodes.create() + bm_interfaces.create() + bm_pxe_ips.create() + bm_deployments.create() + + Index('idx_bm_nodes_service_host_deleted', + bm_nodes.c.service_host, bm_nodes.c.deleted)\ + .create(migrate_engine) + Index('idx_bm_nodes_instance_uuid_deleted', + bm_nodes.c.instance_uuid, bm_nodes.c.deleted)\ + .create(migrate_engine) + Index('idx_bm_nodes_hmcld', + bm_nodes.c.service_host, bm_nodes.c.memory_mb, bm_nodes.c.cpus, + bm_nodes.c.local_gb, bm_nodes.c.deleted)\ + .create(migrate_engine) + + Index('idx_bm_interfaces_bm_node_id_deleted', + bm_interfaces.c.bm_node_id, bm_interfaces.c.deleted)\ + .create(migrate_engine) + + Index('idx_bm_pxe_ips_bm_node_id_deleted', + bm_pxe_ips.c.bm_node_id, bm_pxe_ips.c.deleted)\ + .create(migrate_engine) + + +def downgrade(migrate_engine): + raise NotImplementedError('Downgrade from 001_init is unsupported.') diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/002_drop_bm_deployments.py b/ironic/db/sqlalchemy/migrate_repo/versions/002_drop_bm_deployments.py new file mode 100644 index 00000000..a9fc649e --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/versions/002_drop_bm_deployments.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, Index, MetaData, Table +from sqlalchemy import Integer, String, DateTime, Boolean + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + bm_nodes = Table('bm_nodes', meta, autoload=True) + + image_path = Column('image_path', String(length=255)) + pxe_config_path = Column('pxe_config_path', String(length=255)) + deploy_key = Column('deploy_key', String(length=255)) + root_mb = Column('root_mb', Integer()) + swap_mb = Column('swap_mb', Integer()) + + for c in [image_path, pxe_config_path, deploy_key, root_mb, swap_mb]: + bm_nodes.create_column(c) + + deploy_key_idx = Index('deploy_key_idx', bm_nodes.c.deploy_key) + deploy_key_idx.create(migrate_engine) + + bm_deployments = Table('bm_deployments', meta, autoload=True) + bm_deployments.drop() + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + bm_nodes = Table('bm_nodes', meta, autoload=True) + + for c in ['image_path', 'pxe_config_path', 'deploy_key', 'root_mb', + 'swap_mb']: + bm_nodes.drop_column(c) + + bm_deployments = Table('bm_deployments', meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('deleted', Boolean), + Column('id', Integer, primary_key=True, nullable=False), + Column('bm_node_id', Integer), + Column('key', String(length=255)), + Column('image_path', String(length=255)), + Column('pxe_config_path', String(length=255)), + Column('root_mb', Integer), + Column('swap_mb', Integer), + mysql_engine='InnoDB', + ) + bm_deployments.create() diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/003_add_uuid_to_bm_nodes.py b/ironic/db/sqlalchemy/migrate_repo/versions/003_add_uuid_to_bm_nodes.py new file mode 100644 index 00000000..77871909 --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/versions/003_add_uuid_to_bm_nodes.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, MetaData, String, Table, Index + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + t = Table('bm_nodes', meta, autoload=True) + uuid_col = Column('uuid', String(36)) + t.create_column(uuid_col) + + uuid_ux = Index('uuid_ux', t.c.uuid, unique=True) + uuid_ux.create(migrate_engine) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + t = Table('bm_nodes', meta, autoload=True) + + t.drop_column('uuid') diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/004_add_instance_name_to_bm_nodes.py b/ironic/db/sqlalchemy/migrate_repo/versions/004_add_instance_name_to_bm_nodes.py new file mode 100644 index 00000000..6f85b9ce --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/versions/004_add_instance_name_to_bm_nodes.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, MetaData, String, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + t = Table('bm_nodes', meta, autoload=True) + name_col = Column('instance_name', String(255)) + t.create_column(name_col) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + t = Table('bm_nodes', meta, autoload=True) + t.drop_column('instance_name') diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/005_drop_unused_columns_from_nodes.py b/ironic/db/sqlalchemy/migrate_repo/versions/005_drop_unused_columns_from_nodes.py new file mode 100644 index 00000000..cb361f94 --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/versions/005_drop_unused_columns_from_nodes.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 NTT DOCOMO, INC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, String, Integer, MetaData, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + nodes = Table('bm_nodes', meta, autoload=True) + nodes.drop_column('prov_vlan_id') + nodes.drop_column('registration_status') + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + nodes = Table('bm_nodes', meta, autoload=True) + nodes.create_column(Column('prov_vlan_id', Integer)) + nodes.create_column(Column('registration_status', String(length=16))) diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/006_move_prov_mac_address.py b/ironic/db/sqlalchemy/migrate_repo/versions/006_move_prov_mac_address.py new file mode 100644 index 00000000..6c249067 --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/versions/006_move_prov_mac_address.py @@ -0,0 +1,84 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 NTT DOCOMO, INC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironic.openstack.common import log as logging +from sqlalchemy import and_, MetaData, select, Table, exists +from sqlalchemy import exc + +LOG = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + nodes = Table('bm_nodes', meta, autoload=True) + ifs = Table('bm_interfaces', meta, autoload=True) + + q = select([nodes.c.id, nodes.c.prov_mac_address], + from_obj=nodes) + + # Iterate all elements before starting insert since IntegrityError + # may disturb the iteration. + node_address = {} + for node_id, address in q.execute(): + node_address[node_id] = address + + i = ifs.insert() + for node_id, address in node_address.iteritems(): + try: + i.execute({'bm_node_id': node_id, 'address': address}) + except exc.IntegrityError: + # The address is registered in both bm_nodes and bm_interfaces. + # It is expected. + pass + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + nodes = Table('bm_nodes', meta, autoload=True) + ifs = Table('bm_interfaces', meta, autoload=True) + + subq = exists().where(and_(ifs.c.bm_node_id == nodes.c.id, + ifs.c.address == nodes.c.prov_mac_address)) + + ifs.delete().where(subq).execute() + + # NOTE(arata): + # In fact, this downgrade may not return the db to the previous state. + # It seems to be not so match a problem, so this is just for memo. + # + # Think these two state before upgrading: + # + # (A) address 'x' is duplicate + # bm_nodes.prov_mac_address='x' + # bm_interfaces.address=['x', 'y'] + # + # (B) no address is duplicate + # bm_nodes.prov_mac_address='x' + # bm_interfaces.address=['y'] + # + # Upgrading them results in the same state: + # + # bm_nodes.prov_mac_address='x' + # bm_interfaces.address=['x', 'y'] + # + # Downgrading this results in B, even if the actual initial status was A + # Of course we can change it to downgrade to B, but then we cannot + # downgrade to A; it is an exclusive choice since we do not have + # information about the initial state. diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/__init__.py b/ironic/db/sqlalchemy/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..19071662 --- /dev/null +++ b/ironic/db/sqlalchemy/migrate_repo/versions/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/ironic/db/sqlalchemy/migration.py b/ironic/db/sqlalchemy/migration.py new file mode 100644 index 00000000..7af6bc73 --- /dev/null +++ b/ironic/db/sqlalchemy/migration.py @@ -0,0 +1,111 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import distutils.version as dist_version +import migrate +from migrate.versioning import util as migrate_util +import os +import sqlalchemy + +from ironic import exception +from ironic.db import migration +from ironic.db.sqlalchemy import session + + +@migrate_util.decorator +def patched_with_engine(f, *a, **kw): + url = a[0] + engine = migrate_util.construct_engine(url, **kw) + + try: + kw['engine'] = engine + return f(*a, **kw) + finally: + if isinstance(engine, migrate_util.Engine) and engine is not url: + migrate_util.log.debug('Disposing SQLAlchemy engine %s', engine) + engine.dispose() + + +# TODO(jkoelker) When migrate 0.7.3 is released and nova depends +# on that version or higher, this can be removed +MIN_PKG_VERSION = dist_version.StrictVersion('0.7.3') +if (not hasattr(migrate, '__version__') or + dist_version.StrictVersion(migrate.__version__) < MIN_PKG_VERSION): + migrate_util.with_engine = patched_with_engine + + +# NOTE(jkoelker) Delay importing migrate until we are patched +from migrate import exceptions as versioning_exceptions +from migrate.versioning import api as versioning_api +from migrate.versioning.repository import Repository + + +_REPOSITORY = None + + +def db_sync(version=None): + if version is not None: + try: + version = int(version) + except ValueError: + raise exception.NovaException(_("version should be an integer")) + + current_version = db_version() + repository = _find_migrate_repo() + if version is None or version > current_version: + return versioning_api.upgrade(session.get_engine(), repository, + version) + else: + return versioning_api.downgrade(session.get_engine(), repository, + version) + + +def db_version(): + repository = _find_migrate_repo() + try: + return versioning_api.db_version(session.get_engine(), repository) + except versioning_exceptions.DatabaseNotControlledError: + meta = sqlalchemy.MetaData() + engine = session.get_engine() + meta.reflect(bind=engine) + tables = meta.tables + if len(tables) == 0: + db_version_control(migration.INIT_VERSION) + return versioning_api.db_version(session.get_engine(), repository) + else: + # Some pre-Essex DB's may not be version controlled. + # Require them to upgrade using Essex first. + raise exception.NovaException( + _("Upgrade DB using Essex release first.")) + + +def db_version_control(version=None): + repository = _find_migrate_repo() + versioning_api.version_control(session.get_engine(), repository, version) + return version + + +def _find_migrate_repo(): + """Get the path for the migrate repository.""" + global _REPOSITORY + path = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'migrate_repo') + assert os.path.exists(path) + if _REPOSITORY is None: + _REPOSITORY = Repository(path) + return _REPOSITORY diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py new file mode 100644 index 00000000..61063f03 --- /dev/null +++ b/ironic/db/sqlalchemy/models.py @@ -0,0 +1,75 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +SQLAlchemy models for baremetal data. +""" + +from sqlalchemy import Column, Boolean, Integer, String +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import ForeignKey, Text + +from nova.db.sqlalchemy import models + + +BASE = declarative_base() + + +class BareMetalNode(BASE, models.NovaBase): + """Represents a bare metal node.""" + + __tablename__ = 'bm_nodes' + id = Column(Integer, primary_key=True) + deleted = Column(Boolean, default=False) + uuid = Column(String(36)) + service_host = Column(String(255)) + instance_uuid = Column(String(36), nullable=True) + instance_name = Column(String(255), nullable=True) + cpus = Column(Integer) + memory_mb = Column(Integer) + local_gb = Column(Integer) + pm_address = Column(Text) + pm_user = Column(Text) + pm_password = Column(Text) + prov_mac_address = Column(Text) + task_state = Column(String(255)) + terminal_port = Column(Integer) + image_path = Column(String(255), nullable=True) + pxe_config_path = Column(String(255), nullable=True) + deploy_key = Column(String(255), nullable=True) + root_mb = Column(Integer) + swap_mb = Column(Integer) + + +class BareMetalPxeIp(BASE, models.NovaBase): + __tablename__ = 'bm_pxe_ips' + id = Column(Integer, primary_key=True) + deleted = Column(Boolean, default=False) + address = Column(String(255), unique=True) + server_address = Column(String(255), unique=True) + bm_node_id = Column(Integer, ForeignKey('bm_nodes.id'), nullable=True) + + +class BareMetalInterface(BASE, models.NovaBase): + __tablename__ = 'bm_interfaces' + id = Column(Integer, primary_key=True) + deleted = Column(Boolean, default=False) + bm_node_id = Column(Integer, ForeignKey('bm_nodes.id'), nullable=True) + address = Column(String(255), unique=True) + datapath_id = Column(String(255)) + port_no = Column(Integer) + vif_uuid = Column(String(36), unique=True) diff --git a/ironic/db/sqlalchemy/session.py b/ironic/db/sqlalchemy/session.py new file mode 100644 index 00000000..c23bb317 --- /dev/null +++ b/ironic/db/sqlalchemy/session.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 NTT DOCOMO, INC. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Session Handling for SQLAlchemy backend.""" + +from oslo.config import cfg + +from ironic.openstack.common.db.sqlalchemy import session as nova_session +from nova import paths + +opts = [ + cfg.StrOpt('sql_connection', + default=('sqlite:///' + + paths.state_path_def('baremetal_$sqlite_db')), + help='The SQLAlchemy connection string used to connect to the ' + 'bare-metal database'), + ] + +baremetal_group = cfg.OptGroup(name='baremetal', + title='Baremetal Options') + +CONF = cfg.CONF +CONF.register_group(baremetal_group) +CONF.register_opts(opts, baremetal_group) + +CONF.import_opt('sqlite_db', 'ironic.openstack.common.db.sqlalchemy.session') + +_ENGINE = None +_MAKER = None + + +def get_session(autocommit=True, expire_on_commit=False): + """Return a SQLAlchemy session.""" + global _MAKER + + if _MAKER is None: + engine = get_engine() + _MAKER = nova_session.get_maker(engine, autocommit, expire_on_commit) + + session = _MAKER() + return session + + +def get_engine(): + """Return a SQLAlchemy engine.""" + global _ENGINE + if _ENGINE is None: + _ENGINE = nova_session.create_engine(CONF.baremetal.sql_connection) + return _ENGINE diff --git a/ironic/manager/__init__.py b/ironic/manager/__init__.py new file mode 100644 index 00000000..da37b206 --- /dev/null +++ b/ironic/manager/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from ironic.manager import driver + +BareMetalDriver = driver.BareMetalDriver diff --git a/ironic/openstack/common/rpc/__init__.py b/ironic/openstack/common/rpc/__init__.py index adea0a59..2695f072 100644 --- a/ironic/openstack/common/rpc/__init__.py +++ b/ironic/openstack/common/rpc/__init__.py @@ -297,11 +297,5 @@ def _get_impl(): """Delay import of rpc_backend until configuration is loaded.""" global _RPCIMPL if _RPCIMPL is None: - try: - _RPCIMPL = importutils.import_module(CONF.rpc_backend) - except ImportError: - # For backwards compatibility with older nova config. - impl = CONF.rpc_backend.replace('nova.rpc', - 'nova.openstack.common.rpc') - _RPCIMPL = importutils.import_module(impl) + _RPCIMPL = importutils.import_module(CONF.rpc_backend) return _RPCIMPL diff --git a/ironic/tests/__init__.py b/ironic/tests/__init__.py new file mode 100644 index 00000000..cf3103ea --- /dev/null +++ b/ironic/tests/__init__.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +:mod:`Ironic.tests` -- ironic Unittests +===================================================== + +.. automodule:: ironic.tests + :platform: Unix +""" + +# TODO(mikal): move eventlet imports to ironic.__init__ once we move to PBR +import os +import sys + +import eventlet + +eventlet.monkey_patch(os=False) + +# See http://code.google.com/p/python-nose/issues/detail?id=373 +# The code below enables nosetests to work with i18n _() blocks +import __builtin__ +setattr(__builtin__, '_', lambda x: x) diff --git a/ironic/tests/conf_fixture.py b/ironic/tests/conf_fixture.py index 697f4ed4..321c11cf 100644 --- a/ironic/tests/conf_fixture.py +++ b/ironic/tests/conf_fixture.py @@ -19,23 +19,13 @@ import fixtures from oslo.config import cfg -from nova import config -from nova import ipv6 -from nova import paths -from nova.tests import utils +from ironic import config +from ironic import paths +from ironic.tests import utils CONF = cfg.CONF -CONF.import_opt('use_ipv6', 'nova.netconf') -CONF.import_opt('host', 'nova.netconf') -CONF.import_opt('scheduler_driver', 'nova.scheduler.manager') -CONF.import_opt('fake_network', 'nova.network.manager') -CONF.import_opt('network_size', 'nova.network.manager') -CONF.import_opt('num_networks', 'nova.network.manager') -CONF.import_opt('floating_ip_dns_manager', 'nova.network.floating_ips') -CONF.import_opt('instance_dns_manager', 'nova.network.floating_ips') -CONF.import_opt('policy_file', 'nova.policy') -CONF.import_opt('compute_driver', 'nova.virt.driver') -CONF.import_opt('api_paste_config', 'nova.wsgi') +CONF.import_opt('use_ipv6', 'ironic.netconf') +CONF.import_opt('host', 'ironic.netconf') class ConfFixture(fixtures.Fixture): @@ -48,28 +38,16 @@ class ConfFixture(fixtures.Fixture): super(ConfFixture, self).setUp() self.conf.set_default('api_paste_config', - paths.state_path_def('etc/nova/api-paste.ini')) + paths.state_path_def('etc/ironic/api-paste.ini')) self.conf.set_default('host', 'fake-mini') - self.conf.set_default('compute_driver', 'nova.virt.fake.FakeDriver') - self.conf.set_default('fake_network', True) - self.conf.set_default('fake_rabbit', True) - self.conf.set_default('flat_network_bridge', 'br100') - self.conf.set_default('floating_ip_dns_manager', - 'nova.tests.utils.dns_manager') - self.conf.set_default('instance_dns_manager', - 'nova.tests.utils.dns_manager') - self.conf.set_default('lock_path', None) - self.conf.set_default('network_size', 8) - self.conf.set_default('num_networks', 2) self.conf.set_default('rpc_backend', - 'nova.openstack.common.rpc.impl_fake') + 'ironic.openstack.common.rpc.impl_fake') self.conf.set_default('rpc_cast_timeout', 5) self.conf.set_default('rpc_response_timeout', 5) self.conf.set_default('sql_connection', "sqlite://") self.conf.set_default('sqlite_synchronous', False) self.conf.set_default('use_ipv6', True) self.conf.set_default('verbose', True) - self.conf.set_default('vlan_interface', 'eth0') config.parse_args([], default_config_files=[]) self.addCleanup(self.conf.reset) self.addCleanup(utils.cleanup_dns_managers) diff --git a/ironic/tests/db/__init__.py b/ironic/tests/db/__init__.py new file mode 100644 index 00000000..d8851be0 --- /dev/null +++ b/ironic/tests/db/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# flake8: noqa +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from ironic.tests.db import * diff --git a/ironic/tests/db/base.py b/ironic/tests/db/base.py new file mode 100644 index 00000000..7ece63c9 --- /dev/null +++ b/ironic/tests/db/base.py @@ -0,0 +1,50 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Bare-metal DB test base class.""" + +from oslo.config import cfg + +from ironic import context as ironic_context +from ironic import test +from ironic.db import migration as bm_migration +from ironic.db.sqlalchemy import session as bm_session + +_DB_CACHE = None + +CONF = cfg.CONF +CONF.import_opt('sql_connection', + 'ironic.db.sqlalchemy.session') + + +class Database(test.Database): + + def post_migrations(self): + pass + + +class BMDBTestCase(test.TestCase): + + def setUp(self): + super(BMDBTestCase, self).setUp() + self.flags(sql_connection='sqlite://') + global _DB_CACHE + if not _DB_CACHE: + _DB_CACHE = Database(bm_session, bm_migration, + sql_connection=CONF.sql_connection, + sqlite_db=None, + sqlite_clean_db=None) + self.useFixture(_DB_CACHE) + self.context = nova_context.get_admin_context() diff --git a/ironic/tests/fake_policy.py b/ironic/tests/fake_policy.py index 104c1d82..ecd72eaf 100644 --- a/ironic/tests/fake_policy.py +++ b/ironic/tests/fake_policy.py @@ -18,6 +18,8 @@ policy_data = """ { "admin_api": "role:admin", + "admin_or_owner": "is_admin:True or project_id:%(project_id)s", "context_is_admin": "role:admin or role:administrator", + "default": "rule:admin_or_owner" } """ diff --git a/ironic/tests/manager/__init__.py b/ironic/tests/manager/__init__.py new file mode 100644 index 00000000..45ead634 --- /dev/null +++ b/ironic/tests/manager/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# flake8: noqa +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from ironic.tests.manager import * diff --git a/ironic/tests/policy_fixture.py b/ironic/tests/policy_fixture.py index 91813def..36485ebd 100644 --- a/ironic/tests/policy_fixture.py +++ b/ironic/tests/policy_fixture.py @@ -17,9 +17,9 @@ import os import fixtures from oslo.config import cfg -from nova.openstack.common import policy as common_policy -import nova.policy -from nova.tests import fake_policy +from ironic.openstack.common import policy as common_policy +import ironic.policy +from ironic.tests import fake_policy CONF = cfg.CONF @@ -34,9 +34,9 @@ class PolicyFixture(fixtures.Fixture): with open(self.policy_file_name, 'w') as policy_file: policy_file.write(fake_policy.policy_data) CONF.set_override('policy_file', self.policy_file_name) - nova.policy.reset() - nova.policy.init() - self.addCleanup(nova.policy.reset) + ironic.policy.reset() + ironic.policy.init() + self.addCleanup(ironic.policy.reset) def set_rules(self, rules): common_policy.set_rules(common_policy.Rules( diff --git a/tools/__init__.py b/tools/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 914fcf17..0401a958 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -24,8 +24,6 @@ environment, it should be kept strictly compatible with Python 2.6. Synced in from openstack-common """ -from __future__ import print_function - import optparse import os import subprocess @@ -44,7 +42,7 @@ class InstallVenv(object): self.project = project def die(self, message, *args): - print(message % args, file=sys.stderr) + print >> sys.stderr, message % args sys.exit(1) def check_python_version(self): @@ -91,20 +89,20 @@ class InstallVenv(object): virtual environment. """ if not os.path.isdir(self.venv): - print('Creating venv...', end=' ') + print 'Creating venv...', if no_site_packages: self.run_command(['virtualenv', '-q', '--no-site-packages', self.venv]) else: self.run_command(['virtualenv', '-q', self.venv]) - print('done.') - print('Installing pip in venv...', end=' ') + print 'done.' + print 'Installing pip in venv...', if not self.run_command(['tools/with_venv.sh', 'easy_install', 'pip>1.0']).strip(): self.die("Failed to install pip.") - print('done.') + print 'done.' else: - print("venv already exists...") + print "venv already exists..." pass def pip_install(self, *args): @@ -113,7 +111,7 @@ class InstallVenv(object): redirect_output=False) def install_dependencies(self): - print('Installing dependencies with pip (this can take a while)...') + print 'Installing dependencies with pip (this can take a while)...' # First things first, make sure our venv has the latest pip and # distribute. @@ -155,12 +153,12 @@ class Distro(InstallVenv): return if self.check_cmd('easy_install'): - print('Installing virtualenv via easy_install...', end=' ') + print 'Installing virtualenv via easy_install...', if self.run_command(['easy_install', 'virtualenv']): - print('Succeeded') + print 'Succeeded' return else: - print('Failed') + print 'Failed' self.die('ERROR: virtualenv not found.\n\n%s development' ' requires virtualenv, please install it using your' diff --git a/tools/patch_tox_venv.py b/tools/patch_tox_venv.py index ac2fc924..399acbcf 100644 --- a/tools/patch_tox_venv.py +++ b/tools/patch_tox_venv.py @@ -17,7 +17,7 @@ import os import sys -import tools.install_venv_common as install_venv +import install_venv_common as install_venv def main(argv): @@ -25,8 +25,8 @@ def main(argv): venv = os.environ['VIRTUAL_ENV'] - pip_requires = os.path.join(root, 'requirements.txt') - test_requires = os.path.join(root, 'test-requirements.txt') + pip_requires = os.path.join(root, 'tools', 'pip-requires') + test_requires = os.path.join(root, 'tools', 'test-requires') py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) project = 'Nova' install = install_venv.InstallVenv(root, venv, pip_requires, test_requires, diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 00000000..94e05c12 --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,7 @@ +#!/bin/bash +tools_path=${tools_path:-$(dirname $0)} +venv_path=${venv_path:-${tools_path}} +venv_dir=${venv_name:-/../.venv} +TOOLS=${tools_path} +VENV=${venv:-${venv_path}/${venv_dir}} +source ${VENV}/bin/activate && "$@"