Optimise how to retrieve pull-down options
1. Add separated exception handler 2. Change to call APIs in parallel Change-Id: I51e53f10706e3e1bb7c7a3307e3f31c1e77a12ce
This commit is contained in:
parent
be97728d33
commit
4af156c837
@ -12,183 +12,268 @@
|
||||
|
||||
import json
|
||||
|
||||
from functools import wraps
|
||||
from multiprocessing import pool
|
||||
|
||||
from django.conf import settings
|
||||
from openstack_dashboard import api as dashboard_api
|
||||
from openstack_dashboard.api.neutron import neutronclient
|
||||
|
||||
from heat_dashboard import api
|
||||
from heat_dashboard import api as heat_api
|
||||
|
||||
|
||||
def get_resources(request):
|
||||
try:
|
||||
API_TIMEOUT = settings.API_TIMEOUT
|
||||
except AttributeError:
|
||||
API_TIMEOUT = 60
|
||||
|
||||
volumes = [
|
||||
vol.to_dict() for vol in dashboard_api.cinder.volume_list(request)]
|
||||
volume_snapshots = [
|
||||
volsnap.to_dict()
|
||||
for volsnap in dashboard_api.cinder.volume_snapshot_list(request)]
|
||||
volume_types = [
|
||||
voltype.to_dict()
|
||||
for voltype in dashboard_api.cinder.volume_type_list(request)]
|
||||
volume_backups = [
|
||||
volbackup.to_dict()
|
||||
for volbackup in dashboard_api.cinder.volume_backup_list(request)]
|
||||
try:
|
||||
API_PARALLEL = settings.API_PARALLEL
|
||||
except AttributeError:
|
||||
API_PARALLEL = 2
|
||||
|
||||
images = [
|
||||
img.to_dict()
|
||||
for img in dashboard_api.glance.image_list_detailed(request)[0]]
|
||||
|
||||
neutron_client = neutronclient(request)
|
||||
floatingips = neutron_client.list_floatingips().get('floatingips')
|
||||
networks = neutron_client.list_networks().get('networks')
|
||||
ports = neutron_client.list_ports().get('ports')
|
||||
security_groups = \
|
||||
neutron_client.list_security_groups().get('security_groups')
|
||||
subnets = neutron_client.list_subnets().get('subnets')
|
||||
routers = neutron_client.list_routers().get('routers')
|
||||
# qos_policies = neutron_client.list_security_groups().get('ports')
|
||||
def handle_exception(func):
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
ret, err = None, None
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
except Exception as error:
|
||||
err = error.message
|
||||
return ret if ret else [], err
|
||||
return wrapped
|
||||
|
||||
availability_zones = \
|
||||
[az.to_dict()
|
||||
for az in dashboard_api.nova.availability_zone_list(request)]
|
||||
flavors = \
|
||||
[flavor.to_dict()
|
||||
for flavor in dashboard_api.nova.flavor_list(request)]
|
||||
instances = \
|
||||
[server.to_dict()
|
||||
for server in dashboard_api.nova.server_list(request)[0]]
|
||||
keypairs = \
|
||||
[keypair.to_dict()
|
||||
for keypair in dashboard_api.nova.keypair_list(request)]
|
||||
|
||||
opts = {
|
||||
'user_roles': request.user.roles,
|
||||
'volumes': volumes,
|
||||
'volume_snapshots': volume_snapshots,
|
||||
'volume_types': volume_types,
|
||||
'volume_backups': volume_backups,
|
||||
'images': images,
|
||||
'floatingips': floatingips,
|
||||
'networks': networks,
|
||||
'ports': ports,
|
||||
'security_groups': security_groups,
|
||||
'subnets': subnets,
|
||||
'routers': routers,
|
||||
# 'qos_policies': qos_policies,
|
||||
'availability_zones': availability_zones,
|
||||
'flavors': flavors,
|
||||
'instances': instances,
|
||||
'keypairs': keypairs,
|
||||
}
|
||||
@handle_exception
|
||||
def get_networks(request):
|
||||
return dashboard_api.neutron.network_list(request)
|
||||
|
||||
return json.dumps(opts)
|
||||
|
||||
@handle_exception
|
||||
def get_subnets(request):
|
||||
return dashboard_api.neutron.subnet_list(request)
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_volume_ids(request):
|
||||
return [{'id': vol.id,
|
||||
'name': vol.name if vol.name else '(%s)' % vol.id}
|
||||
for vol in dashboard_api.cinder.volume_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_volume_snapshots(request):
|
||||
return [{'id': volsnap.id,
|
||||
'name': volsnap.name if volsnap.name else '(%s)' % volsnap.id[:6]}
|
||||
for volsnap in dashboard_api.cinder.volume_snapshot_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_volume_types(request):
|
||||
return [{'id': voltype.id,
|
||||
'name': voltype.name if voltype.name else '(%s)' % voltype.id[:6]}
|
||||
for voltype in dashboard_api.cinder.volume_type_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_volume_backups(request):
|
||||
return [{'id': volbackup.id,
|
||||
'name': volbackup.name
|
||||
if volbackup.name else '(%s)' % volbackup.id[:6]}
|
||||
for volbackup in dashboard_api.cinder.volume_backup_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_images(request):
|
||||
images = dashboard_api.glance.image_list_detailed(request)
|
||||
if isinstance(images, tuple):
|
||||
images = images[0]
|
||||
return [{'id': img.id,
|
||||
'name': img.name if img.name else '(%s)' % img.id[:6]}
|
||||
for img in images]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_floatingips(request):
|
||||
return [{'id': fip.id, 'name': fip.floating_ip_address}
|
||||
for fip in dashboard_api.neutron.tenant_floating_ip_list(
|
||||
request, True)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_ports(request):
|
||||
return [{'id': port.id,
|
||||
'name': port.name if port.name else '(%s)' % port.id[:6]}
|
||||
for port in dashboard_api.neutron.port_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_security_groups(request):
|
||||
return [{'id': secgroup.id,
|
||||
'name': secgroup.name
|
||||
if secgroup.name else '(%s)' % secgroup.id[:6]}
|
||||
for secgroup in dashboard_api.neutron.security_group_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_routers(request):
|
||||
return [{'id': router.id,
|
||||
'name': router.name if router.name else '(%s)' % router.id[:6]}
|
||||
for router in dashboard_api.neutron.router_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_qos_policies(request):
|
||||
return [{'id': policy.id,
|
||||
'name': policy.name
|
||||
if policy.name else '(%s)' % policy.id[:6]}
|
||||
for policy in dashboard_api.neutron.policy_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_availability_zones(request):
|
||||
return [{'id': az.zoneName, 'name': az.zoneName}
|
||||
for az in dashboard_api.nova.availability_zone_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_flavors(request):
|
||||
return [{'id': flavor.name, 'name': flavor.name}
|
||||
for flavor in dashboard_api.nova.flavor_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_instances(request):
|
||||
servers = dashboard_api.nova.server_list(request)
|
||||
if isinstance(servers, tuple):
|
||||
servers = servers[0]
|
||||
return [{'id': server.id,
|
||||
'name': server.name if server.name else '(%s)' % server.id[:6]}
|
||||
for server in servers]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_keypairs(request):
|
||||
return [{'name': keypair.name}
|
||||
for keypair in dashboard_api.nova.keypair_list(request)]
|
||||
|
||||
|
||||
@handle_exception
|
||||
def get_template_versions(request):
|
||||
return [{'name': version.version, 'id': version.version}
|
||||
for version in heat_api.heat.template_version_list(request)
|
||||
if version.type == 'hot']
|
||||
|
||||
|
||||
class APIThread(object):
|
||||
thread_pool = pool.ThreadPool(processes=API_PARALLEL)
|
||||
async_results = {}
|
||||
|
||||
def add_thread(self, apikey, func, args):
|
||||
self.async_results[apikey] = self.thread_pool.apply_async(func, args)
|
||||
|
||||
def get_async_result(self, apikey):
|
||||
if apikey not in self.async_results:
|
||||
return [], None
|
||||
try:
|
||||
ret, err = self.async_results[apikey].get(
|
||||
timeout=API_TIMEOUT)
|
||||
except Exception as error:
|
||||
ret, err = [], error.message
|
||||
return ret, err
|
||||
|
||||
|
||||
def _get_network_resources(options, all_networks):
|
||||
try:
|
||||
if all_networks:
|
||||
options['networks'] = [
|
||||
{'id': nw.id,
|
||||
'name': nw.name if nw.name else '(%s)' % nw.id[: 6]}
|
||||
for nw in all_networks if not getattr(nw, 'router:external')]
|
||||
options['floating_networks'] = [
|
||||
{'id': nw.id,
|
||||
'name': nw.name if nw.name else '(%s)' % nw.id[: 6]}
|
||||
for nw in all_networks if getattr(nw, 'router:external')]
|
||||
else:
|
||||
options['networks'] = []
|
||||
options['floating_networks'] = []
|
||||
except Exception:
|
||||
options['networks'] = []
|
||||
options['floating_networks'] = []
|
||||
|
||||
|
||||
def _get_subnet_resources(options, all_subnets):
|
||||
try:
|
||||
if all_subnets and options.get('floating_networks'):
|
||||
floating_network_ids = [nw.get('id')
|
||||
for nw in options['floating_networks']]
|
||||
options['subnets'] = [{'id': sb.id, 'name': sb.name}
|
||||
for sb in all_subnets
|
||||
if sb.network_id not in floating_network_ids]
|
||||
|
||||
options['floating_subnets'] = [
|
||||
{'id': subnet.id, 'name': subnet.name}
|
||||
for subnet in all_subnets
|
||||
if subnet.network_id in floating_network_ids]
|
||||
else:
|
||||
options['subnets'] = []
|
||||
options['floating_subnets'] = []
|
||||
except Exception:
|
||||
options['subnets'] = []
|
||||
options['floating_subnets'] = []
|
||||
|
||||
|
||||
def get_resource_options(request):
|
||||
|
||||
volumes = [{'id': vol.id,
|
||||
'name': vol.name if vol.name else '(%s)' % vol.id}
|
||||
for vol in dashboard_api.cinder.volume_list(request)]
|
||||
volume_snapshots = [
|
||||
{'id': volsnap.id,
|
||||
'name': volsnap.name if volsnap.name else '(%s)' % volsnap.id[:6]}
|
||||
for volsnap in dashboard_api.cinder.volume_snapshot_list(request)]
|
||||
volume_types = [{
|
||||
'id': voltype.id,
|
||||
'name': voltype.name if voltype.name else '(%s)' % voltype.id[:6]}
|
||||
for voltype in dashboard_api.cinder.volume_type_list(request)]
|
||||
volume_backups = [
|
||||
{'id': volbackup.id,
|
||||
'name': volbackup.name
|
||||
if volbackup.name else '(%s)' % volbackup.id[:6]}
|
||||
for volbackup in dashboard_api.cinder.volume_backup_list(request)]
|
||||
|
||||
images = [
|
||||
{'id': img.id,
|
||||
'name': img.name if img.name else '(%s)' % img.id[:6]}
|
||||
for img in dashboard_api.glance.image_list_detailed(request)[0]]
|
||||
|
||||
floatingips = [
|
||||
{'id': fip.id, 'name': fip.floating_ip_address}
|
||||
for fip in dashboard_api.neutron.tenant_floating_ip_list(
|
||||
request, True)]
|
||||
all_networks = dashboard_api.neutron.network_list(request)
|
||||
networks = [{'id': nw.id,
|
||||
'name': nw.name if nw.name else '(%s)' % nw.id[:6]}
|
||||
for nw in all_networks if not nw['router:external']]
|
||||
floating_networks = [{'id': nw.id,
|
||||
'name': nw.name if nw.name else '(%s)' % nw.id[:6]}
|
||||
for nw in all_networks if nw['router:external']]
|
||||
floating_network_ids = [nw.get('id') for nw in floating_networks]
|
||||
|
||||
ports = [{'id': port.id,
|
||||
'name': port.name if port.name else '(%s)' % port.id[:6]}
|
||||
for port in dashboard_api.neutron.port_list(request)]
|
||||
security_groups = [
|
||||
{'id': secgroup.id,
|
||||
'name': secgroup.name
|
||||
if secgroup.name else '(%s)' % secgroup.id[:6]}
|
||||
for secgroup in dashboard_api.neutron.security_group_list(request)]
|
||||
all_subnets = dashboard_api.neutron.subnet_list(request)
|
||||
subnets = [
|
||||
{'id': subnet.id,
|
||||
'name': subnet.name if subnet.name else '(%s)' % subnet.id[:6]}
|
||||
for subnet in all_subnets]
|
||||
|
||||
floating_subnets = [{'id': subnet.id, 'name': subnet.name}
|
||||
for subnet in all_subnets
|
||||
if subnet.network_id in floating_network_ids]
|
||||
|
||||
routers = [
|
||||
{'id': router.id,
|
||||
'name': router.name if router.name else '(%s)' % router.id[:6]}
|
||||
for router in dashboard_api.neutron.router_list(request)]
|
||||
qos_policies = []
|
||||
# qos_policies = [
|
||||
# {'id': policy.id,
|
||||
# 'name': policy.name
|
||||
# if policy.name else '(%s)' % policy.id[:6]}
|
||||
# for policy in dashboard_api.neutron.policy_list(request)]
|
||||
|
||||
availability_zones = [
|
||||
{'id': az.zoneName, 'name': az.zoneName}
|
||||
for az in dashboard_api.nova.availability_zone_list(request)]
|
||||
flavors = [{'id': flavor.name, 'name': flavor.name}
|
||||
for flavor in dashboard_api.nova.flavor_list(request)]
|
||||
instances = [{'id': server.id,
|
||||
'name': server.name
|
||||
if server.name else '(%s)' % server.id[:6]}
|
||||
for server in dashboard_api.nova.server_list(request)[0]]
|
||||
keypairs = [{'name': keypair.name}
|
||||
for keypair in dashboard_api.nova.keypair_list(request)]
|
||||
|
||||
template_versions = [
|
||||
{'name': version.version, 'id': version.version}
|
||||
for version in api.heat.template_version_list(request)
|
||||
if version.type == 'hot']
|
||||
|
||||
opts = {
|
||||
'auth': {
|
||||
'tenant_id': request.user.tenant_id,
|
||||
'admin': request.user.roles[0]['name'] == 'admin',
|
||||
},
|
||||
'volumes': volumes,
|
||||
'volume_snapshots': volume_snapshots,
|
||||
'volume_types': volume_types,
|
||||
'volume_backups': volume_backups,
|
||||
'images': images,
|
||||
'floatingips': floatingips,
|
||||
'floating_networks': floating_networks,
|
||||
'floating_subnets': floating_subnets,
|
||||
'networks': networks,
|
||||
'ports': ports,
|
||||
'security_groups': security_groups,
|
||||
'subnets': subnets,
|
||||
'routers': routers,
|
||||
'qos_policies': qos_policies,
|
||||
'availability_zones': availability_zones,
|
||||
'flavors': flavors,
|
||||
'instances': instances,
|
||||
'keypairs': keypairs,
|
||||
'template_versions': template_versions,
|
||||
api_threads = APIThread()
|
||||
api_mapping = {
|
||||
'volumes': get_volume_ids,
|
||||
'volume_snapshots': get_volume_snapshots,
|
||||
'volume_types': get_volume_types,
|
||||
'volume_backups': get_volume_backups,
|
||||
'images': get_images,
|
||||
'floatingips': get_floatingips,
|
||||
'networks': get_networks,
|
||||
'subnets': get_subnets,
|
||||
'ports': get_ports,
|
||||
'security_group': get_security_groups,
|
||||
'routers': get_routers,
|
||||
'qos_policies': get_qos_policies,
|
||||
'availability_zones': get_availability_zones,
|
||||
'flavors': get_flavors,
|
||||
'instances': get_instances,
|
||||
'keypairs': get_keypairs,
|
||||
'template_versions': get_template_versions,
|
||||
}
|
||||
|
||||
return json.dumps(opts)
|
||||
options = {}
|
||||
errors = {}
|
||||
|
||||
for resource, method in api_mapping.items():
|
||||
api_threads.add_thread(resource, method, args=(request,))
|
||||
|
||||
for resource in api_mapping.keys():
|
||||
ret, err = api_threads.get_async_result(resource)
|
||||
options[resource] = ret
|
||||
if err:
|
||||
errors[resource.replace('_', ' ').capitalize()] = err
|
||||
|
||||
all_networks = options.pop('networks')
|
||||
_get_network_resources(options, all_networks)
|
||||
|
||||
all_subnets = options.pop('subnets')
|
||||
_get_subnet_resources(options, all_subnets)
|
||||
|
||||
role_names = []
|
||||
for role in request.user.roles:
|
||||
role_names.append(role.get('name'))
|
||||
options.update({
|
||||
'auth': {
|
||||
'tenant_id': request.user.tenant_id,
|
||||
'admin': 'admin' in role_names,
|
||||
},
|
||||
})
|
||||
|
||||
if len(errors.keys()) > 0:
|
||||
options.update({'errors': errors})
|
||||
|
||||
return json.dumps(options)
|
||||
|
@ -16,8 +16,6 @@ from heat_dashboard.content.template_generator import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^get_resources$',
|
||||
views.ApiView.as_view(), name="options"),
|
||||
url(r'^get_resource_options$',
|
||||
views.OptionView.as_view(), name="apis"),
|
||||
]
|
||||
|
@ -17,7 +17,7 @@ from django.views import generic
|
||||
|
||||
from horizon.browsers.views import AngularIndexView
|
||||
|
||||
import api
|
||||
from heat_dashboard.content.template_generator import api
|
||||
|
||||
|
||||
class IndexView(AngularIndexView):
|
||||
@ -25,12 +25,6 @@ class IndexView(AngularIndexView):
|
||||
page_title = _("Template Generator")
|
||||
|
||||
|
||||
class ApiView(generic.View):
|
||||
def get(self, request):
|
||||
return HttpResponse(api.get_resources(request),
|
||||
content_type="application/json")
|
||||
|
||||
|
||||
class OptionView(generic.View):
|
||||
def get(self, request):
|
||||
return HttpResponse(api.get_resource_options(request),
|
||||
|
@ -32,3 +32,9 @@ settings.POLICY_FILES.update({
|
||||
# 'propagate': False,
|
||||
# }
|
||||
# })
|
||||
|
||||
# Template Generator retrieve options API TIMEOUT
|
||||
API_TIMEOUT = 60
|
||||
|
||||
# Template Generator retrieve options API PARALLEL LEVEL
|
||||
API_PARALLEL = 2
|
||||
|
@ -15,7 +15,17 @@
|
||||
}).then(function successCallback(response) {
|
||||
// this callback will be called asynchronously
|
||||
// when the response is available
|
||||
hotgenNotify.show_success('Retrieve openstack resources successfully.');
|
||||
if (response.data.errors){
|
||||
var msg = '';
|
||||
angular.forEach(response.data.errors, function(value, key){
|
||||
msg += key + ': '+ value + '. '
|
||||
})
|
||||
|
||||
hotgenNotify.show_warning('Unable to retrieve resources '+msg+'.');
|
||||
}
|
||||
else{
|
||||
hotgenNotify.show_success('Retrieve openstack resources successfully.');
|
||||
}
|
||||
return response.data;
|
||||
}, function errorCallback(response) {
|
||||
// called asynchronously if an error occurs
|
||||
|
@ -60,6 +60,26 @@
|
||||
|
||||
});
|
||||
|
||||
it('should return get_resource_options with errors', function(){
|
||||
spyOn($location, 'absUrl').and.callFake(function (p) {
|
||||
return 'http://some-url/';
|
||||
});
|
||||
requestHandler.respond(200, {
|
||||
'auth': {
|
||||
'tenant_id': 'tenant-id',
|
||||
'admin': false,
|
||||
},'errors': {'a': 'b'}}
|
||||
);
|
||||
$httpBackend.expectGET('http://some-url/get_resource_options');
|
||||
var optionsPromise = hotgenAgent.get_resource_options();
|
||||
optionsPromise.then(function(options){
|
||||
expect(options.auth.tenant_id).toEqual('tenant-id');
|
||||
expect(options.auth.admin).toEqual(false);
|
||||
});
|
||||
$httpBackend.flush();
|
||||
|
||||
});
|
||||
|
||||
it('should return error', function(){
|
||||
spyOn($location, 'absUrl').and.callFake(function (p) {
|
||||
return 'http://some-url';
|
||||
|
@ -14,6 +14,9 @@
|
||||
$scope.nodes = hotgenStates.get_nodes();
|
||||
$scope.selected = hotgenStates.get_selected();
|
||||
$scope.toggle = function (item, list) {
|
||||
if (typeof item == 'undefined' || !(list instanceof Array)){
|
||||
return;
|
||||
}
|
||||
var idx = list.indexOf(item);
|
||||
if (idx > -1) {
|
||||
list.splice(idx, 1);
|
||||
@ -24,7 +27,10 @@
|
||||
};
|
||||
|
||||
$scope.exists = function (item, list) {
|
||||
return list.indexOf(item) > -1;
|
||||
if (typeof item != "undefined" && list instanceof Array){
|
||||
return list.indexOf(item) > -1;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
}
|
||||
@ -46,4 +52,4 @@
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
})();
|
||||
|
209
heat_dashboard/test/test_data/cinder_data.py
Normal file
209
heat_dashboard/test/test_data/cinder_data.py
Normal file
@ -0,0 +1,209 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from cinderclient.v2 import volume_backups as vol_backups
|
||||
from cinderclient.v2 import volume_snapshots as vol_snaps
|
||||
from cinderclient.v2 import volume_types
|
||||
from cinderclient.v2 import volumes
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from openstack_dashboard.test.test_data import utils
|
||||
|
||||
|
||||
def data(TEST):
|
||||
TEST.cinder_volumes = utils.TestDataContainer()
|
||||
TEST.cinder_volume_backups = utils.TestDataContainer()
|
||||
TEST.cinder_volume_types = utils.TestDataContainer()
|
||||
TEST.cinder_volume_snapshots = utils.TestDataContainer()
|
||||
|
||||
# Volumes - Cinder v1
|
||||
volume = volumes.Volume(
|
||||
volumes.VolumeManager(None),
|
||||
{'id': "11023e92-8008-4c8b-8059-7f2293ff3887",
|
||||
'status': 'available',
|
||||
'size': 40,
|
||||
'display_name': 'Volume name',
|
||||
'display_description': 'Volume description',
|
||||
'created_at': '2014-01-27 10:30:00',
|
||||
'volume_type': None,
|
||||
'attachments': []})
|
||||
nameless_volume = volumes.Volume(
|
||||
volumes.VolumeManager(None),
|
||||
{"id": "4b069dd0-6eaa-4272-8abc-5448a68f1cce",
|
||||
"status": 'available',
|
||||
"size": 10,
|
||||
"display_name": '',
|
||||
"display_description": '',
|
||||
"device": "/dev/hda",
|
||||
"created_at": '2010-11-21 18:34:25',
|
||||
"volume_type": 'vol_type_1',
|
||||
"attachments": []})
|
||||
other_volume = volumes.Volume(
|
||||
volumes.VolumeManager(None),
|
||||
{'id': "21023e92-8008-1234-8059-7f2293ff3889",
|
||||
'status': 'in-use',
|
||||
'size': 10,
|
||||
'display_name': u'my_volume',
|
||||
'display_description': '',
|
||||
'created_at': '2013-04-01 10:30:00',
|
||||
'volume_type': None,
|
||||
'attachments': [{"id": "1", "server_id": '1',
|
||||
"device": "/dev/hda"}]})
|
||||
volume_with_type = volumes.Volume(
|
||||
volumes.VolumeManager(None),
|
||||
{'id': "7dcb47fd-07d9-42c2-9647-be5eab799ebe",
|
||||
'name': 'my_volume2',
|
||||
'status': 'in-use',
|
||||
'size': 10,
|
||||
'display_name': u'my_volume2',
|
||||
'display_description': '',
|
||||
'created_at': '2013-04-01 10:30:00',
|
||||
'volume_type': 'vol_type_2',
|
||||
'attachments': [{"id": "2", "server_id": '2',
|
||||
"device": "/dev/hdb"}]})
|
||||
non_bootable_volume = volumes.Volume(
|
||||
volumes.VolumeManager(None),
|
||||
{'id': "21023e92-8008-1234-8059-7f2293ff3890",
|
||||
'status': 'in-use',
|
||||
'size': 10,
|
||||
'display_name': u'my_volume',
|
||||
'display_description': '',
|
||||
'created_at': '2013-04-01 10:30:00',
|
||||
'volume_type': None,
|
||||
'bootable': False,
|
||||
'attachments': [{"id": "1", "server_id": '1',
|
||||
"device": "/dev/hda"}]})
|
||||
|
||||
volume.bootable = 'true'
|
||||
nameless_volume.bootable = 'true'
|
||||
other_volume.bootable = 'true'
|
||||
|
||||
TEST.cinder_volumes.add(api.cinder.Volume(volume))
|
||||
TEST.cinder_volumes.add(api.cinder.Volume(nameless_volume))
|
||||
TEST.cinder_volumes.add(api.cinder.Volume(other_volume))
|
||||
TEST.cinder_volumes.add(api.cinder.Volume(volume_with_type))
|
||||
TEST.cinder_volumes.add(api.cinder.Volume(non_bootable_volume))
|
||||
|
||||
vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||
{'id': u'1',
|
||||
'name': u'vol_type_1',
|
||||
'description': 'type 1 description',
|
||||
'extra_specs': {'foo': 'bar',
|
||||
'volume_backend_name':
|
||||
'backend_1'}})
|
||||
vol_type2 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||
{'id': u'2',
|
||||
'name': u'vol_type_2',
|
||||
'description': 'type 2 description'})
|
||||
vol_type3 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||
{'id': u'3',
|
||||
'name': u'vol_type_3',
|
||||
'is_public': False,
|
||||
'description': 'type 3 description'})
|
||||
TEST.cinder_volume_types.add(vol_type1, vol_type2, vol_type3)
|
||||
|
||||
# Volumes - Cinder v2
|
||||
volume_v2 = volumes.Volume(
|
||||
volumes.VolumeManager(None),
|
||||
{'id': "31023e92-8008-4c8b-8059-7f2293ff1234",
|
||||
'name': 'v2_volume',
|
||||
'description': "v2 Volume Description",
|
||||
'status': 'available',
|
||||
'size': 20,
|
||||
'created_at': '2014-01-27 10:30:00',
|
||||
'volume_type': None,
|
||||
'os-vol-host-attr:host': 'host@backend-name#pool',
|
||||
'bootable': 'true',
|
||||
'attachments': []})
|
||||
volume_v2.bootable = 'true'
|
||||
|
||||
TEST.cinder_volumes.add(api.cinder.Volume(volume_v2))
|
||||
|
||||
snapshot = vol_snaps.Snapshot(
|
||||
vol_snaps.SnapshotManager(None),
|
||||
{'id': '5f3d1c33-7d00-4511-99df-a2def31f3b5d',
|
||||
'display_name': 'test snapshot',
|
||||
'display_description': 'volume snapshot',
|
||||
'size': 40,
|
||||
'status': 'available',
|
||||
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
|
||||
snapshot2 = vol_snaps.Snapshot(
|
||||
vol_snaps.SnapshotManager(None),
|
||||
{'id': 'c9d0881a-4c0b-4158-a212-ad27e11c2b0f',
|
||||
'name': '',
|
||||
'description': 'v2 volume snapshot description',
|
||||
'size': 80,
|
||||
'status': 'available',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
snapshot3 = vol_snaps.Snapshot(
|
||||
vol_snaps.SnapshotManager(None),
|
||||
{'id': 'c9d0881a-4c0b-4158-a212-ad27e11c2b0e',
|
||||
'name': '',
|
||||
'description': 'v2 volume snapshot description 2',
|
||||
'size': 80,
|
||||
'status': 'available',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
snapshot4 = vol_snaps.Snapshot(
|
||||
vol_snaps.SnapshotManager(None),
|
||||
{'id': 'cd6be1eb-82ca-4587-8036-13c37c00c2b1',
|
||||
'name': '',
|
||||
'description': 'v2 volume snapshot with metadata description',
|
||||
'size': 80,
|
||||
'status': 'available',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234',
|
||||
'metadata': {'snapshot_meta_key': 'snapshot_meta_value'}})
|
||||
|
||||
snapshot.bootable = 'true'
|
||||
snapshot2.bootable = 'true'
|
||||
|
||||
TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot))
|
||||
TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot2))
|
||||
TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot3))
|
||||
TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot4))
|
||||
TEST.cinder_volume_snapshots.first()._volume = volume
|
||||
|
||||
volume_backup1 = vol_backups.VolumeBackup(
|
||||
vol_backups.VolumeBackupManager(None),
|
||||
{'id': 'a374cbb8-3f99-4c3f-a2ef-3edbec842e31',
|
||||
'name': 'backup1',
|
||||
'description': 'volume backup 1',
|
||||
'size': 10,
|
||||
'status': 'available',
|
||||
'container_name': 'volumebackups',
|
||||
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
|
||||
|
||||
volume_backup2 = vol_backups.VolumeBackup(
|
||||
vol_backups.VolumeBackupManager(None),
|
||||
{'id': 'c321cbb8-3f99-4c3f-a2ef-3edbec842e52',
|
||||
'name': 'backup2',
|
||||
'description': 'volume backup 2',
|
||||
'size': 20,
|
||||
'status': 'available',
|
||||
'container_name': 'volumebackups',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
|
||||
volume_backup3 = vol_backups.VolumeBackup(
|
||||
vol_backups.VolumeBackupManager(None),
|
||||
{'id': 'c321cbb8-3f99-4c3f-a2ef-3edbec842e53',
|
||||
'name': 'backup3',
|
||||
'description': 'volume backup 3',
|
||||
'size': 20,
|
||||
'status': 'available',
|
||||
'container_name': 'volumebackups',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
|
||||
TEST.cinder_volume_backups.add(volume_backup1)
|
||||
TEST.cinder_volume_backups.add(volume_backup2)
|
||||
TEST.cinder_volume_backups.add(volume_backup3)
|
426
heat_dashboard/test/test_data/glance_data.py
Normal file
426
heat_dashboard/test/test_data/glance_data.py
Normal file
@ -0,0 +1,426 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from glanceclient.v1 import images
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from heat_dashboard.test.test_data import utils
|
||||
|
||||
|
||||
class Namespace(dict):
|
||||
def __repr__(self):
|
||||
return "<Namespace %s>" % self._info
|
||||
|
||||
def __init__(self, info):
|
||||
super(Namespace, self).__init__()
|
||||
self.__dict__.update(info)
|
||||
self.update(info)
|
||||
self._info = info
|
||||
|
||||
def as_json(self, indent=4):
|
||||
return self.__dict__
|
||||
|
||||
|
||||
class APIResourceV2(dict):
|
||||
_base_props = [
|
||||
'id', 'name', 'status', 'visibility', 'protected', 'checksum', 'owner',
|
||||
'size', 'virtual_size', 'container_format', 'disk_format',
|
||||
'created_at', 'updated_at', 'tags', 'direct_url', 'min_ram',
|
||||
'min_disk', 'self', 'file', 'schema', 'locations']
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item == 'schema':
|
||||
return {'properties': {k: '' for k in self._base_props}}
|
||||
else:
|
||||
return self.get(item)
|
||||
|
||||
|
||||
def data(TEST):
|
||||
TEST.images = utils.TestDataContainer()
|
||||
TEST.images_api = utils.TestDataContainer()
|
||||
TEST.snapshots = utils.TestDataContainer()
|
||||
TEST.metadata_defs = utils.TestDataContainer()
|
||||
TEST.imagesV2 = utils.TestDataContainer()
|
||||
|
||||
# Snapshots
|
||||
snapshot_dict = {'name': u'snapshot',
|
||||
'container_format': u'ami',
|
||||
'id': 3,
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'properties': {'image_type': u'snapshot'},
|
||||
'is_public': False,
|
||||
'protected': False}
|
||||
snapshot_dict_no_owner = {'name': u'snapshot 2',
|
||||
'container_format': u'ami',
|
||||
'id': 4,
|
||||
'status': "active",
|
||||
'owner': None,
|
||||
'properties': {'image_type': u'snapshot'},
|
||||
'is_public': False,
|
||||
'protected': False}
|
||||
snapshot_dict_queued = {'name': u'snapshot 2',
|
||||
'container_format': u'ami',
|
||||
'id': 5,
|
||||
'status': "queued",
|
||||
'owner': TEST.tenant.id,
|
||||
'properties': {'image_type': u'snapshot'},
|
||||
'is_public': False,
|
||||
'protected': False}
|
||||
snapshot = images.Image(images.ImageManager(None), snapshot_dict)
|
||||
TEST.snapshots.add(api.glance.Image(snapshot))
|
||||
snapshot = images.Image(images.ImageManager(None), snapshot_dict_no_owner)
|
||||
TEST.snapshots.add(api.glance.Image(snapshot))
|
||||
snapshot = images.Image(images.ImageManager(None), snapshot_dict_queued)
|
||||
TEST.snapshots.add(api.glance.Image(snapshot))
|
||||
|
||||
# Images
|
||||
image_dict = {'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822',
|
||||
'name': 'public_image',
|
||||
'disk_format': u'qcow2',
|
||||
'status': "active",
|
||||
'size': 20 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'novaImage',
|
||||
'properties': {'image_type': u'image'},
|
||||
'is_public': True,
|
||||
'protected': False,
|
||||
'min_ram': 0,
|
||||
'created_at': '2014-02-14T20:56:53'}
|
||||
public_image = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': 'a001c047-22f8-47d0-80a1-8ec94a9524fe',
|
||||
'name': 'private_image',
|
||||
'status': "active",
|
||||
'size': 10 * 1024 ** 2,
|
||||
'virtual_size': 20 * 1024 ** 2,
|
||||
'min_disk': 0,
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'aki',
|
||||
'is_public': False,
|
||||
'protected': False,
|
||||
'min_ram': 0,
|
||||
'created_at': '2014-03-14T12:56:53'}
|
||||
private_image = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c',
|
||||
'name': 'protected_images',
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'size': 2 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 30,
|
||||
'container_format': 'novaImage',
|
||||
'properties': {'image_type': u'image'},
|
||||
'is_public': True,
|
||||
'protected': True,
|
||||
'min_ram': 0,
|
||||
'created_at': '2014-03-16T06:22:14'}
|
||||
protected_image = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': '278905a6-4b52-4d1e-98f9-8c57bb25ba32',
|
||||
'name': None,
|
||||
'status': "active",
|
||||
'size': 5 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'novaImage',
|
||||
'properties': {'image_type': u'image'},
|
||||
'is_public': True,
|
||||
'protected': False,
|
||||
'min_ram': 0}
|
||||
public_image2 = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': '710a1acf-a3e3-41dd-a32d-5d6b6c86ea10',
|
||||
'name': 'private_image 2',
|
||||
'status': "active",
|
||||
'size': 30 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'aki',
|
||||
'is_public': False,
|
||||
'protected': False,
|
||||
'min_ram': 0}
|
||||
private_image2 = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': '7cd892fd-5652-40f3-a450-547615680132',
|
||||
'name': 'private_image 3',
|
||||
'status': "active",
|
||||
'size': 2 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'aki',
|
||||
'is_public': False,
|
||||
'protected': False,
|
||||
'min_ram': 0}
|
||||
private_image3 = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
# A shared image. Not public and not local tenant.
|
||||
image_dict = {'id': 'c8756975-7a3b-4e43-b7f7-433576112849',
|
||||
'name': 'shared_image 1',
|
||||
'status': "active",
|
||||
'size': 8 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': 'someothertenant',
|
||||
'container_format': 'aki',
|
||||
'is_public': False,
|
||||
'protected': False,
|
||||
'min_ram': 0}
|
||||
shared_image1 = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
# "Official" image. Public and tenant matches an entry
|
||||
# in IMAGES_LIST_FILTER_TENANTS.
|
||||
image_dict = {'id': 'f448704f-0ce5-4d34-8441-11b6581c6619',
|
||||
'name': 'official_image 1',
|
||||
'status': "active",
|
||||
'size': 2 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': 'officialtenant',
|
||||
'container_format': 'aki',
|
||||
'is_public': True,
|
||||
'protected': False,
|
||||
'min_ram': 0}
|
||||
official_image1 = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': 'a67e7d45-fe1e-4c5c-bf08-44b4a4964822',
|
||||
'name': 'multi_prop_image',
|
||||
'status': "active",
|
||||
'size': 20 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'novaImage',
|
||||
'properties': {'description': u'a multi prop image',
|
||||
'foo': u'foo val',
|
||||
'bar': u'bar val'},
|
||||
'is_public': True,
|
||||
'protected': False}
|
||||
multi_prop_image = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
# An image without name being returned based on current api
|
||||
image_dict = {'id': 'c8756975-7a3b-4e43-b7f7-433576112849',
|
||||
'status': "active",
|
||||
'size': 8 * 1024 ** 3,
|
||||
'virtual_size': None,
|
||||
'min_disk': 0,
|
||||
'owner': 'someothertenant',
|
||||
'container_format': 'aki',
|
||||
'is_public': False,
|
||||
'protected': False}
|
||||
no_name_image = images.Image(images.ImageManager(None), image_dict)
|
||||
|
||||
TEST.images_api.add(public_image, private_image, protected_image,
|
||||
public_image2, private_image2, private_image3,
|
||||
shared_image1, official_image1, multi_prop_image)
|
||||
|
||||
TEST.images.add(api.glance.Image(public_image),
|
||||
api.glance.Image(private_image),
|
||||
api.glance.Image(protected_image),
|
||||
api.glance.Image(public_image2),
|
||||
api.glance.Image(private_image2),
|
||||
api.glance.Image(private_image3),
|
||||
api.glance.Image(shared_image1),
|
||||
api.glance.Image(official_image1),
|
||||
api.glance.Image(multi_prop_image))
|
||||
|
||||
TEST.empty_name_image = api.glance.Image(no_name_image)
|
||||
|
||||
image_v2_dicts = [{
|
||||
'checksum': 'eb9139e4942121f22bbc2afc0400b2a4',
|
||||
'container_format': 'novaImage',
|
||||
'created_at': '2014-02-14T20:56:53',
|
||||
'direct_url': 'swift+config://ref1/glance/'
|
||||
'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5',
|
||||
'disk_format': u'qcow2',
|
||||
'file': '/v2/images/'
|
||||
'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5/file',
|
||||
'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822',
|
||||
'kernel_id': 'f6ebd5f0-b110-4406-8c1e-67b28d4e85e7',
|
||||
'locations': [
|
||||
{'metadata': {},
|
||||
'url': 'swift+config://ref1/glance/'
|
||||
'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5'}],
|
||||
'min_ram': 0,
|
||||
'name': 'public_image',
|
||||
'image_type': u'image',
|
||||
'min_disk': 0,
|
||||
'owner': TEST.tenant.id,
|
||||
'protected': False,
|
||||
'ramdisk_id': '868efefc-4f2d-4ed8-82b1-7e35576a7a47',
|
||||
'size': 20 * 1024 ** 3,
|
||||
'status': 'active',
|
||||
'tags': ['active_image'],
|
||||
'updated_at': '2015-08-31T19:37:45Z',
|
||||
'virtual_size': None,
|
||||
'visibility': 'public'
|
||||
}, {
|
||||
'checksum': None,
|
||||
'container_format': 'novaImage',
|
||||
'created_at': '2014-03-16T06:22:14',
|
||||
'disk_format': None,
|
||||
'image_type': u'image',
|
||||
'file': '/v2/images/885d1cb0-9f5c-4677-9d03-175be7f9f984/file',
|
||||
'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c',
|
||||
'locations': [],
|
||||
'min_disk': 30,
|
||||
'min_ram': 0,
|
||||
'name': 'protected_images',
|
||||
'owner': TEST.tenant.id,
|
||||
'protected': True,
|
||||
'size': 2 * 1024 ** 3,
|
||||
'status': "active",
|
||||
'tags': ['empty_image'],
|
||||
'updated_at': '2015-09-01T22:37:32Z',
|
||||
'virtual_size': None,
|
||||
'visibility': 'public'
|
||||
}, {
|
||||
'checksum': 'e533283e6aac072533d1d091a7d2e413',
|
||||
'container_format': 'novaImage',
|
||||
'created_at': '2015-09-02T00:31:16Z',
|
||||
'disk_format': 'qcow2',
|
||||
'file': '/v2/images/10ca6b6b-48f4-43ac-8159-aa9e9353f5e4/file',
|
||||
'id': 'a67e7d45-fe1e-4c5c-bf08-44b4a4964822',
|
||||
'image_type': 'an image type',
|
||||
'min_disk': 0,
|
||||
'min_ram': 0,
|
||||
'name': 'multi_prop_image',
|
||||
'owner': TEST.tenant.id,
|
||||
'protected': False,
|
||||
'size': 20 * 1024 ** 3,
|
||||
'status': 'active',
|
||||
'tags': ['custom_property_image'],
|
||||
'updated_at': '2015-09-02T00:31:17Z',
|
||||
'virtual_size': None,
|
||||
'visibility': 'public',
|
||||
'description': u'a multi prop image',
|
||||
'foo': u'foo val',
|
||||
'bar': u'bar val'
|
||||
}]
|
||||
for fixture in image_v2_dicts:
|
||||
apiresource = APIResourceV2(fixture)
|
||||
TEST.imagesV2.add(api.glance.Image(apiresource))
|
||||
|
||||
metadef_dict = {
|
||||
'namespace': 'namespace_1',
|
||||
'display_name': 'Namespace 1',
|
||||
'description': 'Mock desc 1',
|
||||
'resource_type_associations': [
|
||||
{
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'prefix': 'mock',
|
||||
'name': 'mock name'
|
||||
}
|
||||
],
|
||||
'visibility': 'public',
|
||||
'protected': True,
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'properties': {
|
||||
'cpu_mock:mock': {
|
||||
'default': '1',
|
||||
'type': 'integer',
|
||||
'description': 'Number of mocks.',
|
||||
'title': 'mocks'
|
||||
}
|
||||
}
|
||||
}
|
||||
metadef = Namespace(metadef_dict)
|
||||
TEST.metadata_defs.add(metadef)
|
||||
|
||||
metadef_dict = {
|
||||
'namespace': 'namespace_2',
|
||||
'display_name': 'Namespace 2',
|
||||
'description': 'Mock desc 2',
|
||||
'resource_type_associations': [
|
||||
{
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'prefix': 'mock',
|
||||
'name': 'mock name'
|
||||
}
|
||||
],
|
||||
'visibility': 'private',
|
||||
'protected': False,
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'properties': {
|
||||
'hdd_mock:mock': {
|
||||
'default': '2',
|
||||
'type': 'integer',
|
||||
'description': 'Number of mocks.',
|
||||
'title': 'mocks'
|
||||
}
|
||||
}
|
||||
}
|
||||
metadef = Namespace(metadef_dict)
|
||||
TEST.metadata_defs.add(metadef)
|
||||
|
||||
metadef_dict = {
|
||||
'namespace': 'namespace_3',
|
||||
'display_name': 'Namespace 3',
|
||||
'description': 'Mock desc 3',
|
||||
'resource_type_associations': [
|
||||
{
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'prefix': 'mock',
|
||||
'name': 'mock name'
|
||||
}
|
||||
],
|
||||
'visibility': 'public',
|
||||
'protected': False,
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'properties': {
|
||||
'gpu_mock:mock': {
|
||||
'default': '2',
|
||||
'type': 'integer',
|
||||
'description': 'Number of mocks.',
|
||||
'title': 'mocks'
|
||||
}
|
||||
}
|
||||
}
|
||||
metadef = Namespace(metadef_dict)
|
||||
TEST.metadata_defs.add(metadef)
|
||||
|
||||
metadef_dict = {
|
||||
'namespace': 'namespace_4',
|
||||
'display_name': 'Namespace 4',
|
||||
'description': 'Mock desc 4',
|
||||
'resource_type_associations': [
|
||||
{
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'prefix': 'mock',
|
||||
'name': 'OS::Cinder::Volume',
|
||||
'properties_target': 'user'
|
||||
|
||||
}
|
||||
],
|
||||
'visibility': 'public',
|
||||
'protected': True,
|
||||
'created_at': '2014-08-21T08:39:43Z',
|
||||
'properties': {
|
||||
'ram_mock:mock': {
|
||||
'default': '2',
|
||||
'type': 'integer',
|
||||
'description': 'Number of mocks.',
|
||||
'title': 'mocks'
|
||||
}
|
||||
}
|
||||
}
|
||||
metadef = Namespace(metadef_dict)
|
||||
TEST.metadata_defs.add(metadef)
|
@ -22,6 +22,12 @@ from heat_dashboard.test.test_data import utils
|
||||
def data(TEST):
|
||||
# Data returned by openstack_dashboard.api.neutron wrapper.
|
||||
TEST.networks = utils.TestDataContainer()
|
||||
TEST.subnets = utils.TestDataContainer()
|
||||
TEST.ports = utils.TestDataContainer()
|
||||
TEST.routers = utils.TestDataContainer()
|
||||
TEST.floating_ips = utils.TestDataContainer()
|
||||
TEST.security_groups = utils.TestDataContainer()
|
||||
TEST.qos_policies = utils.TestDataContainer()
|
||||
|
||||
# Data return by neutronclient.
|
||||
TEST.api_networks = utils.TestDataContainer()
|
||||
@ -75,3 +81,116 @@ def data(TEST):
|
||||
subnetv6 = neutron.Subnet(subnetv6_dict)
|
||||
network['subnets'] = [subnet, subnetv6]
|
||||
TEST.networks.add(neutron.Network(network))
|
||||
TEST.subnets.add(subnet)
|
||||
TEST.subnets.add(subnetv6)
|
||||
|
||||
# Ports on 1st network.
|
||||
port_dict = {
|
||||
'admin_state_up': True,
|
||||
'device_id': 'af75c8e5-a1cc-4567-8d04-44fcd6922890',
|
||||
'device_owner': 'network:dhcp',
|
||||
'fixed_ips': [{'ip_address': '10.0.0.3',
|
||||
'subnet_id': subnet_dict['id']}],
|
||||
'id': '063cf7f3-ded1-4297-bc4c-31eae876cc91',
|
||||
'mac_address': 'fa:16:3e:9c:d5:7e',
|
||||
'name': '',
|
||||
'network_id': network_dict['id'],
|
||||
'status': 'ACTIVE',
|
||||
'tenant_id': network_dict['tenant_id'],
|
||||
'binding:vnic_type': 'normal',
|
||||
'binding:host_id': 'host',
|
||||
'allowed_address_pairs': [
|
||||
{'ip_address': '174.0.0.201',
|
||||
'mac_address': 'fa:16:3e:7a:7b:18'}
|
||||
],
|
||||
'port_security_enabled': True,
|
||||
'security_groups': [],
|
||||
}
|
||||
|
||||
TEST.ports.add(neutron.Port(port_dict))
|
||||
|
||||
# External network.
|
||||
network_dict = {'admin_state_up': True,
|
||||
'id': '9b466b94-213a-4cda-badf-72c102a874da',
|
||||
'name': 'ext_net',
|
||||
'status': 'ACTIVE',
|
||||
'subnets': ['d6bdc71c-7566-4d32-b3ff-36441ce746e8'],
|
||||
'tenant_id': '3',
|
||||
'router:external': True,
|
||||
'shared': False}
|
||||
subnet_dict = {'allocation_pools': [{'start': '172.24.4.226.',
|
||||
'end': '172.24.4.238'}],
|
||||
'dns_nameservers': [],
|
||||
'host_routes': [],
|
||||
'cidr': '172.24.4.0/28',
|
||||
'enable_dhcp': False,
|
||||
'gateway_ip': '172.24.4.225',
|
||||
'id': 'd6bdc71c-7566-4d32-b3ff-36441ce746e8',
|
||||
'ip_version': 4,
|
||||
'name': 'ext_subnet',
|
||||
'network_id': network_dict['id'],
|
||||
'tenant_id': network_dict['tenant_id']}
|
||||
ext_net = network_dict
|
||||
network = copy.deepcopy(network_dict)
|
||||
subnet = neutron.Subnet(subnet_dict)
|
||||
network['subnets'] = [subnet]
|
||||
TEST.networks.add(neutron.Network(network))
|
||||
TEST.subnets.add(subnet)
|
||||
|
||||
assoc_port = port_dict
|
||||
|
||||
router_dict = {'id': '279989f7-54bb-41d9-ba42-0d61f12fda61',
|
||||
'name': 'router1',
|
||||
'status': 'ACTIVE',
|
||||
'admin_state_up': True,
|
||||
'distributed': True,
|
||||
'external_gateway_info':
|
||||
{'network_id': ext_net['id']},
|
||||
'tenant_id': '1',
|
||||
'availability_zone_hints': ['nova']}
|
||||
TEST.routers.add(neutron.Router(router_dict))
|
||||
|
||||
# Associated (with compute port on 1st network).
|
||||
fip_dict = {'tenant_id': '1',
|
||||
'floating_ip_address': '172.16.88.228',
|
||||
'floating_network_id': ext_net['id'],
|
||||
'id': 'a97af8f2-3149-4b97-abbd-e49ad19510f7',
|
||||
'fixed_ip_address': assoc_port['fixed_ips'][0]['ip_address'],
|
||||
'port_id': assoc_port['id'],
|
||||
'router_id': router_dict['id']}
|
||||
fip_with_instance = copy.deepcopy(fip_dict)
|
||||
fip_with_instance.update({'instance_id': '1',
|
||||
'instance_type': 'compute'})
|
||||
TEST.floating_ips.add(neutron.FloatingIp(fip_with_instance))
|
||||
|
||||
# Security group.
|
||||
|
||||
sec_group_1 = {'tenant_id': '1',
|
||||
'description': 'default',
|
||||
'id': 'faad7c80-3b62-4440-967c-13808c37131d',
|
||||
'name': 'default'}
|
||||
sec_group_2 = {'tenant_id': '1',
|
||||
'description': 'NotDefault',
|
||||
'id': '27a5c9a1-bdbb-48ac-833a-2e4b5f54b31d',
|
||||
'name': 'other_group'}
|
||||
sec_group_3 = {'tenant_id': '1',
|
||||
'description': 'NotDefault',
|
||||
'id': '443a4d7a-4bd2-4474-9a77-02b35c9f8c95',
|
||||
'name': 'another_group'}
|
||||
groups = [sec_group_1, sec_group_2, sec_group_3]
|
||||
sg_name_dict = dict([(sg['id'], sg['name']) for sg in groups])
|
||||
for sg in groups:
|
||||
sg['security_group_rules'] = []
|
||||
# OpenStack Dashboard internaly API.
|
||||
TEST.security_groups.add(
|
||||
neutron.SecurityGroup(copy.deepcopy(sg), sg_name_dict))
|
||||
|
||||
# qos policies
|
||||
policy_dict = {'id': 'a21dcd22-7189-cccc-aa32-22adafaf16a7',
|
||||
'name': 'policy 1',
|
||||
'tenant_id': '1'}
|
||||
TEST.qos_policies.add(neutron.QoSPolicy(policy_dict))
|
||||
policy_dict1 = {'id': 'a21dcd22-7189-ssss-aa32-22adafaf16a7',
|
||||
'name': 'policy 2',
|
||||
'tenant_id': '1'}
|
||||
TEST.qos_policies.add(neutron.QoSPolicy(policy_dict1))
|
||||
|
201
heat_dashboard/test/test_data/nova_data.py
Normal file
201
heat_dashboard/test/test_data/nova_data.py
Normal file
@ -0,0 +1,201 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
from novaclient.v2 import availability_zones
|
||||
from novaclient.v2 import flavors
|
||||
from novaclient.v2 import keypairs
|
||||
from novaclient.v2 import servers
|
||||
|
||||
from heat_dashboard.test.test_data import utils
|
||||
|
||||
|
||||
class FlavorExtraSpecs(dict):
|
||||
def __repr__(self):
|
||||
return "<FlavorExtraSpecs %s>" % self._info
|
||||
|
||||
def __init__(self, info):
|
||||
super(FlavorExtraSpecs, self).__init__()
|
||||
self.__dict__.update(info)
|
||||
self.update(info)
|
||||
self._info = info
|
||||
|
||||
|
||||
SERVER_DATA = """
|
||||
{
|
||||
"server": {
|
||||
"OS-EXT-SRV-ATTR:instance_name": "instance-00000005",
|
||||
"OS-EXT-SRV-ATTR:host": "instance-host",
|
||||
"OS-EXT-STS:task_state": null,
|
||||
"addresses": {
|
||||
"private": [
|
||||
{
|
||||
"version": 4,
|
||||
"addr": "10.0.0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"href": "%(host)s/v1.1/%(tenant_id)s/servers/%(server_id)s",
|
||||
"rel": "self"
|
||||
},
|
||||
{
|
||||
"href": "%(host)s/%(tenant_id)s/servers/%(server_id)s",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
],
|
||||
"image": {
|
||||
"id": "%(image_id)s",
|
||||
"links": [
|
||||
{
|
||||
"href": "%(host)s/%(tenant_id)s/images/%(image_id)s",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OS-EXT-STS:vm_state": "active",
|
||||
"flavor": {
|
||||
"id": "%(flavor_id)s",
|
||||
"links": [
|
||||
{
|
||||
"href": "%(host)s/%(tenant_id)s/flavors/%(flavor_id)s",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": "%(server_id)s",
|
||||
"user_id": "%(user_id)s",
|
||||
"OS-DCF:diskConfig": "MANUAL",
|
||||
"accessIPv4": "",
|
||||
"accessIPv6": "",
|
||||
"progress": null,
|
||||
"OS-EXT-STS:power_state": 1,
|
||||
"config_drive": "",
|
||||
"status": "%(status)s",
|
||||
"updated": "2012-02-28T19:51:27Z",
|
||||
"hostId": "c461ea283faa0ab5d777073c93b126c68139e4e45934d4fc37e403c2",
|
||||
"key_name": "%(key_name)s",
|
||||
"name": "%(name)s",
|
||||
"created": "2012-02-28T19:51:17Z",
|
||||
"tenant_id": "%(tenant_id)s",
|
||||
"metadata": {"someMetaLabel": "someMetaData",
|
||||
"some<b>html</b>label": "<!--",
|
||||
"empty": ""}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def data(TEST):
|
||||
TEST.servers = utils.TestDataContainer()
|
||||
TEST.flavors = utils.TestDataContainer()
|
||||
TEST.keypairs = utils.TestDataContainer()
|
||||
TEST.availability_zones = utils.TestDataContainer()
|
||||
|
||||
# Flavors
|
||||
flavor_1 = flavors.Flavor(flavors.FlavorManager(None),
|
||||
{'id': "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
'name': 'm1.tiny',
|
||||
'vcpus': 1,
|
||||
'disk': 0,
|
||||
'ram': 512,
|
||||
'swap': 0,
|
||||
'rxtx_factor': 1,
|
||||
'extra_specs': {},
|
||||
'os-flavor-access:is_public': True,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 0})
|
||||
flavor_2 = flavors.Flavor(flavors.FlavorManager(None),
|
||||
{'id': "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
'name': 'm1.massive',
|
||||
'vcpus': 1000,
|
||||
'disk': 1024,
|
||||
'ram': 10000,
|
||||
'swap': 0,
|
||||
'rxtx_factor': 1,
|
||||
'extra_specs': {'Trusted': True, 'foo': 'bar'},
|
||||
'os-flavor-access:is_public': True,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||
flavor_3 = flavors.Flavor(flavors.FlavorManager(None),
|
||||
{'id': "dddddddd-dddd-dddd-dddd-dddddddddddd",
|
||||
'name': 'm1.secret',
|
||||
'vcpus': 1000,
|
||||
'disk': 1024,
|
||||
'ram': 10000,
|
||||
'swap': 0,
|
||||
'rxtx_factor': 1,
|
||||
'extra_specs': {},
|
||||
'os-flavor-access:is_public': False,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||
flavor_4 = flavors.Flavor(flavors.FlavorManager(None),
|
||||
{'id': "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
|
||||
'name': 'm1.metadata',
|
||||
'vcpus': 1000,
|
||||
'disk': 1024,
|
||||
'ram': 10000,
|
||||
'swap': 0,
|
||||
'rxtx_factor': 1,
|
||||
'extra_specs': FlavorExtraSpecs(
|
||||
{'key': 'key_mock',
|
||||
'value': 'value_mock'}),
|
||||
'os-flavor-access:is_public': False,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||
TEST.flavors.add(flavor_1, flavor_2, flavor_3, flavor_4)
|
||||
|
||||
# Key pairs
|
||||
keypair = keypairs.Keypair(keypairs.KeypairManager(None),
|
||||
dict(name='keyName'))
|
||||
TEST.keypairs.add(keypair)
|
||||
|
||||
# Servers
|
||||
vals = {"host": "http://nova.example.com:8774",
|
||||
"name": "server_1",
|
||||
"status": "ACTIVE",
|
||||
"tenant_id": TEST.tenants.first().id,
|
||||
"user_id": TEST.user.id,
|
||||
"server_id": "1",
|
||||
"flavor_id": flavor_1.id,
|
||||
"image_id": TEST.images.first().id,
|
||||
"key_name": keypair.name}
|
||||
server_1 = servers.Server(servers.ServerManager(None),
|
||||
json.loads(SERVER_DATA % vals)['server'])
|
||||
vals.update({"name": "server_2",
|
||||
"status": "BUILD",
|
||||
"server_id": "2"})
|
||||
server_2 = servers.Server(servers.ServerManager(None),
|
||||
json.loads(SERVER_DATA % vals)['server'])
|
||||
vals.update({"name": "server_4",
|
||||
"status": "PAUSED",
|
||||
"server_id": "4"})
|
||||
server_4 = servers.Server(servers.ServerManager(None),
|
||||
json.loads(SERVER_DATA % vals)['server'])
|
||||
TEST.servers.add(server_1, server_2, server_4)
|
||||
|
||||
# Availability Zones
|
||||
TEST.availability_zones.add(availability_zones.AvailabilityZone(
|
||||
availability_zones.AvailabilityZoneManager(None),
|
||||
{
|
||||
'zoneName': 'nova',
|
||||
'zoneState': {'available': True},
|
||||
'hosts': {
|
||||
"host001": {
|
||||
"nova-network": {
|
||||
"active": True,
|
||||
"available": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
))
|
@ -14,18 +14,21 @@
|
||||
|
||||
|
||||
def load_test_data(load_onto=None):
|
||||
from heat_dashboard.test.test_data import cinder_data
|
||||
from heat_dashboard.test.test_data import exceptions
|
||||
from heat_dashboard.test.test_data import glance_data
|
||||
from heat_dashboard.test.test_data import heat_data
|
||||
from heat_dashboard.test.test_data import keystone_data
|
||||
from heat_dashboard.test.test_data import neutron_data
|
||||
from heat_dashboard.test.test_data import nova_data
|
||||
|
||||
# The order of these loaders matters, some depend on others.
|
||||
loaders = (
|
||||
exceptions.data,
|
||||
keystone_data.data,
|
||||
# glance_data.data,
|
||||
# nova_data.data,
|
||||
# cinder_data.data,
|
||||
glance_data.data,
|
||||
nova_data.data,
|
||||
cinder_data.data,
|
||||
neutron_data.data,
|
||||
# swift_data.data,
|
||||
heat_data.data,
|
||||
|
105
heat_dashboard/test/tests/content/test_template_generator.py
Normal file
105
heat_dashboard/test/tests/content/test_template_generator.py
Normal file
@ -0,0 +1,105 @@
|
||||
# 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 json
|
||||
|
||||
from mox3.mox import IsA
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
from openstack_dashboard import api as dashboard_api
|
||||
|
||||
from heat_dashboard import api
|
||||
from heat_dashboard.test import helpers as test
|
||||
|
||||
|
||||
class TemplateGeneratorTests(test.TestCase):
|
||||
|
||||
def test_index(self):
|
||||
self.client.get(reverse('horizon:project:template_generator:index'))
|
||||
self.assertTemplateUsed(
|
||||
template_name='project/template_generator/index.html')
|
||||
|
||||
@test.create_stubs({
|
||||
api.heat: ('template_version_list', ),
|
||||
dashboard_api.neutron: (
|
||||
'network_list', 'subnet_list', 'tenant_floating_ip_list',
|
||||
'port_list', 'security_group_list', 'router_list', 'policy_list'),
|
||||
dashboard_api.cinder: (
|
||||
'volume_list', 'volume_snapshot_list',
|
||||
'volume_type_list', 'volume_backup_list'),
|
||||
dashboard_api.glance: ('image_list_detailed', ),
|
||||
dashboard_api.nova: ('availability_zone_list', 'flavor_list',
|
||||
'server_list', 'keypair_list')})
|
||||
def test_option(self):
|
||||
volumes = self.cinder_volumes.list()
|
||||
volume_snapshots = self.cinder_volume_snapshots.list()
|
||||
volume_types = self.cinder_volume_types.list()
|
||||
volume_backups = self.cinder_volume_backups.list()
|
||||
images = self.imagesV2.list()
|
||||
networks = self.networks.list()
|
||||
subnets = self.subnets.list()
|
||||
floating_ips = self.floating_ips.list()
|
||||
ports = self.ports.list()
|
||||
security_groups = self.security_groups.list()
|
||||
routers = self.routers.list()
|
||||
qos_policies = self.qos_policies.list()
|
||||
availability_zones = self.availability_zones.list()
|
||||
flavors = self.flavors.list()
|
||||
instances = self.servers.list()
|
||||
keypairs = self.keypairs.list()
|
||||
template_versions = self.template_versions.list()
|
||||
|
||||
dashboard_api.cinder.volume_list(
|
||||
IsA(http.HttpRequest)).AndReturn(volumes)
|
||||
dashboard_api.cinder.volume_snapshot_list(
|
||||
IsA(http.HttpRequest)).AndReturn(volume_snapshots)
|
||||
dashboard_api.cinder.volume_type_list(
|
||||
IsA(http.HttpRequest)).AndReturn(volume_types)
|
||||
dashboard_api.cinder.volume_backup_list(
|
||||
IsA(http.HttpRequest)).AndReturn(volume_backups)
|
||||
dashboard_api.glance.image_list_detailed(
|
||||
IsA(http.HttpRequest)).AndReturn(images)
|
||||
dashboard_api.neutron.network_list(
|
||||
IsA(http.HttpRequest)).AndReturn(networks)
|
||||
dashboard_api.neutron.subnet_list(
|
||||
IsA(http.HttpRequest)).AndReturn(subnets)
|
||||
dashboard_api.neutron.tenant_floating_ip_list(
|
||||
IsA(http.HttpRequest), True).AndReturn(floating_ips)
|
||||
dashboard_api.neutron.port_list(
|
||||
IsA(http.HttpRequest)).AndReturn(ports)
|
||||
dashboard_api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(security_groups)
|
||||
dashboard_api.neutron.router_list(
|
||||
IsA(http.HttpRequest)).AndReturn(routers)
|
||||
dashboard_api.neutron.policy_list(
|
||||
IsA(http.HttpRequest)).AndReturn(qos_policies)
|
||||
dashboard_api.nova.availability_zone_list(
|
||||
IsA(http.HttpRequest)).AndReturn(availability_zones)
|
||||
dashboard_api.nova.flavor_list(
|
||||
IsA(http.HttpRequest)).AndReturn(flavors)
|
||||
dashboard_api.nova.server_list(
|
||||
IsA(http.HttpRequest)).AndReturn(instances)
|
||||
dashboard_api.nova.keypair_list(
|
||||
IsA(http.HttpRequest)).AndReturn(keypairs)
|
||||
api.heat.template_version_list(
|
||||
IsA(http.HttpRequest)).AndReturn(template_versions)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
resp = self.client.get(reverse(
|
||||
'horizon:project:template_generator:apis'))
|
||||
data = resp.content
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
json_data = json.loads(data)
|
||||
self.assertEqual(len(json_data.keys()), 20)
|
Loading…
Reference in New Issue
Block a user