环境:
ocata版本
neutron-10.0.5
python-neutronclient-6.1.1
horizon-11.0.4
主要讲述如何在ocata版本的openstack网络模块neutron中添加extension。
流程图
先看一张从dashboard到neutron相应组件api的流程图
整个流程大致就是这样:
dashboard(shell)–>neutronclient–>neutron-server–>plugin–>agent
详细的流程讲解这里不作展开,只做相应步骤的代码添加,详细的讲解会在后续的文章提及。
我们这里讲解的是如何添加extension,当然就是要在/neutron/extension目录添加我们的py文件,这里的例子是对
数据库表格的操作
#neutron-server
这一步是对neutron-server访问数据库的操作分析。
如何新增数据库表 请看这里https://blog.csdn.net/energysober/article/details/80289394
##步骤一 添加extensions/test.py
下面时分段讲解
安装需要添加,不同版本可能存在文件的变动
from neutron_lib.api import extensions as api_extensions
from neutron_lib.plugins import directory
from neutron_lib.db import constants as db_const
from neutron_lib import exceptions as nexception
from neutron.quota import resource_registry
from neutron._i18n import _
from neutron.api import extensions
from neutron.api.v2 import base
`
TESTS = 'tests'
RESOURCE_ATTRIBUTE_MAP = {
TESTS: {
'id': {'allow_post': False, 'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True, 'primary_key': True},
'pid': {'allow_post': True, 'allow_put': True,
'validate': {'type:uuid': None},
'is_visible': True},
'project_id': {'allow_post': True, 'allow_put': True
'validate': {'type:string': db_const.PROJECT_ID_FIELD_SIZE},
'is_visible': True},
'name': {'allow_post': True, 'allow_put': False,
'validate': {'type:string': db_const.NAME_FIELD_SIZE},
'default': '',
'is_visible': True},
},`
这个RESOURCE_ATTRIBUTE_MAP 很重要,不可缺少,这里面的字段就是之后你的create、delete、update、get等操作的对象。在这里也就是数据库所有的字段
上面的id由于是调用uuidutils.generate_uuid()生成(步骤四),所以这里设置为False
allow_post : 是否允许post请求
vaildate : 指定类型等
is_visible : 返回信息中是否有此字段
ps:详细的解释请看/neutron/api/v2/attributes.py的注释
@classmethod
def get_name(cls):
return "test"
@classmethod
def get_alias(cls):
return "test"
@classmethod
def get_description(cls):
return "curd a test ."
@classmethod
def get_namespace(cls):
return "http://docs.openstack.org/ext/agent/api/v2.0"
@classmethod
def get_updated(cls):
return "2018-05-10T10:00:00-00:00"
@classmethod
def get_resources(cls):
"""Returns Ext Resources."""
plugin = directory.get_plugin()
resource_registry.register_resource_by_name(TESTS)
params = RESOURCE_ATTRIBUTE_MAP.get(TESTS)
controller = base.create_resource(TESTS,
TEST,
plugin, params,
allow_bulk=True,
allow_pagination=True,
allow_sorting=True)
ex = extensions.ResourceExtension(GROUPS, controller)
return [ex]
def get_extended_resources(self, version):
if version == "2.0":
return RESOURCE_ATTRIBUTE_MAP
else:
return {}
基类 api_extensions.ExtensionDescriptor 是必须要继承的。
前面的几个方法就不做讲解了,主要是get_resources 这个。
这部分不难,只有几行代码,只要认真看,就会发现这个方法实现的是资源的创建,controller的创建。(这里有兴趣的可以看看这篇文件,对neutron-server根据请求调用controller的剖析)
注意:这个的class名并不是顺便起的,要和本文件名相应,就是将本文件名的首字母换成大写(如:test.py class Test)
好了 extension下的test.py文件就这样完成了
既然是要操作数据库当然是要有db文件了,在添加db文件之前就要在model下添加一个文件,这个文件的作用就是定义的字段的格式
from neutron_lib.db import constants as db_const
from neutron_lib.db import model_base
import sqlalchemy as sa
from neutron.db import standard_attr
from neutron.extensions import group
class Test(standard_attr.HasStandardAttributes, model_base.BASEV2,
model_base.HasId, model_base.HasProject):
"""Represents a v2 neutron group."""
__tablename__ = 'groups'
name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE))
pid = sa.Column(sa.String(255), nullable=False)
project_id = sa.Column(sa.String(255), nullable=False)
这个文件就只有这点东西了
下面就到操作db的文件了
常见的操作都有create,delete,get,update,所以这里就要对这几个操作写对应的方法了
from neutron.db import _utils as db_utils
from neutron.db.models import test as test_model
from sqlalchemy import orm
from neutron.db import api as db_api
from neutron.db import common_db_mixin as base_db
from neutron.extensions import group
class TestDbMixin(base_db.CommonDbMixin):
@staticmethod
def _make_test_dict(g, fields=None):
res = {'id': g['id'],
'pid': g['pid'],
'name': g['name'],
'project_id': g['project_id']}
return db_utils.resource_fields(res, fields)
def create_test(self, context, test):
t = test['test']
with context.session.begin(subtransactions=True):
test_db = test_model.Test(
id=uuidutils.generate_uuid(),
pid=t['pid'],
project_id=t['project_id'],
name=t['name'])
context.session.add(test_db)
return self._make_test_dict(test_db)
def delete_test(self, context, test_id):
with db_api.context_manager.writer.using(context):
try:
test_ = self._get_by_id(context,
test_model.Test,
test_id)
except orm.exc.NoResultFound:
raise test.TestNotFound(test_id=test_id)
context.session.delete(test_)
@db_api.retry_if_session_inactive()
def get_test(self, context, test_id, fields=None):
try:
test_ = self._get_by_id(context,
test_model.Test,
test_id)
except orm.exc.NoResultFound:
raise test.TestNotFound(test_id=test_id)
return self._make_test_dict(test_, fields)
@db_api.retry_if_session_inactive()
def get_tests(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
marker_obj = self._get_marker_obj(context, 'test', limit,
marker)
tests = self._get_collection(context, test_model.Test,
self._make_test_dict,
filters=filters, fields=fields,
sorts=sorts,
limit=limit,
marker_obj=marker_obj,
page_reverse=page_reverse)
return self._make_test_dict(tests, fields)
def _get_test(self, context, test_id):
try:
query = self._model_query(context, test_model.Test)
t = query.filter(test_model.Test.id == test_id).one()
except orm.exc.NoResultFound:
raise test.TestNotFound(test_id=test_id)
return t
@db_api.retry_if_session_inactive()
def update_test(self, context, test, id):
t = test['test']
with context.session.begin(subtransactions=True):
test_ = self._get_test(context, id)
test_.update(g)
test_dict = self._make_test_dict(test_)
return test_dict
到这里neutron-server的文件基本添加完了
注意
def create_test(self, context, test):
这里的参数test一定要和create_test的test对应起来,这个坑我找了半天才发现,具体的原因目前还不是很清楚。要再跟代码才行
发现这个原因是/neutron/api/v2/base.py这个文件的do_create()中return请打印**kwargs参数时发现参数结构是这样的{“test”:“test”:{}}, 不知道从什么时候加了一层test的
!!没遇到的请忽略。。。。
还有最后的步骤
这里是在ml2/plugin,也就是core plugin下添加extension,如果想在其它service plugin中添加的话就在/neutron/services/ 目录下,选择操作的plugin的文件夹(想要在metering加就选择metering/metering_plugin.py)的plugin文件
如果想要自己添加一个service plugin的话就请看后续的文章。
添加test_db文件
from neutron.db import test_db
添加父类
在class Ml2Plugin继承的父类最后添加
test_db.TestDbMixin
添加别名
_supported_extension_aliases后添加"test"
这里的名字要和 extension目录下的test.py的一致(这里都是test)
@classmethod
def get_alias(cls):
return "test"
上面就neutron的所有步骤了,不对,还有最后一步
编辑/etc/neutron/policy.json文件
添加一下内容,
"update_test": "rule:admin_or_owner",
"delete_test": "rule:admin_or_owner",
"create_test": "rule:admin_or_owner"
这里对这些接口做了权限
重启neutron-server服务
获取token
curl -X POST -d '{"auth": {"passwordCredentials": {"username": "admin", "password": "password"}, "tenantName": "admin"}}' -H "Content-type: application/json" http://controller:5000/v2.0/tokens | python -m json.tool
返回结果一大堆,直接找到下面这一段
"token": {
"audit_ids": [
"98C63xlDSzKxUsHJKHxEfA"
],
"expires": "2018-05-11T09:34:16.000000Z",
"id": "gAAAAABa9VWIssxDBgLRUBipjKp4k1LSBtlplzXDDB8qNNRX6UYm7_k1W9ux0Pc1ouLlauZNfK4_8VQXFUrXox4NeUAr3Jb8GBhtiPqW3kEJlsacUBWQv4zQeVtJiqnYQUkP010UnO2VWUBMYjCRmUErO7L-6d_7YqnsPhPRV0GXLO68i0MFPUM",
"issued_at": "2018-05-11T08:34:16.000000Z",
"tenant": {
"description": "Bootstrap project for initializing the cloud.",
"enabled": true,
"id": "ae0d927ea53147c587f0256aad476f1e",
"name": "admin"
}
},
复制id的值
get 获取所有数据
curl -g -i GET http://controller:9696/v2.0/tests/ -H "User-Agent: openstacksdk/0.9.13 keystoneauth1/2.18.0 python-requests/2.11.1 CPython/2.7.5" -H "Accept: application/json" -H "X-Auth-Token: gAAAAABa9UXN6ge_yuMo-VMiDGxJnhVMLK_cJ-hWNTr0HD8ND1BXRjyVwOXvsm2meLRAKbjxa4KJbqMMpY5vqgCrwVbNybgmMfF6TigXZT6xgPU5-FqXxJx3Q4JBMdoUobVjfLqYMkWydrEQYo0Jst4e5zA9n9j9WaAWDOlklUknzR0izqggZ1s"
创建数据 post
curl -X POST http://127.0.0.1:9600/v1/controller/ -d '{"name": "name", "id": "id"}' -H "User-Agent: openstacksdk/0.9.13 keystoneauth1/2.18.0 python-requests/2.11.1 CPython/2.7.5" -H "Content-Type: application/json" -H "X-Auth-Token: gAAAAABa9UXN6ge_yuMo-VMiDGxJnhVMLK_cJ-hWNTr0HD8ND1BXRjyVwOXvsm2meLRAKbjxa4KJbqMMpY5vqgCrwVbNybgmMfF6TigXZT6xgPU5-FqXxJx3Q4JBMdoUobVjfLqYMkWydrEQYo0Jst4e5zA9n9j9WaAWDOlklUknzR0izqggZ1s"
其它就不一一填出来了,方法都一样
上面讲到的是neutron-server通过sqlalchemy操作数据库,最后生成了restful的api
下面就讲解用neutronclient端调用这些api
这里暂时不对shell命令的支持,只讲解dashboard那条路线
首先打开 /neutronclient/v2_0/client.py文件
直接找到Client这个类
class Client(ClientBase):
networks_path = "/networks"
network_path = "/networks/%s"
ports_path = "/ports"
port_path = "/ports/%s"
subnets_path = "/subnets"
subnet_path = "/subnets/%s"
subnetpools_path = "/subnetpools"
subnetpool_path = "/subnetpools/%s"
address_scopes_path = "/address-scopes"
address_scope_path = "/address-scopes/%s"
quotas_path = "/quotas"
quota_path = "/quotas/%s"
quota_default_path = "/quotas/%s/default"
extensions_path = "/extensions"
extension_path = "/extensions/%s"
routers_path = "/routers"
router_path = "/routers/%s"
floatingips_path = "/floatingips"
floatingip_path = "/floatingips/%s"
security_groups_path = "/security-groups"
上面是类的一部分代码,上面的xxx_path就是将要访问的资源的地址,顺便跟踪一个请求接口就能发现,这个neutronclient最后就是组成一个http请求,和我们在命令行用curl命令组件的http请求是相似的。
回到正题
我们需要将我们新增的test接口加到上面
在path的最后加上我们的
tests_path = “/test/”
test_path = “/test/%s”
然后就是在EXTED_PLURALS这个dict中增加
“test” : “test”
接着就要添加对test 做curd操作的接口了
def list_tests(self, retrieve_all=True, **_params):
"""Fetches a list of all tests for a project."""
# Pass filters in "params" argument to do_request
return self.list('test', self.tests_path, retrieve_all,
**_params)
def show_test(self, test, **_params):
"""Fetches information of a certain floatingip."""
return self.get(self.test_path % (group), params=_params)
def create_test(self, body=None):
"""Creates a new test."""
return self.post(self.tests_path, body=body)
def update_test(self, test, body=None):
"""Updates a test."""
return self.put(self.test_path % (test), body=body)
def delete_test(self, group):
"""Deletes the specified test."""
return self.delete(self.test_path % (test))
这里的代码就不多讲解了,跟踪一下代码就能发现原理了
上面就是neutronclient的全部了,当然,这里只支持dashboard调用这里接口,并没有对命令行的支持,如果需要对命令行的支持就要解析命令行的参数,再调用这些接口,这些neutronclient都有实现,只需要把我们新增的接口加上去,如果需要的可以参考原因逻辑自行增加,在/neutronclient/shell.py文件中
下面就到了最后一步,dashboard的接口
由于我们的test是在neutron下的,所以接口也就写在neutron模块下好了 /openstack_dashboard/api/neutron.py
添加一下代码:
@profiler.trace
def test_list(request, **params):
LOG.debug("test_list(): params=%s" % (params))
test = neutronclient(request).list_test(**params).get('test')
return test
@profiler.trace
def test_get(request, test_id, **params):
LOG.debug("test_get(): testid=%s, params=%s" % (test_id, params))
test = neutronclient(request).show_test(test_id,
**params).get('test')
return test
@profiler.trace
def test_create(request, **kwargs):
"""Create a test .
"""
LOG.debug("test_create(): kwargs=%s" % kwargs)
if 'project_id' not in kwargs:
kwargs['project_id'] = request.user.project_id
body = {'test': kwargs}
test = neutronclient(request).create_test(body=body).get('test')
return test
@profiler.trace
def test_update(request, test_id, **kwargs):
LOG.debug("test_update(): testid=%s, kwargs=%s" % (test_id, kwargs))
body = {'test': kwargs}
test = neutronclient(request).update_test(test_id,
body=body).get('test')
return test
@profiler.trace
def test_delete(request, test_id):
LOG.debug("test_delete(): testid=%s" % test_id)
neutronclient(request).delete_test(test_id)
没错,这段代码就是对neutronclient接口的封装调用,所以就不多讲解了
接来下怎么调用这些接口呢?怎么验证呢?
方法一
openstack是使用django框架的,所以页面都是在/dashboards目录下实现的,接口也就在这下面调用,具体怎么样就不多解析了,不了解django的可以先去了解下,再回来看看这目录下的代码
方法二
openstack还有另一种方法,就是restful方式调用,也实现了前后端的分离,前端只需要调用一个restful api就可以了 下面就用这种方法验证我们写的接口。
目录:/openstack_dashboard/api/rest
我们在neutron.py文件下添加
class Tests(generic.View):
url_regex = r'neutron/test/$'
@rest_utils.ajax()
def get(self, request):
""" test list
"""
res = api.neutron.test_list(request)
return {"data": res}
@rest_utils.ajax(data_required=True)
def post(self, request):
""" create a test
"""
test = api.neutron.test_create(request, **request.DATA)
return {"data": test}
@urls.register
class Test(generic.View):
url_regex = r'neutron/test/(?P[^/]+)/$'
@rest_utils.ajax()
def get(self, request, test_id):
""" get a test
"""
res = api.neutron.test_get(request, test_id)
return {"data": res}
@rest_utils.ajax()
def delete(self, request, test_id):
""" create a test
"""
test = api.neutron.test_delete(request, **request.DATA)
return {"data": test}
@rest_utils.ajax(data_required=True)
def delete(self, request, test_id):
""" create a test
"""
test = api.neutron.test_update(request, test_id, **request.DATA)
return {"data": test}
给前端使用的接口已经写完了,剩下的就是怎么验证呢?
首先在浏览器打开openstack的dashboard界面 按下F12 选到Console 如下图
如果使用post命令出现403的问题就先运行下面的段代码
$.ajaxSetup({
beforeSend: function(xhr, settings) {
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
文章链接https://blog.csdn.net/qq_24861509/article/details/50140413
GET
$.get("/dashboard/api/neutron/groups/",function(result){
console.log(result)
});
如果这种方式没有delete的接口就用第二种方式:
$.ajax({
type: "DELETE",
url: "/dashboard/api/neutron/test/test_id",
success: function(result){
console.log(result)
}
});
上面就是全部内容了,如果有什么问题可以先分析下日志文件,再根据日志文件分析代码,之后再去google查找问题吧