Added initial project for horizon dashboard
This commit is contained in:
parent
c138dd8f40
commit
3b0f8570de
30
dashboard/ReadMe.txt
Normal file
30
dashboard/ReadMe.txt
Normal file
@ -0,0 +1,30 @@
|
||||
# TO DO:
|
||||
# 1. Fix issue with Create button
|
||||
# 2. Create simple form for Windows Data Center deploy
|
||||
# 3. Remove extra code
|
||||
#
|
||||
|
||||
This file is described how to install new tab on horizon dashboard.
|
||||
We should do the following:
|
||||
1. Copy directory 'windc' to directory '/opt/stack/horizon/openstack_dashboard/dashboards/project'
|
||||
2. Edit file '/opt/stack/horizon/openstack_dashboard/dashboards/project/dashboard.py'
|
||||
Add line with windc project:
|
||||
|
||||
...
|
||||
class BasePanels(horizon.PanelGroup):
|
||||
slug = "compute"
|
||||
name = _("Manage Compute")
|
||||
panels = ('overview',
|
||||
'instances',
|
||||
'volumes',
|
||||
'images_and_snapshots',
|
||||
'access_and_security',
|
||||
'networks',
|
||||
'routers',
|
||||
'windc')
|
||||
|
||||
...
|
||||
|
||||
3. Run the test Django server:
|
||||
cd /opt/stack/horizon
|
||||
python manage.py runserver 67.207.197.36:8080
|
0
dashboard/windc/__init__.py
Normal file
0
dashboard/windc/__init__.py
Normal file
52
dashboard/windc/forms.py
Normal file
52
dashboard/windc/forms.py
Normal file
@ -0,0 +1,52 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateInstance(forms.SelfHandlingForm):
|
||||
tenant_id = forms.CharField(widget=forms.HiddenInput)
|
||||
instance = forms.CharField(widget=forms.HiddenInput)
|
||||
name = forms.CharField(required=True)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
server = api.nova.server_update(request, data['instance'],
|
||||
data['name'])
|
||||
messages.success(request,
|
||||
_('Instance "%s" updated.') % data['name'])
|
||||
return server
|
||||
except:
|
||||
redirect = reverse("horizon:project:instances:index")
|
||||
exceptions.handle(request,
|
||||
_('Unable to update instance.'),
|
||||
redirect=redirect)
|
29
dashboard/windc/panel.py
Normal file
29
dashboard/windc/panel.py
Normal file
@ -0,0 +1,29 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
from openstack_dashboard.dashboards.project import dashboard
|
||||
|
||||
|
||||
class WinDC(horizon.Panel):
|
||||
name = _("WinDC")
|
||||
slug = 'windc'
|
||||
|
||||
|
||||
dashboard.Project.register(WinDC)
|
278
dashboard/windc/tables.py
Normal file
278
dashboard/windc/tables.py
Normal file
@ -0,0 +1,278 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django import shortcuts
|
||||
from django import template
|
||||
from django.core import urlresolvers
|
||||
from django.template.defaultfilters import title
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import string_concat, ugettext_lazy as _
|
||||
|
||||
from horizon.conf import HORIZON_CONFIG
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
from horizon import tables
|
||||
from horizon.templatetags import sizeformat
|
||||
from horizon.utils.filters import replace_underscores
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.project.access_and_security \
|
||||
.floating_ips.workflows import IPAssociationWorkflow
|
||||
from .tabs import InstanceDetailTabs, LogTab, ConsoleTab
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
ACTIVE_STATES = ("ACTIVE",)
|
||||
|
||||
POWER_STATES = {
|
||||
0: "NO STATE",
|
||||
1: "RUNNING",
|
||||
2: "BLOCKED",
|
||||
3: "PAUSED",
|
||||
4: "SHUTDOWN",
|
||||
5: "SHUTOFF",
|
||||
6: "CRASHED",
|
||||
7: "SUSPENDED",
|
||||
8: "FAILED",
|
||||
9: "BUILDING",
|
||||
}
|
||||
|
||||
PAUSE = 0
|
||||
UNPAUSE = 1
|
||||
SUSPEND = 0
|
||||
RESUME = 1
|
||||
|
||||
|
||||
def is_deleting(instance):
|
||||
task_state = getattr(instance, "OS-EXT-STS:task_state", None)
|
||||
if not task_state:
|
||||
return False
|
||||
return task_state.lower() == "deleting"
|
||||
|
||||
|
||||
class RebootWinDC(tables.BatchAction):
|
||||
name = "reboot"
|
||||
action_present = _("Reboot")
|
||||
action_past = _("Rebooted")
|
||||
data_type_singular = _("Instance")
|
||||
data_type_plural = _("Instances")
|
||||
classes = ('btn-danger', 'btn-reboot')
|
||||
|
||||
def allowed(self, request, instance=None):
|
||||
return ((instance.status in ACTIVE_STATES
|
||||
or instance.status == 'SHUTOFF')
|
||||
and not is_deleting(instance))
|
||||
|
||||
def action(self, request, obj_id):
|
||||
api.nova.server_reboot(request, obj_id)
|
||||
|
||||
|
||||
class CreateWinDC(tables.LinkAction):
|
||||
name = "CreateWinDC"
|
||||
verbose_name = _("Create WinDC")
|
||||
url = "horizon:project:windc:create"
|
||||
classes = ("btn-launch", "ajax-modal")
|
||||
|
||||
def allowed(self, request, datum):
|
||||
try:
|
||||
limits = api.nova.tenant_absolute_limits(request, reserved=True)
|
||||
|
||||
instances_available = limits['maxTotalInstances'] \
|
||||
- limits['totalInstancesUsed']
|
||||
cores_available = limits['maxTotalCores'] \
|
||||
- limits['totalCoresUsed']
|
||||
ram_available = limits['maxTotalRAMSize'] - limits['totalRAMUsed']
|
||||
|
||||
if instances_available <= 0 or cores_available <= 0 \
|
||||
or ram_available <= 0:
|
||||
if "disabled" not in self.classes:
|
||||
self.classes = [c for c in self.classes] + ['disabled']
|
||||
self.verbose_name = string_concat(self.verbose_name, ' ',
|
||||
_("(Quota exceeded)"))
|
||||
else:
|
||||
self.verbose_name = _("Create WinDC")
|
||||
classes = [c for c in self.classes if c != "disabled"]
|
||||
self.classes = classes
|
||||
except:
|
||||
LOG.exception("Failed to retrieve quota information")
|
||||
# If we can't get the quota information, leave it to the
|
||||
# API to check when launching
|
||||
|
||||
return True # The action should always be displayed
|
||||
|
||||
|
||||
class DeleteWinDC(tables.BatchAction):
|
||||
name = "DeleteWinDC"
|
||||
action_present = _("DeleteWinDC")
|
||||
action_past = _("Scheduled termination of")
|
||||
data_type_singular = _("Instance")
|
||||
data_type_plural = _("Instances")
|
||||
classes = ('btn-danger', 'btn-terminate')
|
||||
|
||||
def allowed(self, request, instance=None):
|
||||
if instance:
|
||||
# FIXME(gabriel): This is true in Essex, but in FOLSOM an instance
|
||||
# can be terminated in any state. We should improve this error
|
||||
# handling when LP bug 1037241 is implemented.
|
||||
return instance.status not in ("PAUSED", "SUSPENDED")
|
||||
return True
|
||||
|
||||
def action(self, request, obj_id):
|
||||
api.nova.server_delete(request, obj_id)
|
||||
|
||||
|
||||
class EditWinDC(tables.LinkAction):
|
||||
name = "edit"
|
||||
verbose_name = _("Edit Instance")
|
||||
url = "horizon:project:instances:update"
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
|
||||
def allowed(self, request, instance):
|
||||
return not is_deleting(instance)
|
||||
|
||||
|
||||
class ConsoleLink(tables.LinkAction):
|
||||
name = "console"
|
||||
verbose_name = _("Console")
|
||||
url = "horizon:project:instances:detail"
|
||||
classes = ("btn-console",)
|
||||
|
||||
def allowed(self, request, instance=None):
|
||||
return instance.status in ACTIVE_STATES and not is_deleting(instance)
|
||||
|
||||
def get_link_url(self, datum):
|
||||
base_url = super(ConsoleLink, self).get_link_url(datum)
|
||||
tab_query_string = ConsoleTab(InstanceDetailTabs).get_query_string()
|
||||
return "?".join([base_url, tab_query_string])
|
||||
|
||||
|
||||
class LogLink(tables.LinkAction):
|
||||
name = "log"
|
||||
verbose_name = _("View Log")
|
||||
url = "horizon:project:instances:detail"
|
||||
classes = ("btn-log",)
|
||||
|
||||
def allowed(self, request, instance=None):
|
||||
return instance.status in ACTIVE_STATES and not is_deleting(instance)
|
||||
|
||||
def get_link_url(self, datum):
|
||||
base_url = super(LogLink, self).get_link_url(datum)
|
||||
tab_query_string = LogTab(InstanceDetailTabs).get_query_string()
|
||||
return "?".join([base_url, tab_query_string])
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, instance_id):
|
||||
instance = api.nova.server_get(request, instance_id)
|
||||
instance.full_flavor = api.nova.flavor_get(request,
|
||||
instance.flavor["id"])
|
||||
return instance
|
||||
|
||||
|
||||
def get_ips(instance):
|
||||
template_name = 'project/instances/_instance_ips.html'
|
||||
context = {"instance": instance}
|
||||
return template.loader.render_to_string(template_name, context)
|
||||
|
||||
|
||||
def get_size(instance):
|
||||
if hasattr(instance, "full_flavor"):
|
||||
size_string = _("%(name)s | %(RAM)s RAM | %(VCPU)s VCPU "
|
||||
"| %(disk)s Disk")
|
||||
vals = {'name': instance.full_flavor.name,
|
||||
'RAM': sizeformat.mbformat(instance.full_flavor.ram),
|
||||
'VCPU': instance.full_flavor.vcpus,
|
||||
'disk': sizeformat.diskgbformat(instance.full_flavor.disk)}
|
||||
return size_string % vals
|
||||
return _("Not available")
|
||||
|
||||
|
||||
def get_keyname(instance):
|
||||
if hasattr(instance, "key_name"):
|
||||
keyname = instance.key_name
|
||||
return keyname
|
||||
return _("Not available")
|
||||
|
||||
|
||||
def get_power_state(instance):
|
||||
return POWER_STATES.get(getattr(instance, "OS-EXT-STS:power_state", 0), '')
|
||||
|
||||
|
||||
STATUS_DISPLAY_CHOICES = (
|
||||
("resize", "Resize/Migrate"),
|
||||
("verify_resize", "Confirm or Revert Resize/Migrate"),
|
||||
("revert_resize", "Revert Resize/Migrate"),
|
||||
)
|
||||
|
||||
|
||||
TASK_DISPLAY_CHOICES = (
|
||||
("image_snapshot", "Snapshotting"),
|
||||
("resize_prep", "Preparing Resize or Migrate"),
|
||||
("resize_migrating", "Resizing or Migrating"),
|
||||
("resize_migrated", "Resized or Migrated"),
|
||||
("resize_finish", "Finishing Resize or Migrate"),
|
||||
("resize_confirming", "Confirming Resize or Nigrate"),
|
||||
("resize_reverting", "Reverting Resize or Migrate"),
|
||||
("unpausing", "Resuming"),
|
||||
)
|
||||
|
||||
|
||||
class WinDCTable(tables.DataTable):
|
||||
TASK_STATUS_CHOICES = (
|
||||
(None, True),
|
||||
("none", True)
|
||||
)
|
||||
STATUS_CHOICES = (
|
||||
("active", True),
|
||||
("shutoff", True),
|
||||
("error", False),
|
||||
)
|
||||
name = tables.Column("name",
|
||||
link=("horizon:project:instances:detail"),
|
||||
verbose_name=_("WinDC Instance Name"))
|
||||
ip = tables.Column(get_ips, verbose_name=_("IP Address"))
|
||||
size = tables.Column(get_size,
|
||||
verbose_name=_("Type"),
|
||||
attrs={'data-type': 'type'})
|
||||
keypair = tables.Column(get_keyname, verbose_name=_("Keypair"))
|
||||
status = tables.Column("status",
|
||||
filters=(title, replace_underscores),
|
||||
verbose_name=_("Status"),
|
||||
status=True,
|
||||
status_choices=STATUS_CHOICES,
|
||||
display_choices=STATUS_DISPLAY_CHOICES)
|
||||
task = tables.Column("OS-EXT-STS:task_state",
|
||||
verbose_name=_("Task"),
|
||||
filters=(title, replace_underscores),
|
||||
status=True,
|
||||
status_choices=TASK_STATUS_CHOICES,
|
||||
display_choices=TASK_DISPLAY_CHOICES)
|
||||
state = tables.Column(get_power_state,
|
||||
filters=(title, replace_underscores),
|
||||
verbose_name=_("Power State"))
|
||||
|
||||
class Meta:
|
||||
name = "windc"
|
||||
verbose_name = _("WinDC")
|
||||
status_columns = ["status", "task"]
|
||||
row_class = UpdateRow
|
||||
table_actions = (CreateWinDC, DeleteWinDC)
|
||||
row_actions = (EditWinDC, ConsoleLink, LogLink, RebootWinDC)
|
85
dashboard/windc/tabs.py
Normal file
85
dashboard/windc/tabs.py
Normal file
@ -0,0 +1,85 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
class OverviewTab(tabs.Tab):
|
||||
name = _("Overview")
|
||||
slug = "overview"
|
||||
template_name = ("project/instances/"
|
||||
"_detail_overview.html")
|
||||
|
||||
def get_context_data(self, request):
|
||||
return {"instance": self.tab_group.kwargs['instance']}
|
||||
|
||||
|
||||
class LogTab(tabs.Tab):
|
||||
name = _("Log")
|
||||
slug = "log"
|
||||
template_name = "project/instances/_detail_log.html"
|
||||
preload = False
|
||||
|
||||
def get_context_data(self, request):
|
||||
instance = self.tab_group.kwargs['instance']
|
||||
try:
|
||||
data = api.nova.server_console_output(request,
|
||||
instance.id,
|
||||
tail_length=35)
|
||||
except:
|
||||
data = _('Unable to get log for instance "%s".') % instance.id
|
||||
exceptions.handle(request, ignore=True)
|
||||
return {"instance": instance,
|
||||
"console_log": data}
|
||||
|
||||
|
||||
class ConsoleTab(tabs.Tab):
|
||||
name = _("Console")
|
||||
slug = "console"
|
||||
template_name = "project/instances/_detail_console.html"
|
||||
preload = False
|
||||
|
||||
def get_context_data(self, request):
|
||||
instance = self.tab_group.kwargs['instance']
|
||||
# Currently prefer VNC over SPICE, since noVNC has had much more
|
||||
# testing than spice-html5
|
||||
try:
|
||||
console = api.nova.server_vnc_console(request, instance.id)
|
||||
console_url = "%s&title=%s(%s)" % (
|
||||
console.url,
|
||||
getattr(instance, "name", ""),
|
||||
instance.id)
|
||||
except:
|
||||
try:
|
||||
console = api.nova.server_spice_console(request, instance.id)
|
||||
console_url = "%s&title=%s(%s)" % (
|
||||
console.url,
|
||||
getattr(instance, "name", ""),
|
||||
instance.id)
|
||||
except:
|
||||
console_url = None
|
||||
return {'console_url': console_url, 'instance_id': instance.id}
|
||||
|
||||
|
||||
class InstanceDetailTabs(tabs.TabGroup):
|
||||
slug = "instance_details"
|
||||
tabs = (OverviewTab, LogTab, ConsoleTab)
|
||||
sticky = True
|
11
dashboard/windc/templates/windc/create.html
Normal file
11
dashboard/windc/templates/windc/create.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create WinDC" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("CreateWinDC") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'horizon/common/_workflow.html' %}
|
||||
{% endblock %}
|
11
dashboard/windc/templates/windc/index.html
Normal file
11
dashboard/windc/templates/windc/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "WinDC" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("WinDC") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{{ table.render }}
|
||||
{% endblock %}
|
31
dashboard/windc/urls.py
Normal file
31
dashboard/windc/urls.py
Normal file
@ -0,0 +1,31 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from .views import IndexView, CreateWinDCView
|
||||
|
||||
|
||||
VIEW_MOD = 'openstack_dashboard.dashboards.project.windc.views'
|
||||
|
||||
urlpatterns = patterns(VIEW_MOD,
|
||||
url(r'^$', IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', CreateWinDCView.as_view(), name='CreateWinDC')
|
||||
)
|
95
dashboard/windc/views.py
Normal file
95
dashboard/windc/views.py
Normal file
@ -0,0 +1,95 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Views for managing instances.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django import http
|
||||
from django import shortcuts
|
||||
from django.core.urlresolvers import reverse, reverse_lazy
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tabs
|
||||
from horizon import tables
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
from .forms import UpdateInstance
|
||||
from .tabs import InstanceDetailTabs
|
||||
from .tables import WinDCTable
|
||||
from .workflows import CreateWinDC
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
table_class = WinDCTable
|
||||
template_name = 'project/windc/index.html'
|
||||
|
||||
def get_data(self):
|
||||
# Gather our instances
|
||||
try:
|
||||
instances = api.nova.server_list(self.request)
|
||||
except:
|
||||
instances = []
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve instances.'))
|
||||
# Gather our flavors and correlate our instances to them
|
||||
if instances:
|
||||
try:
|
||||
flavors = api.nova.flavor_list(self.request)
|
||||
except:
|
||||
flavors = []
|
||||
exceptions.handle(self.request, ignore=True)
|
||||
|
||||
full_flavors = SortedDict([(str(flavor.id), flavor)
|
||||
for flavor in flavors])
|
||||
# Loop through instances to get flavor info.
|
||||
for instance in instances:
|
||||
try:
|
||||
flavor_id = instance.flavor["id"]
|
||||
if flavor_id in full_flavors:
|
||||
instance.full_flavor = full_flavors[flavor_id]
|
||||
else:
|
||||
# If the flavor_id is not in full_flavors list,
|
||||
# get it via nova api.
|
||||
instance.full_flavor = api.nova.flavor_get(
|
||||
self.request, flavor_id)
|
||||
except:
|
||||
msg = _('Unable to retrieve instance size information.')
|
||||
exceptions.handle(self.request, msg)
|
||||
return instances
|
||||
|
||||
|
||||
class CreateWinDCView(workflows.WorkflowView):
|
||||
workflow_class = CreateWinDC
|
||||
template_name = "project/windc/create.html"
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(CreateWinDCView, self).get_initial()
|
||||
initial['project_id'] = self.request.user.tenant_id
|
||||
initial['user_id'] = self.request.user.id
|
||||
return initial
|
514
dashboard/windc/workflows.py
Normal file
514
dashboard/windc/workflows.py
Normal file
@ -0,0 +1,514 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.utils.text import normalize_newlines
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.api import glance
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SelectProjectUserAction(workflows.Action):
|
||||
project_id = forms.ChoiceField(label=_("Project"))
|
||||
user_id = forms.ChoiceField(label=_("User"))
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(SelectProjectUserAction, self).__init__(request, *args, **kwargs)
|
||||
# Set our project choices
|
||||
projects = [(tenant.id, tenant.name)
|
||||
for tenant in request.user.authorized_tenants]
|
||||
self.fields['project_id'].choices = projects
|
||||
|
||||
# Set our user options
|
||||
users = [(request.user.id, request.user.username)]
|
||||
self.fields['user_id'].choices = users
|
||||
|
||||
class Meta:
|
||||
name = _("Project & User")
|
||||
# Unusable permission so this is always hidden. However, we
|
||||
# keep this step in the workflow for validation/verification purposes.
|
||||
permissions = ("!",)
|
||||
|
||||
|
||||
class SelectProjectUser(workflows.Step):
|
||||
action_class = SelectProjectUserAction
|
||||
contributes = ("project_id", "user_id")
|
||||
|
||||
|
||||
class VolumeOptionsAction(workflows.Action):
|
||||
VOLUME_CHOICES = (
|
||||
('', _("Don't boot from a volume.")),
|
||||
("volume_id", _("Boot from volume.")),
|
||||
("volume_snapshot_id", _("Boot from volume snapshot "
|
||||
"(creates a new volume).")),
|
||||
)
|
||||
# Boot from volume options
|
||||
volume_type = forms.ChoiceField(label=_("Volume Options"),
|
||||
choices=VOLUME_CHOICES,
|
||||
required=False)
|
||||
volume_id = forms.ChoiceField(label=_("Volume"), required=False)
|
||||
volume_snapshot_id = forms.ChoiceField(label=_("Volume Snapshot"),
|
||||
required=False)
|
||||
device_name = forms.CharField(label=_("Device Name"),
|
||||
required=False,
|
||||
initial="vda",
|
||||
help_text=_("Volume mount point (e.g. 'vda' "
|
||||
"mounts at '/dev/vda')."))
|
||||
delete_on_terminate = forms.BooleanField(label=_("Delete on Terminate"),
|
||||
initial=False,
|
||||
required=False,
|
||||
help_text=_("Delete volume on "
|
||||
"instance terminate"))
|
||||
|
||||
class Meta:
|
||||
name = _("Volume Options")
|
||||
permissions = ('openstack.services.volume',)
|
||||
help_text_template = ("project/instances/"
|
||||
"_launch_volumes_help.html")
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(VolumeOptionsAction, self).clean()
|
||||
volume_opt = cleaned_data.get('volume_type', None)
|
||||
|
||||
if volume_opt and not cleaned_data[volume_opt]:
|
||||
raise forms.ValidationError(_('Please choose a volume, or select '
|
||||
'%s.') % self.VOLUME_CHOICES[0][1])
|
||||
return cleaned_data
|
||||
|
||||
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)),
|
||||
("%s - %s GB (%s)" % (volume.display_name,
|
||||
volume.size,
|
||||
visible_label)))
|
||||
|
||||
def populate_volume_id_choices(self, request, context):
|
||||
volume_options = [("", _("Select Volume"))]
|
||||
try:
|
||||
volumes = [v for v in cinder.volume_list(self.request)
|
||||
if v.status == api.cinder.VOLUME_STATE_AVAILABLE]
|
||||
volume_options.extend([self._get_volume_display_name(vol)
|
||||
for vol in volumes])
|
||||
except:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve list of volumes.'))
|
||||
return volume_options
|
||||
|
||||
def populate_volume_snapshot_id_choices(self, request, context):
|
||||
volume_options = [("", _("Select Volume Snapshot"))]
|
||||
try:
|
||||
snapshots = cinder.volume_snapshot_list(self.request)
|
||||
snapshots = [s for s in snapshots
|
||||
if s.status == api.cinder.VOLUME_STATE_AVAILABLE]
|
||||
volume_options.extend([self._get_volume_display_name(snap)
|
||||
for snap in snapshots])
|
||||
except:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve list of volume '
|
||||
'snapshots.'))
|
||||
|
||||
return volume_options
|
||||
|
||||
|
||||
class VolumeOptions(workflows.Step):
|
||||
action_class = VolumeOptionsAction
|
||||
depends_on = ("project_id", "user_id")
|
||||
contributes = ("volume_type",
|
||||
"volume_id",
|
||||
"device_name", # Can be None for an image.
|
||||
"delete_on_terminate")
|
||||
|
||||
def contribute(self, data, context):
|
||||
context = super(VolumeOptions, self).contribute(data, context)
|
||||
# Translate form input to context for volume values.
|
||||
if "volume_type" in data and data["volume_type"]:
|
||||
context['volume_id'] = data.get(data['volume_type'], None)
|
||||
|
||||
if not context.get("volume_type", ""):
|
||||
context['volume_type'] = self.action.VOLUME_CHOICES[0][0]
|
||||
context['volume_id'] = None
|
||||
context['device_name'] = None
|
||||
context['delete_on_terminate'] = None
|
||||
return context
|
||||
|
||||
|
||||
class SetInstanceDetailsAction(workflows.Action):
|
||||
SOURCE_TYPE_CHOICES = (
|
||||
("image_id", _("Image")),
|
||||
("instance_snapshot_id", _("Snapshot")),
|
||||
)
|
||||
source_type = forms.ChoiceField(label=_("Instance Source"),
|
||||
choices=SOURCE_TYPE_CHOICES)
|
||||
image_id = forms.ChoiceField(label=_("Image"), required=False)
|
||||
instance_snapshot_id = forms.ChoiceField(label=_("Instance Snapshot"),
|
||||
required=False)
|
||||
name = forms.CharField(max_length=80, label=_("Instance Name"))
|
||||
flavor = forms.ChoiceField(label=_("Flavor"),
|
||||
help_text=_("Size of image to launch."))
|
||||
count = forms.IntegerField(label=_("Instance Count"),
|
||||
min_value=1,
|
||||
initial=1,
|
||||
help_text=_("Number of instances to launch."))
|
||||
|
||||
class Meta:
|
||||
name = _("Details")
|
||||
help_text_template = ("project/instances/"
|
||||
"_launch_details_help.html")
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(SetInstanceDetailsAction, self).clean()
|
||||
|
||||
# Validate our instance source.
|
||||
source = cleaned_data['source_type']
|
||||
# There should always be at least one image_id choice, telling the user
|
||||
# that there are "No Images Available" so we check for 2 here...
|
||||
if source == 'image_id' and not \
|
||||
filter(lambda x: x[0] != '', self.fields['image_id'].choices):
|
||||
raise forms.ValidationError(_("There are no image sources "
|
||||
"available; you must first create "
|
||||
"an image before attempting to "
|
||||
"launch an instance."))
|
||||
if not cleaned_data[source]:
|
||||
raise forms.ValidationError(_("Please select an option for the "
|
||||
"instance source."))
|
||||
|
||||
# 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)
|
||||
volume_type = self.data.get('volume_type', None)
|
||||
if volume_type and count > 1:
|
||||
msg = _('Launching multiple instances is only supported for '
|
||||
'images and instance snapshots.')
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def _get_available_images(self, request, context):
|
||||
project_id = context.get('project_id', None)
|
||||
if not hasattr(self, "_public_images"):
|
||||
public = {"is_public": True,
|
||||
"status": "active"}
|
||||
try:
|
||||
public_images, _more = glance.image_list_detailed(
|
||||
request, filters=public)
|
||||
except:
|
||||
public_images = []
|
||||
exceptions.handle(request,
|
||||
_("Unable to retrieve public images."))
|
||||
self._public_images = public_images
|
||||
|
||||
# Preempt if we don't have a project_id yet.
|
||||
if project_id is None:
|
||||
setattr(self, "_images_for_%s" % project_id, [])
|
||||
|
||||
if not hasattr(self, "_images_for_%s" % project_id):
|
||||
owner = {"property-owner_id": project_id,
|
||||
"status": "active"}
|
||||
try:
|
||||
owned_images, _more = glance.image_list_detailed(
|
||||
request, filters=owner)
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_("Unable to retrieve images for "
|
||||
"the current project."))
|
||||
setattr(self, "_images_for_%s" % project_id, owned_images)
|
||||
|
||||
owned_images = getattr(self, "_images_for_%s" % project_id)
|
||||
images = owned_images + self._public_images
|
||||
|
||||
# Remove duplicate images
|
||||
image_ids = []
|
||||
final_images = []
|
||||
for image in images:
|
||||
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 Image")))
|
||||
else:
|
||||
choices.insert(0, ("", _("No images available.")))
|
||||
return choices
|
||||
|
||||
def populate_instance_snapshot_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 Instance Snapshot")))
|
||||
else:
|
||||
choices.insert(0, ("", _("No snapshots available.")))
|
||||
return choices
|
||||
|
||||
def populate_flavor_choices(self, request, context):
|
||||
try:
|
||||
flavors = api.nova.flavor_list(request)
|
||||
flavor_list = [(flavor.id, "%s" % flavor.name)
|
||||
for flavor in flavors]
|
||||
except:
|
||||
flavor_list = []
|
||||
exceptions.handle(request,
|
||||
_('Unable to retrieve instance flavors.'))
|
||||
return sorted(flavor_list)
|
||||
|
||||
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:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve quota information."))
|
||||
return super(SetInstanceDetailsAction, self).get_help_text(extra)
|
||||
|
||||
|
||||
class SetInstanceDetails(workflows.Step):
|
||||
action_class = SetInstanceDetailsAction
|
||||
contributes = ("source_type", "source_id", "name", "count", "flavor")
|
||||
|
||||
def prepare_action_context(self, request, context):
|
||||
if 'source_type' in context and 'source_id' in context:
|
||||
context[context['source_type']] = context['source_id']
|
||||
return context
|
||||
|
||||
def contribute(self, data, context):
|
||||
context = super(SetInstanceDetails, self).contribute(data, context)
|
||||
# Allow setting the source dynamically.
|
||||
if ("source_type" in context and "source_id" in context
|
||||
and context["source_type"] not in context):
|
||||
context[context["source_type"]] = context["source_id"]
|
||||
|
||||
# Translate form input to context for source values.
|
||||
if "source_type" in data:
|
||||
context["source_id"] = data.get(data['source_type'], None)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
KEYPAIR_IMPORT_URL = "horizon:project:access_and_security:keypairs:import"
|
||||
|
||||
|
||||
class SetAccessControlsAction(workflows.Action):
|
||||
keypair = forms.DynamicChoiceField(label=_("Keypair"),
|
||||
required=False,
|
||||
help_text=_("Which keypair to use for "
|
||||
"authentication."),
|
||||
add_item_link=KEYPAIR_IMPORT_URL)
|
||||
groups = forms.MultipleChoiceField(label=_("Security Groups"),
|
||||
required=True,
|
||||
initial=["default"],
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
help_text=_("Launch instance in these "
|
||||
"security groups."))
|
||||
|
||||
class Meta:
|
||||
name = _("Access & Security")
|
||||
help_text = _("Control access to your instance via keypairs, "
|
||||
"security groups, and other mechanisms.")
|
||||
|
||||
def populate_keypair_choices(self, request, context):
|
||||
try:
|
||||
keypairs = api.nova.keypair_list(request)
|
||||
keypair_list = [(kp.name, kp.name) for kp in keypairs]
|
||||
except:
|
||||
keypair_list = []
|
||||
exceptions.handle(request,
|
||||
_('Unable to retrieve keypairs.'))
|
||||
if keypair_list:
|
||||
keypair_list.insert(0, ("", _("Select a keypair")))
|
||||
else:
|
||||
keypair_list = (("", _("No keypairs available.")),)
|
||||
return keypair_list
|
||||
|
||||
def populate_groups_choices(self, request, context):
|
||||
try:
|
||||
groups = api.nova.security_group_list(request)
|
||||
security_group_list = [(sg.name, sg.name) for sg in groups]
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_('Unable to retrieve list of security groups'))
|
||||
security_group_list = []
|
||||
return security_group_list
|
||||
|
||||
|
||||
class SetAccessControls(workflows.Step):
|
||||
action_class = SetAccessControlsAction
|
||||
depends_on = ("project_id", "user_id")
|
||||
contributes = ("keypair_id", "security_group_ids")
|
||||
|
||||
def contribute(self, data, context):
|
||||
if data:
|
||||
post = self.workflow.request.POST
|
||||
context['security_group_ids'] = post.getlist("groups")
|
||||
context['keypair_id'] = data.get("keypair", "")
|
||||
return context
|
||||
|
||||
|
||||
class CustomizeAction(workflows.Action):
|
||||
customization_script = forms.CharField(widget=forms.Textarea,
|
||||
label=_("Customization Script"),
|
||||
required=False,
|
||||
help_text=_("A script or set of "
|
||||
"commands to be "
|
||||
"executed after the "
|
||||
"instance has been "
|
||||
"built (max 16kb)."))
|
||||
|
||||
class Meta:
|
||||
name = _("Post-Creation")
|
||||
help_text_template = ("project/instances/"
|
||||
"_launch_customize_help.html")
|
||||
|
||||
|
||||
class PostCreationStep(workflows.Step):
|
||||
action_class = CustomizeAction
|
||||
contributes = ("customization_script",)
|
||||
|
||||
|
||||
class SetNetworkAction(workflows.Action):
|
||||
network = forms.MultipleChoiceField(label=_("Networks"),
|
||||
required=True,
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
help_text=_("Launch instance with"
|
||||
"these networks"))
|
||||
|
||||
class Meta:
|
||||
name = _("Networking")
|
||||
permissions = ('openstack.services.network',)
|
||||
help_text = _("Select networks for your instance.")
|
||||
|
||||
def populate_network_choices(self, request, context):
|
||||
try:
|
||||
tenant_id = self.request.user.tenant_id
|
||||
networks = api.quantum.network_list_for_tenant(request, tenant_id)
|
||||
for n in networks:
|
||||
n.set_id_as_name_if_empty()
|
||||
network_list = [(network.id, network.name) for network in networks]
|
||||
except:
|
||||
network_list = []
|
||||
exceptions.handle(request,
|
||||
_('Unable to retrieve networks.'))
|
||||
return network_list
|
||||
|
||||
|
||||
class SetNetwork(workflows.Step):
|
||||
action_class = SetNetworkAction
|
||||
contributes = ("network_id",)
|
||||
|
||||
def contribute(self, data, context):
|
||||
if data:
|
||||
networks = self.workflow.request.POST.getlist("network")
|
||||
# If no networks are explicitly specified, network list
|
||||
# contains an empty string, so remove it.
|
||||
networks = [n for n in networks if n != '']
|
||||
if networks:
|
||||
context['network_id'] = networks
|
||||
return context
|
||||
|
||||
|
||||
class CreateWinDC(workflows.Workflow):
|
||||
slug = "create_windc"
|
||||
name = _("Create WinDC Instance")
|
||||
finalize_button_name = _("Deploy")
|
||||
success_message = _('Deployed %(count)s named "%(name)s".')
|
||||
failure_message = _('Unable to deploy %(count)s named "%(name)s".')
|
||||
success_url = "horizon:project:windc:index"
|
||||
default_steps = (SelectProjectUser,
|
||||
SetInstanceDetails,
|
||||
SetAccessControls,
|
||||
SetNetwork,
|
||||
VolumeOptions,
|
||||
PostCreationStep)
|
||||
|
||||
def format_status_message(self, message):
|
||||
name = self.context.get('name', 'unknown instance')
|
||||
count = self.context.get('count', 1)
|
||||
if int(count) > 1:
|
||||
return message % {"count": _("%s instances") % count,
|
||||
"name": name}
|
||||
else:
|
||||
return message % {"count": _("instance"), "name": name}
|
||||
|
||||
def handle(self, request, context):
|
||||
custom_script = context.get('customization_script', '')
|
||||
|
||||
# Determine volume mapping options
|
||||
if context.get('volume_type', None):
|
||||
if(context['delete_on_terminate']):
|
||||
del_on_terminate = 1
|
||||
else:
|
||||
del_on_terminate = 0
|
||||
mapping_opts = ("%s::%s"
|
||||
% (context['volume_id'], del_on_terminate))
|
||||
dev_mapping = {context['device_name']: mapping_opts}
|
||||
else:
|
||||
dev_mapping = None
|
||||
|
||||
netids = context.get('network_id', None)
|
||||
if netids:
|
||||
nics = [{"net-id": netid, "v4-fixed-ip": ""}
|
||||
for netid in netids]
|
||||
else:
|
||||
nics = None
|
||||
|
||||
try:
|
||||
api.nova.server_create(request,
|
||||
context['name'],
|
||||
context['source_id'],
|
||||
context['flavor'],
|
||||
context['keypair_id'],
|
||||
normalize_newlines(custom_script),
|
||||
context['security_group_ids'],
|
||||
dev_mapping,
|
||||
nics=nics,
|
||||
instance_count=int(context['count']))
|
||||
return True
|
||||
except:
|
||||
exceptions.handle(request)
|
||||
return False
|
Loading…
x
Reference in New Issue
Block a user