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:
parent
e505d194d3
commit
ccd711ed9f
20
INSTALL.md
20
INSTALL.md
@ -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
55
inception/api/base.py
Normal 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
89
inception/api/client.py
Normal 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
63
inception/api/clouds.py
Normal 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)
|
3
inception/webui/models.py
Normal file
3
inception/webui/models.py
Normal 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
26
inception/webui/panel.py
Normal 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
231
inception/webui/tables.py
Normal 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
59
inception/webui/tabs.py
Normal 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
|
22
inception/webui/templates/inception/_detail_console.html
Normal file
22
inception/webui/templates/inception/_detail_console.html
Normal 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 %}
|
18
inception/webui/templates/inception/_detail_log.html
Normal file
18
inception/webui/templates/inception/_detail_log.html
Normal 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>
|
107
inception/webui/templates/inception/_detail_overview.html
Normal file
107
inception/webui/templates/inception/_detail_overview.html
Normal 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}}, {% 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>
|
72
inception/webui/templates/inception/_flavors_and_quotas.html
Normal file
72
inception/webui/templates/inception/_flavors_and_quotas.html
Normal 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> </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>
|
10
inception/webui/templates/inception/_instance_ips.html
Normal file
10
inception/webui/templates/inception/_instance_ips.html
Normal 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 %}
|
@ -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>
|
@ -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 %}
|
@ -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>
|
@ -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>
|
46
inception/webui/templates/inception/_update_networks.html
Normal file
46
inception/webui/templates/inception/_update_networks.html
Normal 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>
|
15
inception/webui/templates/inception/detail.html
Normal file
15
inception/webui/templates/inception/detail.html
Normal 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 %}
|
11
inception/webui/templates/inception/index.html
Normal file
11
inception/webui/templates/inception/index.html
Normal 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
34
inception/webui/urls.py
Normal 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
132
inception/webui/views.py
Normal 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)
|
15
setup.py
15
setup.py
@ -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'],
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user