Implement multisite VIM feature

Added support for multisite OpenStack VIM feature through a new
'nfvo' extension. This also enables remote VNF life cycle manage-
ment through the VIM Id parameter.

Implements: blueprint multi-site-vim
Depends-On: I3c08945f24343288c2c5614ab4b472d68a1e1d47

Change-Id: I7dd19a0c1ce948474bb3069073b3608ce265beb4
This commit is contained in:
Sripriya 2016-02-25 14:04:47 -08:00
parent 905c4ade56
commit 0fdd5a4717
40 changed files with 1598 additions and 266 deletions

View File

@ -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
}

View File

@ -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

View File

@ -0,0 +1,4 @@
auth_url: 'http://localhost:5000'
username: 'nfv_user'
password: 'devstack'
project_name: 'nfv'

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -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):

View File

@ -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

View File

@ -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')

View File

@ -1 +1 @@
24bec5f211c7
5246a6bd410f

View File

@ -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

View File

169
tacker/db/nfvo/nfvo_db.py Normal file
View File

@ -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

View File

@ -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).

207
tacker/extensions/nfvo.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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

0
tacker/nfvo/__init__.py Normal file
View File

View File

View File

View File

@ -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

View File

@ -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)

View File

@ -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