Supply horizon-compatible Web UI

blueprint horizon-webui

(Auto-linking to blueprint either doesn't work for this project
or requires additional info not prescribed. Here is full link:
https://blueprints.launchpad.net/inception/+spec/horizon-webui

This change is the initial commit of the third and final part
of the horizon-compatible web UI for Inception Cloud (the others are
[1] and [2]).  This commit draws heavily from the horizon tutorial[3]
and from the existing implementation for instance management[4] (the
latter because inception cloud management is by analogy a more complicated
version of it).

Because this effort seeks to operate alongside OpenStack in the
architectural and coding style of OpenStack but require minimal internal
changes to OpenStack it relies on a separate API server from nova
compute.  In addition, this meant that the entire horizon stack could not
be utilized directly and a layer had to be chosen at which to deviate.
Accordingly the code is more verbose (because of horizon generality) than
it might otherwise be.

Installation of this application proceeds by
1. installing the inception package
2. starting the inception API server per INSTALL.md
3. modifying a single line in the horizon config per INSTALL.md

[1] https://review.openstack.org/#/c/47008/
[2] https://review.openstack.org/#/c/58835/
[3] http://docs.openstack.org/developer/horizon/topics/tutorial.html
[4] https://github.com/openstack/horizon/ then
    openstack_dashboard/dashboards/project/instances

Partially implements: blueprint horizon-webui

Change-Id: Id14b30bbb5eafeac928d8e94aeb0553ae52fb661
This commit is contained in:
Andrew Forrest 2013-12-04 18:08:26 +00:00
parent e505d194d3
commit ccd711ed9f
24 changed files with 1043 additions and 1 deletions

View File

@ -23,3 +23,23 @@ security to operate in a production environment.
$ paster serve ./etc/inception/paste-config.ini
Web UI
------
1. Install the inception package as usual.
2. Locate and modify Horizon's openstack_dashboard/settings.py to include 'inception.webui'
in the INSTALLED_APPS tuple.
E.g.
INSTALLED_APPS = (
'openstack_dashboard',
'django.contrib.contenttypes',
'django.contrib.auth',
. . .
'inception.webui',
. . .
)
3. Restart the Horizon service.

55
inception/api/base.py Normal file
View File

@ -0,0 +1,55 @@
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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 novaclient import utils
LOG = logging.getLogger(__name__)
class Manager(utils.HookableMixin):
"""
Nova-style Managers interact with APIs (e.g. servers, flavors, etc)
and expose CRUD ops for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key, obj_class=None, body=None):
_resp, body = self.api.client.get(url)
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key):
_resp, body = self.api.client.get(url)
return self.resource_class(self, body[response_key], loaded=True)
def _create(self, url, body, response_key, return_raw=False, **kwargs):
self.run_hooks('modify_body_for_create', body, **kwargs)
_resp, body = self.api.client.post(url, body=body)
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
def _delete(self, url, body):
_resp, _body = self.api.client.delete(url, body=body)

89
inception/api/client.py Normal file
View File

@ -0,0 +1,89 @@
#!/usr/bin/env python
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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 anyjson
import logging
import requests
LOG = logging.getLogger(__name__)
from inception.api import clouds
class HTTPClient(object):
USER_AGENT = 'python-inceptionclient'
def __init__(self, project_id=None, endpoint=None):
self.project_id = project_id
self.endpoint = endpoint
def request(self, method, url, **kwargs):
# Fix up request headers
hdrs = kwargs.get('headers', {})
hdrs['Accept'] = 'application/json'
hdrs['User-Agent'] = self.USER_AGENT
# If request has a body, treat it as JSON
if 'body' in kwargs:
hdrs['Content-Type'] = 'application/json'
kwargs['data'] = anyjson.serialize(kwargs['body'])
del kwargs['body']
kwargs['headers'] = hdrs
resp = requests.request(method,
(self.endpoint + self.project_id) + url,
**kwargs)
if resp.text:
if resp.status_code == 400:
if ('Connection refused' in resp.text or
'actively refused' in resp.text):
raise exceptions.ConnectionRefused(resp.text)
try:
body = anyjson.deserialize(resp.text)
except ValueError:
pass
body = None
else:
body = None
return resp, body
def get(self, url, **kwargs):
return self.request('GET', url, **kwargs)
def post(self, url, **kwargs):
return self.request('POST', url, **kwargs)
def delete(self, url, **kwargs):
return self.request('DELETE', url, **kwargs)
class Client(object):
def __init__(self, project_id=None,
endpoint='http://127.0.0.1:7653/'):
#TODO(forrest-r): make IC server endpoint a config item
self.client = HTTPClient(project_id=project_id, endpoint=endpoint)
self.clouds = clouds.CloudManager(self)
if __name__ == '__main__':
import sys
# edit as needed and execute directly to test
client = Client()
cloud_list = client.clouds.list()
print "cloud_list=", cloud_list

63
inception/api/clouds.py Normal file
View File

@ -0,0 +1,63 @@
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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.
"""
Inception Cloud Interface -- "in the style of nova client"
"""
from novaclient import base
from inception.api import base as local_base
class Cloud(base.Resource):
def start(self):
self.manager.start(self)
def delete(self):
self.manager.delete(self)
class CloudManager(local_base.Manager):
resource_class = Cloud
def get(self, cloud):
return self._get("/att-inception-clouds/%s" % base.getid(cloud),
"cloud")
def list(self): # , detailed=True, search_opts=None):
return self._list("/att-inception-clouds", "clouds")
def create(self, prefix, num_workers, flavor, gateway_flavor, image, user,
pool, key_name, security_groups, chef_repo, chef_repo_branch,
chefserver_image, dst_dir, userdata, OS_AUTH_URL, OS_PASSWORD,
OS_TENANT_ID, OS_TENANT_NAME, OS_USERNAME, **kwargs):
body = dict(prefix=prefix, num_workers=num_workers,
flavor=flavor, gateway_flavor=gateway_flavor, image=image,
user=user, pool=pool, key_name=key_name,
security_groups=security_groups, chef_repo=chef_repo,
chef_repo_branch=chef_repo_branch,
chefserver_image=chefserver_image, dst_dir=dst_dir,
userdata=userdata, OS_AUTH_URL=OS_AUTH_URL,
OS_PASSWORD=OS_PASSWORD, OS_TENANT_ID=OS_TENANT_ID,
OS_TENANT_NAME=OS_TENANT_NAME, OS_USERNAME=OS_USERNAME,)
self._create("/att-inception-clouds", body, 'cloud')
def delete(self, cloud, OS_AUTH_URL, OS_PASSWORD, OS_TENANT_ID,
OS_TENANT_NAME, OS_USERNAME):
body = dict(OS_AUTH_URL=OS_AUTH_URL, OS_PASSWORD=OS_PASSWORD,
OS_TENANT_ID=OS_TENANT_ID, OS_TENANT_NAME=OS_TENANT_NAME,
OS_USERNAME=OS_USERNAME,)
self._delete("/att-inception-clouds/%s" % base.getid(cloud), body)

View File

@ -0,0 +1,3 @@
"""
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
"""

26
inception/webui/panel.py Normal file
View File

@ -0,0 +1,26 @@
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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 Inception(horizon.Panel):
name = _("Inception Clouds")
slug = "inception"
dashboard.Project.register(Inception)

231
inception/webui/tables.py Normal file
View File

@ -0,0 +1,231 @@
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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.core import urlresolvers
from django.template.defaultfilters import filesizeformat
from django.template.defaultfilters import title
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
import logging
from openstack_dashboard import api
from horizon import tables
from horizon.utils.filters import replace_underscores
import inception.webui.api.inception as iapi
from inception.webui.tabs import InceptionInstanceDetailTabs
from inception.webui.tabs import LogTab
LOG = logging.getLogger(__name__)
ACTIVE_STATES = ("ACTIVE",)
SNAPSHOT_READY_STATES = ("ACTIVE", "SHUTOFF")
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 LaunchLink(tables.LinkAction):
name = "launch"
verbose_name = _("Launch Inception Instance")
url = "horizon:project:inception:launch"
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 = _("Launch Inception Instance")
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 EditInstance(tables.LinkAction):
name = "edit"
verbose_name = _("Edit Inception Instance")
url = "horizon:project:inception:update"
classes = ("ajax-modal", "btn-edit")
def get_link_url(self, project):
return self._get_link_url(project, 'instance_info')
def _get_link_url(self, project, step_slug):
base_url = urlresolvers.reverse(self.url, args=[project.id])
param = urlencode({"step": step_slug})
return "?".join([base_url, param])
def allowed(self, request, instance):
return not is_deleting(instance)
class LogLink(tables.LinkAction):
name = "log"
verbose_name = _("View Orchestrator Log")
url = "horizon:project:inception: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(
InceptionInstanceDetailTabs).get_query_string()
return "?".join([base_url, tab_query_string])
class TerminateInstance(tables.BatchAction):
name = "terminate"
action_present = _("Terminate Inception")
action_past = _("Scheduled termination of")
data_type_singular = _("Instance")
data_type_plural = _("Instances")
classes = ('btn-danger', 'btn-terminate')
def allowed(self, request, instance=None):
return True
def action(self, request, obj_id):
iapi.cloud_delete(request, obj_id)
class InstancesFilterAction(tables.FilterAction):
def filter(self, table, instances, filter_string):
""" Naive case-insensitive search. """
q = filter_string.lower()
return [instance for instance in instances
if q in instance.name.lower()]
def get_power_state(instance):
return getattr(instance, "power_state", 'unknown')
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 UpdateRow(tables.Row):
ajax = True
def get_data(self, request, instance_id):
cloud = iapi.cloud_get(request, instance_id)
return cloud
class InceptionInstancesTable(tables.DataTable):
""" See: openstack_dashboard/dashboards/project/instances/tables.py"""
TASK_STATUS_CHOICES = (
(None, True),
("none", True)
)
STATUS_CHOICES = (
("active", True),
("shutoff", True),
("suspended", True),
("paused", True),
("error", False),
)
prefix = tables.Column("prefix",
link=("horizon:project:inception:detail"),
verbose_name=_("Prefix"))
n_workers = tables.Column("num_workers", verbose_name=_("No. of Workers"))
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("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 = "inception_instances"
verbose_name = _("Inception Instances")
multi_select = True
#table_actions = (myact1, myact2,)
#row_actions = (myact1, myact2,)
row_class = UpdateRow
table_actions = (LaunchLink, TerminateInstance, InstancesFilterAction)
row_actions = (EditInstance, LogLink, TerminateInstance)

59
inception/webui/tabs.py Normal file
View File

@ -0,0 +1,59 @@
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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 import settings
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tabs
from inception.webui.api import inception as iapi
from openstack_dashboard import api
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "inception_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 = "inception_log"
template_name = "project/instances/_detail_log.html"
preload = False
def get_context_data(self, request):
instance = self.tab_group.kwargs['instance']
try:
data = 'log goes here'
# TODO(forrest-r): augment if log storage/retrieval supported
# iapi.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 InceptionInstanceDetailTabs(tabs.TabGroup):
slug = "inception_instance_details"
tabs = (OverviewTab, LogTab)
sticky = True

View File

@ -0,0 +1,22 @@
{% load i18n %}
{% load url from future %}
<h3>{% trans "Instance Console" %}</h3>
{% if console_url %}
<p class='alert alert-info'>{% blocktrans %}If console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %} <a href="{{ console_url }}" style="text-decoration: underline">{% trans "Click here to show only console" %}</a></p>
<iframe id="console_embed" src="{{ console_url }}" style="width:100%;height:100%"></iframe>
<script type="text/javascript">
var fix_height = function() {
$('iframe#console_embed').css({ height: $(document).height() + 'px' });
};
// there are two code paths to this particular block; handle them both
if (typeof($) != 'undefined') {
$(document).ready(fix_height);
} else {
addHorizonLoadEvent(fix_height);
}
</script>
{% else %}
<p class='alert alert-error'>{% blocktrans %}console is currently unavailable. Please try again later.{% endblocktrans %}
<a class='btn btn-mini' href="{% url 'horizon:project:instances:detail' instance_id %}">{% trans "Reload" %}</a></p>
{% endif %}

View File

@ -0,0 +1,18 @@
{% load i18n %}
{% load url from future %}
<div class="clearfix">
<h3 class="pull-left">{% trans "Instance Console Log" %}</h3>
<form id="tail_length" action="{% url 'horizon:project:instances:console' instance.id %}" class="form-inline pull-right">
<label for="tail_length_select">{% trans "Log Length" %}</label>
<input class="span1" type="text" name="length" value="35" />
<button class="btn btn-small btn-primary" type="submit">{% trans "Go" %}</button>
{% url 'horizon:project:instances:console' instance.id as console_url %}
<a class="btn btn-small" target="_blank" href="{{ console_url }}">{% trans "View Full Log" %}</a>
</form>
</div>
<pre class="logs">
{{ console_log }}
</pre>

View File

@ -0,0 +1,107 @@
{% load i18n sizeformat %}
{% load url from future %}
<h3>{% trans "Instance Overview" %}</h3>
<div class="status row-fluid detail">
<h4>{% trans "Info" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Name" %}</dt>
<dd>{{ instance.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ instance.id }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ instance.status|title }}</dd>
</dl>
</div>
<div class="specs row-fluid detail">
<h4>{% trans "Specs" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Flavor" %}</dt>
<dd>{{ instance.full_flavor.name }}</dd>
<dt>{% trans "RAM" %}</dt>
<dd>{{ instance.full_flavor.ram|mbformat }}</dd>
<dt>{% trans "VCPUs" %}</dt>
<dd>{{ instance.full_flavor.vcpus }} {% trans "VCPU" %}</dd>
<dt>{% trans "Disk" %}</dt>
<dd>{{ instance.full_flavor.disk }}{% trans "GB" %}</dd>
{% if instance.full_flavor.ephemeral %}
<dt>{% trans "Ephemeral Disk" %}</dt>
<dd>{{ instance.full_flavor.ephemeral }}{% trans "GB" %}</dd>
{% endif %}
</dl>
</div>
<div class="addresses row-fluid detail">
<h4>{% trans "IP Addresses" %}</h4>
<hr class="header_rule">
<dl>
{% for network, ip_list in instance.addresses.items %}
<dt>{{ network|title }}</dt>
<dd>
{% for ip in ip_list %}
{% if not forloop.last %}{{ ip.addr}},&nbsp;{% else %}{{ip.addr}}{% endif %}
{% endfor %}
</dd>
{% endfor %}
</dl>
</div>
<div class="security_groups row-fluid detail">
<h4>{% trans "Security Groups" %}</h4>
<hr class="header_rule">
<dl>
{% for group in instance.security_groups %}
<dt>{{ group.name }}</dt>
<dd>
<ul>
{% for rule in group.rules %}
<li>{{ rule }}</li>
{% empty %}
<li><em>{% trans "No rules defined." %}</em></li>
{% endfor %}
</ul>
</dd>
{% endfor %}
</dl>
</div>
<div class="meta row-fluid detail">
<h4>{% trans "Meta" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Key Name" %}</dt>
{% with default_key_name="<em>"|add:_("None")|add:"</em>" %}
<dd>{{ instance.key_name|default:default_key_name }}</dd>
{% endwith %}
{% url 'horizon:project:images_and_snapshots:images:detail' instance.image.id as image_url %}
<dt>{% trans "Image Name" %}</dt>
<dd><a href="{{ image_url }}">{{ instance.image_name }}</a></dd>
{% with default_item_value="<em>"|add:_("N/A")|add:"</em>" %}
{% for key, value in instance.metadata.items %}
<dt>{{ key|force_escape }}</dt>
<dd>{{ value|force_escape|default:default_item_value }}</dd>
{% endfor%}
{% endwith %}
</dl>
</div>
<div class="volumes row-fluid detail">
<h4>{% trans "Volumes Attached" %}</h4>
<hr class="header_rule">
<dl>
{% for volume in instance.volumes %}
<dt>{% trans "Attached To" %}</dt>
<dd>
<a href="{% url 'horizon:project:volumes:detail' volume.volumeId %}">{{ volume.name }}</a><span> {% trans "on" %} {{ volume.device }}</span>
</dd>
{% empty %}
<dt>{% trans "Volume" %}</dt>
<dd><em>{% trans "No volumes attached." %}</em></dd>
{% endfor %}
</dl>
</dl>
</div>

View File

@ -0,0 +1,72 @@
{% load i18n horizon humanize %}
{% block help_message %}
{% endblock %}
<h4>{% trans "Flavor Details" %}</h4>
<table class="flavor_table table-striped">
<tbody>
<tr>
<th>&nbsp;</th><th>{% trans "Instance" %}</th><th>{% trans "Gateway" %}</th>
</tr>
<tr>
<td class="flavor_name">{% trans "Name" %}</td>
<td><span id="flavor_name"></span></td>
<td><span id="gateway_flavor_name"></span></td>
</tr>
<tr>
<td class="flavor_name">{% trans "VCPUs" %}</td>
<td><span id="flavor_vcpus"></span></td>
<td><span id="gateway_flavor_vcpus"></span></td></tr>
<tr>
<td class="flavor_name">{% trans "Root Disk" %}</td>
<td><span id="flavor_disk"> </span> {% trans "GB" %}</td>
<td><span id="gateway_flavor_disk"> </span> {% trans "GB" %} </td></tr>
<tr>
<td class="flavor_name">{% trans "Ephemeral Disk" %}</td>
<td><span id="flavor_ephemeral"></span> {% trans "GB" %}</td>
<td><span id="gateway_flavor_ephemeral"></span> {% trans "GB" %} </td></tr>
<tr>
<td class="flavor_name">{% trans "Total Disk" %}</td>
<td><span id="flavor_disk_total"></span> {% trans "GB" %}</td>
<td><span id="gateway_flavor_disk_total"></span> {% trans "GB" %} </td></tr>
<tr>
<td class="flavor_name">{% trans "RAM" %}</td>
<td><span id="flavor_ram"></span> {% trans "MB" %}</td>
<td><span id="gateway_flavor_ram"></span> {% trans "MB" %} </td></tr>
</tbody>
</table>
<div class="quota-dynamic">
<h4>{% trans "Project Limits" %}</h4>
<div class="quota_title clearfix">
<strong>{% trans "Number of Instances" %}</strong>
{% blocktrans with used=usages.totalInstancesUsed|intcomma quota=usages.maxTotalInstances|intcomma %}<p>{{ used }} of {{ quota }} Used</p>{% endblocktrans %}
</div>
<div id="quota_instances" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.maxTotalInstances }}" data-quota-used="{{ usages.totalInstancesUsed }}">
</div>
<div class="quota_title clearfix">
<strong>{% trans "Number of VCPUs" %}</strong>
{% blocktrans with used=usages.totalCoresUsed|intcomma quota=usages.maxTotalCores|intcomma %}<p>{{ used }} of {{ quota }} Used</p>{% endblocktrans %}
</div>
<div id="quota_vcpus" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.maxTotalCores }}" data-quota-used="{{ usages.totalCoresUsed }}">
</div>
<div class="quota_title clearfix">
<strong>{% trans "Total RAM" %}</strong>
{% blocktrans with used=usages.totalRAMUsed|intcomma quota=usages.maxTotalRAMSize|intcomma %}<p>{{ used }} of {{ quota }} MB Used</p>{% endblocktrans %}
</div>
<div id="quota_ram" data-progress-indicator-flavor data-quota-limit="{{ usages.maxTotalRAMSize }}" data-quota-used="{{ usages.totalRAMUsed }}" class="quota_bar">
</div>
</div>
<script type="text/javascript" charset="utf-8">
if(typeof horizon.Quota !== 'undefined') {
horizon.Quota.initWithFlavors({{ flavors|safe|default:"{}" }});
} else {
addHorizonLoadEvent(function() {
horizon.Quota.initWithFlavors({{ flavors|safe|default:"{}" }});
});
}
</script>

View File

@ -0,0 +1,10 @@
{% for ip_group, addresses in instance.addresses.items %}
{% if instance.addresses.items|length > 1 %}
<h4>{{ ip_group }}</h4>
{% endif %}
<ul>
{% for address in addresses %}
<li>{{ address.addr }}</li>
{% endfor %}
</ul>
{% endfor %}

View File

@ -0,0 +1,3 @@
{% load i18n %}
<p>{% blocktrans %}You can customize your instance after it's launched using the options available here.{% endblocktrans %}</p>
<p>{% blocktrans %}The "Customization Script" field is analogous to "User Data" in other systems.{% endblocktrans %}</p>

View File

@ -0,0 +1,7 @@
{% extends 'project/inception/_flavors_and_quotas.html' %}
{% load i18n %}
{% block help_message %}
<p>{% blocktrans %}Specify the details for launching an Inception instance.{% endblocktrans %}</p>
<p>{% blocktrans %}The chart below shows the resources used by this project in relation to the project's quotas.{% endblocktrans %}</p>
{% endblock %}

View File

@ -0,0 +1,3 @@
{% load i18n horizon %}
<p>{% blocktrans %}Choose network from Available networks to Selected Networks by push button or drag and drop, you may change nic order by drag and drop as well. {% endblocktrans %}</p>

View File

@ -0,0 +1,3 @@
{% load i18n horizon %}
<p>{% blocktrans %}An instance can be launched with varying types of attached storage. You may select from those options here.{% endblocktrans %}</p>

View File

@ -0,0 +1,46 @@
{% load i18n %}
<noscript><h3>{{ step }}</h3></noscript>
<table class="table-fixed" id="networkListSortContainer">
<tbody>
<tr>
<td class="actions">
<h4 id="selected_network_h4">{% trans "Selected Networks" %}</h4>
<ul id="selected_network" class="networklist">
</ul>
<h4>{% trans "Available networks" %}</h4>
<ul id="available_network" class="networklist">
</ul>
</td>
<td class="help_text">
{% include "project/instances/_launch_network_help.html" %}
</td>
</tr>
</tbody>
</table>
<table class="table-fixed" id="networkListIdContainer">
<tbody>
<tr>
<td class="actions">
<div id="networkListId">
{% include "horizon/common/_form_fields.html" %}
</div>
</td>
<td class="help_text">
{{ step.get_help_text }}
</td>
</tr>
</tbody>
</table>
<script>
if (typeof $ !== 'undefined') {
horizon.instances.workflow_init($(".workflow"));
} else {
addHorizonLoadEvent(function() {
horizon.instances.workflow_init($(".workflow"));
});
}
</script>

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% load i18n sizeformat %}
{% block title %}{% trans "Inception Instance Detail" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title="Inception Instance Detail: "|add:instance.name %}
{% endblock page_header %}
{% block main %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Inception Instances" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Inception Instances") %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}

34
inception/webui/urls.py Normal file
View File

@ -0,0 +1,34 @@
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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
from django.conf.urls.defaults import url
from inception.webui.views import DetailView
from inception.webui.views import UpdateView
from inception.webui.views import IndexView
from inception.webui.views import LaunchInceptionInstanceView
INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
VIEW_MOD = 'horizon.inception.views'
urlpatterns = patterns(
VIEW_MOD,
url(r'^$', IndexView.as_view(), name='index'),
url(r'^launch$', LaunchInceptionInstanceView.as_view(), name='launch'),
url(r'^(?P<instance_id>[^/]+)/$', DetailView.as_view(), name='detail'),
url(r'^(?P<instance_id>[^/]+)/update$', UpdateView.as_view(),
name='update'),
)

132
inception/webui/views.py Normal file
View File

@ -0,0 +1,132 @@
# Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
#
# 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.core.urlresolvers import reverse_lazy
from django import http
from django import shortcuts
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from horizon import tabs
from horizon import workflows
from openstack_dashboard import api
import inception.webui.api.inception as iapi
from inception.webui.tables import InceptionInstancesTable
from inception.webui.tabs import InceptionInstanceDetailTabs
from inception.webui.workflows.create_inception_instance import \
LaunchInceptionInstance
from inception.webui.workflows.create_inception_instance import \
UpdateInceptionInstance
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
table_class = InceptionInstancesTable
template_name = 'inception/index.html'
def has_more_data(self, table):
LOG.debug("has_more_data called")
return self._more
def get_data(self):
LOG.debug("get_data called")
marker = self.request.GET.get(
InceptionInstancesTable._meta.pagination_param, None)
# Gather our instances
try:
instances, self._more = iapi.cloud_list(
self.request,
search_opts={'marker': marker,
'paginate': True})
except:
self._more = False
instances = []
exceptions.handle(self.request,
_('Unable to retrieve instances.'))
return instances
class LaunchInceptionInstanceView(workflows.WorkflowView):
workflow_class = LaunchInceptionInstance
def get_initial(self):
LOG.debug("LaunchInceptionInstanceView: get_initial()")
initial = super(LaunchInceptionInstanceView, self).get_initial()
initial['project_id'] = self.request.user.tenant_id
initial['user_id'] = self.request.user.id
return initial
class UpdateView(workflows.WorkflowView):
workflow_class = UpdateInceptionInstance
success_url = reverse_lazy("horizon:project:instances:index")
def get_context_data(self, **kwargs):
context = super(UpdateView, self).get_context_data(**kwargs)
context["instance_id"] = self.kwargs['instance_id']
return context
def get_object(self, *args, **kwargs):
if not hasattr(self, "_object"):
instance_id = self.kwargs['instance_id']
try:
self._object = api.nova.server_get(self.request, instance_id)
except:
redirect = reverse("horizon:project:instances:index")
msg = _('Unable to retrieve instance details.')
exceptions.handle(self.request, msg, redirect=redirect)
return self._object
def get_initial(self):
initial = super(UpdateView, self).get_initial()
initial.update({'instance_id': self.kwargs['instance_id'],
'name': getattr(self.get_object(), 'name', '')})
return initial
class DetailView(tabs.TabView):
tab_group_class = InceptionInstanceDetailTabs
template_name = 'project/instances/detail.html'
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context["instance"] = self.get_data()
return context
def get_data(self):
if not hasattr(self, "_instance"):
try:
instance_id = self.kwargs['instance_id']
instance = iapi.cloud_get(self.request, instance_id)
instance.security_groups = api.network.server_security_groups(
self.request, instance_id)
except:
redirect = reverse('horizon:project:instances:index')
exceptions.handle(self.request,
_('Unable to retrieve details for '
'instance "%s".') % instance_id,
redirect=redirect)
self._instance = instance
return self._instance
def get_tabs(self, request, *args, **kwargs):
instance = self.get_data()
return self.tab_group_class(request, instance=instance, **kwargs)

View File

@ -27,6 +27,19 @@ setup(
'bin/pre_switch_kernel.sh',
'bin/setup_chef_repo.sh',
'bin/userdata.sh.template',
])],
]),
('inception/webui/templates/inception',
['inception/webui/templates/inception/detail.html',
'inception/webui/templates/inception/_detail_log.html',
'inception/webui/templates/inception/_detail_overview.html',
'inception/webui/templates/inception/_flavors_and_quotas.html',
'inception/webui/templates/inception/index.html',
'inception/webui/templates/inception/_launch_customize_help.html',
'inception/webui/templates/inception/_launch_details_help.html',
'inception/webui/templates/inception/_launch_network_help.html',
'inception/webui/templates/inception/_launch_volumes_help.html',
'inception/webui/templates/inception/_update_networks.html',
]),
],
scripts=['bin/orchestrator'],
)

View File