diff --git a/devstack/lib/tacker b/devstack/lib/tacker index 78973b753..637f79308 100644 --- a/devstack/lib/tacker +++ b/devstack/lib/tacker @@ -388,4 +388,18 @@ function tacker_create_initial_network { sudo ifconfig ${BR_MGMT} inet ${NETWORK_GATEWAY_MGMT_IP} } - +function tacker_register_default_vim { + local default_vim_id + DEFAULT_VIM_PROJECT_NAME="nfv" + DEFAULT_VIM_USER="nfv_user" + DEFAULT_VIM_PASSWORD="devstack" + DEFAULT_VIM_NAME="VIM0" + get_or_create_project $DEFAULT_VIM_PROJECT_NAME + get_or_create_user $DEFAULT_VIM_USER $DEFAULT_VIM_PASSWORD + get_or_add_user_project_role "admin" $DEFAULT_VIM_USER $DEFAULT_VIM_PROJECT_NAME + get_or_add_user_project_role "advsvc" $DEFAULT_VIM_USER $DEFAULT_VIM_PROJECT_NAME + VIM_CONFIG_FILE="$TACKER_DIR/devstack/samples/vim_config.yaml" + default_vim_id=$(tacker vim-register --name $DEFAULT_VIM_NAME --config-file $VIM_CONFIG_FILE -f value -c id) + echo $default_vim_id + iniset $TACKER_CONF nfvo_vim default_vim $DEFAULT_VIM_NAME +} diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 85e65c2ec..fe3acdfe0 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -34,7 +34,8 @@ if is_service_enabled tacker; then tacker_create_initial_network echo_summary "Upload OpenWrt image" tacker_create_openwrt_image - + echo_summary "Registering default VIM" + tacker_register_default_vim fi if [[ "$1" == "unstack" ]]; then diff --git a/devstack/samples/vim_config.yaml b/devstack/samples/vim_config.yaml new file mode 100644 index 000000000..054f1fe1a --- /dev/null +++ b/devstack/samples/vim_config.yaml @@ -0,0 +1,4 @@ +auth_url: 'http://localhost:5000' +username: 'nfv_user' +password: 'devstack' +project_name: 'nfv' diff --git a/etc/tacker/policy.json b/etc/tacker/policy.json index 369e0a80d..b38bc692c 100644 --- a/etc/tacker/policy.json +++ b/etc/tacker/policy.json @@ -1,136 +1,10 @@ { "context_is_admin": "role:admin", "admin_or_owner": "rule:context_is_admin or tenant_id:%(tenant_id)s", - "admin_or_network_owner": "rule:context_is_admin or tenant_id:%(network:tenant_id)s", "admin_only": "rule:context_is_admin", "regular_user": "", - "shared": "field:networks:shared=True", - "shared_firewalls": "field:firewalls:shared=True", - "external": "field:networks:router:external=True", + "shared": "field:vims:shared=True", "default": "rule:admin_or_owner", - "subnets:private:read": "rule:admin_or_owner", - "subnets:private:write": "rule:admin_or_owner", - "subnets:shared:read": "rule:regular_user", - "subnets:shared:write": "rule:admin_only", - - "create_subnet": "rule:admin_or_network_owner", - "get_subnet": "rule:admin_or_owner or rule:shared", - "update_subnet": "rule:admin_or_network_owner", - "delete_subnet": "rule:admin_or_network_owner", - - "create_network": "", - "get_network": "rule:admin_or_owner or rule:shared or rule:external", - "get_network:router:external": "rule:regular_user", - "get_network:segments": "rule:admin_only", - "get_network:provider:network_type": "rule:admin_only", - "get_network:provider:physical_network": "rule:admin_only", - "get_network:provider:segmentation_id": "rule:admin_only", - "get_network:queue_id": "rule:admin_only", - "create_network:shared": "rule:admin_only", - "create_network:router:external": "rule:admin_only", - "create_network:segments": "rule:admin_only", - "create_network:provider:network_type": "rule:admin_only", - "create_network:provider:physical_network": "rule:admin_only", - "create_network:provider:segmentation_id": "rule:admin_only", - "update_network": "rule:admin_or_owner", - "update_network:segments": "rule:admin_only", - "update_network:shared": "rule:admin_only", - "update_network:provider:network_type": "rule:admin_only", - "update_network:provider:physical_network": "rule:admin_only", - "update_network:provider:segmentation_id": "rule:admin_only", - "delete_network": "rule:admin_or_owner", - - "create_port": "", - "create_port:mac_address": "rule:admin_or_network_owner", - "create_port:fixed_ips": "rule:admin_or_network_owner", - "create_port:port_security_enabled": "rule:admin_or_network_owner", - "create_port:binding:host_id": "rule:admin_only", - "create_port:binding:profile": "rule:admin_only", - "create_port:mac_learning_enabled": "rule:admin_or_network_owner", - "get_port": "rule:admin_or_owner", - "get_port:queue_id": "rule:admin_only", - "get_port:binding:vif_type": "rule:admin_only", - "get_port:binding:vif_details": "rule:admin_only", - "get_port:binding:host_id": "rule:admin_only", - "get_port:binding:profile": "rule:admin_only", - "update_port": "rule:admin_or_owner", - "update_port:fixed_ips": "rule:admin_or_network_owner", - "update_port:port_security_enabled": "rule:admin_or_network_owner", - "update_port:binding:host_id": "rule:admin_only", - "update_port:binding:profile": "rule:admin_only", - "update_port:mac_learning_enabled": "rule:admin_or_network_owner", - "delete_port": "rule:admin_or_owner", - - "create_router:external_gateway_info:enable_snat": "rule:admin_only", - "update_router:external_gateway_info:enable_snat": "rule:admin_only", - - "create_firewall": "", - "get_firewall": "rule:admin_or_owner", - "create_firewall:shared": "rule:admin_only", - "get_firewall:shared": "rule:admin_only", - "update_firewall": "rule:admin_or_owner", - "update_firewall:shared": "rule:admin_only", - "delete_firewall": "rule:admin_or_owner", - - "create_firewall_policy": "", - "get_firewall_policy": "rule:admin_or_owner or rule:shared_firewalls", - "create_firewall_policy:shared": "rule:admin_or_owner", - "update_firewall_policy": "rule:admin_or_owner", - "delete_firewall_policy": "rule:admin_or_owner", - - "create_firewall_rule": "", - "get_firewall_rule": "rule:admin_or_owner or rule:shared_firewalls", - "update_firewall_rule": "rule:admin_or_owner", - "delete_firewall_rule": "rule:admin_or_owner", - - "create_qos_queue": "rule:admin_only", - "get_qos_queue": "rule:admin_only", - - "update_agent": "rule:admin_only", - "delete_agent": "rule:admin_only", - "get_agent": "rule:admin_only", - - "create_dhcp-network": "rule:admin_only", - "delete_dhcp-network": "rule:admin_only", - "get_dhcp-networks": "rule:admin_only", - "create_l3-router": "rule:admin_only", - "delete_l3-router": "rule:admin_only", - "get_l3-routers": "rule:admin_only", - "get_dhcp-agents": "rule:admin_only", - "get_l3-agents": "rule:admin_only", - "get_loadbalancer-agent": "rule:admin_only", - "get_loadbalancer-pools": "rule:admin_only", - - "create_router": "rule:regular_user", - "get_router": "rule:admin_or_owner", - "update_router:add_router_interface": "rule:admin_or_owner", - "update_router:remove_router_interface": "rule:admin_or_owner", - "delete_router": "rule:admin_or_owner", - - "create_floatingip": "rule:regular_user", - "update_floatingip": "rule:admin_or_owner", - "delete_floatingip": "rule:admin_or_owner", - "get_floatingip": "rule:admin_or_owner", - - "create_network_profile": "rule:admin_only", - "update_network_profile": "rule:admin_only", - "delete_network_profile": "rule:admin_only", - "get_network_profiles": "", - "get_network_profile": "", - "update_policy_profiles": "rule:admin_only", - "get_policy_profiles": "", - "get_policy_profile": "", - - "create_metering_label": "rule:admin_only", - "delete_metering_label": "rule:admin_only", - "get_metering_label": "rule:admin_only", - - "create_metering_label_rule": "rule:admin_only", - "delete_metering_label_rule": "rule:admin_only", - "get_metering_label_rule": "rule:admin_only", - - "get_service_provider": "rule:regular_user", - "get_lsn": "rule:admin_only", - "create_lsn": "rule:admin_only" + "get_vim": "rule:admin_or_owner or rule:shared" } diff --git a/etc/tacker/tacker.conf b/etc/tacker/tacker.conf index f5765ac1e..d2f915a4a 100644 --- a/etc/tacker/tacker.conf +++ b/etc/tacker/tacker.conf @@ -61,7 +61,8 @@ lock_path = $state_path/lock # # service_plugins = # Example: service_plugins = router,firewall,lbaas,vpnaas,metering -service_plugins = tacker.vm.plugin.VNFMPlugin + +service_plugins = vnfm,nfvo # Paste configuration file # api_paste_config = api-paste.ini @@ -395,6 +396,15 @@ auth_uri = http://127.0.0.1:5000 # Specify drivers for monitoring # monitor_driver = ping, http_ping +[nfvo_vim] +# Supported VIM drivers, resource orchestration controllers such as OpenStack, kvm +#Default VIM driver is OpenStack +#vim_drivers = openstack +#Default VIM placement if vim id is not provided +default_vim = VIM0 + +[vim_keys] +#openstack = /etc/tacker/vim/fernet_keys [tacker_nova] # parameters for novaclient to talk to nova region_name = RegionOne diff --git a/requirements.txt b/requirements.txt index cfdcd6370..f42dede1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,4 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 tosca-parser>=0.4.0 # Apache-2.0 heat-translator>=0.4.0 # Apache-2.0 +cryptography>=1.0 # BSD/Apache-2.0 diff --git a/setup.cfg b/setup.cfg index ee94d22db..b113e8006 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,9 @@ console_scripts = tacker.service_plugins = dummy = tacker.tests.unit.dummy_plugin:DummyServicePlugin vnfm = tacker.vm.plugin:VNFMPlugin + nfvo = tacker.nfvo.nfvo_plugin:NfvoPlugin +tacker.nfvo.vim.drivers = + openstack = tacker.nfvo.drivers.vim.openstack_driver:OpenStack_Driver tacker.openstack.common.cache.backends = memory = tacker.openstack.common.cache._backends.memory:MemoryBackend tacker.tacker.device.drivers = diff --git a/tacker/common/clients.py b/tacker/common/clients.py index ac63353c3..e722f6513 100644 --- a/tacker/common/clients.py +++ b/tacker/common/clients.py @@ -11,48 +11,33 @@ # under the License. from heatclient import client as heatclient -from keystoneclient.v2_0 import client as ks_client -from oslo_config import cfg - -CONF = cfg.CONF - -OPTS = [ - cfg.StrOpt('heat_uri', - default='http://localhost:8004/v1', - help=_("Heat service URI to create VNF resources" - "specified in the VNFD templates")), -] -CONF.register_opts(OPTS, group='tacker_heat') +from tacker.vm import keystone class OpenstackClients(object): - def __init__(self): + def __init__(self, auth_attr, region_name=None): super(OpenstackClients, self).__init__() - self.keystone_client = None + self.keystone_plugin = keystone.Keystone() self.heat_client = None - self.nova_client = None - self.auth_url = CONF.keystone_authtoken.auth_uri + '/v2.0' - self.auth_username = CONF.keystone_authtoken.username - self.auth_password = CONF.keystone_authtoken.password - self.auth_tenant_name = CONF.keystone_authtoken.project_name + self.keystone_client = None + self.region_name = region_name + self.auth_attr = auth_attr def _keystone_client(self): - return ks_client.Client( - tenant_name=self.auth_tenant_name, - username=self.auth_username, - password=self.auth_password, - auth_url=self.auth_url) + version = self.auth_attr['auth_url'].rpartition('/')[2] + return self.keystone_plugin.initialize_client(version, + **self.auth_attr) def _heat_client(self): - tenant_id = self.auth_token['tenant_id'] - token = self.auth_token['id'] - endpoint = CONF.tacker_heat.heat_uri + '/' + tenant_id - return heatclient.Client('1', endpoint=endpoint, token=token) + endpoint = self.keystone_session.get_endpoint( + service_type='orchestration', region_name=self.region_name) + return heatclient.Client('1', endpoint=endpoint, + session=self.keystone_session) @property - def auth_token(self): - return self.keystone.service_catalog.get_token() + def keystone_session(self): + return self.keystone.session @property def keystone(self): diff --git a/tacker/common/utils.py b/tacker/common/utils.py index 0cb20c1db..7c8bf6dba 100644 --- a/tacker/common/utils.py +++ b/tacker/common/utils.py @@ -27,13 +27,17 @@ import os import random import signal import socket +import sys import uuid from eventlet.green import subprocess import netaddr from oslo_config import cfg +from oslo_utils import importutils +from stevedore import driver from tacker.common import constants as q_const +from tacker.i18n import _LE from tacker.openstack.common import lockutils from tacker.openstack.common import log as logging @@ -63,7 +67,7 @@ MEM_UNITS = { } } } - +CONF = cfg.CONF synchronized = lockutils.synchronized_with_prefix(SYNCHRONIZED_PREFIX) @@ -347,3 +351,44 @@ def change_memory_unit(mem, to): return eval(mem_arr[0] + MEM_UNITS[unit][to]["op"] + MEM_UNITS[unit][to]["val"]) + + +def load_class_by_alias_or_classname(namespace, name): + """Load class using stevedore alias or the class name + + Load class using the stevedore driver manager + :param namespace: namespace where the alias is defined + :param name: alias or class name of the class to be loaded + :returns class if calls can be loaded + :raises ImportError if class cannot be loaded + """ + + if not name: + LOG.error(_LE("Alias or class name is not set")) + raise ImportError(_("Class not found.")) + try: + # Try to resolve class by alias + mgr = driver.DriverManager(namespace, name) + class_to_load = mgr.driver + except RuntimeError: + e1_info = sys.exc_info() + # Fallback to class name + try: + class_to_load = importutils.import_class(name) + except (ImportError, ValueError): + LOG.error(_LE("Error loading class by alias"), + exc_info=e1_info) + LOG.error(_LE("Error loading class by class name"), + exc_info=True) + raise ImportError(_("Class not found.")) + return class_to_load + + +def deep_update(orig_dict, new_dict): + for key, value in new_dict.items(): + if isinstance(value, dict): + if key in orig_dict and isinstance(orig_dict[key], dict): + deep_update(orig_dict[key], value) + continue + + orig_dict[key] = value diff --git a/tacker/db/migration/alembic_migrations/versions/5246a6bd410f_multisite_vim.py b/tacker/db/migration/alembic_migrations/versions/5246a6bd410f_multisite_vim.py new file mode 100644 index 000000000..e826e6e96 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/5246a6bd410f_multisite_vim.py @@ -0,0 +1,68 @@ +# Copyright 2016 OpenStack Foundation +# +# 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. +# + +"""multisite_vim + +Revision ID: 5246a6bd410f +Revises: 24bec5f211c7 +Create Date: 2016-03-22 14:05:15.129330 + +""" + +# revision identifiers, used by Alembic. +revision = '5246a6bd410f' +down_revision = '24bec5f211c7' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.create_table('vims', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('type', sa.String(length=255), nullable=False), + sa.Column('tenant_id', sa.String(length=255), nullable=True), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('placement_attr', sa.PickleType(), nullable=True), + sa.Column('shared', sa.Boolean(), server_default=sa.text(u'true'), + nullable=False), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + op.create_table('vimauths', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('vim_id', sa.String(length=255), nullable=False), + sa.Column('password', sa.String(length=128), nullable=False), + sa.Column('auth_url', sa.String(length=255), nullable=False), + sa.Column('vim_project', sa.PickleType(), nullable=False), + sa.Column('auth_cred', sa.PickleType(), nullable=False), + sa.ForeignKeyConstraint(['vim_id'], ['vims.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('auth_url') + ) + op.add_column(u'devices', sa.Column('placement_attr', sa.PickleType(), + nullable=True)) + op.add_column(u'devices', sa.Column('vim_id', sa.String(length=36), + nullable=False)) + op.create_foreign_key(None, 'devices', 'vims', ['vim_id'], ['id']) + + +def downgrade(active_plugins=None, options=None): + op.drop_constraint(None, 'devices', type_='foreignkey') + op.drop_column(u'devices', 'vim_id') + op.drop_column(u'devices', 'placement_attr') + op.drop_table('vimauths') + op.drop_table('vims') diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index 4e8c3521c..294abc324 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -24bec5f211c7 \ No newline at end of file +5246a6bd410f \ No newline at end of file diff --git a/tacker/db/migration/models/head.py b/tacker/db/migration/models/head.py index ec6ee9f88..b9a7f2b13 100644 --- a/tacker/db/migration/models/head.py +++ b/tacker/db/migration/models/head.py @@ -22,6 +22,7 @@ Based on this comparison database can be healed with healing migration. """ from tacker.db import model_base +from tacker.db.nfvo import nfvo_db # noqa from tacker.db.vm import proxy_db # noqa from tacker.db.vm import vm_db # noqa diff --git a/tacker/db/nfvo/__init__.py b/tacker/db/nfvo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/db/nfvo/nfvo_db.py b/tacker/db/nfvo/nfvo_db.py new file mode 100644 index 000000000..5c0e3751a --- /dev/null +++ b/tacker/db/nfvo/nfvo_db.py @@ -0,0 +1,169 @@ +# Copyright 2016 Brocade Communications System, 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. + +import uuid + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.orm import exc as orm_exc +from sqlalchemy import sql + +from tacker.db import api as tdbapi +from tacker.db import db_base +from tacker.db import model_base +from tacker.db import models_v1 +from tacker.db.vm import vm_db +from tacker.extensions import nfvo +from tacker import manager +from tacker.openstack.common.db import exception +from tacker.openstack.common import uuidutils + + +VIM_ATTRIBUTES = ('id', 'type', 'tenant_id', 'name', 'description', + 'placement_attr', 'shared') +VIM_AUTH_ATTRIBUTES = ('auth_url', 'vim_project', 'password', 'auth_cred') + + +class Vim(model_base.BASE, models_v1.HasTenant): + id = sa.Column(sa.String(255), + primary_key=True, + default=uuidutils.generate_uuid) + type = sa.Column(sa.String(255), nullable=False) + tenant_id = sa.Column(sa.String(255), nullable=True) + name = sa.Column(sa.String(255), nullable=True) + description = sa.Column(sa.String(255), nullable=True) + placement_attr = sa.Column(sa.PickleType, nullable=True) + shared = sa.Column(sa.Boolean, default=True, server_default=sql.true( + ), nullable=False) + vim_auth = orm.relationship('VimAuth') + + +class VimAuth(model_base.BASE, models_v1.HasId): + vim_id = sa.Column(sa.String(255), sa.ForeignKey('vims.id'), + nullable=False) + password = sa.Column(sa.String(128), nullable=False) + auth_url = sa.Column(sa.String(255), nullable=False) + vim_project = sa.Column(sa.PickleType, nullable=False) + auth_cred = sa.Column(sa.PickleType, nullable=False) + __table_args__ = (sa.UniqueConstraint('auth_url'), {}) + + +class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin): + + def __init__(self): + tdbapi.register_models() + super(NfvoPluginDb, self).__init__() + + @property + def _core_plugin(self): + return manager.TackerManager.get_plugin() + + def _make_vim_dict(self, vim_db, fields=None): + res = dict((key, vim_db[key]) for key in VIM_ATTRIBUTES) + vim_auth_db = vim_db.vim_auth + res['auth_url'] = vim_auth_db[0].auth_url + res['vim_project'] = vim_auth_db[0].vim_project + res['auth_cred'] = vim_auth_db[0].auth_cred + res['auth_cred']['password'] = vim_auth_db[0].password + return self._fields(res, fields) + + def _fields(self, resource, fields): + if fields: + return dict(((key, item) for key, item in resource.items() + if key in fields)) + return resource + + def _get_resource(self, context, model, id): + try: + return self._get_by_id(context, model, id) + except orm_exc.NoResultFound: + if issubclass(model, Vim): + raise nfvo.VimNotFoundException(vim_id=id) + else: + raise + + def create_vim(self, context, vim): + vim_cred = vim['auth_cred'] + try: + with context.session.begin(subtransactions=True): + vim_db = Vim( + id=vim.get('id'), + type=vim.get('type'), + tenant_id=vim.get('tenant_id'), + name=vim.get('name'), + description=vim.get('description'), + placement_attr=vim.get('placement_attr')) + context.session.add(vim_db) + vim_auth_db = VimAuth( + id=str(uuid.uuid4()), + vim_id=vim.get('id'), + password=vim_cred.pop('password'), + vim_project=vim.get('vim_project'), + auth_url=vim.get('auth_url'), + auth_cred=vim_cred) + context.session.add(vim_auth_db) + except exception.DBDuplicateEntry: + raise nfvo.VimDuplicateUrlException() + return self._make_vim_dict(vim_db) + + def delete_vim(self, context, vim_id): + with context.session.begin(subtransactions=True): + vim_db = self._get_resource(context, Vim, vim_id) + context.session.query(VimAuth).filter_by( + vim_id=vim_id).delete() + context.session.delete(vim_db) + + def is_vim_still_in_use(self, context, vim_id): + with context.session.begin(subtransactions=True): + devices_db = context.session.query(vm_db.Device).filter_by( + vim_id=vim_id).first() + if devices_db is not None: + raise nfvo.VimInUseException(vim_id=vim_id) + return devices_db + + def get_vim(self, context, vim_id, fields=None): + vim_db = self._get_resource(context, Vim, vim_id) + return self._make_vim_dict(vim_db) + + def get_vims(self, context, filters=None, fields=None): + return self._get_collection(context, Vim, self._make_vim_dict, + filters=filters, fields=fields) + + def update_vim(self, context, vim_id, vim): + with context.session.begin(subtransactions=True): + vim_cred = vim['auth_cred'] + vim_project = vim['vim_project'] + try: + vim_auth_db = (self._model_query(context, VimAuth).filter( + VimAuth.vim_id == vim_id).with_lockmode('update').one()) + except orm_exc.NoResultFound: + raise nfvo.VimNotFound(vim_id=vim_id) + vim_auth_db.update({'auth_cred': vim_cred, 'password': + vim_cred.pop('password'), 'vim_project': + vim_project}) + return self.get_vim(context, vim_id) + + def get_vim_by_name(self, context, vim_name, fields=None): + vim_db = self._get_by_name(context, Vim, vim_name) + return self._make_vim_dict(vim_db) + + def _get_by_name(self, context, model, name): + try: + query = self._model_query(context, model) + return query.filter(model.name == name).one() + except orm_exc.NoResultFound: + if issubclass(model, Vim): + raise diff --git a/tacker/db/vm/vm_db.py b/tacker/db/vm/vm_db.py index 354d03eea..335ccbd1f 100644 --- a/tacker/db/vm/vm_db.py +++ b/tacker/db/vm/vm_db.py @@ -120,6 +120,9 @@ class Device(model_base.BASE, models_v1.HasTenant): attributes = orm.relationship("DeviceAttribute", backref="device") status = sa.Column(sa.String(255), nullable=False) + vim_id = sa.Column(sa.String(36), sa.ForeignKey('vims.id'), nullable=False) + placement_attr = sa.Column(sa.PickleType, nullable=True) + vim = orm.relationship('Vim') class DeviceAttribute(model_base.BASE, models_v1.HasId): @@ -195,7 +198,8 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): 'attributes': self._make_dev_attrs_dict(device_db.attributes), } key_list = ('id', 'tenant_id', 'name', 'description', 'instance_id', - 'template_id', 'status', 'mgmt_url') + 'vim_id', 'placement_attr', 'template_id', 'status', + 'mgmt_url') res.update((key, device_db[key]) for key in key_list) return self._fields(res, fields) @@ -335,13 +339,14 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): # called internally, not by REST API def _create_device_pre(self, context, device): - device = device['device'] LOG.debug(_('device %s'), device) tenant_id = self._get_tenant_id_for_create(context, device) template_id = device['template_id'] name = device.get('name') device_id = device.get('id') or str(uuid.uuid4()) attributes = device.get('attributes', {}) + vim_id = device.get('vim_id') + placement_attr = device.get('placement_attr', {}) with context.session.begin(subtransactions=True): template_db = self._get_resource(context, DeviceTemplate, template_id) @@ -351,13 +356,15 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): description=template_db.description, instance_id=None, template_id=template_id, + vim_id=vim_id, + placement_attr=placement_attr, status=constants.PENDING_CREATE) context.session.add(device_db) for key, value in attributes.items(): - arg = DeviceAttribute( - id=str(uuid.uuid4()), device_id=device_id, - key=key, value=value) - context.session.add(arg) + arg = DeviceAttribute( + id=str(uuid.uuid4()), device_id=device_id, + key=key, value=value) + context.session.add(arg) return self._make_device_dict(device_db) @@ -376,8 +383,10 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): query.update({'status': constants.ERROR}) for (key, value) in device_dict['attributes'].items(): - self._device_attribute_update_or_create(context, device_id, - key, value) + # do not store decrypted vim auth in device attr table + if 'vim_auth' not in key: + self._device_attribute_update_or_create(context, device_id, + key, value) def _create_device_status(self, context, device_id, new_status): with context.session.begin(subtransactions=True): @@ -421,7 +430,8 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): delete(synchronize_session='fetch')) for (key, value) in dev_attrs.items(): - self._device_attribute_update_or_create(context, device_id, + if 'vim_auth' not in key: + self._device_attribute_update_or_create(context, device_id, key, value) def _delete_device_pre(self, context, device_id): @@ -532,7 +542,9 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): description=device_db.description, instance_id=device_db.instance_id, mgmt_url=device_db.mgmt_url, - status=device_db.status) + status=device_db.status, + vim_id=device_db.vim_id, + placement_attr=device_db.placement_attr) context.session.add(new_device_db) (self._model_query(context, DeviceAttribute). diff --git a/tacker/extensions/nfvo.py b/tacker/extensions/nfvo.py new file mode 100644 index 000000000..8e606e8cc --- /dev/null +++ b/tacker/extensions/nfvo.py @@ -0,0 +1,207 @@ +# Copyright 2016 Brocade Communications Systems 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. + +import abc + +import six + +from tacker.api import extensions +from tacker.api.v1 import attributes as attr +from tacker.api.v1 import resource_helper +from tacker.common import exceptions +from tacker.plugins.common import constants +from tacker.services import service_base + + +class VimUnauthorizedException(exceptions.TackerException): + message = _("%(message)s") + + +class VimConnectionException(exceptions.TackerException): + message = _("%(message)s") + + +class VimInUseException(exceptions.TackerException): + message = _("VIM %(vim_id)s is still in use by VNF") + + +class VimDefaultIdException(exceptions.TackerException): + message = _("Default VIM name %(vim_name)s is invalid or there are " + "multiple VIM matches found. Please specify a valid default " + "VIM in tacker.conf") + + +class VimNotFoundException(exceptions.TackerException): + message = _("Specified VIM id %(vim_id)s is invalid. Please verify and " + "pass a valid VIM id") + + +class VimRegionNotFoundException(exceptions.TackerException): + message = _("Unknown VIM region name %(region_name)s") + + +class VimKeyNotFoundException(exceptions.TackerException): + message = _("Unable to find key file for VIM %(vim_id)s") + + +class VimDuplicateUrlException(exceptions.TackerException): + message = _("VIM with specified auth URL already exists. Cannot register " + "duplicate VIM") + +RESOURCE_ATTRIBUTE_MAP = { + + 'vims': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True, + }, + 'tenant_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True + }, + 'type': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True + }, + 'auth_url': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True + }, + 'auth_cred': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:dict_or_nodata': None}, + 'is_visible': True, + }, + 'vim_project': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:dict_or_nodata': None}, + 'is_visible': True, + }, + 'name': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'description': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'placement_attr': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + 'default': None, + }, + 'shared': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': False, + 'convert_to': attr.convert_to_boolean, + 'required_by_policy': True + }, + } +} + + +class Nfvo(extensions.ExtensionDescriptor): + @classmethod + def get_name(cls): + return 'NFVO' + + @classmethod + def get_alias(cls): + return 'NFV Orchestrator' + + @classmethod + def get_description(cls): + return "Extension for NFV Orchestrator" + + @classmethod + def get_namespace(cls): + return 'http://wiki.openstack.org/Tacker' + + @classmethod + def get_updated(cls): + return "2015-12-21T10:00:00-00:00" + + @classmethod + def get_resources(cls): + special_mappings = {} + plural_mappings = resource_helper.build_plural_mappings( + special_mappings, RESOURCE_ATTRIBUTE_MAP) + attr.PLURALS.update(plural_mappings) + return resource_helper.build_resource_info( + plural_mappings, RESOURCE_ATTRIBUTE_MAP, constants.NFVO, + translate_name=True) + + @classmethod + def get_plugin_interface(cls): + return NFVOPluginBase + + def update_attributes_map(self, attributes): + super(Nfvo, self).update_attributes_map( + attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) + + def get_extended_resources(self, version): + version_map = {'1.0': RESOURCE_ATTRIBUTE_MAP} + return version_map.get(version, {}) + + +@six.add_metaclass(abc.ABCMeta) +class NFVOPluginBase(service_base.NFVPluginBase): + def get_plugin_name(self): + return constants.NFVO + + def get_plugin_type(self): + return constants.NFVO + + def get_plugin_description(self): + return 'Tacker NFV Orchestrator plugin' + + @abc.abstractmethod + def create_vim(self, context, vim): + pass + + @abc.abstractmethod + def delete_vim(self, context, vim_id): + pass + + @abc.abstractmethod + def get_vim(self, context, vim_id, fields=None): + pass + + @abc.abstractmethod + def get_vims(self, context, filters=None, fields=None): + pass + + def get_vim_by_name(self, context, vim_name, fields=None): + raise NotImplementedError() diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index 27cb3b7c2..7d6ef3d46 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -23,7 +23,7 @@ from tacker.api.v1 import resource_helper from tacker.common import exceptions from tacker.openstack.common import log as logging from tacker.plugins.common import constants -from tacker.services.service_base import NFVPluginBase +from tacker.services import service_base LOG = logging.getLogger(__name__) @@ -225,6 +225,13 @@ RESOURCE_ATTRIBUTE_MAP = { 'validate': {'type:uuid': None}, 'is_visible': True, }, + 'vim_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, 'name': { 'allow_post': True, 'allow_put': True, @@ -258,6 +265,13 @@ RESOURCE_ATTRIBUTE_MAP = { 'is_visible': True, 'default': {}, }, + 'placement_attr': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:dict_or_none': None}, + 'is_visible': True, + 'default': {}, + }, 'status': { 'allow_post': False, 'allow_put': False, @@ -313,7 +327,7 @@ class Vnfm(extensions.ExtensionDescriptor): @six.add_metaclass(abc.ABCMeta) -class VNFMPluginBase(NFVPluginBase): +class VNFMPluginBase(service_base.NFVPluginBase): def get_plugin_name(self): return constants.VNFM diff --git a/tacker/manager.py b/tacker/manager.py index 12451dd42..ab7cbb2db 100644 --- a/tacker/manager.py +++ b/tacker/manager.py @@ -17,7 +17,6 @@ from oslo_config import cfg from tacker.common import rpc_compat from tacker.common import utils -from tacker.openstack.common import importutils from tacker.openstack.common import log as logging from tacker.openstack.common import periodic_task @@ -101,6 +100,27 @@ class TackerManager(object): self.service_plugins = {} self._load_service_plugins() + @staticmethod + def load_class_for_provider(namespace, plugin_provider): + """Loads plugin using alias or class name + + Load class using stevedore alias or the class name + :param namespace: namespace where alias is defined + :param plugin_provider: plugin alias or class name + :returns plugin that is loaded + :raises ImportError if fails to load plugin + """ + + try: + return utils.load_class_by_alias_or_classname(namespace, + plugin_provider) + except ImportError: + raise ImportError(_("Plugin '%s' not found.") % plugin_provider) + + def _get_plugin_instance(self, namespace, plugin_provider): + plugin_class = self.load_class_for_provider(namespace, plugin_provider) + return plugin_class() + def _load_service_plugins(self): """Loads service plugins. @@ -112,14 +132,10 @@ class TackerManager(object): for provider in plugin_providers: if provider == '': continue - try: - LOG.info(_("Loading Plugin: %s"), provider) - plugin_class = importutils.import_class(provider) - except ImportError: - LOG.exception(_("Error loading plugin")) - raise ImportError(_("Plugin not found.")) - plugin_inst = plugin_class() + LOG.info(_("Loading Plugin: %s"), provider) + plugin_inst = self._get_plugin_instance('tacker.service_plugins', + provider) # only one implementation of svc_type allowed # specifying more than one plugin # for the same type is a fatal exception @@ -129,7 +145,6 @@ class TackerManager(object): plugin_inst.get_plugin_type()) self.service_plugins[plugin_inst.get_plugin_type()] = plugin_inst - # # search for possible agent notifiers declared in service plugin # # (needed by agent management extension) # if (hasattr(self.plugin, 'agent_notifiers') and diff --git a/tacker/nfvo/__init__.py b/tacker/nfvo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/nfvo/drivers/__init__.py b/tacker/nfvo/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/nfvo/drivers/vim/__init__.py b/tacker/nfvo/drivers/vim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/nfvo/drivers/vim/abstract_vim_driver.py b/tacker/nfvo/drivers/vim/abstract_vim_driver.py new file mode 100644 index 000000000..73c3a3131 --- /dev/null +++ b/tacker/nfvo/drivers/vim/abstract_vim_driver.py @@ -0,0 +1,84 @@ +# 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 abc + +import six + +from tacker.api import extensions + + +@six.add_metaclass(abc.ABCMeta) +class VimAbstractDriver(extensions.PluginInterface): + + @abc.abstractmethod + def get_type(self): + """Get VIM Driver type + + Return one of predefined types of VIMs. + """ + pass + + @abc.abstractmethod + def get_name(self): + """Get VIM name + + Return a symbolic name for the VIM driver. + """ + pass + + @abc.abstractmethod + def get_description(self): + pass + + @abc.abstractmethod + def register_vim(self, context, vim_obj): + """Register VIM object in to NFVO plugin + + Validate, encode and store VIM information for deploying VNFs. + """ + pass + + @abc.abstractmethod + def deregister_vim(self, context, vim_id): + """Deregister VIM object from NFVO plugin + + Cleanup VIM data and delete VIM information + """ + pass + + @abc.abstractmethod + def authenticate_vim(self, context, vim_obj): + """Authenticate VIM connection parameters + + Validate authentication credentials and connectivity of VIM + """ + pass + + @abc.abstractmethod + def encode_vim_auth(self, context, vim_id, auth): + """Encrypt VIM credentials + + Encrypt and store VIM sensitive information such as password + """ + pass + + @abc.abstractmethod + def delete_vim_auth(self, vim_id): + """Delete VIM auth keys + + Delete VIM sensitive information such as keys from file system or DB + """ + pass diff --git a/tacker/nfvo/drivers/vim/openstack_driver.py b/tacker/nfvo/drivers/vim/openstack_driver.py new file mode 100644 index 000000000..c21234e83 --- /dev/null +++ b/tacker/nfvo/drivers/vim/openstack_driver.py @@ -0,0 +1,180 @@ +# Copyright 2016 Brocade Communications System, 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. + +import os + +from oslo_config import cfg + +from tacker.common import log +from tacker.extensions import nfvo +from tacker.nfvo.drivers.vim import abstract_vim_driver +from tacker.openstack.common import log as logging +from tacker.vm import keystone + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + +OPTS = [cfg.StrOpt('openstack', default='/etc/tacker/vim/fernet_keys', + help='Dir.path to store fernet keys.')] +cfg.CONF.register_opts(OPTS, 'vim_keys') + + +class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): + """Driver for OpenStack VIM + + OpenStack driver handles interactions with local as well as + remote OpenStack instances. The driver invokes keystone service for VIM + authorization and validation. The driver is also responsible for + discovering placement attributes such as regions, availability zones + """ + + def __init__(self): + self.keystone = keystone.Keystone() + self.keystone.create_key_dir(CONF.vim_keys.openstack) + + def get_type(self): + return 'openstack' + + def get_name(self): + return 'OpenStack VIM Driver' + + def get_description(self): + return 'OpenStack VIM Driver' + + def authenticate_vim(self, vim_obj): + """Validate VIM auth attributes + + Initialize keystoneclient with provided authentication attributes. + """ + auth_url = vim_obj['auth_url'] + auth_cred = vim_obj['auth_cred'] + vim_project = vim_obj['vim_project'] + keystone_version = self._validate_auth_url(auth_url) + + if keystone_version not in auth_url: + vim_obj['auth_url'] = auth_url + '/' + keystone_version + if keystone_version == 'v3': + auth_cred['project_id'] = vim_project.get('id', None) + auth_cred['project_name'] = vim_project.get('name', None) + if 'project_domain_id' not in auth_cred: + auth_cred[ + 'project_domain_id' + ] = CONF.keystone_authtoken.project_domain_id + if 'user_domain_id' not in auth_cred: + auth_cred[ + 'user_domain_id' + ] = CONF.keystone_authtoken.user_domain_id + else: + auth_cred['tenant_id'] = vim_project.pop('id', None) + auth_cred['tenant_name'] = vim_project.pop('name', None) + # user_id is not supported in keystone v2 + auth_cred.pop('user_id', None) + auth_cred['auth_url'] = vim_obj['auth_url'] + return self._initialize_keystone(keystone_version, auth_cred) + + def _validate_auth_url(self, auth_url): + try: + keystone_version = self.keystone.get_version(auth_url) + except Exception as e: + LOG.error(_('VIM Auth URL invalid')) + raise nfvo.VimConnectionException(message=e.message) + return keystone_version + + def _initialize_keystone(self, version, auth): + try: + ks_client = self.keystone.initialize_client(version=version, + **auth) + except Exception as e: + LOG.error(_('VIM authentication failed')) + raise nfvo.VimUnauthorizedException(message=e.message) + return ks_client + + def _find_regions(self, ks_client): + if ks_client.version == 'v2.0': + service_list = ks_client.services.list() + heat_service_id = (service.id for service in + service_list if service.type == 'orchestration') + endpoints_list = ks_client.endpoints.list() + region_list = [endpoint.region for endpoint in endpoints_list if + endpoint.service_id == heat_service_id] + else: + region_info = ks_client.regions.list() + region_list = [region.id for region in region_info] + if not region_list: + LOG.info(_('Unable to find VIM regions')) + return + return region_list + + def discover_placement_attr(self, vim_obj, ks_client): + """Fetch VIM placement information + + Attributes can include regions, AZ. + """ + regions_list = self._find_regions(ks_client) + vim_obj['placement_attr'] = {'regions': regions_list} + return vim_obj + + @log.log + def register_vim(self, vim_obj): + """Validate and register VIM + + Store VIM information in Tacker for + VNF placements + """ + ks_client = self.authenticate_vim(vim_obj) + self.discover_placement_attr(vim_obj, ks_client) + self.encode_vim_auth(vim_obj['id'], vim_obj['auth_cred']) + LOG.debug(_('VIM registration complete %s'), vim_obj) + + @log.log + def deregister_vim(self, vim_id): + """Deregister VIM from NFVO + + Delete VIM keys from file system + """ + self.delete_vim_auth(vim_id) + + @log.log + def delete_vim_auth(self, vim_id): + """Delete vim information + + Delete vim key stored in file system + """ + LOG.debug(_('Attempting to delete key for vim id %s'), vim_id) + key_file = os.path.join(CONF.vim_keys.openstack, vim_id) + try: + os.remove(key_file) + LOG.debug(_('VIM key deleted successfully for vim %s'), vim_id) + except OSError: + LOG.warn(_('VIM key deletion unsuccessful for vim %s'), vim_id) + + @log.log + def encode_vim_auth(self, vim_id, auth): + """Encode VIM credentials + + Store VIM auth using fernet key encryption + """ + fernet_key, fernet_obj = self.keystone.create_fernet_key() + encoded_auth = fernet_obj.encrypt(auth['password'].encode('utf-8')) + auth['password'] = encoded_auth + key_file = os.path.join(CONF.vim_keys.openstack, vim_id) + try: + with open(key_file, 'w') as f: + f.write(fernet_key.decode('utf-8')) + LOG.debug(_('VIM auth successfully stored for vim %s'), vim_id) + except IOError: + raise nfvo.VimKeyNotFoundException(vim_id=vim_id) diff --git a/tacker/nfvo/nfvo_plugin.py b/tacker/nfvo/nfvo_plugin.py new file mode 100644 index 000000000..f85ba1c80 --- /dev/null +++ b/tacker/nfvo/nfvo_plugin.py @@ -0,0 +1,91 @@ +# Copyright 2016 Brocade Communications System, 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. + +import uuid + +from oslo_config import cfg + +from tacker.common import driver_manager +from tacker.common import log +from tacker.common import utils +from tacker.db.nfvo import nfvo_db +from tacker.openstack.common import excutils +from tacker.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class NfvoPlugin(nfvo_db.NfvoPluginDb): + """NFVO reference plugin for NFVO extension + + Implements the NFVO extension and defines public facing APIs for VIM + operations. NFVO internally invokes the appropriate VIM driver in + backend based on configured VIM types. Plugin also interacts with VNFM + extension for providing the specified VIM information + """ + supported_extension_aliases = ['nfvo'] + + OPTS = [ + cfg.ListOpt( + 'vim_drivers', default=['openstack'], + help=_('VIM driver for launching VNFs')), + ] + cfg.CONF.register_opts(OPTS, 'nfvo_vim') + + def __init__(self): + super(NfvoPlugin, self).__init__() + self._vim_drivers = driver_manager.DriverManager( + 'tacker.nfvo.vim.drivers', + cfg.CONF.nfvo_vim.vim_drivers) + + @log.log + def create_vim(self, context, vim): + LOG.debug(_('Create vim called with parameters %s'), vim) + vim_obj = vim['vim'] + vim_type = vim_obj['type'] + vim_obj['id'] = str(uuid.uuid4()) + try: + self._vim_drivers.invoke(vim_type, 'register_vim', vim_obj=vim_obj) + res = super(NfvoPlugin, self).create_vim(context, vim_obj) + return res + except Exception: + with excutils.save_and_reraise_exception(): + self._vim_drivers.invoke(vim_type, 'delete_vim_auth', + vim_id=vim_obj['id']) + + def _get_vim(self, context, vim_id): + if not self.is_vim_still_in_use(context, vim_id): + return self.get_vim(context, vim_id) + + @log.log + def update_vim(self, context, vim_id, vim): + vim_obj = self._get_vim(context, vim_id) + utils.deep_update(vim_obj, vim['vim']) + vim_type = vim_obj['type'] + try: + self._vim_drivers.invoke(vim_type, 'register_vim', vim_obj=vim_obj) + return super(NfvoPlugin, self).update_vim(context, vim_id, vim_obj) + except Exception: + with excutils.save_and_reraise_exception(): + self._vim_drivers.invoke(vim_type, 'delete_vim_auth', + vim_id=vim_obj['id']) + + @log.log + def delete_vim(self, context, vim_id): + vim_obj = self._get_vim(context, vim_id) + self._vim_drivers.invoke(vim_obj['type'], 'deregister_vim', + vim_id=vim_id) + super(NfvoPlugin, self).delete_vim(context, vim_id) diff --git a/tacker/plugins/common/constants.py b/tacker/plugins/common/constants.py index e30cbf84c..e31316f03 100644 --- a/tacker/plugins/common/constants.py +++ b/tacker/plugins/common/constants.py @@ -19,11 +19,13 @@ CORE = "CORE" DUMMY = "DUMMY" VNFM = "VNFM" +NFVO = "NFVO" COMMON_PREFIXES = { CORE: "", DUMMY: "/dummy_svc", VNFM: "", + NFVO: "" } # Service operation status constants diff --git a/tacker/tests/unit/base.py b/tacker/tests/unit/base.py new file mode 100644 index 000000000..44a618309 --- /dev/null +++ b/tacker/tests/unit/base.py @@ -0,0 +1,33 @@ +# Copyright 2016 Brocade Communications System, 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. + + +import mock +from oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslotest import base + +CONF = cfg.CONF + + +class TestCase(base.BaseTestCase): + + def setUp(self): + super(TestCase, self).setUp() + self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + + def _mock(self, target, new=mock.DEFAULT): + patcher = mock.patch(target, new) + return patcher.start() diff --git a/tacker/tests/unit/db/base.py b/tacker/tests/unit/db/base.py index b27659dcc..c5bbed645 100644 --- a/tacker/tests/unit/db/base.py +++ b/tacker/tests/unit/db/base.py @@ -14,10 +14,10 @@ # under the License. import fixtures -import testtools from tacker.db import api as db_api from tacker.db import model_base +from tacker.tests.unit import base class SqlFixture(fixtures.Fixture): @@ -42,7 +42,7 @@ class SqlFixture(fixtures.Fixture): self.addCleanup(clear_tables) -class SqlTestCase(testtools.TestCase): +class SqlTestCase(base.TestCase): def setUp(self): super(SqlTestCase, self).setUp() diff --git a/tacker/tests/unit/db/utils.py b/tacker/tests/unit/db/utils.py index 5170d29a2..af701418c 100644 --- a/tacker/tests/unit/db/utils.py +++ b/tacker/tests/unit/db/utils.py @@ -47,6 +47,7 @@ def get_dummy_vnfd_obj(): def get_dummy_vnf_obj(): return {'vnf': {'description': 'dummy_vnf_description', 'vnfd_id': u'eb094833-995e-49f0-a047-dfb56aaf7c4e', + 'vim_id': u'6261579e-d6f3-49ad-8bc3-a9cb974778ff', 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', 'name': 'dummy_vnf', 'attributes': {}}} @@ -152,3 +153,10 @@ def get_dummy_device_obj_userdata_attr(): 'services': [], 'attributes': {u'param_values': userdata_params}, 'id': '18685f68-2b2a-4185-8566-74f54e548811', 'description': u"Parameterized VNF descriptor"} + + +def get_vim_auth_obj(): + return {'username': 'test_user', 'password': 'test_password', + 'project_id': None, 'project_name': 'test_project', + 'auth_url': 'http://localhost:5000/v3', 'user_domain_id': + 'default', 'project_domain_id': 'default'} diff --git a/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py b/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py index 6b7dffcd1..e7ca2c37c 100644 --- a/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py +++ b/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py @@ -16,10 +16,10 @@ import codecs import mock import os -import testtools import yaml from tacker import context +from tacker.tests.unit import base from tacker.tests.unit.db import utils from tacker.vm.infra_drivers.heat import heat @@ -45,7 +45,7 @@ def _get_template(name): return f.read() -class TestDeviceHeat(testtools.TestCase): +class TestDeviceHeat(base.TestCase): hot_template = _get_template('hot_openwrt.yaml') hot_param_template = _get_template('hot_openwrt_params.yaml') hot_ipparam_template = _get_template('hot_openwrt_ipparams.yaml') @@ -142,7 +142,8 @@ class TestDeviceHeat(testtools.TestCase): expected_result = '4a4c2d44-8a52-4895-9a75-9d1c76c3e738' expected_fields = self._get_expected_fields() result = self.heat_driver.create(plugin=None, context=self.context, - device=device_obj) + device=device_obj, + auth_attr=utils.get_vim_auth_obj()) self.heat_client.create.assert_called_once_with(expected_fields) self.assertEqual(expected_result, result) @@ -151,7 +152,8 @@ class TestDeviceHeat(testtools.TestCase): expected_result = '4a4c2d44-8a52-4895-9a75-9d1c76c3e738' expected_fields = self._get_expected_fields_user_data() result = self.heat_driver.create(plugin=None, context=self.context, - device=device_obj) + device=device_obj, + auth_attr=utils.get_vim_auth_obj()) self.heat_client.create.assert_called_once_with(expected_fields) self.assertEqual(expected_result, result) @@ -160,7 +162,8 @@ class TestDeviceHeat(testtools.TestCase): expected_result = '4a4c2d44-8a52-4895-9a75-9d1c76c3e738' expected_fields = self._get_expected_fields_ipaddr_data() result = self.heat_driver.create(plugin=None, context=self.context, - device=device_obj) + device=device_obj, + auth_attr=utils.get_vim_auth_obj()) self.heat_client.create.assert_called_once_with(expected_fields) self.assertEqual(expected_result, result) @@ -171,13 +174,15 @@ class TestDeviceHeat(testtools.TestCase): self.heat_driver.create_wait(plugin=None, context=self.context, device_dict=device_obj, - device_id=device_id) + device_id=device_id, + auth_attr=utils.get_vim_auth_obj()) self.assertEqual(device_obj, expected_result) def test_delete(self): device_id = '4a4c2d44-8a52-4895-9a75-9d1c76c3e738' self.heat_driver.delete(plugin=None, context=self.context, - device_id=device_id) + device_id=device_id, + auth_attr=utils.get_vim_auth_obj()) self.heat_client.delete.assert_called_once_with(device_id) def test_update(self): @@ -185,9 +190,10 @@ class TestDeviceHeat(testtools.TestCase): device_config_obj = utils.get_dummy_device_update_config_attr() expected_device_update = self._get_expected_device_update_obj() device_id = '4a4c2d44-8a52-4895-9a75-9d1c76c3e738' - self.heat_driver.update(None, self.context, - device_id, device_obj, - device_config_obj) + self.heat_driver.update(plugin=None, context=self.context, + device_id=device_id, device_dict=device_obj, + device=device_config_obj, + auth_attr=utils.get_vim_auth_obj()) self.assertEqual(device_obj, expected_device_update) def test_create_device_template_pre_tosca(self): @@ -238,10 +244,10 @@ class TestDeviceHeat(testtools.TestCase): expected_result = '4a4c2d44-8a52-4895-9a75-9d1c76c3e738' expected_fields = self._get_expected_fields_tosca(hot_tpl_name) expected_device = self._get_expected_tosca_device(tosca_tpl_name, - hot_tpl_name) + hot_tpl_name) result = self.heat_driver.create(plugin=None, context=self.context, - device=device) - # self.heat_client.create.assert_called_once_with(expected_fields) + device=device, + auth_attr=utils.get_vim_auth_obj()) actual_fields = self.heat_client.create.call_args[0][0] actual_fields["template"] = yaml.safe_load(actual_fields["template"]) expected_fields["template"] = \ @@ -249,6 +255,7 @@ class TestDeviceHeat(testtools.TestCase): self.assertEqual(actual_fields, expected_fields) device["attributes"]["heat_template"] = yaml.safe_load( device["attributes"]["heat_template"]) + self.heat_client.create.assert_called_once_with(expected_fields) self.assertEqual(expected_result, result) self.assertEqual(device, expected_device) diff --git a/tacker/tests/unit/vm/nfvo/__init__.py b/tacker/tests/unit/vm/nfvo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/unit/vm/nfvo/drivers/__init__.py b/tacker/tests/unit/vm/nfvo/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/unit/vm/nfvo/drivers/vim/__init__.py b/tacker/tests/unit/vm/nfvo/drivers/vim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/unit/vm/nfvo/drivers/vim/test_openstack_driver.py b/tacker/tests/unit/vm/nfvo/drivers/vim/test_openstack_driver.py new file mode 100644 index 000000000..48da8a557 --- /dev/null +++ b/tacker/tests/unit/vm/nfvo/drivers/vim/test_openstack_driver.py @@ -0,0 +1,117 @@ +# Copyright 2016 Brocade Communications System, 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. + +import mock +from oslo_config import cfg + +from tacker.nfvo.drivers.vim import openstack_driver +from tacker.tests.unit import base +from tacker.tests.unit.db import utils + +OPTS = [cfg.StrOpt('user_domain_id', default='default', help='User Domain Id'), + cfg.StrOpt('project_domain_id', default='default', help='Project ' + 'Domain Id')] +cfg.CONF.register_opts(OPTS, 'keystone_authtoken') +CONF = cfg.CONF + + +class FakeKeystone(mock.Mock): + pass + + +class mock_dict(dict): + def __getattr__(self, item): + return self.get(item) + + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + +class TestOpenstack_Driver(base.TestCase): + def setUp(self): + super(TestOpenstack_Driver, self).setUp() + self._mock_keystone() + self.keystone.create_key_dir.return_value = 'test_keys' + self.config_fixture.config(group='vim_keys', openstack='/tmp/') + self.openstack_driver = openstack_driver.OpenStack_Driver() + self.vim_obj = self.get_vim_obj() + self.auth_obj = utils.get_vim_auth_obj() + self.addCleanup(mock.patch.stopall) + + def _mock_keystone(self): + self.keystone = mock.Mock(wraps=FakeKeystone()) + fake_keystone = mock.Mock() + fake_keystone.return_value = self.keystone + self._mock( + 'tacker.vm.keystone.Keystone', fake_keystone) + + def get_vim_obj(self): + return {'id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', 'type': + 'openstack', 'auth_url': 'http://localhost:5000', + 'auth_cred': {'username': 'test_user', 'password': + 'test_password'}, 'name': 'VIM0', + 'vim_project': {'name': 'test_project'}} + + def test_register_keystone_v3(self): + regions = [mock_dict({'id': 'RegionOne'})] + attrs = {'regions.list.return_value': regions} + keystone_version = 'v3' + mock_ks_client = mock.Mock(version=keystone_version, **attrs) + self.keystone.get_version.return_value = keystone_version + self._test_register_vim(self.vim_obj, mock_ks_client) + mock_ks_client.regions.list.assert_called_once_with() + self.keystone.initialize_client.assert_called_once_with( + version=keystone_version, **self.auth_obj) + + def test_register_keystone_v2(self): + services_list = [mock_dict({'type': 'orchestration', 'id': + 'test_id'})] + endpoints_regions = mock_dict({'region': 'RegionOne'}) + endpoints_list = [mock_dict({'service_id': 'test_id', 'regions': + endpoints_regions})] + attrs = {'endpoints.list.return_value': endpoints_list, + 'services.list.return_value': services_list} + keystone_version = 'v2.0' + mock_ks_client = mock.Mock(version='v2.0', **attrs) + self.keystone.get_version.return_value = keystone_version + auth_obj = {'tenant_name': 'test_project', 'username': 'test_user', + 'password': 'test_password', 'auth_url': + 'http://localhost:5000/v2.0', 'tenant_id': None} + self._test_register_vim(self.vim_obj, mock_ks_client) + self.keystone.initialize_client.assert_called_once_with( + version=keystone_version, **auth_obj) + + def _test_register_vim(self, vim_obj, mock_ks_client): + self.keystone.initialize_client.return_value = mock_ks_client + fernet_attrs = {'encrypt.return_value': 'encrypted_password'} + mock_fernet_obj = mock.Mock(**fernet_attrs) + mock_fernet_key = 'test_fernet_key' + self.keystone.create_fernet_key.return_value = (mock_fernet_key, + mock_fernet_obj) + file_mock = mock.mock_open() + with mock.patch('six.moves.builtins.open', file_mock, create=True): + self.openstack_driver.register_vim(vim_obj) + mock_fernet_obj.encrypt.assert_called_once_with(mock.ANY) + file_mock().write.assert_called_once_with('test_fernet_key') + + @mock.patch('tacker.nfvo.drivers.vim.openstack_driver.os.remove') + @mock.patch('tacker.nfvo.drivers.vim.openstack_driver.os.path' + '.join') + def test_deregister_vim(self, mock_os_path, mock_os_remove): + vim_id = 'my_id' + file_path = CONF.vim_keys.openstack + '/' + vim_id + mock_os_path.return_value = file_path + self.openstack_driver.deregister_vim(vim_id) + mock_os_remove.assert_called_once_with(file_path) diff --git a/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py new file mode 100644 index 000000000..f6a7fec72 --- /dev/null +++ b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py @@ -0,0 +1,115 @@ +# Copyright 2016 Brocade Communications System, 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. + +import uuid + +import mock + +from tacker import context +from tacker.db.nfvo import nfvo_db +from tacker.nfvo import nfvo_plugin +from tacker.tests.unit.db import base as db_base + + +class FakeDriverManager(mock.Mock): + def invoke(self, *args, **kwargs): + if 'create' in args: + return str(uuid.uuid4()) + + +class TestNfvoPlugin(db_base.SqlTestCase): + def setUp(self): + super(TestNfvoPlugin, self).setUp() + self.addCleanup(mock.patch.stopall) + self.context = context.get_admin_context() + self._mock_driver_manager() + self.nfvo_plugin = nfvo_plugin.NfvoPlugin() + + def _mock_driver_manager(self): + self._driver_manager = mock.Mock(wraps=FakeDriverManager()) + self._driver_manager.__contains__ = mock.Mock( + return_value=True) + fake_driver_manager = mock.Mock() + fake_driver_manager.return_value = self._driver_manager + self._mock( + 'tacker.common.driver_manager.DriverManager', fake_driver_manager) + + def _insert_dummy_vim(self): + session = self.context.session + vim_db = nfvo_db.Vim( + id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='fake_vim', + description='fake_vim_description', + type='openstack', + placement_attr={'regions': ['RegionOne']}) + vim_auth_db = nfvo_db.VimAuth( + vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + password='encrypted_pw', + auth_url='http://localhost:5000', + vim_project={'name': 'test_project'}, + auth_cred={'username': 'test_user', 'user_domain_id': 'default', + 'project_domain_d': 'default'}) + session.add(vim_db) + session.add(vim_auth_db) + session.flush() + + def test_create_vim(self): + vim_dict = {'vim': {'type': 'openstack', 'auth_url': + 'http://localhost:5000', 'vim_project': {'name': + 'test_project'}, 'auth_cred': {'username': 'test_user', + 'password': + 'test_password'}, + 'name': 'VIM0'}} + vim_type = 'openstack' + res = self.nfvo_plugin.create_vim(self.context, vim_dict) + self._driver_manager.invoke.assert_called_once_with(vim_type, + 'register_vim', + vim_obj=vim_dict[ + 'vim']) + self.assertIsNotNone(res) + self.assertIn('id', res) + self.assertIn('placement_attr', res) + + def test_delete_vim(self): + self._insert_dummy_vim() + vim_type = 'openstack' + vim_id = '6261579e-d6f3-49ad-8bc3-a9cb974778ff' + self.nfvo_plugin.delete_vim(self.context, vim_id) + self._driver_manager.invoke.assert_called_once_with(vim_type, + 'deregister_vim', + vim_id=vim_id) + + def test_update_vim(self): + vim_dict = {'vim': {'id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'vim_project': {'name': 'new_project'}, + 'auth_cred': {'username': 'new_user', + 'password': 'new_password'}}} + vim_type = 'openstack' + vim_auth_username = vim_dict['vim']['auth_cred']['username'] + vim_auth_password = vim_dict['vim']['auth_cred']['password'] + vim_project = vim_dict['vim']['vim_project'] + self._insert_dummy_vim() + res = self.nfvo_plugin.update_vim(self.context, vim_dict['vim']['id'], + vim_dict) + self._driver_manager.invoke.assert_called_once_with(vim_type, + 'register_vim', + vim_obj=mock.ANY) + self.assertIsNotNone(res) + self.assertIn('id', res) + self.assertIn('placement_attr', res) + self.assertEqual(vim_project, res['vim_project']) + self.assertEqual(vim_auth_username, res['auth_cred']['username']) + self.assertEqual(vim_auth_password, res['auth_cred']['password']) diff --git a/tacker/tests/unit/vm/test_plugin.py b/tacker/tests/unit/vm/test_plugin.py index 00df64ca3..87c89185f 100644 --- a/tacker/tests/unit/vm/test_plugin.py +++ b/tacker/tests/unit/vm/test_plugin.py @@ -18,6 +18,7 @@ import uuid import mock from tacker import context +from tacker.db.nfvo import nfvo_db from tacker.db.vm import vm_db from tacker.extensions import vnfm from tacker.tests.unit.db import base as db_base @@ -39,14 +40,21 @@ class FakeGreenPool(mock.Mock): pass +class FakeVimClient(mock.Mock): + pass + + class TestVNFMPlugin(db_base.SqlTestCase): def setUp(self): super(TestVNFMPlugin, self).setUp() self.addCleanup(mock.patch.stopall) self.context = context.get_admin_context() + self._mock_vim_client() + self._stub_get_vim() self._mock_device_manager() self._mock_vnf_monitor() self._mock_green_pool() + self._insert_dummy_vim() self.vnfm_plugin = plugin.VNFMPlugin() def _mock_device_manager(self): @@ -58,12 +66,20 @@ class TestVNFMPlugin(db_base.SqlTestCase): self._mock( 'tacker.common.driver_manager.DriverManager', fake_device_manager) - def _mock_vnf_monitor(self): - self._vnf_monitor = mock.Mock(wraps=FakeVNFMonitor()) - fake_vnf_monitor = mock.Mock() - fake_vnf_monitor.return_value = self._vnf_monitor + def _mock_vim_client(self): + self.vim_client = mock.Mock(wraps=FakeVimClient()) + fake_vim_client = mock.Mock() + fake_vim_client.return_value = self.vim_client self._mock( - 'tacker.vm.monitor.VNFMonitor', fake_vnf_monitor) + 'tacker.vm.vim_client.VimClient', fake_vim_client) + + def _stub_get_vim(self): + vim_obj = {'vim_id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'vim_name': 'fake_vim', 'vim_auth': + {'auth_url': 'http://localhost:5000', 'password': + 'test_pw', 'username': 'test_user', 'project_name': + 'test_project'}} + self.vim_client.get_vim.return_value = vim_obj def _mock_green_pool(self): self._pool = mock.Mock(wraps=FakeGreenPool()) @@ -72,9 +88,12 @@ class TestVNFMPlugin(db_base.SqlTestCase): self._mock( 'eventlet.GreenPool', fake_green_pool) - def _mock(self, target, new=mock.DEFAULT): - patcher = mock.patch(target, new) - return patcher.start() + def _mock_vnf_monitor(self): + self._vnf_monitor = mock.Mock(wraps=FakeVNFMonitor()) + fake_vnf_monitor = mock.Mock() + fake_vnf_monitor.return_value = self._vnf_monitor + self._mock( + 'tacker.vm.monitor.VNFMonitor', fake_vnf_monitor) def _insert_dummy_device_template(self): session = self.context.session @@ -98,11 +117,33 @@ class TestVNFMPlugin(db_base.SqlTestCase): description='fake_device_description', instance_id='da85ea1a-4ec4-4201-bbb2-8d9249eca7ec', template_id='eb094833-995e-49f0-a047-dfb56aaf7c4e', + vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + placement_attr={'region': 'RegionOne'}, status='ACTIVE') session.add(device_db) session.flush() return device_db + def _insert_dummy_vim(self): + session = self.context.session + vim_db = nfvo_db.Vim( + id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='fake_vim', + description='fake_vim_description', + type='openstack', + placement_attr={'regions': ['RegionOne']}) + vim_auth_db = nfvo_db.VimAuth( + vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + password='encrypted_pw', + auth_url='http://localhost:5000', + vim_project={'name': 'test_project'}, + auth_cred={'username': 'test_user', 'user_domain_id': 'default', + 'project_domain_d': 'default'}) + session.add(vim_db) + session.add(vim_auth_db) + session.flush() + def test_create_vnfd(self): vnfd_obj = utils.get_dummy_vnfd_obj() result = self.vnfm_plugin.create_vnfd(self.context, vnfd_obj) @@ -144,7 +185,8 @@ class TestVNFMPlugin(db_base.SqlTestCase): self._device_manager.invoke.assert_called_with(mock.ANY, mock.ANY, plugin=mock.ANY, context=mock.ANY, - device=mock.ANY) + device=mock.ANY, + auth_attr=mock.ANY) self._pool.spawn_n.assert_called_once_with(mock.ANY) def test_delete_vnf(self): @@ -155,10 +197,12 @@ class TestVNFMPlugin(db_base.SqlTestCase): self._device_manager.invoke.assert_called_with(mock.ANY, mock.ANY, plugin=mock.ANY, context=mock.ANY, - device_id=mock.ANY) + device_id=mock.ANY, + auth_attr=mock.ANY, + region_name=mock.ANY) self._vnf_monitor.delete_hosting_vnf.assert_called_with(mock.ANY) self._pool.spawn_n.assert_called_once_with(mock.ANY, mock.ANY, - mock.ANY) + mock.ANY, mock.ANY) def test_update_vnf(self): self._insert_dummy_device_template() @@ -173,4 +217,4 @@ class TestVNFMPlugin(db_base.SqlTestCase): self.assertIn('attributes', result) self.assertIn('mgmt_url', result) self._pool.spawn_n.assert_called_once_with(mock.ANY, mock.ANY, - mock.ANY) + mock.ANY, mock.ANY) diff --git a/tacker/vm/infra_drivers/heat/heat.py b/tacker/vm/infra_drivers/heat/heat.py index adc8cfc7b..28cc19cec 100644 --- a/tacker/vm/infra_drivers/heat/heat.py +++ b/tacker/vm/infra_drivers/heat/heat.py @@ -174,11 +174,12 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver): @log.log def _process_vdu_network_interfaces(self, vdu_id, vdu_dict, properties, template_dict): + def make_port_dict(): port_dict = { 'type': 'OS::Neutron::Port', 'properties': { - 'port_security_enabled': False + 'port_security_enabled': False } } port_dict['properties'].setdefault('fixed_ips', []) @@ -229,9 +230,8 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver): networks_list.append(dict(network_param)) @log.log - def create(self, plugin, context, device): + def create(self, plugin, context, device, auth_attr): LOG.debug(_('device %s'), device) - heatclient_ = HeatClient(context) attributes = device['device_template']['attributes'].copy() vnfd_yaml = attributes.pop('vnfd', None) fields = dict((key, attributes.pop(key)) for key @@ -251,6 +251,9 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver): fields.setdefault(key, {}).update( jsonutils.loads(dev_attrs.pop(key))) + region_name = device.get('placement_attr', {}).get('region_name', None) + heatclient_ = HeatClient(auth_attr, region_name) + LOG.debug('vnfd_yaml %s', vnfd_yaml) if vnfd_yaml is not None: vnfd_dict = yamlparser.simple_ordered_parse(vnfd_yaml) @@ -381,8 +384,10 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver): stack = heatclient_.create(fields) return stack['stack']['id'] - def create_wait(self, plugin, context, device_dict, device_id): - heatclient_ = HeatClient(context) + def create_wait(self, plugin, context, device_dict, device_id, auth_attr): + region_name = device_dict.get('placement_attr', {}).get( + 'region_name', None) + heatclient_ = HeatClient(auth_attr, region_name) stack = heatclient_.get(device_id) status = stack.stack_status @@ -422,9 +427,11 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver): device_dict['mgmt_url'] = jsonutils.dumps(mgmt_ips) @log.log - def update(self, plugin, context, device_id, device_dict, device): - # checking if the stack exists at the moment - heatclient_ = HeatClient(context) + def update(self, plugin, context, device_id, device_dict, device, + auth_attr): + region_name = device_dict.get('placement_attr', {}).get( + 'region_name', None) + heatclient_ = HeatClient(auth_attr, region_name) heatclient_.get(device_id) # update config attribute @@ -461,18 +468,20 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver): new_yaml = yaml.dump(config_dict) device_dict.setdefault('attributes', {})['config'] = new_yaml - def update_wait(self, plugin, context, device_id): + def update_wait(self, plugin, context, device_id, auth_attr, + region_name=None): # do nothing but checking if the stack exists at the moment - heatclient_ = HeatClient(context) + heatclient_ = HeatClient(auth_attr, region_name) heatclient_.get(device_id) - def delete(self, plugin, context, device_id): - heatclient_ = HeatClient(context) + def delete(self, plugin, context, device_id, auth_attr, region_name=None): + heatclient_ = HeatClient(auth_attr, region_name) heatclient_.delete(device_id) @log.log - def delete_wait(self, plugin, context, device_id): - heatclient_ = HeatClient(context) + def delete_wait(self, plugin, context, device_id, auth_attr, + region_name=None): + heatclient_ = HeatClient(auth_attr, region_name) stack = heatclient_.get(device_id) status = stack.stack_status @@ -513,9 +522,11 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver): class HeatClient(object): - def __init__(self, context, password=None): + def __init__(self, auth_attr, region_name=None): # context, password are unused - self.stacks = clients.OpenstackClients().heat.stacks + self.heat = clients.OpenstackClients(auth_attr, region_name).heat + self.stacks = self.heat.stacks + self.resource_types = self.heat.resource_types def create(self, fields): fields = fields.copy() diff --git a/tacker/vm/keystone.py b/tacker/vm/keystone.py new file mode 100644 index 000000000..438b45f98 --- /dev/null +++ b/tacker/vm/keystone.py @@ -0,0 +1,86 @@ +# Copyright 2016 Brocade Communications System, 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. + +import os + +from cryptography.fernet import Fernet +from keystoneclient.auth import identity +from keystoneclient import client +from keystoneclient import exceptions +from keystoneclient import session +from oslo_config import cfg + +from tacker.openstack.common import log as logging + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class Keystone(object): + """Keystone module for OpenStack VIM + + Handles identity operations for a given OpenStack + instance such as version, session and client + """ + + def get_version(self, base_url=None): + try: + keystone_client = client.Client(auth_url=base_url) + except exceptions.ConnectionRefused: + raise + return keystone_client.version + + def get_session(self, auth_plugin): + ses = session.Session(auth=auth_plugin) + return ses + + def get_endpoint(self, ses, service_type, region_name=None): + return ses.get_endpoint(service_type, region_name) + + def initialize_client(self, version, **kwargs): + if version == 'v2.0': + from keystoneclient.v2_0 import client + auth_plugin = identity.v2.Password(**kwargs) + else: + from keystoneclient.v3 import client + auth_plugin = identity.v3.Password(**kwargs) + ses = self.get_session(auth_plugin=auth_plugin) + try: + cli = client.Client(session=ses) + return cli + except (exceptions.AuthorizationFailure, + exceptions.Unauthorized): + LOG.warn(_("Authorization failed for user")) + raise + + @staticmethod + def create_key_dir(path): + if not os.access(path, os.F_OK): + LOG.info(_( + '[fernet_tokens] key_repository does not appear to exist; ' + 'attempting to create it')) + try: + os.makedirs(path, 0o700) + except OSError: + LOG.error(_( + 'Failed to create [fernet_tokens] key_repository: either' + 'it already exists or you don\'t have sufficient' + 'permissions to create it')) + + def create_fernet_key(self): + fernet_key = Fernet.generate_key() + fernet_obj = Fernet(fernet_key) + return fernet_key, fernet_obj diff --git a/tacker/vm/monitor.py b/tacker/vm/monitor.py index f5df0645c..bfd2ba2b3 100644 --- a/tacker/vm/monitor.py +++ b/tacker/vm/monitor.py @@ -239,7 +239,7 @@ class ActionRespawn(ActionPolicy): @ActionPolicy.register('respawn', 'heat') class ActionRespawnHeat(ActionPolicy): @classmethod - def execute_action(cls, plugin, device_dict): + def execute_action(cls, plugin, device_dict, auth_attr): device_id = device_dict['id'] LOG.error(_('device %s dead'), device_id) if plugin._mark_device_dead(device_dict['id']): @@ -257,25 +257,19 @@ class ActionRespawnHeat(ActionPolicy): attributes = device_dict['attributes'].copy() attributes['dead_device_id'] = device_id new_device = {'id': new_device_id, 'attributes': attributes} - for key in ('tenant_id', 'template_id', 'name'): + for key in ('tenant_id', 'template_id', 'name', 'vim_id', + 'placement_attr'): new_device[key] = device_dict[key] LOG.debug(_('new_device %s'), new_device) - + placement_attr = device_dict.get('placement_attr', {}) + region_name = placement_attr.get('region_name', None) # kill heat stack - heatclient = heat.HeatClient(None) + heatclient = heat.HeatClient(auth_attr=auth_attr, + region_name=region_name) heatclient.delete(device_dict['instance_id']) - # keystone v2.0 specific - authtoken = CONF.keystone_authtoken - token = clients.OpenstackClients().auth_token - + # TODO(anyone) set the current request ctxt instead of admin ctxt context = t_context.get_admin_context() - context.tenant_name = authtoken.project_name - context.user_name = authtoken.username - context.auth_token = token['id'] - context.tenant_id = token['tenant_id'] - context.user_id = token['user_id'] - new_device_dict = plugin.create_device_sync( context, {'device': new_device}) LOG.info(_('respawned new device %s'), new_device_dict['id']) @@ -295,7 +289,7 @@ class ActionRespawnHeat(ActionPolicy): new_device_dict.setdefault('attributes', {})['config'] = config plugin.config_device(context, new_device_dict) - plugin.add_device_to_monitor(new_device_dict) + plugin.add_device_to_monitor(new_device_dict, auth_attr) @ActionPolicy.register('log') diff --git a/tacker/vm/plugin.py b/tacker/vm/plugin.py index 29b96ffc5..bbef4aaae 100644 --- a/tacker/vm/plugin.py +++ b/tacker/vm/plugin.py @@ -28,14 +28,16 @@ from tacker.api.v1 import attributes from tacker.common import driver_manager from tacker.db.vm import vm_db from tacker.extensions import vnfm +from tacker.i18n import _LE from tacker.openstack.common import excutils -from tacker.openstack.common.gettextutils import _LE from tacker.openstack.common import log as logging from tacker.plugins.common import constants from tacker.vm.mgmt_drivers import constants as mgmt_constants from tacker.vm import monitor +from tacker.vm import vim_client LOG = logging.getLogger(__name__) +CONF = cfg.CONF class VNFMMgmtMixin(object): @@ -46,7 +48,7 @@ class VNFMMgmtMixin(object): 'Hosting Device/logical service ' 'instance tacker plugin will use')), cfg.IntOpt('boot_wait', default=30, - help=_('Time interval to wait for VM to boot')), + help=_('Time interval to wait for VM to boot')) ] cfg.CONF.register_opts(OPTS, 'tacker') @@ -115,6 +117,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): super(VNFMPlugin, self).__init__() self._pool = eventlet.GreenPool() self.boot_wait = cfg.CONF.tacker.boot_wait + self.vim_client = vim_client.VimClient() self._device_manager = driver_manager.DriverManager( 'tacker.tacker.device.drivers', cfg.CONF.tacker.infra_driver) @@ -160,7 +163,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): ########################################################################### # hosting device - def add_device_to_monitor(self, device_dict): + def add_device_to_monitor(self, device_dict, vim_auth): dev_attrs = device_dict['attributes'] mgmt_url = device_dict['mgmt_url'] if 'monitoring_policy' in dev_attrs and mgmt_url: @@ -168,7 +171,8 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): action_cls = monitor.ActionPolicy.get_policy(action, device_dict) if action_cls: - action_cls.execute_action(self, hosting_vnf['device']) + action_cls.execute_action(self, hosting_vnf['device'], + vim_auth) hosting_vnf = self._vnf_monitor.to_hosting_vnf( device_dict, action_cb) @@ -189,7 +193,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): } self.update_device(context, device_id, update) - def _create_device_wait(self, context, device_dict): + def _create_device_wait(self, context, device_dict, auth_attr): driver_name = self._infra_driver_name(device_dict) device_id = device_dict['id'] instance_id = self._instance_id(device_dict) @@ -198,7 +202,8 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): try: self._device_manager.invoke( driver_name, 'create_wait', plugin=self, context=context, - device_dict=device_dict, device_id=instance_id) + device_dict=device_dict, device_id=instance_id, + auth_attr=auth_attr) except vnfm.DeviceCreateWaitFailed: LOG.error(_LE("VNF Create failed for vnf_id %s"), device_id) create_failed = True @@ -233,7 +238,16 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): device_dict['status'] = new_status self._create_device_status(context, device_id, new_status) - def _create_device(self, context, device): + def get_vim(self, context, device): + region_name = device.setdefault('placement_attr', {}).get( + 'region_name', None) + vim_res = self.vim_client.get_vim(context, device['vim_id'], + region_name) + device['placement_attr']['vim_name'] = vim_res['vim_name'] + device['vim_id'] = vim_res['vim_id'] + return vim_res['vim_auth'] + + def _create_device(self, context, device, vim_auth): device_dict = self._create_device_pre(context, device) device_id = device_dict['id'] driver_name = self._infra_driver_name(device_dict) @@ -242,7 +256,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): try: instance_id = self._device_manager.invoke( driver_name, 'create', plugin=self, - context=context, device=device_dict) + context=context, device=device_dict, auth_attr=vim_auth) except Exception: with excutils.save_and_reraise_exception(): self.delete_device(context, device_id) @@ -251,16 +265,17 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): self._create_device_post(context, device_id, None, None, device_dict) return - device_dict['instance_id'] = instance_id return device_dict def create_device(self, context, device): - device_dict = self._create_device(context, device) + device_info = device['device'] + vim_auth = self.get_vim(context, device_info) + device_dict = self._create_device(context, device_info, vim_auth) def create_device_wait(): - self._create_device_wait(context, device_dict) - self.add_device_to_monitor(device_dict) + self._create_device_wait(context, device_dict, vim_auth) + self.add_device_to_monitor(device_dict, vim_auth) self.config_device(context, device_dict) self.spawn_n(create_device_wait) return device_dict @@ -268,11 +283,13 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): # not for wsgi, but for service to create hosting device # the device is NOT added to monitor. def create_device_sync(self, context, device): - device_dict = self._create_device(context, device) - self._create_device_wait(context, device_dict) + device_info = device['device'] + vim_auth = self.get_vim(context, device_info) + device_dict = self._create_device(context, device_info, vim_auth) + self._create_device_wait(context, device_dict, vim_auth) return device_dict - def _update_device_wait(self, context, device_dict): + def _update_device_wait(self, context, device_dict, vim_auth): driver_name = self._infra_driver_name(device_dict) instance_id = self._instance_id(device_dict) kwargs = { @@ -280,10 +297,14 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): mgmt_constants.KEY_KWARGS: {'device': device_dict}, } new_status = constants.ACTIVE + placement_attr = device_dict['placement_attr'] + region_name = placement_attr.get('region_name', None) + try: self._device_manager.invoke( driver_name, 'update_wait', plugin=self, - context=context, device_id=instance_id) + context=context, device_id=instance_id, auth_attr=vim_auth, + region_name=region_name) self.mgmt_call(context, device_dict, kwargs) except Exception: LOG.exception(_('_update_device_wait')) @@ -296,6 +317,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): def update_device(self, context, device_id, device): device_dict = self._update_device_pre(context, device_id) + vim_auth = self.get_vim(context, device_dict) driver_name = self._infra_driver_name(device_dict) instance_id = self._instance_id(device_dict) @@ -303,25 +325,28 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): self.mgmt_update_pre(context, device_dict) self._device_manager.invoke( driver_name, 'update', plugin=self, context=context, - device_id=instance_id, device_dict=device_dict, device=device) + device_id=instance_id, device_dict=device_dict, + device=device, auth_attr=vim_auth) except Exception: with excutils.save_and_reraise_exception(): device_dict['status'] = constants.ERROR self.mgmt_update_post(context, device_dict) self._update_device_post(context, device_id, constants.ERROR) - self.spawn_n(self._update_device_wait, context, device_dict) + self.spawn_n(self._update_device_wait, context, device_dict, vim_auth) return device_dict - def _delete_device_wait(self, context, device_dict): + def _delete_device_wait(self, context, device_dict, auth_attr): driver_name = self._infra_driver_name(device_dict) instance_id = self._instance_id(device_dict) - e = None + placement_attr = device_dict['placement_attr'] + region_name = placement_attr.get('region_name', None) try: self._device_manager.invoke( driver_name, 'delete_wait', plugin=self, - context=context, device_id=instance_id) + context=context, device_id=instance_id, auth_attr=auth_attr, + region_name=region_name) except Exception as e_: e = e_ device_dict['status'] = constants.ERROR @@ -332,10 +357,12 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): def delete_device(self, context, device_id): device_dict = self._delete_device_pre(context, device_id) + vim_auth = self.get_vim(context, device_dict) self._vnf_monitor.delete_hosting_vnf(device_id) driver_name = self._infra_driver_name(device_dict) instance_id = self._instance_id(device_dict) - + placement_attr = device_dict['placement_attr'] + region_name = placement_attr.get('region_name', None) kwargs = { mgmt_constants.KEY_ACTION: mgmt_constants.ACTION_DELETE_DEVICE, mgmt_constants.KEY_KWARGS: {'device': device_dict}, @@ -344,7 +371,10 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): self.mgmt_delete_pre(context, device_dict) self.mgmt_call(context, device_dict, kwargs) self._device_manager.invoke(driver_name, 'delete', plugin=self, - context=context, device_id=instance_id) + context=context, + device_id=instance_id, + auth_attr=vim_auth, + region_name=region_name) except Exception as e: # TODO(yamahata): when the devaice is already deleted. mask # the error, and delete row in db @@ -355,7 +385,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): self._delete_device_post(context, device_id, e) self._delete_device_post(context, device_id, None) - self.spawn_n(self._delete_device_wait, context, device_dict) + self.spawn_n(self._delete_device_wait, context, device_dict, vim_auth) def create_vnf(self, context, vnf): vnf['device'] = vnf.pop('vnf') diff --git a/tacker/vm/vim_client.py b/tacker/vm/vim_client.py new file mode 100644 index 000000000..a350dbabc --- /dev/null +++ b/tacker/vm/vim_client.py @@ -0,0 +1,107 @@ +# Copyright 2015-2016 Brocade Communications Systems 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. + +import os + +from cryptography.fernet import Fernet +from oslo_config import cfg + +from tacker.extensions import nfvo +from tacker import manager +from tacker.openstack.common import log as logging +from tacker.plugins.common import constants + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + +OPTS = [ + cfg.StrOpt( + 'default_vim', help=_('Default VIM for launching VNFs')) +] +cfg.CONF.register_opts(OPTS, 'nfvo_vim') + + +class VimClient(object): + def get_vim(self, context, vim_id=None, region_name=None): + """Get Vim information for provided VIM id + + Initiate the NFVO plugin, request VIM information for the provided + VIM id and validate region + """ + nfvo_plugin = manager.TackerManager.get_service_plugins().get( + constants.NFVO) + + if not vim_id: + LOG.debug(_('VIM id not provided. Attempting to find default ' + 'VIM id')) + vim_name = cfg.CONF.nfvo_vim.default_vim + if not vim_name: + raise nfvo.VimDefaultIdException( + message='Default VIM is not specified. Either specify a ' + 'valid VIM in the VNF create or set default VIM in' + ' tacker.conf') + try: + vim_info = nfvo_plugin.get_vim_by_name(context, vim_name) + except Exception: + raise nfvo.VimDefaultIdException( + vim_name=vim_name) + else: + try: + vim_info = nfvo_plugin.get_vim(context, vim_id) + except Exception: + raise nfvo.VimNotFoundException(vim_id=vim_id) + LOG.debug(_('VIM info found for vim id %s'), vim_id) + if region_name and not self.region_valid(vim_info['placement_attr'] + ['regions'], region_name): + raise nfvo.VimRegionNotFoundException(region_name=region_name) + + vim_auth = self._build_vim_auth(vim_info) + vim_res = {'vim_auth': vim_auth, 'vim_id': vim_info['id'], + 'vim_name': vim_info.get('name', vim_info['id'])} + return vim_res + + @staticmethod + def region_valid(vim_regions, region_name): + return region_name in vim_regions + + def _build_vim_auth(self, vim_info): + LOG.debug('VIM id is %s', vim_info['id']) + vim_auth = vim_info['auth_cred'] + vim_auth['password'] = self._decode_vim_auth(vim_info['id'], + vim_auth[ + 'password'].encode( + 'utf-8')) + vim_auth['auth_url'] = vim_info['auth_url'] + return vim_auth + + def _decode_vim_auth(self, vim_id, cred): + """Decode Vim credentials + + Decrypt VIM cred. using Fernet Key + """ + vim_key = self._find_vim_key(vim_id) + f = Fernet(vim_key) + if not f: + LOG.warn(_('Unable to decode VIM auth')) + raise nfvo.VimNotFoundException('Unable to decode VIM auth key') + return f.decrypt(cred) + + @staticmethod + def _find_vim_key(vim_id): + key_file = os.path.join(CONF.vim_keys.openstack, vim_id) + LOG.debug(_('Attempting to open key file for vim id %s'), vim_id) + with open(key_file, 'r') as f: + return f.read() + LOG.warn(_('VIM id invalid or key not found for %s'), vim_id)