diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index bbb2bc0f5..437cc0431 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -344,12 +344,12 @@ def keypair_list(request): def server_create(request, name, image, flavor, key_name, user_data, security_groups, block_device_mapping, nics=None, - instance_count=1, admin_pass=None): + availability_zone=None, instance_count=1, admin_pass=None): return Server(novaclient(request).servers.create( name, image, flavor, userdata=user_data, security_groups=security_groups, key_name=key_name, block_device_mapping=block_device_mapping, - nics=nics, + nics=nics, availability_zone=availability_zone, min_count=instance_count, admin_pass=admin_pass), request) @@ -571,3 +571,7 @@ def tenant_absolute_limits(request, reserved=False): else: limits_dict[limit.name] = limit.value return limits_dict + + +def availability_zone_list(request, detailed=False): + return novaclient(request).availability_zones.list(detailed=detailed) diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index ee3888ee1..216f87764 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -797,7 +797,8 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('flavor_list', 'keypair_list', - 'security_group_list',), + 'security_group_list', + 'availability_zone_list',), cinder: ('volume_snapshot_list', 'volume_list',), quotas: ('tenant_quota_usages',), @@ -836,6 +837,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) self.mox.ReplayAll() @@ -862,6 +865,7 @@ class InstanceTests(test.TestCase): api.nova: ('flavor_list', 'keypair_list', 'security_group_list', + 'availability_zone_list', 'server_create',), cinder: ('volume_list', 'volume_snapshot_list',)}) @@ -871,6 +875,7 @@ class InstanceTests(test.TestCase): keypair = self.keypairs.first() server = self.servers.first() sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() customization_script = 'user data' nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] @@ -880,6 +885,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, 'status': 'active'}) \ @@ -907,6 +914,7 @@ class InstanceTests(test.TestCase): [sec_group.name], None, nics=nics, + availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'') @@ -921,6 +929,7 @@ class InstanceTests(test.TestCase): 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, + 'availability_zone': avail_zone.zoneName, 'volume_type': '', 'network': self.networks.first().id, 'count': 1} @@ -936,6 +945,7 @@ class InstanceTests(test.TestCase): api.nova: ('flavor_list', 'keypair_list', 'security_group_list', + 'availability_zone_list', 'server_create',), cinder: ('volume_list', 'volume_snapshot_list',)}) @@ -946,6 +956,7 @@ class InstanceTests(test.TestCase): server = self.servers.first() volume = self.volumes.first() sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() customization_script = 'user data' device_name = u'vda' volume_choice = "%s:vol" % volume.id @@ -974,6 +985,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -1012,6 +1025,7 @@ class InstanceTests(test.TestCase): api.nova: ('flavor_list', 'keypair_list', 'security_group_list', + 'availability_zone_list', 'server_create',), cinder: ('volume_list', 'volume_snapshot_list',)}) @@ -1021,6 +1035,7 @@ class InstanceTests(test.TestCase): server = self.servers.first() volume = self.volumes.first() sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() customization_script = 'user data' device_name = u'vda' volume_choice = "%s:vol" % volume.id @@ -1033,6 +1048,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, 'status': 'active'}) \ @@ -1060,6 +1077,7 @@ class InstanceTests(test.TestCase): [sec_group.name], block_device_mapping, nics=nics, + availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'') @@ -1073,6 +1091,7 @@ class InstanceTests(test.TestCase): 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, + 'availability_zone': avail_zone.zoneName, 'volume_type': 'volume_id', 'volume_id': volume_choice, 'device_name': device_name, @@ -1090,7 +1109,8 @@ class InstanceTests(test.TestCase): api.nova: ('server_create', 'flavor_list', 'keypair_list', - 'security_group_list',), + 'security_group_list', + 'availability_zone_list',), cinder: ('volume_list', 'volume_snapshot_list',)}) def test_launch_instance_post_no_images_available_boot_from_volume(self): @@ -1099,6 +1119,7 @@ class InstanceTests(test.TestCase): server = self.servers.first() volume = self.volumes.first() sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() customization_script = 'user data' device_name = u'vda' volume_choice = "%s:vol" % volume.id @@ -1111,6 +1132,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, 'status': 'active'}) \ @@ -1139,6 +1162,7 @@ class InstanceTests(test.TestCase): [sec_group.name], block_device_mapping, nics=nics, + availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass=u'') @@ -1153,6 +1177,7 @@ class InstanceTests(test.TestCase): 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, + 'availability_zone': avail_zone.zoneName, 'network': self.networks.first().id, 'volume_type': 'volume_id', 'volume_id': volume_choice, @@ -1169,6 +1194,7 @@ class InstanceTests(test.TestCase): quotas: ('tenant_quota_usages',), api.nova: ('flavor_list', 'keypair_list', + 'availability_zone_list', 'security_group_list',), cinder: ('volume_list', 'volume_snapshot_list',)}) @@ -1177,6 +1203,7 @@ class InstanceTests(test.TestCase): keypair = self.keypairs.first() server = self.servers.first() sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() customization_script = 'user data' api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -1203,6 +1230,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn([]) cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -1218,6 +1247,7 @@ class InstanceTests(test.TestCase): 'project_id': self.tenants.first().id, 'user_id': self.user.id, 'groups': sec_group.name, + 'availability_zone': avail_zone.zoneName, 'volume_type': '', 'count': 1} url = reverse('horizon:project:instances:launch') @@ -1237,7 +1267,8 @@ class InstanceTests(test.TestCase): 'volume_snapshot_list',), api.nova: ('flavor_list', 'keypair_list', - 'security_group_list',)}) + 'security_group_list', + 'availability_zone_list',)}) def test_launch_flavorlist_error(self): cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) @@ -1268,6 +1299,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) self.mox.ReplayAll() @@ -1281,6 +1314,7 @@ class InstanceTests(test.TestCase): api.nova: ('flavor_list', 'keypair_list', 'security_group_list', + 'availability_zone_list', 'server_create',), cinder: ('volume_list', 'volume_snapshot_list',)}) @@ -1290,6 +1324,7 @@ class InstanceTests(test.TestCase): keypair = self.keypairs.first() server = self.servers.first() sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() customization_script = 'userData' nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] @@ -1299,6 +1334,8 @@ class InstanceTests(test.TestCase): api.nova.keypair_list(IgnoreArg()).AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, 'status': 'active'}) \ @@ -1324,6 +1361,7 @@ class InstanceTests(test.TestCase): [sec_group.name], None, nics=nics, + availability_zone=avail_zone.zoneName, instance_count=IsA(int), admin_pass='password') \ .AndRaise(self.exceptions.keystone) @@ -1333,6 +1371,7 @@ class InstanceTests(test.TestCase): form_data = {'flavor': flavor.id, 'source_type': 'image_id', 'image_id': image.id, + 'availability_zone': avail_zone.zoneName, 'keypair': keypair.name, 'name': server.name, 'customization_script': customization_script, @@ -1354,7 +1393,8 @@ class InstanceTests(test.TestCase): quotas: ('tenant_quota_usages',), api.nova: ('flavor_list', 'keypair_list', - 'security_group_list',), + 'security_group_list', + 'availability_zone_list',), cinder: ('volume_list', 'volume_snapshot_list',)}) def test_launch_form_instance_count_error(self): @@ -1364,6 +1404,7 @@ class InstanceTests(test.TestCase): server = self.servers.first() volume = self.volumes.first() sec_group = self.security_groups.first() + avail_zone = self.availability_zones.first() customization_script = 'user data' device_name = u'vda' volume_choice = "%s:vol" % volume.id @@ -1374,6 +1415,8 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, 'status': 'active'}) \ @@ -1403,6 +1446,7 @@ class InstanceTests(test.TestCase): form_data = {'flavor': flavor.id, 'source_type': 'image_id', 'image_id': image.id, + 'availability_zone': avail_zone.zoneName, 'keypair': keypair.name, 'name': server.name, 'customization_script': customization_script, @@ -1469,7 +1513,8 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('flavor_list', 'keypair_list', - 'security_group_list',), + 'security_group_list', + 'availability_zone_list',), cinder: ('volume_snapshot_list', 'volume_list',), quotas: ('tenant_quota_usages',), @@ -1509,6 +1554,8 @@ class InstanceTests(test.TestCase): .AndReturn([keypair]) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest)) \ + .AndReturn(self.availability_zones.list()) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py index 0c2f301a6..3aa1e4691 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py @@ -179,6 +179,8 @@ class SetInstanceDetailsAction(workflows.Action): image_id = forms.ChoiceField(label=_("Image"), required=False) instance_snapshot_id = forms.ChoiceField(label=_("Instance Snapshot"), required=False) + availability_zone = forms.ChoiceField(label=_("Availability Zone"), + required=False) name = forms.CharField(max_length=80, label=_("Instance Name")) flavor = forms.ChoiceField(label=_("Flavor"), help_text=_("Size of image to launch.")) @@ -271,6 +273,23 @@ class SetInstanceDetailsAction(workflows.Action): _('Unable to retrieve instance flavors.')) return sorted(flavor_list) + def populate_availability_zone_choices(self, request, context): + try: + zones = api.nova.availability_zone_list(request) + except: + zones = [] + exceptions.handle(request, + _('Unable to retrieve availability zones.')) + + zone_list = [(zone.zoneName, zone.zoneName) + for zone in zones if zone.zoneState['available']] + zone_list.sort() + if zone_list: + zone_list.insert(0, ("", _("Any Availability Zone"))) + else: + zone_list.insert(0, ("", _("No availability zones found."))) + return zone_list + def get_help_text(self): extra = {} try: @@ -287,7 +306,8 @@ class SetInstanceDetailsAction(workflows.Action): class SetInstanceDetails(workflows.Step): action_class = SetInstanceDetailsAction - contributes = ("source_type", "source_id", "name", "count", "flavor") + contributes = ("source_type", "source_id", "availability_zone", + "name", "count", "flavor") def prepare_action_context(self, request, context): if 'source_type' in context and 'source_id' in context: @@ -504,6 +524,8 @@ class LaunchInstance(workflows.Workflow): else: nics = None + avail_zone = context.get('availability_zone', None) + try: api.nova.server_create(request, context['name'], @@ -514,6 +536,7 @@ class LaunchInstance(workflows.Workflow): context['security_group_ids'], dev_mapping, nics=nics, + availability_zone=avail_zone, instance_count=int(context['count']), admin_pass=context['admin_pass']) return True diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index fcad91416..77fc41448 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -21,7 +21,8 @@ from novaclient.v1_1 import (flavors, keypairs, servers, volumes, floating_ips, usage, certs, volume_snapshots as vol_snaps, security_group_rules as rules, - security_groups as sec_groups) + security_groups as sec_groups, + availability_zones) from openstack_dashboard.api.base import Quota, QuotaSet as QuotaSetWrapper from openstack_dashboard.api.nova import FloatingIp as NetFloatingIp @@ -153,6 +154,7 @@ def data(TEST): TEST.certs = TestDataContainer() TEST.volume_snapshots = TestDataContainer() TEST.volume_types = TestDataContainer() + TEST.availability_zones = TestDataContainer() # Data return by novaclient. # It is used if API layer does data conversion. @@ -448,3 +450,11 @@ def data(TEST): 'data': 'certificate_data'} certificate = certs.Certificate(certs.CertificateManager(None), cert_data) TEST.certs.add(certificate) + + # Availability Zones + TEST.availability_zones.add( + availability_zones.AvailabilityZone( + availability_zones.AvailabilityZoneManager(None), + {'zoneName': 'nova', 'zoneState': {'available': True}} + ) + )