diff --git a/heat_dashboard/content/template_generator/api.py b/heat_dashboard/content/template_generator/api.py index 6b2a4695..ff3338a4 100644 --- a/heat_dashboard/content/template_generator/api.py +++ b/heat_dashboard/content/template_generator/api.py @@ -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) diff --git a/heat_dashboard/content/template_generator/urls.py b/heat_dashboard/content/template_generator/urls.py index 3da73863..e82572db 100644 --- a/heat_dashboard/content/template_generator/urls.py +++ b/heat_dashboard/content/template_generator/urls.py @@ -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"), ] diff --git a/heat_dashboard/content/template_generator/views.py b/heat_dashboard/content/template_generator/views.py index 4b0c89ea..3b1b163a 100644 --- a/heat_dashboard/content/template_generator/views.py +++ b/heat_dashboard/content/template_generator/views.py @@ -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), diff --git a/heat_dashboard/local_settings.d/_1699_orchestration_settings.py b/heat_dashboard/local_settings.d/_1699_orchestration_settings.py index af641dc4..163b63a6 100644 --- a/heat_dashboard/local_settings.d/_1699_orchestration_settings.py +++ b/heat_dashboard/local_settings.d/_1699_orchestration_settings.py @@ -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 diff --git a/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.js b/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.js index d322cfc8..9cffe34c 100644 --- a/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.js +++ b/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.js @@ -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 diff --git a/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.spec.js b/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.spec.js index 2a92e66b..95dd06f9 100644 --- a/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.spec.js +++ b/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/agent.module.spec.js @@ -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'; diff --git a/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/dependson.directive.js b/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/dependson.directive.js index 0c6e8bf4..9819e82b 100644 --- a/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/dependson.directive.js +++ b/heat_dashboard/static/dashboard/project/heat_dashboard/template_generator/js/components/dependson.directive.js @@ -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 @@ } }); -})(); \ No newline at end of file +})(); diff --git a/heat_dashboard/test/test_data/cinder_data.py b/heat_dashboard/test/test_data/cinder_data.py new file mode 100644 index 00000000..0bd9d9ca --- /dev/null +++ b/heat_dashboard/test/test_data/cinder_data.py @@ -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) diff --git a/heat_dashboard/test/test_data/glance_data.py b/heat_dashboard/test/test_data/glance_data.py new file mode 100644 index 00000000..544efe6a --- /dev/null +++ b/heat_dashboard/test/test_data/glance_data.py @@ -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 "" % 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) diff --git a/heat_dashboard/test/test_data/neutron_data.py b/heat_dashboard/test/test_data/neutron_data.py index 837200c1..80eb0dfb 100644 --- a/heat_dashboard/test/test_data/neutron_data.py +++ b/heat_dashboard/test/test_data/neutron_data.py @@ -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)) diff --git a/heat_dashboard/test/test_data/nova_data.py b/heat_dashboard/test/test_data/nova_data.py new file mode 100644 index 00000000..d6c2e872 --- /dev/null +++ b/heat_dashboard/test/test_data/nova_data.py @@ -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 "" % 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", + "somehtmllabel": "