class AttachVolume(forms.SelfHandlingForm): volume = forms.ChoiceField(label=_("Volume ID"), widget=forms.ThemableSelectWidget(), help_text=_("Select a volume to attach " "to this instance.")) device = forms.CharField(label=_("Device Name"), widget=forms.HiddenInput(), required=False, help_text=_("Actual device name may differ due " "to hypervisor settings. If not " "specified, then hypervisor will " "select a device name.")) instance_id = forms.CharField(widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): super(AttachVolume, self).__init__(*args, **kwargs) # Populate volume choices volume_list = kwargs.get('initial', {}).get("volume_list", []) volumes = [] for volume in volume_list: # Only show volumes that aren't attached to an instance already if not volume.attachments: volumes.append((volume.id, '%(name)s (%(id)s)' % { "name": volume.name, "id": volume.id })) if volumes: volumes.insert(0, ("", _("Select a volume"))) else: volumes.insert(0, ("", _("No volumes available"))) self.fields['volume'].choices = volumes def handle(self, request, data): instance_id = self.initial.get("instance_id", None) volume_choices = dict(self.fields['volume'].choices) volume = volume_choices.get(data['volume'], _("Unknown volume (None)")) volume_id = data.get('volume') device = data.get('device') or None try: attach = api.nova.instance_volume_attach(request, volume_id, instance_id, device) message = _('Attaching volume %(vol)s to instance ' '%(inst)s on %(dev)s.') % { "vol": volume, "inst": instance_id, "dev": attach.device } messages.info(request, message) except Exception as ex: redirect = reverse('horizon:project:tasks:index') if isinstance(ex, api.nova.VolumeMultiattachNotSupported): # Use the specific error from the specific message. msg = six.text_type(ex) else: # Use a generic error message. msg = _('Unable to attach volume.') exceptions.handle(request, msg, redirect=redirect) return True
class CreateSubnetInfoAction(workflows.Action): subnet_name = forms.CharField(max_length=255, widget=forms.TextInput(attrs={}), label=_("Subnet Name"), required=False) address_source = forms.ChoiceField( required=False, label=_('Network Address Source'), choices=[('manual', _('Enter Network Address manually')), ('subnetpool', _('Allocate Network Address from a pool'))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'source', })) subnetpool = forms.ChoiceField( label=_("Address pool"), widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched switchable', 'data-required-when-shown': 'true', 'data-slug': 'subnetpool', 'data-switch-on': 'source', 'data-source-subnetpool': _('Address pool') }, data_attrs=('name', 'prefixes', 'ip_version', 'min_prefixlen', 'max_prefixlen', 'default_prefixlen'), transform=lambda x: "%s (%s)" % (x.name, ", ".join(x.prefixes)) if 'prefixes' in x else "%s" % (x.name)), required=False) prefixlen = forms.ChoiceField( widget=forms.ThemableSelectWidget(attrs={ 'class': 'switched', 'data-switch-on': 'subnetpool', }), label=_('Network Mask'), required=False) cidr = forms.IPField(label=_("Network Address"), required=False, initial="", error_messages={ 'required': _('Specify "Network Address" or ' 'clear "Create Subnet" checkbox ' 'in previous step.') }, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-manual': _("Network Address"), }), help_text=_("Network address in CIDR format " "(e.g. 192.168.0.0/24, 2001:DB8::/48)"), version=forms.IPv4 | forms.IPv6, mask=True) ip_version = forms.ChoiceField( choices=[(4, 'IPv4'), (6, 'IPv6')], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'ipversion', }), label=_("IP Version"), required=False) gateway_ip = forms.IPField( label=_("Gateway IP"), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'gateway_ip', 'data-source-manual': _("Gateway IP") }), required=False, initial="", help_text=_("IP address of Gateway (e.g. 192.168.0.254) " "The default value is the first IP of the " "network address " "(e.g. 192.168.0.1 for 192.168.0.0/24, " "2001:DB8::1 for 2001:DB8::/48). " "If you use the default, leave blank. " "If you do not want to use a gateway, " "check 'Disable Gateway' below."), version=forms.IPv4 | forms.IPv6, mask=False) no_gateway = forms.BooleanField(label=_("Disable Gateway"), widget=forms.CheckboxInput( attrs={ 'class': 'switchable', 'data-slug': 'gateway_ip', 'data-hide-on-checked': 'true' }), initial=False, required=False) check_subnet_range = True class Meta(object): name = _("Subnet") help_text = _('Creates a subnet associated with the network.' ' You need to enter a valid "Network Address"' ' and "Gateway IP". If you did not enter the' ' "Gateway IP", the first value of a network' ' will be assigned by default. If you do not want' ' gateway please check the "Disable Gateway" checkbox.' ' Advanced configuration is available by clicking on' ' the "Subnet Details" tab.') def __init__(self, request, context, *args, **kwargs): super().__init__(request, context, *args, **kwargs) if not setting_utils.get_dict_config('OPENSTACK_NEUTRON_NETWORK', 'enable_ipv6'): self.fields['ip_version'].widget = forms.HiddenInput() self.fields['ip_version'].initial = 4 try: if api.neutron.is_extension_supported(request, 'subnet_allocation'): self.fields['subnetpool'].choices = \ self.get_subnetpool_choices(request) else: self.hide_subnetpool_choices() except Exception: self.hide_subnetpool_choices() msg = _('Unable to initialize subnetpools') exceptions.handle(request, msg) if len(self.fields['subnetpool'].choices) > 1: # Pre-populate prefixlen choices to satisfy Django # ChoiceField Validation. This is overridden w/data from # subnetpool on select. self.fields['prefixlen'].choices = \ zip(list(range(0, 128 + 1)), list(range(0, 128 + 1))) # Populate data-fields for switching the prefixlen field # when user selects a subnetpool other than # "Provider default pool" for (id_, name) in self.fields['subnetpool'].choices: if not id_: continue key = 'data-subnetpool-' + id_ self.fields['prefixlen'].widget.attrs[key] = \ _('Network Mask') else: self.hide_subnetpool_choices() def get_subnetpool_choices(self, request): subnetpool_choices = [('', _('Select a pool'))] for subnetpool in api.neutron.subnetpool_list(request): subnetpool_choices.append((subnetpool.id, subnetpool)) return subnetpool_choices def hide_subnetpool_choices(self): self.fields['address_source'].widget = forms.HiddenInput() self.fields['subnetpool'].choices = [] self.fields['subnetpool'].widget = forms.HiddenInput() self.fields['prefixlen'].widget = forms.HiddenInput() def _check_subnet_range(self, subnet, allow_cidr): allowed_net = netaddr.IPNetwork(allow_cidr) return subnet in allowed_net def _check_cidr_allowed(self, ip_version, subnet): if not self.check_subnet_range: return allowed_cidr = settings.ALLOWED_PRIVATE_SUBNET_CIDR version_str = 'ipv%s' % ip_version allowed_ranges = allowed_cidr.get(version_str, []) if allowed_ranges: under_range = any( self._check_subnet_range(subnet, allowed_range) for allowed_range in allowed_ranges) if not under_range: range_str = ', '.join(allowed_ranges) msg = (_("CIDRs allowed for user private %(ip_ver)s " "networks are %(allowed)s.") % { 'ip_ver': '%s' % version_str, 'allowed': range_str }) raise forms.ValidationError(msg) def _check_subnet_data(self, cleaned_data): cidr = cleaned_data.get('cidr') ip_version = int(cleaned_data.get('ip_version')) gateway_ip = cleaned_data.get('gateway_ip') no_gateway = cleaned_data.get('no_gateway') address_source = cleaned_data.get('address_source') subnetpool = cleaned_data.get('subnetpool') if not subnetpool and address_source == 'subnetpool': msg = _('Specify "Address pool" or select ' '"Enter Network Address manually" and specify ' '"Network Address".') raise forms.ValidationError(msg) if not cidr and address_source != 'subnetpool': msg = _('Specify "Network Address" or ' 'clear "Create Subnet" checkbox in previous step.') raise forms.ValidationError(msg) if address_source == 'subnetpool' and 'cidr' in self._errors: del self._errors['cidr'] elif cidr: subnet = netaddr.IPNetwork(cidr) if subnet.version != ip_version: msg = _('Network Address and IP version are inconsistent.') raise forms.ValidationError(msg) if (ip_version == 4 and subnet.prefixlen == 32) or \ (ip_version == 6 and subnet.prefixlen == 128): msg = _("The subnet in the Network Address is " "too small (/%s).") % subnet.prefixlen self._errors['cidr'] = self.error_class([msg]) self._check_cidr_allowed(ip_version, subnet) if not no_gateway and gateway_ip: if netaddr.IPAddress(gateway_ip).version is not ip_version: msg = _('Gateway IP and IP version are inconsistent.') raise forms.ValidationError(msg) if no_gateway and 'gateway_ip' in self._errors: del self._errors['gateway_ip'] def _remove_fields_errors(self): self._errors = {} def clean(self): with_subnet = self.initial.get('with_subnet') if not with_subnet: self._remove_fields_errors() return None cleaned_data = super().clean() self._check_subnet_data(cleaned_data) return cleaned_data
class ImportPackageForm(forms.Form): import_type = forms.ChoiceField( label=_("Package Source"), choices=IMPORT_TYPE_CHOICES, widget=horizon_forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'source' })) url = PackageURLField( label=_("Package URL"), required=False, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_url': _('Package URL') }), help_text=_('An external http/https URL to load the package from.')) repo_name = horizon_forms.CharField( label=_("Package Name"), required=False, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_name': _('Package Name') }), help_text=_( 'Package name in the repository, usually a fully qualified name'), ) package = forms.FileField( label=_('Application Package'), required=False, widget=forms.FileInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-upload': _('Application Package') }), help_text=_('A local zip file to upload')) repo_version = horizon_forms.CharField( label=_("Package version"), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_name': _('Package version') }), required=False) def __init__(self, *args, **kwargs): super(ImportPackageForm, self).__init__(*args, **kwargs) self.fields['repo_version'].widget.attrs['placeholder'] = \ _('Optional') def clean_package(self): package = self.cleaned_data.get('package') if package: max_size_in_bytes = consts.MAX_FILE_SIZE_MB << 20 if package.size > max_size_in_bytes: msg = _('It is forbidden to upload files larger than ' '{0} MB.').format(consts.MAX_FILE_SIZE_MB) LOG.error(msg) raise forms.ValidationError(msg) return package def clean(self): cleaned_data = super(ImportPackageForm, self).clean() import_type = cleaned_data.get('import_type') if import_type == 'upload' and not cleaned_data.get('package'): msg = _('Please supply a package file') LOG.error(msg) raise forms.ValidationError(msg) elif import_type == 'by_name' and not cleaned_data.get('repo_name'): msg = _('Please supply a package name') LOG.error(msg) raise forms.ValidationError(msg) elif import_type == 'by_url' and not cleaned_data.get('url'): msg = _('Please supply a package url') LOG.error(msg) raise forms.ValidationError(msg) return cleaned_data
class RetypeForm(forms.SelfHandlingForm): name = forms.CharField(label=_('Volume Name'), widget=forms.TextInput( attrs={'readonly': 'readonly'})) volume_type = forms.ThemableChoiceField(label=_('Type')) MIGRATION_POLICY_CHOICES = [('never', _('Never')), ('on-demand', _('On Demand'))] migration_policy = forms.ChoiceField(label=_('Migration Policy'), widget=forms.ThemableSelectWidget(), choices=MIGRATION_POLICY_CHOICES, initial='never', required=False) def __init__(self, request, *args, **kwargs): super().__init__(request, *args, **kwargs) try: volume_types = cinder.volume_type_list(request) except Exception: redirect_url = reverse("horizon:project:volumes:index") error_message = _('Unable to retrieve the volume type list.') exceptions.handle(request, error_message, redirect=redirect_url) origin_type = self.initial['volume_type'] type_list = [(t.name, _("%s (current)") % t.name if origin_type == t.name else t.name) for t in volume_types] if not type_list: type_list.insert(0, ("", _("No other volume types available"))) self.fields['volume_type'].choices = sorted(type_list) def clean_volume_type(self): volume_type = self.cleaned_data.get("volume_type") if self.initial['volume_type'] == volume_type: msg = _('The new type must be different from the ' 'current volume type.') raise forms.ValidationError(msg) return volume_type def handle(self, request, data): volume_id = self.initial['id'] try: cinder.volume_retype(request, volume_id, data['volume_type'], data['migration_policy']) message = _( 'Successfully sent the request to change the volume ' 'type to "%(vtype)s" for volume: "%(name)s"') params = {'name': data['name'], 'vtype': data['volume_type']} messages.info(request, message % params) return True except Exception: redirect = reverse("horizon:project:volumes:index") error_message = _( 'Unable to change the volume type for volume: "%s"') \ % data['name'] exceptions.handle(request, error_message, redirect=redirect)
class CreateNetwork(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Name"), required=False) tenant_id = forms.ThemableChoiceField(label=_("Project")) if api.neutron.is_port_profiles_supported(): widget = None else: widget = forms.HiddenInput() net_profile_id = forms.ChoiceField(label=_("Network Profile"), required=False, widget=widget) network_type = forms.ChoiceField( label=_("Provider Network Type"), help_text=_("The physical mechanism by which the virtual " "network is implemented."), widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'network_type' })) physical_network = forms.CharField( max_length=255, label=_("Physical Network"), help_text=_("The name of the physical network over which the " "virtual network is implemented."), initial='default', widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'network_type', })) segmentation_id = forms.IntegerField( label=_("Segmentation ID"), widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'network_type', })) admin_state = forms.BooleanField(label=_("Enable Admin State"), initial=True, required=False) shared = forms.BooleanField(label=_("Shared"), initial=False, required=False) external = forms.BooleanField(label=_("External Network"), initial=False, required=False) @classmethod def _instantiate(cls, request, *args, **kwargs): return cls(request, *args, **kwargs) def __init__(self, request, *args, **kwargs): super(CreateNetwork, self).__init__(request, *args, **kwargs) tenant_choices = [('', _("Select a project"))] tenants, has_more = api.keystone.tenant_list(request) for tenant in tenants: if tenant.enabled: tenant_choices.append((tenant.id, tenant.name)) self.fields['tenant_id'].choices = tenant_choices if api.neutron.is_port_profiles_supported(): self.fields['net_profile_id'].choices = ( self.get_network_profile_choices(request)) try: is_extension_supported = \ api.neutron.is_extension_supported(request, 'provider') except Exception: msg = _("Unable to verify Neutron service providers") exceptions.handle(self.request, msg) self._hide_provider_network_type() is_extension_supported = False if is_extension_supported: neutron_settings = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) self.seg_id_range = SEGMENTATION_ID_RANGE.copy() seg_id_range = neutron_settings.get('segmentation_id_range') if seg_id_range: self.seg_id_range.update(seg_id_range) self.provider_types = PROVIDER_TYPES.copy() extra_provider_types = neutron_settings.get('extra_provider_types') if extra_provider_types: self.provider_types.update(extra_provider_types) self.nettypes_with_seg_id = [ net_type for net_type in self.provider_types if self.provider_types[net_type]['require_segmentation_id'] ] self.nettypes_with_physnet = [ net_type for net_type in self.provider_types if self.provider_types[net_type]['require_physical_network'] ] supported_provider_types = neutron_settings.get( 'supported_provider_types', DEFAULT_PROVIDER_TYPES) if supported_provider_types == ['*']: supported_provider_types = DEFAULT_PROVIDER_TYPES undefined_provider_types = [ net_type for net_type in supported_provider_types if net_type not in self.provider_types ] if undefined_provider_types: LOG.error('Undefined provider network types are found: %s', undefined_provider_types) seg_id_help = [ _("For %(type)s networks, valid IDs are %(min)s to %(max)s.") % { 'type': net_type, 'min': self.seg_id_range[net_type][0], 'max': self.seg_id_range[net_type][1] } for net_type in self.nettypes_with_seg_id ] self.fields['segmentation_id'].help_text = ' '.join(seg_id_help) # Register network types which require segmentation ID attrs = dict( ('data-network_type-%s' % network_type, _('Segmentation ID')) for network_type in self.nettypes_with_seg_id) self.fields['segmentation_id'].widget.attrs.update(attrs) # Register network types which require physical network attrs = dict( ('data-network_type-%s' % network_type, _('Physical Network')) for network_type in self.nettypes_with_physnet) self.fields['physical_network'].widget.attrs.update(attrs) network_type_choices = [ (net_type, self.provider_types[net_type]['display_name']) for net_type in supported_provider_types ] if len(network_type_choices) == 0: self._hide_provider_network_type() else: self.fields['network_type'].choices = network_type_choices def get_network_profile_choices(self, request): profile_choices = [('', _("Select a profile"))] for profile in self._get_profiles(request, 'network'): profile_choices.append((profile.id, profile.name)) return profile_choices def _get_profiles(self, request, type_p): profiles = [] try: profiles = api.neutron.profile_list(request, type_p) except Exception: msg = _('Network Profiles could not be retrieved.') exceptions.handle(request, msg) return profiles def _hide_provider_network_type(self): self.fields['network_type'].widget = forms.HiddenInput() self.fields['physical_network'].widget = forms.HiddenInput() self.fields['segmentation_id'].widget = forms.HiddenInput() self.fields['network_type'].required = False self.fields['physical_network'].required = False self.fields['segmentation_id'].required = False def handle(self, request, data): try: params = { 'name': data['name'], 'tenant_id': data['tenant_id'], 'admin_state_up': data['admin_state'], 'shared': data['shared'], 'router:external': data['external'] } if api.neutron.is_port_profiles_supported(): params['net_profile_id'] = data['net_profile_id'] if api.neutron.is_extension_supported(request, 'provider'): network_type = data['network_type'] params['provider:network_type'] = network_type if network_type in self.nettypes_with_physnet: params['provider:physical_network'] = ( data['physical_network']) if network_type in self.nettypes_with_seg_id: params['provider:segmentation_id'] = ( data['segmentation_id']) network = api.neutron.network_create(request, **params) msg = _('Network %s was successfully created.') % data['name'] LOG.debug(msg) messages.success(request, msg) return network except Exception: redirect = reverse('horizon:network:networks:index') msg = _('Failed to create network %s') % data['name'] exceptions.handle(request, msg, redirect=redirect) def clean(self): cleaned_data = super(CreateNetwork, self).clean() if api.neutron.is_extension_supported(self.request, 'provider'): self._clean_physical_network(cleaned_data) self._clean_segmentation_id(cleaned_data) return cleaned_data def _clean_physical_network(self, data): network_type = data.get('network_type') if ('physical_network' in self._errors and network_type not in self.nettypes_with_physnet): # In this case the physical network is not required, so we can # ignore any errors. del self._errors['physical_network'] def _clean_segmentation_id(self, data): network_type = data.get('network_type') if 'segmentation_id' in self._errors: if (network_type not in self.nettypes_with_seg_id and not self.data.get("segmentation_id")): # In this case the segmentation ID is not required, so we can # ignore the field is required error. del self._errors['segmentation_id'] elif network_type in self.nettypes_with_seg_id: seg_id = data.get('segmentation_id') seg_id_range = { 'min': self.seg_id_range[network_type][0], 'max': self.seg_id_range[network_type][1] } if seg_id < seg_id_range['min'] or seg_id > seg_id_range['max']: msg = (_('For a %(network_type)s network, valid segmentation ' 'IDs are %(min)s through %(max)s.') % { 'network_type': network_type, 'min': seg_id_range['min'], 'max': seg_id_range['max'] }) self._errors['segmentation_id'] = self.error_class([msg])
def __init__(self, request, *args, **kwargs): super(CreateNetwork, self).__init__(request, *args, **kwargs) tenant_choices = [('', _("Select a project"))] tenants, has_more = api.keystone.tenant_list(request) for tenant in tenants: if tenant.enabled: tenant_choices.append((tenant.id, tenant.name)) self.fields['tenant_id'].choices = tenant_choices try: is_extension_supported = \ api.neutron.is_extension_supported(request, 'provider') except Exception: msg = _("Unable to verify Neutron service providers") exceptions.handle(self.request, msg) self._hide_provider_network_type() is_extension_supported = False if is_extension_supported: neutron_settings = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) self.seg_id_range = SEGMENTATION_ID_RANGE.copy() seg_id_range = neutron_settings.get('segmentation_id_range') if seg_id_range: self.seg_id_range.update(seg_id_range) self.provider_types = PROVIDER_TYPES.copy() extra_provider_types = neutron_settings.get('extra_provider_types') if extra_provider_types: self.provider_types.update(extra_provider_types) self.nettypes_with_seg_id = [ net_type for net_type in self.provider_types if self.provider_types[net_type]['require_segmentation_id'] ] self.nettypes_with_physnet = [ net_type for net_type in self.provider_types if self.provider_types[net_type]['require_physical_network'] ] supported_provider_types = neutron_settings.get( 'supported_provider_types', DEFAULT_PROVIDER_TYPES) if supported_provider_types == ['*']: supported_provider_types = DEFAULT_PROVIDER_TYPES undefined_provider_types = [ net_type for net_type in supported_provider_types if net_type not in self.provider_types ] if undefined_provider_types: LOG.error('Undefined provider network types are found: %s', undefined_provider_types) seg_id_help = [ _("For %(type)s networks, valid IDs are %(min)s to %(max)s.") % { 'type': net_type, 'min': self.seg_id_range[net_type][0], 'max': self.seg_id_range[net_type][1] } for net_type in self.nettypes_with_seg_id ] self.fields['segmentation_id'].help_text = ' '.join(seg_id_help) # Register network types which require segmentation ID attrs = dict( ('data-network_type-%s' % network_type, _('Segmentation ID')) for network_type in self.nettypes_with_seg_id) self.fields['segmentation_id'].widget.attrs.update(attrs) physical_networks = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}).get('physical_networks', []) if physical_networks: self.fields['physical_network'] = forms.ThemableChoiceField( label=_("Physical Network"), choices=[(net, net) for net in physical_networks], widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'network_type', }), help_text=_("The name of the physical network over " "which the virtual network is implemented."), ) # Register network types which require physical network attrs = dict( ('data-network_type-%s' % network_type, _('Physical Network')) for network_type in self.nettypes_with_physnet) self.fields['physical_network'].widget.attrs.update(attrs) network_type_choices = [ (net_type, self.provider_types[net_type]['display_name']) for net_type in supported_provider_types ] if len(network_type_choices) == 0: self._hide_provider_network_type() else: self.fields['network_type'].choices = network_type_choices
class AddVipAction(workflows.Action): name = forms.CharField(max_length=80, label=_("Name")) description = forms.CharField(initial="", required=False, max_length=80, label=_("Description")) subnet_id = forms.ThemableChoiceField(label=_("VIP Subnet"), initial="", required=False) address = forms.IPField(label=_("IP address"), version=forms.IPv4 | forms.IPv6, mask=False, required=False) protocol_port = forms.IntegerField( label=_("Protocol Port"), min_value=1, help_text=_("Enter an integer value " "between 1 and 65535."), validators=[validators.validate_port_range]) protocol = forms.ThemableChoiceField(label=_("Protocol")) session_persistence = forms.ChoiceField( required=False, initial={}, label=_("Session Persistence"), widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'persistence' })) cookie_name = forms.CharField( initial="", required=False, max_length=80, label=_("Cookie Name"), help_text=_("Required for APP_COOKIE persistence;" " Ignored otherwise."), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'persistence', 'data-persistence-app_cookie': 'APP_COOKIE', })) connection_limit = forms.IntegerField( required=False, min_value=-1, label=_("Connection Limit"), help_text=_("Maximum number of connections allowed " "for the VIP or '-1' if the limit is not set")) admin_state_up = forms.ThemableChoiceField(choices=[(True, _('UP')), (False, _('DOWN'))], label=_("Admin State")) def __init__(self, request, *args, **kwargs): super(AddVipAction, self).__init__(request, *args, **kwargs) tenant_id = request.user.tenant_id subnet_id_choices = [('', _("Select a Subnet"))] try: networks = api.neutron.network_list_for_tenant(request, tenant_id) except Exception: exceptions.handle(request, _('Unable to retrieve networks list.')) networks = [] for n in networks: for s in n['subnets']: name = "%s (%s)" % (s.name, s.cidr) subnet_id_choices.append((s.id, name)) self.fields['subnet_id'].choices = subnet_id_choices protocol_choices = [('', _("Select a Protocol"))] [protocol_choices.append((p, p)) for p in AVAILABLE_PROTOCOLS] self.fields['protocol'].choices = protocol_choices session_persistence_choices = [('', _("No Session Persistence"))] for mode in ('SOURCE_IP', 'HTTP_COOKIE', 'APP_COOKIE'): session_persistence_choices.append((mode.lower(), mode)) self.fields[ 'session_persistence'].choices = session_persistence_choices def clean(self): cleaned_data = super(AddVipAction, self).clean() persistence = cleaned_data.get('session_persistence') if persistence: cleaned_data['session_persistence'] = persistence.upper() if (cleaned_data.get('session_persistence') == 'APP_COOKIE' and not cleaned_data.get('cookie_name')): msg = _('Cookie name is required for APP_COOKIE persistence.') self._errors['cookie_name'] = self.error_class([msg]) return cleaned_data class Meta(object): name = _("Specify VIP") permissions = ('openstack.services.network', ) help_text_template = 'project/loadbalancers/_create_vip_help.html'
class AddGroupInfoAction(workflows.Action): name = forms.CharField(label=_("Name"), max_length=255) description = forms.CharField( widget=forms.widgets.Textarea(attrs={'rows': 4}), label=_("Description"), required=False) group_type = forms.ChoiceField(label=_("Group Type"), widget=forms.ThemableSelectWidget()) availability_zone = forms.ChoiceField( label=_("Availability Zone"), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-no_source_type': _('Availability Zone'), 'data-source-image_source': _('Availability Zone') })) def __init__(self, request, *args, **kwargs): super(AddGroupInfoAction, self).__init__(request, *args, **kwargs) self.fields['availability_zone'].choices = \ availability_zones(request) try: # Group type name 'default_cgsnapshot_type' is reserved for # consistency group and it cannot be used for a group type. # Let's exclude it. group_types = [(t.id, t.name) for t in api.cinder.group_type_list(request) if t.name != 'default_cgsnapshot_type'] except Exception: exceptions.handle(request, _('Unable to retrieve group types.')) if group_types: group_types.insert(0, ("", _("Select group type"))) else: group_types.insert(0, ("", _("No valid group type"))) self.fields['group_type'].choices = group_types class Meta(object): name = _("Group Information") help_text = _("Volume groups provide a mechanism for " "creating snapshots of multiple volumes at the same " "point-in-time to ensure data consistency\n\n" "A volume group can support more than one volume " "type, but it can only contain volumes hosted by the " "same back end.") slug = "set_group_info" def clean(self): cleaned_data = super(AddGroupInfoAction, self).clean() name = cleaned_data.get('name') try: groups = cinder.group_list(self.request) except Exception: msg = _('Unable to get group list') exceptions.check_message(["Connection", "refused"], msg) raise if groups is not None and name is not None: for group in groups: if group.name.lower() == name.lower(): # ensure new name has reasonable length formatted_name = name if len(name) > 20: formatted_name = name[:14] + "..." + name[-3:] raise forms.ValidationError( _('The name "%s" is already used by ' 'another group.') % formatted_name) return cleaned_data
class AddRuleAction(workflows.Action): name = forms.CharField(max_length=80, label=_("Name"), required=False) description = forms.CharField(max_length=80, label=_("Description"), required=False) protocol = forms.ThemableChoiceField( label=_("Protocol"), choices=[('tcp', _('TCP')), ('udp', _('UDP')), ('icmp', _('ICMP')), ('any', _('ANY'))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'protocol', })) action = forms.ThemableChoiceField( label=_("Action"), choices=[('allow', _('ALLOW')), ('deny', _('DENY')), ('reject', _('REJECT'))], ) source_ip_address = forms.IPField(label=_("Source IP Address/Subnet"), version=forms.IPv4 | forms.IPv6, required=False, mask=True) destination_ip_address = forms.IPField( label=_("Destination IP Address/Subnet"), version=forms.IPv4 | forms.IPv6, required=False, mask=True) source_port = forms.CharField( max_length=80, label=_("Source Port/Port Range"), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'protocol', 'data-protocol-tcp': _("Source Port/Port Range"), 'data-protocol-udp': _("Source Port/Port Range"), }), required=False, validators=[port_validator]) destination_port = forms.CharField( max_length=80, label=_("Destination Port/Port Range"), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'protocol', 'data-protocol-tcp': _("Destination Port/Port Range"), 'data-protocol-udp': _("Destination Port/Port Range"), }), required=False, validators=[port_validator]) ip_version = forms.ThemableChoiceField(label=_("IP Version"), required=False, choices=[('4', '4'), ('6', '6')]) shared = forms.BooleanField(label=_("Shared"), initial=False, required=False) enabled = forms.BooleanField(label=_("Enabled"), initial=True, required=False) def __init__(self, request, *args, **kwargs): super(AddRuleAction, self).__init__(request, *args, **kwargs) # Only admin user can update the 'shared' attribute self.ignore_shared = False if not policy.check( (("neutron-fwaas", "create_firewall_rule:shared"), ), request): self.fields['shared'].widget = forms.CheckboxInput( attrs={ 'readonly': 'readonly', 'disabled': 'disabled' }) self.fields['shared'].help_text = _( 'Non admin users are not allowed to set the shared property ' 'of the rule.') self.ignore_shared = True def _check_ip_addr_and_ip_version(self, cleaned_data): ip_version = int(str(cleaned_data.get('ip_version'))) src_ip = cleaned_data.get('source_ip_address') dst_ip = cleaned_data.get('destination_ip_address') msg = _('Source/Destination Network Address and IP version ' 'are inconsistent. Please make them consistent.') if (src_ip and netaddr.IPNetwork(src_ip).version != ip_version): self._errors['ip_version'] = self.error_class([msg]) elif (dst_ip and netaddr.IPNetwork(dst_ip).version != ip_version): self._errors['ip_version'] = self.error_class([msg]) def clean(self): cleaned_data = super(AddRuleAction, self).clean() self._check_ip_addr_and_ip_version(cleaned_data) class Meta(object): name = _("Rule") permissions = ('openstack.services.network', ) help_text = _("Create a firewall rule.\n\n" "A firewall rule is an association of the following " "attributes:\n\n" "<li>IP Addresses: The addresses from/to which the " "traffic filtration needs to be applied.</li>" "<li>IP Version: The type of IP packets (IP V4/V6) " "that needs to be filtered.</li>" "<li>Protocol: Type of packets (UDP, ICMP, TCP, Any) " "that needs to be checked.</li>" "<li>Action: Action is the type of filtration " "required, it can be Reject/Deny/Allow data " "packets.</li>\n" "The protocol and action fields are required, all " "others are optional.")
class SetInstanceDetailsAction(workflows.Action): availability_zone = forms.ThemableChoiceField(label=_("Availability Zone"), required=False) name = forms.CharField(label=_("Instance Name"), max_length=255) flavor = forms.ThemableChoiceField(label=_("Flavor"), help_text=_("Size of image to launch.")) count = forms.IntegerField(label=_("Number of Instances"), min_value=1, initial=1) source_type = forms.ThemableChoiceField(label=_("Instance Boot Source"), initial='image_id', help_text=_( "Choose Your Boot Source " "Type."), disabled=True) image_id = forms.ChoiceField( label=_("Image Name"), required=False, widget=forms.ThemableSelectWidget(data_attrs=('volume_size', ), transform=lambda x: ("%s (%s)" % (x.name, filesizeformat(x.bytes))))) class Meta(object): name = _("Details") help_text_template = ("project/order/" "_launch_details_help.html") def __init__(self, request, context, *args, **kwargs): self._init_images_cache() self.request = request self.context = context super(SetInstanceDetailsAction, self).__init__(request, context, *args, **kwargs) source_type_choices = [ ('', _("Select source")), ("image_id", _("Boot from image")), ] self.fields['source_type'].choices = source_type_choices @memoized.memoized_method def _get_flavor(self, flavor_id): try: # We want to retrieve details for a given flavor, # however flavor_list uses a memoized decorator # so it is used instead of flavor_get to reduce the number # of API calls. flavors = instance_utils.flavor_list(self.request) flavor = [x for x in flavors if x.id == flavor_id][0] except IndexError: flavor = None return flavor @memoized.memoized_method def _get_image(self, image_id): try: # We want to retrieve details for a given image, # however get_available_images uses a cache of image list, # so it is used instead of image_get to reduce the number # of API calls. images = image_utils.get_available_images( self.request, self.context.get('project_id'), self._images_cache) image = [x for x in images if x.id == image_id][0] except IndexError: image = None return image def _check_quotas(self, cleaned_data): count = cleaned_data.get('count', 1) # Prevent launching more instances than the quota allows usages = quotas.tenant_quota_usages(self.request, targets=( 'instances', 'cores', 'ram', )) # available_count = usages['instances']['available'] # if available_count < count: # msg = (_('The requested instance(s) cannot be launched ' # 'as your quota will be exceeded: Available: ' # '%(avail)s, Requested: %(req)s.') # % {'avail': available_count, 'req': count}) # raise forms.ValidationError(msg) source_type = cleaned_data.get('source_type') if source_type in ('volume_image_id', 'volume_snapshot_id'): available_volume = usages['volumes']['available'] if available_volume < count: msg = (_('The requested instance cannot be launched. ' 'Requested volume exceeds quota: Available: ' '%(avail)s, Requested: %(req)s.') % { 'avail': available_volume, 'req': count }) raise forms.ValidationError(msg) flavor_id = cleaned_data.get('flavor') flavor = self._get_flavor(flavor_id) count_error = [] # Validate cores and ram. available_cores = usages['cores']['available'] if flavor and available_cores < count * flavor.vcpus: count_error.append( _("Cores(Available: %(avail)s, " "Requested: %(req)s)") % { 'avail': available_cores, 'req': count * flavor.vcpus }) available_ram = usages['ram']['available'] if flavor and available_ram < count * flavor.ram: count_error.append( _("RAM(Available: %(avail)s, " "Requested: %(req)s)") % { 'avail': available_ram, 'req': count * flavor.ram }) if count_error: value_str = ", ".join(count_error) msg = (_('The requested instance cannot be launched. ' 'The following requested resource(s) exceed ' 'quota(s): %s.') % value_str) if count == 1: self._errors['flavor'] = self.error_class([msg]) else: self._errors['count'] = self.error_class([msg]) def _check_flavor_for_image(self, cleaned_data): # Prevents trying to launch an image needing more resources. image_id = cleaned_data.get('image_id') image = self._get_image(image_id) flavor_id = cleaned_data.get('flavor') flavor = self._get_flavor(flavor_id) if not image or not flavor: return props_mapping = (("min_ram", "ram"), ("min_disk", "disk")) for iprop, fprop in props_mapping: if (getattr(image, iprop) > 0 and getattr(flavor, fprop) > 0 and getattr(image, iprop) > getattr(flavor, fprop)): msg = (_("The flavor '%(flavor)s' is too small " "for requested image.\n" "Minimum requirements: " "%(min_ram)s MB of RAM and " "%(min_disk)s GB of Root Disk.") % { 'flavor': flavor.name, 'min_ram': image.min_ram, 'min_disk': image.min_disk }) self._errors['image_id'] = self.error_class([msg]) break # Not necessary to continue the tests. def _check_source_image(self, cleaned_data): if not cleaned_data.get('image_id'): msg = _("You must select an image.") self._errors['image_id'] = self.error_class([msg]) else: self._check_flavor_for_image(cleaned_data) def _check_source(self, cleaned_data): # Validate our instance source. source_type = self.data.get('source_type', None) source_check_methods = { 'image_id': self._check_source_image, } check_method = source_check_methods.get(source_type) if check_method: check_method(cleaned_data) def clean(self): cleaned_data = super(SetInstanceDetailsAction, self).clean() self._check_quotas(cleaned_data) self._check_source(cleaned_data) return cleaned_data def populate_flavor_choices(self, request, context): return instance_utils.flavor_field_data(request, False) def populate_availability_zone_choices(self, request, context): try: zones = api.nova.availability_zone_list(request) except Exception: 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 not zone_list: zone_list.insert(0, ("", _("No availability zones found"))) elif len(zone_list) > 1: zone_list.insert(0, ("", _("Any Availability Zone"))) return zone_list def get_help_text(self, extra_context=None): extra = {} if extra_context is None else dict(extra_context) try: data = api.order.order_hypervisor(self.request, self.context['start_time'], self.context['stop_time']) total_restouce = data.get('total_resource', None) free_resource = data.get('free_resource', None) extra['usages'] = { 'disk': { 'available': free_resource.get('disk', None), 'used': total_restouce.get('disk', None) - free_resource.get('disk', None), 'quota': total_restouce.get('disk', None), }, 'cores': { 'available': free_resource.get('vcpus', None), 'used': total_restouce.get('vcpus', None) - free_resource.get('vcpus', None), 'quota': total_restouce.get('vcpus', None), }, 'ram': { 'available': free_resource.get('ram', None), 'used': total_restouce.get('ram', None) - free_resource.get('ram', None), 'quota': total_restouce.get('ram', None), } } extra['usages_json'] = json.dumps(extra['usages']) flavors = json.dumps( [f._info for f in instance_utils.flavor_list(self.request)]) extra['flavors'] = flavors images = image_utils.get_available_images( self.request, self.initial['project_id'], self._images_cache) if images is not None: attrs = [{ 'id': i.id, 'min_disk': getattr(i, 'min_disk', 0), 'min_ram': getattr(i, 'min_ram', 0), 'size': functions.bytes_to_gigabytes(i.size) } for i in images] extra['images'] = json.dumps(attrs) except Exception: exceptions.handle(self.request, _("Unable to retrieve quota information.")) return super(SetInstanceDetailsAction, self).get_help_text(extra) def _init_images_cache(self): if not hasattr(self, '_images_cache'): self._images_cache = {} def populate_image_id_choices(self, request, context): choices = [] images = image_utils.get_available_images(request, context.get('project_id'), self._images_cache) for image in images: if image_tables.get_image_type(image) != "snapshot": image.bytes = getattr(image, 'virtual_size', None) or image.size image.volume_size = max( image.min_disk, functions.bytes_to_gigabytes(image.bytes)) choices.append((image.id, image)) if context.get('image_id') == image.id and \ 'volume_size' not in context: context['volume_size'] = image.volume_size if choices: choices.sort(key=lambda c: c[1].name or '') choices.insert(0, ("", _("Select Image"))) else: choices.insert(0, ("", _("No images available"))) return choices
class AttachInterface(forms.SelfHandlingForm): instance_id = forms.CharField(widget=forms.HiddenInput()) specification_method = forms.ThemableChoiceField( label=_("The way to specify an interface"), initial=False, widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'specification_method', })) port = forms.ThemableChoiceField( label=_("Port"), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'specification_method', 'data-specification_method-port': _('Port'), })) network = forms.ThemableChoiceField( label=_("Network"), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'specification_method', 'data-specification_method-network': _('Network'), })) fixed_ip = forms.IPField( label=_("Fixed IP Address"), required=False, help_text=_("IP address for the new port"), version=forms.IPv4 | forms.IPv6, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'specification_method', 'data-specification_method-network': _('Fixed IP Address'), })) def __init__(self, request, *args, **kwargs): super(AttachInterface, self).__init__(request, *args, **kwargs) networks = instance_utils.network_field_data(request, include_empty_option=True, with_cidr=True) self.fields['network'].choices = networks choices = [('network', _("by Network (and IP address)"))] ports = instance_utils.port_field_data(request, with_network=True) if len(ports) > 0: self.fields['port'].choices = ports choices.append(('port', _("by Port"))) self.fields['specification_method'].choices = choices def clean_network(self): specification_method = self.cleaned_data.get('specification_method') network = self.cleaned_data.get('network') if specification_method == 'network' and not network: msg = _('This field is required.') self._errors['network'] = self.error_class([msg]) return network def handle(self, request, data): instance_id = data['instance_id'] try: net_id = port_id = fixed_ip = None if data['specification_method'] == 'port': port_id = data.get('port') else: net_id = data.get('network') if data.get('fixed_ip'): fixed_ip = data.get('fixed_ip') api.nova.interface_attach(request, instance_id, net_id=net_id, fixed_ip=fixed_ip, port_id=port_id) msg = _('Attaching interface for instance %s.') % instance_id messages.success(request, msg) except Exception: redirect = reverse('horizon:project:instances:index') exceptions.handle(request, _("Unable to attach interface."), redirect=redirect) return True
class ImportX509Certificate(forms.SelfHandlingForm): name = forms.RegexField(required=False, max_length=255, label=_("Certificate Name"), regex=NAME_REGEX, error_messages=ERROR_MESSAGES) source_type = forms.ChoiceField( label=_('Source'), required=False, choices=[('file', _('Import File')), ('raw', _('Direct Input'))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'source' })) cert_file = forms.FileField( label=_("Choose file"), widget=forms.FileInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-file': _('PEM Certificate File') }), required=False) direct_input = forms.CharField( label=_('PEM Certificate'), widget=forms.widgets.Textarea( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-raw': _('PEM Certificate') }), required=False) def clean(self): data = super(ImportX509Certificate, self).clean() # The cert can be missing based on particular upload # conditions. Code defensively for it here... cert_file = data.get('cert_file', None) cert_raw = data.get('direct_input', None) if cert_raw and cert_file: raise forms.ValidationError( _("Cannot specify both file and direct input.")) if not cert_raw and not cert_file: raise forms.ValidationError( _("No input was provided for the certificate value.")) try: if cert_file: cert_pem = self.files['cert_file'].read() else: cert_pem = str(data['direct_input']) cert_obj = load_pem_x509_certificate(cert_pem.encode('utf-8'), default_backend()) cert_der = cert_obj.public_bytes(Encoding.DER) except Exception as e: msg = _('There was a problem loading the certificate: %s. ' 'Is the certificate valid and in PEM format?') % e raise forms.ValidationError(msg) data['cert_data'] = base64.b64encode(cert_der).decode('utf-8') return data def handle(self, request, data): try: cert_pem = data.get('cert_data') cert_uuid = client.import_object(request, data=cert_pem, name=data['name'], object_type=x_509.X509) if data['name']: identifier = data['name'] else: identifier = cert_uuid messages.success( request, _('Successfully imported certificate: %s') % identifier) return cert_uuid except Exception as e: msg = _('Unable to import certificate: %s') messages.error(request, msg % e) exceptions.handle(request, ignore=True) self.api_error(_('Unable to import certificate.')) return False
class CreatePortInfoAction(workflows.Action): name = forms.CharField(max_length=255, label=_("Name"), required=False) admin_state = forms.BooleanField(label=_("Enable Admin State"), initial=True, required=False) device_id = forms.CharField(max_length=100, label=_("Device ID"), help_text=_("Device ID attached to the port"), required=False) device_owner = forms.CharField( max_length=100, label=_("Device Owner"), help_text=_("Owner of the device attached to the port"), required=False) specify_ip = forms.ThemableChoiceField( label=_("Specify IP address or subnet"), help_text=_("To specify a subnet or a fixed IP, select any options."), required=False, choices=[('', _("Unspecified")), ('subnet_id', _("Subnet")), ('fixed_ip', _("Fixed IP Address"))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'specify_ip', })) subnet_id = forms.ThemableChoiceField( label=_("Subnet"), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-required-when-shown': 'true', 'data-switch-on': 'specify_ip', 'data-specify_ip-subnet_id': _('Subnet'), })) fixed_ip = forms.IPField( label=_("Fixed IP Address"), required=False, help_text=_("Specify the subnet IP address for the new port"), version=forms.IPv4 | forms.IPv6, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-required-when-shown': 'true', 'data-switch-on': 'specify_ip', 'data-specify_ip-fixed_ip': _('Fixed IP Address'), })) mac_address = forms.MACAddressField( label=_("MAC Address"), required=False, help_text=_("Specify the MAC address for the new port")) mac_state = forms.BooleanField(label=_("MAC Learning State"), initial=False, required=False) port_security_enabled = forms.BooleanField( label=_("Port Security"), help_text=_("Enable anti-spoofing rules for the port"), initial=True, required=False, widget=forms.CheckboxInput( attrs={ 'class': 'switchable', 'data-slug': 'port_security_enabled', 'data-hide-tab': 'create_port__create_security_groups', 'data-hide-on-checked': 'false' })) binding__vnic_type = forms.ThemableChoiceField( label=_("VNIC Type"), help_text=_("The VNIC type that is bound to the network port"), required=False) def __init__(self, request, context, *args, **kwargs): super(CreatePortInfoAction, self).__init__(request, context, *args, **kwargs) # prepare subnet choices and input area for each subnet subnet_choices = self._get_subnet_choices(context) if subnet_choices: subnet_choices.insert(0, ('', _("Select a subnet"))) self.fields['subnet_id'].choices = subnet_choices else: self.fields['specify_ip'].widget = forms.HiddenInput() self.fields['subnet_id'].widget = forms.HiddenInput() self.fields['fixed_ip'].widget = forms.HiddenInput() self._hide_field_if_not_supported( request, 'mac_state', 'mac-learning', _("Unable to retrieve MAC learning state")) self._hide_field_if_not_supported( request, 'port_security_enabled', 'port-security', _("Unable to retrieve port security state")) self._populate_vnic_type_choices(request) def _hide_field_if_not_supported(self, request, field, extension_alias, failure_message): is_supproted = False try: is_supproted = api.neutron.is_extension_supported( request, extension_alias) except Exception: exceptions.handle(self.request, failure_message) if not is_supproted: del self.fields[field] return is_supproted def _populate_vnic_type_choices(self, request): neutron_settings = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) supported_vnic_types = neutron_settings.get('supported_vnic_types', ['*']) # When a list of VNIC types is empty, hide the corresponding field. if not supported_vnic_types: del self.fields['binding__vnic_type'] return binding_supported = self._hide_field_if_not_supported( request, 'binding__vnic_type', 'binding', _("Unable to verify the VNIC types extension in Neutron")) if not binding_supported: # binding__vnic_type field is already deleted, so return here return if supported_vnic_types == ['*']: vnic_type_choices = api.neutron.VNIC_TYPES else: vnic_type_choices = [ vnic_type for vnic_type in api.neutron.VNIC_TYPES if vnic_type[0] in supported_vnic_types ] self.fields['binding__vnic_type'].choices = vnic_type_choices def _get_subnet_choices(self, context): try: network_id = context['network_id'] network = api.neutron.network_get(self.request, network_id) except Exception: return [] # NOTE(amotoki): When a user cannot retrieve a subnet info, # subnet ID is stored in network.subnets field. # If so, we skip such subnet as subnet choices. # This happens usually for external networks. # TODO(amotoki): Ideally it is better to disable/hide # Create Port button in the port table, but as of Pike # the default neutron policy.json for "create_port" is empty # and there seems no appropriate policy. This is a dirty hack. return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr)) for subnet in network.subnets if isinstance(subnet, api.neutron.Subnet)] class Meta(object): name = _("Info") slug = 'create_info' help_text_template = 'project/networks/ports/_create_port_help.html'
class UpdateIPSecPolicy(forms.SelfHandlingForm): name = forms.CharField(max_length=80, label=_("Name"), required=False) ipsecpolicy_id = forms.CharField( label=_("ID"), widget=forms.TextInput(attrs={'readonly': 'readonly'})) description = forms.CharField(required=False, max_length=80, label=_("Description")) # Currently this field has only one choice, so mark it as readonly. auth_algorithm = forms.ChoiceField( label=_("Authorization algorithm"), choices=[('sha1', _('sha1'))], widget=forms.TextInput(attrs={'readonly': 'readonly'}), required=False) encapsulation_mode = forms.ChoiceField(label=_("Encapsulation mode"), choices=[('tunnel', _('tunnel')), ('transport', _('transport'))], required=False) encryption_algorithm = forms.ChoiceField(label=_("Encryption algorithm"), choices=[ ('3des', _('3des')), ('aes-128', _('aes-128')), ('aes-192', _('aes-192')), ('aes-256', _('aes-256')) ], required=False) # Currently this field has only one choice, so mark it as readonly. lifetime_units = forms.ChoiceField( label=_("Lifetime units"), choices=[('seconds', _('seconds'))], widget=forms.ThemableSelectWidget(attrs={'readonly': 'readonly'}), required=False) lifetime_value = forms.IntegerField( min_value=60, label=_("Lifetime value"), help_text=_("Equal to or greater than 60"), required=False) pfs = forms.ChoiceField(label=_("Perfect Forward Secrecy"), choices=[('group2', _('group2')), ('group5', _('group5')), ('group14', _('group14'))], required=False) transform_protocol = forms.ChoiceField(label=_("Transform Protocol"), choices=[('esp', _('esp')), ('ah', _('ah')), ('ah-esp', _('ah-esp'))], required=False) failure_url = 'horizon:project:vpn:index' def handle(self, request, context): try: data = { 'ipsecpolicy': { 'name': context['name'], 'description': context['description'], 'auth_algorithm': context['auth_algorithm'], 'encapsulation_mode': context['encapsulation_mode'], 'encryption_algorithm': context['encryption_algorithm'], 'lifetime': { 'units': context['lifetime_units'], 'value': context['lifetime_value'] }, 'pfs': context['pfs'], 'transform_protocol': context['transform_protocol'], } } ipsecpolicy = api.vpn.ipsecpolicy_update(request, context['ipsecpolicy_id'], **data) msg = (_('IPSec Policy %s was successfully updated.') % context['name']) LOG.debug(msg) messages.success(request, msg) return ipsecpolicy except Exception as e: msg = _('Failed to update IPSec Policy %s') % context['name'] LOG.info('%s: %s' % (msg, e)) redirect = reverse(self.failure_url) exceptions.handle(request, msg, redirect=redirect)
class SetResumeDetailAction(workflows.Action): image_id = forms.ChoiceField( label=_("Image Name"), required=True, widget=forms.ThemableSelectWidget(data_attrs=('volume_size', ), transform=lambda x: ("%s (%s)" % (x.name, filesizeformat(x.bytes))))) name = forms.CharField(max_length=80, label=_("Instance Name"), initial="resumed_vm") flavor = forms.ChoiceField(label=_("Flavor"), required=True, help_text=_("Size of image to launch.")) class Meta: name = _("Base VM Info") help_text_template = ("project/cloudlet/instance/" "_resume_details_help.html") def clean(self): cleaned_data = super(SetResumeDetailAction, self).clean() return cleaned_data def _get_available_images(self, request, context): if not hasattr(self, '_images_cache'): images_cache = {} if images_cache is None: images_cache = {} public_images = images_cache.get('public_images', []) images_by_project = images_cache.get('images_by_project', {}) if 'public_images' not in images_cache: public = {"is_public": True, "status": "active"} try: images, _more, _prev = glance.image_list_detailed( request, filters=public) [public_images.append(image) for image in images] images_cache['public_images'] = public_images except Exception: exceptions.handle(request, _("Unable to retrieve public images.")) # Preempt if we don't have a project_id yet. project_id = context.get('project_id', None) if project_id is None: images_by_project[project_id] = [] if project_id not in images_by_project: owner = {"property-owner_id": project_id, "status": "active"} try: owned_images, _more, _prev = glance.image_list_detailed( request, filters=owner) images_by_project[project_id] = owned_images except Exception: owned_images = [] exceptions.handle( request, _("Unable to retrieve images for " "the current project.")) else: owned_images = images_by_project[project_id] if 'images_by_project' not in images_cache: images_cache['images_by_project'] = images_by_project images = owned_images + public_images base_vms = list() for image in images: if hasattr(image, 'properties') == True: properties = getattr(image, 'properties') cloudlet_type = properties.get('cloudlet_type', None) if cloudlet_type == 'cloudlet_base_disk': base_vms.append(image) image_ids = [] final_images = [] for image in base_vms: if image.id not in image_ids: image_ids.append(image.id) final_images.append(image) return [ image for image in final_images if image.container_format not in ('aki', 'ari') ] def populate_image_id_choices(self, request, context): images = self._get_available_images(request, context) choices = [(image.id, image.name) for image in images if image.properties.get("image_type", '') == "snapshot"] if choices: choices.insert(0, ("", _("Select Base VM"))) else: choices.insert(0, ("", _("No Base VM is available."))) return choices def populate_flavor_choices(self, request, context): # return all flavors of Base VM image try: matching_flavors = set() flavors = api.nova.flavor_list(request) basevm_images = self._get_available_images(request, context) for basevm_image in basevm_images: if basevm_image.properties is None or \ len(basevm_image.properties) == 0: continue libvirt_xml_str = basevm_image.properties.get( 'base_resource_xml_str', None) if libvirt_xml_str is None: continue qemu_mem = utils.QemuMemory() cpu_count, memory_mb = qemu_mem.get_resource_size( libvirt_xml_str) disk_gb = basevm_image.min_disk ret_flavors = utils.find_matching_flavor( flavors, cpu_count, memory_mb, disk_gb) matching_flavors.update(ret_flavors) if len(matching_flavors) > 0: self.fields['flavor'].initial = list(matching_flavors)[0] else: self.fields['flavor'].initial = (0, "No valid flavor") except Exception as e: matching_flavors = set() exceptions.handle(request, _('Unable to retrieve instance flavors.')) return sorted(list(matching_flavors)) def get_help_text(self): extra = {} try: extra['usages'] = quotas.tenant_quota_usages(self.request) extra['usages_json'] = json.dumps(extra['usages']) flavors = json.dumps( [f._info for f in api.nova.flavor_list(self.request)]) extra['flavors'] = flavors except Exception: exceptions.handle(self.request, _("Unable to retrieve quota information.")) return super(SetResumeDetailAction, self).get_help_text(extra)
class RebuildInstanceForm(forms.SelfHandlingForm): instance_id = forms.CharField(widget=forms.HiddenInput()) image = forms.ChoiceField(label=_("Select Image"), widget=forms.ThemableSelectWidget( attrs={'class': 'image-selector'}, data_attrs=('size', 'display-name'), transform=_image_choice_title)) password = forms.RegexField( label=_("Rebuild Password"), required=False, widget=forms.PasswordInput(render_value=False), regex=validators.password_validator(), error_messages={'invalid': validators.password_validator_msg()}) confirm_password = forms.CharField( label=_("Confirm Rebuild Password"), required=False, widget=forms.PasswordInput(render_value=False)) disk_config = forms.ThemableChoiceField(label=_("Disk Partition"), required=False) def __init__(self, request, *args, **kwargs): super(RebuildInstanceForm, self).__init__(request, *args, **kwargs) instance_id = kwargs.get('initial', {}).get('instance_id') self.fields['instance_id'].initial = instance_id images = image_utils.get_available_images(request, request.user.tenant_id) choices = [(image.id, image) for image in images] if choices: choices.insert(0, ("", _("Select Image"))) else: choices.insert(0, ("", _("No images available"))) self.fields['image'].choices = choices if not api.nova.can_set_server_password(): del self.fields['password'] del self.fields['confirm_password'] try: if not api.nova.extension_supported("DiskConfig", request): del self.fields['disk_config'] else: # Set our disk_config choices config_choices = [("AUTO", _("Automatic")), ("MANUAL", _("Manual"))] self.fields['disk_config'].choices = config_choices except Exception: exceptions.handle( request, _('Unable to retrieve extensions ' 'information.')) def clean(self): cleaned_data = super(RebuildInstanceForm, self).clean() if 'password' in cleaned_data: passwd = cleaned_data.get('password') confirm = cleaned_data.get('confirm_password') if passwd is not None and confirm is not None: if passwd != confirm: raise forms.ValidationError(_("Passwords do not match.")) return cleaned_data # We have to protect the entire "data" dict because it contains the # password and confirm_password strings. @sensitive_variables('data', 'password') def handle(self, request, data): instance = data.get('instance_id') image = data.get('image') password = data.get('password') or None disk_config = data.get('disk_config', None) try: api.nova.server_rebuild(request, instance, image, password, disk_config) messages.info(request, _('Rebuilding instance %s.') % instance) except Exception: redirect = reverse('horizon:project:instances:index') exceptions.handle(request, _("Unable to rebuild instance."), redirect=redirect) return True
class CreateNetwork(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Name"), required=False) tenant_id = forms.ThemableChoiceField(label=_("Project")) network_type = forms.ChoiceField( label=_("Provider Network Type"), help_text=_("The physical mechanism by which the virtual " "network is implemented."), widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'network_type' })) physical_network = forms.CharField( max_length=255, label=_("Physical Network"), help_text=_("The name of the physical network over which the " "virtual network is implemented. Specify one of the " "physical networks defined in your neutron deployment."), widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'network_type', })) segmentation_id = forms.IntegerField( label=_("Segmentation ID"), widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'network_type', })) admin_state = forms.BooleanField(label=_("Enable Admin State"), initial=True, required=False) shared = forms.BooleanField(label=_("Shared"), initial=False, required=False) external = forms.BooleanField(label=_("External Network"), initial=False, required=False) with_subnet = forms.BooleanField(label=_("Create Subnet"), widget=forms.CheckboxInput( attrs={ 'class': 'switchable', 'data-slug': 'with_subnet', 'data-hide-tab': 'create_network__' 'createsubnetinfo' 'action,' 'create_network__' 'createsubnetdetail' 'action', 'data-hide-on-checked': 'false' }), initial=True, required=False) az_hints = forms.MultipleChoiceField( label=_("Availability Zone Hints"), required=False, help_text=_("Availability zones where the DHCP agents may be " "scheduled. Leaving this unset is equivalent to " "selecting all availability zones")) @classmethod def _instantiate(cls, request, *args, **kwargs): return cls(request, *args, **kwargs) def __init__(self, request, *args, **kwargs): super(CreateNetwork, self).__init__(request, *args, **kwargs) tenant_choices = [('', _("Select a project"))] tenants, has_more = api.keystone.tenant_list(request) for tenant in tenants: if tenant.enabled: tenant_choices.append((tenant.id, tenant.name)) self.fields['tenant_id'].choices = tenant_choices try: is_extension_supported = \ api.neutron.is_extension_supported(request, 'provider') except Exception: msg = _("Unable to verify Neutron service providers") exceptions.handle(self.request, msg) self._hide_provider_network_type() is_extension_supported = False if is_extension_supported: neutron_settings = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) self.seg_id_range = SEGMENTATION_ID_RANGE.copy() seg_id_range = neutron_settings.get('segmentation_id_range') if seg_id_range: self.seg_id_range.update(seg_id_range) self.provider_types = PROVIDER_TYPES.copy() extra_provider_types = neutron_settings.get('extra_provider_types') if extra_provider_types: self.provider_types.update(extra_provider_types) self.nettypes_with_seg_id = [ net_type for net_type in self.provider_types if self.provider_types[net_type]['require_segmentation_id'] ] self.nettypes_with_physnet = [ net_type for net_type in self.provider_types if self.provider_types[net_type]['require_physical_network'] ] supported_provider_types = neutron_settings.get( 'supported_provider_types', DEFAULT_PROVIDER_TYPES) if supported_provider_types == ['*']: supported_provider_types = DEFAULT_PROVIDER_TYPES undefined_provider_types = [ net_type for net_type in supported_provider_types if net_type not in self.provider_types ] if undefined_provider_types: LOG.error('Undefined provider network types are found: %s', undefined_provider_types) seg_id_help = [ _("For %(type)s networks, valid IDs are %(min)s to %(max)s.") % { 'type': net_type, 'min': self.seg_id_range[net_type][0], 'max': self.seg_id_range[net_type][1] } for net_type in self.nettypes_with_seg_id ] self.fields['segmentation_id'].help_text = ' '.join(seg_id_help) # Register network types which require segmentation ID attrs = dict( ('data-network_type-%s' % network_type, _('Segmentation ID')) for network_type in self.nettypes_with_seg_id) self.fields['segmentation_id'].widget.attrs.update(attrs) physical_networks = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}).get('physical_networks', []) if physical_networks: self.fields['physical_network'] = forms.ThemableChoiceField( label=_("Physical Network"), choices=[(net, net) for net in physical_networks], widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'network_type', }), help_text=_("The name of the physical network over " "which the virtual network is implemented."), ) # Register network types which require physical network attrs = dict( ('data-network_type-%s' % network_type, _('Physical Network')) for network_type in self.nettypes_with_physnet) self.fields['physical_network'].widget.attrs.update(attrs) network_type_choices = [ (net_type, self.provider_types[net_type]['display_name']) for net_type in supported_provider_types ] if len(network_type_choices) == 0: self._hide_provider_network_type() else: self.fields['network_type'].choices = network_type_choices try: if api.neutron.is_extension_supported(request, 'network_availability_zone'): zones = api.neutron.list_availability_zones( self.request, 'network', 'available') self.fields['az_hints'].choices = [(zone['name'], zone['name']) for zone in zones] else: del self.fields['az_hints'] except Exception: msg = _('Failed to get availability zone list.') messages.warning(request, msg) del self.fields['az_hints'] def _hide_provider_network_type(self): self.fields['network_type'].widget = forms.HiddenInput() self.fields['physical_network'].widget = forms.HiddenInput() self.fields['segmentation_id'].widget = forms.HiddenInput() self.fields['network_type'].required = False self.fields['physical_network'].required = False self.fields['segmentation_id'].required = False def handle(self, request, data): try: params = { 'name': data['name'], 'tenant_id': data['tenant_id'], 'admin_state_up': data['admin_state'], 'shared': data['shared'], 'router:external': data['external'] } if api.neutron.is_extension_supported(request, 'provider'): network_type = data['network_type'] params['provider:network_type'] = network_type if network_type in self.nettypes_with_physnet: params['provider:physical_network'] = ( data['physical_network']) if network_type in self.nettypes_with_seg_id: params['provider:segmentation_id'] = ( data['segmentation_id']) if 'az_hints' in data and data['az_hints']: params['availability_zone_hints'] = data['az_hints'] network = api.neutron.network_create(request, **params) LOG.debug('Network %s was successfully created.', data['name']) return network except Exception: redirect = reverse('horizon:admin:networks:index') msg = _('Failed to create network %s') % data['name'] exceptions.handle(request, msg, redirect=redirect) def clean(self): cleaned_data = super(CreateNetwork, self).clean() if api.neutron.is_extension_supported(self.request, 'provider'): self._clean_physical_network(cleaned_data) self._clean_segmentation_id(cleaned_data) return cleaned_data def _clean_physical_network(self, data): network_type = data.get('network_type') if ('physical_network' in self._errors and network_type not in self.nettypes_with_physnet): # In this case the physical network is not required, so we can # ignore any errors. del self._errors['physical_network'] def _clean_segmentation_id(self, data): network_type = data.get('network_type') if 'segmentation_id' in self._errors: if (network_type not in self.nettypes_with_seg_id and not self.data.get("segmentation_id")): # In this case the segmentation ID is not required, so we can # ignore the field is required error. del self._errors['segmentation_id'] elif network_type in self.nettypes_with_seg_id: seg_id = data.get('segmentation_id') seg_id_range = { 'min': self.seg_id_range[network_type][0], 'max': self.seg_id_range[network_type][1] } if seg_id < seg_id_range['min'] or seg_id > seg_id_range['max']: msg = (_('For a %(network_type)s network, valid segmentation ' 'IDs are %(min)s through %(max)s.') % { 'network_type': network_type, 'min': seg_id_range['min'], 'max': seg_id_range['max'] }) self._errors['segmentation_id'] = self.error_class([msg])
class ImportOpaqueData(forms.SelfHandlingForm): name = forms.RegexField(required=False, max_length=255, label=_("Data Name"), regex=shared_forms.NAME_REGEX, error_messages=shared_forms.ERROR_MESSAGES) source_type = forms.ChoiceField( label=_('Source'), required=False, choices=[('file', _('File')), ('raw', _('Direct Input'))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'source' })) object_file = forms.FileField(label=_("Choose file"), help_text=_("A local file to upload."), widget=forms.FileInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-file': _('File') }), required=False) direct_input = forms.CharField( label=_('Object Bytes'), help_text=_('The bytes of the object, represented in hex.'), widget=forms.widgets.Textarea( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-raw': _('Bytes') }), required=False) def __init__(self, request, *args, **kwargs): super(ImportOpaqueData, self).__init__(request, *args, **kwargs) def clean(self): data = super(ImportOpaqueData, self).clean() # The data can be missing based on particular upload # conditions. Code defensively for it here... data_file = data.get('object_file', None) data_raw = data.get('direct_input', None) if data_raw and data_file: raise forms.ValidationError( _("Cannot specify both file and direct input.")) if not data_raw and not data_file: raise forms.ValidationError( _("No input was provided for the object value.")) try: if data_file: data_bytes = self.files['object_file'].read() else: data_str = data['direct_input'] data_bytes = binascii.unhexlify(data_str) data['object_bytes'] = base64.b64encode(data_bytes) except Exception as e: msg = _('There was a problem loading the object: %s. ' 'Is the object valid and in the correct format?') % e raise forms.ValidationError(msg) return data def handle(self, request, data): try: data_bytes = data.get('object_bytes') data_uuid = client.import_object( request, data=data_bytes, name=data['name'], object_type=opaque_data.OpaqueData) if data['name']: data_identifier = data['name'] else: data_identifier = data_uuid messages.success( request, _('Successfully imported object: %s') % data_identifier) return data_uuid except Exception as e: msg = _('Unable to import object: %s') messages.error(msg % e) exceptions.handle(request, ignore=True) self.api_error(_('Unable to import object.')) return False
class CreateNamespaceForm(forms.SelfHandlingForm): source_type = forms.ChoiceField( label=_('Namespace Definition Source'), choices=[('file', _('Metadata Definition File')), ('raw', _('Direct Input'))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'source' })) metadef_file = forms.FileField( label=_("Metadata Definition File"), help_text=_("A local metadata definition file to upload."), widget=forms.FileInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-required-when-shown': 'true', 'data-source-file': _('Metadata Definition File') }), required=False) direct_input = forms.CharField( label=_('Namespace JSON'), help_text=_('The JSON formatted contents of a namespace.'), widget=forms.widgets.Textarea( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-required-when-shown': 'true', 'data-source-raw': _('Namespace JSON') }), required=False) public = forms.BooleanField(label=_("Public"), required=False) protected = forms.BooleanField(label=_("Protected"), required=False) def clean(self): data = super().clean() # The key can be missing based on particular upload # conditions. Code defensively for it here... metadef_file = data.get('metadef_file', None) metadata_raw = data.get('direct_input', None) if metadata_raw and metadef_file: raise ValidationError( _("Cannot specify both file and direct input.")) if not metadata_raw and not metadef_file: raise ValidationError( _("No input was provided for the namespace content.")) try: if metadef_file: ns_str = self.files['metadef_file'].read() else: ns_str = data['direct_input'] namespace = json.loads(ns_str) if data['public']: namespace['visibility'] = 'public' else: namespace['visibility'] = 'private' namespace['protected'] = data['protected'] for protected_prop in constants.METADEFS_PROTECTED_PROPS: namespace.pop(protected_prop, None) data['namespace'] = namespace except Exception as e: msg = _('There was a problem loading the namespace: %s.') % e raise forms.ValidationError(msg) return data def handle(self, request, data): try: namespace = glance.metadefs_namespace_create( request, data['namespace']) messages.success( request, _('Namespace %s has been created.') % namespace['namespace']) return namespace except Exception as e: msg = _('Unable to create new namespace. %s') msg %= e.message.split('Failed validating', 1)[0] exceptions.handle(request, message=msg) return False
class UpdateIKEPolicy(forms.SelfHandlingForm): name = forms.CharField(max_length=80, label=_("Name"), required=False) ikepolicy_id = forms.CharField( label=_("ID"), widget=forms.TextInput(attrs={'readonly': 'readonly'})) description = forms.CharField(required=False, max_length=80, label=_("Description")) # Currently this field has only one choice, so mark it as readonly. auth_algorithm = forms.ChoiceField( label=_("Authorization algorithm"), choices=[('sha1', _('sha1'))], widget=forms.ThemableSelectWidget(attrs={'readonly': 'readonly'}), required=False) encryption_algorithm = forms.ChoiceField(label=_("Encryption algorithm"), choices=[ ('3des', _('3des')), ('aes-128', _('aes-128')), ('aes-192', _('aes-192')), ('aes-256', _('aes-256')) ], required=False) ike_version = forms.ChoiceField(label=_("IKE version"), choices=[('v1', _('v1')), ('v2', _('v2'))], required=False) # Currently this field has only one choice, so mark it as readonly. lifetime_units = forms.ChoiceField( label=_("Lifetime units for IKE keys"), choices=[('seconds', _('seconds'))], widget=forms.ThemableSelectWidget(attrs={'readonly': 'readonly'}), required=False) lifetime_value = forms.IntegerField( min_value=60, label=_("Lifetime value for IKE keys"), help_text=_("Equal to or greater than 60"), required=False) pfs = forms.ChoiceField(label=_("Perfect Forward Secrecy"), choices=[('group2', _('group2')), ('group5', _('group5')), ('group14', _('group14'))], required=False) # Currently this field has only one choice, so mark it as readonly. phase1_negotiation_mode = forms.ChoiceField( label=_("IKE Phase1 negotiation mode"), choices=[('main', 'main')], widget=forms.ThemableSelectWidget(attrs={'readonly': 'readonly'}), required=False) failure_url = 'horizon:project:vpn:index' def handle(self, request, context): try: data = { 'ikepolicy': { 'name': context['name'], 'description': context['description'], 'auth_algorithm': context['auth_algorithm'], 'encryption_algorithm': context['encryption_algorithm'], 'ike_version': context['ike_version'], 'lifetime': { 'units': context['lifetime_units'], 'value': context['lifetime_value'] }, 'pfs': context['pfs'], 'phase1_negotiation_mode': context['phase1_negotiation_mode'], } } ikepolicy = api.vpn.ikepolicy_update(request, context['ikepolicy_id'], **data) msg = (_('IKE Policy %s was successfully updated.') % context['name']) messages.success(request, msg) return ikepolicy except Exception as e: LOG.info('Failed to update IKE Policy %(id)s: %(exc)s', { 'id': context['ikepolicy_id'], 'exc': e }) msg = _('Failed to update IKE Policy %s') % context['name'] redirect = reverse(self.failure_url) exceptions.handle(request, msg, redirect=redirect)
class CreateForm(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Volume Name"), required=False) description = forms.CharField(max_length=255, widget=forms.Textarea( attrs={'rows': 4}), label=_("Description"), required=False) volume_source_type = forms.ChoiceField( label=_("Volume Source"), required=False, widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'source'})) snapshot_source = forms.ChoiceField( label=_("Use snapshot as a source"), widget=forms.ThemableSelectWidget( attrs={'class': 'snapshot-selector switched', 'data-switch-on': 'source', 'data-source-snapshot_source': _("Use snapshot as a source"), 'data-required-when-shown': 'true'}, data_attrs=('size', 'name'), transform=lambda x: "%s (%s GiB)" % (x.name, x.size)), required=False) image_source = forms.ChoiceField( label=_("Use image as a source"), widget=forms.ThemableSelectWidget( attrs={'class': 'image-selector switched', 'data-switch-on': 'source', 'data-source-image_source': _("Use image as a source"), 'data-required-when-shown': 'true'}, data_attrs=('size', 'name', 'min_disk'), transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))), required=False) volume_source = forms.ChoiceField( label=_("Use a volume as source"), widget=forms.ThemableSelectWidget( attrs={'class': 'image-selector switched', 'data-switch-on': 'source', 'data-source-volume_source': _("Use a volume as source"), 'data-required-when-shown': 'true'}, data_attrs=('size', 'name'), transform=lambda x: "%s (%s GiB)" % (x.name, x.size)), required=False) type = forms.ChoiceField( label=_("Type"), required=False, widget=forms.ThemableSelectWidget( attrs={'class': 'switched', 'data-switch-on': 'source', 'data-source-no_source_type': _('Type'), 'data-source-image_source': _('Type')})) size = forms.IntegerField(min_value=1, initial=1, label=_("Size (GiB)")) availability_zone = forms.ChoiceField( label=_("Availability Zone"), required=False, widget=forms.ThemableSelectWidget( attrs={'class': 'switched', 'data-switch-on': 'source', 'data-source-no_source_type': _('Availability Zone'), 'data-source-image_source': _('Availability Zone')})) group = forms.ThemableChoiceField( label=_("Group"), required=False, help_text=_("Group which the new volume belongs to. Choose " "'No group' if the new volume belongs to no group.")) def prepare_source_fields_if_snapshot_specified(self, request): try: snapshot = self.get_snapshot(request, request.GET["snapshot_id"]) self.fields['name'].initial = snapshot.name self.fields['size'].initial = snapshot.size self.fields['snapshot_source'].choices = ((snapshot.id, snapshot),) try: # Set the volume type from the original volume orig_volume = cinder.volume_get(request, snapshot.volume_id) self.fields['type'].initial = orig_volume.volume_type except Exception: pass self.fields['size'].help_text = ( _('Volume size must be equal to or greater than the ' 'snapshot size (%sGiB)') % snapshot.size) self.fields['type'].widget = forms.widgets.HiddenInput() del self.fields['image_source'] del self.fields['volume_source'] del self.fields['volume_source_type'] del self.fields['availability_zone'] except Exception: exceptions.handle(request, _('Unable to load the specified snapshot.')) def prepare_source_fields_if_image_specified(self, request): self.fields['availability_zone'].choices = \ availability_zones(request) try: image = self.get_image(request, request.GET["image_id"]) image.bytes = image.size self.fields['name'].initial = image.name min_vol_size = functions.bytes_to_gigabytes( image.size) size_help_text = (_('Volume size must be equal to or greater ' 'than the image size (%s)') % filesizeformat(image.size)) properties = getattr(image, 'properties', {}) min_disk_size = (getattr(image, 'min_disk', 0) or properties.get('min_disk', 0)) if min_disk_size > min_vol_size: min_vol_size = min_disk_size size_help_text = (_('Volume size must be equal to or ' 'greater than the image minimum ' 'disk size (%sGiB)') % min_disk_size) self.fields['size'].initial = min_vol_size self.fields['size'].help_text = size_help_text self.fields['image_source'].choices = ((image.id, image),) del self.fields['snapshot_source'] del self.fields['volume_source'] del self.fields['volume_source_type'] except Exception: msg = _('Unable to load the specified image. %s') exceptions.handle(request, msg % request.GET['image_id']) def prepare_source_fields_if_volume_specified(self, request): self.fields['availability_zone'].choices = \ availability_zones(request) volume = None try: volume = self.get_volume(request, request.GET["volume_id"]) except Exception: msg = _('Unable to load the specified volume. %s') exceptions.handle(request, msg % request.GET['volume_id']) if volume is not None: self.fields['name'].initial = volume.name self.fields['description'].initial = volume.description min_vol_size = volume.size size_help_text = (_('Volume size must be equal to or greater ' 'than the origin volume size (%sGiB)') % volume.size) self.fields['size'].initial = min_vol_size self.fields['size'].help_text = size_help_text self.fields['volume_source'].choices = ((volume.id, volume),) self.fields['type'].initial = volume.type del self.fields['snapshot_source'] del self.fields['image_source'] del self.fields['volume_source_type'] def prepare_source_fields_default(self, request): source_type_choices = [] self.fields['availability_zone'].choices = \ availability_zones(request) try: available = api.cinder.VOLUME_STATE_AVAILABLE snapshots = cinder.volume_snapshot_list( request, search_opts=dict(status=available)) if snapshots: source_type_choices.append(("snapshot_source", _("Snapshot"))) choices = [('', _("Choose a snapshot"))] + \ [(s.id, s) for s in snapshots] self.fields['snapshot_source'].choices = choices else: del self.fields['snapshot_source'] except Exception: exceptions.handle(request, _("Unable to retrieve volume snapshots.")) images = utils.get_available_images(request, request.user.tenant_id) if images: source_type_choices.append(("image_source", _("Image"))) choices = [('', _("Choose an image"))] for image in images: image.bytes = image.size image.size = functions.bytes_to_gigabytes(image.bytes) choices.append((image.id, image)) self.fields['image_source'].choices = choices else: del self.fields['image_source'] volumes = self.get_volumes(request) if volumes: source_type_choices.append(("volume_source", _("Volume"))) choices = [('', _("Choose a volume"))] for volume in volumes: choices.append((volume.id, volume)) self.fields['volume_source'].choices = choices else: del self.fields['volume_source'] if source_type_choices: choices = ([('no_source_type', _("No source, empty volume"))] + source_type_choices) self.fields['volume_source_type'].choices = choices else: del self.fields['volume_source_type'] def _populate_group_choices(self, request): try: groups = cinder.group_list(request) except cinder_exc.VersionNotFoundForAPIMethod: del self.fields['group'] return except Exception: redirect = reverse("horizon:project:volumes:index") exceptions.handle(request, _('Unable to retrieve the volume group list.'), redirect=redirect) group_choices = [(g.id, g.name or g.id) for g in groups] group_choices.insert(0, ("", _("No group"))) self.fields['group'].choices = group_choices def __init__(self, request, *args, **kwargs): super().__init__(request, *args, **kwargs) volume_types = [] try: volume_types = cinder.volume_type_list(request) except Exception: redirect_url = reverse("horizon:project:volumes:index") error_message = _('Unable to retrieve the volume type list.') exceptions.handle(request, error_message, redirect=redirect_url) self.fields['type'].choices = [("", _("No volume type"))] + \ [(type.name, type.name) for type in volume_types] if 'initial' in kwargs and 'type' in kwargs['initial']: # if there is a default volume type to select, then remove # the first ""No volume type" entry self.fields['type'].choices.pop(0) if "snapshot_id" in request.GET: self.prepare_source_fields_if_snapshot_specified(request) elif 'image_id' in request.GET: self.prepare_source_fields_if_image_specified(request) elif 'volume_id' in request.GET: self.prepare_source_fields_if_volume_specified(request) else: self.prepare_source_fields_default(request) self._populate_group_choices(request) def clean(self): cleaned_data = super().clean() source_type = self.cleaned_data.get('volume_source_type') if (source_type == 'image_source' and not cleaned_data.get('image_source')): msg = _('Image source must be specified') self._errors['image_source'] = self.error_class([msg]) elif (source_type == 'snapshot_source' and not cleaned_data.get('snapshot_source')): msg = _('Snapshot source must be specified') self._errors['snapshot_source'] = self.error_class([msg]) elif (source_type == 'volume_source' and not cleaned_data.get('volume_source')): msg = _('Volume source must be specified') self._errors['volume_source'] = self.error_class([msg]) return cleaned_data def get_volumes(self, request): volumes = [] try: available = api.cinder.VOLUME_STATE_AVAILABLE volumes = cinder.volume_list(self.request, search_opts=dict(status=available)) except Exception: exceptions.handle(request, _('Unable to retrieve list of volumes.')) return volumes def handle(self, request, data): try: usages = quotas.tenant_quota_usages( self.request, targets=('volumes', 'gigabytes')) availableGB = usages['gigabytes']['available'] availableVol = usages['volumes']['available'] snapshot_id = None image_id = None volume_id = None source_type = data.get('volume_source_type', None) az = data.get('availability_zone', None) or None volume_type = data.get('type') if (data.get("snapshot_source", None) and source_type in ['', None, 'snapshot_source']): # Create from Snapshot snapshot = self.get_snapshot(request, data["snapshot_source"]) snapshot_id = snapshot.id if data['size'] < snapshot.size: error_message = (_('The volume size cannot be less than ' 'the snapshot size (%sGiB)') % snapshot.size) raise ValidationError(error_message) az = None volume_type = "" elif (data.get("image_source", None) and source_type in ['', None, 'image_source']): # Create from Snapshot image = self.get_image(request, data["image_source"]) image_id = image.id image_size = functions.bytes_to_gigabytes(image.size) if data['size'] < image_size: error_message = (_('The volume size cannot be less than ' 'the image size (%s)') % filesizeformat(image.size)) raise ValidationError(error_message) properties = getattr(image, 'properties', {}) min_disk_size = (getattr(image, 'min_disk', 0) or properties.get('min_disk', 0)) if min_disk_size > 0 and data['size'] < min_disk_size: error_message = (_('The volume size cannot be less than ' 'the image minimum disk size (%sGiB)') % min_disk_size) raise ValidationError(error_message) elif (data.get("volume_source", None) and source_type in ['', None, 'volume_source']): # Create from volume volume = self.get_volume(request, data["volume_source"]) volume_id = volume.id volume_type = None if data['size'] < volume.size: error_message = (_('The volume size cannot be less than ' 'the source volume size (%sGiB)') % volume.size) raise ValidationError(error_message) else: if type(data['size']) is str: data['size'] = int(data['size']) if availableGB < data['size']: error_message = _('A volume of %(req)iGiB cannot be created ' 'as you only have %(avail)iGiB of your ' 'quota available.') params = {'req': data['size'], 'avail': availableGB} raise ValidationError(error_message % params) if availableVol <= 0: error_message = _('You are already using all of your available' ' volumes.') raise ValidationError(error_message) metadata = {} volume = cinder.volume_create(request, data['size'], data['name'], data['description'], volume_type, snapshot_id=snapshot_id, image_id=image_id, metadata=metadata, availability_zone=az, source_volid=volume_id, group_id=data.get('group') or None) message = _('Creating volume "%s"') % volume.name messages.info(request, message) return volume except ValidationError as e: self.api_error(e.messages[0]) return False except Exception: redirect = reverse("horizon:project:volumes:index") exceptions.handle(request, _("Unable to create volume."), redirect=redirect) @memoized def get_snapshot(self, request, id): return cinder.volume_snapshot_get(request, id) @memoized def get_image(self, request, id): return glance.image_get(request, id) @memoized def get_volume(self, request, id): return cinder.volume_get(request, id)
class CreatePort(forms.SelfHandlingForm): network_name = forms.CharField( label=_("Network Name"), widget=forms.TextInput(attrs={'readonly': 'readonly'}), required=False) network_id = forms.CharField( label=_("Network ID"), widget=forms.TextInput(attrs={'readonly': 'readonly'})) name = forms.CharField(max_length=255, label=_("Name"), required=False) admin_state = forms.BooleanField(label=_("Enable Admin State"), initial=True, required=False) device_id = forms.CharField(max_length=100, label=_("Device ID"), help_text=_("Device ID attached to the port"), required=False) device_owner = forms.CharField( max_length=100, label=_("Device Owner"), help_text=_("Owner of the device attached to the port"), required=False) specify_ip = forms.ThemableChoiceField( label=_("Specify IP address or subnet"), help_text=_("To specify a subnet or a fixed IP, select any options."), initial=False, required=False, choices=[('', _("Unspecified")), ('subnet_id', _("Subnet")), ('fixed_ip', _("Fixed IP Address"))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'specify_ip', })) subnet_id = forms.ThemableChoiceField( label=_("Subnet"), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'specify_ip', 'data-specify_ip-subnet_id': _('Subnet'), })) fixed_ip = forms.IPField( label=_("Fixed IP Address"), required=False, help_text=_("Specify the subnet IP address for the new port"), version=forms.IPv4 | forms.IPv6, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'specify_ip', 'data-specify_ip-fixed_ip': _('Fixed IP Address'), })) mac_address = forms.MACAddressField( label=_("MAC Address"), required=False, help_text=_("Specify the MAC address for the new port")) failure_url = 'horizon:project:networks:detail' def __init__(self, request, *args, **kwargs): super(CreatePort, self).__init__(request, *args, **kwargs) # prepare subnet choices and input area for each subnet subnet_choices = self._get_subnet_choices(kwargs['initial']) if subnet_choices: subnet_choices.insert(0, ('', _("Select a subnet"))) self.fields['subnet_id'].choices = subnet_choices else: self.fields['specify_ip'].widget = forms.HiddenInput() self.fields['subnet_id'].widget = forms.HiddenInput() self.fields['fixed_ip'].widget = forms.HiddenInput() if api.neutron.is_extension_supported(request, 'mac-learning'): self.fields['mac_state'] = forms.BooleanField( label=_("MAC Learning State"), initial=False, required=False) try: if api.neutron.is_extension_supported(request, 'port-security'): self.fields['port_security_enabled'] = forms.BooleanField( label=_("Port Security"), help_text=_("Enable anti-spoofing rules for the port"), initial=True, required=False) except Exception: msg = _("Unable to retrieve port security state") exceptions.handle(self.request, msg) def _get_subnet_choices(self, kwargs): try: network_id = kwargs['network_id'] network = api.neutron.network_get(self.request, network_id) except Exception: return [] return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr)) for subnet in network.subnets] def handle(self, request, data): try: params = { 'network_id': data['network_id'], 'admin_state_up': data['admin_state'], 'name': data['name'], 'device_id': data['device_id'], 'device_owner': data['device_owner'] } if data.get('specify_ip') == 'subnet_id': if data.get('subnet_id'): params['fixed_ips'] = [{"subnet_id": data['subnet_id']}] elif data.get('specify_ip') == 'fixed_ip': if data.get('fixed_ip'): params['fixed_ips'] = [{"ip_address": data['fixed_ip']}] if data.get('mac_state'): params['mac_learning_enabled'] = data['mac_state'] if 'port_security_enabled' in data: params['port_security_enabled'] = data['port_security_enabled'] # Send mac_address only when it is specified. if data['mac_address']: params['mac_address'] = data['mac_address'] port = api.neutron.port_create(request, **params) if port['name']: msg = _('Port %s was successfully created.') % port['name'] else: msg = _('Port %s was successfully created.') % port['id'] messages.success(request, msg) return port except Exception as e: LOG.info('Failed to create a port for network %(id)s: %(exc)s', { 'id': data['network_id'], 'exc': e }) msg = (_('Failed to create a port for network %s') % data['network_id']) redirect = reverse(self.failure_url, args=(data['network_id'], )) exceptions.handle(request, msg, redirect=redirect)
class CreatePort(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Name"), required=False) admin_state = forms.BooleanField(label=_("Enable Admin State"), initial=True, required=False) device_id = forms.CharField(max_length=100, label=_("Device ID"), help_text=_("Device ID attached to the port"), required=False) device_owner = forms.CharField( max_length=100, label=_("Device Owner"), help_text=_("Owner of the device attached to the port"), required=False) specify_ip = forms.ThemableChoiceField( label=_("Specify IP address or subnet"), help_text=_("To specify a subnet or a fixed IP, select any options."), required=False, choices=[('', _("Unspecified")), ('subnet_id', _("Subnet")), ('fixed_ip', _("Fixed IP Address"))], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'specify_ip', })) subnet_id = forms.ThemableChoiceField( label=_("Subnet"), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'specify_ip', 'data-specify_ip-subnet_id': _('Subnet'), })) fixed_ip = forms.IPField( label=_("Fixed IP Address"), required=False, help_text=_("Specify the subnet IP address for the new port"), version=forms.IPv4 | forms.IPv6, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'specify_ip', 'data-specify_ip-fixed_ip': _('Fixed IP Address'), })) mac_address = forms.MACAddressField( label=_("MAC Address"), required=False, help_text=_("Specify the MAC address for the new port")) failure_url = 'horizon:project:networks:detail' def __init__(self, request, *args, **kwargs): super(CreatePort, self).__init__(request, *args, **kwargs) # prepare subnet choices and input area for each subnet subnet_choices = self._get_subnet_choices(kwargs['initial']) if subnet_choices: subnet_choices.insert(0, ('', _("Select a subnet"))) self.fields['subnet_id'].choices = subnet_choices else: self.fields['specify_ip'].widget = forms.HiddenInput() self.fields['subnet_id'].widget = forms.HiddenInput() self.fields['fixed_ip'].widget = forms.HiddenInput() if api.neutron.is_extension_supported(request, 'mac-learning'): self.fields['mac_state'] = forms.BooleanField( label=_("MAC Learning State"), initial=False, required=False) try: if api.neutron.is_extension_supported(request, 'port-security'): self.fields['port_security_enabled'] = forms.BooleanField( label=_("Port Security"), help_text=_("Enable anti-spoofing rules for the port"), initial=True, required=False) except Exception: msg = _("Unable to retrieve port security state") exceptions.handle(self.request, msg) def _get_subnet_choices(self, kwargs): try: network_id = kwargs['network_id'] network = api.neutron.network_get(self.request, network_id) except Exception: return [] # NOTE(amotoki): When a user cannot retrieve a subnet info, # subnet ID is stored in network.subnets field. # If so, we skip such subnet as subnet choices. # This happens usually for external networks. # TODO(amotoki): Ideally it is better to disable/hide # Create Port button in the port table, but as of Pike # the default neutron policy.json for "create_port" is empty # and there seems no appropriate policy. This is a dirty hack. return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr)) for subnet in network.subnets if isinstance(subnet, api.neutron.Subnet)] def handle(self, request, data): try: params = { 'network_id': self.initial['network_id'], 'admin_state_up': data['admin_state'], 'name': data['name'], 'device_id': data['device_id'], 'device_owner': data['device_owner'] } if data.get('specify_ip') == 'subnet_id': if data.get('subnet_id'): params['fixed_ips'] = [{"subnet_id": data['subnet_id']}] elif data.get('specify_ip') == 'fixed_ip': if data.get('fixed_ip'): params['fixed_ips'] = [{"ip_address": data['fixed_ip']}] if data.get('mac_state'): params['mac_learning_enabled'] = data['mac_state'] if 'port_security_enabled' in data: params['port_security_enabled'] = data['port_security_enabled'] # Send mac_address only when it is specified. if data['mac_address']: params['mac_address'] = data['mac_address'] port = api.neutron.port_create(request, **params) if port['name']: msg = _('Port %s was successfully created.') % port['name'] else: msg = _('Port %s was successfully created.') % port['id'] messages.success(request, msg) return port except Exception as e: LOG.info('Failed to create a port for network %(id)s: %(exc)s', { 'id': self.initial['network_id'], 'exc': e }) if isinstance(e, neutron_exc.Forbidden): msg = (_('You are not allowed to create a port ' 'for network %s.') % self.initial['network_id']) else: msg = (_('Failed to create a port for network %s') % self.initial['network_id']) redirect = reverse(self.failure_url, args=(self.initial['network_id'], )) exceptions.handle(request, msg, redirect=redirect)
class TemplateForm(forms.SelfHandlingForm): class Meta(object): name = _('Select Template') help_text = _('Select a template to launch a stack.') # TODO(jomara) - update URL choice for template & environment files # w/ client side download when applicable base_choices = [('file', _('File')), ('raw', _('Direct Input'))] url_choice = [('url', _('URL'))] attributes = {'class': 'switchable', 'data-slug': 'templatesource'} template_source = forms.ChoiceField( label=_('Template Source'), choices=base_choices + url_choice, widget=forms.ThemableSelectWidget(attrs=attributes)) attributes = create_upload_form_attributes('template', 'file', _('Template File')) template_upload = forms.FileField( label=_('Template File'), help_text=_('A local template to upload.'), widget=forms.FileInput(attrs=attributes), required=False) attributes = create_upload_form_attributes('template', 'url', _('Template URL')) template_url = forms.URLField( label=_('Template URL'), help_text=_('An external (HTTP) URL to load the template from.'), widget=forms.TextInput(attrs=attributes), required=False) attributes = create_upload_form_attributes('template', 'raw', _('Template Data')) template_data = forms.CharField( label=_('Template Data'), help_text=_('The raw contents of the template.'), widget=forms.widgets.Textarea(attrs=attributes), required=False) attributes = {'data-slug': 'envsource', 'class': 'switchable'} environment_source = forms.ChoiceField( label=_('Environment Source'), choices=base_choices, widget=forms.ThemableSelectWidget(attrs=attributes), required=False) attributes = create_upload_form_attributes('env', 'file', _('Environment File')) environment_upload = forms.FileField( label=_('Environment File'), help_text=_('A local environment to upload.'), widget=forms.FileInput(attrs=attributes), required=False) attributes = create_upload_form_attributes('env', 'raw', _('Environment Data')) environment_data = forms.CharField( label=_('Environment Data'), help_text=_('The raw contents of the environment file.'), widget=forms.widgets.Textarea(attrs=attributes), required=False) if django.VERSION >= (1, 9): # Note(Itxaka): On django>=1.9 Charfield has an strip option that # we need to set to False as to not hit # https://bugs.launchpad.net/python-heatclient/+bug/1546166 environment_data.strip = False template_data.strip = False def __init__(self, *args, **kwargs): self.next_view = kwargs.pop('next_view') super(TemplateForm, self).__init__(*args, **kwargs) def clean(self): cleaned = super(TemplateForm, self).clean() files = self.request.FILES self.clean_uploaded_files('template', _('template'), cleaned, files) self.clean_uploaded_files('environment', _('environment'), cleaned, files) # Validate the template and get back the params. kwargs = {} if cleaned['environment_data']: kwargs['environment'] = cleaned['environment_data'] try: files, tpl =\ api.heat.get_template_files(cleaned.get('template_data'), cleaned.get('template_url')) kwargs['files'] = files kwargs['template'] = tpl validated = api.heat.template_validate(self.request, **kwargs) cleaned['template_validate'] = validated cleaned['template_validate']['files'] = files cleaned['template_validate']['template'] = tpl except Exception as e: raise forms.ValidationError(six.text_type(e)) return cleaned def clean_uploaded_files(self, prefix, field_label, cleaned, files): """Cleans Template & Environment data from form upload. Does some of the crunchy bits for processing uploads vs raw data depending on what the user specified. Identical process for environment data & template data. :type prefix: str :param prefix: prefix (environment, template) of field :type field_label: str :param field_label: translated prefix str for messages :type input_type: dict :param prefix: existing cleaned fields from form :rtype: dict :return: cleaned dict including environment & template data """ upload_str = prefix + "_upload" data_str = prefix + "_data" url = cleaned.get(prefix + '_url') data = cleaned.get(prefix + '_data') has_upload = upload_str in files # Uploaded file handler if has_upload and not url: log_template_name = files[upload_str].name LOG.info('got upload %s', log_template_name) tpl = files[upload_str].read() if tpl.startswith('{'): try: json.loads(tpl) except Exception as e: msg = _('There was a problem parsing the' ' %(prefix)s: %(error)s') msg = msg % {'prefix': prefix, 'error': six.text_type(e)} raise forms.ValidationError(msg) cleaned[data_str] = tpl # URL handler elif url and (has_upload or data): msg = _('Please specify a %s using only one source method.') msg = msg % field_label raise forms.ValidationError(msg) elif prefix == 'template': # Check for raw template input - blank environment allowed if not url and not data: msg = _('You must specify a template via one of the ' 'available sources.') raise forms.ValidationError(msg) def create_kwargs(self, data): kwargs = { 'parameters': data['template_validate'], 'environment_data': data['environment_data'] } if data.get('stack_id'): kwargs['stack_id'] = data['stack_id'] return kwargs def handle(self, request, data): kwargs = self.create_kwargs(data) # NOTE (gabriel): This is a bit of a hack, essentially rewriting this # request so that we can chain it as an input to the next view... # but hey, it totally works. request.method = 'GET' return self.next_view.as_view()(request, **kwargs)
class AddRule(forms.SelfHandlingForm): id = forms.CharField(widget=forms.HiddenInput()) rule_menu = forms.ChoiceField( label=_('Rule'), widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'rule_menu' })) description = forms.CharField( label=_('Description'), required=False, max_length=255, widget=forms.Textarea(attrs={'rows': 2}), help_text=_('A brief description of the security group rule ' 'you are adding')) # "direction" field is enabled only when custom mode. # It is because most common rules in local_settings.py is meaningful # when its direction is 'ingress'. direction = forms.ChoiceField( label=_('Direction'), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'rule_menu', 'data-rule_menu-tcp': _('Direction'), 'data-rule_menu-udp': _('Direction'), 'data-rule_menu-icmp': _('Direction'), 'data-rule_menu-custom': _('Direction'), 'data-rule_menu-all_tcp': _('Direction'), 'data-rule_menu-all_udp': _('Direction'), 'data-rule_menu-all_icmp': _('Direction'), })) ip_protocol = forms.IntegerField( label=_('IP Protocol'), required=False, help_text=_("Enter an integer value between 0 and 255."), validators=[utils_validators.validate_ip_protocol], widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'rule_menu', 'data-rule_menu-custom': _('IP Protocol') })) port_or_range = forms.ChoiceField( label=_('Open Port'), choices=[('port', _('Port')), ('range', _('Port Range'))], widget=forms.ThemableSelectWidget( attrs={ 'class': 'switchable switched', 'data-slug': 'range', 'data-switch-on': 'rule_menu', 'data-rule_menu-tcp': _('Open Port'), 'data-rule_menu-udp': _('Open Port') })) port = forms.IntegerField( label=_("Port"), required=False, help_text=_("Enter an integer value " "between 1 and 65535."), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-required-when-shown': 'true', 'data-switch-on': 'range', 'data-range-port': _('Port') }), validators=[utils_validators.validate_port_range]) from_port = forms.IntegerField( label=_("From Port"), required=False, help_text=_("Enter an integer value " "between 1 and 65535."), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-required-when-shown': 'true', 'data-switch-on': 'range', 'data-range-range': _('From Port') }), validators=[utils_validators.validate_port_range]) to_port = forms.IntegerField( label=_("To Port"), required=False, help_text=_("Enter an integer value " "between 1 and 65535."), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-required-when-shown': 'true', 'data-switch-on': 'range', 'data-range-range': _('To Port') }), validators=[utils_validators.validate_port_range]) icmp_type = forms.IntegerField( label=_("Type"), required=False, help_text=_("Enter a value for ICMP type " "in the range (-1: 255)"), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'rule_menu', 'data-rule_menu-icmp': _('Type') }), validators=[utils_validators.validate_icmp_type_range]) icmp_code = forms.IntegerField( label=_("Code"), required=False, help_text=_("Enter a value for ICMP code " "in the range (-1: 255)"), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'rule_menu', 'data-rule_menu-icmp': _('Code') }), validators=[utils_validators.validate_icmp_code_range]) remote = forms.ChoiceField( label=_('Remote'), choices=[('cidr', _('CIDR')), ('sg', _('Security Group'))], help_text=_('To specify an allowed IP ' 'range, select "CIDR". ' 'To allow access from all ' 'members of another security ' 'group select "Security ' 'Group".'), widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'remote' })) cidr = forms.IPField(label=_("CIDR"), required=False, initial="0.0.0.0/0", help_text=_("Classless Inter-Domain Routing " "(e.g. 192.168.0.0/24, or " "2001:db8::/128)"), version=forms.IPv4 | forms.IPv6, mask=True, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'remote', 'data-remote-cidr': _('CIDR') })) security_group = forms.ChoiceField( label=_('Security Group'), required=False, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'remote', 'data-remote-sg': _('Security ' 'Group') })) # When cidr is used ethertype is determined from IP version of cidr. # When source group, ethertype needs to be specified explicitly. ethertype = forms.ChoiceField(label=_('Ether Type'), required=False, choices=[('IPv4', _('IPv4')), ('IPv6', _('IPv6'))], widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-slug': 'ethertype', 'data-switch-on': 'remote', 'data-remote-sg': _('Ether Type') })) def __init__(self, *args, **kwargs): sg_list = kwargs.pop('sg_list', []) super(AddRule, self).__init__(*args, **kwargs) # Determine if there are security groups available for the # remote group option; add the choices and enable the option if so. if sg_list: security_groups_choices = sg_list else: security_groups_choices = [("", _("No security groups available"))] self.fields['security_group'].choices = security_groups_choices # TODO(amotoki): settings.SECURITY_GROUP_RULES may contains 'backend' # parameter. If 'backend' is used, error message should be emitted. backend = 'neutron' rules_dict = getattr(settings, 'SECURITY_GROUP_RULES', []) common_rules = [(k, rules_dict[k]['name']) for k in rules_dict if rules_dict[k].get('backend', backend) == backend] common_rules.sort() custom_rules = [('tcp', _('Custom TCP Rule')), ('udp', _('Custom UDP Rule')), ('icmp', _('Custom ICMP Rule')), ('custom', _('Other Protocol'))] self.fields['rule_menu'].choices = custom_rules + common_rules self.rules = rules_dict self.fields['direction'].choices = [('ingress', _('Ingress')), ('egress', _('Egress'))] self.fields['ip_protocol'].help_text = _( "Enter an integer value between -1 and 255 " "(-1 means wild card).") self.fields['port_or_range'].choices = [ ('port', _('Port')), ('range', _('Port Range')), ('all', _('All ports')), ] if not getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}).get( 'enable_ipv6', True): self.fields['cidr'].version = forms.IPv4 self.fields['ethertype'].widget = forms.TextInput( attrs={'readonly': 'readonly'}) self.fields['ethertype'].initial = 'IPv4' try: is_desc_supported = api.neutron.is_extension_supported( self.request, 'standard-attr-description') except Exception: exceptions.handle( self.request, _('Failed to check if description field is supported.')) is_desc_supported = False if not is_desc_supported: del self.fields['description'] def _update_and_pop_error(self, cleaned_data, key, value): cleaned_data[key] = value self.errors.pop(key, None) def _clean_rule_icmp(self, cleaned_data, rule_menu): icmp_type = cleaned_data.get("icmp_type", None) icmp_code = cleaned_data.get("icmp_code", None) self._update_and_pop_error(cleaned_data, 'ip_protocol', rule_menu) if icmp_type == -1 and icmp_code != -1: msg = _('ICMP code is provided but ICMP type is missing.') raise ValidationError(msg) if self.errors.get('icmp_type'): msg = _('The ICMP type not in range (-1, 255)') raise ValidationError(msg) if self.errors.get('icmp_code'): msg = _('The ICMP code not in range (-1, 255)') raise ValidationError(msg) self._update_and_pop_error(cleaned_data, 'from_port', icmp_type) self._update_and_pop_error(cleaned_data, 'to_port', icmp_code) self._update_and_pop_error(cleaned_data, 'port', None) def _clean_rule_tcp_udp(self, cleaned_data, rule_menu): port_or_range = cleaned_data.get("port_or_range") from_port = cleaned_data.get("from_port", None) to_port = cleaned_data.get("to_port", None) port = cleaned_data.get("port", None) self._update_and_pop_error(cleaned_data, 'ip_protocol', rule_menu) self._update_and_pop_error(cleaned_data, 'icmp_code', None) self._update_and_pop_error(cleaned_data, 'icmp_type', None) if port_or_range == 'all': self._update_and_pop_error(cleaned_data, 'port', None) self._update_and_pop_error(cleaned_data, 'from_port', None) self._update_and_pop_error(cleaned_data, 'to_port', None) elif port_or_range == "port": self._update_and_pop_error(cleaned_data, 'from_port', port) self._update_and_pop_error(cleaned_data, 'to_port', port) if port is None: msg = _('The specified port is invalid.') raise ValidationError(msg) else: self._update_and_pop_error(cleaned_data, 'port', None) if from_port is None: msg = _('The "from" port number is invalid.') raise ValidationError(msg) if to_port is None: msg = _('The "to" port number is invalid.') raise ValidationError(msg) if to_port < from_port: msg = _('The "to" port number must be greater than ' 'or equal to the "from" port number.') raise ValidationError(msg) def _clean_rule_custom(self, cleaned_data, rule_menu): # custom IP protocol rule so we need to fill unused fields so # the validation works unused_fields = [ 'icmp_code', 'icmp_type', 'from_port', 'to_port', 'port' ] for unused_field in unused_fields: self._update_and_pop_error(cleaned_data, unused_field, None) def _apply_rule_menu(self, cleaned_data, rule_menu): cleaned_data['ip_protocol'] = self.rules[rule_menu]['ip_protocol'] cleaned_data['from_port'] = int(self.rules[rule_menu]['from_port']) cleaned_data['to_port'] = int(self.rules[rule_menu]['to_port']) self._update_and_pop_error(cleaned_data, 'icmp_code', None) self._update_and_pop_error(cleaned_data, 'icmp_type', None) if rule_menu not in ['all_tcp', 'all_udp', 'all_icmp']: direction = self.rules[rule_menu].get('direction') cleaned_data['direction'] = direction def _clean_rule_menu(self, cleaned_data): rule_menu = cleaned_data.get('rule_menu') if rule_menu == 'icmp': self._clean_rule_icmp(cleaned_data, rule_menu) elif rule_menu in ('tcp', 'udp'): self._clean_rule_tcp_udp(cleaned_data, rule_menu) elif rule_menu == 'custom': self._clean_rule_custom(cleaned_data, rule_menu) else: self._apply_rule_menu(cleaned_data, rule_menu) def _adjust_ip_protocol_of_icmp(self, data): # Note that this needs to be called after IPv4/IPv6 is determined. try: ip_protocol = int(data['ip_protocol']) except ValueError: # string representation of IP protocol ip_protocol = data['ip_protocol'] is_ipv6 = data['ethertype'] == 'IPv6' if isinstance(ip_protocol, int): # When IP protocol number is specified, we assume a user # knows more detail on IP protocol number, # so a warning message on a mismatch between IP proto number # and IP version is displayed. if is_ipv6 and ip_protocol == 1: msg = _('58 (ipv6-icmp) should be specified for IPv6 ' 'instead of 1.') self._errors['ip_protocol'] = self.error_class([msg]) elif not is_ipv6 and ip_protocol == 58: msg = _('1 (icmp) should be specified for IPv4 ' 'instead of 58.') self._errors['ip_protocol'] = self.error_class([msg]) else: # ICMPv6 uses different an IP protocol name and number. # To allow 'icmp' for both IPv4 and IPv6 in the form, # we need to replace 'icmp' with 'ipv6-icmp' based on IP version. if is_ipv6 and ip_protocol == 'icmp': data['ip_protocol'] = 'ipv6-icmp' def clean(self): cleaned_data = super(AddRule, self).clean() self._clean_rule_menu(cleaned_data) # NOTE(amotoki): There are two cases where cleaned_data['direction'] # is empty: (1) Nova Security Group is used. Since "direction" is # HiddenInput, direction field exists but its value is ''. # (2) Template except all_* is used. In this case, the default value # is None. To make sure 'direction' field has 'ingress' or 'egress', # fill this field here if it is not specified. if not cleaned_data['direction']: cleaned_data['direction'] = 'ingress' remote = cleaned_data.get("remote") if remote == "cidr": self._update_and_pop_error(cleaned_data, 'security_group', None) else: self._update_and_pop_error(cleaned_data, 'cidr', None) # If cleaned_data does not contain a non-empty value, IPField already # has validated it, so skip the further validation for cidr. # In addition cleaned_data['cidr'] is None means source_group is used. if 'cidr' in cleaned_data and cleaned_data['cidr'] is not None: cidr = cleaned_data['cidr'] if not cidr: msg = _('CIDR must be specified.') self._errors['cidr'] = self.error_class([msg]) else: # If cidr is specified, ethertype is determined from IP address # version. It is used only when Neutron is enabled. ip_ver = netaddr.IPNetwork(cidr).version cleaned_data['ethertype'] = 'IPv6' if ip_ver == 6 else 'IPv4' self._adjust_ip_protocol_of_icmp(cleaned_data) return cleaned_data def handle(self, request, data): redirect = reverse("horizon:project:security_groups:detail", args=[data['id']]) params = {} if 'description' in data: params['description'] = data['description'] try: rule = api.neutron.security_group_rule_create( request, filters.get_int_or_uuid(data['id']), data['direction'], data['ethertype'], data['ip_protocol'], data['from_port'], data['to_port'], data['cidr'], data['security_group'], **params) messages.success( request, _('Successfully added rule: %s') % six.text_type(rule)) return rule except exceptions.Conflict as error: exceptions.handle(request, error, redirect=redirect) except Exception: exceptions.handle(request, _('Unable to add rule to security group.'), redirect=redirect)
class CustomizeAction(workflows.Action): class Meta(object): name = _("Post-Creation") help_text_template = ("project/instances/" "_launch_customize_help.html") source_choices = [('', _('Select Script Source')), ('raw', _('Direct Input')), ('file', _('File'))] attributes = {'class': 'switchable', 'data-slug': 'scriptsource'} script_source = forms.ChoiceField( label=_('Customization Script Source'), choices=source_choices, widget=forms.ThemableSelectWidget(attrs=attributes), required=False) script_help = _("A script or set of commands to be executed after the " "instance has been built (max 16kb).") script_upload = forms.FileField( label=_('Script File'), help_text=script_help, widget=forms.FileInput( attrs={ 'class': 'switched', 'data-switch-on': 'scriptsource', 'data-scriptsource-file': _('Script File') }), required=False) script_data = forms.CharField( label=_('Script Data'), help_text=script_help, widget=forms.widgets.Textarea( attrs={ 'class': 'switched', 'data-switch-on': 'scriptsource', 'data-scriptsource-raw': _('Script Data') }), required=False) def __init__(self, *args): super(CustomizeAction, self).__init__(*args) def clean(self): cleaned = super(CustomizeAction, self).clean() files = self.request.FILES script = self.clean_uploaded_files('script', files) if script is not None: cleaned['script_data'] = script return cleaned def clean_uploaded_files(self, prefix, files): upload_str = prefix + "_upload" has_upload = upload_str in files if has_upload: upload_file = files[upload_str] log_script_name = upload_file.name LOG.info('got upload %s' % log_script_name) if upload_file._size > 16 * units.Ki: # 16kb msg = _('File exceeds maximum size (16kb)') raise forms.ValidationError(msg) else: script = upload_file.read() if script != "": try: normalize_newlines(script) except Exception as e: msg = _('There was a problem parsing the' ' %(prefix)s: %(error)s') msg = msg % { 'prefix': prefix, 'error': six.text_type(e) } raise forms.ValidationError(msg) return script else: return None
class CreateSubnetDetailAction(workflows.Action): enable_dhcp = forms.BooleanField(label=_("Enable DHCP"), initial=True, required=False) ipv6_modes = forms.ChoiceField( label=_("IPv6 Address Configuration Mode"), widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'ipversion', 'data-ipversion-6': _("IPv6 Address Configuration Mode"), }), initial=utils.IPV6_DEFAULT_MODE, required=False, help_text=_("Specifies how IPv6 addresses and additional information " "are configured. We can specify SLAAC/DHCPv6 stateful/" "DHCPv6 stateless provided by OpenStack, " "or specify no option. " "'No options specified' means addresses are configured " "manually or configured by a non-OpenStack system.")) allocation_pools = forms.CharField( widget=forms.Textarea(attrs={'rows': 4}), label=_("Allocation Pools"), help_text=_("IP address allocation pools. Each entry is: " "start_ip_address,end_ip_address " "(e.g., 192.168.1.100,192.168.1.120) " "and one entry per line."), required=False) dns_nameservers = forms.CharField( widget=forms.widgets.Textarea(attrs={'rows': 4}), label=_("DNS Name Servers"), help_text=_("IP address list of DNS name servers for this subnet. " "One entry per line."), required=False) host_routes = forms.CharField( widget=forms.widgets.Textarea(attrs={'rows': 4}), label=_("Host Routes"), help_text=_("Additional routes announced to the hosts. " "Each entry is: destination_cidr,nexthop " "(e.g., 192.168.200.0/24,10.56.1.254) " "and one entry per line."), required=False) class Meta(object): name = _("Subnet Details") help_text = _('Specify additional attributes for the subnet.') def __init__(self, request, context, *args, **kwargs): super().__init__(request, context, *args, **kwargs) if not setting_utils.get_dict_config('OPENSTACK_NEUTRON_NETWORK', 'enable_ipv6'): self.fields['ipv6_modes'].widget = forms.HiddenInput() def populate_ipv6_modes_choices(self, request, context): return [(value, _("%s (Default)") % label) if value == utils.IPV6_DEFAULT_MODE else (value, label) for value, label in utils.IPV6_MODE_CHOICES] def _convert_ip_address(self, ip, field_name): try: return netaddr.IPAddress(ip) except (netaddr.AddrFormatError, ValueError): msg = (_('%(field_name)s: Invalid IP address (value=%(ip)s)') % { 'field_name': field_name, 'ip': ip }) raise forms.ValidationError(msg) def _convert_ip_network(self, network, field_name): try: return netaddr.IPNetwork(network) except (netaddr.AddrFormatError, ValueError): msg = ( _('%(field_name)s: Invalid IP address (value=%(network)s)') % { 'field_name': field_name, 'network': network }) raise forms.ValidationError(msg) def _check_allocation_pools(self, allocation_pools): for p in allocation_pools.splitlines(): p = p.strip() if not p: continue pool = p.split(',') if len(pool) != 2: msg = _('Start and end addresses must be specified ' '(value=%s)') % p raise forms.ValidationError(msg) start, end = [ self._convert_ip_address(ip, "allocation_pools") for ip in pool ] if start > end: msg = _('Start address is larger than end address ' '(value=%s)') % p raise forms.ValidationError(msg) def _check_dns_nameservers(self, dns_nameservers): for ns in dns_nameservers.splitlines(): ns = ns.strip() if not ns: continue self._convert_ip_address(ns, "dns_nameservers") def _check_host_routes(self, host_routes): for r in host_routes.splitlines(): r = r.strip() if not r: continue route = r.split(',') if len(route) != 2: msg = _('Host Routes format error: ' 'Destination CIDR and nexthop must be specified ' '(value=%s)') % r raise forms.ValidationError(msg) self._convert_ip_network(route[0], "host_routes") self._convert_ip_address(route[1], "host_routes") def clean(self): cleaned_data = super().clean() self._check_allocation_pools(cleaned_data.get('allocation_pools')) self._check_host_routes(cleaned_data.get('host_routes')) self._check_dns_nameservers(cleaned_data.get('dns_nameservers')) return cleaned_data
class SetInstanceDetailsAction(workflows.Action): availability_zone = forms.ThemableChoiceField(label=_("Availability Zone"), required=False) name = forms.CharField(label=_("Instance Name"), max_length=255) flavor = forms.ThemableChoiceField(label=_("Flavor"), help_text=_("Size of image to launch.")) count = forms.IntegerField(label=_("Number of Instances"), min_value=1, initial=1) source_type = forms.ThemableChoiceField(label=_("Instance Boot Source"), help_text=_( "Choose Your Boot Source " "Type.")) instance_snapshot_id = forms.ThemableChoiceField( label=_("Instance Snapshot"), required=False) volume_id = forms.ThemableChoiceField(label=_("Volume"), required=False) volume_snapshot_id = forms.ThemableChoiceField(label=_("Volume Snapshot"), required=False) image_id = forms.ChoiceField( label=_("Image Name"), required=False, widget=forms.ThemableSelectWidget(data_attrs=('volume_size', ), transform=lambda x: ("%s (%s)" % (x.name, filesizeformat(x.bytes))))) volume_size = forms.IntegerField(label=_("Device size (GB)"), initial=1, min_value=0, required=False, help_text=_("Volume size in gigabytes " "(integer value).")) device_name = forms.CharField(label=_("Device Name"), required=False, initial="vda", help_text=_("Volume mount point (e.g. 'vda' " "mounts at '/dev/vda'). Leave " "this field blank to let the " "system choose a device name " "for you.")) vol_delete_on_instance_delete = forms.BooleanField( label=_("Delete Volume on Instance Delete"), initial=False, required=False, help_text=_("Delete volume when the instance is deleted")) class Meta(object): name = _("Details") help_text_template = ("project/instances/" "_launch_details_help.html") def __init__(self, request, context, *args, **kwargs): self._init_images_cache() self.request = request self.context = context super(SetInstanceDetailsAction, self).__init__(request, context, *args, **kwargs) # Hide the device field if the hypervisor doesn't support it. if not nova.can_set_mount_point(): self.fields['device_name'].widget = forms.widgets.HiddenInput() source_type_choices = [ ('', _("Select source")), ("image_id", _("Boot from image")), ("instance_snapshot_id", _("Boot from snapshot")), ] if cinder.is_volume_service_enabled(request): source_type_choices.append(("volume_id", _("Boot from volume"))) try: if api.nova.extension_supported("BlockDeviceMappingV2Boot", request): source_type_choices.append( ("volume_image_id", _("Boot from image (creates a new volume)"))) except Exception: exceptions.handle( request, _('Unable to retrieve extensions ' 'information.')) source_type_choices.append( ("volume_snapshot_id", _("Boot from volume snapshot (creates a new volume)"))) self.fields['source_type'].choices = source_type_choices @memoized.memoized_method def _get_flavor(self, flavor_id): try: # We want to retrieve details for a given flavor, # however flavor_list uses a memoized decorator # so it is used instead of flavor_get to reduce the number # of API calls. flavors = instance_utils.flavor_list(self.request) flavor = [x for x in flavors if x.id == flavor_id][0] except IndexError: flavor = None return flavor @memoized.memoized_method def _get_image(self, image_id): try: # We want to retrieve details for a given image, # however get_available_images uses a cache of image list, # so it is used instead of image_get to reduce the number # of API calls. images = image_utils.get_available_images( self.request, self.context.get('project_id'), self._images_cache) image = [x for x in images if x.id == image_id][0] except IndexError: image = None return image def _check_quotas(self, cleaned_data): count = cleaned_data.get('count', 1) # Prevent launching more instances than the quota allows usages = quotas.tenant_quota_usages(self.request) available_count = usages['instances']['available'] if available_count < count: error_message = ungettext_lazy( 'The requested instance cannot be launched as you only ' 'have %(avail)i of your quota available. ', 'The requested %(req)i instances cannot be launched as you ' 'only have %(avail)i of your quota available.', count) params = {'req': count, 'avail': available_count} raise forms.ValidationError(error_message % params) source_type = cleaned_data.get('source_type') if source_type in ('volume_image_id', 'volume_snapshot_id'): available_volume = usages['volumes']['available'] if available_volume < count: msg = (_('The requested instance cannot be launched. ' 'Requested volume exceeds quota: Available: ' '%(avail)s, Requested: %(req)s.') % { 'avail': available_volume, 'req': count }) raise forms.ValidationError(msg) flavor_id = cleaned_data.get('flavor') flavor = self._get_flavor(flavor_id) count_error = [] # Validate cores and ram. available_cores = usages['cores']['available'] if flavor and available_cores < count * flavor.vcpus: count_error.append( _("Cores(Available: %(avail)s, " "Requested: %(req)s)") % { 'avail': available_cores, 'req': count * flavor.vcpus }) available_ram = usages['ram']['available'] if flavor and available_ram < count * flavor.ram: count_error.append( _("RAM(Available: %(avail)s, " "Requested: %(req)s)") % { 'avail': available_ram, 'req': count * flavor.ram }) if count_error: value_str = ", ".join(count_error) msg = (_('The requested instance cannot be launched. ' 'The following requested resource(s) exceed ' 'quota(s): %s.') % value_str) if count == 1: self._errors['flavor'] = self.error_class([msg]) else: self._errors['count'] = self.error_class([msg]) def _check_flavor_for_image(self, cleaned_data): # Prevents trying to launch an image needing more resources. image_id = cleaned_data.get('image_id') image = self._get_image(image_id) flavor_id = cleaned_data.get('flavor') flavor = self._get_flavor(flavor_id) if not image or not flavor: return props_mapping = (("min_ram", "ram"), ("min_disk", "disk")) for iprop, fprop in props_mapping: if getattr(image, iprop) > 0 and \ getattr(image, iprop) > getattr(flavor, fprop): msg = (_("The flavor '%(flavor)s' is too small " "for requested image.\n" "Minimum requirements: " "%(min_ram)s MB of RAM and " "%(min_disk)s GB of Root Disk.") % { 'flavor': flavor.name, 'min_ram': image.min_ram, 'min_disk': image.min_disk }) self._errors['image_id'] = self.error_class([msg]) break # Not necessary to continue the tests. def _check_volume_for_image(self, cleaned_data): image_id = cleaned_data.get('image_id') image = self._get_image(image_id) volume_size = cleaned_data.get('volume_size') if not image or not volume_size: return volume_size = int(volume_size) img_gigs = functions.bytes_to_gigabytes(image.size) smallest_size = max(img_gigs, image.min_disk) if volume_size < smallest_size: msg = (_("The Volume size is too small for the" " '%(image_name)s' image and has to be" " greater than or equal to " "'%(smallest_size)d' GB.") % { 'image_name': image.name, 'smallest_size': smallest_size }) self._errors['volume_size'] = self.error_class([msg]) def _check_source_image(self, cleaned_data): if not cleaned_data.get('image_id'): msg = _("You must select an image.") self._errors['image_id'] = self.error_class([msg]) else: self._check_flavor_for_image(cleaned_data) def _check_source_volume_image(self, cleaned_data): volume_size = self.data.get('volume_size', None) if not volume_size: msg = _("You must set volume size") self._errors['volume_size'] = self.error_class([msg]) if float(volume_size) <= 0: msg = _("Volume size must be greater than 0") self._errors['volume_size'] = self.error_class([msg]) if not cleaned_data.get('image_id'): msg = _("You must select an image.") self._errors['image_id'] = self.error_class([msg]) return else: self._check_flavor_for_image(cleaned_data) self._check_volume_for_image(cleaned_data) def _check_source_instance_snapshot(self, cleaned_data): # using the array form of get blows up with KeyError # if instance_snapshot_id is nil if not cleaned_data.get('instance_snapshot_id'): msg = _("You must select a snapshot.") self._errors['instance_snapshot_id'] = self.error_class([msg]) def _check_source_volume(self, cleaned_data): if not cleaned_data.get('volume_id'): msg = _("You must select a volume.") self._errors['volume_id'] = self.error_class([msg]) # Prevent launching multiple instances with the same volume. # TODO(gabriel): is it safe to launch multiple instances with # a snapshot since it should be cloned to new volumes? count = cleaned_data.get('count', 1) if count > 1: msg = _('Launching multiple instances is only supported for ' 'images and instance snapshots.') raise forms.ValidationError(msg) def _check_source_volume_snapshot(self, cleaned_data): if not cleaned_data.get('volume_snapshot_id'): msg = _("You must select a snapshot.") self._errors['volume_snapshot_id'] = self.error_class([msg]) def _check_source(self, cleaned_data): # Validate our instance source. source_type = self.data.get('source_type', None) source_check_methods = { 'image_id': self._check_source_image, 'volume_image_id': self._check_source_volume_image, 'instance_snapshot_id': self._check_source_instance_snapshot, 'volume_id': self._check_source_volume, 'volume_snapshot_id': self._check_source_volume_snapshot } check_method = source_check_methods.get(source_type) if check_method: check_method(cleaned_data) def clean(self): cleaned_data = super(SetInstanceDetailsAction, self).clean() self._check_quotas(cleaned_data) self._check_source(cleaned_data) return cleaned_data def populate_flavor_choices(self, request, context): return instance_utils.flavor_field_data(request, False) def populate_availability_zone_choices(self, request, context): try: zones = api.nova.availability_zone_list(request) except Exception: 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 not zone_list: zone_list.insert(0, ("", _("No availability zones found"))) elif len(zone_list) > 1: zone_list.insert(0, ("", _("Any Availability Zone"))) return zone_list def get_help_text(self, extra_context=None): extra = {} if extra_context is None else dict(extra_context) try: extra['usages'] = api.nova.tenant_absolute_limits(self.request, reserved=True) extra['usages_json'] = json.dumps(extra['usages']) flavors = json.dumps( [f._info for f in instance_utils.flavor_list(self.request)]) extra['flavors'] = flavors images = image_utils.get_available_images( self.request, self.initial['project_id'], self._images_cache) if images is not None: attrs = [{ 'id': i.id, 'min_disk': getattr(i, 'min_disk', 0), 'min_ram': getattr(i, 'min_ram', 0), 'size': functions.bytes_to_gigabytes(i.size) } for i in images] extra['images'] = json.dumps(attrs) except Exception: exceptions.handle(self.request, _("Unable to retrieve quota information.")) return super(SetInstanceDetailsAction, self).get_help_text(extra) def _init_images_cache(self): if not hasattr(self, '_images_cache'): self._images_cache = {} def _get_volume_display_name(self, volume): if hasattr(volume, "volume_id"): vol_type = "snap" visible_label = _("Snapshot") else: vol_type = "vol" visible_label = _("Volume") return (("%s:%s" % (volume.id, vol_type)), (_("%(name)s - %(size)s GB (%(label)s)") % { 'name': volume.name, 'size': volume.size, 'label': visible_label })) def populate_image_id_choices(self, request, context): choices = [] images = image_utils.get_available_images(request, context.get('project_id'), self._images_cache) for image in images: image.bytes = getattr(image, 'virtual_size', None) or image.size image.volume_size = max(image.min_disk, functions.bytes_to_gigabytes(image.bytes)) choices.append((image.id, image)) if context.get('image_id') == image.id and \ 'volume_size' not in context: context['volume_size'] = image.volume_size if choices: choices.sort(key=lambda c: c[1].name or '') choices.insert(0, ("", _("Select Image"))) else: choices.insert(0, ("", _("No images available"))) return choices def populate_instance_snapshot_id_choices(self, request, context): images = image_utils.get_available_images(request, context.get('project_id'), self._images_cache) choices = [(image.id, image.name) for image in images if image.properties.get("image_type", '') == "snapshot"] if choices: choices.sort(key=operator.itemgetter(1)) choices.insert(0, ("", _("Select Instance Snapshot"))) else: choices.insert(0, ("", _("No snapshots available"))) return choices def populate_volume_id_choices(self, request, context): volumes = [] try: if cinder.is_volume_service_enabled(request): available = api.cinder.VOLUME_STATE_AVAILABLE volumes = [ self._get_volume_display_name(v) for v in cinder.volume_list( self.request, search_opts=dict(status=available, bootable=True)) ] except Exception: exceptions.handle(self.request, _('Unable to retrieve list of volumes.')) if volumes: volumes.insert(0, ("", _("Select Volume"))) else: volumes.insert(0, ("", _("No volumes available"))) return volumes def populate_volume_snapshot_id_choices(self, request, context): snapshots = [] try: if cinder.is_volume_service_enabled(request): available = api.cinder.VOLUME_STATE_AVAILABLE snapshots = [ self._get_volume_display_name(s) for s in cinder.volume_snapshot_list( self.request, search_opts=dict(status=available)) ] except Exception: exceptions.handle( self.request, _('Unable to retrieve list of volume ' 'snapshots.')) if snapshots: snapshots.insert(0, ("", _("Select Volume Snapshot"))) else: snapshots.insert(0, ("", _("No volume snapshots available"))) return snapshots
class CreateImageForm(CreateParent): name = forms.CharField(max_length=255, label=_("Name")) description = forms.CharField(max_length=255, widget=forms.Textarea(attrs={'rows': 4}), label=_("Description"), required=False) source_type = forms.ChoiceField( label=_('Image Source'), required=False, choices=[('url', _('Image Location')), ('file', _('Image File'))], widget=forms.Select(attrs={ 'class': 'switchable', 'data-slug': 'source' })) image_url_attrs = { 'class': 'switched', 'data-switch-on': 'source', 'data-source-url': _('Image Location'), 'ng-model': 'ctrl.copyFrom', 'ng-change': 'ctrl.selectImageFormat(ctrl.copyFrom)', 'placeholder': 'http://example.com/image.img' } image_url = ImageURLField(label=_("Image Location"), help_text=_("An external (HTTP/HTTPS) URL to " "load the image from."), widget=forms.TextInput(attrs=image_url_attrs), required=False) image_attrs = { 'class': 'switched', 'data-switch-on': 'source', 'data-source-file': _('Image File'), 'ng-model': 'ctrl.imageFile', 'ng-change': 'ctrl.selectImageFormat(ctrl.imageFile.name)', 'image-file-on-change': None } image_file = FileField(label=_("Image File"), help_text=_("A local image to upload."), widget=forms.FileInput(attrs=image_attrs), required=False) kernel = forms.ChoiceField( label=_('Kernel'), required=False, widget=forms.ThemableSelectWidget(transform=lambda x: "%s (%s)" % ( x.name, defaultfilters.filesizeformat(x.size)))) ramdisk = forms.ChoiceField( label=_('Ramdisk'), required=False, widget=forms.ThemableSelectWidget(transform=lambda x: "%s (%s)" % ( x.name, defaultfilters.filesizeformat(x.size)))) disk_format = forms.ChoiceField( label=_('Format'), choices=[], widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'ng-model': 'ctrl.diskFormat' })) architecture = forms.CharField( max_length=255, label=_("Architecture"), help_text=_('CPU architecture of the image.'), required=False) minimum_disk = forms.IntegerField( label=_("Minimum Disk (GB)"), min_value=0, help_text=_('The minimum disk size required to boot the image. ' 'If unspecified, this value defaults to 0 (no minimum).'), required=False) minimum_ram = forms.IntegerField( label=_("Minimum RAM (MB)"), min_value=0, help_text=_('The minimum memory size required to boot the image. ' 'If unspecified, this value defaults to 0 (no minimum).'), required=False) is_copying = forms.BooleanField( label=_("Copy Data"), initial=True, required=False, help_text=_('Specify this option to copy image data to the image ' 'service. If unspecified, image data will be used in its ' 'current location.'), widget=forms.CheckboxInput( attrs={ 'class': 'switched', 'data-source-url': _('Image Location'), 'data-switch-on': 'source' })) is_public = forms.BooleanField( label=_("Public"), help_text=_('Make the image visible across projects.'), required=False) protected = forms.BooleanField( label=_("Protected"), help_text=_('Prevent the deletion of the image.'), required=False) def __init__(self, request, *args, **kwargs): super(CreateImageForm, self).__init__(request, *args, **kwargs) if (api.glance.get_image_upload_mode() == 'off' or not policy.check( (("image", "upload_image"), ), request)): self._hide_file_source_type() if not policy.check((("image", "set_image_location"), ), request): self._hide_url_source_type() # GlanceV2 feature removals if api.glance.VERSIONS.active >= 2: # NOTE: GlanceV2 doesn't support copy-from feature, sorry! self._hide_is_copying() if not getattr(settings, 'IMAGES_ALLOW_LOCATION', False): self._hide_url_source_type() if (api.glance.get_image_upload_mode() == 'off' or not policy.check( (("image", "upload_image"), ), request)): # Neither setting a location nor uploading image data is # allowed, so throw an error. msg = _('The current Horizon settings indicate no valid ' 'image creation methods are available. Providing ' 'an image location and/or uploading from the ' 'local file system must be allowed to support ' 'image creation.') messages.error(request, msg) raise ValidationError(msg) if not policy.check((("image", "publicize_image"), ), request): self._hide_is_public() self.fields['disk_format'].choices = IMAGE_FORMAT_CHOICES try: kernel_images = api.glance.image_list_detailed( request, filters={'disk_format': 'aki'})[0] except Exception: kernel_images = [] msg = _('Unable to retrieve image list.') messages.error(request, msg) if kernel_images: choices = [('', _("Choose an image"))] for image in kernel_images: choices.append((image.id, image)) self.fields['kernel'].choices = choices else: del self.fields['kernel'] try: ramdisk_images = api.glance.image_list_detailed( request, filters={'disk_format': 'ari'})[0] except Exception: ramdisk_images = [] msg = _('Unable to retrieve image list.') messages.error(request, msg) if ramdisk_images: choices = [('', _("Choose an image"))] for image in ramdisk_images: choices.append((image.id, image)) self.fields['ramdisk'].choices = choices else: del self.fields['ramdisk'] def _hide_file_source_type(self): self.fields['image_file'].widget = HiddenInput() source_type = self.fields['source_type'] source_type.choices = [ choice for choice in source_type.choices if choice[0] != 'file' ] if len(source_type.choices) == 1: source_type.widget = HiddenInput() def _hide_url_source_type(self): self.fields['image_url'].widget = HiddenInput() source_type = self.fields['source_type'] source_type.choices = [ choice for choice in source_type.choices if choice[0] != 'url' ] if len(source_type.choices) == 1: source_type.widget = HiddenInput() def _hide_is_public(self): self.fields['is_public'].widget = HiddenInput() self.fields['is_public'].initial = False def _hide_is_copying(self): self.fields['is_copying'].widget = HiddenInput() self.fields['is_copying'].initial = False def clean(self): data = super(CreateImageForm, self).clean() # The image_file key can be missing based on particular upload # conditions. Code defensively for it here... source_type = data.get('source_type', None) image_file = data.get('image_file', None) image_url = data.get('image_url', None) if not image_url and not image_file: msg = _("An image file or an external location must be specified.") if source_type == 'file': raise ValidationError({'image_file': [ msg, ]}) else: raise ValidationError({'image_url': [ msg, ]}) else: return data def handle(self, request, data): meta = create_image_metadata(data) # Add image source file or URL to metadata if (api.glance.get_image_upload_mode() != 'off' and policy.check( (("image", "upload_image"), ), request) and data.get('image_file', None)): meta['data'] = data['image_file'] elif data.get('is_copying'): meta['copy_from'] = data['image_url'] else: meta['location'] = data['image_url'] try: image = api.glance.image_create(request, **meta) messages.info( request, _('Your image %s has been queued for creation.') % meta['name']) return image except Exception as e: msg = _('Unable to create new image') # TODO(nikunj2512): Fix this once it is fixed in glance client if hasattr(e, 'code') and e.code == 400: if "Invalid disk format" in e.details: msg = _('Unable to create new image: Invalid disk format ' '%s for image.') % meta['disk_format'] elif "Image name too long" in e.details: msg = _('Unable to create new image: Image name too long.') elif "not supported" in e.details: msg = _('Unable to create new image: URL scheme not ' 'supported.') exceptions.handle(request, msg) return False
class CreateForm(forms.SelfHandlingForm): # General fields name = forms.CharField(label=_("Lease Name"), required=True, max_length=80) start_date = forms.DateTimeField( label=_("Start Date"), required=False, help_text=_('Enter YYYY-MM-DD HH:MM or blank for now'), input_formats=['%Y-%m-%d %H:%M'], widget=forms.DateTimeInput( attrs={'placeholder': 'YYYY-MM-DD HH:MM (blank for now)'})) end_date = forms.DateTimeField( label=_("End Date"), required=False, help_text=_('Enter YYYY-MM-DD HH:MM or blank for Start Date + 24h'), input_formats=['%Y-%m-%d %H:%M'], widget=forms.DateTimeInput( attrs={ 'placeholder': 'YYYY-MM-DD HH:MM (blank for Start Date + ' '24h)' })) resource_type = forms.ChoiceField( label=_("Resource Type"), required=True, choices=(('host', _('Physical Host')), ('instance', _('Virtual Instance'))), widget=forms.ThemableSelectWidget(attrs={ 'class': 'switchable', 'data-slug': 'source' })) # Fields for host reservation min_hosts = forms.IntegerField( label=_('Minimum Number of Hosts'), required=False, help_text=_('Enter the minimum number of hosts to reserve.'), min_value=1, initial=1, widget=forms.NumberInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-host': _('Minimum Number of Hosts') })) max_hosts = forms.IntegerField( label=_('Maximum Number of Hosts'), required=False, help_text=_('Enter the maximum number of hosts to reserve.'), min_value=1, initial=1, widget=forms.NumberInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-host': _('Maximum Number of Hosts') })) hypervisor_properties = forms.CharField( label=_("Hypervisor Properties"), required=False, help_text=_('Enter properties of a hypervisor to reserve.'), max_length=255, widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-host': _('Hypervisor Properties'), 'placeholder': 'e.g. [">=", "$vcpus", "2"]' })) # Fields for instance reservation amount = forms.IntegerField( label=_('Instance Count'), required=False, help_text=_('Enter the number of instances to reserve.'), min_value=1, initial=1, widget=forms.NumberInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-instance': _('Instance Count') })) vcpus = forms.IntegerField( label=_('Number of VCPUs'), required=False, help_text=_('Enter the number of VCPUs per instance.'), min_value=1, initial=1, widget=forms.NumberInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-instance': _('Number of VCPUs') })) memory_mb = forms.IntegerField( label=_('RAM (MB)'), required=False, help_text=_('Enter the size of RAM (MB) per instance'), min_value=1, initial=1, widget=forms.NumberInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-instance': _('RAM (MB)') })) disk_gb = forms.IntegerField( label=_('Root Disk (GB)'), required=False, help_text=_('Enter the root disk size (GB) per instance'), min_value=0, initial=0, widget=forms.NumberInput( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-instance': _('Root Disk (GB)') })) affinity = forms.ChoiceField( label=_("Affinity Rule"), required=False, choices=( (None, _('None')), (True, _('Affinity')), (False, _('Anti-Affinity')), ), initial=None, widget=forms.ThemableSelectWidget( attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-instance': _('Affinity Rule') })) # Fields for both of host and instance reservations resource_properties = forms.CharField( label=_("Resource Properties"), required=False, help_text=_('Enter properties of a resource to reserve.'), max_length=255, widget=forms.TextInput( attrs={'placeholder': 'e.g. ["==", "$extra_key", "extra_value"]'})) def handle(self, request, data): if data['resource_type'] == 'host': reservations = [{ 'resource_type': 'physical:host', 'min': data['min_hosts'], 'max': data['max_hosts'], 'hypervisor_properties': (data['hypervisor_properties'] or ''), 'resource_properties': data['resource_properties'] or '' }] elif data['resource_type'] == 'instance': reservations = [{ 'resource_type': 'virtual:instance', 'amount': data['amount'], 'vcpus': data['vcpus'], 'memory_mb': data['memory_mb'], 'disk_gb': data['disk_gb'], 'affinity': data['affinity'], 'resource_properties': data['resource_properties'] or '' }] events = [] try: api.client.lease_create( request, data['name'], data['start_date'].strftime('%Y-%m-%d %H:%M'), data['end_date'].strftime('%Y-%m-%d %H:%M'), reservations, events) messages.success( request, _('Lease %s was successfully ' 'created.') % data['name']) return True except Exception as e: LOG.error('Error submitting lease: %s', e) exceptions.handle(request, message='An error occurred while creating this ' 'lease: %s. Please try again.' % e) def clean(self): cleaned_data = super(CreateForm, self).clean() local = timezone( self.request.session.get( 'django_timezone', self.request.COOKIES.get('django_timezone', 'UTC'))) if cleaned_data['start_date']: cleaned_data['start_date'] = local.localize( cleaned_data['start_date'].replace(tzinfo=None)).astimezone( timezone('UTC')) else: cleaned_data['start_date'] = datetime.datetime.utcnow() if cleaned_data['end_date']: cleaned_data['end_date'] = local.localize( cleaned_data['end_date'].replace(tzinfo=None)).astimezone( timezone('UTC')) else: cleaned_data['end_date'] = (cleaned_data['start_date'] + datetime.timedelta(days=1))