Browse Source

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
Andrew Forrest 5 years ago
parent
commit
ccd711ed9f

+ 20
- 0
INSTALL.md View File

@@ -23,3 +23,23 @@ security to operate in a production environment.
23 23
 
24 24
         $ paster serve ./etc/inception/paste-config.ini
25 25
 
26
+Web UI
27
+------
28
+
29
+1. Install the inception package as usual.
30
+
31
+2. Locate and modify Horizon's openstack_dashboard/settings.py to include 'inception.webui'
32
+   in the INSTALLED_APPS tuple.
33
+
34
+   E.g.
35
+
36
+    INSTALLED_APPS = (
37
+        'openstack_dashboard',
38
+        'django.contrib.contenttypes',
39
+        'django.contrib.auth',
40
+        . . .
41
+        'inception.webui',
42
+        . . .
43
+    )
44
+
45
+3. Restart the Horizon service.

+ 55
- 0
inception/api/base.py View File

@@ -0,0 +1,55 @@
1
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import logging
16
+
17
+from novaclient import utils
18
+
19
+LOG = logging.getLogger(__name__)
20
+
21
+
22
+class Manager(utils.HookableMixin):
23
+    """
24
+    Nova-style Managers interact with APIs (e.g. servers, flavors, etc)
25
+    and expose CRUD ops for them.
26
+    """
27
+
28
+    resource_class = None
29
+
30
+    def __init__(self, api):
31
+        self.api = api
32
+
33
+    def _list(self, url, response_key, obj_class=None, body=None):
34
+        _resp, body = self.api.client.get(url)
35
+        if obj_class is None:
36
+            obj_class = self.resource_class
37
+
38
+        data = body[response_key]
39
+        return [obj_class(self, res, loaded=True) for res in data if res]
40
+
41
+    def _get(self, url, response_key):
42
+        _resp, body = self.api.client.get(url)
43
+        return self.resource_class(self, body[response_key], loaded=True)
44
+
45
+    def _create(self, url, body, response_key, return_raw=False, **kwargs):
46
+        self.run_hooks('modify_body_for_create', body, **kwargs)
47
+
48
+        _resp, body = self.api.client.post(url, body=body)
49
+        if return_raw:
50
+            return body[response_key]
51
+
52
+        return self.resource_class(self, body[response_key])
53
+
54
+    def _delete(self, url, body):
55
+        _resp, _body = self.api.client.delete(url, body=body)

+ 89
- 0
inception/api/client.py View File

@@ -0,0 +1,89 @@
1
+#!/usr/bin/env python
2
+
3
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
4
+#
5
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+#    not use this file except in compliance with the License. You may obtain
7
+#    a copy of the License at
8
+#
9
+#         http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+#    Unless required by applicable law or agreed to in writing, software
12
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+#    License for the specific language governing permissions and limitations
15
+#    under the License.
16
+
17
+import anyjson
18
+import logging
19
+import requests
20
+
21
+LOG = logging.getLogger(__name__)
22
+
23
+from inception.api import clouds
24
+
25
+
26
+class HTTPClient(object):
27
+    USER_AGENT = 'python-inceptionclient'
28
+
29
+    def __init__(self, project_id=None, endpoint=None):
30
+        self.project_id = project_id
31
+        self.endpoint = endpoint
32
+
33
+    def request(self, method, url, **kwargs):
34
+        # Fix up request headers
35
+        hdrs = kwargs.get('headers', {})
36
+        hdrs['Accept'] = 'application/json'
37
+        hdrs['User-Agent'] = self.USER_AGENT
38
+
39
+        # If request has a body, treat it as JSON
40
+        if 'body' in kwargs:
41
+            hdrs['Content-Type'] = 'application/json'
42
+            kwargs['data'] = anyjson.serialize(kwargs['body'])
43
+            del kwargs['body']
44
+
45
+        kwargs['headers'] = hdrs
46
+
47
+        resp = requests.request(method,
48
+                                (self.endpoint + self.project_id) + url,
49
+                                **kwargs)
50
+
51
+        if resp.text:
52
+            if resp.status_code == 400:
53
+                if ('Connection refused' in resp.text or
54
+                        'actively refused' in resp.text):
55
+                    raise exceptions.ConnectionRefused(resp.text)
56
+            try:
57
+                body = anyjson.deserialize(resp.text)
58
+            except ValueError:
59
+                pass
60
+                body = None
61
+        else:
62
+            body = None
63
+
64
+        return resp, body
65
+
66
+    def get(self, url, **kwargs):
67
+        return self.request('GET', url, **kwargs)
68
+
69
+    def post(self, url, **kwargs):
70
+        return self.request('POST', url, **kwargs)
71
+
72
+    def delete(self, url, **kwargs):
73
+        return self.request('DELETE', url, **kwargs)
74
+
75
+
76
+class Client(object):
77
+    def __init__(self, project_id=None,
78
+                 endpoint='http://127.0.0.1:7653/'):
79
+        #TODO(forrest-r): make IC server endpoint a config item
80
+        self.client = HTTPClient(project_id=project_id, endpoint=endpoint)
81
+        self.clouds = clouds.CloudManager(self)
82
+
83
+if __name__ == '__main__':
84
+    import sys
85
+    # edit as needed and execute directly to test
86
+    client = Client()
87
+    cloud_list = client.clouds.list()
88
+
89
+    print "cloud_list=", cloud_list

+ 63
- 0
inception/api/clouds.py View File

@@ -0,0 +1,63 @@
1
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+"""
16
+Inception Cloud Interface  -- "in the style of nova client"
17
+"""
18
+
19
+from novaclient import base
20
+from inception.api import base as local_base
21
+
22
+
23
+class Cloud(base.Resource):
24
+
25
+    def start(self):
26
+        self.manager.start(self)
27
+
28
+    def delete(self):
29
+        self.manager.delete(self)
30
+
31
+
32
+class CloudManager(local_base.Manager):
33
+
34
+    resource_class = Cloud
35
+
36
+    def get(self, cloud):
37
+        return self._get("/att-inception-clouds/%s" % base.getid(cloud),
38
+                         "cloud")
39
+
40
+    def list(self):     # , detailed=True, search_opts=None):
41
+        return self._list("/att-inception-clouds", "clouds")
42
+
43
+    def create(self, prefix, num_workers, flavor, gateway_flavor, image, user,
44
+               pool, key_name, security_groups, chef_repo, chef_repo_branch,
45
+               chefserver_image, dst_dir, userdata, OS_AUTH_URL, OS_PASSWORD,
46
+               OS_TENANT_ID, OS_TENANT_NAME, OS_USERNAME, **kwargs):
47
+        body = dict(prefix=prefix, num_workers=num_workers,
48
+                    flavor=flavor, gateway_flavor=gateway_flavor, image=image,
49
+                    user=user, pool=pool, key_name=key_name,
50
+                    security_groups=security_groups, chef_repo=chef_repo,
51
+                    chef_repo_branch=chef_repo_branch,
52
+                    chefserver_image=chefserver_image, dst_dir=dst_dir,
53
+                    userdata=userdata, OS_AUTH_URL=OS_AUTH_URL,
54
+                    OS_PASSWORD=OS_PASSWORD, OS_TENANT_ID=OS_TENANT_ID,
55
+                    OS_TENANT_NAME=OS_TENANT_NAME, OS_USERNAME=OS_USERNAME,)
56
+        self._create("/att-inception-clouds", body, 'cloud')
57
+
58
+    def delete(self, cloud, OS_AUTH_URL, OS_PASSWORD, OS_TENANT_ID,
59
+               OS_TENANT_NAME, OS_USERNAME):
60
+        body = dict(OS_AUTH_URL=OS_AUTH_URL, OS_PASSWORD=OS_PASSWORD,
61
+                    OS_TENANT_ID=OS_TENANT_ID, OS_TENANT_NAME=OS_TENANT_NAME,
62
+                    OS_USERNAME=OS_USERNAME,)
63
+        self._delete("/att-inception-clouds/%s" % base.getid(cloud), body)

+ 3
- 0
inception/webui/models.py View File

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

+ 26
- 0
inception/webui/panel.py View File

@@ -0,0 +1,26 @@
1
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from django.utils.translation import ugettext_lazy as _
16
+
17
+import horizon
18
+from openstack_dashboard.dashboards.project import dashboard
19
+
20
+
21
+class Inception(horizon.Panel):
22
+    name = _("Inception Clouds")
23
+    slug = "inception"
24
+
25
+
26
+dashboard.Project.register(Inception)

+ 231
- 0
inception/webui/tables.py View File

@@ -0,0 +1,231 @@
1
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from django.core import urlresolvers
16
+from django.template.defaultfilters import filesizeformat
17
+from django.template.defaultfilters import title
18
+from django.utils.http import urlencode
19
+from django.utils.translation import ugettext_lazy as _
20
+
21
+import logging
22
+
23
+from openstack_dashboard import api
24
+from horizon import tables
25
+from horizon.utils.filters import replace_underscores
26
+
27
+import inception.webui.api.inception as iapi
28
+from inception.webui.tabs import InceptionInstanceDetailTabs
29
+from inception.webui.tabs import LogTab
30
+
31
+LOG = logging.getLogger(__name__)
32
+
33
+
34
+ACTIVE_STATES = ("ACTIVE",)
35
+SNAPSHOT_READY_STATES = ("ACTIVE", "SHUTOFF")
36
+
37
+POWER_STATES = {
38
+    0: "NO STATE",
39
+    1: "RUNNING",
40
+    2: "BLOCKED",
41
+    3: "PAUSED",
42
+    4: "SHUTDOWN",
43
+    5: "SHUTOFF",
44
+    6: "CRASHED",
45
+    7: "SUSPENDED",
46
+    8: "FAILED",
47
+    9: "BUILDING",
48
+}
49
+
50
+PAUSE = 0
51
+UNPAUSE = 1
52
+SUSPEND = 0
53
+RESUME = 1
54
+
55
+
56
+def is_deleting(instance):
57
+    task_state = getattr(instance, "OS-EXT-STS:task_state", None)
58
+    if not task_state:
59
+        return False
60
+    return task_state.lower() == "deleting"
61
+
62
+
63
+class LaunchLink(tables.LinkAction):
64
+    name = "launch"
65
+    verbose_name = _("Launch Inception Instance")
66
+    url = "horizon:project:inception:launch"
67
+    classes = ("btn-launch", "ajax-modal")
68
+
69
+    def allowed(self, request, datum):
70
+        try:
71
+            limits = api.nova.tenant_absolute_limits(request, reserved=True)
72
+
73
+            instances_available = (limits['maxTotalInstances']
74
+                                   - limits['totalInstancesUsed'])
75
+            cores_available = (limits['maxTotalCores']
76
+                               - limits['totalCoresUsed'])
77
+            ram_available = limits['maxTotalRAMSize'] - limits['totalRAMUsed']
78
+
79
+            if (instances_available <= 0 or cores_available <= 0
80
+                    or ram_available <= 0):
81
+                if "disabled" not in self.classes:
82
+                    self.classes = [c for c in self.classes] + ['disabled']
83
+                    self.verbose_name = string_concat(self.verbose_name, ' ',
84
+                                                      _("(Quota exceeded)"))
85
+            else:
86
+                self.verbose_name = _("Launch Inception Instance")
87
+                classes = [c for c in self.classes if c != "disabled"]
88
+                self.classes = classes
89
+        except:
90
+            LOG.exception("Failed to retrieve quota information")
91
+            # If we can't get the quota information, leave it to the
92
+            # API to check when launching
93
+
94
+        return True  # The action should always be displayed
95
+
96
+
97
+class EditInstance(tables.LinkAction):
98
+    name = "edit"
99
+    verbose_name = _("Edit Inception Instance")
100
+    url = "horizon:project:inception:update"
101
+    classes = ("ajax-modal", "btn-edit")
102
+
103
+    def get_link_url(self, project):
104
+        return self._get_link_url(project, 'instance_info')
105
+
106
+    def _get_link_url(self, project, step_slug):
107
+        base_url = urlresolvers.reverse(self.url, args=[project.id])
108
+        param = urlencode({"step": step_slug})
109
+        return "?".join([base_url, param])
110
+
111
+    def allowed(self, request, instance):
112
+        return not is_deleting(instance)
113
+
114
+
115
+class LogLink(tables.LinkAction):
116
+    name = "log"
117
+    verbose_name = _("View Orchestrator Log")
118
+    url = "horizon:project:inception:instances:detail"
119
+    classes = ("btn-log",)
120
+
121
+    def allowed(self, request, instance=None):
122
+        return instance.status in ACTIVE_STATES and not is_deleting(instance)
123
+
124
+    def get_link_url(self, datum):
125
+        base_url = super(LogLink, self).get_link_url(datum)
126
+        tab_query_string = LogTab(
127
+            InceptionInstanceDetailTabs).get_query_string()
128
+        return "?".join([base_url, tab_query_string])
129
+
130
+
131
+class TerminateInstance(tables.BatchAction):
132
+    name = "terminate"
133
+    action_present = _("Terminate Inception")
134
+    action_past = _("Scheduled termination of")
135
+    data_type_singular = _("Instance")
136
+    data_type_plural = _("Instances")
137
+    classes = ('btn-danger', 'btn-terminate')
138
+
139
+    def allowed(self, request, instance=None):
140
+        return True
141
+
142
+    def action(self, request, obj_id):
143
+        iapi.cloud_delete(request, obj_id)
144
+
145
+
146
+class InstancesFilterAction(tables.FilterAction):
147
+
148
+    def filter(self, table, instances, filter_string):
149
+        """ Naive case-insensitive search. """
150
+        q = filter_string.lower()
151
+        return [instance for instance in instances
152
+                if q in instance.name.lower()]
153
+
154
+
155
+def get_power_state(instance):
156
+    return getattr(instance, "power_state", 'unknown')
157
+
158
+
159
+STATUS_DISPLAY_CHOICES = (
160
+    ("resize", "Resize/Migrate"),
161
+    ("verify_resize", "Confirm or Revert Resize/Migrate"),
162
+    ("revert_resize", "Revert Resize/Migrate"),
163
+)
164
+
165
+
166
+TASK_DISPLAY_CHOICES = (
167
+    ("image_snapshot", "Snapshotting"),
168
+    ("resize_prep", "Preparing Resize or Migrate"),
169
+    ("resize_migrating", "Resizing or Migrating"),
170
+    ("resize_migrated", "Resized or Migrated"),
171
+    ("resize_finish", "Finishing Resize or Migrate"),
172
+    ("resize_confirming", "Confirming Resize or Nigrate"),
173
+    ("resize_reverting", "Reverting Resize or Migrate"),
174
+    ("unpausing", "Resuming"),
175
+)
176
+
177
+
178
+class UpdateRow(tables.Row):
179
+    ajax = True
180
+
181
+    def get_data(self, request, instance_id):
182
+        cloud = iapi.cloud_get(request, instance_id)
183
+        return cloud
184
+
185
+
186
+class InceptionInstancesTable(tables.DataTable):
187
+    """ See: openstack_dashboard/dashboards/project/instances/tables.py"""
188
+
189
+    TASK_STATUS_CHOICES = (
190
+        (None, True),
191
+        ("none", True)
192
+    )
193
+
194
+    STATUS_CHOICES = (
195
+        ("active", True),
196
+        ("shutoff", True),
197
+        ("suspended", True),
198
+        ("paused", True),
199
+        ("error", False),
200
+    )
201
+
202
+    prefix = tables.Column("prefix",
203
+                           link=("horizon:project:inception:detail"),
204
+                           verbose_name=_("Prefix"))
205
+    n_workers = tables.Column("num_workers", verbose_name=_("No. of Workers"))
206
+
207
+    status = tables.Column("status",
208
+                           filters=(title, replace_underscores),
209
+                           verbose_name=_("Status"),
210
+                           status=True,
211
+                           status_choices=STATUS_CHOICES,
212
+                           display_choices=STATUS_DISPLAY_CHOICES)
213
+    task = tables.Column("task_state",
214
+                         verbose_name=_("Task"),
215
+                         filters=(title, replace_underscores),
216
+                         status=True,
217
+                         status_choices=TASK_STATUS_CHOICES,
218
+                         display_choices=TASK_DISPLAY_CHOICES)
219
+    state = tables.Column(get_power_state,
220
+                          filters=(title, replace_underscores),
221
+                          verbose_name=_("Power State"))
222
+
223
+    class Meta:
224
+        name = "inception_instances"
225
+        verbose_name = _("Inception Instances")
226
+        multi_select = True
227
+        #table_actions = (myact1, myact2,)
228
+        #row_actions = (myact1, myact2,)
229
+        row_class = UpdateRow
230
+        table_actions = (LaunchLink, TerminateInstance, InstancesFilterAction)
231
+        row_actions = (EditInstance, LogLink, TerminateInstance)

+ 59
- 0
inception/webui/tabs.py View File

@@ -0,0 +1,59 @@
1
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from django.conf import settings
16
+from django.utils.translation import ugettext_lazy as _
17
+
18
+from horizon import exceptions
19
+from horizon import tabs
20
+
21
+from inception.webui.api import inception as iapi
22
+from openstack_dashboard import api
23
+
24
+
25
+class OverviewTab(tabs.Tab):
26
+    name = _("Overview")
27
+    slug = "inception_overview"
28
+    template_name = ("project/instances/"
29
+                     "_detail_overview.html")
30
+
31
+    def get_context_data(self, request):
32
+        return {"instance": self.tab_group.kwargs['instance']}
33
+
34
+
35
+class LogTab(tabs.Tab):
36
+    name = _("Log")
37
+    slug = "inception_log"
38
+    template_name = "project/instances/_detail_log.html"
39
+    preload = False
40
+
41
+    def get_context_data(self, request):
42
+        instance = self.tab_group.kwargs['instance']
43
+        try:
44
+            data = 'log goes here'
45
+            # TODO(forrest-r): augment if log storage/retrieval supported
46
+            # iapi.server_console_output(request,
47
+            #                            instance.id,
48
+            #                            tail_length=35)
49
+        except:
50
+            data = _('Unable to get log for instance "%s".') % instance.id
51
+            exceptions.handle(request, ignore=True)
52
+        return {"instance": instance,
53
+                "console_log": data}
54
+
55
+
56
+class InceptionInstanceDetailTabs(tabs.TabGroup):
57
+    slug = "inception_instance_details"
58
+    tabs = (OverviewTab, LogTab)
59
+    sticky = True

+ 22
- 0
inception/webui/templates/inception/_detail_console.html View File

@@ -0,0 +1,22 @@
1
+{% load i18n %}
2
+{% load url from future %}
3
+
4
+<h3>{% trans "Instance Console" %}</h3>
5
+{% if console_url %}
6
+<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>
7
+<iframe id="console_embed" src="{{ console_url }}" style="width:100%;height:100%"></iframe>
8
+<script type="text/javascript">
9
+    var fix_height = function() {
10
+        $('iframe#console_embed').css({ height: $(document).height() + 'px' });
11
+    };
12
+    // there are two code paths to this particular block; handle them both
13
+    if (typeof($) != 'undefined') {
14
+        $(document).ready(fix_height);
15
+    } else {
16
+        addHorizonLoadEvent(fix_height);
17
+    }
18
+</script>
19
+{% else %}
20
+<p class='alert alert-error'>{% blocktrans %}console is currently unavailable. Please try again later.{% endblocktrans %}
21
+<a class='btn btn-mini' href="{% url 'horizon:project:instances:detail' instance_id %}">{% trans "Reload" %}</a></p>
22
+{% endif %}

+ 18
- 0
inception/webui/templates/inception/_detail_log.html View File

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

+ 107
- 0
inception/webui/templates/inception/_detail_overview.html View File

@@ -0,0 +1,107 @@
1
+{% load i18n sizeformat %}
2
+{% load url from future %}
3
+
4
+<h3>{% trans "Instance Overview" %}</h3>
5
+
6
+<div class="status row-fluid detail">
7
+  <h4>{% trans "Info" %}</h4>
8
+  <hr class="header_rule">
9
+  <dl>
10
+    <dt>{% trans "Name" %}</dt>
11
+    <dd>{{ instance.name }}</dd>
12
+    <dt>{% trans "ID" %}</dt>
13
+    <dd>{{ instance.id }}</dd>
14
+    <dt>{% trans "Status" %}</dt>
15
+    <dd>{{ instance.status|title }}</dd>
16
+  </dl>
17
+</div>
18
+
19
+<div class="specs row-fluid detail">
20
+  <h4>{% trans "Specs" %}</h4>
21
+  <hr class="header_rule">
22
+  <dl>
23
+    <dt>{% trans "Flavor" %}</dt>
24
+    <dd>{{ instance.full_flavor.name }}</dd>
25
+    <dt>{% trans "RAM" %}</dt>
26
+    <dd>{{ instance.full_flavor.ram|mbformat }}</dd>
27
+    <dt>{% trans "VCPUs" %}</dt>
28
+    <dd>{{ instance.full_flavor.vcpus }} {% trans "VCPU" %}</dd>
29
+    <dt>{% trans "Disk" %}</dt>
30
+    <dd>{{ instance.full_flavor.disk }}{% trans "GB" %}</dd>
31
+    {% if instance.full_flavor.ephemeral %}
32
+    <dt>{% trans "Ephemeral Disk" %}</dt>
33
+    <dd>{{ instance.full_flavor.ephemeral }}{% trans "GB" %}</dd>
34
+    {% endif %}
35
+  </dl>
36
+</div>
37
+
38
+<div class="addresses row-fluid detail">
39
+  <h4>{% trans "IP Addresses" %}</h4>
40
+  <hr class="header_rule">
41
+  <dl>
42
+    {% for network, ip_list in instance.addresses.items %}
43
+    <dt>{{ network|title }}</dt>
44
+    <dd>
45
+      {% for ip in ip_list %}
46
+        {% if not forloop.last %}{{ ip.addr}},&nbsp;{% else %}{{ip.addr}}{% endif %}
47
+      {% endfor %}
48
+    </dd>
49
+    {% endfor %}
50
+  </dl>
51
+</div>
52
+
53
+<div class="security_groups row-fluid detail">
54
+  <h4>{% trans "Security Groups" %}</h4>
55
+  <hr class="header_rule">
56
+  <dl>
57
+  {% for group in instance.security_groups %}
58
+    <dt>{{ group.name }}</dt>
59
+    <dd>
60
+      <ul>
61
+      {% for rule in group.rules %}
62
+        <li>{{ rule }}</li>
63
+        {% empty %}
64
+        <li><em>{% trans "No rules defined." %}</em></li>
65
+      {% endfor %}
66
+      </ul>
67
+    </dd>
68
+  {% endfor %}
69
+  </dl>
70
+</div>
71
+
72
+<div class="meta row-fluid detail">
73
+  <h4>{% trans "Meta" %}</h4>
74
+  <hr class="header_rule">
75
+  <dl>
76
+    <dt>{% trans "Key Name" %}</dt>
77
+    {% with default_key_name="<em>"|add:_("None")|add:"</em>" %}
78
+    <dd>{{ instance.key_name|default:default_key_name }}</dd>
79
+    {% endwith %}
80
+    {% url 'horizon:project:images_and_snapshots:images:detail' instance.image.id as image_url %}
81
+    <dt>{% trans "Image Name" %}</dt>
82
+    <dd><a href="{{ image_url }}">{{ instance.image_name }}</a></dd>
83
+    {% with default_item_value="<em>"|add:_("N/A")|add:"</em>" %}
84
+    {% for key, value in instance.metadata.items %}
85
+    <dt>{{ key|force_escape }}</dt>
86
+    <dd>{{ value|force_escape|default:default_item_value }}</dd>
87
+    {% endfor%}
88
+    {% endwith %}
89
+  </dl>
90
+</div>
91
+
92
+<div class="volumes row-fluid detail">
93
+  <h4>{% trans "Volumes Attached" %}</h4>
94
+  <hr class="header_rule">
95
+    <dl>
96
+      {% for volume in instance.volumes %}
97
+        <dt>{% trans "Attached To" %}</dt>
98
+        <dd>
99
+          <a href="{% url 'horizon:project:volumes:detail' volume.volumeId %}">{{ volume.name }}</a><span> {% trans "on" %} {{ volume.device }}</span>
100
+        </dd>
101
+        {% empty %}
102
+          <dt>{% trans "Volume" %}</dt>
103
+          <dd><em>{% trans "No volumes attached." %}</em></dd>
104
+      {% endfor %}
105
+    </dl>
106
+  </dl>
107
+</div>

+ 72
- 0
inception/webui/templates/inception/_flavors_and_quotas.html View File

@@ -0,0 +1,72 @@
1
+{% load i18n horizon humanize %}
2
+
3
+{% block help_message %}
4
+{% endblock %}
5
+
6
+<h4>{% trans "Flavor Details" %}</h4>
7
+<table class="flavor_table table-striped">
8
+  <tbody>
9
+    <tr>
10
+      <th>&nbsp;</th><th>{% trans "Instance" %}</th><th>{% trans "Gateway" %}</th>
11
+    </tr>
12
+    <tr>
13
+      <td class="flavor_name">{% trans "Name" %}</td>
14
+      <td><span id="flavor_name"></span></td>
15
+      <td><span id="gateway_flavor_name"></span></td>
16
+    </tr>
17
+    <tr>
18
+      <td class="flavor_name">{% trans "VCPUs" %}</td>
19
+      <td><span id="flavor_vcpus"></span></td>
20
+      <td><span id="gateway_flavor_vcpus"></span></td></tr>
21
+    <tr>
22
+      <td class="flavor_name">{% trans "Root Disk" %}</td>
23
+      <td><span id="flavor_disk"> </span> {% trans "GB" %}</td>
24
+      <td><span id="gateway_flavor_disk"> </span> {% trans "GB" %} </td></tr>
25
+    <tr>
26
+      <td class="flavor_name">{% trans "Ephemeral Disk" %}</td>
27
+      <td><span id="flavor_ephemeral"></span> {% trans "GB" %}</td>
28
+      <td><span id="gateway_flavor_ephemeral"></span> {% trans "GB" %} </td></tr>
29
+    <tr>
30
+      <td class="flavor_name">{% trans "Total Disk" %}</td>
31
+      <td><span id="flavor_disk_total"></span> {% trans "GB" %}</td>
32
+      <td><span id="gateway_flavor_disk_total"></span> {% trans "GB" %} </td></tr>
33
+    <tr>
34
+      <td class="flavor_name">{% trans "RAM" %}</td>
35
+      <td><span id="flavor_ram"></span> {% trans "MB" %}</td>
36
+      <td><span id="gateway_flavor_ram"></span> {% trans "MB" %} </td></tr>
37
+  </tbody>
38
+</table>
39
+
40
+<div class="quota-dynamic">
41
+  <h4>{% trans "Project Limits" %}</h4>
42
+  <div class="quota_title clearfix">
43
+    <strong>{% trans "Number of Instances" %}</strong>
44
+    {% blocktrans with used=usages.totalInstancesUsed|intcomma quota=usages.maxTotalInstances|intcomma %}<p>{{ used }} of {{ quota }} Used</p>{% endblocktrans %}
45
+  </div>
46
+  <div id="quota_instances" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.maxTotalInstances }}" data-quota-used="{{ usages.totalInstancesUsed }}">
47
+  </div>
48
+
49
+  <div class="quota_title clearfix">
50
+    <strong>{% trans "Number of VCPUs" %}</strong>
51
+    {% blocktrans with used=usages.totalCoresUsed|intcomma quota=usages.maxTotalCores|intcomma %}<p>{{ used }} of {{ quota }} Used</p>{% endblocktrans %}
52
+  </div>
53
+  <div id="quota_vcpus" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.maxTotalCores }}" data-quota-used="{{ usages.totalCoresUsed }}">
54
+  </div>
55
+
56
+  <div class="quota_title clearfix">
57
+    <strong>{% trans "Total RAM" %}</strong>
58
+    {% blocktrans with used=usages.totalRAMUsed|intcomma quota=usages.maxTotalRAMSize|intcomma %}<p>{{ used }} of {{ quota }} MB Used</p>{% endblocktrans %}
59
+  </div>
60
+  <div id="quota_ram" data-progress-indicator-flavor data-quota-limit="{{ usages.maxTotalRAMSize }}" data-quota-used="{{ usages.totalRAMUsed }}" class="quota_bar">
61
+  </div>
62
+</div>
63
+
64
+<script type="text/javascript" charset="utf-8">
65
+  if(typeof horizon.Quota !== 'undefined') {
66
+    horizon.Quota.initWithFlavors({{ flavors|safe|default:"{}" }});
67
+  } else {
68
+    addHorizonLoadEvent(function() {
69
+      horizon.Quota.initWithFlavors({{ flavors|safe|default:"{}" }});
70
+    });
71
+  }
72
+</script>

+ 10
- 0
inception/webui/templates/inception/_instance_ips.html View File

@@ -0,0 +1,10 @@
1
+{% for ip_group, addresses in instance.addresses.items %}
2
+    {% if instance.addresses.items|length > 1 %}
3
+    <h4>{{ ip_group }}</h4>
4
+    {% endif %}
5
+    <ul>
6
+    {% for address in addresses %}
7
+      <li>{{ address.addr }}</li>
8
+    {% endfor %}
9
+    </ul>
10
+{% endfor %}

+ 3
- 0
inception/webui/templates/inception/_launch_customize_help.html View File

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

+ 7
- 0
inception/webui/templates/inception/_launch_details_help.html View File

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

+ 3
- 0
inception/webui/templates/inception/_launch_network_help.html View File

@@ -0,0 +1,3 @@
1
+{% load i18n horizon %}
2
+
3
+<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>

+ 3
- 0
inception/webui/templates/inception/_launch_volumes_help.html View File

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

+ 46
- 0
inception/webui/templates/inception/_update_networks.html View File

@@ -0,0 +1,46 @@
1
+{% load i18n %}
2
+
3
+<noscript><h3>{{ step }}</h3></noscript>
4
+<table class="table-fixed" id="networkListSortContainer">
5
+  <tbody>
6
+    <tr>
7
+      <td class="actions">
8
+        <h4 id="selected_network_h4">{% trans "Selected Networks" %}</h4>
9
+        <ul id="selected_network" class="networklist">
10
+        </ul>
11
+        <h4>{% trans "Available networks" %}</h4>
12
+        <ul id="available_network" class="networklist">
13
+        </ul>
14
+      </td>
15
+      <td class="help_text">
16
+          {% include "project/instances/_launch_network_help.html" %}
17
+      </td>
18
+    </tr>
19
+  </tbody>
20
+</table>
21
+
22
+<table class="table-fixed" id="networkListIdContainer">
23
+  <tbody>
24
+    <tr>
25
+      <td class="actions">
26
+          <div id="networkListId">
27
+            {% include "horizon/common/_form_fields.html" %}
28
+          </div>
29
+      </td>
30
+      <td class="help_text">
31
+          {{ step.get_help_text }}
32
+      </td>
33
+    </tr>
34
+  </tbody>
35
+</table>
36
+
37
+
38
+<script>
39
+  if (typeof $ !== 'undefined') {
40
+    horizon.instances.workflow_init($(".workflow"));
41
+  } else {
42
+    addHorizonLoadEvent(function() {
43
+      horizon.instances.workflow_init($(".workflow"));
44
+    });
45
+  }
46
+</script>

+ 15
- 0
inception/webui/templates/inception/detail.html View File

@@ -0,0 +1,15 @@
1
+{% extends 'base.html' %}
2
+{% load i18n sizeformat %}
3
+{% block title %}{% trans "Inception Instance Detail" %}{% endblock %}
4
+
5
+{% block page_header %}
6
+  {% include "horizon/common/_page_header.html" with title="Inception Instance Detail: "|add:instance.name %}
7
+{% endblock page_header %}
8
+
9
+{% block main %}
10
+<div class="row-fluid">
11
+  <div class="span12">
12
+  {{ tab_group.render }}
13
+  </div>
14
+</div>
15
+{% endblock %}

+ 11
- 0
inception/webui/templates/inception/index.html View File

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

+ 34
- 0
inception/webui/urls.py View File

@@ -0,0 +1,34 @@
1
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from django.conf.urls.defaults import patterns
16
+from django.conf.urls.defaults import url
17
+
18
+from inception.webui.views import DetailView
19
+from inception.webui.views import UpdateView
20
+from inception.webui.views import IndexView
21
+from inception.webui.views import LaunchInceptionInstanceView
22
+
23
+
24
+INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
25
+VIEW_MOD = 'horizon.inception.views'
26
+
27
+urlpatterns = patterns(
28
+    VIEW_MOD,
29
+    url(r'^$', IndexView.as_view(), name='index'),
30
+    url(r'^launch$', LaunchInceptionInstanceView.as_view(), name='launch'),
31
+    url(r'^(?P<instance_id>[^/]+)/$', DetailView.as_view(), name='detail'),
32
+    url(r'^(?P<instance_id>[^/]+)/update$', UpdateView.as_view(),
33
+        name='update'),
34
+)

+ 132
- 0
inception/webui/views.py View File

@@ -0,0 +1,132 @@
1
+#    Copyright (C) 2013 AT&T Labs Inc. All Rights Reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import logging
16
+
17
+from django.core.urlresolvers import reverse
18
+from django.core.urlresolvers import reverse_lazy
19
+from django import http
20
+from django import shortcuts
21
+from django.utils.datastructures import SortedDict
22
+from django.utils.translation import ugettext_lazy as _
23
+
24
+from horizon import exceptions
25
+from horizon import tables
26
+from horizon import tabs
27
+from horizon import workflows
28
+
29
+from openstack_dashboard import api
30
+import inception.webui.api.inception as iapi
31
+from inception.webui.tables import InceptionInstancesTable
32
+from inception.webui.tabs import InceptionInstanceDetailTabs
33
+from inception.webui.workflows.create_inception_instance import \
34
+    LaunchInceptionInstance
35
+from inception.webui.workflows.create_inception_instance import \
36
+    UpdateInceptionInstance
37
+
38
+LOG = logging.getLogger(__name__)
39
+
40
+
41
+class IndexView(tables.DataTableView):
42
+    table_class = InceptionInstancesTable
43
+    template_name = 'inception/index.html'
44
+
45
+    def has_more_data(self, table):
46
+        LOG.debug("has_more_data called")
47
+        return self._more
48
+
49
+    def get_data(self):
50
+        LOG.debug("get_data called")
51
+        marker = self.request.GET.get(
52
+            InceptionInstancesTable._meta.pagination_param, None)
53
+        # Gather our instances
54
+        try:
55
+            instances, self._more = iapi.cloud_list(
56
+                self.request,
57
+                search_opts={'marker': marker,
58
+                             'paginate': True})
59
+        except:
60
+            self._more = False
61
+            instances = []
62
+            exceptions.handle(self.request,
63
+                              _('Unable to retrieve instances.'))
64
+        return instances
65
+
66
+
67
+class LaunchInceptionInstanceView(workflows.WorkflowView):
68
+    workflow_class = LaunchInceptionInstance
69
+
70
+    def get_initial(self):
71
+        LOG.debug("LaunchInceptionInstanceView: get_initial()")
72
+        initial = super(LaunchInceptionInstanceView, self).get_initial()
73
+        initial['project_id'] = self.request.user.tenant_id
74
+        initial['user_id'] = self.request.user.id
75
+        return initial
76
+
77
+
78
+class UpdateView(workflows.WorkflowView):
79
+    workflow_class = UpdateInceptionInstance
80
+    success_url = reverse_lazy("horizon:project:instances:index")
81
+
82
+    def get_context_data(self, **kwargs):
83
+        context = super(UpdateView, self).get_context_data(**kwargs)
84
+        context["instance_id"] = self.kwargs['instance_id']
85
+        return context
86
+
87
+    def get_object(self, *args, **kwargs):
88
+        if not hasattr(self, "_object"):
89
+            instance_id = self.kwargs['instance_id']
90
+            try:
91
+                self._object = api.nova.server_get(self.request, instance_id)
92
+            except:
93
+                redirect = reverse("horizon:project:instances:index")
94
+                msg = _('Unable to retrieve instance details.')
95
+                exceptions.handle(self.request, msg, redirect=redirect)
96
+        return self._object
97
+
98
+    def get_initial(self):
99
+        initial = super(UpdateView, self).get_initial()
100
+        initial.update({'instance_id': self.kwargs['instance_id'],
101
+                        'name': getattr(self.get_object(), 'name', '')})
102
+        return initial
103
+
104
+
105
+class DetailView(tabs.TabView):
106
+    tab_group_class = InceptionInstanceDetailTabs
107
+    template_name = 'project/instances/detail.html'
108
+
109
+    def get_context_data(self, **kwargs):
110
+        context = super(DetailView, self).get_context_data(**kwargs)
111
+        context["instance"] = self.get_data()
112
+        return context
113
+
114
+    def get_data(self):
115
+        if not hasattr(self, "_instance"):
116
+            try:
117
+                instance_id = self.kwargs['instance_id']
118
+                instance = iapi.cloud_get(self.request, instance_id)
119
+                instance.security_groups = api.network.server_security_groups(
120
+                    self.request, instance_id)
121
+            except:
122
+                redirect = reverse('horizon:project:instances:index')
123
+                exceptions.handle(self.request,
124
+                                  _('Unable to retrieve details for '
125
+                                    'instance "%s".') % instance_id,
126
+                                  redirect=redirect)
127
+            self._instance = instance
128
+        return self._instance
129
+
130
+    def get_tabs(self, request, *args, **kwargs):
131
+        instance = self.get_data()
132
+        return self.tab_group_class(request, instance=instance, **kwargs)

+ 14
- 1
setup.py View File

@@ -27,6 +27,19 @@ setup(
27 27
                          'bin/pre_switch_kernel.sh',
28 28
                          'bin/setup_chef_repo.sh',
29 29
                          'bin/userdata.sh.template',
30
-                         ])],
30
+                         ]),
31
+                ('inception/webui/templates/inception',
32
+                 ['inception/webui/templates/inception/detail.html',
33
+                 'inception/webui/templates/inception/_detail_log.html',
34
+                 'inception/webui/templates/inception/_detail_overview.html',
35
+                 'inception/webui/templates/inception/_flavors_and_quotas.html',
36
+                 'inception/webui/templates/inception/index.html',
37
+                 'inception/webui/templates/inception/_launch_customize_help.html',
38
+                 'inception/webui/templates/inception/_launch_details_help.html',
39
+                 'inception/webui/templates/inception/_launch_network_help.html',
40
+                 'inception/webui/templates/inception/_launch_volumes_help.html',
41
+                 'inception/webui/templates/inception/_update_networks.html',
42
+                 ]),
43
+               ],
31 44
     scripts=['bin/orchestrator'],
32 45
 )

+ 0
- 0
webui/__init__.py View File


Loading…
Cancel
Save